import { useEffect, useRef, useState } from 'react'; import { AlertCircle, CheckCircle2, Clock, Loader2 } from 'lucide-react'; import { useOrganizationServiceContext } from '../../../services/OrganizationService.tsx'; import { useAuthServiceContext } from '../../../services/AuthService.tsx'; import { isSaveRetired, outcomeUnknown, useSaveScope, type SaveTransport } from '../../../utils/saveOperation'; import { onViewerChange } from '../../../utils/writeThroughHold'; import { Button } from '../../../components/ui/button.tsx'; import { Input } from '../../../components/ui/input.tsx'; import { Label } from '../../../components/ui/label.tsx'; import { Textarea } from '../../../components/ui/textarea.tsx'; import { Alert, AlertDescription } from '../../../components/ui/alert.tsx'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../components/ui/select.tsx'; import { relayServerFailure } from '../../../utils/fetchFailureMessage'; import SandboxDisabled from '../../../components/SandboxMode/SandboxDisabled'; import { environment } from '../../../environments/environment'; import type { SmsRegistrationForm, SmsRegistrationPublic } from '../../../config/smsRegistration.ts'; const emptyForm = (): SmsRegistrationForm => ({ legalName: '', ein: '', website: '', contactEmail: '', contactPhone: '', street: '', city: '', state: '', postalCode: '', entityType: 'private', transactionalSample: '', promotionalSample: '', optInDescription: '', }); const StatusLine = ({ status, pack, liveSubmit, }: { status: SmsRegistrationPublic['status']; pack?: SmsRegistrationPublic['package']; liveSubmit?: boolean; }) => { if (status === 'approved') { // Each stream sends only from its own approved number, and the read can report approval before a number; // a missing one is named rather than assumed. const missing = [!pack?.transactionalNumber && 'reminder', !pack?.promotionalNumber && 'marketing'].filter(Boolean); return ( {missing.length === 0 ? `Carrier registration is complete. Reminders can send only from this practice's reminder number (${pack?.transactionalNumber}) and marketing texts only from its marketing number (${pack?.promotionalNumber}); ` : `Carrier registration is complete, but this practice does not have an approved ${missing.join(' or ')} number yet, so those texts cannot send until one is assigned. `} The SMS add-on and the other sending requirements still apply. ); } if (status === 'rejected') { return ( The carriers asked for updates. Edit the form and submit again. Until it is approved again, do not count on text reminders or marketing texts. ); } if (status === 'pending' && liveSubmit === false) { return ( Carrier submission is not enabled here. The form is saved; it has not been sent to the carriers. Texts cannot send on it. ); } if (status === 'pending') { const step = !pack?.brand || pack.brand !== 'COMPLETE' ? 'Brand review is in progress (usually one to two business days).' : pack.vetting && pack.vetting !== 'COMPLETE' && pack.vetting !== 'DENIED' ? 'Brand vetting is in progress.' : !pack.transactionalCampaign || pack.transactionalCampaign !== 'COMPLETE' || !pack.promotionalCampaign || pack.promotionalCampaign !== 'COMPLETE' ? 'Use-case campaigns are in progress (this is the long wait, up to about four weeks).' : 'Waiting on 10DLC phone numbers.'; return ( {step} Check again to refresh the status AWS reports. Texts cannot send on this registration until it is approved. ); } return ( Text appointment reminders and marketing texts both need this carrier registration and the practice's own approved number for each: reminders send only from the reminder number, marketing only from the marketing number. The SMS add-on and the other sending requirements still apply. Saving or submitting this form does not start texting. Email reminders do not depend on it. ); }; const Field = ({ id, label, value, onChange, disabled, placeholder, }: { id: string; label: string; value: string; disabled: boolean; placeholder?: string; onChange: (value: string) => void; }) => (
onChange(event.target.value)} />
); const SmsRegistrationSection = () => { const { getSmsRegistration, saveSmsRegistration, submitSmsRegistration, checkSmsRegistration } = useOrganizationServiceContext(); const { currentOrganization } = useAuthServiceContext(); const practiceId = currentOrganization?.id; // Every read and action belongs to this practice and this signed-in person: a switch of either retires it, so // nothing it sends goes out afterwards and nothing it answers lands on the next practice. const scope = useSaveScope(practiceId); const [form, setForm] = useState(emptyForm); const [status, setStatus] = useState('none'); const [rejectionReason, setRejectionReason] = useState(); const [pack, setPack] = useState(); const [liveSubmit, setLiveSubmit] = useState(); const [busy, setBusy] = useState(false); const [error, setError] = useState(); // Nothing is saved or sent over a registration this form has not read. const [loaded, setLoaded] = useState(false); // An action whose answer was lost may have gone through; it is not offered again until the form is read again. const [unconfirmed, setUnconfirmed] = useState(false); const [readAgain, setReadAgain] = useState(0); const acting = useRef(false); const locked = status === 'pending' || status === 'approved'; const sandbox = environment.inSandboxMode; const readOnly = locked || sandbox; const apply = (payload: SmsRegistrationPublic) => { setStatus(payload.status); setRejectionReason(payload.rejectionReason); setPack(payload.package); setLiveSubmit(payload.liveSubmit); if (payload.form) setForm(payload.form); }; useEffect(() => { // The last practice's registration is gone before this one is asked for, whatever this one answers. setForm(emptyForm()); setStatus('none'); setRejectionReason(undefined); setPack(undefined); setLiveSubmit(undefined); setError(undefined); setLoaded(false); setUnconfirmed(false); setBusy(false); if (!practiceId) return undefined; scope.renew(); acting.current = false; const operation = scope.start(); setBusy(true); operation.step(() => getSmsRegistration(operation.transport)) .then((payload) => { if (!operation.belongs()) return; apply(payload); setLoaded(true); }) .catch((err) => { if (isSaveRetired(err) || !operation.belongs()) return; setError(relayServerFailure(err, 'Could not load text message registration.')); }) .finally(() => { if (operation.belongs()) setBusy(false); operation.retire(); }); return () => operation.retire(); }, [practiceId, getSmsRegistration, scope, readAgain]); // Another person signed in on this page reads the registration afresh; nothing of the last one carries over. useEffect(() => onViewerChange(() => setReadAgain((count) => count + 1)), []); const set = (key: keyof SmsRegistrationForm) => (value: string) => setForm((current) => ({ ...current, [key]: value })); const run = async (work: (transport: SaveTransport) => Promise) => { if (sandbox || !loaded || unconfirmed || acting.current) return; acting.current = true; const operation = scope.start(); setBusy(true); setError(undefined); try { const payload = await operation.step(() => work(operation.transport), true); if (!operation.belongs()) return; apply(payload); } catch (err) { if (isSaveRetired(err) || !operation.belongs()) return; if (outcomeUnknown(err)) { setUnconfirmed(true); setError('The answer was lost, so this may have gone through. Read the registration again before trying again.'); return; } setError(relayServerFailure(err, 'Could not update text message registration.')); } finally { if (operation.belongs()) { setBusy(false); acting.current = false; } operation.retire(); } }; const actionsOff = busy || sandbox || !loaded || unconfirmed; return (

Text message registration

{rejectionReason && status === 'rejected' && (

{rejectionReason}

)} {error &&

{error}

}