From b1e75845f14f644bc3c3efa566d983ad3f69ca05 Mon Sep 17 00:00:00 2001 From: mortezaei Date: Sat, 22 Aug 2026 17:31:20 +0330 Subject: [PATCH] fix(navigation): implement synchronous hardware back handler to prevent accidental webview exit on section back --- src/app/layout.tsx | 5 ++++ .../questions-list/questions-list-client.tsx | 8 +++--- .../Componentes/hardware-back-bridge.tsx | 11 +++++--- src/hooks/use-hardware-back-handler.ts | 25 ++++++++++++++++++- src/types/window.d.ts | 4 +++ 5 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 7f9c933..d90d076 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -320,6 +320,11 @@ export default async function RootLayout({ return Promise.resolve({ handled: false }); }; } + if (!window.__habibHandleHardwareBackSync) { + window.__habibHandleHardwareBackSync = function() { + return false; + }; + } })(); `, }} diff --git a/src/app/questions-list/questions-list-client.tsx b/src/app/questions-list/questions-list-client.tsx index 4aa2ba8..9a5f6a3 100644 --- a/src/app/questions-list/questions-list-client.tsx +++ b/src/app/questions-list/questions-list-client.tsx @@ -152,15 +152,15 @@ export default function QuestionsListClient() { }, []); const handleCloseSection = useCallback(() => { + setActiveSectionSlug(null); if (typeof window !== "undefined") { const params = new URLSearchParams(window.location.search); if (params.get("section")) { - setActiveSectionSlug(null); - window.history.back(); - return; + const url = new URL(window.location.href); + url.searchParams.delete("section"); + window.history.replaceState({}, "", url.toString()); } } - setActiveSectionSlug(null); }, []); const questionListItems = useMemo( diff --git a/src/components/Componentes/hardware-back-bridge.tsx b/src/components/Componentes/hardware-back-bridge.tsx index 5f71810..05c4610 100644 --- a/src/components/Componentes/hardware-back-bridge.tsx +++ b/src/components/Componentes/hardware-back-bridge.tsx @@ -1,11 +1,14 @@ "use client"; import { useEffect } from "react"; -import { handleHardwareBack } from "@/hooks/use-hardware-back-handler"; +import { + handleHardwareBack, + handleHardwareBackSync, +} from "@/hooks/use-hardware-back-handler"; /** * Wires the React hardware-back handler stack to the global - * window.__habibHandleHardwareBack function. + * window.__habibHandleHardwareBack and window.__habibHandleHardwareBackSync functions. * * Mount this once in the Providers tree (after React hydration). * It replaces the bootstrap stub with the real handler that walks @@ -14,11 +17,13 @@ import { handleHardwareBack } from "@/hooks/use-hardware-back-handler"; export function HardwareBackBridge() { useEffect(() => { window.__habibHandleHardwareBack = handleHardwareBack; + window.__habibHandleHardwareBackSync = handleHardwareBackSync; return () => { - // On unmount (shouldn't happen in practice), restore the stub. + // On unmount (shouldn't happen in practice), restore the stubs. window.__habibHandleHardwareBack = () => Promise.resolve({ handled: false }); + window.__habibHandleHardwareBackSync = () => false; }; }, []); diff --git a/src/hooks/use-hardware-back-handler.ts b/src/hooks/use-hardware-back-handler.ts index 1b6f5f7..148ea45 100644 --- a/src/hooks/use-hardware-back-handler.ts +++ b/src/hooks/use-hardware-back-handler.ts @@ -60,6 +60,29 @@ export function useHardwareBackHandler( }, [enabled]); } +/** + * Synchronously invokes the topmost hardware-back handler if registered. + * Returns true if a handler consumed the event, or false if Flutter should handle/close. + */ +export function handleHardwareBackSync(): boolean { + if (backHandlerStack.length === 0) { + return false; + } + + const handler = backHandlerStack[backHandlerStack.length - 1]; + try { + const result = handler(); + if (result instanceof Promise) { + // Promise was initiated by invoking the handler; it is handled in JS! + return true; + } + return Boolean(result); + } catch (error) { + console.warn("[HardwareBackSync] Handler threw:", error); + return false; + } +} + /** * Called by the root bootstrap script when Flutter sends a hardware back event. * Returns { handled: true } if a web handler consumed the event, or @@ -74,7 +97,7 @@ export async function handleHardwareBack(): Promise<{ handled: boolean }> { const handler = backHandlerStack[backHandlerStack.length - 1]; try { const result = await handler(); - return { handled: result }; + return { handled: Boolean(result) }; } catch (error) { console.warn("[HardwareBack] Handler threw:", error); return { handled: false }; diff --git a/src/types/window.d.ts b/src/types/window.d.ts index 6798bbe..6b3eacf 100644 --- a/src/types/window.d.ts +++ b/src/types/window.d.ts @@ -100,6 +100,10 @@ declare global { * or { handled: false } if Flutter should close the WebView screen. */ __habibHandleHardwareBack?: () => Promise<{ handled: boolean }>; + /** + * Synchronous variant called by Flutter for instant response without Promise serialization issues. + */ + __habibHandleHardwareBackSync?: () => boolean; /** * Unique ID per document load. If this changes on back navigation, * it proves a hard reload / WebView recreation happened.