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() {
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";
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(

24
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;
}

65
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 (
<QueryClientProvider client={queryClient}>
<ViewPaddingsProvider />
{children}
<AppFocusReloader>
<ViewPaddingsProvider />
{children}
</AppFocusReloader>
</QueryClientProvider>
);
}

27
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(),

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

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

5
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"
);
}

2
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}`;

Loading…
Cancel
Save