The flow becomes "redirect the user to have them authenticate, getting back a code. Make an API call to trade that code in for access. Once access expires, try to refresh it. If refreshing fails, send the user back to re-authenticate.".
The challenge people tend to hit is mistakenly trying to implement broad + reusable code at the start. OAuth 2 is described as a framework (e.g. optional parts and extensions leading to most likely non-interoperable implementations). Without a profile like OpenID Connect Basic Client, this includes a lot of extra work. Once you stop striving to implement generic interoperability in your client (or shoot for a limited profile like OpenID Connect Basic Client), the whole client implementation can fit in < 1 page of code.