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 (