Three interrelated fixes for the Android Google sign-in flow:
1. API base URL mismatch (404 error):
- auth-form.tsx used 'window.location.origin || VITE_API_BASE_URL',
which fell back to 'http://tauri.localhost' in Android WebView
instead of the actual API server.
- Fix: import shared 'apiBaseUrl' from api-client.ts (already had
the correct fallback: 'https://zeavisedu.asepharyana.my.id').
- Added .env with VITE_API_BASE_URL for dev mode resilience.
2. Deep-link caused IPC callback errors:
- 'processDeepLinkUrl()' used window.location.href = target,
triggering a full page reload that orphaned pending Tauri IPC
promises, causing 'Cannot read properties of undefined (reading
'runCallback')' errors.
- Cold-start: keep get_current but use window.location.href (safe
at boot — no SPA state to lose).
- Warm-start: use sessionStorage + custom DOM event + React Router
navigate() via new <DeepLinkRouterHandler /> layout route,
avoiding any page reload.
3. SPA navigation did not trigger OAuth token handler:
- LoginPage's useEffect for ?token=xxx depended only on
[setUser, queryClient, navigate] — location.search changes
from a SPA navigate() call were ignored.
- Fix: added location.search and location to deps.
- Added visibilitychange + focus listeners so returning from the
Google auth browser always re-checks URL params.
Co-Authored-By: Claude <noreply@anthropic.com>
113 lines
3.8 KiB
TypeScript
113 lines
3.8 KiB
TypeScript
/**
|
|
* Lightweight Tauri environment detection and utilities.
|
|
* Uses raw __TAURI_INTERNALS__ IPC to avoid bundling/import issues on Android.
|
|
*
|
|
* Deep-link flow (no full page reloads — uses React Router navigate()):
|
|
* 1. Tauri deep-link plugin receives URL via intent/custom-scheme.
|
|
* 2. processDeepLinkUrl stores the target path in sessionStorage +
|
|
* dispatches a custom DOM event.
|
|
* 3. <DeepLinkRouterHandler /> inside <RouterProvider> picks it up and
|
|
* calls navigate(), keeping the React app alive.
|
|
*/
|
|
|
|
let _isTauri: boolean | null = null;
|
|
|
|
export function isTauri(): boolean {
|
|
if (_isTauri !== null) return _isTauri;
|
|
_isTauri =
|
|
typeof window !== 'undefined' &&
|
|
'__TAURI_INTERNALS__' in window;
|
|
return _isTauri;
|
|
}
|
|
|
|
/** Get the Tauri IPC invoke function directly from the global internals. */
|
|
function tauriInvoke(): (cmd: string, args?: Record<string, unknown>) => Promise<unknown> {
|
|
const T = (window as any).__TAURI_INTERNALS__;
|
|
if (!T?.invoke) throw new Error('Tauri IPC not available');
|
|
return T.invoke.bind(T);
|
|
}
|
|
|
|
export async function openUrl(url: string): Promise<void> {
|
|
if (!isTauri()) {
|
|
window.location.href = url;
|
|
return;
|
|
}
|
|
try {
|
|
const invoke = tauriInvoke();
|
|
await invoke('plugin:opener|open_url', { url });
|
|
} catch (err) {
|
|
console.error('Tauri openUrl failed, trying fallback:', err);
|
|
window.location.href = url;
|
|
}
|
|
}
|
|
|
|
// ── Deep link handling (no full reload) ─────────────────────────────────
|
|
|
|
const DEEP_LINK_KEY = 'zeavis_pending_deeplink';
|
|
const DEEP_LINK_EVENT = 'zeavis:deeplink';
|
|
|
|
/** Store a target path for the React Router to pick up without page reload. */
|
|
function storeDeepLinkTarget(target: string): void {
|
|
try { sessionStorage.setItem(DEEP_LINK_KEY, target); } catch { /* ignore */ }
|
|
}
|
|
|
|
/** Read and clear the stored deep link target. */
|
|
export function consumeDeepLinkTarget(): string | null {
|
|
try {
|
|
const v = sessionStorage.getItem(DEEP_LINK_KEY);
|
|
if (v) sessionStorage.removeItem(DEEP_LINK_KEY);
|
|
return v;
|
|
} catch { return null; }
|
|
}
|
|
|
|
|
|
export async function setupDeepLinkHandler(): Promise<void> {
|
|
if (!isTauri()) return;
|
|
|
|
try {
|
|
const invoke = tauriInvoke();
|
|
|
|
// Cold-start: app opened via intent:// (e.g. from Google OAuth callback).
|
|
// Use window.location.href for this (full page reload) — at cold start there
|
|
// is no SPA state to lose, so redirecting via location.href avoids orphaned
|
|
// IPC promises that cause "Cannot read properties of undefined (reading 'runCallback')".
|
|
invoke('plugin:deep-link|get_current')
|
|
.then((urls: any) => {
|
|
if (!urls?.[0]) return;
|
|
const target = extractDeepLinkTarget(urls[0]);
|
|
if (target && target !== window.location.pathname + window.location.search + window.location.hash) {
|
|
window.location.href = target;
|
|
}
|
|
})
|
|
.catch(() => {});
|
|
|
|
// Warm-start: listen for new URLs (already running app).
|
|
// Use React Router navigate() here since we have SPA state.
|
|
const { listen } = await import('@tauri-apps/api/event');
|
|
listen('deep-link://new-url', (event: any) => {
|
|
const urls = event.payload as string[];
|
|
for (const url of urls) {
|
|
const target = extractDeepLinkTarget(url);
|
|
if (target) {
|
|
storeDeepLinkTarget(target);
|
|
window.dispatchEvent(new CustomEvent(DEEP_LINK_EVENT, { detail: target }));
|
|
}
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.error('Tauri deep-link setup failed:', err);
|
|
}
|
|
}
|
|
|
|
/** Extract path+query+hash from a deep-link URL. */
|
|
function extractDeepLinkTarget(url: string): string {
|
|
try {
|
|
const u = new URL(url);
|
|
return u.pathname + u.search + u.hash;
|
|
} catch {
|
|
let m = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
|
|
if (!m) m = url.match(/^[^:]+:\/(\/.*)?$/);
|
|
return m?.[1] ?? '';
|
|
}
|
|
}
|