Opt-in form
An SMS consent form whose exact shape survived a real TCR/carrier review. Not legal advice — but it is field-tested compliance UI, free to restyle.
Try both paths: submitting with and without the box checked.
Installation
npx shadcn@latest add @handset/opt-in-formNo API dependency — you decide where submissions go.
Why it looks like this
Carrier reviewers rejected an earlier version of this form for "forced opt-in": a required phone field plus a required consent checkbox means the user can't complete the form without agreeing to marketing. The compliant shape — the one that passed — makes the checkbox explicitly optional, labels it so, and treats an unchecked submission as "store nothing, send nothing," with a visible "not enrolled" result. The disclosure covers the CTIA checklist: brand name, message frequency, "consent is not a condition of purchase," msg & data rates, STOP/HELP, and links to your privacy policy and terms.
Usage
import { OptInForm } from "@/components/handset/opt-in-form";
<OptInForm
brandName="Brightside Property Co" // as registered on your campaign
messageFrequency="up to 4 messages per month"
privacyUrl="/privacy"
termsUrl="/terms"
onSubmit={async ({ phone, consented }) => {
if (!consented) return; // store nothing, send nothing
await fetch("/api/optins", { // your consent registry
method: "POST",
body: JSON.stringify({ phone, language: "v1", at: new Date() }),
});
}}
/>Record the exact disclosure language and timestamp with each consent — that record is what you show a carrier auditor. Send the confirmation text through your Handset number so STOP handling is automatic.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| brandName | string | — | Your brand as registered on the 10DLC campaign. |
| messageFrequency | string | "recurring messages" | Match your campaign registration, e.g. "up to 4 messages per month". |
| privacyUrl | string | — | Linked in the disclosure. |
| termsUrl | string | — | Linked in the disclosure. |
| onSubmit | ({ phone, consented }) => Promise<void> | — | Receives every submission; consented=false must mean no enrollment. |
| className | string | — | Merged onto the form. |