Browse Source

feat: implement auth bridge, marriage hooks, and HTTP proxy infrastructure

front-test-2
ghorbani 4 weeks ago
parent
commit
1f83522af5
  1. 2
      src/app/layout.tsx
  2. 19
      src/app/new-match/page.tsx
  3. 24
      src/app/new-match/profile/page.tsx
  4. 65
      src/app/providers.tsx
  5. 27
      src/hooks/marriage/use-case-respond.ts
  6. 3
      src/hooks/marriage/use-profile-main.ts
  7. 5
      src/lib/auth-bridge.ts
  8. 2
      src/lib/http.ts

2
src/app/layout.tsx

@ -113,7 +113,7 @@ export default function RootLayout({
} }
}, },
get: function() { 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"}';
} }
}); });
} }

19
src/app/new-match/page.tsx

@ -1,7 +1,10 @@
"use client"; "use client";
import Image from "next/image"; 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 { FaLock } from "react-icons/fa6";
import AdvisorActionsCard from "@/components/ui/advisor-actions-card"; import AdvisorActionsCard from "@/components/ui/advisor-actions-card";
import NavigationButton from "@/components/ui/navigation-button"; import NavigationButton from "@/components/ui/navigation-button";
@ -183,9 +186,21 @@ function FieldLine({ field }: { field: DisplayField }) {
} }
export default function NewMatchPage() { export default function NewMatchPage() {
const { dictionary: t } = useI18n();
const router = useRouter();
const { dictionary: t, locale } = useI18n();
const { top, bottom } = useViewPaddings(); const { top, bottom } = useViewPaddings();
const { data: profile, isError, isLoading } = useMarriageProfileQuery(); 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 matchSummary = profile?.match_summary ?? null;
const matchDisplay = useMatchSummaryDisplay(matchSummary); const matchDisplay = useMatchSummaryDisplay(matchSummary);
const pairedFields = [matchDisplay.age, matchDisplay.city].filter( const pairedFields = [matchDisplay.age, matchDisplay.city].filter(

24
src/app/new-match/profile/page.tsx

@ -2,7 +2,7 @@
import Image from "next/image"; import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";
import Button from "@/components/ui/button"; import Button from "@/components/ui/button";
import DismissReasonSheet from "@/components/ui/dismiss-reason-sheet"; import DismissReasonSheet from "@/components/ui/dismiss-reason-sheet";
import FemaleConsentSheet from "@/components/ui/female-consent-sheet"; import FemaleConsentSheet from "@/components/ui/female-consent-sheet";
@ -19,6 +19,7 @@ import type {
} from "@/hooks/marriage/types"; } from "@/hooks/marriage/types";
import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond"; import { useRespondToMarriageCaseMutation } from "@/hooks/marriage/use-case-respond";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import { getSubmitPath } from "@/lib/get-submit-path";
import { localizePath } from "@/translations/config"; import { localizePath } from "@/translations/config";
import { useI18n } from "@/translations/provider"; import { useI18n } from "@/translations/provider";
@ -150,7 +151,18 @@ export default function NewMatchProfilePage() {
const [isRejectSheetOpen, setIsRejectSheetOpen] = useState(false); const [isRejectSheetOpen, setIsRejectSheetOpen] = useState(false);
const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] = const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] =
useState(false); 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 caseId = profile?.active_case?.case_id;
const caseStatus = profile?.active_case?.status; const caseStatus = profile?.active_case?.status;
const isFemaleProfile = profile?.gender === "female"; const isFemaleProfile = profile?.gender === "female";
@ -158,11 +170,9 @@ export default function NewMatchProfilePage() {
const respondMutation = useRespondToMarriageCaseMutation(caseId ?? "", { const respondMutation = useRespondToMarriageCaseMutation(caseId ?? "", {
onSuccess: async (_, variables) => { onSuccess: async (_, variables) => {
if (variables.action === "accept") { 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; return;
} }

65
src/app/providers.tsx

@ -1,9 +1,55 @@
"use client"; "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"; 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 = { type ProvidersProps = {
children: ReactNode; children: ReactNode;
}; };
@ -14,10 +60,12 @@ export default function Providers({ children }: ProvidersProps) {
new QueryClient({ new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
refetchOnWindowFocus: false,
refetchOnWindowFocus: "always",
refetchOnMount: "always",
refetchOnReconnect: "always",
retry: 1, 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 ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<ViewPaddingsProvider />
{children}
<AppFocusReloader>
<ViewPaddingsProvider />
{children}
</AppFocusReloader>
</QueryClientProvider> </QueryClientProvider>
); );
} }

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

@ -36,6 +36,33 @@ export function useRespondToMarriageCaseMutation(
...options, ...options,
mutationFn: (payload) => respondToMarriageCase(caseId, payload), mutationFn: (payload) => respondToMarriageCase(caseId, payload),
onSuccess: async (data, variables, onMutateResult, context) => { 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([ await Promise.all([
queryClient.refetchQueries({ queryClient.refetchQueries({
queryKey: marriageQueryKeys.profile(), queryKey: marriageQueryKeys.profile(),

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

@ -18,6 +18,9 @@ export function useMarriageProfileQuery<TData = MarriageProfileResponse>(
options?: QueryOptions<MarriageProfileResponse, TData>, options?: QueryOptions<MarriageProfileResponse, TData>,
) { ) {
return useQuery({ return useQuery({
staleTime: 0,
refetchOnMount: "always",
refetchOnWindowFocus: true,
...options, ...options,
queryFn: getMarriageProfile, queryFn: getMarriageProfile,
queryKey: marriageQueryKeys.profile(), queryKey: marriageQueryKeys.profile(),

5
src/lib/auth-bridge.ts

@ -141,6 +141,9 @@ class AuthBridge {
this.isReady = true; this.isReady = true;
this.resolvePending(token); this.resolvePending(token);
this.notifyReady(); this.notifyReady();
if (typeof window !== "undefined" && (window as any).__queryClient) {
(window as any).__queryClient.invalidateQueries();
}
} }
private resolvePending(token: string | null) { private resolvePending(token: string | null) {
@ -210,7 +213,7 @@ class AuthBridge {
getClientCookie(TOKEN_COOKIE_NAME) ?? getClientCookie(TOKEN_COOKIE_NAME) ??
getClientCookie("habib_token") ?? getClientCookie("habib_token") ??
process.env.NEXT_PUBLIC_DEFAULT_TOKEN ?? process.env.NEXT_PUBLIC_DEFAULT_TOKEN ??
"545f61bb3e061ccb9b19f84715eb1b1ed4740331"
"f3a7543b44ef0a713d1ee0d4f7866b3825cf1308"
); );
} }

2
src/lib/http.ts

@ -74,7 +74,7 @@ http.interceptors.request.use((config) => {
authBridge.getToken() ?? authBridge.getToken() ??
getClientCookie("HABIB_TOKEN") ?? getClientCookie("HABIB_TOKEN") ??
process.env.NEXT_PUBLIC_DEFAULT_TOKEN ?? process.env.NEXT_PUBLIC_DEFAULT_TOKEN ??
"545f61bb3e061ccb9b19f84715eb1b1ed4740331";
"f3a7543b44ef0a713d1ee0d4f7866b3825cf1308";
if (token) { if (token) {
config.headers.Authorization = `Token ${token}`; config.headers.Authorization = `Token ${token}`;

Loading…
Cancel
Save