Browse Source

refactor: centralize profile persistence and navigation syncing via reactive QueryClient subscription

master
mortezaei 2 weeks ago
parent
commit
b9e4b28f64
  1. 6
      src/app/new-match/new-match-client.tsx
  2. 27
      src/app/providers.tsx
  3. 11
      src/hooks/marriage/use-case-respond.ts
  4. 5
      src/hooks/marriage/use-match-start.ts
  5. 10
      src/hooks/marriage/use-profile-basic.ts
  6. 10
      src/hooks/marriage/use-profile-main.test.ts
  7. 18
      src/hooks/marriage/use-profile-main.ts

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

@ -596,8 +596,8 @@ export default function NewMatchClient() {
fullProfileObject: profile, 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; return;
} }
const targetPath = getSubmitPath(profile); const targetPath = getSubmitPath(profile);
@ -605,7 +605,7 @@ export default function NewMatchClient() {
console.log("🚀 [NewMatchClient] Redirecting to:", targetPath); console.log("🚀 [NewMatchClient] Redirecting to:", targetPath);
router.replace(localizePath(targetPath, locale)); router.replace(localizePath(targetPath, locale));
} }
}, [profile, locale, router, isFetched]);
}, [profile, locale, router]);
// Signal Flutter to lift its loading cover immediately on mount // Signal Flutter to lift its loading cover immediately on mount
useHabibWebReady(true); useHabibWebReady(true);

27
src/app/providers.tsx

