feat(billing): make the expired-trial state visible with a clear upgrade path (#1725)

getCompanyEntitlements now derives an entitlementState (trial /
trial_expired / lapsed_subscription / paid / none) plus trialExpiredAt
from the grants it already fetches, reading company_subscriptions.status
inside the existing Promise.all so churned payers get 'abonnemang' copy
instead of 'provperiod'. The state threads through CompanyContext and the
dashboard layout.

Two new surfaces, both hidden in sandbox:
- SubscriptionTouchpoint replaces the sidebar trial pill: countdown while
  the trial runs, a persistent muted upgrade link to /settings/billing
  once it lapses (visible even collapsed, icon-only with aria-label), and
  the first mobile bottom-sheet touchpoint.
- TrialExpiredDialog: one-time on-entry notice with 'Se abonnemang' and a
  ghost dismiss; acknowledgement persists per user+company in
  user_preferences.ui_state.trial_expired_ack (read server-side, no
  flash), set on dismiss and click-through alike.

Narrows the 2026-07-11 'no trial-expired nag' decision at the founder's
direction after a user could not find the upgrade path at all; see
DECISIONS.md.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Jakob Wennberg
2026-08-20 10:05:39 +02:00
committed by GitHub
co-authored by Jakob Wennberg Claude Fable 5
parent 834cc4d0e8
commit c402421908
13 changed files with 476 additions and 62 deletions
+11
View File
@@ -144,8 +144,19 @@
"expand_nav": "Expand menu",
"members_roles": "Members and roles",
"subscription": "Subscription",
"trial_expired_cta": "Trial ended · Upgrade",
"subscription_lapsed_cta": "Subscription ended · Reactivate",
"discord_community": "Discord community"
},
"trial_expired_dialog": {
"title": "Your trial has expired",
"title_dated": "Your trial expired {date}",
"title_lapsed": "Your subscription has ended",
"body_paused": "External connections are paused: the AI assistant, bank sync, Skatteverket and email sending.",
"body_free": "Your bookkeeping, invoices and reports are still here and cost nothing.",
"cta_primary": "View subscription",
"cta_secondary": "Continue without"
},
"agentKnowledge": {
"load_error_title": "Couldn't load the knowledge profile",
"load_error_description": "Something went wrong loading what your agent knows. Try reopening this tab.",
+11
View File
@@ -144,8 +144,19 @@
"expand_nav": "Expandera menyn",
"members_roles": "Medlemmar och roller",
"subscription": "Abonnemang",
"trial_expired_cta": "Provperioden är slut · Uppgradera",
"subscription_lapsed_cta": "Abonnemanget har avslutats · Aktivera",
"discord_community": "Discord-community"
},
"trial_expired_dialog": {
"title": "Din provperiod har löpt ut",
"title_dated": "Din provperiod löpte ut {date}",
"title_lapsed": "Ditt abonnemang har avslutats",
"body_paused": "De externa kopplingarna är pausade: AI-assistenten, bankkopplingen, Skatteverket och e-postutskick.",
"body_free": "Bokföringen, fakturorna och rapporterna finns kvar och kostar ingenting.",
"cta_primary": "Se abonnemang",
"cta_secondary": "Fortsätt utan"
},
"agentKnowledge": {
"load_error_title": "Kunde inte läsa in kunskapsprofilen",
"load_error_description": "Något gick fel när det din agent vet skulle läsas in. Försök öppna fliken igen.",