Documentation Index
Fetch the complete documentation index at: https://setup.despia.com/llms.txt
Use this file to discover all available pages before exploring further.
The core concept
The login page decides the OAuth flow based on user agent.
When your login page loads, check if it’s running in Despia:
const userAgent = navigator.userAgent.toLowerCase();
const isDespia = userAgent.includes('despia');
if (isDespia) {
// Use Despia native flow
// redirect_uri: https://yourapp.com/native-callback
} else {
// Use standard web flow
// redirect_uri: https://yourapp.com/auth
}
Why this matters: The OAuth browser session (ASWebAuthenticationSession/Chrome Custom Tabs) has the browser’s user agent, not Despia’s. So you can’t check user agent in the callback page - you must decide the flow on the login page.
Two complete flows
Web flow (standard OAuth)
When: userAgent doesn’t include ‘despia’
Flow:
- Login page → redirects to OAuth provider
- OAuth provider → redirects back to
/auth
/auth page → sets session, navigates to /dashboard
Code - Login page:
// On login button click
async function handleLogin() {
const userAgent = navigator.userAgent.toLowerCase();
const isDespia = userAgent.includes('despia');
if (!isDespia) {
// Web flow: redirect directly
window.location.href = 'https://provider.com/oauth/authorize?' +
'client_id=xxx&' +
'redirect_uri=' + encodeURIComponent('https://yourapp.com/auth') + '&' +
'response_type=code';
}
}
Code - /auth page:
// Parse tokens from URL
const code = new URLSearchParams(window.location.search).get('code');
if (code) {
// Exchange for tokens
const response = await fetch('/api/token', {
method: 'POST',
body: JSON.stringify({ code })
});
const { access_token } = await response.json();
// Store session
localStorage.setItem('access_token', access_token);
// Navigate to app
window.location.href = '/dashboard';
}
Despia native flow
When: userAgent includes ‘despia’
Flow:
- Login page → calls
despia('oauth://...') to open native browser
- OAuth provider → redirects to
/native-callback (in native browser)
/native-callback → extracts tokens, redirects to deeplink with oauth/ prefix
- Native app → intercepts deeplink, closes browser, navigates to
/auth
/auth page → receives tokens from URL, sets session
Code - Login page:
import despia from 'despia-native';
async function handleLogin() {
const userAgent = navigator.userAgent.toLowerCase();
const isDespia = userAgent.includes('despia');
if (isDespia) {
// Despia flow: open in native browser
const oauthUrl = 'https://provider.com/oauth/authorize?' +
'client_id=xxx&' +
'redirect_uri=' + encodeURIComponent('https://yourapp.com/native-callback') + '&' +
'response_type=code';
// Opens ASWebAuthenticationSession (iOS) or Chrome Custom Tabs (Android)
despia(`oauth://?url=${encodeURIComponent(oauthUrl)}`);
}
}
Code - /native-callback page:
// This page runs inside the native browser session
// Extract tokens and close the browser
const code = new URLSearchParams(window.location.search).get('code');
if (code) {
// Exchange for tokens
const response = await fetch('/api/token', {
method: 'POST',
body: JSON.stringify({ code })
});
const { access_token, refresh_token } = await response.json();
// Redirect to deeplink to CLOSE the browser
// The oauth/ prefix tells Despia to close the browser session
window.location.href =
`yourappdeeplink://oauth/auth?` +
`access_token=${encodeURIComponent(access_token)}&` +
`refresh_token=${encodeURIComponent(refresh_token)}`;
}
Code - /auth page (receives deeplink):
// Parse tokens from URL (deeplink redirects here)
const searchParams = new URLSearchParams(window.location.search);
const access_token = searchParams.get('access_token');
const refresh_token = searchParams.get('refresh_token');
if (access_token) {
// Store session
localStorage.setItem('access_token', decodeURIComponent(access_token));
if (refresh_token) {
localStorage.setItem('refresh_token', decodeURIComponent(refresh_token));
}
// Navigate to app
window.location.href = '/dashboard';
}
Key differences
| Step | Web Flow | Despia Flow |
|---|
| Login page | window.location.href = oauthUrl | despia('oauth://?url=...') |
| OAuth redirect | → /auth | → /native-callback |
| Callback action | Set session, navigate | Redirect to deeplink |
| Deeplink | None | yourappdeeplink://oauth/auth?tokens |
| Browser closes | N/A | When deeplink called |
| Final landing | /auth (already there) | /auth (via deeplink) |
Apple Sign-In special handling
Apple Sign-In on iOS devices needs different handling:
async function handleAppleLogin() {
const userAgent = navigator.userAgent.toLowerCase();
const isIOSDespia = userAgent.includes('despia-iphone') ||
userAgent.includes('despia-ipad');
const isAndroidDespia = userAgent.includes('despia-android');
const appleOAuthUrl = 'https://appleid.apple.com/auth/authorize?...';
if (isIOSDespia) {
// iOS: Direct redirect (triggers native Apple dialog)
window.location.href = appleOAuthUrl;
} else if (isAndroidDespia) {
// Android: Use oauth:// for Chrome Custom Tabs
despia(`oauth://?url=${encodeURIComponent(appleOAuthUrl)}`);
} else {
// Web: Standard redirect
window.location.href = appleOAuthUrl;
}
}
Why: iOS has native Apple Sign-In built into WebKit. Direct redirect triggers the native dialog.
Troubleshooting
Browser session doesn’t open
Problem: User clicks login, nothing happens.
Check:
console.log('User agent:', navigator.userAgent);
console.log('Is Despia:', navigator.userAgent.includes('despia'));
console.log('OAuth URL:', oauthUrl);
Common issues:
- OAuth URL not properly encoded: Use
encodeURIComponent()
- Missing
yourappdeeplink:// prefix: despia('oauth://?url=...') not just despia(url)
- Testing in web browser instead of Despia app
Browser doesn’t close after login
Problem: User completes OAuth, stuck in browser.
Cause: Missing oauth/ prefix in deeplink.
Wrong:
// Browser won't close
window.location.href = `yourappdeeplink://auth?access_token=${token}`;
Right:
// Browser closes because of oauth/ prefix
window.location.href = `yourappdeeplink://oauth/auth?access_token=${token}`;
The oauth/ prefix in the deeplink is what tells Despia to close ASWebAuthenticationSession/Chrome Custom Tabs.
Tokens not reaching /auth page
Problem: Browser closes but user not logged in.
Check /auth page:
console.log('Full URL:', window.location.href);
console.log('Access token:', new URLSearchParams(window.location.search).get('access_token'));
Common issues:
- Tokens not encoded in callback: Use
encodeURIComponent(token) when building deeplink
- Reading from wrong place: Tokens are in query params (
?), not hash (#)
- Not decoding: Use
decodeURIComponent() when parsing
Redirect URI mismatch
Problem: OAuth provider shows “redirect_uri mismatch” error.
Cause: Callback URL not registered with OAuth provider.
Fix:
- For Despia flow: Register
https://yourapp.com/native-callback
- For web flow: Register
https://yourapp.com/auth
- URLs must match exactly (no trailing slash differences)
Hash tokens lost in SPA routing
Problem: Tokens disappear from URL hash.
Solution: Create static HTML callback page.
Create /native-callback.html:
<!DOCTYPE html>
<html>
<head>
<title>Completing sign in...</title>
</head>
<body>
<div style="padding: 2rem; text-align: center;">
Completing sign in...
</div>
<script>
(function() {
// Parse hash tokens
const hash = window.location.hash.substring(1);
const params = new URLSearchParams(hash);
const access_token = params.get('access_token');
const refresh_token = params.get('refresh_token');
if (access_token) {
// Redirect to deeplink to close browser
window.location.href =
`myapp://oauth/auth?` +
`access_token=${encodeURIComponent(access_token)}&` +
`refresh_token=${encodeURIComponent(refresh_token || '')}`;
}
})();
</script>
</body>
</html>
Update OAuth redirect:
redirect_uri: https://yourapp.com/native-callback.html
Why this works: Static HTML loads directly, no router involved, hash preserved.
Complete implementation
1. Login page (handles both flows):
import despia from 'despia-native';
async function handleLogin() {
const userAgent = navigator.userAgent.toLowerCase();
const isDespia = userAgent.includes('despia');
// Get OAuth URL from your backend
const response = await fetch('/api/oauth/start', {
method: 'POST',
body: JSON.stringify({
// Different redirect URIs for different flows
redirect_uri: isDespia
? 'https://yourapp.com/native-callback'
: 'https://yourapp.com/auth',
is_native: isDespia
}),
headers: { 'Content-Type': 'application/json' }
});
const { oauth_url } = await response.json();
if (isDespia) {
// Despia: Open native browser session
despia(`oauth://?url=${encodeURIComponent(oauth_url)}`);
} else {
// Web: Standard redirect
window.location.href = oauth_url;
}
}
2. /native-callback page (Despia only):
// Runs in native browser session
(function() {
// Get authorization code
const code = new URLSearchParams(window.location.search).get('code');
if (code) {
// Exchange for tokens
fetch('/api/oauth/exchange', {
method: 'POST',
body: JSON.stringify({ code }),
headers: { 'Content-Type': 'application/json' }
})
.then(r => r.json())
.then(({ access_token, refresh_token }) => {
// Close browser with deeplink
window.location.href =
`yourappdeeplink://oauth/auth?` +
`access_token=${encodeURIComponent(access_token)}&` +
`refresh_token=${encodeURIComponent(refresh_token)}`;
});
}
})();
3. /auth page (both flows):
// Runs on page load
(function() {
// Check for code (web flow)
const code = new URLSearchParams(window.location.search).get('code');
if (code) {
// Web flow: Exchange code for tokens
fetch('/api/oauth/exchange', {
method: 'POST',
body: JSON.stringify({ code }),
headers: { 'Content-Type': 'application/json' }
})
.then(r => r.json())
.then(({ access_token, refresh_token }) => {
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);
window.location.href = '/dashboard';
});
return;
}
// Check for tokens (Despia flow via deeplink)
const access_token = new URLSearchParams(window.location.search).get('access_token');
if (access_token) {
// Despia flow: Tokens already in URL
const refresh_token = new URLSearchParams(window.location.search).get('refresh_token');
localStorage.setItem('access_token', decodeURIComponent(access_token));
if (refresh_token) {
localStorage.setItem('refresh_token', decodeURIComponent(refresh_token));
}
window.location.href = '/dashboard';
}
})();
Debug checklist
When OAuth isn’t working:
On login page:
- Check user agent:
console.log(navigator.userAgent)
- Verify correct flow selected
- Verify OAuth URL is valid
- Verify OAuth URL is encoded
In /native-callback (Despia only):
- Check page loads:
console.log('Callback loaded')
- Check tokens received:
console.log('Token:', !!access_token)
- Check deeplink format:
myapp://oauth/auth?...
- Verify
oauth/ prefix present
In /auth page:
- Check URL:
console.log(window.location.href)
- Check for code (web):
console.log('Code:', code)
- Check for tokens (Despia):
console.log('Token:', access_token)
- Verify tokens stored in localStorage
- Verify navigation to /dashboard happens
Remember
The login page determines the flow.
- Check
navigator.userAgent.includes('despia')
- If true → Despia flow with
despia('oauth://...') and /native-callback
- If false → Web flow with standard redirect and
/auth
**The **oauth/prefix is critical.
- Deeplink format:
yourappdeeplink://oauth/auth?tokens
- Without
oauth/ → browser won’t close
- With
oauth/ → browser closes and app receives tokens
Apple Sign-In on iOS is special.
- Check for
despia-iphone or despia-ipad
- Use direct redirect (no
oauth:// prefix)
- Native Apple dialog opens automatically
For support or questions, contact: support@despia.com