@ -11,8 +11,14 @@ import HardwareBackBridge from "@/components/Componentes/hardware-back-bridge";
import SilentReloader from "@/components/Componentes/silent-reloader"; import SilentReloader from "@/components/Componentes/silent-reloader";
import { ViewPaddingsProvider } from "@/components/Componentes/view-paddings-provider"; 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 { marriageQueryKeys } from "@/hooks/marriage/query-keys";
import type { MarriageProfileResponse } from "@/hooks/marriage/types"; import type { MarriageProfileResponse } from "@/hooks/marriage/types";
import { saveScopedMarriageProfile } from "@/lib/user-scoped-storage";
// AppFocusReloader was extracted to SilentReloader in silent-reloader.tsx // 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) { if (initialProfile) {
console.log("⚡ [Providers] Initializing QueryClient cache with SSR initialProfile:", { console.log("⚡ [Providers] Initializing QueryClient cache with SSR initialProfile:", {
id: initialProfile.id, id: initialProfile.id,

11
src/hooks/marriage/use-case-respond.ts

@ -1,7 +1,9 @@
"use client"; "use client";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache";
import { http } from "@/lib/http"; import { http } from "@/lib/http";
import { saveScopedMarriageProfile } from "@/lib/user-scoped-storage";
import type { MutationOptions } from "./options"; import type { MutationOptions } from "./options";
import { pathParam } from "./path-param"; import { pathParam } from "./path-param";
import { marriageQueryKeys } from "./query-keys"; import { marriageQueryKeys } from "./query-keys";
@ -37,14 +39,17 @@ export function useRespondToMarriageCaseMutation(
mutationFn: (payload) => respondToMarriageCase(caseId, payload), mutationFn: (payload) => respondToMarriageCase(caseId, payload),
onSuccess: async (data, variables, onMutateResult, context) => { onSuccess: async (data, variables, onMutateResult, context) => {
if (variables?.action === "reject") { if (variables?.action === "reject") {
setCachedMarriageEntryPath("/finding-match");
queryClient.setQueryData(marriageQueryKeys.profile(), (old: any) => { queryClient.setQueryData(marriageQueryKeys.profile(), (old: any) => {
if (!old) return old; if (!old) return old;
return {
const updated = {
...old, ...old,
status: "waiting", status: "waiting",
active_case: null, active_case: null,
match_summary: null, match_summary: null,
}; };
saveScopedMarriageProfile(updated);
return updated;
}); });
} else if (data?.case) { } else if (data?.case) {
queryClient.setQueryData(marriageQueryKeys.profile(), (old: any) => { queryClient.setQueryData(marriageQueryKeys.profile(), (old: any) => {
@ -66,10 +71,12 @@ export function useRespondToMarriageCaseMutation(
: "completed" : "completed"
: "waiting", : "waiting",
}; };
return {
const updated = {
...old, ...old,
active_case: updatedActiveCase, active_case: updatedActiveCase,
}; };
saveScopedMarriageProfile(updated);
return updated;
}); });
} }

5
src/hooks/marriage/use-match-start.ts

@ -3,6 +3,7 @@
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache"; import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache";
import { http } from "@/lib/http"; import { http } from "@/lib/http";
import { saveScopedMarriageProfile } from "@/lib/user-scoped-storage";
import type { MutationOptions } from "./options"; import type { MutationOptions } from "./options";
import { marriageQueryKeys } from "./query-keys"; import { marriageQueryKeys } from "./query-keys";
import type { StartMarriageMatchResponse } from "./types"; import type { StartMarriageMatchResponse } from "./types";
@ -27,10 +28,12 @@ export function useStartMarriageMatchMutation(
setCachedMarriageEntryPath("/finding-match"); setCachedMarriageEntryPath("/finding-match");
queryClient.setQueryData(marriageQueryKeys.profile(), (old: any) => { queryClient.setQueryData(marriageQueryKeys.profile(), (old: any) => {
if (!old) return old; if (!old) return old;
return {
const updated = {
...old, ...old,
status: "waiting", status: "waiting",
}; };
saveScopedMarriageProfile(updated);
return updated;
}); });
Promise.all([ Promise.all([
queryClient.invalidateQueries({ queryClient.invalidateQueries({

10
src/hooks/marriage/use-profile-basic.ts

@ -2,6 +2,7 @@
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { http } from "@/lib/http"; import { http } from "@/lib/http";
import { saveScopedMarriageProfile } from "@/lib/user-scoped-storage";
import type { MutationOptions } from "./options"; import type { MutationOptions } from "./options";
import { marriageQueryKeys } from "./query-keys"; import { marriageQueryKeys } from "./query-keys";
import type { import type {
@ -29,6 +30,15 @@ export function useUpdateMarriageProfileBasicMutation(
...options, ...options,
mutationFn: updateMarriageProfileBasic, mutationFn: updateMarriageProfileBasic,
onSuccess: async (data, variables, onMutateResult, context) => { 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({ await queryClient.invalidateQueries({
queryKey: marriageQueryKeys.profile(), queryKey: marriageQueryKeys.profile(),
}); });

10
src/hooks/marriage/use-profile-main.test.ts

@ -168,16 +168,16 @@ describe("getInitialMarriageProfile", () => {
expect(initial?.match_summary?.public_info[1].value).toBe("Designer"); 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({ mocks.getScopedMarriageProfile.mockReturnValue({
id: 101, id: 101,
status: "match_found",
status: "waiting",
match_summary: null, match_summary: null,
}); });
mocks.getMarriageData.mockReturnValue({ mocks.getMarriageData.mockReturnValue({
id: 101, id: 101,
status: "match_found",
status: "in_case",
match_summary: { match_summary: {
id: 202, id: 202,
gender: "female", gender: "female",
@ -186,8 +186,8 @@ describe("getInitialMarriageProfile", () => {
}); });
const initial = 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", () => { it("returns undefined if neither Flutter nor local storage has data", () => {

18
src/hooks/marriage/use-profile-main.ts

@ -40,25 +40,15 @@ export function getInitialMarriageProfile(
persistedPublicInfoCount: persisted?.match_summary?.public_info?.length, 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) { if (persisted) {
console.log("⚡ [getInitialMarriageProfile] Using persisted profile from localStorage");
console.log("⚡ [getInitialMarriageProfile] Using persisted profile from localStorage:", persisted.status);
return persisted; 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) { if (flutterData) {
console.log("⚡ [getInitialMarriageProfile] Using Flutter initial data fallback");
console.log("⚡ [getInitialMarriageProfile] Using Flutter initial data fallback:", flutterData.status);
return flutterData; return flutterData;
} }

Loading…
Cancel
Save