Browse Source

feat(auth): implement early marriage data hydration via root layout

Inject marriage data into the global window object from cookies during
the initial server-side render to prevent hydration mismatches and
reduce client-side latency.

- Add script injection in `RootLayout` to bootstrap `window.HABIB_MARRIAGE`
- Update `AuthBridge` to support `__HABIB_BOOTSTRAP__` and improved
  cookie parsing logic
- Enhance `getClientCookie` with robust URI decoding error handling
master
mortezaei 2 weeks ago
parent
commit
a5e4cf0584
  1. 16
      src/app/candidate-contact/candidate-contact-client.tsx
  2. 5
      src/app/new-match/new-match-client.tsx
  3. 7
      src/app/questions-list/questions-list-client.tsx
  4. 5
      src/app/request-accepted/request-accepted-client.tsx
  5. 5
      src/app/request-sent/request-sent-client.tsx
  6. 5
      src/app/terms/page.tsx
  7. 17
      src/hooks/use-habib-web-ready.ts

16
src/app/candidate-contact/candidate-contact-client.tsx

@ -45,10 +45,13 @@ export default function CandidateContactClient() {
const { isProfileOpen, openProfile, closeProfile } = const { isProfileOpen, openProfile, closeProfile } =
useMatchProfileOverlay(); useMatchProfileOverlay();
const { data: profile, isLoading: isProfileLoading } =
useMarriageProfileQuery({
refetchInterval: 3000,
});
const {
data: profile,
isLoading: isProfileLoading,
isFetched,
} = useMarriageProfileQuery({
refetchInterval: 3000,
});
useEffect(() => { useEffect(() => {
if (!profile) { if (!profile) {
@ -60,8 +63,9 @@ export default function CandidateContactClient() {
} }
}, [profile, router, locale]); }, [profile, router, locale]);
// Signal Flutter to lift its loading cover immediately with 0ms latency.
useHabibWebReady(true);
// Signal Flutter to lift its loading cover once profile is available or query finished.
const isVisuallyReady = Boolean(profile) || isFetched;
useHabibWebReady(isVisuallyReady);
const isRedirecting = useMemo(() => { const isRedirecting = useMemo(() => {
if (!profile) return false; if (!profile) return false;

5
src/app/new-match/new-match-client.tsx

@ -611,8 +611,9 @@ export default function NewMatchClient() {
} }
}, [profile, locale, router, isLoading, isFetched, isError]); }, [profile, locale, router, isLoading, isFetched, isError]);
// Signal Flutter to lift its loading cover immediately with 0ms latency.
useHabibWebReady(true);
// Signal Flutter to lift its loading cover only once match summary is populated in DOM or query finished.
const isVisuallyReady = Boolean(profile?.match_summary) || isFetched || isError;
useHabibWebReady(isVisuallyReady);
const handleOpenProfile = async () => { const handleOpenProfile = async () => {
// If profile has not yet completed its initial server fetch or is in flight: // If profile has not yet completed its initial server fetch or is in flight:

7
src/app/questions-list/questions-list-client.tsx

@ -91,7 +91,7 @@ export default function QuestionsListClient() {
const { dictionary: t, locale } = useI18n(); const { dictionary: t, locale } = useI18n();
const router = useRouter(); const router = useRouter();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { data: profile, isLoading: isProfileLoading, isError: isProfileError, refetch: refetchProfile } =
const { data: profile, isLoading: isProfileLoading, isError: isProfileError, isFetched: isProfileFetched, refetch: refetchProfile } =
useMarriageProfileQuery(); useMarriageProfileQuery();
const { data: overview, isLoading: isSchemaLoading, isError: isSchemaError, refetch: refetchOverview } = useFormOverviewQuery( const { data: overview, isLoading: isSchemaLoading, isError: isSchemaError, refetch: refetchOverview } = useFormOverviewQuery(
"profile", "profile",
@ -136,8 +136,9 @@ export default function QuestionsListClient() {
void fetchGeoCountryCode(); void fetchGeoCountryCode();
}, []); }, []);
// Signal Flutter to lift its loading cover immediately with 0ms latency.
useHabibWebReady(true);
// Signal Flutter to lift its loading cover once the profile is available or query finished.
const isVisuallyReady = Boolean(profile) || isProfileFetched;
useHabibWebReady(isVisuallyReady);
const startMatchMutation = useStartMarriageMatchMutation({ const startMatchMutation = useStartMarriageMatchMutation({
onSuccess: () => { onSuccess: () => {

5
src/app/request-accepted/request-accepted-client.tsx

@ -267,8 +267,9 @@ export default function RequestAcceptedClient() {
} }
}, [profile, isFetched, router, locale, noContactReportedSuccess]); }, [profile, isFetched, router, locale, noContactReportedSuccess]);
// Signal Flutter to lift its loading cover immediately with 0ms latency.
useHabibWebReady(true);
// Signal Flutter to lift its loading cover only once profile is populated in DOM or query finished.
const isVisuallyReady = Boolean(profile) || isFetched;
useHabibWebReady(isVisuallyReady);
const handleOpenProfile = async () => { const handleOpenProfile = async () => {
if (!isFetched || isFetching) { if (!isFetched || isFetching) {

5
src/app/request-sent/request-sent-client.tsx

@ -57,8 +57,9 @@ export default function RequestSentClient() {
} }
}, [profile, isFetched, locale, router]); }, [profile, isFetched, locale, router]);
// Signal Flutter to lift its loading cover immediately with 0ms latency.
useHabibWebReady(true);
// Signal Flutter to lift its loading cover only once profile is populated in DOM or query finished.
const isVisuallyReady = Boolean(profile) || isFetched;
useHabibWebReady(isVisuallyReady);
const isRedirecting = useMemo(() => { const isRedirecting = useMemo(() => {
if (!profile) return false; if (!profile) return false;

5
src/app/terms/page.tsx

@ -18,8 +18,9 @@ export default function TermsRoute() {
const router = useRouter(); const router = useRouter();
const { locale, dictionary: t } = useI18n(); const { locale, dictionary: t } = useI18n();
// Signal Flutter to lift its loading cover immediately with 0ms latency.
useHabibWebReady(true);
// Signal Flutter to lift its loading cover once profile or fallback is ready.
const isVisuallyReady = Boolean(profile) || !isLoading || isError;
useHabibWebReady(isVisuallyReady);
useEffect(() => { useEffect(() => {
if (!isLoading && profile) { if (!isLoading && profile) {

17
src/hooks/use-habib-web-ready.ts

@ -20,7 +20,17 @@ import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache";
export function useHabibWebReady(ready: boolean) { export function useHabibWebReady(ready: boolean) {
useEffect(() => { useEffect(() => {
if (ready && typeof window !== "undefined") { if (ready && typeof window !== "undefined") {
window.__announceHabibWebReady?.();
let cancelled = false;
// Double requestAnimationFrame guarantees the browser has executed layout & paint
// of the ready DOM state to the GPU compositor before Flutter lifts the native cover.
const raf1 = requestAnimationFrame(() => {
const raf2 = requestAnimationFrame(() => {
if (!cancelled) {
window.__announceHabibWebReady?.();
}
});
});
const pathname = window.location.pathname; const pathname = window.location.pathname;
if (pathname) { if (pathname) {
@ -35,6 +45,11 @@ export function useHabibWebReady(ready: boolean) {
setCachedMarriageEntryPath(route); setCachedMarriageEntryPath(route);
} }
} }
return () => {
cancelled = true;
cancelAnimationFrame(raf1);
};
} }
}, [ready]); }, [ready]);
} }
Loading…
Cancel
Save