Browse Source

fix back button

master
mortezaei 1 week ago
parent
commit
e5b503b483
  1. 15
      src/app/layout.tsx
  2. 2
      src/app/providers.tsx
  3. 19
      src/app/questions-list/[slug]/question-detail-client.tsx
  4. 15
      src/app/questions-list/questions-list-client.tsx
  5. 28
      src/components/Componentes/hardware-back-bridge.tsx
  6. 2
      src/components/Componentes/question-exit-navigation-button.tsx
  7. 2
      src/components/Componentes/question-section-flow.tsx
  8. 15
      src/components/Componentes/use-sheet-scroll-lock.ts
  9. 84
      src/hooks/use-hardware-back-handler.ts
  10. 11
      src/types/window.d.ts

15
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 });
};
}
})();
`,
}}

2
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) {
<SilentReloader>
<ViewPaddingsProvider />
<FlutterLocaleSync />
<HardwareBackBridge />
{children}
</SilentReloader>
</QueryClientProvider>

19
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)}
/>
<h1 className="min-w-0 flex-1 truncate text-center text-[14px] font-semibold text-white">
{loadingTitle}
@ -412,7 +423,7 @@ export default function QuestionDetailClient({
variant="transparent"
icon="close"
iconLabel={closeLabel}
onClick={() => router.push(questionsListHref)}
onClick={() => router.replace(questionsListHref)}
/>
<h1 className="min-w-0 flex-1 truncate text-center text-[14px] font-semibold text-white">
{errorTitle}
@ -455,7 +466,7 @@ export default function QuestionDetailClient({
variant="transparent"
icon="close"
iconLabel={closeLabel}
onClick={() => router.push(questionsListHref)}
onClick={() => router.replace(questionsListHref)}
/>
<h1 className="min-w-0 flex-1 truncate text-center text-[14px] font-semibold text-white">
{errorTitle}

15
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();

28
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;

2
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);
}
}}
/>

2
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]);

15
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;

84
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<boolean>> = [];
/**
* 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<boolean>,
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;

11
src/types/window.d.ts

@ -88,6 +88,17 @@ declare global {
__HABIB_BOOTSTRAP__?: NonNullable<FlutterResponseEvent["data"]>;
__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;
}
}

Loading…
Cancel
Save