import { createFileRoute, useNavigate } from '@tanstack/react-router'; import { z } from 'zod'; import { useEffect } from 'react'; import { Currency, isCurrency } from '@archer/domain'; import { tokenStorage } from '@/infrastructure/storage/LocalTokenStorage'; import { authenticatedUserStore } from '@/infrastructure/storage/AuthenticatedUserStore'; import { AuthenticatedUserMapper } from '@/infrastructure/http/api/auth/mappers/AuthenticatedUserMapper'; import type { AuthenticatedAccount } from '@/domain/entities/AuthenticatedUser'; import { decodeJwt } from '@/lib/jwt'; import { base64ToUtf8 } from '@/lib/base64'; const callbackSearchSchema = z.object({ token: z.string(), refresh: z.string(), }); export const Route = createFileRoute('/auth/callback')({ validateSearch: callbackSearchSchema, component: AuthCallbackPage, }); /** * Pull the `account` slice out of the URL fragment (#account=) and * decode it. Landing-web passes it this way so the dashboard has authoritative * isProfileComplete + emailVerified + preferredCurrency from first render, * instead of defaulting to false/USD until /auth/me catches up. * * Fragment (not query) keeps the payload out of server logs and Referer. */ function readAccountFragment(): AuthenticatedAccount | null { if (typeof window === 'undefined') return null; const hash = window.location.hash.replace(/^#/, ''); if (!hash) return null; const raw = new URLSearchParams(hash).get('account'); if (!raw) return null; try { const decoded = JSON.parse(base64ToUtf8(decodeURIComponent(raw))) as Partial; const currencyCandidate = (decoded as AuthenticatedAccount).preferredCurrency; return { preferredCurrency: isCurrency(currencyCandidate) ? currencyCandidate : Currency.USD, isProfileComplete: Boolean(decoded.isProfileComplete), missingProfileFields: Array.isArray(decoded.missingProfileFields) ? decoded.missingProfileFields : [], emailVerified: Boolean(decoded.emailVerified), }; } catch { return null; } } function AuthCallbackPage() { const { token, refresh } = Route.useSearch(); const navigate = useNavigate(); useEffect(() => { // Store tokens tokenStorage.setTokens(token, refresh); const payload = decodeJwt(token); const accountFromFragment = readAccountFragment(); if (payload) { try { // JWT gives us identity + company context; the URL fragment gives us // billing-state flags landing-web already fetched from /auth/login. // Merge them so gate-time reads of authed.account are correct. const baseUser = AuthenticatedUserMapper.fromJwtPayload(payload); authenticatedUserStore.set( accountFromFragment ? baseUser.with({ account: accountFromFragment }) : baseUser, ); } catch { /* missing currentCompanyId — guard will bounce to /login */ } } // Replace URL to remove tokens + account fragment from history window.history.replaceState({}, '', '/auth/callback'); // Navigate to dashboard navigate({ to: '/dashboard' }); }, [token, refresh, navigate]); return (

Redirecting to dashboard...

); }