import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { AnswerBlock } from '../components/signup-mirror/answer-block'; import { MirrorFooter } from '../components/signup-mirror/mirror-footer'; import { MirrorHeader } from '../components/signup-mirror/mirror-header'; import { MirrorTopBar } from '../components/signup-mirror/mirror-topbar'; import { MoneyMakerBlock } from '../components/signup-mirror/money-maker-block'; import { SANS } from '../components/signup-mirror/theme'; import { Spinner } from '../components/ui/Spinner'; import { useAppStateContext } from '../context/user.data.context'; import { MirrorPayload } from '../service/company-id/company-id.interface'; import { confirmCompanyIdMirror, dispatchCompanyIdMirrorScan, getCompanyIdMirror, } from '../service/company-id/company-id.service'; const SCAN_POLL_MS = 4000; const WORK_TIMEOUT_MS = 25000; type AnswerState = Record; const PageLoading = (): JSX.Element => (
); const SignupMirror = (): JSX.Element => { const navigate = useNavigate(); const { token, clientId } = useAppStateContext(); const [mirror, setMirror] = useState(null); const [loadingMirror, setLoadingMirror] = useState(true); const [workTimedOut, setWorkTimedOut] = useState(false); const [answers, setAnswers] = useState({}); const [money, setMoney] = useState(['', '', '']); const [showHelper, setShowHelper] = useState(false); const [saving, setSaving] = useState(false); const seededRef = useRef(false); const scanDispatchedRef = useRef(false); const textareas = useRef>({}); const blocks = mirror?.blocks ?? []; const scanning = !!mirror && !mirror.scanned && !workTimedOut; const populating = !!mirror && !mirror.populated && !workTimedOut; const working = scanning || populating; const refreshMirror = useCallback(async () => { if (!clientId || !token) return; try { const fresh = await getCompanyIdMirror(clientId, token); setMirror(fresh); } catch (error) { // keep the last snapshot on transient errors console.error(error); } }, [clientId, token]); // Initial load - render immediately once the mirror arrives instead of // gating the whole screen behind a spinner. useEffect(() => { if (!clientId || !token) return undefined; let cancelled = false; (async () => { try { const fresh = await getCompanyIdMirror(clientId, token); if (cancelled) return; setMirror(fresh); if (!seededRef.current) { seededRef.current = true; setAnswers( Object.fromEntries( fresh.blocks.map(block => [ block.field_id, { value: block.value, confirmed: false }, ]) ) ); setMoney(fresh.money.length === 3 ? fresh.money : ['', '', '']); } } catch (error) { console.error(error); } finally { if (!cancelled) setLoadingMirror(false); } })(); return () => { cancelled = true; }; }, [clientId, token]); // Fire the scan exactly once, guarded by a ref (survives re-renders without // re-triggering, unlike a plain state flag which could race with polling). useEffect(() => { if (!scanning || scanDispatchedRef.current || !clientId || !token) return; scanDispatchedRef.current = true; dispatchCompanyIdMirrorScan(clientId, token).catch(error => { console.error(error); }); }, [scanning, clientId, token]); // Poll while working, stop at scanned && populated or the 25s hard cap. useEffect(() => { if (!working) return undefined; const timer = setInterval(refreshMirror, SCAN_POLL_MS); return () => clearInterval(timer); }, [working, refreshMirror]); useEffect(() => { if (!mirror || (mirror.scanned && mirror.populated)) return undefined; const timer = setTimeout(() => setWorkTimedOut(true), WORK_TIMEOUT_MS); return () => clearTimeout(timer); }, [mirror]); // Fill in freshly-populated site-read values without clobbering edits the // merchant has already started typing. useEffect(() => { const currentBlocks = mirror?.blocks ?? []; setAnswers(prev => { let changed = false; const next = { ...prev }; for (const block of currentBlocks) { if (!prev[block.field_id]?.value && block.value) { next[block.field_id] = { value: block.value, confirmed: prev[block.field_id]?.confirmed ?? false, }; changed = true; } } return changed ? next : prev; }); }, [mirror]); const confirmedCount = blocks.filter( block => answers[block.field_id]?.confirmed ).length; const done = blocks.length > 0 && confirmedCount === blocks.length; const moneyFilled = money.some(entry => entry.trim().length > 0); const ctaEnabled = done && moneyFilled && !saving; const setValue = (key: string, value: string) => setAnswers(prev => ({ ...prev, [key]: { ...prev[key], value } })); const confirm = (key: string) => setAnswers(prev => prev[key]?.value.trim() ? { ...prev, [key]: { ...prev[key], confirmed: true } } : prev ); const reopen = (key: string) => setAnswers(prev => ({ ...prev, [key]: { ...prev[key], confirmed: false }, })); const fixIt = (key: string) => { reopen(key); const node = textareas.current[key]; if (node) node.focus(); }; const setMoneyAt = (index: number, value: string) => setMoney(prev => prev.map((entry, entryIndex) => (entryIndex === index ? value : entry)) ); const onCta = async (): Promise => { if (saving) return; if (!ctaEnabled) { setShowHelper(true); return; } if (!clientId || !token) return; const values = Object.fromEntries( blocks.map(block => [block.field_id, answers[block.field_id].value]) ); setSaving(true); try { await confirmCompanyIdMirror(clientId, token, values, money); navigate('/launch-discount'); } catch (error) { console.error(error); setSaving(false); } }; if (!token || !clientId || loadingMirror) { return ; } if (!mirror) { return (
The Signup Mirror could not be loaded. Refresh to retry.
); } return (
{blocks.map(block => ( setValue(block.field_id, value)} onConfirm={() => confirm(block.field_id)} onFixIt={() => fixIt(block.field_id)} onReopen={() => reopen(block.field_id)} registerTextarea={node => { textareas.current[block.field_id] = node; }} /> ))}
[ block.field_id, !!answers[block.field_id]?.confirmed, ]) )} working={working} confirmedCount={confirmedCount} ctaEnabled={ctaEnabled} saving={saving} showHelper={showHelper} onCta={onCta} onHelperChange={setShowHelper} />
); }; export default SignupMirror;