onairos
Version:
The Onairos Library is a collection of functions that enable Applications to connect and communicate data with Onairos Identities via User Authorization. Integration for developers is seamless, simple and effective for all applications. LLM SDK capabiliti
411 lines (373 loc) • 18.5 kB
HTML
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OAuth Callback - Onairos</title>
<style>
/* Plain white page for OAuth callback - minimal UI */
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: #ffffff;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
color: #333;
}
.container {
text-align: center;
background: #ffffff;
padding: 40px 30px;
max-width: 400px;
color: #333;
}
.success-icon, .error-icon {
font-size: 48px;
margin-bottom: 20px;
}
.message {
font-size: 20px;
font-weight: 600;
margin-bottom: 10px;
}
.details {
font-size: 14px;
color: #666;
margin-bottom: 20px;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Hide all content by default for success state */
#success-state .success-icon,
#success-state .message,
#success-state .details {
display: none;
}
</style>
</head>
<body>
<div class="container">
<!-- Start with blank page for all success cases -->
<div id="loading-state" style="display: none;">
<div class="spinner"></div>
<div class="message">Processing your connection...</div>
<div class="details">Please wait while we complete the setup.</div>
</div>
<div id="success-state" style="display: none;">
<div class="success-icon">✅</div>
<div class="message">Successfully Connected!</div>
<div class="details" id="success-details">You can close this tab now.</div>
</div>
<div id="error-state" style="display: none;">
<div class="error-icon">❌</div>
<div class="message">Connection Failed</div>
<div class="details" id="error-details">Please try again.</div>
</div>
</div>
<script>
// Parse URL parameters
const urlParams = new URLSearchParams(window.location.search);
const success = urlParams.get('success');
const error = urlParams.get('error');
const platform = urlParams.get('platform');
const returnUrlParam = urlParams.get('returnUrl');
const stateParam = urlParams.get('state');
const details = urlParams.get('details');
const email = urlParams.get('email'); // Extract email from URL params
// Get elements
const loadingState = document.getElementById('loading-state');
const successState = document.getElementById('success-state');
const errorState = document.getElementById('error-state');
const errorDetails = document.getElementById('error-details');
const successDetails = document.getElementById('success-details');
// Detect if we're in a popup or tab
const isInPopup = window.opener !== null && !window.opener.closed;
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
// Try to recover returnUrl from OAuth `state` (crucial for cross-origin flows where localStorage isn't shared)
const extractReturnUrlFromState = (rawState) => {
if (!rawState) return null;
const variants = [];
try { variants.push(rawState); } catch (_) {}
try { variants.push(decodeURIComponent(rawState)); } catch (_) {}
try { variants.push(atob(rawState)); } catch (_) {}
try { variants.push(decodeURIComponent(atob(rawState))); } catch (_) {}
for (const v of variants) {
if (!v) continue;
try {
const obj = JSON.parse(v);
const maybe =
obj?.returnUrl ||
obj?.redirectUrl ||
obj?.callbackUrl ||
obj?.return_url ||
obj?.returnURL;
if (typeof maybe === 'string' && maybe.length > 0) return maybe;
} catch (_) {
// ignore parse failures
}
}
return null;
};
const stateReturnUrl = extractReturnUrlFromState(stateParam);
if (stateReturnUrl) {
try { localStorage.setItem('onairos_last_return_url', stateReturnUrl); } catch (_) {}
}
const resolveReturnUrl = () => {
return (
returnUrlParam ||
stateReturnUrl ||
localStorage.getItem('onairos_return_url') ||
localStorage.getItem('onairos_last_return_url') ||
''
);
};
console.log('🔄 OAuth callback received:', { success, error, platform, details, email, isInPopup, isMobile, hasReturnUrl: !!resolveReturnUrl() });
if (success === 'true' && platform) {
// Success flow
console.log(`✅ OAuth success for platform: ${platform}`);
// Keep the page plain white - hide all content
loadingState.style.display = 'none';
successState.style.display = 'none';
// Signal success to parent window via localStorage (works across tabs)
localStorage.setItem(`onairos_${platform}_success`, 'true');
localStorage.setItem(`onairos_${platform}_timestamp`, Date.now().toString());
// Store email if provided (for Gmail OAuth)
if (email) {
localStorage.setItem(`onairos_${platform}_email`, email);
// Also store as generic oauth_email for compatibility
localStorage.setItem('onairos_oauth_email', email);
localStorage.setItem('onairos_gmail_email', email); // Direct key for Gmail
console.log(`📧 Stored ${platform} email in localStorage:`, email);
}
// Try to close the Capacitor Browser automatically
try {
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Browser) {
console.log('🚪 Attempting to close Capacitor Browser automatically...');
window.Capacitor.Plugins.Browser.close();
console.log('✅ Called Browser.close()');
}
} catch (e) {
console.log('⚠️ Could not close Capacitor Browser:', e.message);
}
// Try to notify parent via postMessage (works for popups)
try {
if (window.opener && !window.opener.closed) {
window.opener.postMessage({
type: 'oauth-success',
platform: platform,
email: email,
gmailEmail: email, // Alias for Gmail
success: true
}, '*');
console.log('📤 Sent onairos oauth-success postMessage to parent window');
}
} catch (e) {
console.log('⚠️ Could not send postMessage (cross-origin or no opener):', e.message);
}
// Redirect behavior:
// - For popups: close immediately
// - For same-page redirects (mobile): redirect back to stored return URL
if (isInPopup) {
// Popup window - close immediately
setTimeout(() => {
console.log('🚪 Closing OAuth popup window (success)');
try {
window.close();
} catch (e) {
console.log('⚠️ Could not close popup:', e.message);
}
}, 100);
} else {
// Same-page redirect (mobile) - redirect back to stored return URL
const returnUrl = resolveReturnUrl();
// If we have a return URL, ALWAYS go back (mobile Safari often loses opener/context).
if (returnUrl) {
console.log('🔄 Redirecting back to:', returnUrl);
successDetails.textContent = 'Redirecting back...';
// Build redirect URL with success params
let redirectUrl;
try {
redirectUrl = new URL(returnUrl);
} catch (e) {
redirectUrl = new URL(window.location.origin);
}
redirectUrl.searchParams.set('onairos_oauth_success', 'true');
redirectUrl.searchParams.set('onairos_oauth_platform', platform);
if (email) {
redirectUrl.searchParams.set('onairos_oauth_email', email);
}
// Keep connector context so the SDK can reliably re-open UniversalOnboarding after redirect,
// even if query params get stripped or a SPA router eats them.
// The SDK (OnairosButton) is responsible for clearing these keys once it consumes them.
try { localStorage.setItem('onairos_post_oauth_flow', 'onboarding'); } catch (_) {}
// Redirect immediately
console.log('📍 Redirecting to:', redirectUrl.toString());
window.location.replace(redirectUrl.toString());
} else {
// Fallback: show message with manual navigation option
const appUrl = window.location.origin;
successDetails.innerHTML = `
<div style="margin-bottom: 16px;">Connection successful! You can close this tab.</div>
<a href="${appUrl}"
style="display: inline-block; padding: 12px 24px; background: #667eea; color: white; text-decoration: none; border-radius: 8px; font-weight: 600;"
onclick="localStorage.setItem('onairos_should_check_oauth', 'true');">
Return to App
</a>
`;
}
}
} else if (error || success === 'false') {
// Error flow
console.log(`❌ OAuth error for platform: ${platform}`, error);
// Show error state
loadingState.style.display = 'none';
errorState.style.display = 'block';
if (errorDetails) {
errorDetails.textContent = error || details || 'Please try again.';
}
// Signal error to parent window
const platformKey = platform || 'unknown';
localStorage.setItem(`onairos_${platformKey}_error`, error || 'Unknown error');
localStorage.setItem(`onairos_${platformKey}_timestamp`, Date.now().toString());
// Try to notify parent via postMessage (works for popups on desktop web)
try {
if (window.opener && !window.opener.closed) {
window.opener.postMessage({
type: 'oauth-failure',
platform: platformKey,
error: error || 'Unknown error',
success: false
}, '*');
console.log('📤 Sent onairos oauth-failure postMessage to parent window');
}
} catch (e) {
console.log('⚠️ Could not send postMessage (cross-origin or no opener):', e.message);
}
// Desktop popup: close immediately (same behavior as success).
// Redirecting here can strand a popup/tab on api2.onairos.uk even though the parent can recover.
if (isInPopup) {
// Try to close the Capacitor Browser automatically (native flows can surface errors too)
try {
if (window.Capacitor && window.Capacitor.Plugins && window.Capacitor.Plugins.Browser) {
console.log('🚪 Attempting to close Capacitor Browser automatically (error)...');
window.Capacitor.Plugins.Browser.close();
console.log('✅ Called Browser.close()');
}
} catch (e) {
console.log('⚠️ Could not close Capacitor Browser:', e.message);
}
setTimeout(() => {
console.log('🚪 Closing OAuth popup window (error)');
try {
window.close();
} catch (e) {
console.log('⚠️ Could not close popup:', e.message);
}
}, 100);
return;
}
// Non-popup (mobile/new-tab): redirect back if we can resolve a return URL.
// On mobile, the OAuth callback is often a full tab (no window.opener) and cannot be closed by script.
let returnUrl = resolveReturnUrl();
// AGGRESSIVE FALLBACK for mobile: if we have no returnUrl but we're on a mobile device,
// try to go back to the referring page or the app origin.
if (!returnUrl && isMobile) {
try {
// Try document.referrer first (the page that initiated the OAuth)
if (document.referrer && document.referrer.length > 0) {
returnUrl = document.referrer;
console.log('📱 Mobile error fallback: using document.referrer:', returnUrl);
} else {
// Last resort: go to the origin root (localhost:3000, etc)
returnUrl = window.location.origin;
console.log('📱 Mobile error fallback: using window.location.origin:', returnUrl);
}
} catch (e) {
console.log('⚠️ Could not resolve mobile fallback URL:', e.message);
}
}
if (returnUrl) {
try {
let redirectUrl;
try {
redirectUrl = new URL(returnUrl);
} catch (e) {
redirectUrl = new URL(window.location.origin);
}
redirectUrl.searchParams.set('onairos_oauth_success', 'false');
redirectUrl.searchParams.set('onairos_oauth_platform', platformKey);
redirectUrl.searchParams.set('onairos_oauth_error', error || 'Unknown error');
console.log('📍 Redirecting back (error) to:', redirectUrl.toString());
try {
localStorage.removeItem('onairos_return_url');
localStorage.removeItem('onairos_oauth_context');
localStorage.removeItem('onairos_oauth_platform');
} catch (_) {}
window.location.replace(redirectUrl.toString());
return;
} catch (e) {
console.log('⚠️ Error redirect back failed:', e.message);
}
}
// No returnUrl in a non-popup context: show error briefly, then try history.back() as last resort
console.log('⚠️ No returnUrl available - showing error briefly then attempting history.back()');
setTimeout(() => {
console.log('🔙 Attempting history.back() to escape error page');
try {
window.history.back();
} catch (e) {
console.log('⚠️ history.back() failed:', e.message);
}
}, 3000);
} else {
// No clear success or error - treat as potential success
console.log('🤔 Ambiguous OAuth callback, treating as success');
// Keep the page plain white - hide all content
loadingState.style.display = 'none';
successState.style.display = 'none';
if (platform) {
localStorage.setItem(`onairos_${platform}_success`, 'true');
localStorage.setItem(`onairos_${platform}_timestamp`, Date.now().toString());
}
// Best-effort close after a short delay
setTimeout(() => {
console.log('🚪 Attempting to close OAuth window (ambiguous result)');
try {
window.close();
} catch (e) {
console.log('⚠️ Could not close window:', e.message);
}
}, 2000);
}
// Fallback close attempt ONLY for real popups. For mobile/new-tab flows this is noisy and blocked.
if (isInPopup) {
setTimeout(() => {
console.log('🚪 Fallback: Attempting to close OAuth window');
try {
window.close();
} catch (e) {
console.log('⚠️ Fallback: Could not close window:', e.message);
}
}, 5000);
}
// Handle cases where window.close() might not work
window.addEventListener('beforeunload', () => {
console.log('🚪 OAuth popup window closing');
});
</script>
</body>
</html>