6 file(s), +173 / -63 lines
| Path | Role | + | - |
|---|---|---|---|
| src/components/settings/channels/channel-consent-required.stories.tsx | Component | +80 | -0 |
| src/components/settings/channels/channel-consent-required.tsx | Component | +70 | -0 |
| src/components/settings/channels/channel-empty-state.stories.tsx | Component | +0 | -16 |
| src/components/settings/channels/channel-empty-state.tsx | Component | +3 | -40 |
| src/components/settings/channels/channels-page.tsx | Component | +8 | -6 |
| src/components/settings/channels/use-channel-enrollment.ts | Component | +12 | -1 |
Connecting WhatsApp is consent-gated. POST /app/channels/enrollment-codes
403s with pramaan.channel_consent.required until a firm admin accepts the
current channel data-transit consent version (RFD-AGT-013 D21).
This is the first-run state of every firm —
accept_current_channel_consent is never granted at provisioning, so no firm
holds channel consent until an admin clicks Accept. This Story does not build that flow (it
was already wired and live); it dresses the gate in the new design language, makes the
accept-failure path visible, and splits the card into its own component.
Open My connection with no binding and no consent → the amber gate blocks the connect → an admin taps Accept channel consent → the enrolment-code request auto-retries and the connect flow resumes. A member sees the same card, worded for them, with no button.
| Surface | Change |
|---|---|
| New component |
The consent gate is now a sibling, channel-consent-required.tsx. Previously
it was a second, unrelated card living behind an early return inside
channel-empty-state.tsx — one file, two responsibilities. Each card now owns
one responsibility; the page (channels-page.tsx) is the orchestrator that
chooses between them.
|
| Design language |
The old plain white box is now an amber warning card fronted by a
shield-alert mark (lucide). Every colour maps to an existing token:
background --surface-app-warning, border
--amber-warning-border, text --surface-app-warning-ink. All
three were confirmed present (the first and third defined in
src/app/globals.css over the amber palette; the border in
design/v1/colors_and_type.css) and are already used together by
enrollment-timer-pill.tsx. No hex / rgb / hsl literal was introduced.
|
| Admin variant |
Amber card + consentRequiredAdminBody copy + a PrimaryButton
“Accept channel consent” that shows its loading state
(acceptingConsent) through the round-trip.
|
| Member variant |
The same amber card with consentRequiredMemberBody copy and
no button — a non-admin POSTing consent gets a 403, so the member is
told which admin action is needed rather than handed a button that would fail.
|
| Accept-failure state (the real gap) |
Before, a failed accept only fired an ephemeral sonner toast and reset the
card to its plain required state — an admin who clicked Accept and hit a failure was
left with no persistent explanation. Now the hook tracks a distinct
acceptFailed flag and the card renders a persistent inline
role="alert" error (couldNotAcceptConsent) under the copy, with
the Accept button still available to retry. The toast is kept as the immediate cue.
|
| Copy |
No new copy. The six existing Channels.consent* /
acceptConsent* keys (title, admin body, member body,
acceptConsent, acceptingConsent,
couldNotAcceptConsent) are reused, and each already exists in both
messages/en.json and messages/hi.json.
|
use-channel-enrollment.ts keeps its control flow. On a successful, granted
accept the onSuccess chain still calls issueCode.mutate(), so the
enrolment-code request auto-retries and the connect flow resumes with no second click. The
only addition is the acceptFailed state: set on a rejected accept
and on a returned granted: false; cleared on retry
(onMutate) and on any success.
Settings/Channels/ChannelConsentRequired → Consent blocked — firm admin — amber gate with the Accept CTA.Settings/Channels/ChannelConsentRequired → Consent blocked — member — same card, member copy, no button.Settings/Channels/ChannelConsentRequired → Accept failed — the persistent inline error under the copy, button re-enabled.Settings/Channels/ChannelConsentRequired → Accepting — the CTA holding its loading state.Settings/Channels/ChannelConsentRequired → Hindi — the amber card against longer Devanagari copy.Settings/Channels/ChannelEmptyState — unchanged first-run / connecting / Hindi stories (consent stories moved to the sibling; no orphan left).pnpm lint — 0 errors. pnpm test — 1699 passed.pnpm build and pnpm storybook:build — both succeed.pnpm check:routes — surface registry fresh (44 surfaces).git diff --check — clean.
Storybook and unit tests prove every state renders and that the failure path is now visible
and persistent. They do not exercise the live POST /app/channels/consent/accept
against a real, never-granted firm. Because this is the first-run state of every firm, the
real proof is a new firm reaching My connection, seeing the amber admin gate, clicking Accept,
and the enrolment code being issued on the auto-retry — and a member on the same firm seeing
the buttonless card. That end-to-end run against real data belongs to UAT on a deployed
environment and remains open until performed.