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:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
834cc4d0e8
commit
c402421908
@@ -3,6 +3,7 @@
|
||||
import { createContext, useContext } from 'react'
|
||||
import type { Company, CompanyRole, Team } from '@/types'
|
||||
import type { CapabilityKey } from '@/lib/entitlements/keys'
|
||||
import type { EntitlementState } from '@/lib/entitlements/has-capability'
|
||||
|
||||
interface CompanyContextValue {
|
||||
company: Company | null
|
||||
@@ -19,6 +20,14 @@ interface CompanyContextValue {
|
||||
* touchpoint in the sidebar.
|
||||
*/
|
||||
trialEndsAt: string | null
|
||||
/**
|
||||
* Paid-lifecycle state (trial / trial_expired / lapsed_subscription /
|
||||
* paid / none). 'none' in the company:null shells. Drives the expired-trial
|
||||
* touchpoint and dialog.
|
||||
*/
|
||||
entitlementState: EntitlementState
|
||||
/** When the lapsed trial ran out; set only while entitlementState is 'trial_expired'. */
|
||||
trialExpiredAt: string | null
|
||||
}
|
||||
|
||||
const CompanyContext = createContext<CompanyContextValue | null>(null)
|
||||
|
||||
Reference in New Issue
Block a user