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>
214 lines
5.5 KiB
TypeScript
214 lines
5.5 KiB
TypeScript
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import {
|
|
createBrowserRouter,
|
|
RouterProvider,
|
|
Navigate,
|
|
Outlet,
|
|
useNavigate,
|
|
} from "react-router-dom";
|
|
import { AuthInitializer } from "@/components/auth-initializer";
|
|
import { AuthGuard } from "@/components/auth-guard";
|
|
import { DashboardPage } from "@/pages/dashboard-page";
|
|
import { ScanPage } from "@/pages/scan-page";
|
|
import { LibraryPage } from "@/pages/library-page";
|
|
import { CatalogPage } from "@/pages/catalog-page";
|
|
import { DiseaseDetailPage } from "@/pages/disease-detail-page";
|
|
import { DiagnosisDetailPage } from "@/pages/diagnosis-detail-page";
|
|
import { ExpertReviewsPage } from "@/pages/expert-reviews-page";
|
|
import { DiagnosesPage } from "@/pages/diagnoses-page";
|
|
import { TelemetryPage } from "@/pages/telemetry-page";
|
|
import { LoginPage } from "@/pages/login-page";
|
|
import { RegisterPage } from "@/pages/register-page";
|
|
import { MainLayout } from "@/components/layout/main-layout";
|
|
import { useEffect, useRef } from "react";
|
|
import { useAuthStore } from "@/store/auth-store";
|
|
import { apiClient } from "@/lib/api-client";
|
|
import { setupDeepLinkHandler, consumeDeepLinkTarget } from "@/lib/tauri";
|
|
|
|
function LogoutProses() {
|
|
const setUser = useAuthStore((state) => state.setUser);
|
|
|
|
useEffect(() => {
|
|
apiClient.logout().then(() => {
|
|
setUser(null);
|
|
|
|
}).catch((error) => {
|
|
console.error("Oops, gagal logout dari server:", error);
|
|
setUser(null);
|
|
});
|
|
}, [setUser]);
|
|
|
|
return <Navigate to="/login" replace />;
|
|
}
|
|
import { trackPageView, trackError } from "./lib/telemetry";
|
|
|
|
const queryClient = new QueryClient();
|
|
|
|
const router = createBrowserRouter([
|
|
{
|
|
element: <DeepLinkRouterHandler />,
|
|
children: [
|
|
{ path: "/", element: <Navigate to="/login" replace /> },
|
|
{ path: "/login", element: <LoginPage /> },
|
|
{ path: "/register", element: <RegisterPage /> },
|
|
{
|
|
path: "/dashboard",
|
|
element: (
|
|
<AuthGuard>
|
|
<MainLayout>
|
|
<DashboardPage />
|
|
</MainLayout>
|
|
</AuthGuard>
|
|
),
|
|
},
|
|
{
|
|
path: "/scan",
|
|
element: (
|
|
<AuthGuard>
|
|
<MainLayout>
|
|
<ScanPage />
|
|
</MainLayout>
|
|
</AuthGuard>
|
|
),
|
|
},
|
|
{
|
|
path: "/library",
|
|
element: (
|
|
<AuthGuard>
|
|
<MainLayout>
|
|
<LibraryPage />
|
|
</MainLayout>
|
|
</AuthGuard>
|
|
),
|
|
},
|
|
{
|
|
path: "/diagnoses",
|
|
element: (
|
|
<AuthGuard>
|
|
<MainLayout>
|
|
<DiagnosesPage />
|
|
</MainLayout>
|
|
</AuthGuard>
|
|
),
|
|
},
|
|
{
|
|
path: "/diagnoses/:id",
|
|
element: (
|
|
<AuthGuard>
|
|
<MainLayout>
|
|
<DiagnosisDetailPage />
|
|
</MainLayout>
|
|
</AuthGuard>
|
|
),
|
|
},
|
|
{
|
|
path: "/expert/reviews",
|
|
element: (
|
|
<AuthGuard requireExpert={true}>
|
|
<MainLayout>
|
|
<ExpertReviewsPage />
|
|
</MainLayout>
|
|
</AuthGuard>
|
|
),
|
|
},
|
|
{
|
|
path: "/catalog",
|
|
element: (
|
|
<AuthGuard>
|
|
<MainLayout>
|
|
<CatalogPage />
|
|
</MainLayout>
|
|
</AuthGuard>
|
|
),
|
|
},
|
|
{
|
|
path: "/catalog/:slug",
|
|
element: (
|
|
<AuthGuard>
|
|
<MainLayout>
|
|
<DiseaseDetailPage />
|
|
</MainLayout>
|
|
</AuthGuard>
|
|
),
|
|
},
|
|
{
|
|
path: "/logout",
|
|
element: <LogoutProses />,
|
|
},
|
|
{
|
|
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() {
|
|
const location = window.location;
|
|
useEffect(() => {
|
|
trackPageView(location.pathname + location.search);
|
|
}, [location.pathname, location.search]);
|
|
return null;
|
|
}
|
|
|
|
function GlobalErrorTracker() {
|
|
useEffect(() => {
|
|
const handler = (event: ErrorEvent) => {
|
|
trackError(event.filename || "global");
|
|
};
|
|
window.addEventListener("error", handler);
|
|
return () => window.removeEventListener("error", handler);
|
|
}, []);
|
|
return null;
|
|
}
|
|
|
|
export function App() {
|
|
// Register deep link handler for Android OAuth return
|
|
useEffect(() => {
|
|
setupDeepLinkHandler();
|
|
}, []);
|
|
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<AuthInitializer />
|
|
<PageViewTracker />
|
|
<GlobalErrorTracker />
|
|
<RouterProvider router={router} />
|
|
</QueryClientProvider>
|
|
);
|
|
}
|