Compliance hooks

The headless layer behind the 10DLC and E911 forms: register brands, campaigns, and emergency addresses, and follow a registration to approval. Added in @handset/react 0.4.0.

useCompliance

const { registerBrand, registerCampaign, registerE911, isSubmitting, error, reset } = useCompliance();

const brand = await registerBrand({
  legalName: "Bayview Dental LLC",
  ein: "12-3456789",
  entityType: "private_company",
  contactEmail: "ops@bayviewdental.com",
  phone: "+14155550142",
  street: "500 Ocean Ave", city: "San Francisco", state: "CA", postalCode: "94112",
});

const campaign = await registerCampaign({
  tenantId, brandId: brand.id,
  useCase: "appointment_reminders",
  description: "Appointment reminders and reschedule links.",
  sampleMessages: ["Your appointment is tomorrow at 2 PM. Reply C to confirm.", "Reply R to reschedule."],
  optInDescription: "Patients check a consent box on the booking form and agree to appointment texts.",
});

await registerE911({ tenantId, street: "500 Ocean Ave", city: "San Francisco", state: "CA", postalCode: "94112" });

Each mutation takes camelCase input, maps it to the API body, and resolves with the created resource (or throws — error holds the last failure, isSubmitting is true while any one is in flight). The API enforces the carrier rules the forms mirror: a valid EIN and E.164 phone, 2–5 sampleMessages, and an optInDescription of at least 40 characters. This is the hook behind brand-registration-form, campaign-registration-form, and e911-address-form.

useBrand / useCampaign

const { brand } = useBrand(brandId, { pollMs: 15000 });
brand?.status;            // "pending" → "approved" | "rejected"
brand?.rejection_reason;  // set when rejected

const { campaign } = useCampaign(campaignId);
campaign?.status;
campaign?.throughput;     // { messages_per_minute, daily_cap } once approved

Both read a single registration and poll while it's pending, automatically stopping once it settles (approved or rejected) so you're not polling a decided brand forever. Pass pollMs: 0 to fetch once. This pair drives the compliance-status component.

Settled check

import { complianceSettled } from "@handset/react";

complianceSettled(brand?.status); // false while pending, true once approved/rejected

The same predicate the hooks use to decide when to stop polling — handy if you drive your own UI off a registration's status.