diff --git a/src/app/new-match/new-match-client.tsx b/src/app/new-match/new-match-client.tsx index dd57331..d841eaf 100644 --- a/src/app/new-match/new-match-client.tsx +++ b/src/app/new-match/new-match-client.tsx @@ -596,8 +596,8 @@ export default function NewMatchClient() { fullProfileObject: profile, }); - if (!profile || !isFetched) { - console.log("⏳ [NewMatchClient] Skipping redirect because !profile or !isFetched (isFetched=", isFetched, ")"); + if (!profile) { + console.log("⏳ [NewMatchClient] Skipping redirect because !profile"); return; } const targetPath = getSubmitPath(profile); @@ -605,7 +605,7 @@ export default function NewMatchClient() { console.log("🚀 [NewMatchClient] Redirecting to:", targetPath); router.replace(localizePath(targetPath, locale)); } - }, [profile, locale, router, isFetched]); + }, [profile, locale, router]); // Signal Flutter to lift its loading cover immediately on mount useHabibWebReady(true); diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 0963f7f..c487999 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -11,8 +11,14 @@ import HardwareBackBridge from "@/components/Componentes/hardware-back-bridge"; import SilentReloader from "@/components/Componentes/silent-reloader"; import { ViewPaddingsProvider } from "@/components/Componentes/view-paddings-provider"; +import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache"; +import { + getSubmitPath, + hasCompletedMarriageProfileBasics, +} from "@/lib/get-submit-path"; import { marriageQueryKeys } from "@/hooks/marriage/query-keys"; import type { MarriageProfileResponse } from "@/hooks/marriage/types"; +import { saveScopedMarriageProfile } from "@/lib/user-scoped-storage"; // AppFocusReloader was extracted to SilentReloader in silent-reloader.tsx @@ -43,6 +49,27 @@ export default function Providers({ children, initialProfile }: ProvidersProps) }, }, }); + + // Centralized reactive profile subscriber: automatically syncs every profile state update + // to LocalStorage and Flutter native entry path bridge without requiring manual calls in mutations. + qc.getQueryCache().subscribe((event) => { + if ( + (event?.type === "updated" || event?.type === "added") && + Array.isArray(event.query.queryKey) && + event.query.queryKey[0] === "marriage" && + event.query.queryKey[1] === "profile" && + event.query.state.data + ) { + const profile = event.query.state.data as MarriageProfileResponse; + if (profile && typeof profile === "object") { + saveScopedMarriageProfile(profile); + if (hasCompletedMarriageProfileBasics(profile)) { + setCachedMarriageEntryPath(getSubmitPath(profile)); + } + } + } + }); + if (initialProfile) { console.log("⚡ [Providers] Initializing QueryClient cache with SSR initialProfile:", { id: initialProfile.id, diff --git a/src/hooks/marriage/use-case-respond.ts b/src/hooks/marriage/use-case-respond.ts index 4f0e9e3..cbfcc92 100644 --- a/src/hooks/marriage/use-case-respond.ts +++ b/src/hooks/marriage/use-case-respond.ts @@ -1,7 +1,9 @@ "use client"; import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache"; import { http } from "@/lib/http"; +import { saveScopedMarriageProfile } from "@/lib/user-scoped-storage"; import type { MutationOptions } from "./options"; import { pathParam } from "./path-param"; import { marriageQueryKeys } from "./query-keys"; @@ -37,14 +39,17 @@ export function useRespondToMarriageCaseMutation( mutationFn: (payload) => respondToMarriageCase(caseId, payload), onSuccess: async (data, variables, onMutateResult, context) => { if (variables?.action === "reject") { + setCachedMarriageEntryPath("/finding-match"); queryClient.setQueryData(marriageQueryKeys.profile(), (old: any) => { if (!old) return old; - return { + const updated = { ...old, status: "waiting", active_case: null, match_summary: null, }; + saveScopedMarriageProfile(updated); + return updated; }); } else if (data?.case) { queryClient.setQueryData(marriageQueryKeys.profile(), (old: any) => { @@ -66,10 +71,12 @@ export function useRespondToMarriageCaseMutation( : "completed" : "waiting", }; - return { + const updated = { ...old, active_case: updatedActiveCase, }; + saveScopedMarriageProfile(updated); + return updated; }); } diff --git a/src/hooks/marriage/use-match-start.ts b/src/hooks/marriage/use-match-start.ts index 600c5ac..6cecb8f 100644 --- a/src/hooks/marriage/use-match-start.ts +++ b/src/hooks/marriage/use-match-start.ts @@ -3,6 +3,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache"; import { http } from "@/lib/http"; +import { saveScopedMarriageProfile } from "@/lib/user-scoped-storage"; import type { MutationOptions } from "./options"; import { marriageQueryKeys } from "./query-keys"; import type { StartMarriageMatchResponse } from "./types"; @@ -27,10 +28,12 @@ export function useStartMarriageMatchMutation( setCachedMarriageEntryPath("/finding-match"); queryClient.setQueryData(marriageQueryKeys.profile(), (old: any) => { if (!old) return old; - return { + const updated = { ...old, status: "waiting", }; + saveScopedMarriageProfile(updated); + return updated; }); Promise.all([ queryClient.invalidateQueries({ diff --git a/src/hooks/marriage/use-profile-basic.ts b/src/hooks/marriage/use-profile-basic.ts index 9318d2a..6920004 100644 --- a/src/hooks/marriage/use-profile-basic.ts +++ b/src/hooks/marriage/use-profile-basic.ts @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { http } from "@/lib/http"; +import { saveScopedMarriageProfile } from "@/lib/user-scoped-storage"; import type { MutationOptions } from "./options"; import { marriageQueryKeys } from "./query-keys"; import type { @@ -29,6 +30,15 @@ export function useUpdateMarriageProfileBasicMutation( ...options, mutationFn: updateMarriageProfileBasic, onSuccess: async (data, variables, onMutateResult, context) => { + queryClient.setQueryData(marriageQueryKeys.profile(), (old: any) => { + if (!old) return data; + const updated = { + ...old, + ...data, + }; + saveScopedMarriageProfile(updated); + return updated; + }); await queryClient.invalidateQueries({ queryKey: marriageQueryKeys.profile(), }); diff --git a/src/hooks/marriage/use-profile-main.test.ts b/src/hooks/marriage/use-profile-main.test.ts index 7846aa7..6d99218 100644 --- a/src/hooks/marriage/use-profile-main.test.ts +++ b/src/hooks/marriage/use-profile-main.test.ts @@ -168,16 +168,16 @@ describe("getInitialMarriageProfile", () => { expect(initial?.match_summary?.public_info[1].value).toBe("Designer"); }); - it("prefers Flutter data if Flutter has match_summary and local storage does not", () => { + it("prefers persisted localStorage profile (e.g. status: waiting after reject) over stale Flutter data", () => { mocks.getScopedMarriageProfile.mockReturnValue({ id: 101, - status: "match_found", + status: "waiting", match_summary: null, }); mocks.getMarriageData.mockReturnValue({ id: 101, - status: "match_found", + status: "in_case", match_summary: { id: 202, gender: "female", @@ -186,8 +186,8 @@ describe("getInitialMarriageProfile", () => { }); const initial = getInitialMarriageProfile(); - expect(initial?.match_summary?.id).toBe(202); - expect(initial?.match_summary?.public_info[0].value).toBe("Zahra"); + expect(initial?.status).toBe("waiting"); + expect(initial?.match_summary).toBeNull(); }); it("returns undefined if neither Flutter nor local storage has data", () => { diff --git a/src/hooks/marriage/use-profile-main.ts b/src/hooks/marriage/use-profile-main.ts index 9adf54c..02c5462 100644 --- a/src/hooks/marriage/use-profile-main.ts +++ b/src/hooks/marriage/use-profile-main.ts @@ -40,25 +40,15 @@ export function getInitialMarriageProfile( persistedPublicInfoCount: persisted?.match_summary?.public_info?.length, }); - // Priority 1: If Flutter provided match_summary (e.g. active match from user/me), ALWAYS prefer Flutter data - if (flutterData?.match_summary) { - console.log("⚡ [getInitialMarriageProfile] Using Flutter initial data (has match_summary)"); - return { - ...flutterData, - id: flutterData.id || persisted?.id || 0, - status: flutterData.status || persisted?.status || "in_case", - }; - } - - // Priority 2: Use locally persisted profile from previous successful server response + // Priority 1: Use locally persisted profile from previous user actions & server responses if (persisted) { - console.log("⚡ [getInitialMarriageProfile] Using persisted profile from localStorage"); + console.log("⚡ [getInitialMarriageProfile] Using persisted profile from localStorage:", persisted.status); return persisted; } - // Priority 3: Fallback to Flutter native bridge data + // Priority 2: Fallback to Flutter native bridge data if localStorage is empty (e.g. first launch on new device) if (flutterData) { - console.log("⚡ [getInitialMarriageProfile] Using Flutter initial data fallback"); + console.log("⚡ [getInitialMarriageProfile] Using Flutter initial data fallback:", flutterData.status); return flutterData; }