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
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.
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.
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.
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 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).
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)
-
Connected. Open
Settings → Firm → Channels → WhatsAppwith 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. - Previous connections. Past bindings sit below on the subtle surface with their date and reason — read-only, nothing to disconnect.
- 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.
- Loading. While the bindings query is in flight, the neutral spinner shows — no layout jump when data lands.
- 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
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.
--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
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.
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.json | Config | +5 | -1 |
| messages/hi.json | Config | +5 | -1 |
| openapi/app-api.json | Config | +11 | -1 |
| src/components/settings/channels/channel-binding-card.stories.tsx | Component | +0 | -47 |
| src/components/settings/channels/channel-binding-card.tsx | Component | +0 | -76 |
| src/components/settings/channels/channel-connect-steps.stories.tsx | Component | +47 | -0 |
| src/components/settings/channels/channel-connect-steps.tsx | Component | +37 | -0 |
| src/components/settings/channels/channel-connection-card.stories.tsx | Component | +52 | -0 |
| src/components/settings/channels/channel-connection-card.tsx | Component | +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.tsx | Component | +94 | -0 |
| src/components/settings/channels/channel-history.stories.tsx | Component | +0 | -32 |
| src/components/settings/channels/channel-history.tsx | Component | +0 | -31 |
| src/components/settings/channels/channel-load-error.stories.tsx | Component | +44 | -0 |
| src/components/settings/channels/channel-load-error.tsx | Component | +42 | -0 |
| src/components/settings/channels/channel-previous-connections.stories.tsx | Component | +63 | -0 |
| src/components/settings/channels/channel-previous-connections.tsx | Component | +72 | -0 |
| src/components/settings/channels/channels-page.stories.tsx | Component | +68 | -12 |
| src/components/settings/channels/channels-page.tsx | Component | +17 | -17 |
| src/components/settings/channels/connect-channel-card.tsx | Component | +0 | -66 |
| src/gen/client/appCreateMatterProceedingFiling.ts | Generated contract | +2 | -0 |
| src/gen/hooks/useAppCreateMatterProceedingFiling.ts | Generated contract | +6 | -0 |
| src/gen/index.ts | Generated contract | +2 | -0 |
| src/gen/schemas/filingCreateRequest.json | Generated contract | +1 | -1 |
| src/gen/types/AppCreateMatterProceedingFiling.ts | Generated contract | +7 | -0 |
| src/gen/types/FilingCreateRequest.ts | Generated contract | +1 | -1 |
| src/gen/types/index.ts | Generated contract | +1 | -0 |
| src/gen/zod/appCreateMatterProceedingFilingSchema.ts | Generated contract | +10 | -0 |
| src/gen/zod/filingCreateRequestSchema.ts | Generated contract | +5 | -1 |
| src/gen/zod/index.ts | Generated contract | +1 | -0 |