PR: #767: feat(channels): steady-state My connection — connected, history, empty, loading, error [Story-3066] by @ayush-pramaan
View PR →
Story-3066 · Settings → Channels (WhatsApp)

My connection: connected, history, empty, loading, error

The steady-state half of the redesigned My connection view. Three separate white boxes — the connected card, the connect card, and the history list — were one object described three ways. They are now a single, coherent surface: one connection, its past lives, and a first-run empty state that shows a person how connecting works before they start. Loading and load-error round out the states a real firm actually hits.

What changed

One object, not three boxes

channel-binding-card.tsx, connect-channel-card.tsx and channel-history.tsx are deleted (stories too). In their place: channel-connection-card.tsx, channel-previous-connections.tsx, channel-empty-state.tsx + channel-connect-steps.tsx, and channel-load-error.tsx. No orphans left behind.

Connected card

The one live binding as an object: the WhatsApp glyph, a StatusBadge Active, the full E.164 number in font-mono-app, Connected {date}, and a Button outline Disconnect. Revoked bindings never appear here.

Previous connections

Revoked bindings become quiet history rows on --surface-app-page-subtle — number, a Disconnected badge, and the date plus why it ended (reasonUnbind / reasonAdminRevoke / reasonOffboard / reasonRemoteLogout, all pre-existing in both locales). Renders nothing when there is no history.

Empty first-run + 3-up step strip

Nothing bound: a centered WhatsApp glyph tile, the connect CTA, and a 3-up step strip on the subtle surface seamed into the same card — a mono 01/02/03 numeral + the mock's step copy per cell. Each cell has a divider on its right — except the last (last:border-r-0), so no 1px line seams against the card's own edge. Rows fill the full content width (no max-width cap), matching the register and the approved WhatsApp Settings mock.

Loading + load-error

Loading shows the neutral SettingsLoading. A failed bindings query shows ChannelLoadError — the destructive line + a PrimaryButton Retry with a loading state, matching Billing & Usage. Retry refetches the client query in place (distinct from the RSC-level CatalogLoadError).

Disconnect — reused, unchanged

The confirm (disconnect-binding-dialog.tsx) and the optimistic unbind hook (use-unbind-binding.ts) already landed in Story-3069 and already satisfy the AC. They are reused as-is: the confirm names the full number and states what dies; the hook flips the cache via optimisticCollectionPatch.

The five states (My connection)

  1. Connected. Open Settings → Firm → Channels → WhatsApp with an active binding: the connection card shows the glyph, Active badge, full number, bound date, and Disconnect. Confirming Disconnect flips the row to Disconnected instantly (optimistic), then the success toast lands once the write settles; a server rejection rolls back and toasts the error.
  2. Previous connections. Past bindings sit below on the subtle surface with their date and reason — read-only, nothing to disconnect.
  3. Empty first-run. A firm with zero bindings sees the connect CTA plus the 3-up step strip. This is the real first run — no fixture hides it.
  4. Loading. While the bindings query is in flight, the neutral spinner shows — no layout jump when data lands.
  5. Load error. If the query fails, the destructive notice + Retry appears; Retry refetches and shows its loading state until the list returns.

Acceptance criteria → where it lives

Criterion Implementation
ChannelConnectionCard — glyph, Active badge, full number (mono), Connected {date}, outline Disconnect channel-connection-card.tsx — WhatsAppGlyph, StatusBadge, providerSubject in font-mono-app, boundAt, Button variant="outline".
Previous-connections rows on --surface-app-page-subtle with reason text channel-previous-connections.tsx — rows on bg-(--surface-app-page-subtle), revokedAt + revocationReasonLabelKey.
Empty state + 3-up step strip; border-right removed from last cell channel-empty-state.tsx composes channel-connect-steps.tsx — grid-cols-3 with border-r + last:border-r-0.
Loading (SettingsLoading) + load-error; Retry is PrimaryButton with loading (like Billing) SettingsLoading in channels-page.tsx; channel-load-error.tsx — PrimaryButton loading loadingLabel={retrying}.
Disconnect confirm names full number + states what dies + reconnect possible Reused disconnect-binding-dialog.tsx → Channels.disconnectBody (already satisfied — see PR body).
Disconnect keeps optimisticCollectionPatch (instant flip, rollback + error toast) Reused use-unbind-binding.ts — optimisticCollectionPatch, await mutateAsync in try/catch.
channel-binding-card / connect-channel-card / channel-history deleted; nothing imports them All three + their stories git rm'd; rg shows zero import statements referencing them.
All new copy in en.json and hi.json messages/{en,hi}.json — retrying + connectStep{1,2,3} (step copy verbatim from the mock, Vani not "Mira"); reason/consent/status/empty keys reused.
Storybook covers every state Component stories per new component (incl. Hindi); channels-page.stories.tsx — Connected, Empty, With history, Loading, Load error, Hindi.

