Use this file to discover all available pages before exploring further.
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.
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:
Problem
Why it matters
No visible address bar
Users cannot verify they are on the real provider’s site
WebView restrictions
Google and Apple block OAuth inside embedded WebViews for security
App Store requirements
Both platforms require OAuth to happen in a trusted browser session
Session isolation
Tokens obtained in the browser session cannot transfer directly to your WebView
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.
Everything in Despia’s OAuth mechanism comes down to two URL protocols:
Protocol
What it does
oauth://
Opens a secure browser session with the URL you provide
{scheme}://oauth/
Closes the secure browser session and navigates your WebView to the path that follows
// Open a native secure browser sessiondespia(`oauth://?url=${encodeURIComponent(oauthUrl)}`)// Close it and return to your app (fires from inside the browser session)window.location.href = `myapp://oauth/auth?access_token=xxx`
Everything else is standard OAuth. Despia does not modify the protocol, it provides the secure transport.
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/ 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.
Deeplink
Result
myapp://oauth/auth?token=xxx
Browser closes, WebView navigates to /auth?token=xxx
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.
// When generating the OAuth URLconst state = `${crypto.randomUUID()}|myapp` // uuid|deeplink_scheme// In native-callbackconst state = new URLSearchParams(window.location.search).get('state')const scheme = state.includes('|') ? state.split('|')[1] : 'myapp'window.location.href = scheme + '://oauth/auth?access_token=' + encodeURIComponent(token)
The state parameter also serves as CSRF protection in the standard OAuth sense. Both uses are compatible.
Different providers return tokens differently from the callback. Your /native-callback page needs to handle whichever your provider uses.
Flow
How tokens arrive
Examples
Implicit
URL hash #access_token=xxx
Supabase Google (legacy), some OIDC providers
Authorization code
Query param ?code=xxx, then backend exchange
TikTok, GitHub, most modern providers
// In native-callback, handle bothvar params = new URLSearchParams(window.location.search)var hash = new URLSearchParams(window.location.hash.substring(1))var code = params.get('code') // authorization code flowvar accessToken = hash.get('access_token') // implicit flowif (code) { // exchange code via backend, then fire deeplink with tokens} else if (accessToken) { // fire deeplink directly with token from hash}
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.
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:
Provider → POST to your backend → backend redirects to myapp://oauth/auth?tokens
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.
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.