diff --git a/src/app/finding-match/finding-match-client.tsx b/src/app/finding-match/finding-match-client.tsx index e8b0b6d..48cc5be 100644 --- a/src/app/finding-match/finding-match-client.tsx +++ b/src/app/finding-match/finding-match-client.tsx @@ -195,15 +195,17 @@ export default function FindingMatchClient() { id="rejection-notice-title" className="text-[16px] font-bold leading-[1.3] text-[#171717]" > - {t["Your request was rejected"]} + {t["Your request was declined"] || + t["Your request was rejected"]}

- { + {t[ + "Your request was declined by the lady. You will be introduced to other candidates in the future." + ] || t[ "Your request was rejected by the lady. You will be introduced to other candidates in the future." - ] - } + ]}

diff --git a/src/app/intro/intro-client.test.tsx b/src/app/intro/intro-client.test.tsx index ab1a1d3..22f6982 100644 --- a/src/app/intro/intro-client.test.tsx +++ b/src/app/intro/intro-client.test.tsx @@ -1,4 +1,4 @@ -import { render, screen, cleanup } from "@testing-library/react"; +import { render, screen, cleanup, fireEvent } from "@testing-library/react"; import { describe, it, expect, vi, afterEach } from "vitest"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import IntroClient from "./intro-client"; @@ -52,6 +52,18 @@ vi.mock("@/hooks/use-hardware-back-handler", () => ({ useHardwareBackHandler: vi.fn(), })); +vi.mock("@/components/Componentes/slider-page", () => ({ + default: () =>
Slider Content
, +})); + +vi.mock("@/lib/auth-bridge", () => ({ + authBridge: { + isAuthenticated: vi.fn(() => false), + ensureToken: vi.fn(() => Promise.resolve(null)), + getToken: vi.fn(() => null), + }, +})); + describe("IntroClient", () => { afterEach(() => { cleanup(); @@ -73,4 +85,18 @@ describe("IntroClient", () => { // Video thumbnail image should NOT be rendered expect(screen.queryByAltText("video")).not.toBeInTheDocument(); }); + + it("opens onboarding steps when Submit is clicked even if user has no auth token", async () => { + const queryClient = new QueryClient(); + render( + + + , + ); + + const submitButton = screen.getByText("Submit"); + fireEvent.click(submitButton); + + expect(await screen.findByTestId("slider-page")).toBeInTheDocument(); + }); }); diff --git a/src/app/intro/intro-client.tsx b/src/app/intro/intro-client.tsx index f342b90..7fd3d68 100644 --- a/src/app/intro/intro-client.tsx +++ b/src/app/intro/intro-client.tsx @@ -96,7 +96,10 @@ export default function IntroClient() { if (!authBridge.isAuthenticated()) { const token = await authBridge.ensureToken(); if (!token) { - console.warn("No token from bridge – login was not completed"); + console.warn( + "No token from bridge – opening intro onboarding steps", + ); + handleOpenSteps(); return; } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index ce81bfa..2752b60 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -226,6 +226,17 @@ export default async function RootLayout({ }); } + // Check URL query parameters for auth token on initial script execution + try { + if (window.location && window.location.search) { + var searchParams = new URLSearchParams(window.location.search); + var urlTok = searchParams.get('token') || searchParams.get('auth_token') || searchParams.get('habib_token') || searchParams.get('HABIB_TOKEN'); + if (urlTok && urlTok.trim() !== '' && urlTok !== 'NO_TOKEN') { + window.HABIB_TOKEN = urlTok.trim(); + } + } + } catch (e) {} + if (!Object.getOwnPropertyDescriptor(window, 'HABIB_COINS')) { Object.defineProperty(window, 'HABIB_COINS', { configurable: true, @@ -253,6 +264,19 @@ export default async function RootLayout({ if (!config) return; configApplied = true; window.__HABIB_BOOTSTRAP__ = config; + + var configToken = + config.token || + config.auth_token || + config.authToken || + (config.data && (config.data.token || config.data.auth_token || config.data.authToken)) || + (config.payload && (config.payload.token || config.payload.auth_token || config.payload.authToken)); + + if (configToken && typeof configToken === 'string' && configToken.trim() !== '' && configToken !== 'NO_TOKEN') { + console.log('⚡ [Layout Bootstrap] Applying auth token from Flutter bootstrap config'); + window.HABIB_TOKEN = configToken.trim(); + } + var marriageData = config.marriage || config.marriageData || diff --git a/src/app/new-match/new-match-client.tsx b/src/app/new-match/new-match-client.tsx index d841eaf..f228e10 100644 --- a/src/app/new-match/new-match-client.tsx +++ b/src/app/new-match/new-match-client.tsx @@ -176,8 +176,10 @@ import { formatFieldLabel, formatFieldValue, formatOptionValue, + getOrderedSummaryFields, isMarriagePhoneFieldValue, titleFromKey, + type SummaryFieldItem, } from "@/lib/marriage-field-formatter"; function toDisplayField( @@ -267,143 +269,19 @@ function useMatchSummaryDisplay( displayName = `Profile #${matchSummary.id}`; } - // 2. Age (extract and calculate from date_of_birth if available) - const dobIdx = fields.findIndex( - (f) => - f.key === "personal_identity.date_of_birth" || - f.key?.endsWith(".date_of_birth") || - f.key?.toLowerCase().includes("date_of_birth") || - f.key?.toLowerCase().includes("birth_date"), - ); - let age: DisplayField | null = null; - if (dobIdx !== -1 && fields[dobIdx].value) { - usedIndexes.add(dobIdx); - const calculatedAge = calculateAgeFromDob(String(fields[dobIdx].value)); - if (calculatedAge) { - age = { - id: fields[dobIdx].key, - label: t ? t["Age"] || "Age" : "Age", - value: `${calculatedAge}`, - }; - } - } - if (!age) { - age = pickField(fields, fieldCandidateMatchers.age, usedIndexes, t); - } - - // 3. Country of Current Residence - let currentCountry = pickField( - fields, - fieldCandidateMatchers.currentCountry, - usedIndexes, - t, - ); - - // 4. City / State of Current Residence - let currentCity = pickField( - fields, - fieldCandidateMatchers.currentCity, - usedIndexes, - t, - ); - - // If either country or city was not matched as a standalone field, check composite residence - if (!currentCountry || !currentCity) { - const residenceIdx = fields.findIndex( - (f, idx) => - !usedIndexes.has(idx) && - matchesCandidate(f, fieldCandidateMatchers.residence), - ); - - if (residenceIdx !== -1) { - const residenceField = fields[residenceIdx]; - if ( - typeof residenceField.value === "object" && - residenceField.value !== null && - !Array.isArray(residenceField.value) - ) { - const valObj = residenceField.value as { - country?: string; - city?: string; - state?: string; - }; - if (valObj.country && !currentCountry) { - currentCountry = { - id: `${residenceField.key}.country`, - label: - (t && - (t["Current Country of Residence"] || - t["Country of Residence"] || - t["Country"])) || - "Country of Residence", - value: formatOptionValue(valObj.country, t) || valObj.country, - }; - } - const cityVal = [valObj.city, valObj.state].filter(Boolean).join(", "); - if (cityVal && !currentCity) { - currentCity = { - id: `${residenceField.key}.city`, - label: - (t && - (t["Current City / State of Residence"] || - t["City / State of Residence"] || - t["City"])) || - "City / State of Residence", - value: formatOptionValue(cityVal, t) || cityVal, - }; - } - usedIndexes.add(residenceIdx); - } else if (!currentCountry && !currentCity) { - const disp = toDisplayField(residenceField, t); - if (disp) { - usedIndexes.add(residenceIdx); - currentCountry = disp; - } - } - } - } - - // 5. Highest level of education - const educationLevel = pickField( - fields, - fieldCandidateMatchers.educationLevel, - usedIndexes, - t, - ); - - // 6. Field of study - const fieldOfStudy = pickField( - fields, - fieldCandidateMatchers.fieldOfStudy, - usedIndexes, - t, - ); - - // 7. Job Title - const jobTitle = pickField( - fields, - fieldCandidateMatchers.jobTitle, - usedIndexes, - t, - ); - - // 8. Hobbies & Main Interests - const hobbies = pickField( - fields, - fieldCandidateMatchers.hobbies, - usedIndexes, - t, - ); + // 7 ordered summary items: + // 1. Place of Birth (Country and City) + // 2. Current Place of Residence (Country, City / State) + // 3. Height in Centimeters + // 4. What is your highest completed formal educational degree? + // 5. Field of Study + // 6. What is your current employment status? + // 7. Job Title and Field of Activity + const items = getOrderedSummaryFields(fields, t); return { name: displayName, - age, - currentCountry, - currentCity, - educationLevel, - fieldOfStudy, - jobTitle, - hobbies, + items, }; }, [matchSummary, t]); } @@ -817,54 +695,13 @@ export default function NewMatchClient() { {/* Info Items List */}
- {matchDisplay.age && ( + {matchDisplay.items.map((item) => ( } + key={item.id} + field={item} + icon={} /> - )} - - {matchDisplay.currentCountry && ( - } - /> - )} - - {matchDisplay.currentCity && ( - } - /> - )} - - {matchDisplay.educationLevel && ( - } - /> - )} - - {matchDisplay.fieldOfStudy && ( - } - /> - )} - - {matchDisplay.jobTitle && ( - } - /> - )} - - {matchDisplay.hobbies && ( - } - /> - )} + ))} {/* Button */}
)} - onClose={() => setIsRejectSheetOpen(false)} + onClose={() => setIsDeclineSheetOpen(false)} /> ) : null} - {isMaleRejectWarningOpen ? ( + {isMaleDeclineWarningOpen ? ( - {t.Reject} + {t.Decline || t["Decline"] || t.Reject} )} - onClose={() => setIsMaleRejectWarningOpen(false)} + onClose={() => setIsMaleDeclineWarningOpen(false)} /> ) : null} {isDismissReasonSheetOpen ? ( @@ -683,7 +671,6 @@ export default function NewMatchProfilePage({ @@ -692,7 +679,10 @@ export default function NewMatchProfilePage({ style={{ paddingBottom: `calc(16px + var(--safe-bottom, 0px))` }} className="shrink-0 z-30 w-full rounded-t-[24px] bg-white px-[17px] pt-4 shadow-[0_-4px_24px_rgba(0,0,0,0.08)]" > - {caseStatus === "payment_done" || + {(Boolean(onClose) && !isAcceptProfileEnabled) || + caseStatus === "female_accepted" || + caseStatus === "payment_pending" || + caseStatus === "payment_done" || caseStatus === "contacted" || caseStatus === "finalized" || profile?.status === "matched" ? ( @@ -707,17 +697,17 @@ export default function NewMatchProfilePage({
+ onSuccess={async () => { + try { + await handleNoContactReport(); + setIsNoContactConfirmOpen(false); + } catch (err) { + console.error("Failed to report no contact", err); + } + }} + /> } - onClose={() => setIsNoContactConfirmOpen(false)} - closeOnOutside={true} + onClose={() => { + if (!contactStatusMutation.isPending) { + setIsNoContactConfirmOpen(false); + } + }} + closeOnOutside={!contactStatusMutation.isPending} /> ) : null} @@ -670,19 +694,19 @@ export default function RequestAcceptedClient() {

)}
+ ) : noContactReportedSuccess ? ( +

+ {t["Your report has been submitted to support."]} +

) : (

- {noContactReportedSuccess + {isFemaleProfile ? t[ - "Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you." + "The selected candidate will contact your family shortly." ] - : isFemaleProfile - ? t[ - "The selected candidate will contact your family shortly." - ] - : t[ - "You can now view their family's contact details and arrange further steps." - ]} + : t[ + "You can now view their family's contact details and arrange further steps." + ]}

)} @@ -693,18 +717,33 @@ export default function RequestAcceptedClient() {
{isFemaleProfile && contactStatusMutation.isPending ? null : isFemaleProfile ? ( - + <> + + + + ) : ( <> @@ -735,70 +774,116 @@ export default function RequestAcceptedClient() { )}
+ ) : noContactReportedSuccess ? ( +
+
+
+ +
+

+ {t["Report Registered"] || "Report Registered"} +

+

+ {t[ + "Thank you for your feedback. Our support team will investigate the matter and notify you of the result. Please wait patiently during the review; our support will contact you." + ]} +

+
+ + +
) : ( -
- {isFemaleProfile ? ( - - ) : ( +
+ {isFemaleProfile && ( )} - ) : ( -
- {paymentMutation.isPending || isFetchingContact ? ( - +
+ )} - + + +
)} diff --git a/src/components/Componentes/dismiss-reason-sheet.tsx b/src/components/Componentes/dismiss-reason-sheet.tsx index 789ad7f..13b8462 100644 --- a/src/components/Componentes/dismiss-reason-sheet.tsx +++ b/src/components/Componentes/dismiss-reason-sheet.tsx @@ -146,11 +146,12 @@ export function DismissReasonSheet({

- { + {t[ + "Please provide the full reason for declining the submitted item" + ] || t[ "Please provide the full reason for rejecting the submitted item" - ] - } + ]}

void; + onSuccess: () => void | Promise; onCancel?: () => void; text: string; cancelText?: string; disabled?: boolean; isLoading?: boolean; + isSubmitting?: boolean; theme?: "default" | "green"; }; @@ -22,10 +23,11 @@ export function SwipeButton({ cancelText, disabled = false, isLoading = false, + isSubmitting = false, theme = "default", }: SwipeButtonProps) { const { dictionary: t } = useI18n(); - const [clicked, setClicked] = useState(false); + const [internalSubmitting, setInternalSubmitting] = useState(false); if (isLoading) { if (onCancel) { @@ -43,9 +45,16 @@ export function SwipeButton({ ); } - const handleClick = () => { - setClicked(true); - onSuccess(); + const busy = isSubmitting || internalSubmitting; + + const handleClick = async () => { + if (disabled || busy) return; + setInternalSubmitting(true); + try { + await onSuccess(); + } finally { + setInternalSubmitting(false); + } }; const cancelLabel = cancelText || t?.["Cancel"] || "Cancel"; @@ -55,12 +64,12 @@ export function SwipeButton({ const actionButton = (