diff --git a/src/app/candidate-contact/candidate-contact-client.tsx b/src/app/candidate-contact/candidate-contact-client.tsx index 500d786..a5acd36 100644 --- a/src/app/candidate-contact/candidate-contact-client.tsx +++ b/src/app/candidate-contact/candidate-contact-client.tsx @@ -45,10 +45,13 @@ export default function CandidateContactClient() { const { isProfileOpen, openProfile, closeProfile } = useMatchProfileOverlay(); - const { data: profile, isLoading: isProfileLoading } = - useMarriageProfileQuery({ - refetchInterval: 3000, - }); + const { + data: profile, + isLoading: isProfileLoading, + isFetched, + } = useMarriageProfileQuery({ + refetchInterval: 3000, + }); useEffect(() => { if (!profile) { @@ -60,8 +63,9 @@ export default function CandidateContactClient() { } }, [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(() => { if (!profile) return false; diff --git a/src/app/new-match/new-match-client.tsx b/src/app/new-match/new-match-client.tsx index fcb440b..5ebfdbc 100644 --- a/src/app/new-match/new-match-client.tsx +++ b/src/app/new-match/new-match-client.tsx @@ -611,8 +611,9 @@ export default function NewMatchClient() { } }, [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 () => { // If profile has not yet completed its initial server fetch or is in flight: diff --git a/src/app/questions-list/questions-list-client.tsx b/src/app/questions-list/questions-list-client.tsx index 9ff6c95..56b28c3 100644 --- a/src/app/questions-list/questions-list-client.tsx +++ b/src/app/questions-list/questions-list-client.tsx @@ -91,7 +91,7 @@ export default function QuestionsListClient() { const { dictionary: t, locale } = useI18n(); const router = useRouter(); const queryClient = useQueryClient(); - const { data: profile, isLoading: isProfileLoading, isError: isProfileError, refetch: refetchProfile } = + const { data: profile, isLoading: isProfileLoading, isError: isProfileError, isFetched: isProfileFetched, refetch: refetchProfile } = useMarriageProfileQuery(); const { data: overview, isLoading: isSchemaLoading, isError: isSchemaError, refetch: refetchOverview } = useFormOverviewQuery( "profile", @@ -136,8 +136,9 @@ export default function QuestionsListClient() { 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({ onSuccess: () => { diff --git a/src/app/request-accepted/request-accepted-client.tsx b/src/app/request-accepted/request-accepted-client.tsx index 94aa180..4eb6c6d 100644 --- a/src/app/request-accepted/request-accepted-client.tsx +++ b/src/app/request-accepted/request-accepted-client.tsx @@ -267,8 +267,9 @@ export default function RequestAcceptedClient() { } }, [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 () => { if (!isFetched || isFetching) { diff --git a/src/app/request-sent/request-sent-client.tsx b/src/app/request-sent/request-sent-client.tsx index 631df4d..b6e21f7 100644 --- a/src/app/request-sent/request-sent-client.tsx +++ b/src/app/request-sent/request-sent-client.tsx @@ -57,8 +57,9 @@ export default function RequestSentClient() { } }, [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(() => { if (!profile) return false; diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx index 778ad61..4ab90c7 100644 --- a/src/app/terms/page.tsx +++ b/src/app/terms/page.tsx @@ -18,8 +18,9 @@ export default function TermsRoute() { const router = useRouter(); 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(() => { if (!isLoading && profile) { diff --git a/src/hooks/use-habib-web-ready.ts b/src/hooks/use-habib-web-ready.ts index c6df617..ea5ac5e 100644 --- a/src/hooks/use-habib-web-ready.ts +++ b/src/hooks/use-habib-web-ready.ts @@ -20,7 +20,17 @@ import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache"; export function useHabibWebReady(ready: boolean) { useEffect(() => { 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; if (pathname) { @@ -35,6 +45,11 @@ export function useHabibWebReady(ready: boolean) { setCachedMarriageEntryPath(route); } } + + return () => { + cancelled = true; + cancelAnimationFrame(raf1); + }; } }, [ready]); }