Scope notes

Consent carried forward, not redesigned

The deleted connect-channel-card also rendered the consent-required gate. That behaviour is carried into channel-empty-state.tsx unchanged so nothing regresses; the consent-variant redesign is Story-3068's scope, not this ticket's. The step strip is withheld while consent blocks the connect — there is nothing to walk through yet.

Tokens only

--surface-app-page-subtle for history rows and step badges, --surface-app-divider for the seams, font-mono-app for numbers. No hex / rgb / hsl in src/**.

Verification

Gate

pnpm lint (0 errors), pnpm test (1699 passed), pnpm build, pnpm check:routes (44 surfaces fresh), pnpm storybook:build, and git diff --check — all green.

i18n + tokens

en/hi key parity verified; the hardcoded-copy scan and the color-literal scan over the channels directory are both empty. The connection card, history rows, step strip, empty state and load-error each ship a Hindi story to prove the fixed-width chrome holds for the longer, taller Devanagari script.

Files changed

30 file(s), +691 / -313 lines

Path Role + -
messages/en.jsonConfig+5-1
messages/hi.jsonConfig+5-1
openapi/app-api.jsonConfig+11-1
src/components/settings/channels/channel-binding-card.stories.tsxComponent+0-47
src/components/settings/channels/channel-binding-card.tsxComponent+0-76
src/components/settings/channels/channel-connect-steps.stories.tsxComponent+47-0
src/components/settings/channels/channel-connect-steps.tsxComponent+37-0
src/components/settings/channels/channel-connection-card.stories.tsxComponent+52-0
src/components/settings/channels/channel-connection-card.tsxComponent+71-0
src/components/settings/channels/{connect-channel-card.stories.tsx => channel-empty-state.stories.tsx}Component+27-26
src/components/settings/channels/channel-empty-state.tsxComponent+94-0
src/components/settings/channels/channel-history.stories.tsxComponent+0-32
src/components/settings/channels/channel-history.tsxComponent+0-31
src/components/settings/channels/channel-load-error.stories.tsxComponent+44-0
src/components/settings/channels/channel-load-error.tsxComponent+42-0
src/components/settings/channels/channel-previous-connections.stories.tsxComponent+63-0
src/components/settings/channels/channel-previous-connections.tsxComponent+72-0
src/components/settings/channels/channels-page.stories.tsxComponent+68-12
src/components/settings/channels/channels-page.tsxComponent+17-17
src/components/settings/channels/connect-channel-card.tsxComponent+0-66
src/gen/client/appCreateMatterProceedingFiling.tsGenerated contract+2-0
src/gen/hooks/useAppCreateMatterProceedingFiling.tsGenerated contract+6-0
src/gen/index.tsGenerated contract+2-0
src/gen/schemas/filingCreateRequest.jsonGenerated contract+1-1
src/gen/types/AppCreateMatterProceedingFiling.tsGenerated contract+7-0
src/gen/types/FilingCreateRequest.tsGenerated contract+1-1
src/gen/types/index.tsGenerated contract+1-0
src/gen/zod/appCreateMatterProceedingFilingSchema.tsGenerated contract+10-0
src/gen/zod/filingCreateRequestSchema.tsGenerated contract+5-1
src/gen/zod/index.tsGenerated contract+1-0