diff --git a/src/app/candidate-contact/candidate-contact-client.tsx b/src/app/candidate-contact/candidate-contact-client.tsx
index be5c42b..7a0cf21 100644
--- a/src/app/candidate-contact/candidate-contact-client.tsx
+++ b/src/app/candidate-contact/candidate-contact-client.tsx
@@ -11,6 +11,9 @@ import DismissReasonSheet from "@/components/Componentes/dismiss-reason-sheet";
import OutcomeSelectionSheet from "@/components/Componentes/outcome-selection-sheet";
import PageHeader from "@/components/Componentes/page-header";
import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
+import MarriageAdvisorsOverlay, {
+ useMarriageAdvisorsOverlay,
+} from "@/components/Componentes/marriage-advisors-overlay";
import { PageBackground } from "@/components/Componentes/page-background";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import {
@@ -34,6 +37,8 @@ export default function CandidateContactClient() {
const [isCallResultSheetOpen, setIsCallResultSheetOpen] = useState(false);
const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] =
useState(false);
+ const { isAdvisorOpen, openAdvisors, closeAdvisors } =
+ useMarriageAdvisorsOverlay();
const { data: profile, isLoading: isProfileLoading } =
useMarriageProfileQuery({
@@ -273,7 +278,7 @@ export default function CandidateContactClient() {
avatars={advisorAvatars}
extraCount={7}
getAdvisorLabel={t["Get Advisor"]}
- getAdvisorHref="/marriage-advisors"
+ onGetAdvisor={openAdvisors}
/>
{/* Profile Locked banner */}
@@ -298,6 +303,11 @@ export default function CandidateContactClient() {
+
+
>
);
}
diff --git a/src/app/finding-match/finding-match-client.tsx b/src/app/finding-match/finding-match-client.tsx
index edae5af..feeae48 100644
--- a/src/app/finding-match/finding-match-client.tsx
+++ b/src/app/finding-match/finding-match-client.tsx
@@ -7,6 +7,9 @@ import { useHabibWebReady } from "@/hooks/use-habib-web-ready";
import { FaLock, FaPen } from "react-icons/fa6";
import { IoAlertCircle } from "react-icons/io5";
import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
+import MarriageAdvisorsOverlay, {
+ useMarriageAdvisorsOverlay,
+} from "@/components/Componentes/marriage-advisors-overlay";
import Button from "@/components/Componentes/button";
import { PageLoadingSkeleton } from "@/components/Componentes/page-loading-skeleton";
import { FixToTheEnd } from "@/components/Componentes/fix-to-the-end";
@@ -27,6 +30,8 @@ const advisorAvatars = [
export default function FindingMatchClient() {
const router = useRouter();
const { dictionary: t, locale } = useI18n();
+ const { isAdvisorOpen, openAdvisors, closeAdvisors } =
+ useMarriageAdvisorsOverlay();
const { data: profile, isLoading } = useMarriageProfileQuery({
refetchInterval: 3000,
});
@@ -157,7 +162,7 @@ export default function FindingMatchClient() {
avatars={advisorAvatars}
extraCount={7}
getAdvisorLabel={copy.getAdvisor}
- getAdvisorHref="/marriage-advisors"
+ onGetAdvisor={openAdvisors}
/>
@@ -179,6 +184,11 @@ export default function FindingMatchClient() {
)}
+
+
>
);
}
diff --git a/src/app/marriage-advisors/page.tsx b/src/app/marriage-advisors/page.tsx
index 3ae339a..da57734 100644
--- a/src/app/marriage-advisors/page.tsx
+++ b/src/app/marriage-advisors/page.tsx
@@ -17,7 +17,11 @@ import { postActionToFlutter } from "@/lib/webview-actions";
const FALLBACK_AVATAR = "/assets/images/Avatar Image.png";
-export default function MarriageAdvisorsPage() {
+type MarriageAdvisorsPageProps = {
+ onClose?: () => void;
+};
+
+export default function MarriageAdvisorsPage({ onClose }: MarriageAdvisorsPageProps = {}) {
const { dictionary: t } = useI18n();
const [isSupportOpen, setIsSupportOpen] = useState(false);
@@ -51,6 +55,7 @@ export default function MarriageAdvisorsPage() {
variant="transparent"
icon="back"
iconLabel={t["Go back"]}
+ onClick={onClose}
/>
@@ -667,6 +672,11 @@ export default function NewMatchClient() {
)}
+
+
>
);
}
diff --git a/src/app/request-accepted/request-accepted-client.tsx b/src/app/request-accepted/request-accepted-client.tsx
index f9ca364..4404f5a 100644
--- a/src/app/request-accepted/request-accepted-client.tsx
+++ b/src/app/request-accepted/request-accepted-client.tsx
@@ -6,6 +6,9 @@ import { useRouter } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { useHabibWebReady } from "@/hooks/use-habib-web-ready";
import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
+import MarriageAdvisorsOverlay, {
+ useMarriageAdvisorsOverlay,
+} from "@/components/Componentes/marriage-advisors-overlay";
import CallResultSheet from "@/components/Componentes/call-result-sheet";
import DismissReasonSheet from "@/components/Componentes/dismiss-reason-sheet";
import FemaleConsentSheet from "@/components/Componentes/female-consent-sheet";
@@ -174,6 +177,8 @@ function ContactInfoPhoneCard({ item }: { item: ContactInfoPhoneItem }) {
export default function RequestAcceptedClient() {
const { dictionary: t, locale } = useI18n();
const router = useRouter();
+ const { isAdvisorOpen, openAdvisors, closeAdvisors } =
+ useMarriageAdvisorsOverlay();
const [isCallResultSheetOpen, setIsCallResultSheetOpen] = useState(false);
const [isDismissReasonSheetOpen, setIsDismissReasonSheetOpen] =
useState(false);
@@ -727,7 +732,7 @@ export default function RequestAcceptedClient() {
avatars={advisorAvatars}
extraCount={7}
getAdvisorLabel={t["Get Advisor"]}
- getAdvisorHref="/marriage-advisors"
+ onGetAdvisor={openAdvisors}
/>
@@ -752,6 +757,11 @@ export default function RequestAcceptedClient() {
+
+
>
);
}
diff --git a/src/app/request-sent/request-sent-client.tsx b/src/app/request-sent/request-sent-client.tsx
index d368841..9b0a06e 100644
--- a/src/app/request-sent/request-sent-client.tsx
+++ b/src/app/request-sent/request-sent-client.tsx
@@ -7,6 +7,9 @@ import { useEffect, useMemo } from "react";
import { useHabibWebReady } from "@/hooks/use-habib-web-ready";
import { PageLoadingSkeleton } from "@/components/Componentes/page-loading-skeleton";
import AdvisorActionsCard from "@/components/Componentes/advisor-actions-card";
+import MarriageAdvisorsOverlay, {
+ useMarriageAdvisorsOverlay,
+} from "@/components/Componentes/marriage-advisors-overlay";
import PageHeader from "@/components/Componentes/page-header";
import { PageBackground } from "@/components/Componentes/page-background";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
@@ -23,6 +26,8 @@ const advisorAvatars = [
export default function RequestSentClient() {
const router = useRouter();
const { dictionary: t, locale } = useI18n();
+ const { isAdvisorOpen, openAdvisors, closeAdvisors } =
+ useMarriageAdvisorsOverlay();
const { data: profile, isLoading } = useMarriageProfileQuery({
refetchInterval: 3000,
});
@@ -115,7 +120,7 @@ export default function RequestSentClient() {
avatars={advisorAvatars}
extraCount={7}
getAdvisorLabel={copy.getAdvisor}
- getAdvisorHref="/marriage-advisors"
+ onGetAdvisor={openAdvisors}
/>
@@ -139,6 +144,11 @@ export default function RequestSentClient() {
+
+
>
);
}
diff --git a/src/components/Componentes/advisor-actions-card.tsx b/src/components/Componentes/advisor-actions-card.tsx
index 7cb7585..f1722a6 100644
--- a/src/components/Componentes/advisor-actions-card.tsx
+++ b/src/components/Componentes/advisor-actions-card.tsx
@@ -18,7 +18,8 @@ type AdvisorActionsCardProps = {
avatars: AdvisorAvatar[];
extraCount: number;
getAdvisorLabel: string;
- getAdvisorHref: string;
+ getAdvisorHref?: string;
+ onGetAdvisor?: () => void;
className?: string;
};
@@ -29,6 +30,7 @@ export function AdvisorActionsCard({
extraCount: fallbackExtraCount,
getAdvisorLabel,
getAdvisorHref,
+ onGetAdvisor,
className,
}: AdvisorActionsCardProps) {
const { data, isLoading } = useMarriageAdvisorsQuery();
@@ -96,7 +98,8 @@ export function AdvisorActionsCard({
diff --git a/src/components/Componentes/marriage-advisors-overlay.test.tsx b/src/components/Componentes/marriage-advisors-overlay.test.tsx
new file mode 100644
index 0000000..4fa1659
--- /dev/null
+++ b/src/components/Componentes/marriage-advisors-overlay.test.tsx
@@ -0,0 +1,134 @@
+import { act, cleanup, render, renderHook, screen } from "@testing-library/react";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { I18nProvider } from "@/translations/provider";
+import {
+ MarriageAdvisorsOverlay,
+ useMarriageAdvisorsOverlay,
+} from "./marriage-advisors-overlay";
+
+vi.mock("next/navigation", () => ({
+ useRouter: () => ({
+ push: vi.fn(),
+ replace: vi.fn(),
+ back: vi.fn(),
+ }),
+}));
+
+vi.mock("@/hooks/marriage/use-marriage-advisors", () => ({
+ useMarriageAdvisorsQuery: () => ({
+ data: { results: [] },
+ isLoading: false,
+ isError: false,
+ refetch: vi.fn(),
+ }),
+}));
+
+vi.mock("@/hooks/marriage/use-profile-main", () => ({
+ useMarriageProfileQuery: () => ({
+ data: null,
+ isLoading: false,
+ refetch: vi.fn(),
+ }),
+}));
+
+function createWrapper() {
+ const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: { retry: false },
+ },
+ });
+
+ return function Wrapper({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+ };
+}
+
+describe("MarriageAdvisorsOverlay & useMarriageAdvisorsOverlay", () => {
+ beforeEach(() => {
+ vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
+ vi.stubGlobal("requestAnimationFrame", (cb: FrameRequestCallback) => {
+ return setTimeout(() => cb(Date.now()), 0);
+ });
+ vi.stubGlobal("cancelAnimationFrame", (id: number) => {
+ clearTimeout(id);
+ });
+ document.body.className = "";
+ });
+
+ afterEach(() => {
+ cleanup();
+ vi.restoreAllMocks();
+ vi.useRealTimers();
+ document.body.className = "";
+ });
+
+ it("hook manages open and close state with history sync", () => {
+ const { result } = renderHook(() => useMarriageAdvisorsOverlay());
+
+ expect(result.current.isAdvisorOpen).toBe(false);
+
+ act(() => {
+ result.current.openAdvisors();
+ });
+
+ expect(result.current.isAdvisorOpen).toBe(true);
+
+ act(() => {
+ result.current.closeAdvisors();
+ });
+
+ expect(result.current.isAdvisorOpen).toBe(false);
+ });
+
+ it("renders overlay dialog and advisors page when open is true", () => {
+ const handleClose = vi.fn();
+ const wrapper = createWrapper();
+
+ render(
+ ,
+ { wrapper },
+ );
+
+ act(() => {
+ vi.advanceTimersByTime(16);
+ });
+
+ const overlay = screen.getByRole("dialog");
+ expect(overlay).toBeInTheDocument();
+ expect(overlay).toHaveClass("section-overlay");
+ expect(document.body.classList.contains("section-overlay-open")).toBe(true);
+ });
+
+ it("handles closing animation and removes body class when open becomes false", () => {
+ const handleClose = vi.fn();
+ const wrapper = createWrapper();
+
+ const { rerender } = render(
+ ,
+ { wrapper },
+ );
+
+ act(() => {
+ vi.advanceTimersByTime(16);
+ });
+
+ expect(screen.getByRole("dialog")).toHaveAttribute("data-state", "open");
+ expect(document.body.classList.contains("section-overlay-open")).toBe(true);
+
+ rerender();
+
+ expect(screen.getByRole("dialog")).toHaveAttribute("data-state", "closing");
+ expect(document.body.classList.contains("section-overlay-open")).toBe(false);
+
+ act(() => {
+ vi.advanceTimersByTime(210);
+ });
+
+ expect(screen.queryByRole("dialog")).not.toBeInTheDocument();
+ });
+});
diff --git a/src/components/Componentes/marriage-advisors-overlay.tsx b/src/components/Componentes/marriage-advisors-overlay.tsx
new file mode 100644
index 0000000..beb93e5
--- /dev/null
+++ b/src/components/Componentes/marriage-advisors-overlay.tsx
@@ -0,0 +1,77 @@
+"use client";
+
+import { useCallback, useEffect, useState } from "react";
+import MarriageAdvisorsPage from "@/app/marriage-advisors/page";
+import SectionOverlayHost from "@/components/Componentes/section-overlay-host";
+import { useHardwareBackHandler } from "@/hooks/use-hardware-back-handler";
+
+export type MarriageAdvisorsOverlayProps = {
+ open: boolean;
+ onClose: () => void;
+};
+
+/**
+ * Hook to manage Marriage Advisors slide-in overlay state.
+ * Syncs with browser history (?advisors=open) and intercepts hardware back in Flutter.
+ */
+export function useMarriageAdvisorsOverlay() {
+ const [isAdvisorOpen, setIsAdvisorOpen] = useState(false);
+
+ useEffect(() => {
+ const readAdvisorsFromUrl = () => {
+ if (typeof window === "undefined") return;
+ const params = new URLSearchParams(window.location.search);
+ setIsAdvisorOpen(params.get("advisors") === "open");
+ };
+
+ readAdvisorsFromUrl();
+ window.addEventListener("popstate", readAdvisorsFromUrl);
+ return () => window.removeEventListener("popstate", readAdvisorsFromUrl);
+ }, []);
+
+ const openAdvisors = useCallback(() => {
+ setIsAdvisorOpen(true);
+ if (typeof window !== "undefined") {
+ const url = new URL(window.location.href);
+ url.searchParams.set("advisors", "open");
+ window.history.pushState({ advisors: "open" }, "", url.toString());
+ }
+ }, []);
+
+ const closeAdvisors = useCallback(() => {
+ if (typeof window !== "undefined") {
+ const params = new URLSearchParams(window.location.search);
+ if (params.get("advisors") === "open") {
+ setIsAdvisorOpen(false);
+ window.history.back();
+ return;
+ }
+ }
+ setIsAdvisorOpen(false);
+ }, []);
+
+ // Intercept hardware back in Flutter WebView when advisor overlay is open
+ useHardwareBackHandler(() => {
+ closeAdvisors();
+ return true; // handled: keep WebView screen open
+ }, isAdvisorOpen);
+
+ return {
+ isAdvisorOpen,
+ openAdvisors,
+ closeAdvisors,
+ };
+}
+
+export function MarriageAdvisorsOverlay({
+ open,
+ onClose,
+}: MarriageAdvisorsOverlayProps) {
+ return (
+
+
+
+ );
+}
+
+export default MarriageAdvisorsOverlay;