diff --git a/src/components/Componentes/dismiss-reason-sheet.tsx b/src/components/Componentes/dismiss-reason-sheet.tsx index ca217f1..6ab7f51 100644 --- a/src/components/Componentes/dismiss-reason-sheet.tsx +++ b/src/components/Componentes/dismiss-reason-sheet.tsx @@ -2,6 +2,7 @@ import type { HTMLAttributes } from "react"; import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; +import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import { useI18n } from "@/translations/provider"; import Button from "./button"; @@ -77,6 +78,8 @@ export function DismissReasonSheet({ }, EXIT_ANIMATION_MS); }, [isClosing, onClose]); + useHardwareBackHandler(closeSheet, isVisible && !isClosing); + useEffect(() => { if (!isVisible) { return; diff --git a/src/components/Componentes/female-consent-sheet.tsx b/src/components/Componentes/female-consent-sheet.tsx index f6758f9..356ed62 100644 --- a/src/components/Componentes/female-consent-sheet.tsx +++ b/src/components/Componentes/female-consent-sheet.tsx @@ -3,6 +3,7 @@ import Image from "next/image"; import type { HTMLAttributes, ReactNode } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; +import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import { useI18n } from "@/translations/provider"; const EXIT_ANIMATION_MS = 220; @@ -50,6 +51,8 @@ export function FemaleConsentSheet({ }, EXIT_ANIMATION_MS); }, [isClosing, onClose]); + useHardwareBackHandler(closeSheet, isVisible && !isClosing); + const controls = { close: closeSheet }; const resolvedButtons = typeof buttons === "function" ? buttons(controls) : buttons; diff --git a/src/components/Componentes/female-outcome-sheet.tsx b/src/components/Componentes/female-outcome-sheet.tsx index 6b49395..4a37a47 100644 --- a/src/components/Componentes/female-outcome-sheet.tsx +++ b/src/components/Componentes/female-outcome-sheet.tsx @@ -1,6 +1,7 @@ "use client"; import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; +import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import { useI18n } from "@/translations/provider"; import SwipeButton from "./swipe-button"; @@ -76,6 +77,8 @@ export function FemaleOutcomeSheet({ }, EXIT_ANIMATION_MS); }, [isClosing, onClose]); + useHardwareBackHandler(closeSheet, isVisible && !isClosing); + useEffect(() => { if (!isVisible) { return; diff --git a/src/components/Componentes/help-modal.tsx b/src/components/Componentes/help-modal.tsx index b27c41f..c9722b3 100644 --- a/src/components/Componentes/help-modal.tsx +++ b/src/components/Componentes/help-modal.tsx @@ -3,6 +3,7 @@ import type { ReactNode } from "react"; import { useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; +import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import Button from "./button"; import { useI18n } from "@/translations/provider"; @@ -63,6 +64,8 @@ export function HelpModal({ }, EXIT_ANIMATION_MS); }, [isClosing, onClose]); + useHardwareBackHandler(closeSheet, isOpen && !isClosing); + // Lock body scroll useEffect(() => { if (!isOpen || !mounted) return; diff --git a/src/components/Componentes/information-sheet.tsx b/src/components/Componentes/information-sheet.tsx index 6d7c5a4..a435feb 100644 --- a/src/components/Componentes/information-sheet.tsx +++ b/src/components/Componentes/information-sheet.tsx @@ -6,6 +6,7 @@ import { useCallback, useEffect, useRef, useState } from "react"; import Button from "./button"; import { LoadingSkeleton } from "./loading-skeleton"; import { LoadingThreeDot } from "./loading-three-dot"; +import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import { useI18n } from "@/translations/provider"; const EXIT_ANIMATION_MS = 220; @@ -171,6 +172,8 @@ export function InformationSheet({ }, EXIT_ANIMATION_MS); }, [isClosing, onClose]); + useHardwareBackHandler(closeSheet, isVisible && !isClosing); + const controls = { close: closeSheet }; const resolvedTitle = typeof title === "function" ? title(controls) : title; const resolvedButtons = diff --git a/src/components/Componentes/outcome-selection-sheet.tsx b/src/components/Componentes/outcome-selection-sheet.tsx index f8c5648..b95a0de 100644 --- a/src/components/Componentes/outcome-selection-sheet.tsx +++ b/src/components/Componentes/outcome-selection-sheet.tsx @@ -2,6 +2,7 @@ import type { HTMLAttributes } from "react"; import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; +import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import { useI18n } from "@/translations/provider"; const EXIT_ANIMATION_MS = 220; @@ -51,6 +52,8 @@ export function OutcomeSelectionSheet({ }, EXIT_ANIMATION_MS); }, [isClosing, onClose]); + useHardwareBackHandler(closeSheet, isVisible && !isClosing); + useEffect(() => { if (!isVisible) { return; diff --git a/src/components/Componentes/report-actions-sheet.tsx b/src/components/Componentes/report-actions-sheet.tsx index 62317d4..2ba7b78 100644 --- a/src/components/Componentes/report-actions-sheet.tsx +++ b/src/components/Componentes/report-actions-sheet.tsx @@ -3,6 +3,7 @@ import { useEffect, useState } from "react"; import Button from "./button"; import { useFlutterBridge } from "@/hooks/useFlutterBridge"; +import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import { copyToClipboard, downloadFile, @@ -33,6 +34,8 @@ export function ReportActionsSheet({ onClose }: ReportActionsSheetProps) { setIsClosing(true); }; + useHardwareBackHandler(closeSheet, isVisible && !isClosing); + // ✅ دکمه WEB_READY const handleSendWebReady = () => { sendToFlutter("WEB_READY", { diff --git a/src/components/Componentes/support-sheet.tsx b/src/components/Componentes/support-sheet.tsx index 9084959..6fe9a07 100644 --- a/src/components/Componentes/support-sheet.tsx +++ b/src/components/Componentes/support-sheet.tsx @@ -4,6 +4,7 @@ import Image from "next/image"; import { useCallback, useEffect, useRef, useState } from "react"; import { createPortal } from "react-dom"; import { FiHeadphones } from "react-icons/fi"; +import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; import { useI18n } from "@/translations/provider"; const EXIT_ANIMATION_MS = 220; @@ -45,6 +46,8 @@ export function SupportSheet({ isOpen, onClose }: SupportSheetProps) { }, EXIT_ANIMATION_MS); }, [isClosing, onClose]); + useHardwareBackHandler(closeSheet, isOpen && !isClosing); + // Lock body scroll useEffect(() => { if (!isOpen || !mounted) return; diff --git a/src/components/Componentes/video-player.tsx b/src/components/Componentes/video-player.tsx index d1ceeee..2cc536c 100644 --- a/src/components/Componentes/video-player.tsx +++ b/src/components/Componentes/video-player.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, type FC } from "react"; import { createPortal } from "react-dom"; +import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler"; type VideoPlayerProps = { isOpen: boolean; @@ -24,6 +25,8 @@ export const VideoPlayer: FC = ({ setMounted(true); }, []); + useHardwareBackHandler(onClose, isOpen); + const handleLoadedMetadata = (e: React.SyntheticEvent) => { const video = e.currentTarget; if (video.videoWidth && video.videoHeight) { diff --git a/src/hooks/use-hardware-back-handler.ts b/src/hooks/use-hardware-back-handler.ts index 148ea45..059f66d 100644 --- a/src/hooks/use-hardware-back-handler.ts +++ b/src/hooks/use-hardware-back-handler.ts @@ -13,32 +13,19 @@ import { useEffect, useRef } from "react"; * Pages register themselves with useHardwareBackHandler(). Sheets and modals * also register — the last one wins, matching the visual stacking order. */ -const backHandlerStack: Array<() => boolean | Promise> = []; +const backHandlerStack: Array<() => void | 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. + * button (Android), Flutter calls window.__habibHandleHardwareBackSync() or + * window.__habibHandleHardwareBack(). * - * @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; - * }); + * @param handler - Return true (or void) if you handled the back (e.g. closed a sheet). + * Return false if you didn't handle it (Flutter should close the WebView). + * @param enabled - When false, the handler is not registered. */ export function useHardwareBackHandler( - handler: () => boolean | Promise, + handler: () => void | boolean | Promise, enabled = true, ) { const handlerRef = useRef(handler); @@ -76,7 +63,7 @@ export function handleHardwareBackSync(): boolean { // Promise was initiated by invoking the handler; it is handled in JS! return true; } - return Boolean(result); + return result === undefined ? true : Boolean(result); } catch (error) { console.warn("[HardwareBackSync] Handler threw:", error); return false; @@ -97,7 +84,7 @@ export async function handleHardwareBack(): Promise<{ handled: boolean }> { const handler = backHandlerStack[backHandlerStack.length - 1]; try { const result = await handler(); - return { handled: Boolean(result) }; + return { handled: result === undefined ? true : Boolean(result) }; } catch (error) { console.warn("[HardwareBack] Handler threw:", error); return { handled: false };