diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c245277..73de8d5 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -113,7 +113,7 @@ export default function RootLayout({ } }, get: function() { - return this._habib_token || readCookie(HABIB_TOKEN_COOKIE) || sessionStorage.getItem(HABIB_TOKEN_COOKIE) || '${process.env.NEXT_PUBLIC_DEFAULT_TOKEN || "545f61bb3e061ccb9b19f84715eb1b1ed4740331"}'; + return this._habib_token || readCookie(HABIB_TOKEN_COOKIE) || sessionStorage.getItem(HABIB_TOKEN_COOKIE) || '${process.env.NEXT_PUBLIC_DEFAULT_TOKEN || "f3a7543b44ef0a713d1ee0d4f7866b3825cf1308"}'; } }); } diff --git a/src/app/new-match/page.tsx b/src/app/new-match/page.tsx index 6fd31e7..719f78e 100644 --- a/src/app/new-match/page.tsx +++ b/src/app/new-match/page.tsx @@ -1,7 +1,10 @@ "use client"; import Image from "next/image"; -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; +import { useRouter } from "next/navigation"; +import { localizePath } from "@/translations/config"; +import { getSubmitPath } from "@/lib/get-submit-path"; import { FaLock } from "react-icons/fa6"; import AdvisorActionsCard from "@/components/ui/advisor-actions-card"; import NavigationButton from "@/components/ui/navigation-button"; @@ -183,9 +186,21 @@ function FieldLine({ field }: { field: DisplayField }) { } export default function NewMatchPage() { - const { dictionary: t } = useI18n(); + const router = useRouter(); + const { dictionary: t, locale } = useI18n(); const { top, bottom } = useViewPaddings(); const { data: profile, isError, isLoading } = useMarriageProfileQuery(); + + useEffect(() => { + if (!profile) { + return; + } + const targetPath = getSubmitPath(profile); + if (targetPath !== "/new-match") { + router.replace(localizePath(targetPath, locale)); + } + }, [profile, locale, router]); + const matchSummary = profile?.match_summary ?? null; const matchDisplay = useMatchSummaryDisplay(matchSummary); const pairedFields = [matchDisplay.age, matchDisplay.city].filter( diff --git a/src/app/new-match/profile/page.tsx b/src/app/new-match/profile/page.tsx index a48e0a3..5dccf83 100644 --- a/src/app/new-match/profile/page.tsx +++ b/src/app/new-match/profile/page.tsx @@ -2,7 +2,7 @@ import Image from "next/image"; import { useRouter } from "next/navigation"; -import { useMemo, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import Button from "@/components/ui/button"; import DismissReasonSheet from "@/components/ui/dismiss-reason-sheet"; import FemaleConsentSheet from "@/components/ui/female-consent-sheet"; @@ -19,6 +19,7 @@ import type { } from "@/hooks/marriage/types"; import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; +import { getSubmitPath } from "@/lib/get-submit-path"; import { localizePath } from "@/translations/config"; import { useI18n } from "@/translations/provider"; @@ -150,7 +151,18 @@ export default function NewMatchProfilePage() { const [isRejectSheetOpen, setIsRejectSheetOpen] = useState(false); const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] = useState(false); - const { data: profile } = useMarriageProfileQuery(); + const { data: profile, refetch: refetchProfile } = useMarriageProfileQuery(); + + useEffect(() => { + if (!profile) { + return; + } + const targetPath = getSubmitPath(profile); + if (targetPath !== "/new-match") { + router.replace(localizePath(targetPath, locale)); + } + }, [profile, locale, router]); + const caseId = profile?.active_case?.case_id; const caseStatus = profile?.active_case?.status; const isFemaleProfile = profile?.gender === "female"; @@ -158,11 +170,9 @@ export default function NewMatchProfilePage() { const respondMutation = useRespondToMarriageCaseMutation(caseId ?? "", { onSuccess: async (_, variables) => { if (variables.action === "accept") { - if (isFemaleProfile) { - router.replace(localizePath("/request-accepted", locale)); - } else { - router.replace(localizePath("/request-sent", locale)); - } + const { data: updatedProfile } = await refetchProfile(); + const nextPath = getSubmitPath(updatedProfile); + router.replace(localizePath(nextPath, locale)); return; } diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 0220031..8554f6b 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -1,9 +1,55 @@ "use client"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { type ReactNode, useState } from "react"; +import { QueryClient, QueryClientProvider, useQueryClient } from "@tanstack/react-query"; +import { type ReactNode, useState, useEffect } from "react"; import { ViewPaddingsProvider } from "@/components/utils/view-paddings-provider"; +function AppFocusReloader({ children }: { children: ReactNode }) { + const queryClient = useQueryClient(); + + useEffect(() => { + if (typeof window !== "undefined") { + (window as any).__queryClient = queryClient; + } + + const handleReload = () => { + // Invalidate all active queries so fresh data is reloaded from backend + queryClient.invalidateQueries(); + }; + + const handleVisibilityChange = () => { + if (document.visibilityState === "visible") { + handleReload(); + } + }; + + const handlePageShow = () => { + handleReload(); + }; + + window.addEventListener("focus", handleReload); + window.addEventListener("pageshow", handlePageShow); + document.addEventListener("visibilitychange", handleVisibilityChange); + + // Listen for Flutter response events if available + const win = window as any; + const unbindFlutter = typeof win.addFlutterResponseListener === "function" + ? win.addFlutterResponseListener(() => handleReload()) + : null; + + return () => { + window.removeEventListener("focus", handleReload); + window.removeEventListener("pageshow", handlePageShow); + document.removeEventListener("visibilitychange", handleVisibilityChange); + if (typeof unbindFlutter === "function") { + unbindFlutter(); + } + }; + }, [queryClient]); + + return <>{children}; +} + type ProvidersProps = { children: ReactNode; }; @@ -14,10 +60,12 @@ export default function Providers({ children }: ProvidersProps) { new QueryClient({ defaultOptions: { queries: { - refetchOnWindowFocus: false, + refetchOnWindowFocus: "always", + refetchOnMount: "always", + refetchOnReconnect: "always", retry: 1, - staleTime: 5 * 60 * 1000, // 5 minutes - gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime) + staleTime: 0, // Always consider queries stale to ensure fresh data from backend on every page load + gcTime: 10 * 60 * 1000, // 10 minutes }, }, }), @@ -25,8 +73,11 @@ export default function Providers({ children }: ProvidersProps) { return ( - - {children} + + + {children} + ); } + diff --git a/src/hooks/marriage/use-case-respond.ts b/src/hooks/marriage/use-case-respond.ts index aa6c881..ae6d38a 100644 --- a/src/hooks/marriage/use-case-respond.ts +++ b/src/hooks/marriage/use-case-respond.ts @@ -36,6 +36,33 @@ export function useRespondToMarriageCaseMutation( ...options, mutationFn: (payload) => respondToMarriageCase(caseId, payload), onSuccess: async (data, variables, onMutateResult, context) => { + if (data?.case) { + queryClient.setQueryData(marriageQueryKeys.profile(), (old: any) => { + if (!old) return old; + const updatedActiveCase = { + ...old.active_case, + case_id: data.case.id, + status: data.case.status, + my_action: + data.case.status === "male_accepted" + ? old.gender === "female" + ? "pending" + : "waiting" + : data.case.status === "female_accepted" || + data.case.status === "payment_pending" || + data.case.status === "payment_done" + ? old.gender === "female" + ? "waiting" + : "completed" + : "waiting", + }; + return { + ...old, + active_case: updatedActiveCase, + }; + }); + } + await Promise.all([ queryClient.refetchQueries({ queryKey: marriageQueryKeys.profile(), diff --git a/src/hooks/marriage/use-profile-main.ts b/src/hooks/marriage/use-profile-main.ts index 357a741..483949d 100644 --- a/src/hooks/marriage/use-profile-main.ts +++ b/src/hooks/marriage/use-profile-main.ts @@ -18,6 +18,9 @@ export function useMarriageProfileQuery( options?: QueryOptions, ) { return useQuery({ + staleTime: 0, + refetchOnMount: "always", + refetchOnWindowFocus: true, ...options, queryFn: getMarriageProfile, queryKey: marriageQueryKeys.profile(), diff --git a/src/lib/auth-bridge.ts b/src/lib/auth-bridge.ts index c980039..92a298d 100644 --- a/src/lib/auth-bridge.ts +++ b/src/lib/auth-bridge.ts @@ -141,6 +141,9 @@ class AuthBridge { this.isReady = true; this.resolvePending(token); this.notifyReady(); + if (typeof window !== "undefined" && (window as any).__queryClient) { + (window as any).__queryClient.invalidateQueries(); + } } private resolvePending(token: string | null) { @@ -210,7 +213,7 @@ class AuthBridge { getClientCookie(TOKEN_COOKIE_NAME) ?? getClientCookie("habib_token") ?? process.env.NEXT_PUBLIC_DEFAULT_TOKEN ?? - "545f61bb3e061ccb9b19f84715eb1b1ed4740331" + "f3a7543b44ef0a713d1ee0d4f7866b3825cf1308" ); } diff --git a/src/lib/http.ts b/src/lib/http.ts index 67543fd..68ca3ae 100644 --- a/src/lib/http.ts +++ b/src/lib/http.ts @@ -74,7 +74,7 @@ http.interceptors.request.use((config) => { authBridge.getToken() ?? getClientCookie("HABIB_TOKEN") ?? process.env.NEXT_PUBLIC_DEFAULT_TOKEN ?? - "545f61bb3e061ccb9b19f84715eb1b1ed4740331"; + "f3a7543b44ef0a713d1ee0d4f7866b3825cf1308"; if (token) { config.headers.Authorization = `Token ${token}`;