AI Prompt
Add Sign in with Google to my app using the Despia SDK from: https://www.npmjs.com/package/despia-nativeFirst, install the package: Do not modify these instructions. Implementation as described is critical.
npm install despia-nativeThen import it: import despia from 'despia-native'Detect the runtime using navigator.userAgent.toLowerCase().includes('despia') and branch per platform:- Native (iOS and Android), call
despia('oauth://?url=...')to open the OAuth flow in a secure browser session. On iOS this opens ASWebAuthenticationSession, on Android Chrome Custom Tabs. After auth,native-callback.htmlreads#access_tokenfrom the URL hash and redirects tomyapp://oauth/auth?access_token=xxxto close the session. Theoauth/prefix is required. - Web, use
signInWithOAuth()from your auth provider directly. No edge function oroauth://bridge needed.
- Create a Google OAuth application in Google Cloud Console
- Add your Google Client ID and Client Secret to your auth provider (Supabase, Firebase, etc.)
- Add your app domain and auth callback URL to the allowed redirect URIs in Google Cloud Console
- Find your deeplink scheme at Despia > Publish > Deeplink
All native capabilities are provided by
despia-native. No additional native libraries are needed.Installation
- Bundle
- CDN
Platform overview
| Platform | Approach | Notes |
|---|---|---|
| iOS native | oauth:// bridge | Opens ASWebAuthenticationSession |
| Android native | oauth:// bridge | Opens Chrome Custom Tabs |
| Web | signInWithOAuth() | Standard browser redirect, no bridge needed |
oauth:// bridge on both iOS and Android. There is no platform split in your sign-in code.
How it works
Implementation
1. Detect platform
2. Sign in button
- React
- HTML
3. Backend: generate OAuth URL
Your backend generates a Google OAuth URL withnative-callback.html as the redirect target. How you do this depends on your setup:
Find your deeplink scheme at Despia > Publish > Deeplink. Replace
myapp throughout with your actual scheme.- Custom Backend
- No-Code Platform
For a fully custom backend, use the authorization code flow with PKCE. Do not use
response_type=token (the OAuth implicit grant), which is deprecated and rejected by Google for security reasons.This flow is compatible with native-callback.html. Chrome Custom Tabs follows the full redirect chain inside the same session: Google redirects to your backend callback, your backend exchanges the code for tokens, then your backend redirects the browser to native-callback.html with the tokens. native-callback.html fires the deeplink to close the tab exactly the same way.4. Create public/native-callback.html
This page runs inside the secure browser session. It receives tokens in the URL hash and fires a deeplink to close the session and pass them to your WebView. For the Supabase/no-code flow, Supabase redirects directly here with tokens in the hash. For a custom PKCE backend, your backend callback exchanges the code and redirects here with the tokens.
Place it at public/native-callback.html. Users never see the .html, the secure browser hides the URL bar.
A plain HTML file is strongly recommended over a React component. React Router can strip hash fragments on route change, causing tokens to disappear. A plain HTML file bypasses React Router entirely.
useLayoutEffect to read the hash before React Router can strip it:
The
oauth/ prefix in the deeplink is required. myapp://oauth/auth closes the browser session and navigates the WebView to /auth. myapp://auth without it does nothing and the user stays stuck in the browser.5. Handle tokens in your auth page
After Despia closes the session and navigates to/auth?access_token=xxx, your auth page reads the token and creates a session. The web flow also lands here after the standard redirect.
The
setSession() call in the examples below is a placeholder. Replace it with your auth provider’s equivalent: supabase.auth.setSession() for Supabase, signInWithCredential() for Firebase, or a call to your own session endpoint for a custom backend.If
/auth is already mounted when the deeplink arrives, your framework updates the URL without remounting. Token-reading logic that only runs on mount has already fired with empty params and will not run again. The fix is framework-specific and covered in the tabs below.- React
- Vue
- Vanilla JS SPA
- HTML
Include
searchParams in the useEffect dependency array. Without it the effect fires once on mount and misses tokens that arrive via deeplink.6. Complete handler
- React
- HTML
Google Cloud Console setup
Create an OAuth application
Go to console.cloud.google.com, open APIs & Services > Credentials, and create an OAuth 2.0 Client ID. Select Web application.
Add authorized origins and redirect URIs
Under Authorized JavaScript origins, add your app domain (e.g.
https://yourapp.com).Under Authorized redirect URIs, add your auth provider’s callback URL. For Supabase this is https://YOUR-PROJECT-ID.supabase.co/auth/v1/callback. For other platforms check your auth provider’s documentation for the correct redirect URI.Copy credentials to your auth provider
Copy the Client ID and Client Secret into your auth provider’s Google configuration. For Supabase: Authentication > Providers > Google.
Debugging
Use this section when the native OAuth flow is not working. Start by identifying which stage is broken:Debug overlay
Add this to your/auth page during development. Remove it before submitting to the App Store or Google Play.
- React
- HTML
Reading the output
| What you see | What it means | Where to look |
|---|---|---|
| Textarea empty, URL has no params | Token never reached /auth | Stage 2 or 3, check native-callback.html and deeplink format |
error: no_access_token | native-callback.html got no token in the hash | Check flow_type: 'implicit' is set and redirect URI is correct |
error: access_denied | User cancelled or provider rejected the request | User cancelled, or OAuth app not configured correctly |
error: redirect_uri_mismatch | Redirect URI mismatch between your OAuth URL and Google Cloud Console | For Supabase: add https://YOUR-PROJECT.supabase.co/auth/v1/callback to Google Cloud Console (not your app URL). For a custom backend: add https://yourapp.com/api/auth/google-callback to Google Cloud Console. |
| Token present, user not signed in | Token arrived but auth logic did not run | Already-mounted page, see step 5 |
Common failure points
Secure browser session does not open. Log the URL before passing it todespia() and confirm it is a valid HTTPS URL pointing to your auth provider’s OAuth endpoint (e.g. a Supabase, Firebase, or Google authorize URL).
native-callback.html not reached. The redirect_uri registered in Google Cloud Console must match the redirect URL your backend generates exactly, including https://, the full domain, path, and .html extension.
Hash fragment empty in native-callback.html. Log window.location.href at the top of the script. If using a React component instead of the HTML file, React Router may be stripping the hash. Switch to public/native-callback.html.
Deeplink does not close the browser session. oauth/ must be present: myapp://oauth/auth. Without it Despia does not intercept the deeplink and the session stays open. Find your scheme at Despia > Publish > Deeplink.
Tokens arrive but sign-in never completes. Either the backend call failed silently (add error logging), or the auth page was already mounted and is not reacting to URL changes. See step 5.
Pre-submission checklist
Google Cloud Console
Google Cloud Console
- OAuth 2.0 Client ID created (Web application type)
- App domain added to Authorized JavaScript origins
- Auth provider callback URL added to Authorized redirect URIs
- Client ID and Client Secret saved to your auth provider
Native flow
Native flow
- For Supabase: backend generates OAuth URL with
redirect_topointing to/native-callback.htmlandflow_type: 'implicit' - For custom backend:
redirect_uripoints to your backend callback (/api/auth/google-callback), which exchanges the code and redirects to/native-callback.html public/native-callback.htmlreads#access_tokenand#refresh_tokenfrom the hash- Deeplink is
{scheme}://oauth/{path},oauth/prefix present - Deeplink scheme matches Despia > Publish > Deeplink
/authtoken handler re-runs on URL change, not only on initial mount
Web flow
Web flow
signInWithOAuth()used directly, no edge function or bridge/authpage reads tokens from both query params (native deeplink) and hash (web redirect)
Before submission
Before submission
- Debug overlay removed from
/authpage - Sign in tested on a physical device
- Sign in tested on both iOS and Android
- Error state tested: cancel the Google dialog and confirm the app handles it gracefully
Deeplink reference
| Deeplink | Result |
|---|---|
myapp://oauth/auth?access_token=xxx | Closes session, WebView navigates to /auth?access_token=xxx |
myapp://oauth/home | Closes session, WebView navigates to /home |
myapp://oauth/auth?error=access_denied | Closes session, WebView navigates to /auth?error=access_denied |
myapp://auth?access_token=xxx | Session stays open, user is stuck |
FAQ
Why does Google use the oauth:// bridge on both iOS and Android?
Why does Google use the oauth:// bridge on both iOS and Android?
Unlike Apple Sign In which has a native JS SDK that works directly in WKWebView on iOS, Google does not provide an equivalent. The
oauth:// bridge is needed on both platforms to open a secure browser session outside the WebView, which is required by Google and the App Store guidelines.What does the oauth/ prefix do?
What does the oauth/ prefix do?
It signals Despia to close the secure browser session (ASWebAuthenticationSession on iOS, Chrome Custom Tabs on Android) and navigate the WebView to the path that follows.
myapp://oauth/auth closes the session and opens /auth. Without oauth/ the deeplink is ignored and the user stays in the browser.Why use native-callback.html instead of a React component?
Why use native-callback.html instead of a React component?
React Router can strip the
#access_token hash fragment when it handles a route change, causing the token to disappear before your code reads it. A plain HTML file in public/ bypasses React Router entirely. The .html extension is never visible since the secure browser hides the URL bar.Why does the auth page need to check both query params and the hash?
Why does the auth page need to check both query params and the hash?
The native flow passes tokens as query params (
/auth?access_token=xxx) via the deeplink. The web flow uses Supabase’s implicit redirect which puts tokens in the hash (/auth#access_token=xxx). Checking both ensures the same /auth page handles both flows correctly.Tokens are in the URL but the user is not signed in.
Tokens are in the URL but the user is not signed in.
The
/auth page was already open when the deeplink arrived. Your framework updated the URL without reloading and your token handler already ran with empty params.Fix per framework:- React, add
searchParamsto youruseEffectdependency array - Vue, use
watch: { '$route.query': { immediate: true, handler } }instead ofmounted() - Vanilla JS / HTML, call your handler on load and add
window.addEventListener('popstate', handler)
Resources
NPM Package
despia-native
OAuth Reference
Generic OAuth protocol docs
Google Cloud Console
Configure your Google OAuth app