feat(android): APK loads live web, fix cold-start deep links for Google OAuth
APK architecture change: replaces bundled React SPA with minimal redirect page that always loads live web content. No more APK rebuilds for web changes. Root cause of OAuth failure on Android: 1. Cold-start deep links lost — APK's old bundled JS called onOpenUrl() (warm-start listener only) but NOT getCurrent() which is required for cold-start deep links. Fix: redirect page + setupDeepLinkHandler both call getCurrent() before redirecting/navigating. 2. Session cookie was dropped — renderTauriDeepLinkPage returned a raw new Response() which overwrote the Set-Cookie header set by the callback handler. Fix: inject Set-Cookie into the Response. 3. tauri.conf.json frontendDist → "./web/dist-tauri" (redirect page) 4. Added @tauri-apps/plugin-deep-link and @tauri-apps/plugin-opener as web app deps so live-web imports work in Tauri WebView. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -314,12 +314,18 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
||||
}
|
||||
|
||||
const token = await createSession(user.id);
|
||||
set.headers['Set-Cookie'] = createSessionCookie(token, request.headers);
|
||||
const sessionCookie = createSessionCookie(token, request.headers);
|
||||
|
||||
authCounter.labels('login', 'true').inc();
|
||||
|
||||
const successUrl = `${env.webAppUrl}/login?token=${encodeURIComponent(token)}`;
|
||||
if (platform === 'tauri') return renderTauriDeepLinkPage(successUrl);
|
||||
if (platform === 'tauri') {
|
||||
// Inject Set-Cookie into the response so the browser gets it on redirect
|
||||
const resp = renderTauriDeepLinkPage(successUrl);
|
||||
resp.headers.set('Set-Cookie', sessionCookie);
|
||||
return resp;
|
||||
}
|
||||
set.headers['Set-Cookie'] = sessionCookie;
|
||||
set.status = 302;
|
||||
set.headers['Location'] = successUrl;
|
||||
} catch (err) {
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
"version": "0.1.0",
|
||||
"identifier": "com.zeavis.edu",
|
||||
"build": {
|
||||
"frontendDist": "../web/dist",
|
||||
"frontendDist": "../web/dist-tauri",
|
||||
"devUrl": "http://localhost:5173",
|
||||
"beforeBuildCommand": "cd ../web && bun run build",
|
||||
"beforeDevCommand": "cd ../web && bun run dev"
|
||||
"beforeBuildCommand": "echo 'Nothing to build (APK loads live web content)'",
|
||||
"beforeDevCommand": "echo 'Dev mode - connect to localhost:5173'"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": false,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>ZeaVis Edu</title>
|
||||
<script>
|
||||
var LIVE = 'https://zeavisedu.asepharyana.my.id';
|
||||
var T = window.__TAURI_INTERNALS__;
|
||||
|
||||
function navigate(path) {
|
||||
window.location.replace(LIVE + path);
|
||||
}
|
||||
|
||||
// On cold start, check if the app was opened via a deep link (Google OAuth)
|
||||
// before redirecting to the live web app.
|
||||
if (T && T.invoke) {
|
||||
T.invoke('plugin:deep-link|get_current')
|
||||
.then(function(urls) {
|
||||
if (urls && urls.length > 0 && urls[0]) {
|
||||
try {
|
||||
var u = new URL(urls[0]);
|
||||
var target = u.pathname + u.search + u.hash;
|
||||
if (target && target !== '/') {
|
||||
// Preserve full path + query (e.g. /login?token=xxx)
|
||||
navigate(target);
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* malformed URL — fall through */ }
|
||||
}
|
||||
// No deep link — redirect to live app home
|
||||
navigate('/');
|
||||
})
|
||||
.catch(function() { navigate('/'); });
|
||||
} else {
|
||||
// Not in Tauri (dev mode or unknown) — redirect to live
|
||||
navigate('/');
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body style="background:#f0fdf4;font-family:sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0">
|
||||
<p style="color:#16a34a">Memuat ZeaVis Edu...</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,6 +12,8 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tanstack/react-query": "^5.100.11",
|
||||
"@tauri-apps/plugin-deep-link": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"@zeavis/shared": "workspace:*",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
+31
-21
@@ -29,32 +29,42 @@ export async function openUrl(url: string): Promise<void> {
|
||||
* callback page redirects to zeavisedu://... which triggers this listener.
|
||||
* We extract the path + query and navigate the WebView there.
|
||||
*/
|
||||
function processDeepLinkUrl(url: string): void {
|
||||
// url looks like: zeavisedu://login/login?token=xxx
|
||||
// We need to extract path+query and navigate the WebView there
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const target = u.pathname + u.search + u.hash;
|
||||
if (target && target !== '/') {
|
||||
window.location.href = target;
|
||||
return;
|
||||
}
|
||||
} catch { /* try fallback below */ }
|
||||
|
||||
// Fallback: handle both double-slash (://) and single-slash (:/) schemes
|
||||
let match = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
|
||||
if (!match) {
|
||||
match = url.match(/^[^:]+:\/(\/.*)?$/);
|
||||
}
|
||||
if (match?.[1]) {
|
||||
window.location.href = match[1];
|
||||
}
|
||||
}
|
||||
|
||||
export async function setupDeepLinkHandler(): Promise<void> {
|
||||
if (!isTauri()) return;
|
||||
|
||||
const { onOpenUrl } = await import('@tauri-apps/plugin-deep-link');
|
||||
const { onOpenUrl, getCurrent } = await import('@tauri-apps/plugin-deep-link');
|
||||
|
||||
// Handle cold-start deep links (app opened via intent)
|
||||
getCurrent().then((urls) => {
|
||||
if (urls?.[0]) processDeepLinkUrl(urls[0]);
|
||||
}).catch(() => { /* ignore */ });
|
||||
|
||||
// Handle warm-start deep links (app already running, new intent received)
|
||||
onOpenUrl((urls) => {
|
||||
for (const url of urls) {
|
||||
// url looks like: zeavisedu://login/login?token=xxx
|
||||
// Extract path + query after the host
|
||||
try {
|
||||
const u = new URL(url);
|
||||
const target = u.pathname + u.search + u.hash;
|
||||
if (target && target !== '/') {
|
||||
window.location.href = target;
|
||||
return;
|
||||
}
|
||||
} catch { /* try fallback below */ }
|
||||
|
||||
// Fallback: extract everything after scheme, handling both // and /
|
||||
let match = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
|
||||
if (!match) {
|
||||
// Also handle single-slash non-hierarchical URLs (e.g. zeavisedu:/path)
|
||||
match = url.match(/^[^:]+:\/(\/.*)?$/);
|
||||
}
|
||||
if (match?.[1]) {
|
||||
window.location.href = match[1];
|
||||
}
|
||||
processDeepLinkUrl(url);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.2.4",
|
||||
"@tanstack/react-query": "^5.100.11",
|
||||
"@tauri-apps/plugin-deep-link": "^2",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
"@vitejs/plugin-react": "^6.0.2",
|
||||
"@zeavis/shared": "workspace:*",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
|
||||
Reference in New Issue
Block a user