diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index a9de196..1c12781 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -301,6 +301,21 @@ export default async function RootLayout({
}
}, 50);
}
+
+ // 5. Document Boot ID — reload detection instrumentation.
+ // If this ID changes across a back navigation, a hard reload
+ // or WebView recreation happened (not SPA navigation).
+ window.__habibDocumentBootId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
+
+ // 6. Hardware Back contract stub.
+ // Flutter should call: window.__habibHandleHardwareBack()
+ // The real handler stack is set up by React (use-hardware-back-handler.ts).
+ // This stub ensures the function exists even before React hydrates.
+ if (!window.__habibHandleHardwareBack) {
+ window.__habibHandleHardwareBack = function() {
+ return Promise.resolve({ handled: false });
+ };
+ }
})();
`,
}}
diff --git a/src/app/providers.tsx b/src/app/providers.tsx
index 72e5bcb..7be85c1 100644
--- a/src/app/providers.tsx
+++ b/src/app/providers.tsx
@@ -6,6 +6,7 @@ import {
} from "@tanstack/react-query";
import { type ReactNode, useState } from "react";
import FlutterLocaleSync from "@/components/Componentes/flutter-locale-sync";
+import HardwareBackBridge from "@/components/Componentes/hardware-back-bridge";
import SilentReloader from "@/components/Componentes/silent-reloader";
import { ViewPaddingsProvider } from "@/components/Componentes/view-paddings-provider";
@@ -44,6 +45,7 @@ export default function Providers({ children }: ProvidersProps) {
+
{children}
diff --git a/src/app/questions-list/[slug]/question-detail-client.tsx b/src/app/questions-list/[slug]/question-detail-client.tsx
index 264ffb4..f92e846 100644
--- a/src/app/questions-list/[slug]/question-detail-client.tsx
+++ b/src/app/questions-list/[slug]/question-detail-client.tsx
@@ -1,7 +1,8 @@
"use client";
import { useRouter } from "next/navigation";
-import { useEffect, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler";
import Button from "@/components/Componentes/button";
import DataErrorState from "@/components/Componentes/data-error-state";
import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end";
@@ -166,6 +167,16 @@ export default function QuestionDetailClient({
const [hasTestProgress, setHasTestProgress] = useState(false);
const queryClient = useQueryClient();
+ // Hardware back in the detail page = navigate back to questions list.
+ // QuestionAnswersProvider's pagehide/unmount safety net will flush
+ // any pending answers automatically when the component unmounts.
+ const handleHardwareBack = useCallback(async () => {
+ router.replace(questionsListHref);
+ return true; // handled — keep WebView open
+ }, [router, questionsListHref]);
+
+ useHardwareBackHandler(handleHardwareBack);
+
useEffect(() => {
if (typeof window !== "undefined") {
const draftKey = `marriage:tests:${itemSlug}:draft`;
@@ -360,7 +371,7 @@ export default function QuestionDetailClient({
variant="transparent"
icon="close"
iconLabel={closeLabel}
- onClick={() => router.push(questionsListHref)}
+ onClick={() => router.replace(questionsListHref)}
/>
{loadingTitle}
@@ -412,7 +423,7 @@ export default function QuestionDetailClient({
variant="transparent"
icon="close"
iconLabel={closeLabel}
- onClick={() => router.push(questionsListHref)}
+ onClick={() => router.replace(questionsListHref)}
/>
{errorTitle}
@@ -455,7 +466,7 @@ export default function QuestionDetailClient({
variant="transparent"
icon="close"
iconLabel={closeLabel}
- onClick={() => router.push(questionsListHref)}
+ onClick={() => router.replace(questionsListHref)}
/>
{errorTitle}
diff --git a/src/app/questions-list/questions-list-client.tsx b/src/app/questions-list/questions-list-client.tsx
index 39138ba..f92ca89 100644
--- a/src/app/questions-list/questions-list-client.tsx
+++ b/src/app/questions-list/questions-list-client.tsx
@@ -28,7 +28,7 @@ import {
applyProfilePatchResultToCache,
updateMarriageSectionData,
} from "@/hooks/marriage/use-section-data";
-import { useCloseServiceOnBack } from "@/hooks/use-close-service-on-back";
+import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler";
import { getAssessmentLocalProgress } from "@/lib/assessment-progress";
import {
getSubmitPath,
@@ -47,7 +47,18 @@ import { prefetchSectionsWithBoundedConcurrency } from "./section-prefetch";
import SectionsRequest from "./sections-request";
export default function QuestionsListClient() {
- useCloseServiceOnBack();
+ // Hardware back on the root questions list = close the Flutter service.
+ // Unlike the old useCloseServiceOnBack, this does NOT push fake history
+ // entries. Flutter calls __habibHandleHardwareBack() and we return false
+ // (meaning "I didn't handle it — you should close").
+ useHardwareBackHandler(() => {
+ if (typeof window !== "undefined" && (window as any).HabibApp?.postMessage) {
+ (window as any).HabibApp.postMessage(
+ JSON.stringify({ action: "close_service" }),
+ );
+ }
+ return false; // Tell Flutter to close the WebView screen
+ });
const { dictionary: t, locale } = useI18n();
const router = useRouter();
const queryClient = useQueryClient();
diff --git a/src/components/Componentes/hardware-back-bridge.tsx b/src/components/Componentes/hardware-back-bridge.tsx
new file mode 100644
index 0000000..5f71810
--- /dev/null
+++ b/src/components/Componentes/hardware-back-bridge.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import { useEffect } from "react";
+import { handleHardwareBack } from "@/hooks/use-hardware-back-handler";
+
+/**
+ * Wires the React hardware-back handler stack to the global
+ * window.__habibHandleHardwareBack function.
+ *
+ * Mount this once in the Providers tree (after React hydration).
+ * It replaces the bootstrap stub with the real handler that walks
+ * the registered stack.
+ */
+export function HardwareBackBridge() {
+ useEffect(() => {
+ window.__habibHandleHardwareBack = handleHardwareBack;
+
+ return () => {
+ // On unmount (shouldn't happen in practice), restore the stub.
+ window.__habibHandleHardwareBack = () =>
+ Promise.resolve({ handled: false });
+ };
+ }, []);
+
+ return null;
+}
+
+export default HardwareBackBridge;
diff --git a/src/components/Componentes/question-exit-navigation-button.tsx b/src/components/Componentes/question-exit-navigation-button.tsx
index 084b462..09ba455 100644
--- a/src/components/Componentes/question-exit-navigation-button.tsx
+++ b/src/components/Componentes/question-exit-navigation-button.tsx
@@ -45,7 +45,7 @@ export function QuestionExitNavigationButton({
// ignore
} finally {
const target = localizePath(exitHref || "/questions-list", locale);
- router.push(target);
+ router.replace(target);
}
}}
/>
diff --git a/src/components/Componentes/question-section-flow.tsx b/src/components/Componentes/question-section-flow.tsx
index bb58def..a73e1a1 100644
--- a/src/components/Componentes/question-section-flow.tsx
+++ b/src/components/Componentes/question-section-flow.tsx
@@ -64,7 +64,7 @@ function SectionFlowContent({
// ignore
} finally {
const target = localizePath(exitHref || "/questions-list", locale);
- router.push(target);
+ router.replace(target);
}
}, [exitHref, flushAnswers, locale, router, isSubmitting]);
diff --git a/src/components/Componentes/use-sheet-scroll-lock.ts b/src/components/Componentes/use-sheet-scroll-lock.ts
index d9b1204..72d4854 100644
--- a/src/components/Componentes/use-sheet-scroll-lock.ts
+++ b/src/components/Componentes/use-sheet-scroll-lock.ts
@@ -1,6 +1,7 @@
"use client";
-import { useEffect, useRef } from "react";
+import { useCallback, useEffect, useRef } from "react";
+import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler";
let activeSheetCount = 0;
let bodyHadDropdownClass = false;
@@ -23,6 +24,18 @@ export function useSheetScrollLock(
const onBackRef = useRef(onBack);
onBackRef.current = onBack;
+ // Register in the hardware-back handler stack so that Flutter's
+ // __habibHandleHardwareBack() closes the sheet instead of navigating.
+ const handleHardwareBack = useCallback(() => {
+ if (onBackRef.current) {
+ onBackRef.current();
+ return true; // handled — sheet closed
+ }
+ return false;
+ }, []);
+
+ useHardwareBackHandler(handleHardwareBack, isOpen);
+
useEffect(() => {
if (!isOpen) return;
diff --git a/src/hooks/use-hardware-back-handler.ts b/src/hooks/use-hardware-back-handler.ts
new file mode 100644
index 0000000..1b6f5f7
--- /dev/null
+++ b/src/hooks/use-hardware-back-handler.ts
@@ -0,0 +1,84 @@
+"use client";
+
+import { useEffect, useRef } from "react";
+
+/**
+ * Global stack of hardware-back handlers.
+ *
+ * When Flutter sends a hardware back event, the root handler in layout.tsx
+ * pops the last handler from this stack and calls it. If the handler returns
+ * true, it means "I handled it — keep the WebView open". If it returns false
+ * (or the stack is empty), Flutter should close the WebView screen.
+ *
+ * Pages register themselves with useHardwareBackHandler(). Sheets and modals
+ * also register — the last one wins, matching the visual stacking order.
+ */
+const backHandlerStack: Array<() => boolean | Promise> = [];
+
+/**
+ * Register a hardware-back handler. When the user presses the hardware back
+ * button (Android), Flutter calls window.__habibHandleHardwareBack(). The
+ * root handler walks the stack from top to bottom and calls the first handler.
+ *
+ * @param handler - Return true if you handled the back (e.g. closed a sheet,
+ * flushed answers and navigated). Return false if you didn't handle it
+ * (Flutter should close the WebView).
+ * @param enabled - When false, the handler is not registered. Useful for
+ * conditionally enabling back handling.
+ *
+ * @example
+ * // In questions-list (root): back = close service
+ * useHardwareBackHandler(() => false);
+ *
+ * // In question-detail: back = flush + navigate
+ * useHardwareBackHandler(async () => {
+ * await flushAnswers({ force: true });
+ * router.replace(questionsListHref);
+ * return true;
+ * });
+ */
+export function useHardwareBackHandler(
+ handler: () => boolean | Promise,
+ enabled = true,
+) {
+ const handlerRef = useRef(handler);
+ handlerRef.current = handler;
+
+ useEffect(() => {
+ if (!enabled) return;
+
+ // Wrap in a stable closure so we can remove the exact reference.
+ const stableHandler = () => handlerRef.current();
+ backHandlerStack.push(stableHandler);
+
+ return () => {
+ const index = backHandlerStack.lastIndexOf(stableHandler);
+ if (index !== -1) {
+ backHandlerStack.splice(index, 1);
+ }
+ };
+ }, [enabled]);
+}
+
+/**
+ * Called by the root bootstrap script when Flutter sends a hardware back event.
+ * Returns { handled: true } if a web handler consumed the event, or
+ * { handled: false } if Flutter should close the WebView screen.
+ */
+export async function handleHardwareBack(): Promise<{ handled: boolean }> {
+ if (backHandlerStack.length === 0) {
+ return { handled: false };
+ }
+
+ // Pop the topmost handler (last registered = topmost in visual stack).
+ const handler = backHandlerStack[backHandlerStack.length - 1];
+ try {
+ const result = await handler();
+ return { handled: result };
+ } catch (error) {
+ console.warn("[HardwareBack] Handler threw:", error);
+ return { handled: false };
+ }
+}
+
+export default useHardwareBackHandler;
diff --git a/src/types/window.d.ts b/src/types/window.d.ts
index 631a738..85af656 100644
--- a/src/types/window.d.ts
+++ b/src/types/window.d.ts
@@ -88,6 +88,17 @@ declare global {
__HABIB_BOOTSTRAP__?: NonNullable;
__habibWebReadySent?: boolean;
__announceHabibWebReady?: () => void;
+ /**
+ * Called by Flutter when the user presses the hardware back button.
+ * Returns { handled: true } if web consumed the event (keep WebView open),
+ * or { handled: false } if Flutter should close the WebView screen.
+ */
+ __habibHandleHardwareBack?: () => Promise<{ handled: boolean }>;
+ /**
+ * Unique ID per document load. If this changes on back navigation,
+ * it proves a hard reload / WebView recreation happened.
+ */
+ __habibDocumentBootId?: string;
}
}