What
A single React component that owns every post-conversion state on My Decks / job rows, replacing the current ad-hoc mix of error strings, paywall banners, and "download" rows with one consistent surface.
Why
From the W20 retro (docs/retros/2026-W19.md):
- 73 % of users who hit a Python conversion crash never log back in (37 of 51 in the last 90 days).
- 65 % of users who hit the paywall cap never return — the cancel message has no upgrade CTA today.
Both bounce paths are caused by the same UX gap: raw error strings + no recovery action.
Variants (8)
Each variant: one-line headline (user voice, no UUIDs / no stack traces), one-line "what to do next", one primary action, one secondary action. Variants:
success — "34 cards in Pharmacology. Ready to download." + Download / Open in Anki guide
processing — "Converting your Notion page…" + (no actions)
paywalled — "You've hit 100 cards this month. Upgrade for unlimited." + Upgrade / Plans
failed-empty — Notion page had no content. Try a different page / Help
failed-permissions — Notion didn't share the page. Re-share / Notion permissions guide
failed-unsupported — File type isn't supported. Use a supported format / Help
failed-timeout — Conversion took too long. Try splitting / Contact support
failed-other — Generic fallback. Try again / Contact support (no raw error string)
The five failed-* variants cover ~95 % of all failures from the 90-day retro window.
Where it lands
web/src/pages/DownloadsPage/components/ConversionResult.tsx (new)
- Replaces the inline error rendering in
UploadObjectEntry.tsx
- Reuses the
PaywallBanner visual pattern (already validated by users)
Acceptance
- All 8 variants render in Storybook / vite preview
- Every variant has user-voice copy reviewed against
VOICE.md (no "Oops", no exclamation, no implementation jargon)
- No raw error message, no ISO timestamp, no job UUID in body copy
- 30-day metric after ship: "never returned after failure" rate drops from 73 % toward < 40 %
Out of scope
Effort: M (~1 week). From the W20 trio analysis; closed PR #2206 is the source of these numbers.
What
A single React component that owns every post-conversion state on My Decks / job rows, replacing the current ad-hoc mix of error strings, paywall banners, and "download" rows with one consistent surface.
Why
From the W20 retro (
docs/retros/2026-W19.md):Both bounce paths are caused by the same UX gap: raw error strings + no recovery action.
Variants (8)
Each variant: one-line headline (user voice, no UUIDs / no stack traces), one-line "what to do next", one primary action, one secondary action. Variants:
success— "34 cards in Pharmacology. Ready to download." + Download / Open in Anki guideprocessing— "Converting your Notion page…" + (no actions)paywalled— "You've hit 100 cards this month. Upgrade for unlimited." + Upgrade / Plansfailed-empty— Notion page had no content. Try a different page / Helpfailed-permissions— Notion didn't share the page. Re-share / Notion permissions guidefailed-unsupported— File type isn't supported. Use a supported format / Helpfailed-timeout— Conversion took too long. Try splitting / Contact supportfailed-other— Generic fallback. Try again / Contact support (no raw error string)The five
failed-*variants cover ~95 % of all failures from the 90-day retro window.Where it lands
web/src/pages/DownloadsPage/components/ConversionResult.tsx(new)UploadObjectEntry.tsxPaywallBannervisual pattern (already validated by users)Acceptance
VOICE.md(no "Oops", no exclamation, no implementation jargon)Out of scope
failed-otherEffort: M (~1 week). From the W20 trio analysis; closed PR #2206 is the source of these numbers.