From 7225e65546449c208409377c3c40782ba34b2bdb Mon Sep 17 00:00:00 2001 From: mortezaei Date: Sat, 22 Aug 2026 03:09:46 +0330 Subject: [PATCH] fix --- .../Componentes/question-answer-storage.tsx | 39 ++++++- .../Componentes/question-birthplace.tsx | 31 ++++-- .../Componentes/question-number.tsx | 6 + .../Componentes/question-phone.test.tsx | 29 ++--- src/components/Componentes/question-sheet.tsx | 11 +- src/hooks/marriage/types.ts | 6 + src/lib/geo-region.ts | 104 +++++------------- src/lib/marriage-field-formatter.ts | 19 +++- 8 files changed, 128 insertions(+), 117 deletions(-) diff --git a/src/components/Componentes/question-answer-storage.tsx b/src/components/Componentes/question-answer-storage.tsx index 149470e..b8be8f9 100644 --- a/src/components/Componentes/question-answer-storage.tsx +++ b/src/components/Componentes/question-answer-storage.tsx @@ -17,6 +17,7 @@ import type { MarriageField, MarriageFieldValue, MarriagePhoneFieldValue, + MarriageBirthplaceFieldValue, UpdateMarriageSectionDataPayload, } from "@/hooks/marriage/types"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; @@ -80,7 +81,7 @@ export function getQuestionAnswersStorageKey(slug: string) { } export function hasQuestionAnswerValue(value: MarriageFieldValue) { - if (value === null) { + if (value === null || value === undefined) { return false; } @@ -88,9 +89,40 @@ export function hasQuestionAnswerValue(value: MarriageFieldValue) { return value.trim().length > 0; } + if (typeof value === "object") { + if (Array.isArray(value)) { + return value.length > 0; + } + const phone = value as Partial; + if ( + typeof phone.countryCode === "string" || + typeof phone.phoneNumber === "string" + ) { + return Boolean(phone.countryCode?.trim() || phone.phoneNumber?.trim()); + } + const bp = value as Partial; + if (typeof bp.country === "string" || typeof bp.city === "string") { + return Boolean(bp.country?.trim() || bp.city?.trim()); + } + } + return true; } +function isMarriageBirthplaceFieldValue( + value: unknown, +): value is MarriageBirthplaceFieldValue { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return false; + } + + const bpValue = value as Partial; + + return ( + typeof bpValue.country === "string" && typeof bpValue.city === "string" + ); +} + function isMarriageField(value: unknown): value is MarriageField { if (!value || typeof value !== "object") { return false; @@ -107,14 +139,15 @@ function isMarriageField(value: unknown): value is MarriageField { typeof field.value === "number" || typeof field.value === "boolean" || Array.isArray(field.value) || - isMarriagePhoneFieldValue(field.value)) + isMarriagePhoneFieldValue(field.value) || + isMarriageBirthplaceFieldValue(field.value)) ); } function isMarriagePhoneFieldValue( value: unknown, ): value is MarriagePhoneFieldValue { - if (!value || typeof value !== "object") { + if (!value || typeof value !== "object" || Array.isArray(value)) { return false; } diff --git a/src/components/Componentes/question-birthplace.tsx b/src/components/Componentes/question-birthplace.tsx index 268747f..7a38f7a 100644 --- a/src/components/Componentes/question-birthplace.tsx +++ b/src/components/Componentes/question-birthplace.tsx @@ -208,17 +208,17 @@ export function QuestionBirthplace({ return () => window.removeEventListener("keydown", handleKeyDown); }, [isOpen, closeSheet]); - const lastInternalAnswerRef = useRef(null); + const lastInternalAnswerRef = useRef(null); const updateAnswers = (country: string, city: string) => { const cleanCountry = country?.trim() || ""; const cleanCity = city?.trim() || ""; - const formatted = - cleanCountry && cleanCity - ? `${cleanCountry}, ${cleanCity}` - : cleanCountry || cleanCity || null; - lastInternalAnswerRef.current = formatted; - setAnswerValue(question, formatted); + const payload = + cleanCountry || cleanCity + ? { country: cleanCountry, city: cleanCity } + : null; + lastInternalAnswerRef.current = payload; + setAnswerValue(question, payload); }; // GeoIP detection logic using unified getUserGeoRegion @@ -304,6 +304,21 @@ export function QuestionBirthplace({ if (rawValue === lastInternalAnswerRef.current) { return; } + if ( + typeof rawValue === "object" && + rawValue !== null && + typeof lastInternalAnswerRef.current === "object" && + lastInternalAnswerRef.current !== null + ) { + const currentObj = lastInternalAnswerRef.current as BirthplaceValue; + const rawObj = rawValue as BirthplaceValue; + if ( + (rawObj.country?.trim() || "") === (currentObj.country?.trim() || "") && + (rawObj.city?.trim() || "") === (currentObj.city?.trim() || "") + ) { + return; + } + } const updated = parseValue(rawValue); const resolvedC = resolveCountryName(updated.country, locale) || updated.country; if (resolvedC !== selectedCountry) { @@ -315,7 +330,7 @@ export function QuestionBirthplace({ if (resolvedC || updated.city) { setDetectedLocation([resolvedC, updated.city].filter(Boolean).join(", ")); } - lastInternalAnswerRef.current = typeof rawValue === "string" ? rawValue : null; + lastInternalAnswerRef.current = rawValue as BirthplaceValue | string | null; }, [rawValue, locale]); const options = getCountryList(locale); diff --git a/src/components/Componentes/question-number.tsx b/src/components/Componentes/question-number.tsx index 479cf50..86e7b0a 100644 --- a/src/components/Componentes/question-number.tsx +++ b/src/components/Componentes/question-number.tsx @@ -466,6 +466,12 @@ function getCountryFromStorage(): string { f.key?.includes("mhl_skwnt_fly"), ); const value = field?.value; + if (typeof value === "object" && value !== null) { + const obj = value as { country?: string; city?: string }; + if (typeof obj.country === "string" && obj.country.trim()) { + return obj.country.trim(); + } + } if (typeof value === "string") { const parts = value.split(",").map((p) => p.trim()); if (parts.length >= 2) { diff --git a/src/components/Componentes/question-phone.test.tsx b/src/components/Componentes/question-phone.test.tsx index 4620e97..0bf7d4d 100644 --- a/src/components/Componentes/question-phone.test.tsx +++ b/src/components/Componentes/question-phone.test.tsx @@ -117,35 +117,20 @@ describe("QuestionPhone IP country detection and shimmer", () => { }); }); - it("falls back to secondary fetch when Habib region API fails and shows resolved code", async () => { + it("falls back to default region without calling external fetch when Habib region API fails", async () => { httpMocks.get.mockRejectedValue(new Error("Network failure")); - - let resolveIpFetch!: (value: unknown) => void; - const ipPromise = new Promise((resolve) => { - resolveIpFetch = resolve; - }); - - vi.spyOn(globalThis, "fetch").mockImplementation(() => - ipPromise.then( - (data) => - ({ - ok: true, - json: async () => data, - }) as unknown as Response, - ), - ); + const fetchSpy = vi.spyOn(globalThis, "fetch"); const { container } = render(); - await act(async () => { - resolveIpFetch({ country_calling_code: "+98" }); - }); - await waitFor(() => { expect(container.querySelectorAll(".shimmer-bg").length).toBe(0); - expect(screen.getByText("+98")).toBeDefined(); - expect(screen.getByText("🇮🇷")).toBeDefined(); + expect(screen.getByText("+44")).toBeDefined(); + expect(screen.getByText("🇬🇧")).toBeDefined(); }); + + // Verify external fetch was NEVER called + expect(fetchSpy).not.toHaveBeenCalled(); }); it("shows default country code when all IP requests fail", async () => { diff --git a/src/components/Componentes/question-sheet.tsx b/src/components/Componentes/question-sheet.tsx index 1dcc1be..67e7c18 100644 --- a/src/components/Componentes/question-sheet.tsx +++ b/src/components/Componentes/question-sheet.tsx @@ -14,6 +14,7 @@ import { registerCompactQuestionSheet } from "./question-viewport-coordinator"; import { useSheetScrollLock } from "./use-sheet-scroll-lock"; const EXIT_ANIMATION_MS = 300; +const EMPTY_ARRAY: string[] = []; export type QuestionSheetProps = { question: QuestionField; @@ -30,11 +31,11 @@ export function QuestionSheet({ question, disabled }: QuestionSheetProps) { question.type === "checkbox" || (question.extras?.range && question.extras.range[1] > 1); - const selectedList = Array.isArray(rawValue) - ? rawValue - : typeof rawValue === "string" && rawValue - ? [rawValue] - : []; + const selectedList = useMemo(() => { + if (Array.isArray(rawValue)) return rawValue; + if (typeof rawValue === "string" && rawValue) return [rawValue]; + return EMPTY_ARRAY; + }, [rawValue]); const singleValue = typeof rawValue === "string" ? rawValue : ""; const [isOpen, setIsOpen] = useState(false); diff --git a/src/hooks/marriage/types.ts b/src/hooks/marriage/types.ts index 77d7e6f..a24f866 100644 --- a/src/hooks/marriage/types.ts +++ b/src/hooks/marriage/types.ts @@ -31,12 +31,18 @@ export type MarriagePhoneFieldValue = { phoneNumber: string; }; +export type MarriageBirthplaceFieldValue = { + country: string; + city: string; +}; + export type MarriageFieldValue = | string | string[] | number | boolean | MarriagePhoneFieldValue + | MarriageBirthplaceFieldValue | null; export type MarriageField = { diff --git a/src/lib/geo-region.ts b/src/lib/geo-region.ts index c433d48..d2cb734 100644 --- a/src/lib/geo-region.ts +++ b/src/lib/geo-region.ts @@ -140,83 +140,20 @@ async function fetchHttpGeoRegion(): Promise { ); } - // 2. Secondary fallback: ipapi.co with 2s timeout - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), 2000); - try { - console.log("[GEO_BRIDGE_LOG] 🔄 Falling back to ipapi.co..."); - const res = await fetch("https://ipapi.co/json/", { - signal: controller.signal, - }); - clearTimeout(timeoutId); - if (res?.ok) { - const data = await res.json(); - if ( - data && - (data.country_name || data.city || data.country_calling_code) - ) { - const rawPhone = data.country_calling_code - ? String(data.country_calling_code).trim() - : ""; - const phoneCode = rawPhone.startsWith("+") - ? rawPhone - : rawPhone - ? `+${rawPhone}` - : "+44"; - const region: UserGeoRegion = { - ip: data.ip, - city: data.city, - country: data.country_name, - countryCode: data.country_code, - phoneCode, - }; - setStoredUserGeoRegion(region); - return region; - } - } - } catch { - clearTimeout(timeoutId); - } - - // 3. Tertiary fallback: ipwho.is with 2s timeout - const secondaryController = new AbortController(); - const secondaryTimeoutId = setTimeout( - () => secondaryController.abort(), - 2000, - ); - try { - console.log("[GEO_BRIDGE_LOG] 🔄 Falling back to ipwho.is..."); - const res = await fetch("https://ipwho.is/", { - signal: secondaryController.signal, - }); - clearTimeout(secondaryTimeoutId); - if (res?.ok) { - const data = await res.json(); - if (data && (data.country || data.city || data.calling_code)) { - const rawPhone = data.calling_code - ? String(data.calling_code).trim() - : ""; - const phoneCode = rawPhone.startsWith("+") - ? rawPhone - : rawPhone - ? `+${rawPhone}` - : "+44"; - const region: UserGeoRegion = { - ip: data.ip, - city: data.city, - country: data.country, - countryCode: data.country_code, - phoneCode, - }; - setStoredUserGeoRegion(region); - return region; - } - } - } catch { - clearTimeout(secondaryTimeoutId); + // Default fallback: preserve previously cached/stored region if present + const existing = getStoredUserGeoRegion(); + if ( + existing && + (existing.country || existing.phoneCode || existing.countryCode || existing.city) + ) { + console.log( + "[GEO_BRIDGE_LOG] 📦 Preserving existing stored user geo region:", + JSON.stringify(existing), + ); + setStoredUserGeoRegion(existing); + return existing; } - // 4. Default fallback console.log("[GEO_BRIDGE_LOG] ⚠️ Using default fallback region (+44)..."); const defaultRegion: UserGeoRegion = { phoneCode: "+44", @@ -224,6 +161,15 @@ async function fetchHttpGeoRegion(): Promise { setStoredUserGeoRegion(defaultRegion); return defaultRegion; } catch { + const existing = getStoredUserGeoRegion(); + if ( + existing && + (existing.country || existing.phoneCode || existing.countryCode || existing.city) + ) { + setStoredUserGeoRegion(existing); + return existing; + } + const defaultRegion: UserGeoRegion = { phoneCode: "+44", }; @@ -395,14 +341,18 @@ export function getUserGeoRegion(force = false): Promise { (Boolean(window.HabibApp?.postMessage) || typeof (window as any).sendToFlutter === "function"); - if (isFlutter) { + const isDev = process.env.NODE_ENV === "development"; + + if (isFlutter && !isDev) { console.log( "[GEO_BRIDGE_LOG] 📱 Inside Flutter WebView detected, requesting location via Flutter bridge", ); geoRegionPromise = fetchFlutterBridgeGeoRegion(); } else { console.log( - "[GEO_BRIDGE_LOG] 🌐 Standard browser environment detected, requesting location via HTTP", + isDev && isFlutter + ? "[GEO_BRIDGE_LOG] 🛠️ Development environment (npm run dev) detected, skipping Flutter bridge action and requesting location via HTTP" + : "[GEO_BRIDGE_LOG] 🌐 Standard browser environment detected, requesting location via HTTP", ); geoRegionPromise = fetchHttpGeoRegion(); } diff --git a/src/lib/marriage-field-formatter.ts b/src/lib/marriage-field-formatter.ts index 9a4ad25..34f1628 100644 --- a/src/lib/marriage-field-formatter.ts +++ b/src/lib/marriage-field-formatter.ts @@ -2,6 +2,7 @@ import type { MarriageField, MarriageFieldValue, MarriagePhoneFieldValue, + MarriageBirthplaceFieldValue, } from "@/hooks/marriage/types"; import { dictionaries } from "@/translations/dictionaries"; @@ -23,7 +24,7 @@ for (const enKey of Object.keys(dictionaries.en)) { export function isMarriagePhoneFieldValue( value: unknown, ): value is MarriagePhoneFieldValue { - if (!value || typeof value !== "object") { + if (!value || typeof value !== "object" || Array.isArray(value)) { return false; } @@ -35,6 +36,20 @@ export function isMarriagePhoneFieldValue( ); } +export function isMarriageBirthplaceFieldValue( + value: unknown, +): value is MarriageBirthplaceFieldValue { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return false; + } + + const bpValue = value as Partial; + + return ( + typeof bpValue.country === "string" && typeof bpValue.city === "string" + ); +} + export function formatFieldValue(value: MarriageFieldValue): string | null { if (value === null || value === undefined || value === "") { return null; @@ -51,7 +66,7 @@ export function formatFieldValue(value: MarriageFieldValue): string | null { if (typeof value === "object") { if ("country" in value || "city" in value || "state" in value) { const v = value as { country?: string; state?: string; city?: string }; - const parts = [v.country, v.state, v.city] + const parts = [v.city, v.state, v.country] .map((p) => (typeof p === "string" ? p.trim() : "")) .filter(Boolean); return parts.join(", ");