diff --git a/AGENTS.md b/AGENTS.md index 4f53884..de6c274 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,4 +11,7 @@ This version has breaking changes — APIs, conventions, and file structure may - CRITICAL AUTOMATION RULE: Whenever any new static text, data, field, component label, tooltip, help description, key, or text item is added or updated in the project, its translation key and values MUST automatically be added/updated in ALL 24 locale JSON files (`src/translations/locales/` & `src/data/questions/`). - QUESTION HELP & TOOLTIPS RULE: All question help texts, tooltips, and modal descriptions must be stored with key-value entries in the respective locale JSON files, ensuring complete synchronization across all supported languages. +# Database Source-of-Truth & API Target Protection +- فرانت همواره باید به بک‌اند محلی (`http://127.0.0.1:8000`) که به دیتابیس `najm2` (سرور staging) متصل است وصل باشد. تغییر آدرس و تارگت اتصال دیتابیس/API به سرورهای دیگر تنها با اجازه صریح کاربر مجاز است، اما اعمال تغییرات در بک‌اند یا دیتابیس نیازی به اجازه ندارد (Ask for permission ONLY when you want to change the address of database to a different one; changes in backend or database are OK and do not need permission). + diff --git a/src/app/candidate-contact/candidate-contact-client.tsx b/src/app/candidate-contact/candidate-contact-client.tsx index fa0d9e2..df9ae51 100644 --- a/src/app/candidate-contact/candidate-contact-client.tsx +++ b/src/app/candidate-contact/candidate-contact-client.tsx @@ -77,6 +77,41 @@ export default function CandidateContactClient() { const isFinalized = caseStatus === "finalized" || profile?.status === "matched"; + const contactSharedAtStr = + profile?.active_case?.contact_shared_at || + (profile?.active_case as any)?.payment_done_at; + + const FORTY_EIGHT_HOURS_MS = 48 * 60 * 60 * 1000; + + const [currentTime, setCurrentTime] = useState(() => Date.now()); + + useEffect(() => { + const interval = setInterval(() => { + setCurrentTime(Date.now()); + }, 15000); + return () => clearInterval(interval); + }, []); + + const contactSharedTimestamp = useMemo(() => { + if (!contactSharedAtStr) return null; + const parsed = new Date(contactSharedAtStr).getTime(); + return isNaN(parsed) ? null : parsed; + }, [contactSharedAtStr]); + + const canReportNoContact = useMemo(() => { + if (profile?.active_case?.can_report_no_contact) { + return true; + } + if (contactSharedTimestamp) { + return currentTime - contactSharedTimestamp >= FORTY_EIGHT_HOURS_MS; + } + return false; + }, [ + profile?.active_case?.can_report_no_contact, + contactSharedTimestamp, + currentTime, + ]); + const contactStatusMutation = useSubmitMarriageContactStatusMutation( caseId ?? "", ); @@ -84,7 +119,7 @@ export default function CandidateContactClient() { const outcomeMutation = useSubmitMarriageOutcomeMutation(caseId ?? ""); const handleNoContactReport = async () => { - if (!caseId || contactStatusMutation.isPending) return; + if (!caseId || contactStatusMutation.isPending || !canReportNoContact) return; await contactStatusMutation.mutateAsync({ action: "no_contact", custom_note: @@ -224,9 +259,24 @@ export default function CandidateContactClient() {
+ + +
+ ) : caseStatus === "contacted" || isFemaleContactConfirmed || (isFemaleProfile && contactStatusMutation.isPending) ? (
{isFemaleProfile && - contactStatusMutation.isPending ? null : isFemaleProfile ? ( - <> - - - - - ) : ( + contactStatusMutation.isPending ? null : ( <> @@ -899,12 +997,17 @@ export default function RequestAcceptedClient() { {isFemaleProfile ? ( - ), -})); - function renderSheet(onSubmit = vi.fn()) { render( @@ -48,7 +32,11 @@ describe("FemaleOutcomeSheet", () => { it("requires a cancellation reason and submits it as a failure", async () => { const onSubmit = renderSheet(); - await userEvent.click(screen.getByLabelText("Canceled")); + await userEvent.click( + screen.getByLabelText( + /We did not reach the agreement needed to continue acquaintance/i, + ), + ); const confirmation = screen.getByRole("button", { name: "Confirm", @@ -67,7 +55,11 @@ describe("FemaleOutcomeSheet", () => { it("requires a written note when the other cancellation reason is selected", async () => { renderSheet(); - await userEvent.click(screen.getByLabelText("Canceled")); + await userEvent.click( + screen.getByLabelText( + /We did not reach the agreement needed to continue acquaintance/i, + ), + ); await userEvent.click( screen.getByRole("button", { name: "Other reasons" }), ); diff --git a/src/components/Componentes/female-outcome-sheet.tsx b/src/components/Componentes/female-outcome-sheet.tsx index ee698d2..dda9a37 100644 --- a/src/components/Componentes/female-outcome-sheet.tsx +++ b/src/components/Componentes/female-outcome-sheet.tsx @@ -3,7 +3,6 @@ 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"; const EXIT_ANIMATION_MS = 200; @@ -100,7 +99,7 @@ export function FemaleOutcomeSheet({ return null; } - const isCanceledSwipeDisabled = + const isCanceledDisabled = selectedReasons.length === 0 || (selectedReasons.includes(reasons[reasons.length - 1]) && reasonText.trim() === ""); @@ -169,7 +168,7 @@ export function FemaleOutcomeSheet({ className={[ "flex cursor-pointer items-center gap-3 rounded-[14px] px-[14px] py-[18px] text-left border transition-all", outcome === "ongoing" - ? "bg-[#E8F8F3] border-[#00AC78]/30" + ? "bg-[#FFECEF] border-[#F0445B]/30" : "bg-[#ECECEC] border-transparent", ].join(" ")} > @@ -188,12 +187,12 @@ export function FemaleOutcomeSheet({ className={[ "flex h-6 w-6 shrink-0 items-center justify-center rounded-full border transition-colors", outcome === "ongoing" - ? "border-[#00AC78] bg-transparent" + ? "border-[#F0445B] bg-transparent" : "border-[#9E9E9E] bg-transparent", ].join(" ")} > {outcome === "ongoing" ? ( - + ) : null} @@ -205,7 +204,7 @@ export function FemaleOutcomeSheet({ - {/* Option B: Canceled */} + {/* Option B: Canceled / Decline */} @@ -339,35 +349,45 @@ export function FemaleOutcomeSheet({
- {/* SwipeButton Confirmation Area */} -
+ {/* Buttons Area */} +
+ + {outcome === "ongoing" ? ( - { + ) : ( - { + )}
diff --git a/src/components/Componentes/navigation-button.tsx b/src/components/Componentes/navigation-button.tsx index 4d452a0..35bddcc 100644 --- a/src/components/Componentes/navigation-button.tsx +++ b/src/components/Componentes/navigation-button.tsx @@ -10,7 +10,7 @@ import { isInFlutterWebView } from "@/lib/webview-actions"; import { useI18n } from "@/translations/provider"; import HelpModal from "./help-modal"; import InformationSheet from "./information-sheet"; -import { hasSupportAccess } from "./support-access"; +import { hasSupportAccess, openDirectSupportContact } from "./support-access"; import SupportSheet from "./support-sheet"; import TermsSheet from "./terms-sheet"; import { UiIcon } from "./ui-icon"; @@ -89,25 +89,7 @@ export function NavigationButton({ profile.active_subscription.is_valid !== false; const handleDirectSupportContact = () => { - if (typeof window !== "undefined" && "HabibApp" in window) { - try { - const app = ( - window as Window & { - HabibApp?: { postMessage: (msg: string) => void }; - } - ).HabibApp; - app?.postMessage( - JSON.stringify({ - action: "open_consultant_page", - data: { consultant: "habib@gmail.com" }, - }), - ); - } catch (err) { - console.error("Error calling HabibApp bridge", err); - } - } else if (typeof window !== "undefined") { - window.open("mailto:habib@gmail.com", "_blank"); - } + openDirectSupportContact(); }; const handleOpenSubscriptionModal = () => { diff --git a/src/components/Componentes/support-access.ts b/src/components/Componentes/support-access.ts index c15d30f..7f155d7 100644 --- a/src/components/Componentes/support-access.ts +++ b/src/components/Componentes/support-access.ts @@ -6,3 +6,30 @@ import type { MarriageProfile } from "@/hooks/marriage/types"; export function hasSupportAccess(profile?: MarriageProfile): boolean { return true; } + +/** + * Triggers direct support contact via HabibApp bridge or email fallback, + * identical to the three-dots menu Support button. + */ +export function openDirectSupportContact(): void { + if (typeof window !== "undefined" && "HabibApp" in window) { + try { + const app = ( + window as Window & { + HabibApp?: { postMessage: (msg: string) => void }; + } + ).HabibApp; + app?.postMessage( + JSON.stringify({ + action: "open_consultant_page", + data: { consultant: "habib@gmail.com" }, + }), + ); + } catch (err) { + console.error("Error calling HabibApp bridge", err); + } + } else if (typeof window !== "undefined") { + window.open("mailto:habib@gmail.com", "_blank"); + } +} + diff --git a/src/components/Componentes/swipe-button.tsx b/src/components/Componentes/swipe-button.tsx index 4b03f0d..d9c5199 100644 --- a/src/components/Componentes/swipe-button.tsx +++ b/src/components/Componentes/swipe-button.tsx @@ -13,7 +13,7 @@ type SwipeButtonProps = { disabled?: boolean; isLoading?: boolean; isSubmitting?: boolean; - theme?: "default" | "green"; + theme?: "default" | "green" | "pink"; }; export function SwipeButton({ @@ -58,8 +58,12 @@ export function SwipeButton({ }; const cancelLabel = cancelText || t?.["Cancel"] || "Cancel"; - const isGreen = theme === "green"; - const buttonBg = isGreen ? "bg-[#00AC78]" : "bg-[#F0445B]"; + const buttonBg = + theme === "green" + ? "bg-[#00AC78]" + : theme === "pink" + ? "bg-gradient-to-r from-[#FF6687] to-[#FF456C] shadow-[0_8px_16px_rgba(255,69,108,0.25)]" + : "bg-[#F0445B]"; const actionButton = (