From bc8cce4b0a7979df78f37fe1974ff0a51f61b0e7 Mon Sep 17 00:00:00 2001 From: "Muhammad A. Ghorbani" Date: Thu, 3 Sep 2026 11:18:20 +0330 Subject: [PATCH] feat: add multi-step marriage profile onboarding slider with native navigation handling --- .../questions-list/questions-list-client.tsx | 42 ++++----- src/app/terms/page.tsx | 29 +++--- .../Componentes/question-birthplace.tsx | 92 ++++++++++++------- src/components/Componentes/slider-page.tsx | 72 ++++++++++----- .../Componentes/slider-slide-one.tsx | 16 +++- src/components/Componentes/ui-config.test.tsx | 89 ++++++++++++++++++ 6 files changed, 247 insertions(+), 93 deletions(-) diff --git a/src/app/questions-list/questions-list-client.tsx b/src/app/questions-list/questions-list-client.tsx index 54013e9..dc7577f 100644 --- a/src/app/questions-list/questions-list-client.tsx +++ b/src/app/questions-list/questions-list-client.tsx @@ -104,6 +104,20 @@ export default function QuestionsListClient({ }); const { dictionary: t, locale } = useI18n(); const router = useRouter(); + + const handleHeaderBack = useCallback(() => { + if (onClose) { + onClose(); + } else if (typeof window !== "undefined" && (window as any).HabibApp) { + (window as any).HabibApp.postMessage( + JSON.stringify({ action: "close_service" }), + ); + } else if (typeof window !== "undefined" && window.history.length > 1) { + router.back(); + } else { + router.replace(localizePath("/intro", locale)); + } + }, [onClose, router, locale]); const queryClient = useQueryClient(); const { data: profile, isLoading: isProfileLoading, isError: isProfileError, isFetched: isProfileFetched, refetch: refetchProfile } = useMarriageProfileQuery(); @@ -723,15 +737,7 @@ export default function QuestionsListClient({ iconLabel={t["Close questions list"]} onClick={(e) => { e.preventDefault(); - if (onClose) { - onClose(); - } else if (typeof window !== "undefined" && (window as any).HabibApp) { - (window as any).HabibApp.postMessage( - JSON.stringify({ action: "close_service" }), - ); - } else { - router.push("/"); - } + handleHeaderBack(); }} />

@@ -805,13 +811,7 @@ export default function QuestionsListClient({ iconLabel={t["Close questions list"]} onClick={(e) => { e.preventDefault(); - if (typeof window !== "undefined" && (window as any).HabibApp) { - (window as any).HabibApp.postMessage( - JSON.stringify({ action: "close_service" }), - ); - } else { - router.push("/"); - } + handleHeaderBack(); }} />

@@ -930,15 +930,7 @@ export default function QuestionsListClient({ iconLabel={t["Close questions list"]} onClick={(e) => { e.preventDefault(); - if (onClose) { - onClose(); - } else if (typeof window !== "undefined" && (window as any).HabibApp) { - (window as any).HabibApp.postMessage( - JSON.stringify({ action: "close_service" }), - ); - } else { - router.push("/"); - } + handleHeaderBack(); }} />

diff --git a/src/app/terms/page.tsx b/src/app/terms/page.tsx index f694391..3113c51 100644 --- a/src/app/terms/page.tsx +++ b/src/app/terms/page.tsx @@ -8,6 +8,7 @@ import { hasCompletedMarriageProfileBasics, } from "@/lib/get-submit-path"; import { useHabibWebReady } from "@/hooks/use-habib-web-ready"; +import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import SliderPage from "@/components/Componentes/slider-page"; import Button from "@/components/Componentes/button"; import { useI18n } from "@/translations/provider"; @@ -18,22 +19,26 @@ export default function TermsRoute() { const router = useRouter(); const { locale, dictionary: t } = useI18n(); - // Signal Flutter to lift its loading cover immediately on mount - useHabibWebReady(true); + const isBasicsCompleted = Boolean( + profile && + (profile.status !== "pending_onboarding" || + hasCompletedMarriageProfileBasics(profile)), + ); + + // Signal Flutter to lift its loading cover, but only cache /terms if onboarding is required + useHabibWebReady(!isLoading && !isBasicsCompleted); + + // At the root /terms screen, hardware back tells Flutter to close the WebView screen + useHardwareBackHandler(() => false, !isBasicsCompleted); useEffect(() => { - if (!isLoading && profile) { - if ( - profile.status !== "pending_onboarding" || - hasCompletedMarriageProfileBasics(profile) - ) { - const target = getSubmitPath(profile); - if (target !== "/terms") { - router.replace(localizePath(target, locale)); - } + if (!isLoading && profile && isBasicsCompleted) { + const target = getSubmitPath(profile); + if (target !== "/terms") { + router.replace(localizePath(target, locale)); } } - }, [profile, isLoading, router, locale]); + }, [profile, isLoading, isBasicsCompleted, router, locale]); if (isLoading) { return ( diff --git a/src/components/Componentes/question-birthplace.tsx b/src/components/Componentes/question-birthplace.tsx index 067844c..00bdad5 100644 --- a/src/components/Componentes/question-birthplace.tsx +++ b/src/components/Componentes/question-birthplace.tsx @@ -163,6 +163,14 @@ export function QuestionBirthplace({ question.id?.includes("residence") || question.id?.includes("living_area") || question.id?.includes("location"); + + const isCurrentResidenceQuestion = Boolean( + question.id === "contact_residence.current_residence_location" || + question.id?.endsWith(".current_residence_location") || + question.id?.endsWith(".current_residence") || + question.ui_config?.read_only_fields === true + ); + const storedRegion = isResidence ? getStoredUserGeoRegion() : null; const initial = parseValue(rawValue); @@ -189,6 +197,13 @@ export function QuestionBirthplace({ const initialCity = hasSavedAnswer ? initial.city || "" : ""; + const fallbackCountry = isResidence + ? resolveCountryName(storedRegion?.country, locale) || + storedRegion?.country || + "" + : ""; + const fallbackCity = isResidence ? storedRegion?.city || "" : ""; + const initialLoc = localizedInitialCountry || initialCity ? [localizedInitialCountry, initialCity].filter(Boolean).join(", ") @@ -203,12 +218,14 @@ export function QuestionBirthplace({ : ""; const [selectedCountry, setSelectedCountry] = useState( - () => localizedInitialCountry || "", + () => localizedInitialCountry || fallbackCountry, ); - const [cityInput, setCityInput] = useState(() => initialCity); + const [cityInput, setCityInput] = useState(() => initialCity || fallbackCity); - const cityInputStateRef = useRef(initialCity); - const selectedCountryStateRef = useRef(localizedInitialCountry || ""); + const cityInputStateRef = useRef(initialCity || fallbackCity); + const selectedCountryStateRef = useRef( + localizedInitialCountry || fallbackCountry, + ); const lastCoordsRef = useRef<{ latitude?: number; longitude?: number } | undefined>( storedRegion?.latitude && storedRegion?.longitude @@ -642,13 +659,16 @@ export function QuestionBirthplace({
@@ -686,13 +708,19 @@ export function QuestionBirthplace({ ref={cityInputRef} type="text" data-no-auto-focus + readOnly={isCurrentResidenceQuestion} disabled={disabled} value={cityInput} - onChange={handleCityChange} - onFocus={handleCityFocus} - onBlur={handleCityBlur} + onChange={isCurrentResidenceQuestion ? undefined : handleCityChange} + onFocus={isCurrentResidenceQuestion ? (e) => e.target.blur() : handleCityFocus} + onBlur={isCurrentResidenceQuestion ? undefined : handleCityBlur} placeholder={cityPlaceholder} - className="h-[54px] w-full rounded-[16px] border border-[#D0D5DD] bg-white px-4.5 text-[15px] font-medium text-[#181818] placeholder:text-[#98A2B3] hover:border-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F] outline-none transition-all" + className={[ + "h-[54px] w-full rounded-[16px] border border-[#D0D5DD] bg-white px-4.5 text-[15px] font-medium text-[#181818] placeholder:text-[#98A2B3] outline-none transition-all", + isCurrentResidenceQuestion + ? "cursor-default select-none focus:border-[#D0D5DD] focus:ring-0" + : "hover:border-[#98A2B3] focus:border-[#6F6F6F] focus:ring-1 focus:ring-[#6F6F6F]", + ].join(" ")} /> diff --git a/src/components/Componentes/slider-page.tsx b/src/components/Componentes/slider-page.tsx index 601446e..8e0aadf 100644 --- a/src/components/Componentes/slider-page.tsx +++ b/src/components/Componentes/slider-page.tsx @@ -1,10 +1,12 @@ "use client"; import { useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useQueryClient } from "@tanstack/react-query"; import { useUpdateMarriageProfileBasicMutation } from "@/hooks/marriage/use-profile-basic"; +import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import { authBridge } from "@/lib/auth-bridge"; +import { setCachedMarriageEntryPath } from "@/lib/entry-route-cache"; import { isInFlutterWebView } from "@/lib/webview-actions"; import { getSubmitPath, @@ -63,9 +65,9 @@ export default function SliderPage({ onClose }: SliderPageProps = {}) { setActiveSlide(targetIndex); }; - const goToPreviousSlide = () => { - goToSlide(activeSlide - 1); - }; + const goToPreviousSlide = useCallback(() => { + setActiveSlide((prev) => Math.max(0, prev - 1)); + }, []); const goToNextSlide = () => { if (activeSlide === 0 && !hasReadRules) { @@ -74,6 +76,47 @@ export default function SliderPage({ onClose }: SliderPageProps = {}) { goToSlide(activeSlide + 1); }; + const handleBackNavigation = useCallback(() => { + if (activeSlide > 0) { + goToPreviousSlide(); + return; + } + if (typeof window !== "undefined") { + try { + window.sessionStorage.setItem("skip_intro_redirect", "true"); + } catch (e) { + console.warn("sessionStorage is not accessible:", e); + } + } + if (onClose) { + onClose(); + return; + } + if (typeof window !== "undefined" && (window as any).HabibApp) { + (window as any).HabibApp.postMessage( + JSON.stringify({ action: "close_service" }), + ); + return; + } + if (typeof window !== "undefined" && window.history.length > 1) { + router.back(); + } else { + router.replace(localizePath("/intro", locale)); + } + }, [activeSlide, goToPreviousSlide, onClose, router, locale]); + + useHardwareBackHandler(() => { + if (activeSlide > 0) { + goToPreviousSlide(); + return true; // Handled: keep WebView open, move to previous slide + } + if (onClose) { + onClose(); + return true; // Handled: closed the overlay + } + return false; // Root: tell Flutter to close WebView screen + }); + const completeSlider = async () => { if (isSubmitting) return; setIsSubmitting(true); @@ -116,6 +159,7 @@ export default function SliderPage({ onClose }: SliderPageProps = {}) { const targetPath = getSubmitPath(freshProfile); const localizedTarget = localizePath(targetPath, locale); + setCachedMarriageEntryPath(targetPath); router.prefetch?.(localizedTarget); router.replace(localizedTarget); } catch (error: any) { @@ -147,24 +191,7 @@ export default function SliderPage({ onClose }: SliderPageProps = {}) { { - if (activeSlide > 0) { - goToPreviousSlide(); - return; - } - if (typeof window !== "undefined") { - try { - window.sessionStorage.setItem("skip_intro_redirect", "true"); - } catch (e) { - console.warn("sessionStorage is not accessible:", e); - } - } - if (onClose) { - onClose(); - } else { - router.back(); - } - }} + onClick={handleBackNavigation} />