Files
zeavis-edu/apps/web/src/pages/login-page.tsx
T
MythEclipseandClaude c75cba214e 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>
2026-06-16 16:36:31 +07:00

157 lines
5.3 KiB
TypeScript

import { useState, useEffect, useRef } from "react";
import { Link, useNavigate, useLocation } from "react-router-dom";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { AuthForm } from "@/components/auth-form";
import { apiClient, setAuthToken } from "@/lib/api-client";
import { useAuthStore } from "@/store/auth-store";
import { isTauri } from "@/lib/tauri";
function getUrlParam(name: string): string | null {
return new URLSearchParams(window.location.search).get(name);
}
export function LoginPage() {
const navigate = useNavigate();
const queryClient = useQueryClient();
const setUser = useAuthStore((state) => state.setUser);
const location = useLocation();
const [error, setError] = useState<string | null>(null);
const oauthTokenConsumed = useRef(false);
const [oauthProcessing, setOauthProcessing] = useState(false);
// 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(() => {
const token = getUrlParam("token");
if (!token || oauthTokenConsumed.current) return;
oauthTokenConsumed.current = true;
setOauthProcessing(true);
// Store token for future API calls and fetch user
setAuthToken(token);
apiClient
.getMe()
.then((data) => {
setUser(data.user);
queryClient.setQueryData(["auth", "me"], data);
navigate("/dashboard", { replace: true });
})
.catch((err) => {
setAuthToken(null);
setOauthProcessing(false);
setError(err instanceof Error ? err.message : "Google login gagal");
});
}, [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
const oauthError = getUrlParam("error");
const meQuery = useQuery({
queryKey: ["auth", "me"],
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({
mutationFn: apiClient.login,
onSuccess: (response) => {
setUser(response.user);
queryClient.setQueryData(["auth", "me"], response);
navigate("/dashboard");
},
onError: (err) =>
setError(err instanceof Error ? err.message : "Login gagal"),
});
// Show loading spinner while OAuth token is being processed
if (oauthProcessing) {
return (
<main className="flex min-h-screen items-center justify-center px-6 py-12">
<div className="flex flex-col items-center gap-3">
<div className="h-10 w-10 border-4 border-green-500 border-t-transparent rounded-full animate-spin" />
<p className="text-gray-500 text-sm">
Menyelesaikan login dengan Google...
</p>
</div>
</main>
);
}
return (
<main className="relative flex min-h-screen items-center justify-center px-6 py-12">
{/* Background Image with Overlay */}
<div
className="absolute inset-0 z-0"
style={{
backgroundImage: `linear-gradient(rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0.4)), url(https://cdn.pixabay.com/photo/2014/09/09/19/07/corn-field-440338_1280.jpg)`,
backgroundSize: "cover",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
}}
/>
{/* Glassmorphism Container */}
<div className="relative z-10 w-full max-w-sm md:max-w-md space-y-6 bg-white/60 backdrop-blur-md p-8 md:p-10 rounded-3xl shadow-2xl border border-white/50">
<AuthForm
mode="login"
isSubmitting={mutation.isPending}
error={oauthError || error}
googleOAuthEnabled={Boolean(
meQuery.data?.features.googleOAuthEnabled,
)}
onSubmit={async ({ email, password }) => {
setError(null);
return mutation.mutateAsync({ email, password });
}}
onFieldChange={() => setError(null)}
/>
<p className="text-center text-sm text-slate-700">
Belum punya akun?{" "}
<Link
className="text-emerald-700 font-bold hover:underline"
to="/register"
>
Daftar
</Link>
</p>
</div>
</main>
);
}