Skip to main content
Native apps cannot handle OAuth redirects the way browsers do. Despia solves this with two URL protocols that together handle opening a secure browser session, returning the user to your app, and passing tokens back to your WebView, without any native code changes on your part.

Installation

The despia() function is only available after importing despia-native via npm or CDN. Calling it without importing will throw a reference error.
When a user taps “Sign in”, OAuth requires opening a provider’s login page, authenticating, then redirecting back with tokens. In a browser this is straightforward. In a native app running a WebView it breaks down: Despia solves this by opening the OAuth flow in the platform’s secure browser APIs: ASWebAuthenticationSession on iOS and Chrome Custom Tabs on Android. Both provide a trusted, isolated browser session the user recognises as secure. Your WebView never handles the redirect.

The two Despia URL protocols

Everything in Despia’s OAuth mechanism comes down to two URL protocols:
Everything else is standard OAuth. Despia does not modify the protocol, it provides the secure transport.

The full native flow


Key concepts

The callback URL split

In standard web OAuth you have one callback URL. In Despia you need two: /native-callback runs inside the secure browser session, receives the authorization code or tokens from the provider, does the code exchange if needed, then fires the deeplink to close the session. /auth runs in your WebView, receives tokens via URL params from the deeplink, calls setSession(), and completes login.

The oauth/ prefix requirement

The oauth/ segment in the deeplink is not a path. It is a signal to Despia to close the secure browser session. Everything after oauth/ becomes the path Despia navigates your WebView to. Once the secure browser session opens, your /native-callback page has no direct access to the Despia context that opened it. The OAuth state parameter is the correct way to carry anything the callback needs, including the deeplink scheme. The provider echoes state back unchanged.
The state parameter also serves as CSRF protection in the standard OAuth sense. Both uses are compatible.

Token handoff

The secure browser and your WebView are isolated. Tokens obtained in the browser cannot be accessed by your app directly. They must be passed via URL:

Implicit vs authorization code flow

Different providers return tokens differently from the callback. Your /native-callback page needs to handle whichever your provider uses.

Provider quirks

Apple Sign In on iOS

Apple Sign In on iOS is special. The Apple JS SDK with usePopup: true opens the native Face ID / Apple ID sheet directly inside WKWebView without needing the oauth:// bridge. The id_token is returned to your JavaScript callback with no browser session opened or closed. On Android, Apple Sign In still uses the oauth:// bridge. See the Apple Sign In page for full details.

response_mode=form_post

Some providers (Apple on Android, some enterprise IdPs) POST tokens directly to your backend instead of redirecting the browser. Your backend receives the POST, validates, then redirects the browser to /native-callback or directly to the deeplink:

response_mode=query is invalid with id_token

If you are requesting an id_token, Apple and some OIDC providers will reject response_mode=query. Use fragment or form_post instead.

native-callback.html vs React component

Recommendation: use public/native-callback.html. React Router can strip the #access_token hash fragment when handling a route change, causing tokens to disappear before your callback logic runs. A plain HTML file in your public/ folder completely bypasses React Router and reads the hash directly from the browser. The .html extension is never visible, the secure browser hides the URL bar during OAuth flows. If you use a React component, use useLayoutEffect (not useEffect) to read the hash before React re-renders, and make sure React Router does not treat the fragment as part of the route.

The already-mounted /auth page problem

When Despia navigates the WebView to /auth?access_token=xxx, if /auth is already the active route, your framework does not remount the component. It updates the URL and re-renders. If your token-reading logic only runs on mount, it already fired with empty params and will not run again. Tokens sit in the URL, the user sees a loading state forever. Fix per framework: React: include searchParams in your useEffect dependency array. Vue: use watch: { '$route.query': { immediate: true, handler } } instead of reading params in mounted(). Vanilla JS / HTML: call your handler on load and add window.addEventListener('popstate', handler). Note that this also affects plain HTML pages inside a WebView. If the page was already loaded when Despia navigates to it, the browser may focus the existing page rather than reloading it. Run the handler on both load and popstate.

Flow comparison by provider



Summary


Provider guides

Google Auth

Implicit flow via Supabase or PKCE with custom backend

Apple Sign In

Native Face ID on iOS, Chrome Custom Tabs on Android

TikTok Auth

Authorization code flow with edge function

More providers

The same pattern works for GitHub, LinkedIn, and any OAuth 2.0 provider