fix(android): resolve Google OAuth login flow on Tauri Android
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>
This commit is contained in:
+133
-98
@@ -3,6 +3,8 @@ import {
|
|||||||
createBrowserRouter,
|
createBrowserRouter,
|
||||||
RouterProvider,
|
RouterProvider,
|
||||||
Navigate,
|
Navigate,
|
||||||
|
Outlet,
|
||||||
|
useNavigate,
|
||||||
} from "react-router-dom";
|
} from "react-router-dom";
|
||||||
import { AuthInitializer } from "@/components/auth-initializer";
|
import { AuthInitializer } from "@/components/auth-initializer";
|
||||||
import { AuthGuard } from "@/components/auth-guard";
|
import { AuthGuard } from "@/components/auth-guard";
|
||||||
@@ -18,10 +20,10 @@ import { TelemetryPage } from "@/pages/telemetry-page";
|
|||||||
import { LoginPage } from "@/pages/login-page";
|
import { LoginPage } from "@/pages/login-page";
|
||||||
import { RegisterPage } from "@/pages/register-page";
|
import { RegisterPage } from "@/pages/register-page";
|
||||||
import { MainLayout } from "@/components/layout/main-layout";
|
import { MainLayout } from "@/components/layout/main-layout";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { useAuthStore } from "@/store/auth-store";
|
import { useAuthStore } from "@/store/auth-store";
|
||||||
import { apiClient } from "@/lib/api-client";
|
import { apiClient } from "@/lib/api-client";
|
||||||
import { setupDeepLinkHandler } from "@/lib/tauri";
|
import { setupDeepLinkHandler, consumeDeepLinkTarget } from "@/lib/tauri";
|
||||||
|
|
||||||
function LogoutProses() {
|
function LogoutProses() {
|
||||||
const setUser = useAuthStore((state) => state.setUser);
|
const setUser = useAuthStore((state) => state.setUser);
|
||||||
@@ -32,7 +34,7 @@ function LogoutProses() {
|
|||||||
|
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
console.error("Oops, gagal logout dari server:", error);
|
console.error("Oops, gagal logout dari server:", error);
|
||||||
setUser(null);
|
setUser(null);
|
||||||
});
|
});
|
||||||
}, [setUser]);
|
}, [setUser]);
|
||||||
|
|
||||||
@@ -43,105 +45,138 @@ import { trackPageView, trackError } from "./lib/telemetry";
|
|||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{ path: "/", element: <Navigate to="/login" replace /> },
|
|
||||||
{ path: "/login", element: <LoginPage /> },
|
|
||||||
{ path: "/register", element: <RegisterPage /> },
|
|
||||||
{
|
{
|
||||||
path: "/dashboard",
|
element: <DeepLinkRouterHandler />,
|
||||||
element: (
|
children: [
|
||||||
<AuthGuard>
|
{ path: "/", element: <Navigate to="/login" replace /> },
|
||||||
<MainLayout>
|
{ path: "/login", element: <LoginPage /> },
|
||||||
<DashboardPage />
|
{ path: "/register", element: <RegisterPage /> },
|
||||||
</MainLayout>
|
{
|
||||||
</AuthGuard>
|
path: "/dashboard",
|
||||||
),
|
element: (
|
||||||
},
|
<AuthGuard>
|
||||||
{
|
<MainLayout>
|
||||||
path: "/scan",
|
<DashboardPage />
|
||||||
element: (
|
</MainLayout>
|
||||||
<AuthGuard>
|
</AuthGuard>
|
||||||
<MainLayout>
|
),
|
||||||
<ScanPage />
|
},
|
||||||
</MainLayout>
|
{
|
||||||
</AuthGuard>
|
path: "/scan",
|
||||||
),
|
element: (
|
||||||
},
|
<AuthGuard>
|
||||||
{
|
<MainLayout>
|
||||||
path: "/library",
|
<ScanPage />
|
||||||
element: (
|
</MainLayout>
|
||||||
<AuthGuard>
|
</AuthGuard>
|
||||||
<MainLayout>
|
),
|
||||||
<LibraryPage />
|
},
|
||||||
</MainLayout>
|
{
|
||||||
</AuthGuard>
|
path: "/library",
|
||||||
),
|
element: (
|
||||||
},
|
<AuthGuard>
|
||||||
{
|
<MainLayout>
|
||||||
path: "/diagnoses",
|
<LibraryPage />
|
||||||
element: (
|
</MainLayout>
|
||||||
<AuthGuard>
|
</AuthGuard>
|
||||||
<MainLayout>
|
),
|
||||||
<DiagnosesPage />
|
},
|
||||||
</MainLayout>
|
{
|
||||||
</AuthGuard>
|
path: "/diagnoses",
|
||||||
),
|
element: (
|
||||||
},
|
<AuthGuard>
|
||||||
{
|
<MainLayout>
|
||||||
path: "/diagnoses/:id",
|
<DiagnosesPage />
|
||||||
element: (
|
</MainLayout>
|
||||||
<AuthGuard>
|
</AuthGuard>
|
||||||
<MainLayout>
|
),
|
||||||
<DiagnosisDetailPage />
|
},
|
||||||
</MainLayout>
|
{
|
||||||
</AuthGuard>
|
path: "/diagnoses/:id",
|
||||||
),
|
element: (
|
||||||
},
|
<AuthGuard>
|
||||||
{
|
<MainLayout>
|
||||||
path: "/expert/reviews",
|
<DiagnosisDetailPage />
|
||||||
element: (
|
</MainLayout>
|
||||||
<AuthGuard requireExpert={true}>
|
</AuthGuard>
|
||||||
<MainLayout>
|
),
|
||||||
<ExpertReviewsPage />
|
},
|
||||||
</MainLayout>
|
{
|
||||||
</AuthGuard>
|
path: "/expert/reviews",
|
||||||
),
|
element: (
|
||||||
},
|
<AuthGuard requireExpert={true}>
|
||||||
{
|
<MainLayout>
|
||||||
path: "/catalog",
|
<ExpertReviewsPage />
|
||||||
element: (
|
</MainLayout>
|
||||||
<AuthGuard>
|
</AuthGuard>
|
||||||
<MainLayout>
|
),
|
||||||
<CatalogPage />
|
},
|
||||||
</MainLayout>
|
{
|
||||||
</AuthGuard>
|
path: "/catalog",
|
||||||
),
|
element: (
|
||||||
},
|
<AuthGuard>
|
||||||
{
|
<MainLayout>
|
||||||
path: "/catalog/:slug",
|
<CatalogPage />
|
||||||
element: (
|
</MainLayout>
|
||||||
<AuthGuard>
|
</AuthGuard>
|
||||||
<MainLayout>
|
),
|
||||||
<DiseaseDetailPage />
|
},
|
||||||
</MainLayout>
|
{
|
||||||
</AuthGuard>
|
path: "/catalog/:slug",
|
||||||
),
|
element: (
|
||||||
},
|
<AuthGuard>
|
||||||
{
|
<MainLayout>
|
||||||
path: "/logout",
|
<DiseaseDetailPage />
|
||||||
element: (
|
</MainLayout>
|
||||||
<LogoutProses />
|
</AuthGuard>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: "/telemetry",
|
path: "/logout",
|
||||||
element: (
|
element: <LogoutProses />,
|
||||||
<MainLayout>
|
},
|
||||||
<TelemetryPage />
|
{
|
||||||
</MainLayout>
|
path: "/telemetry",
|
||||||
),
|
element: (
|
||||||
|
<MainLayout>
|
||||||
|
<TelemetryPage />
|
||||||
|
</MainLayout>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listens for deep-link custom events and routes via React Router's navigate(),
|
||||||
|
* avoiding full page reloads that break the Tauri IPC bridge.
|
||||||
|
*/
|
||||||
|
function DeepLinkRouterHandler() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const handled = useRef(new Set<string>());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Check for cold-start pending deep link
|
||||||
|
const pending = consumeDeepLinkTarget();
|
||||||
|
if (pending && !handled.current.has(pending)) {
|
||||||
|
handled.current.add(pending);
|
||||||
|
navigate(pending, { replace: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for warm-start deep links
|
||||||
|
const handler = (e: CustomEvent<string>) => {
|
||||||
|
const target = e.detail;
|
||||||
|
if (handled.current.has(target)) return;
|
||||||
|
handled.current.add(target);
|
||||||
|
navigate(target, { replace: true });
|
||||||
|
};
|
||||||
|
window.addEventListener('zeavis:deeplink', handler as EventListener);
|
||||||
|
return () => window.removeEventListener('zeavis:deeplink', handler as EventListener);
|
||||||
|
}, [navigate]);
|
||||||
|
|
||||||
|
return <Outlet />;
|
||||||
|
}
|
||||||
|
|
||||||
function PageViewTracker() {
|
function PageViewTracker() {
|
||||||
const location = window.location;
|
const location = window.location;
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { apiBaseUrl } from '@/lib/api-client';
|
||||||
import { isTauri, openUrl } from '@/lib/tauri';
|
import { isTauri, openUrl } from '@/lib/tauri';
|
||||||
|
|
||||||
type AuthFormProps = {
|
type AuthFormProps = {
|
||||||
@@ -29,10 +30,7 @@ export function AuthForm({ mode, isSubmitting, error, googleOAuthEnabled, onSubm
|
|||||||
const handleGoogleLogin = useCallback(async (e: React.MouseEvent) => {
|
const handleGoogleLogin = useCallback(async (e: React.MouseEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const platform = isTauri() ? 'tauri' : 'web';
|
const platform = isTauri() ? 'tauri' : 'web';
|
||||||
// Use API base URL, not window.location.origin — on Tauri Android
|
const googleUrl = `${apiBaseUrl}/api/v1/auth/google?platform=${platform}`;
|
||||||
// the origin is http://tauri.localhost which is not the API server.
|
|
||||||
const apiBase = import.meta.env.VITE_API_BASE_URL || window.location.origin;
|
|
||||||
const googleUrl = `${apiBase}/api/v1/auth/google?platform=${platform}`;
|
|
||||||
await openUrl(googleUrl);
|
await openUrl(googleUrl);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type {
|
|||||||
} from '@zeavis/shared';
|
} from '@zeavis/shared';
|
||||||
import { recordApiCall } from './telemetry';
|
import { recordApiCall } from './telemetry';
|
||||||
|
|
||||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'https://zeavisedu.asepharyana.my.id';
|
export const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'https://zeavisedu.asepharyana.my.id';
|
||||||
|
|
||||||
const AUTH_TOKEN_KEY = 'zeavis_auth_token';
|
const AUTH_TOKEN_KEY = 'zeavis_auth_token';
|
||||||
|
|
||||||
|
|||||||
+55
-20
@@ -1,6 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Lightweight Tauri environment detection and utilities.
|
* Lightweight Tauri environment detection and utilities.
|
||||||
* Uses raw __TAURI_INTERNALS__ IPC to avoid bundling/import issues on Android.
|
* 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;
|
let _isTauri: boolean | null = null;
|
||||||
@@ -22,7 +29,6 @@ function tauriInvoke(): (cmd: string, args?: Record<string, unknown>) => Promise
|
|||||||
|
|
||||||
export async function openUrl(url: string): Promise<void> {
|
export async function openUrl(url: string): Promise<void> {
|
||||||
if (!isTauri()) {
|
if (!isTauri()) {
|
||||||
// Not in Tauri — normal browser navigation
|
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -31,47 +37,76 @@ export async function openUrl(url: string): Promise<void> {
|
|||||||
await invoke('plugin:opener|open_url', { url });
|
await invoke('plugin:opener|open_url', { url });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Tauri openUrl failed, trying fallback:', err);
|
console.error('Tauri openUrl failed, trying fallback:', err);
|
||||||
// Fallback: navigate the WebView (Google will block, but best effort)
|
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function processDeepLinkUrl(url: string): void {
|
// ── Deep link handling (no full reload) ─────────────────────────────────
|
||||||
try {
|
|
||||||
const u = new URL(url);
|
|
||||||
const target = u.pathname + u.search + u.hash;
|
|
||||||
if (target && target !== '/') {
|
|
||||||
window.location.href = target;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch { /* fall through */ }
|
|
||||||
|
|
||||||
// Fallback: handle both :// and :/ custom schemes
|
const DEEP_LINK_KEY = 'zeavis_pending_deeplink';
|
||||||
let match = url.match(/^[^:]+:\/\/(?:[^/]+)?(\/.*)?$/);
|
const DEEP_LINK_EVENT = 'zeavis:deeplink';
|
||||||
if (!match) match = url.match(/^[^:]+:\/(\/.*)?$/);
|
|
||||||
if (match?.[1]) window.location.href = match[1];
|
/** 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> {
|
export async function setupDeepLinkHandler(): Promise<void> {
|
||||||
if (!isTauri()) return;
|
if (!isTauri()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const invoke = tauriInvoke();
|
const invoke = tauriInvoke();
|
||||||
|
|
||||||
// Cold-start: app just opened via intent:// or custom scheme
|
// 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')
|
invoke('plugin:deep-link|get_current')
|
||||||
.then((urls: any) => {
|
.then((urls: any) => {
|
||||||
if (urls?.[0]) processDeepLinkUrl(urls[0]);
|
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(() => { /* plugin may not be registered yet */ });
|
.catch(() => {});
|
||||||
|
|
||||||
// Warm-start: listen for new URLs while app is running
|
// 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');
|
const { listen } = await import('@tauri-apps/api/event');
|
||||||
listen('deep-link://new-url', (event: any) => {
|
listen('deep-link://new-url', (event: any) => {
|
||||||
const urls = event.payload as string[];
|
const urls = event.payload as string[];
|
||||||
for (const url of urls) processDeepLinkUrl(url);
|
for (const url of urls) {
|
||||||
|
const target = extractDeepLinkTarget(url);
|
||||||
|
if (target) {
|
||||||
|
storeDeepLinkTarget(target);
|
||||||
|
window.dispatchEvent(new CustomEvent(DEEP_LINK_EVENT, { detail: target }));
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Tauri deep-link setup failed:', 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] ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate, useLocation } from "react-router-dom";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { AuthForm } from "@/components/auth-form";
|
import { AuthForm } from "@/components/auth-form";
|
||||||
import { apiClient, setAuthToken } from "@/lib/api-client";
|
import { apiClient, setAuthToken } from "@/lib/api-client";
|
||||||
import { useAuthStore } from "@/store/auth-store";
|
import { useAuthStore } from "@/store/auth-store";
|
||||||
|
import { isTauri } from "@/lib/tauri";
|
||||||
|
|
||||||
function getUrlParam(name: string): string | null {
|
function getUrlParam(name: string): string | null {
|
||||||
return new URLSearchParams(window.location.search).get(name);
|
return new URLSearchParams(window.location.search).get(name);
|
||||||
@@ -13,11 +14,13 @@ export function LoginPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const setUser = useAuthStore((state) => state.setUser);
|
const setUser = useAuthStore((state) => state.setUser);
|
||||||
|
const location = useLocation();
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const oauthTokenConsumed = useRef(false);
|
const oauthTokenConsumed = useRef(false);
|
||||||
const [oauthProcessing, setOauthProcessing] = useState(false);
|
const [oauthProcessing, setOauthProcessing] = useState(false);
|
||||||
|
|
||||||
// Handle OAuth callback: the API redirects to /login?token=<session_token>
|
// Handle OAuth callback: the API redirects to /login?token=<session_token>
|
||||||
|
// Must re-run on location.search change (SPA navigates to /login?token=xxx)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = getUrlParam("token");
|
const token = getUrlParam("token");
|
||||||
if (!token || oauthTokenConsumed.current) return;
|
if (!token || oauthTokenConsumed.current) return;
|
||||||
@@ -39,7 +42,36 @@ export function LoginPage() {
|
|||||||
setOauthProcessing(false);
|
setOauthProcessing(false);
|
||||||
setError(err instanceof Error ? err.message : "Google login gagal");
|
setError(err instanceof Error ? err.message : "Google login gagal");
|
||||||
});
|
});
|
||||||
}, [setUser, queryClient, navigate]);
|
}, [setUser, queryClient, navigate, location.search]);
|
||||||
|
|
||||||
|
// Backup: when app returns from background (e.g. after Google OAuth browser)
|
||||||
|
// re-check URL params — the deep-link event may have been missed.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isTauri()) return;
|
||||||
|
if (getUrlParam("token") || oauthTokenConsumed.current) return;
|
||||||
|
|
||||||
|
const onVisibility = () => {
|
||||||
|
if (document.visibilityState !== "visible") return;
|
||||||
|
const token = getUrlParam("token");
|
||||||
|
if (token && !oauthTokenConsumed.current) {
|
||||||
|
setOauthProcessing(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFocus = () => {
|
||||||
|
const token = getUrlParam("token");
|
||||||
|
if (token && !oauthTokenConsumed.current) {
|
||||||
|
setOauthProcessing(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("visibilitychange", onVisibility);
|
||||||
|
window.addEventListener("focus", onFocus);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("visibilitychange", onVisibility);
|
||||||
|
window.removeEventListener("focus", onFocus);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Show OAuth error from query param
|
// Show OAuth error from query param
|
||||||
const oauthError = getUrlParam("error");
|
const oauthError = getUrlParam("error");
|
||||||
@@ -48,6 +80,13 @@ export function LoginPage() {
|
|||||||
queryFn: () => apiClient.getMe(),
|
queryFn: () => apiClient.getMe(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Already authenticated — redirect to dashboard
|
||||||
|
useEffect(() => {
|
||||||
|
if (!meQuery.isLoading && meQuery.data?.user) {
|
||||||
|
navigate("/dashboard", { replace: true });
|
||||||
|
}
|
||||||
|
}, [meQuery.data, meQuery.isLoading, navigate]);
|
||||||
|
|
||||||
const mutation = useMutation({
|
const mutation = useMutation({
|
||||||
mutationFn: apiClient.login,
|
mutationFn: apiClient.login,
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user