Browse Source

fix: prevent premature redirects using stale initialData and improve payment error handling for Habcoin transactions

master
mortezaei 2 weeks ago
parent
commit
e06f5bd344
  1. 2
      src/app/api/proxy/route.ts
  2. 22
      src/app/new-match/new-match-client.tsx
  3. 20
      src/app/request-accepted/request-accepted-client.tsx
  4. 7
      src/app/request-sent/request-sent-client.tsx
  5. 5
      src/hooks/marriage/use-profile-main.ts

2
src/app/api/proxy/route.ts

@ -150,7 +150,7 @@ function getRequestHeaders(request: NextRequest, targetUrl: URL) {
const securityKey = const securityKey =
process.env.NEXT_PUBLIC_SECURITY_KEY || process.env.NEXT_PUBLIC_SECURITY_KEY ||
process.env.SECURITY_KEY || process.env.SECURITY_KEY ||
"t5yugymks5458fd4ghfg6h6";
"t5yugymks5458fd4ghfg6h6fg";
if (securityKey && !headers.has("security-key")) { if (securityKey && !headers.has("security-key")) {
headers.set("security-key", securityKey); headers.set("security-key", securityKey);
} }

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

@ -489,7 +489,8 @@ export default function NewMatchClient() {
useMarriageAdvisorsOverlay(); useMarriageAdvisorsOverlay();
const { isProfileOpen, openProfile, closeProfile } = useMatchProfileOverlay(); const { isProfileOpen, openProfile, closeProfile } = useMatchProfileOverlay();
const [isOpeningProfile, setIsOpeningProfile] = useState(false); const [isOpeningProfile, setIsOpeningProfile] = useState(false);
const { data: profile, isError, isLoading } = useMarriageProfileQuery();
const { data: profile, isError, isLoading, isFetched } =
useMarriageProfileQuery();
const [isPaymentSheetOpen, setIsPaymentSheetOpen] = useState(false); const [isPaymentSheetOpen, setIsPaymentSheetOpen] = useState(false);
const [isDeclineConfirmOpen, setIsDeclineConfirmOpen] = useState(false); const [isDeclineConfirmOpen, setIsDeclineConfirmOpen] = useState(false);
const [paymentError, setPaymentError] = useState<string | null>(null); const [paymentError, setPaymentError] = useState<string | null>(null);
@ -523,8 +524,7 @@ export default function NewMatchClient() {
}); });
const handlePayment = async () => { const handlePayment = async () => {
const recommendedPlanId = profile?.recommended_plan?.id;
if (!recommendedPlanId) return;
const recommendedPlanId = profile?.recommended_plan?.id ?? 1;
try { try {
setPaymentError(null); setPaymentError(null);
@ -539,9 +539,13 @@ export default function NewMatchClient() {
} catch (err: any) { } catch (err: any) {
console.error("Payment failed", err); console.error("Payment failed", err);
const msg = const msg =
err?.response?.data?.error || err?.message || "Payment failed";
err?.response?.data?.error ||
err?.response?.data?.detail ||
err?.response?.data?.message ||
err?.message ||
"Payment failed";
const modalT = (t as any).paymentModal || {}; const modalT = (t as any).paymentModal || {};
if (msg === "Not enough coins") {
if (msg === "Not enough coins" || msg?.includes?.("Not enough coins")) {
setIsInsufficientCoins(true); setIsInsufficientCoins(true);
setPaymentError( setPaymentError(
modalT.insufficientCoins || modalT.insufficientCoins ||
@ -575,6 +579,7 @@ export default function NewMatchClient() {
useEffect(() => { useEffect(() => {
console.log("🔍 [NewMatchClient] Current React Query Profile State:", { console.log("🔍 [NewMatchClient] Current React Query Profile State:", {
isLoading, isLoading,
isFetched,
isError, isError,
hasProfile: Boolean(profile), hasProfile: Boolean(profile),
profileId: profile?.id, profileId: profile?.id,
@ -589,11 +594,16 @@ export default function NewMatchClient() {
if (!profile) { if (!profile) {
return; return;
} }
// Don't redirect based on stale native-injected initialData that may be
// missing active_case; wait for the first real server response.
if (!isFetched && profile.status === "in_case" && !profile.active_case) {
return;
}
const targetPath = getSubmitPath(profile); const targetPath = getSubmitPath(profile);
if (targetPath !== "/new-match") { if (targetPath !== "/new-match") {
router.replace(localizePath(targetPath, locale)); router.replace(localizePath(targetPath, locale));
} }
}, [profile, locale, router, isLoading, isError]);
}, [profile, locale, router, isLoading, isFetched, isError]);
// Signal Flutter to lift its loading cover immediately with 0ms latency. // Signal Flutter to lift its loading cover immediately with 0ms latency.
useHabibWebReady(true); useHabibWebReady(true);

20
src/app/request-accepted/request-accepted-client.tsx

@ -243,7 +243,7 @@ export default function RequestAcceptedClient() {
useState(false); useState(false);
const [showPaymentSuccessToast, setShowPaymentSuccessToast] = useState(false); const [showPaymentSuccessToast, setShowPaymentSuccessToast] = useState(false);
const [isOpeningProfile, setIsOpeningProfile] = useState(false); const [isOpeningProfile, setIsOpeningProfile] = useState(false);
const { data: profile, isLoading } = useMarriageProfileQuery();
const { data: profile, isLoading, isFetched } = useMarriageProfileQuery();
const isFemaleProfile = profile?.gender === "female"; const isFemaleProfile = profile?.gender === "female";
const contactSharedAtStr = profile?.active_case?.contact_shared_at; const contactSharedAtStr = profile?.active_case?.contact_shared_at;
@ -252,11 +252,14 @@ export default function RequestAcceptedClient() {
if (!profile || noContactReportedSuccess) { if (!profile || noContactReportedSuccess) {
return; return;
} }
if (!isFetched && profile.status === "in_case" && !profile.active_case) {
return;
}
const targetPath = getSubmitPath(profile); const targetPath = getSubmitPath(profile);
if (targetPath !== "/request-accepted") { if (targetPath !== "/request-accepted") {
router.replace(localizePath(targetPath, locale)); router.replace(localizePath(targetPath, locale));
} }
}, [profile, router, locale, noContactReportedSuccess]);
}, [profile, isFetched, router, locale, noContactReportedSuccess]);
// Signal Flutter to lift its loading cover immediately with 0ms latency. // Signal Flutter to lift its loading cover immediately with 0ms latency.
useHabibWebReady(true); useHabibWebReady(true);
@ -368,7 +371,8 @@ export default function RequestAcceptedClient() {
}; };
const handlePayment = async () => { const handlePayment = async () => {
if (!recommendedPlanId || paymentMutation.isPending) {
const planId = recommendedPlanId ?? 1;
if (paymentMutation.isPending) {
return; return;
} }
@ -376,7 +380,7 @@ export default function RequestAcceptedClient() {
setPaymentError(null); setPaymentError(null);
setIsInsufficientCoins(false); setIsInsufficientCoins(false);
const paymentResponse = const paymentResponse =
await paymentMutation.mutateAsync(recommendedPlanId);
await paymentMutation.mutateAsync(planId);
const paymentUrl = extractHabcoinPaymentUrl(paymentResponse); const paymentUrl = extractHabcoinPaymentUrl(paymentResponse);
if (paymentUrl) { if (paymentUrl) {
@ -394,8 +398,12 @@ export default function RequestAcceptedClient() {
} catch (err: any) { } catch (err: any) {
console.error("Habcoin payment request failed", err); console.error("Habcoin payment request failed", err);
const msg = const msg =
err?.response?.data?.error || err?.message || "Payment failed";
if (msg === "Not enough coins") {
err?.response?.data?.error ||
err?.response?.data?.detail ||
err?.response?.data?.message ||
err?.message ||
"Payment failed";
if (msg === "Not enough coins" || msg?.includes?.("Not enough coins")) {
setIsInsufficientCoins(true); setIsInsufficientCoins(true);
setPaymentError( setPaymentError(
t["Insufficient coin balance. Please recharge your account."] || t["Insufficient coin balance. Please recharge your account."] ||

7
src/app/request-sent/request-sent-client.tsx

@ -34,7 +34,7 @@ export default function RequestSentClient() {
const { isProfileOpen, openProfile, closeProfile } = const { isProfileOpen, openProfile, closeProfile } =
useMatchProfileOverlay(); useMatchProfileOverlay();
const [isOpeningProfile, setIsOpeningProfile] = useState(false); const [isOpeningProfile, setIsOpeningProfile] = useState(false);
const { data: profile, isLoading } = useMarriageProfileQuery({
const { data: profile, isLoading, isFetched } = useMarriageProfileQuery({
refetchInterval: 3000, refetchInterval: 3000,
}); });
@ -42,11 +42,14 @@ export default function RequestSentClient() {
if (!profile) { if (!profile) {
return; return;
} }
if (!isFetched && profile.status === "in_case" && !profile.active_case) {
return;
}
const targetPath = getSubmitPath(profile); const targetPath = getSubmitPath(profile);
if (targetPath !== "/request-sent") { if (targetPath !== "/request-sent") {
router.replace(localizePath(targetPath, locale)); router.replace(localizePath(targetPath, locale));
} }
}, [profile, locale, router]);
}, [profile, isFetched, locale, router]);
// Signal Flutter to lift its loading cover immediately with 0ms latency. // Signal Flutter to lift its loading cover immediately with 0ms latency.
useHabibWebReady(true); useHabibWebReady(true);

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

@ -79,6 +79,7 @@ export function useMarriageProfileQuery<TData = MarriageProfileResponse>(
return { return {
...old, ...old,
...initial, ...initial,
active_case: initial.active_case ?? old.active_case,
match_summary: initial.match_summary ?? old.match_summary, match_summary: initial.match_summary ?? old.match_summary,
}; };
}); });
@ -95,6 +96,10 @@ export function useMarriageProfileQuery<TData = MarriageProfileResponse>(
staleTime: 5 * 60 * 1000, staleTime: 5 * 60 * 1000,
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
initialData: getInitialMarriageProfile as any, initialData: getInitialMarriageProfile as any,
// Native-injected initialData is a snapshot from app-launch time and can be
// incomplete (e.g. missing active_case). Marking it stale forces a fresh
// fetch on mount instead of trusting it for the whole staleTime window.
initialDataUpdatedAt: 0,
...options, ...options,
queryFn: getMarriageProfile, queryFn: getMarriageProfile,
queryKey: marriageQueryKeys.profile(), queryKey: marriageQueryKeys.profile(),

Loading…
Cancel
Save