@eifi1/ui-kit 0.41.0 → 0.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -7
- package/dist/admin/admin-area-labels.d.ts +59 -0
- package/dist/admin/admin-area-labels.js +35 -0
- package/dist/admin/admin-area-labels.js.map +1 -0
- package/dist/admin/admin-forbidden.d.ts +35 -0
- package/dist/admin/admin-forbidden.js +36 -0
- package/dist/admin/admin-forbidden.js.map +1 -0
- package/dist/admin/admin-overview.d.ts +110 -0
- package/dist/admin/admin-overview.js +92 -0
- package/dist/admin/admin-overview.js.map +1 -0
- package/dist/admin/admin-sections.d.ts +94 -0
- package/dist/admin/admin-sections.js +81 -0
- package/dist/admin/admin-sections.js.map +1 -0
- package/dist/admin/admin-tool-links.d.ts +48 -0
- package/dist/admin/admin-tool-links.js +31 -0
- package/dist/admin/admin-tool-links.js.map +1 -0
- package/dist/billing/billing-banner.d.ts +33 -2
- package/dist/billing/billing-banner.js +29 -3
- package/dist/billing/billing-banner.js.map +1 -1
- package/dist/billing/billing-labels.d.ts +36 -0
- package/dist/billing/billing-labels.js +10 -0
- package/dist/billing/billing-labels.js.map +1 -1
- package/dist/billing/billing-overview.d.ts +33 -0
- package/dist/billing/billing-overview.js +6 -0
- package/dist/billing/billing-overview.js.map +1 -1
- package/dist/billing/billing-standing.d.ts +8 -0
- package/dist/billing/billing-standing.js.map +1 -1
- package/dist/billing/cancel-request.d.ts +53 -0
- package/dist/billing/cancel-request.js +21 -0
- package/dist/billing/cancel-request.js.map +1 -0
- package/dist/billing/cancel-subscription.d.ts +46 -0
- package/dist/billing/cancel-subscription.js +57 -0
- package/dist/billing/cancel-subscription.js.map +1 -0
- package/dist/billing/plan-card.d.ts +45 -1
- package/dist/billing/plan-card.js +53 -2
- package/dist/billing/plan-card.js.map +1 -1
- package/dist/billing/plan-switch.d.ts +10 -1
- package/dist/billing/plan-switch.js +22 -13
- package/dist/billing/plan-switch.js.map +1 -1
- package/dist/billing/provider-page.d.ts +40 -0
- package/dist/billing/provider-page.js +33 -0
- package/dist/billing/provider-page.js.map +1 -0
- package/dist/billing/subscription-actions.d.ts +68 -4
- package/dist/billing/subscription-actions.js +65 -5
- package/dist/billing/subscription-actions.js.map +1 -1
- package/dist/components/account-chips.d.ts +50 -2
- package/dist/components/account-chips.js +33 -0
- package/dist/components/account-chips.js.map +1 -1
- package/dist/components/empty-state.d.ts +2 -1
- package/dist/components/empty-state.js.map +1 -1
- package/dist/eslint/index.js +1 -1
- package/dist/feedback/feedback-menu.d.ts +7 -4
- package/dist/feedback/feedback-menu.js +4 -2
- package/dist/feedback/feedback-menu.js.map +1 -1
- package/dist/hooks/use-search-param-state.js +4 -1
- package/dist/hooks/use-search-param-state.js.map +1 -1
- package/dist/i18n/defaults.js +7 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/german.js +113 -0
- package/dist/i18n/german.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +12 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/en.js +142 -1
- package/dist/i18n/locales/en.js.map +3 -3
- package/dist/i18n/locales/es.js +110 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.js +109 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.js +110 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.js +110 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.js +109 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/review.js +34 -1
- package/dist/i18n/review.js.map +1 -1
- package/dist/index.d.ts +24 -2
- package/dist/index.js +29 -1
- package/dist/index.js.map +1 -1
- package/dist/search/global-search.d.ts +3 -0
- package/dist/search/global-search.js +3 -1
- package/dist/search/global-search.js.map +1 -1
- package/dist/settings/settings-catalogue.d.ts +9 -1
- package/dist/settings/settings-catalogue.js +1 -1
- package/dist/settings/settings-catalogue.js.map +1 -1
- package/dist/settings/settings-layout.d.ts +7 -1
- package/dist/settings/settings-layout.js +35 -27
- package/dist/settings/settings-layout.js.map +1 -1
- package/dist/shell/account-menu-language.d.ts +46 -0
- package/dist/shell/account-menu-language.js +64 -0
- package/dist/shell/account-menu-language.js.map +1 -0
- package/dist/shell/admin-menu.d.ts +69 -0
- package/dist/shell/admin-menu.js +64 -0
- package/dist/shell/admin-menu.js.map +1 -0
- package/dist/shell/app-shell.d.ts +10 -0
- package/dist/shell/app-shell.js +22 -4
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/dev-switchers.d.ts +53 -0
- package/dist/shell/dev-switchers.js +107 -0
- package/dist/shell/dev-switchers.js.map +1 -0
- package/dist/shell/sync-status-indicator.d.ts +86 -0
- package/dist/shell/sync-status-indicator.js +170 -0
- package/dist/shell/sync-status-indicator.js.map +1 -0
- package/dist/shell/top-bar-actions.d.ts +49 -0
- package/dist/shell/top-bar-actions.js +31 -0
- package/dist/shell/top-bar-actions.js.map +1 -0
- package/dist/shell/top-bar-parts-labels.d.ts +90 -0
- package/dist/shell/top-bar-parts-labels.js +51 -0
- package/dist/shell/top-bar-parts-labels.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +13 -1
- package/dist/shell/topbar-action-menu.js +5 -2
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell/topbar-controls.d.ts +16 -3
- package/dist/shell/topbar-controls.js +5 -1
- package/dist/shell/topbar-controls.js.map +1 -1
- package/dist/shell/tour-menu.d.ts +67 -0
- package/dist/shell/tour-menu.js +87 -0
- package/dist/shell/tour-menu.js.map +1 -0
- package/dist/shell.d.ts +16 -0
- package/dist/shell.js +21 -0
- package/dist/shell.js.map +1 -1
- package/dist/tour/tour-catalogue-labels.d.ts +44 -0
- package/dist/tour/tour-catalogue-labels.js +32 -0
- package/dist/tour/tour-catalogue-labels.js.map +1 -0
- package/dist/tour/tour-catalogue.d.ts +68 -0
- package/dist/tour/tour-catalogue.js +189 -0
- package/dist/tour/tour-catalogue.js.map +1 -0
- package/dist/tour/tour-info.d.ts +112 -0
- package/dist/tour/tour-info.js +36 -0
- package/dist/tour/tour-info.js.map +1 -0
- package/dist/tour/use-tour-progress.d.ts +61 -0
- package/dist/tour/use-tour-progress.js +116 -0
- package/dist/tour/use-tour-progress.js.map +1 -0
- package/dist/tour/use-tour-url-sync.d.ts +58 -0
- package/dist/tour/use-tour-url-sync.js +110 -0
- package/dist/tour/use-tour-url-sync.js.map +1 -0
- package/dist/tour.d.ts +8 -0
- package/dist/tour.js +18 -0
- package/dist/tour.js.map +1 -1
- package/package.json +1 -1
- package/src/admin/admin-area-labels.ts +91 -0
- package/src/admin/admin-forbidden.tsx +75 -0
- package/src/admin/admin-overview.tsx +217 -0
- package/src/admin/admin-sections.tsx +198 -0
- package/src/admin/admin-tool-links.tsx +84 -0
- package/src/billing/billing-banner.tsx +57 -6
- package/src/billing/billing-labels.ts +59 -0
- package/src/billing/billing-overview.ts +37 -0
- package/src/billing/billing-standing.ts +8 -0
- package/src/billing/cancel-request.ts +75 -0
- package/src/billing/cancel-subscription.tsx +102 -0
- package/src/billing/plan-card.tsx +97 -1
- package/src/billing/plan-switch.tsx +16 -0
- package/src/billing/provider-page.ts +75 -0
- package/src/billing/subscription-actions.tsx +155 -20
- package/src/components/account-chips.tsx +89 -2
- package/src/components/empty-state.tsx +2 -1
- package/src/feedback/feedback-menu.tsx +8 -3
- package/src/hooks/use-search-param-state.ts +6 -1
- package/src/i18n/defaults.ts +6 -0
- package/src/i18n/german.ts +127 -0
- package/src/i18n/kit-labels.tsx +12 -0
- package/src/i18n/locales/en.ts +32 -0
- package/src/i18n/locales/es.ts +121 -0
- package/src/i18n/locales/fr.ts +121 -0
- package/src/i18n/locales/hu.ts +119 -0
- package/src/i18n/locales/it.ts +122 -0
- package/src/i18n/locales/zh.ts +114 -0
- package/src/i18n/review.ts +33 -0
- package/src/index.ts +38 -2
- package/src/search/global-search.tsx +5 -0
- package/src/settings/settings-catalogue.ts +10 -2
- package/src/settings/settings-layout.tsx +58 -34
- package/src/shell/account-menu-language.tsx +116 -0
- package/src/shell/admin-menu.tsx +146 -0
- package/src/shell/app-shell.tsx +44 -3
- package/src/shell/dev-switchers.tsx +179 -0
- package/src/shell/sync-status-indicator.tsx +301 -0
- package/src/shell/top-bar-actions.tsx +79 -0
- package/src/shell/top-bar-parts-labels.ts +142 -0
- package/src/shell/topbar-action-menu.tsx +15 -2
- package/src/shell/topbar-controls.tsx +20 -3
- package/src/shell/tour-menu.tsx +179 -0
- package/src/shell.ts +27 -0
- package/src/tour/tour-catalogue-labels.ts +75 -0
- package/src/tour/tour-catalogue.tsx +316 -0
- package/src/tour/tour-info.ts +144 -0
- package/src/tour/use-tour-progress.ts +198 -0
- package/src/tour/use-tour-url-sync.ts +221 -0
- package/src/tour.ts +11 -0
|
@@ -51,7 +51,40 @@ export interface BillingOverview {
|
|
|
51
51
|
* server-kit before 0.13 still type-checks; absent, read it as false.
|
|
52
52
|
*/
|
|
53
53
|
switching?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* 0.42 (server-kit 0.14, §16.3): who may open the provider's portal. `"buyer_only"`:
|
|
56
|
+
* the row has a buyer (`provider_payer_user_id`) and the viewer isn't them — every
|
|
57
|
+
* portal target would answer `403 billing_portal_buyer_only`, so "Payment and
|
|
58
|
+
* invoices" is a caption naming the buyer and "Cancel subscription" is the in-app
|
|
59
|
+
* cancel (`POST /billing/cancel`). `"open"`, or absent (server-kit before 0.14): as
|
|
60
|
+
* before. Pass it to `SubscriptionActions`' `portal`.
|
|
61
|
+
*/
|
|
62
|
+
portal?: BillingPortalAccess;
|
|
63
|
+
/**
|
|
64
|
+
* 0.42 (§16.3, §16.7 G): the buyer's display name, for the caption under
|
|
65
|
+
* `"buyer_only"`; `null` when the buyer is gone or no longer a member of the company,
|
|
66
|
+
* and the words then say "the admin who bought the subscription".
|
|
67
|
+
*/
|
|
68
|
+
buyer_name?: string | null;
|
|
54
69
|
}
|
|
70
|
+
/** 0.42 (§16.3): `BillingOverview.portal` and `BillingStanding.portal` — server-kit's
|
|
71
|
+
* `Literal["open", "buyer_only"]`. */
|
|
72
|
+
export type BillingPortalAccess = "open" | "buyer_only";
|
|
73
|
+
/**
|
|
74
|
+
* 0.42 (§16.2 decision 5, §16.7 E): whether the payer's free grant runs WITHOUT AN END —
|
|
75
|
+
* an operator's grant (`source: "manual"`) with `comped_until` empty. A purchase under
|
|
76
|
+
* it leaves the row `comped` on the grant's plan (§12.11): only the paid price moves,
|
|
77
|
+
* so the payer would pay for a plan that never applies. Pass it to `PlanPicker`'s and
|
|
78
|
+
* `PlanSwitchConfirm`'s `permanentGrant`, which warn before the checkout or the switch.
|
|
79
|
+
*
|
|
80
|
+
* `source` is checked explicitly, not implied by the missing date: Kurvenschmiede's
|
|
81
|
+
* pre-launch beta rows are `comped` with `comped_until` null too (their launch-plus-12-
|
|
82
|
+
* months end isn't written yet, §3.2), and they are not permanent. A grant with an end
|
|
83
|
+
* gets no warning either: the bought plan applies from that end on.
|
|
84
|
+
*/
|
|
85
|
+
export declare function isPermanentGrant(overview: Pick<BillingOverview, "status" | "source"> & {
|
|
86
|
+
comped_until?: BillingOverview["comped_until"];
|
|
87
|
+
}): boolean;
|
|
55
88
|
/** 0.39: what the payer pays at the provider (server-kit's `PaidPrice`, §15.2.10),
|
|
56
89
|
* read from the row's `provider_price_id`. */
|
|
57
90
|
export interface PaidPrice {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/billing/billing-overview.ts"],"sourcesContent":["import type { DateInput } from \"../lib/format\";\nimport type { SubscriptionStatus } from \"./billing-labels\";\nimport type { BillingCurrency, BillingInterval } from \"./plan-price\";\n\n/**\n * The billing API's shapes the kit's parts read (docs/billing-harmonization.md §4,\n * §14.5) — server-kit 0.7's schemas, field for field under their wire names, for an app\n * whose client is hand-written (kastlan's `features/billing/types.ts`, Kurvenschmiede's\n * `shared/types/billing.ts`, which no mirror test covers).\n */\n\n/** Where a row's current state came from (server-kit's `SubscriptionSource`, §3.2). */\nexport type SubscriptionSource = \"trial\" | \"provider\" | \"manual\" | \"beta\";\n\n/** `GET /billing/overview` (§4): where the payer's subscription stands, what the plan\n * allows and what is used. Only ever the payer's own (§12.6). */\nexport interface BillingOverview {\n /** The plan's code, lowercase (§3.1). */\n plan: string;\n status: SubscriptionStatus;\n source: SubscriptionSource;\n /** False: read-only (§3.3). */\n in_good_standing: boolean;\n trial_ends_at: DateInput;\n /** When the free grant ends; null: no end. */\n comped_until: DateInput;\n current_period_end: DateInput;\n cancel_at_period_end: boolean;\n /** Dimension → limit; null for unlimited. */\n limits: Record<string, number | null>;\n /** What the payer has, per dimension (§12.15). */\n usage: Record<string, number>;\n currency: BillingCurrency;\n /**\n * 0.7 (§14.5): the provider has a customer for this payer, whatever the status or the\n * source — `SubscriptionActions`' `atProvider`. False: \"nothing to manage yet\", and a\n * portal request would answer `409 billing_not_at_provider`.\n */\n at_provider: boolean;\n /**\n * 0.39 (server-kit 0.12, §15.2.10): the price the payer pays at the provider — its\n * plan, period and currency — or null with none (a trial, a grant without a purchase).\n * Under a grant the row keeps the grant's plan, status and source (§12.11), so this\n * is what shows that a switch landed; `checkoutFingerprint` reads it.\n */\n paid: PaidPrice | null;\n /**\n * 0.40 (server-kit 0.13, §15.2.10): a provider subscription runs — the server's own\n * rule, the one its checkout (409 `billing_already_subscribed`) and switch (409\n * `billing_not_subscribed`) routes apply, so exactly one of the two answers. While it\n * is true a `PlanPicker` pick opens `PlanSwitchConfirm` (`POST /billing/plan/preview`,\n * then `POST /billing/plan`); while false, the checkout (`POST /billing/checkout`).\n * Read it instead of mirroring the rule on the client. Optional so an overview from\n * server-kit before 0.13 still type-checks; absent, read it as false.\n */\n switching?: boolean;\n /**\n * 0.42 (server-kit 0.14, §16.3): who may open the provider's portal. `\"buyer_only\"`:\n * the row has a buyer (`provider_payer_user_id`) and the viewer isn't them — every\n * portal target would answer `403 billing_portal_buyer_only`, so \"Payment and\n * invoices\" is a caption naming the buyer and \"Cancel subscription\" is the in-app\n * cancel (`POST /billing/cancel`). `\"open\"`, or absent (server-kit before 0.14): as\n * before. Pass it to `SubscriptionActions`' `portal`.\n */\n portal?: BillingPortalAccess;\n /**\n * 0.42 (§16.3, §16.7 G): the buyer's display name, for the caption under\n * `\"buyer_only\"`; `null` when the buyer is gone or no longer a member of the company,\n * and the words then say \"the admin who bought the subscription\".\n */\n buyer_name?: string | null;\n}\n\n/** 0.42 (§16.3): `BillingOverview.portal` and `BillingStanding.portal` — server-kit's\n * `Literal[\"open\", \"buyer_only\"]`. */\nexport type BillingPortalAccess = \"open\" | \"buyer_only\";\n\n/**\n * 0.42 (§16.2 decision 5, §16.7 E): whether the payer's free grant runs WITHOUT AN END —\n * an operator's grant (`source: \"manual\"`) with `comped_until` empty. A purchase under\n * it leaves the row `comped` on the grant's plan (§12.11): only the paid price moves,\n * so the payer would pay for a plan that never applies. Pass it to `PlanPicker`'s and\n * `PlanSwitchConfirm`'s `permanentGrant`, which warn before the checkout or the switch.\n *\n * `source` is checked explicitly, not implied by the missing date: Kurvenschmiede's\n * pre-launch beta rows are `comped` with `comped_until` null too (their launch-plus-12-\n * months end isn't written yet, §3.2), and they are not permanent. A grant with an end\n * gets no warning either: the bought plan applies from that end on.\n */\nexport function isPermanentGrant(\n overview: Pick<BillingOverview, \"status\" | \"source\"> & { comped_until?: BillingOverview[\"comped_until\"] },\n): boolean {\n return overview.status === \"comped\" && overview.source === \"manual\" && overview.comped_until == null;\n}\n\n/** 0.39: what the payer pays at the provider (server-kit's `PaidPrice`, §15.2.10),\n * read from the row's `provider_price_id`. */\nexport interface PaidPrice {\n /** The plan's code, lowercase. */\n plan: string;\n interval: BillingInterval;\n currency: BillingCurrency;\n}\n\n/** 0.39: `POST /billing/plan/preview` and `POST /billing/plan`'s body (server-kit's\n * `PlanSwitchRequest`, §15.2.10) — the plan and period to switch to. The currency stays\n * the subscription's. */\nexport interface PlanSwitchRequest {\n plan: string;\n interval: BillingInterval;\n}\n\n/**\n * 0.39: what a plan switch costs — or did (server-kit's `PlanSwitchPreview`, §15.2.10):\n * Paddle's preview, prorated at once, both routes' answer. `PlanSwitchConfirm` shows it\n * before the payer agrees.\n */\nexport interface PlanSwitchPreview {\n /** The subscription's currency. */\n currency: BillingCurrency;\n /** Charged at once, in minor units: an upgrade's prorated difference — or, negative, a\n * downgrade's credit to the customer's balance, which Paddle takes off the next\n * charges. */\n now_minor: number;\n /** The next regular charge, in minor units. */\n next_minor?: number | null;\n /** When it is charged; null when Paddle names no date. */\n next_billed_at?: string | null;\n /** The plan and period switched to. */\n plan: string;\n interval: BillingInterval;\n /** The dimensions whose usage is over the new plan's limit (§3.4: a limit gates\n * creation only — the switch is allowed). */\n over_limit: string[];\n /** The new plan's limits; null for unlimited. */\n limits: Record<string, number | null>;\n /** What the payer has, per dimension. */\n usage: Record<string, number>;\n}\n\n/**\n * Where in the provider's portal a request lands (server-kit's `PortalTarget`, §14.5):\n *\n * - `overview` — \"Payment and invoices\";\n * - `cancel` — \"Cancel subscription\": the subscription's cancel link (§12.26);\n * - `payment_method` — the payment-failed banner's \"Update payment method\".\n *\n * The server answers the overview where Paddle gives no such link (a paused or cancelled\n * subscription).\n */\nexport type PortalTarget = \"overview\" | \"cancel\" | \"payment_method\";\n\n/** `POST /billing/portal`'s optional body (server-kit's `PortalRequest`, §14.5); left\n * out, the overview. The answer is `{url}`, a fresh link that is never stored. */\nexport interface PortalRequest {\n target?: PortalTarget;\n}\n"],"mappings":";AAyFO,SAAS,iBACd,UACS;AACT,SAAO,SAAS,WAAW,YAAY,SAAS,WAAW,YAAY,SAAS,gBAAgB;AAClG;","names":[]}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DateInput } from "../lib/format.js";
|
|
2
2
|
import type { SubscriptionStatus } from "./billing-labels.js";
|
|
3
|
+
import type { BillingPortalAccess } from "./billing-overview.js";
|
|
3
4
|
/**
|
|
4
5
|
* What a device knows of the payer's standing — the fields of `GET /billing/overview`
|
|
5
6
|
* (docs/billing-harmonization.md §4) that decide the read-only lock, under their wire
|
|
@@ -14,6 +15,13 @@ export interface BillingStanding {
|
|
|
14
15
|
/** When a free grant ends: a beta user's 12 months, an operator's grant. Empty for a
|
|
15
16
|
* grant without an end (§12.8). */
|
|
16
17
|
comped_until?: DateInput;
|
|
18
|
+
/** 0.42 (server-kit 0.14, §16.7 A): who may open the provider's portal, as the
|
|
19
|
+
* overview's `portal` — `GET /billing/status` says it too, for the payment-failed
|
|
20
|
+
* banner (`BillingBanner`'s `portal`). Absent: `"open"`. */
|
|
21
|
+
portal?: BillingPortalAccess;
|
|
22
|
+
/** 0.42 (§16.7 A): the buyer's display name, as the overview's `buyer_name`; `null`
|
|
23
|
+
* when the buyer is gone or has left the company. */
|
|
24
|
+
buyer_name?: string | null;
|
|
17
25
|
}
|
|
18
26
|
/**
|
|
19
27
|
* The moment this payer's data becomes read-only by itself, known IN ADVANCE — or `null`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/billing/billing-standing.ts"],"sourcesContent":["import { toDate } from \"../lib/format\";\nimport type { DateInput } from \"../lib/format\";\nimport type { SubscriptionStatus } from \"./billing-labels\";\n\n/**\n * What a device knows of the payer's standing — the fields of `GET /billing/overview`\n * (docs/billing-harmonization.md §4) that decide the read-only lock, under their wire\n * names. Pass the overview as it came; the other fields are ignored.\n */\nexport interface BillingStanding {\n status: SubscriptionStatus;\n /** The server's answer at the time of the read (§3.3). */\n in_good_standing: boolean;\n /** When a trial ends (§3.2). Empty for a pure guest whose trial has not started (§12.9). */\n trial_ends_at?: DateInput;\n /** When a free grant ends: a beta user's 12 months, an operator's grant. Empty for a\n * grant without an end (§12.8). */\n comped_until?: DateInput;\n}\n\n/**\n * The moment this payer's data becomes read-only by itself, known IN ADVANCE — or `null`\n * when no date says so (docs/billing-harmonization.md §12.5).\n *\n * A device writes locally first and sends later (keksdose's outbox, an encrypted budget\n * always), so a lock that only arrives with the server's 402 comes after the UI already\n * said \"saved\" — keksdose's phantom save (dev#496). The two dates that end good standing\n * on a schedule are in the overview, so the device locks itself on time, offline too:\n * a trial at `trial_ends_at`, a grant at `comped_until`. Only `past_due` → `expired`\n * depends on the provider's retries and waits for the next sync.\n */\nexport function billingLockAt(standing: BillingStanding): Date | null {\n if (standing.status === \"trialing\") return toDate(standing.trial_ends_at ?? null);\n if (standing.status === \"comped\") return toDate(standing.comped_until ?? null);\n return null;\n}\n\n/**\n * Whether the payer's data is read-only at `now` (default: the present moment): the\n * server already said so (`in_good_standing: false`), or a date from\n * {@link billingLockAt} has passed since the overview was read. Mirror it into the app's\n * offline write gate and its `WriteLockProvider` (§3.3, §12.5).\n *\n * A GUEST's device cannot do this for the owner: the owner's dates are the owner's\n * personal data (§12.6). An item shared with a guest carries `locked: \"billing\" | null`,\n * which the guest's device mirrors as it comes.\n */\nexport function isBillingReadOnly(standing: BillingStanding, now: number | Date = Date.now()): boolean {\n if (!standing.in_good_standing) return true;\n const at = billingLockAt(standing);\n const instant = typeof now === \"number\" ? now : now.getTime();\n return at !== null && at.getTime() <= instant;\n}\n\nconst DAY = 86_400_000;\n\n/**\n * Whole calendar days from `now` to `endsAt`, in the device's time zone — 0 on the day\n * itself (and after it), 1 the day before — or `null` for no readable date. The count a\n * trial or grant banner says (\"ends tomorrow\", \"ends in 5 days\"), from the day a reader\n * lives in rather than from 24-hour blocks: a trial ending at 01:00 tomorrow ends\n * \"tomorrow\", not \"today\".\n */\nexport function billingDaysLeft(endsAt: DateInput, now: number | Date = Date.now()): number | null {\n const end = toDate(endsAt);\n if (!end) return null;\n const from = typeof now === \"number\" ? new Date(now) : now;\n const endDay = new Date(end.getFullYear(), end.getMonth(), end.getDate()).getTime();\n const fromDay = new Date(from.getFullYear(), from.getMonth(), from.getDate()).getTime();\n // `round`, not `floor`: a day across a DST change is 23 or 25 hours long.\n return Math.max(0, Math.round((endDay - fromDay) / DAY));\n}\n"],"mappings":";AAAA,SAAS,cAAc;
|
|
1
|
+
{"version":3,"sources":["../../src/billing/billing-standing.ts"],"sourcesContent":["import { toDate } from \"../lib/format\";\nimport type { DateInput } from \"../lib/format\";\nimport type { SubscriptionStatus } from \"./billing-labels\";\nimport type { BillingPortalAccess } from \"./billing-overview\";\n\n/**\n * What a device knows of the payer's standing — the fields of `GET /billing/overview`\n * (docs/billing-harmonization.md §4) that decide the read-only lock, under their wire\n * names. Pass the overview as it came; the other fields are ignored.\n */\nexport interface BillingStanding {\n status: SubscriptionStatus;\n /** The server's answer at the time of the read (§3.3). */\n in_good_standing: boolean;\n /** When a trial ends (§3.2). Empty for a pure guest whose trial has not started (§12.9). */\n trial_ends_at?: DateInput;\n /** When a free grant ends: a beta user's 12 months, an operator's grant. Empty for a\n * grant without an end (§12.8). */\n comped_until?: DateInput;\n /** 0.42 (server-kit 0.14, §16.7 A): who may open the provider's portal, as the\n * overview's `portal` — `GET /billing/status` says it too, for the payment-failed\n * banner (`BillingBanner`'s `portal`). Absent: `\"open\"`. */\n portal?: BillingPortalAccess;\n /** 0.42 (§16.7 A): the buyer's display name, as the overview's `buyer_name`; `null`\n * when the buyer is gone or has left the company. */\n buyer_name?: string | null;\n}\n\n/**\n * The moment this payer's data becomes read-only by itself, known IN ADVANCE — or `null`\n * when no date says so (docs/billing-harmonization.md §12.5).\n *\n * A device writes locally first and sends later (keksdose's outbox, an encrypted budget\n * always), so a lock that only arrives with the server's 402 comes after the UI already\n * said \"saved\" — keksdose's phantom save (dev#496). The two dates that end good standing\n * on a schedule are in the overview, so the device locks itself on time, offline too:\n * a trial at `trial_ends_at`, a grant at `comped_until`. Only `past_due` → `expired`\n * depends on the provider's retries and waits for the next sync.\n */\nexport function billingLockAt(standing: BillingStanding): Date | null {\n if (standing.status === \"trialing\") return toDate(standing.trial_ends_at ?? null);\n if (standing.status === \"comped\") return toDate(standing.comped_until ?? null);\n return null;\n}\n\n/**\n * Whether the payer's data is read-only at `now` (default: the present moment): the\n * server already said so (`in_good_standing: false`), or a date from\n * {@link billingLockAt} has passed since the overview was read. Mirror it into the app's\n * offline write gate and its `WriteLockProvider` (§3.3, §12.5).\n *\n * A GUEST's device cannot do this for the owner: the owner's dates are the owner's\n * personal data (§12.6). An item shared with a guest carries `locked: \"billing\" | null`,\n * which the guest's device mirrors as it comes.\n */\nexport function isBillingReadOnly(standing: BillingStanding, now: number | Date = Date.now()): boolean {\n if (!standing.in_good_standing) return true;\n const at = billingLockAt(standing);\n const instant = typeof now === \"number\" ? now : now.getTime();\n return at !== null && at.getTime() <= instant;\n}\n\nconst DAY = 86_400_000;\n\n/**\n * Whole calendar days from `now` to `endsAt`, in the device's time zone — 0 on the day\n * itself (and after it), 1 the day before — or `null` for no readable date. The count a\n * trial or grant banner says (\"ends tomorrow\", \"ends in 5 days\"), from the day a reader\n * lives in rather than from 24-hour blocks: a trial ending at 01:00 tomorrow ends\n * \"tomorrow\", not \"today\".\n */\nexport function billingDaysLeft(endsAt: DateInput, now: number | Date = Date.now()): number | null {\n const end = toDate(endsAt);\n if (!end) return null;\n const from = typeof now === \"number\" ? new Date(now) : now;\n const endDay = new Date(end.getFullYear(), end.getMonth(), end.getDate()).getTime();\n const fromDay = new Date(from.getFullYear(), from.getMonth(), from.getDate()).getTime();\n // `round`, not `floor`: a day across a DST change is 23 or 25 hours long.\n return Math.max(0, Math.round((endDay - fromDay) / DAY));\n}\n"],"mappings":";AAAA,SAAS,cAAc;AAuChB,SAAS,cAAc,UAAwC;AACpE,MAAI,SAAS,WAAW,WAAY,QAAO,OAAO,SAAS,iBAAiB,IAAI;AAChF,MAAI,SAAS,WAAW,SAAU,QAAO,OAAO,SAAS,gBAAgB,IAAI;AAC7E,SAAO;AACT;AAYO,SAAS,kBAAkB,UAA2B,MAAqB,KAAK,IAAI,GAAY;AACrG,MAAI,CAAC,SAAS,iBAAkB,QAAO;AACvC,QAAM,KAAK,cAAc,QAAQ;AACjC,QAAM,UAAU,OAAO,QAAQ,WAAW,MAAM,IAAI,QAAQ;AAC5D,SAAO,OAAO,QAAQ,GAAG,QAAQ,KAAK;AACxC;AAEA,MAAM,MAAM;AASL,SAAS,gBAAgB,QAAmB,MAAqB,KAAK,IAAI,GAAkB;AACjG,QAAM,MAAM,OAAO,MAAM;AACzB,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,OAAO,OAAO,QAAQ,WAAW,IAAI,KAAK,GAAG,IAAI;AACvD,QAAM,SAAS,IAAI,KAAK,IAAI,YAAY,GAAG,IAAI,SAAS,GAAG,IAAI,QAAQ,CAAC,EAAE,QAAQ;AAClF,QAAM,UAAU,IAAI,KAAK,KAAK,YAAY,GAAG,KAAK,SAAS,GAAG,KAAK,QAAQ,CAAC,EAAE,QAAQ;AAEtF,SAAO,KAAK,IAAI,GAAG,KAAK,OAAO,SAAS,WAAW,GAAG,CAAC;AACzD;","names":[]}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the payer asked for in the app (0.43, kastlan `18bf4c0`): `"cancel"` after
|
|
3
|
+
* `POST /billing/cancel` answered 204, `"resume"` after `POST /billing/resume` — while the
|
|
4
|
+
* overview still shows the old `cancel_at_period_end`, until the provider's
|
|
5
|
+
* `subscription.updated` has landed (§16.3). `null`: nothing asked, or the row has moved.
|
|
6
|
+
*/
|
|
7
|
+
export type CancelRequested = "cancel" | "resume" | null;
|
|
8
|
+
/** What the page shows for the row as read now — `CancelRequest.view(rowScheduled)`. */
|
|
9
|
+
export interface CancelRequestView {
|
|
10
|
+
/** For `SubscriptionActions`' `cancelRequested`. */
|
|
11
|
+
requested: CancelRequested;
|
|
12
|
+
/** What the plan line says — the asked-for state until the row moves, then the row's:
|
|
13
|
+
* "Cancels on …" at once after a cancel, "Renews on …" after "Keep subscription". */
|
|
14
|
+
cancelScheduled: boolean;
|
|
15
|
+
/** Call on the 204 of a cancel or of a resume. A cancel and its undo both asked before
|
|
16
|
+
* the row moved cancel out. */
|
|
17
|
+
noteAsked: () => void;
|
|
18
|
+
}
|
|
19
|
+
export interface CancelRequest {
|
|
20
|
+
/** Read the overview again every few seconds while this is true — the overview query's
|
|
21
|
+
* `waiting` (kastlan's `useBillingOverview({ waiting })`, a `refetchInterval` inside) —
|
|
22
|
+
* so the page follows the provider. Needs no overview data: pass it to the query. */
|
|
23
|
+
waiting: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* The row as read now (the overview's `cancel_at_period_end`; `undefined` while it
|
|
26
|
+
* loads) → what the page shows. Call it during render, after the query: when the row
|
|
27
|
+
* has moved past what was asked, it drops the request (React's "adjust state while
|
|
28
|
+
* rendering" pattern), so `waiting` turns false and the query stops polling.
|
|
29
|
+
*/
|
|
30
|
+
view: (rowScheduled: boolean | null | undefined) => CancelRequestView;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The in-app cancel's follow-up (0.43, kastlan's derivation in `18bf4c0`): after "Cancel
|
|
34
|
+
* subscription now" or "Keep subscription" the server answers 204 at once, but the row's
|
|
35
|
+
* `cancel_at_period_end` only changes when the provider's webhook lands. Until then the
|
|
36
|
+
* page shows what was asked for, and a second press would ask the provider again.
|
|
37
|
+
*
|
|
38
|
+
* The state lives in the hook and needs no overview at construction, so it goes BEFORE
|
|
39
|
+
* the overview query, which reads `waiting`, and the view comes after it — the overview
|
|
40
|
+
* read once (build review 0.43, kastlan B2; kastlan `subscription-section.tsx:74-92`):
|
|
41
|
+
*
|
|
42
|
+
* const request = useCancelRequest();
|
|
43
|
+
* const { data } = useBillingOverview({ waiting: request.waiting });
|
|
44
|
+
* const view = request.view(data?.cancel_at_period_end);
|
|
45
|
+
* <p>{standingLine({ ...data, cancel_at_period_end: view.cancelScheduled })}</p>
|
|
46
|
+
* <SubscriptionActions
|
|
47
|
+
* cancelAtPeriodEnd={data.cancel_at_period_end}
|
|
48
|
+
* cancelRequested={view.requested}
|
|
49
|
+
* onResume={() => resume.mutate(undefined, { onSuccess: view.noteAsked })}
|
|
50
|
+
* …
|
|
51
|
+
* />
|
|
52
|
+
*/
|
|
53
|
+
export declare function useCancelRequest(): CancelRequest;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
function useCancelRequest() {
|
|
4
|
+
const [asked, setAsked] = useState(null);
|
|
5
|
+
const view = (row) => {
|
|
6
|
+
if (row === void 0 || row === null) return { requested: null, cancelScheduled: false, noteAsked: () => {
|
|
7
|
+
} };
|
|
8
|
+
if (asked !== null && asked.scheduled !== row) setAsked(null);
|
|
9
|
+
const open = asked !== null && asked.scheduled === row;
|
|
10
|
+
return {
|
|
11
|
+
requested: open ? row ? "resume" : "cancel" : null,
|
|
12
|
+
cancelScheduled: open ? !row : row,
|
|
13
|
+
noteAsked: () => setAsked((a) => a !== null && a.scheduled === row ? null : { scheduled: row })
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
return { waiting: asked !== null, view };
|
|
17
|
+
}
|
|
18
|
+
export {
|
|
19
|
+
useCancelRequest
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=cancel-request.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/billing/cancel-request.ts"],"sourcesContent":["import { useState } from \"react\";\n\n/**\n * What the payer asked for in the app (0.43, kastlan `18bf4c0`): `\"cancel\"` after\n * `POST /billing/cancel` answered 204, `\"resume\"` after `POST /billing/resume` — while the\n * overview still shows the old `cancel_at_period_end`, until the provider's\n * `subscription.updated` has landed (§16.3). `null`: nothing asked, or the row has moved.\n */\nexport type CancelRequested = \"cancel\" | \"resume\" | null;\n\n/** What the page shows for the row as read now — `CancelRequest.view(rowScheduled)`. */\nexport interface CancelRequestView {\n /** For `SubscriptionActions`' `cancelRequested`. */\n requested: CancelRequested;\n /** What the plan line says — the asked-for state until the row moves, then the row's:\n * \"Cancels on …\" at once after a cancel, \"Renews on …\" after \"Keep subscription\". */\n cancelScheduled: boolean;\n /** Call on the 204 of a cancel or of a resume. A cancel and its undo both asked before\n * the row moved cancel out. */\n noteAsked: () => void;\n}\n\nexport interface CancelRequest {\n /** Read the overview again every few seconds while this is true — the overview query's\n * `waiting` (kastlan's `useBillingOverview({ waiting })`, a `refetchInterval` inside) —\n * so the page follows the provider. Needs no overview data: pass it to the query. */\n waiting: boolean;\n /**\n * The row as read now (the overview's `cancel_at_period_end`; `undefined` while it\n * loads) → what the page shows. Call it during render, after the query: when the row\n * has moved past what was asked, it drops the request (React's \"adjust state while\n * rendering\" pattern), so `waiting` turns false and the query stops polling.\n */\n view: (rowScheduled: boolean | null | undefined) => CancelRequestView;\n}\n\n/**\n * The in-app cancel's follow-up (0.43, kastlan's derivation in `18bf4c0`): after \"Cancel\n * subscription now\" or \"Keep subscription\" the server answers 204 at once, but the row's\n * `cancel_at_period_end` only changes when the provider's webhook lands. Until then the\n * page shows what was asked for, and a second press would ask the provider again.\n *\n * The state lives in the hook and needs no overview at construction, so it goes BEFORE\n * the overview query, which reads `waiting`, and the view comes after it — the overview\n * read once (build review 0.43, kastlan B2; kastlan `subscription-section.tsx:74-92`):\n *\n * const request = useCancelRequest();\n * const { data } = useBillingOverview({ waiting: request.waiting });\n * const view = request.view(data?.cancel_at_period_end);\n * <p>{standingLine({ ...data, cancel_at_period_end: view.cancelScheduled })}</p>\n * <SubscriptionActions\n * cancelAtPeriodEnd={data.cancel_at_period_end}\n * cancelRequested={view.requested}\n * onResume={() => resume.mutate(undefined, { onSuccess: view.noteAsked })}\n * …\n * />\n */\nexport function useCancelRequest(): CancelRequest {\n // What was asked, kept with the row's state it was asked against.\n const [asked, setAsked] = useState<{ scheduled: boolean } | null>(null);\n const view = (row: boolean | null | undefined): CancelRequestView => {\n // Not read yet: nothing to show against, and nothing to drop.\n if (row === undefined || row === null) return { requested: null, cancelScheduled: false, noteAsked: () => {} };\n // The row has moved: it says the rest. Adjusted during render, React's pattern for\n // state that follows a prop.\n if (asked !== null && asked.scheduled !== row) setAsked(null);\n const open = asked !== null && asked.scheduled === row;\n return {\n requested: open ? (row ? \"resume\" : \"cancel\") : null,\n cancelScheduled: open ? !row : row,\n noteAsked: () => setAsked((a) => (a !== null && a.scheduled === row ? null : { scheduled: row })),\n };\n };\n return { waiting: asked !== null, view };\n}\n"],"mappings":";AAAA,SAAS,gBAAgB;AAyDlB,SAAS,mBAAkC;AAEhD,QAAM,CAAC,OAAO,QAAQ,IAAI,SAAwC,IAAI;AACtE,QAAM,OAAO,CAAC,QAAuD;AAEnE,QAAI,QAAQ,UAAa,QAAQ,KAAM,QAAO,EAAE,WAAW,MAAM,iBAAiB,OAAO,WAAW,MAAM;AAAA,IAAC,EAAE;AAG7G,QAAI,UAAU,QAAQ,MAAM,cAAc,IAAK,UAAS,IAAI;AAC5D,UAAM,OAAO,UAAU,QAAQ,MAAM,cAAc;AACnD,WAAO;AAAA,MACL,WAAW,OAAQ,MAAM,WAAW,WAAY;AAAA,MAChD,iBAAiB,OAAO,CAAC,MAAM;AAAA,MAC/B,WAAW,MAAM,SAAS,CAAC,MAAO,MAAM,QAAQ,EAAE,cAAc,MAAM,OAAO,EAAE,WAAW,IAAI,CAAE;AAAA,IAClG;AAAA,EACF;AACA,SAAO,EAAE,SAAS,UAAU,MAAM,KAAK;AACzC;","names":[]}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { LabelOverride } from "../i18n/kit-labels.js";
|
|
2
|
+
import type { DateInput } from "../lib/format.js";
|
|
3
|
+
import type { BillingLabels } from "./billing-labels.js";
|
|
4
|
+
export interface CancelSubscriptionConfirmProps {
|
|
5
|
+
open: boolean;
|
|
6
|
+
/** `false` when the admin dismisses it — Back, Escape, the backdrop. Confirm leaves it
|
|
7
|
+
* open: close it once `POST /billing/cancel` answers. */
|
|
8
|
+
onOpenChange: (open: boolean) => void;
|
|
9
|
+
/** The overview's `current_period_end`: the day the subscription ends. Formatted with
|
|
10
|
+
* the kit's date formatter (`formatDate`, `"medium"`). */
|
|
11
|
+
periodEnd: DateInput;
|
|
12
|
+
/** Confirm: the app's `POST /billing/cancel` (server-kit 0.14, §16.3). */
|
|
13
|
+
onConfirm: () => void;
|
|
14
|
+
/** The cancel is under way: Confirm's spinner, no second press, and Escape and Back
|
|
15
|
+
* don't close. */
|
|
16
|
+
pending?: boolean;
|
|
17
|
+
/** The date's locale. Default: the provider's. */
|
|
18
|
+
locale?: string;
|
|
19
|
+
/** Prop > `<UiKitProvider labels={{ billing }}>` > English. */
|
|
20
|
+
labels?: LabelOverride<BillingLabels>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The in-app cancel's confirm (docs/billing-harmonization.md §16.2 decision 2, §16.4,
|
|
24
|
+
* ui-kit 0.42): "Cancel subscription" for an admin the provider's portal doesn't open for
|
|
25
|
+
* (`SubscriptionActions`' `portal="buyer_only"` → `onCancelInApp`). It names the day the
|
|
26
|
+
* subscription ends — "Your subscription ends on 9 Nov 2026. Until then everything works
|
|
27
|
+
* as before." — and its button reads "Cancel subscription now" (German "Jetzt kündigen",
|
|
28
|
+
* the confirmation button of §312k BGB).
|
|
29
|
+
*
|
|
30
|
+
* <CancelSubscriptionConfirm
|
|
31
|
+
* open={asking}
|
|
32
|
+
* onOpenChange={setAsking}
|
|
33
|
+
* periodEnd={overview.current_period_end}
|
|
34
|
+
* pending={cancel.isPending}
|
|
35
|
+
* onConfirm={() => cancel.mutate()}
|
|
36
|
+
* />
|
|
37
|
+
*
|
|
38
|
+
* The cancel is at the period's end (Paddle's `cancel`, not immediately); the row follows
|
|
39
|
+
* by webhook (`cancel_at_period_end`), as after a cancel in the portal, and
|
|
40
|
+
* `SubscriptionActions` then offers "Keep subscription" (`onResume`).
|
|
41
|
+
*
|
|
42
|
+
* It takes no billing lock (`COMMIT_EXCEPT_BILLING` built in, §3.3): a read-only payer
|
|
43
|
+
* may always cancel. A demo lock still holds it, as the server's demo guard does. The kit
|
|
44
|
+
* sends no request.
|
|
45
|
+
*/
|
|
46
|
+
export declare function CancelSubscriptionConfirm({ open, onOpenChange, periodEnd, onConfirm, pending, locale: localeProp, labels: labelsProp, }: CancelSubscriptionConfirmProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
|
|
4
|
+
import { DEFAULT_COMMON_LABELS } from "../i18n/shared-labels.js";
|
|
5
|
+
import { formatDate } from "../lib/format.js";
|
|
6
|
+
import { Button } from "../components/button.js";
|
|
7
|
+
import { DialogFrame } from "../components/dialog-frame.js";
|
|
8
|
+
import { COMMIT_EXCEPT_BILLING } from "../components/write-lock.js";
|
|
9
|
+
import { useBillingLabels } from "./billing-labels.js";
|
|
10
|
+
function CancelSubscriptionConfirm({
|
|
11
|
+
open,
|
|
12
|
+
onOpenChange,
|
|
13
|
+
periodEnd,
|
|
14
|
+
onConfirm,
|
|
15
|
+
pending = false,
|
|
16
|
+
locale: localeProp,
|
|
17
|
+
labels: labelsProp
|
|
18
|
+
}) {
|
|
19
|
+
const labels = useBillingLabels(labelsProp);
|
|
20
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
21
|
+
const locale = useKitLocale(localeProp);
|
|
22
|
+
return /* @__PURE__ */ jsx(
|
|
23
|
+
DialogFrame,
|
|
24
|
+
{
|
|
25
|
+
open,
|
|
26
|
+
title: labels.cancelConfirmTitle,
|
|
27
|
+
description: labels.cancelConfirmBody(formatDate(periodEnd, "medium", { locale })),
|
|
28
|
+
onClose: () => {
|
|
29
|
+
if (!pending) onOpenChange(false);
|
|
30
|
+
},
|
|
31
|
+
onKeyDown: (e) => {
|
|
32
|
+
if (pending && e.key === "Escape") e.preventDefault();
|
|
33
|
+
},
|
|
34
|
+
"aria-busy": pending || void 0,
|
|
35
|
+
"data-cancel-subscription-confirm": "",
|
|
36
|
+
actions: (close) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
37
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: close, disabled: pending, children: common.back }),
|
|
38
|
+
/* @__PURE__ */ jsx(
|
|
39
|
+
Button,
|
|
40
|
+
{
|
|
41
|
+
variant: "danger",
|
|
42
|
+
commit: COMMIT_EXCEPT_BILLING,
|
|
43
|
+
pending,
|
|
44
|
+
onClick: () => {
|
|
45
|
+
if (!pending) onConfirm();
|
|
46
|
+
},
|
|
47
|
+
children: labels.cancelConfirmAction
|
|
48
|
+
}
|
|
49
|
+
)
|
|
50
|
+
] })
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
CancelSubscriptionConfirm
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=cancel-subscription.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/billing/cancel-subscription.tsx"],"sourcesContent":["import type { KeyboardEvent } from \"react\";\n\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_COMMON_LABELS } from \"../i18n/shared-labels\";\nimport { formatDate } from \"../lib/format\";\nimport type { DateInput } from \"../lib/format\";\nimport { Button } from \"../components/button\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { COMMIT_EXCEPT_BILLING } from \"../components/write-lock\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\n\nexport interface CancelSubscriptionConfirmProps {\n open: boolean;\n /** `false` when the admin dismisses it — Back, Escape, the backdrop. Confirm leaves it\n * open: close it once `POST /billing/cancel` answers. */\n onOpenChange: (open: boolean) => void;\n /** The overview's `current_period_end`: the day the subscription ends. Formatted with\n * the kit's date formatter (`formatDate`, `\"medium\"`). */\n periodEnd: DateInput;\n /** Confirm: the app's `POST /billing/cancel` (server-kit 0.14, §16.3). */\n onConfirm: () => void;\n /** The cancel is under way: Confirm's spinner, no second press, and Escape and Back\n * don't close. */\n pending?: boolean;\n /** The date's locale. Default: the provider's. */\n locale?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * The in-app cancel's confirm (docs/billing-harmonization.md §16.2 decision 2, §16.4,\n * ui-kit 0.42): \"Cancel subscription\" for an admin the provider's portal doesn't open for\n * (`SubscriptionActions`' `portal=\"buyer_only\"` → `onCancelInApp`). It names the day the\n * subscription ends — \"Your subscription ends on 9 Nov 2026. Until then everything works\n * as before.\" — and its button reads \"Cancel subscription now\" (German \"Jetzt kündigen\",\n * the confirmation button of §312k BGB).\n *\n * <CancelSubscriptionConfirm\n * open={asking}\n * onOpenChange={setAsking}\n * periodEnd={overview.current_period_end}\n * pending={cancel.isPending}\n * onConfirm={() => cancel.mutate()}\n * />\n *\n * The cancel is at the period's end (Paddle's `cancel`, not immediately); the row follows\n * by webhook (`cancel_at_period_end`), as after a cancel in the portal, and\n * `SubscriptionActions` then offers \"Keep subscription\" (`onResume`).\n *\n * It takes no billing lock (`COMMIT_EXCEPT_BILLING` built in, §3.3): a read-only payer\n * may always cancel. A demo lock still holds it, as the server's demo guard does. The kit\n * sends no request.\n */\nexport function CancelSubscriptionConfirm({\n open,\n onOpenChange,\n periodEnd,\n onConfirm,\n pending = false,\n locale: localeProp,\n labels: labelsProp,\n}: CancelSubscriptionConfirmProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale(localeProp);\n\n return (\n <DialogFrame\n open={open}\n title={labels.cancelConfirmTitle}\n description={labels.cancelConfirmBody(formatDate(periodEnd, \"medium\", { locale }))}\n onClose={() => {\n if (!pending) onOpenChange(false);\n }}\n onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {\n if (pending && e.key === \"Escape\") e.preventDefault();\n }}\n aria-busy={pending || undefined}\n data-cancel-subscription-confirm=\"\"\n actions={(close) => (\n <>\n <Button variant=\"secondary\" onClick={close} disabled={pending}>\n {common.back}\n </Button>\n <Button\n variant=\"danger\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={pending}\n onClick={() => {\n if (!pending) onConfirm();\n }}\n >\n {labels.cancelConfirmAction}\n </Button>\n </>\n )}\n />\n );\n}\n"],"mappings":";AAmFQ,mBACE,KADF;AAjFR,SAAS,cAAc,oBAAoB;AAE3C,SAAS,6BAA6B;AACtC,SAAS,kBAAkB;AAE3B,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B,SAAS,6BAA6B;AACtC,SAAS,wBAAwB;AA8C1B,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,QAAQ;AACV,GAAmC;AACjC,QAAM,SAAS,iBAAiB,UAAU;AAC1C,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,SAAS,aAAa,UAAU;AAEtC,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,OAAO;AAAA,MACd,aAAa,OAAO,kBAAkB,WAAW,WAAW,UAAU,EAAE,OAAO,CAAC,CAAC;AAAA,MACjF,SAAS,MAAM;AACb,YAAI,CAAC,QAAS,cAAa,KAAK;AAAA,MAClC;AAAA,MACA,WAAW,CAAC,MAAqC;AAC/C,YAAI,WAAW,EAAE,QAAQ,SAAU,GAAE,eAAe;AAAA,MACtD;AAAA,MACA,aAAW,WAAW;AAAA,MACtB,oCAAiC;AAAA,MACjC,SAAS,CAAC,UACR,iCACE;AAAA,4BAAC,UAAO,SAAQ,aAAY,SAAS,OAAO,UAAU,SACnD,iBAAO,MACV;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,QAAQ;AAAA,YACR;AAAA,YACA,SAAS,MAAM;AACb,kBAAI,CAAC,QAAS,WAAU;AAAA,YAC1B;AAAA,YAEC,iBAAO;AAAA;AAAA,QACV;AAAA,SACF;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
|
|
@@ -115,6 +115,24 @@ export interface PlanPickerProps extends Omit<PlanDisplayProps, "currency" | "in
|
|
|
115
115
|
onChoose: (choice: PlanChoice) => void;
|
|
116
116
|
/** The checkout is being opened: the button's spinner, no second press. */
|
|
117
117
|
pending?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* 0.42 (§16.2 decision 5, §16.8 B): the payer's free grant runs without an end —
|
|
120
|
+
* `isPermanentGrant(overview)`. A purchase under it changes nothing the payer gets
|
|
121
|
+
* while the grant runs (§12.11), so the button first opens a confirm —
|
|
122
|
+
* `grantWarningTitle`, `grantWarningBody`, "Buy anyway" (`grantWarningAction`) and
|
|
123
|
+
* "Back" — and {@link onChoose} runs only on "Buy anyway". Warned, not refused.
|
|
124
|
+
*
|
|
125
|
+
* Not while {@link switching}: then the pick opens `PlanSwitchConfirm`, which takes
|
|
126
|
+
* the same `permanentGrant` and shows the warning above its figures — one warning,
|
|
127
|
+
* in the dialog where the payer agrees, rather than a confirm before a confirm.
|
|
128
|
+
*/
|
|
129
|
+
permanentGrant?: boolean;
|
|
130
|
+
/**
|
|
131
|
+
* 0.42: the overview's `switching` — the app sends this pick to `PlanSwitchConfirm`
|
|
132
|
+
* (§15.2.10), not the checkout. Only decides where {@link permanentGrant}'s warning
|
|
133
|
+
* shows; the button and {@link onChoose} are the same either way.
|
|
134
|
+
*/
|
|
135
|
+
switching?: boolean;
|
|
118
136
|
/** The provider's localised price for a plan, or `undefined` for the catalogue's —
|
|
119
137
|
* see {@link PlanCardProps.pricePreview}. */
|
|
120
138
|
pricePreview?: (plan: BillingPlan, choice: Omit<PlanChoice, "plan">) => ReactNode | undefined;
|
|
@@ -143,6 +161,32 @@ export interface PlanPickerProps extends Omit<PlanDisplayProps, "currency" | "in
|
|
|
143
161
|
* The button never takes the write lock: billing itself always works, a read-only payer
|
|
144
162
|
* must be able to pay (§3.3). It is off, with the reason on hover and focus, while
|
|
145
163
|
* nothing is picked or the current plan is.
|
|
164
|
+
*
|
|
165
|
+
* 0.42 (§16.8 B): under a grant without an end (`permanentGrant`) the payer is warned
|
|
166
|
+
* before paying for a plan that won't apply — here, in a confirm before `onChoose`, on
|
|
167
|
+
* the way to the checkout; on the way to a switch (`switching`) in `PlanSwitchConfirm`
|
|
168
|
+
* instead, so the warning shows once on either path.
|
|
169
|
+
*/
|
|
170
|
+
export declare function PlanPicker({ plans, current, value: valueProp, defaultValue, onValueChange, interval: intervalProp, defaultInterval, onIntervalChange, currency: currencyProp, defaultCurrency, onCurrencyChange, currencies: currenciesProp, onChoose, pending, permanentGrant, switching, pricePreview, legend, name, className, gridClassName, cardClassName, dimensionLabels, formatLimit, locale: localeProp, labels: labelsProp, }: PlanPickerProps): import("react").JSX.Element;
|
|
171
|
+
export interface GrantWarningConfirmProps {
|
|
172
|
+
/** Shown, controlled. */
|
|
173
|
+
open: boolean;
|
|
174
|
+
/** `false` on "Back", Escape, the backdrop — and after "Buy anyway". */
|
|
175
|
+
onOpenChange: (open: boolean) => void;
|
|
176
|
+
/** "Buy anyway": the app opens its checkout. The dialog closes itself after it. */
|
|
177
|
+
onConfirm: () => void;
|
|
178
|
+
/** Prop > `<UiKitProvider labels={{ billing }}>` > English: `grantWarningTitle`,
|
|
179
|
+
* `grantWarningBody`, `grantWarningAction`. "Back" is `common.back`. */
|
|
180
|
+
labels?: LabelOverride<BillingLabels>;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 0.42 (§16.8 B): the permanent-grant warning before a checkout — `grantWarningTitle`,
|
|
184
|
+
* `grantWarningBody`, "Back" and "Buy anyway" (`grantWarningAction`). {@link PlanPicker}
|
|
185
|
+
* shows it itself under `permanentGrant` when not `switching`. Exported for the path the
|
|
186
|
+
* picker cannot see: an app that meant to switch, learned there is no subscription to
|
|
187
|
+
* switch (keksdose: `/billing/plan/preview` → 409 `billing_not_subscribed`) and falls back
|
|
188
|
+
* to a checkout — show this before that checkout, so a permanent-grant holder is still
|
|
189
|
+
* warned. Carries `data-grant-warning`.
|
|
146
190
|
*/
|
|
147
|
-
export declare function
|
|
191
|
+
export declare function GrantWarningConfirm({ open, onOpenChange, onConfirm, labels: labelsProp }: GrantWarningConfirmProps): import("react").JSX.Element;
|
|
148
192
|
export {};
|
|
@@ -9,6 +9,7 @@ import { ChoiceCard } from "../components/choice-card.js";
|
|
|
9
9
|
import { Chip } from "../components/chip.js";
|
|
10
10
|
import { ToggleGroup } from "../components/toggle-group.js";
|
|
11
11
|
import { Button } from "../components/button.js";
|
|
12
|
+
import { DialogFrame } from "../components/dialog-frame.js";
|
|
12
13
|
import { useBillingLabels } from "./billing-labels.js";
|
|
13
14
|
import { BILLING_INTERVALS, billingCurrencyFor, formatPlanPrice, planPrice } from "./plan-price.js";
|
|
14
15
|
function planRelation(plans, code, current) {
|
|
@@ -132,6 +133,8 @@ function PlanPicker({
|
|
|
132
133
|
currencies: currenciesProp,
|
|
133
134
|
onChoose,
|
|
134
135
|
pending,
|
|
136
|
+
permanentGrant = false,
|
|
137
|
+
switching = false,
|
|
135
138
|
pricePreview,
|
|
136
139
|
legend,
|
|
137
140
|
name,
|
|
@@ -146,6 +149,7 @@ function PlanPicker({
|
|
|
146
149
|
const labels = useBillingLabels(labelsProp);
|
|
147
150
|
const locale = useKitLocale(localeProp);
|
|
148
151
|
const generated = useId();
|
|
152
|
+
const [warned, setWarned] = useState(null);
|
|
149
153
|
const groupName = name ?? generated;
|
|
150
154
|
const legendId = `${generated}-legend`;
|
|
151
155
|
const [ownValue, setOwnValue] = useState(() => defaultValue ?? current ?? null);
|
|
@@ -236,13 +240,60 @@ function PlanPicker({
|
|
|
236
240
|
className: "max-sm:w-full",
|
|
237
241
|
disabledReason: blocked,
|
|
238
242
|
pending,
|
|
239
|
-
onClick: () =>
|
|
243
|
+
onClick: () => {
|
|
244
|
+
if (!picked) return;
|
|
245
|
+
const choice = { plan: picked.code, interval, currency };
|
|
246
|
+
if (permanentGrant && !switching) setWarned(choice);
|
|
247
|
+
else onChoose(choice);
|
|
248
|
+
},
|
|
240
249
|
children: buttonText
|
|
241
250
|
}
|
|
242
|
-
) })
|
|
251
|
+
) }),
|
|
252
|
+
permanentGrant && !switching && /* @__PURE__ */ jsx(
|
|
253
|
+
GrantWarningConfirm,
|
|
254
|
+
{
|
|
255
|
+
open: warned !== null,
|
|
256
|
+
onOpenChange: (open) => {
|
|
257
|
+
if (!open) setWarned(null);
|
|
258
|
+
},
|
|
259
|
+
onConfirm: () => {
|
|
260
|
+
if (warned) onChoose(warned);
|
|
261
|
+
},
|
|
262
|
+
labels: labelsProp
|
|
263
|
+
}
|
|
264
|
+
)
|
|
243
265
|
] });
|
|
244
266
|
}
|
|
267
|
+
function GrantWarningConfirm({ open, onOpenChange, onConfirm, labels: labelsProp }) {
|
|
268
|
+
const labels = useBillingLabels(labelsProp);
|
|
269
|
+
const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
|
|
270
|
+
return /* @__PURE__ */ jsx(
|
|
271
|
+
DialogFrame,
|
|
272
|
+
{
|
|
273
|
+
open,
|
|
274
|
+
title: labels.grantWarningTitle,
|
|
275
|
+
description: labels.grantWarningBody,
|
|
276
|
+
onClose: () => onOpenChange(false),
|
|
277
|
+
"data-grant-warning": "",
|
|
278
|
+
actions: (close) => /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
279
|
+
/* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: close, children: common.back }),
|
|
280
|
+
/* @__PURE__ */ jsx(
|
|
281
|
+
Button,
|
|
282
|
+
{
|
|
283
|
+
variant: "primary",
|
|
284
|
+
onClick: () => {
|
|
285
|
+
onConfirm();
|
|
286
|
+
onOpenChange(false);
|
|
287
|
+
},
|
|
288
|
+
children: labels.grantWarningAction
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
] })
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
}
|
|
245
295
|
export {
|
|
296
|
+
GrantWarningConfirm,
|
|
246
297
|
PlanCard,
|
|
247
298
|
PlanPicker,
|
|
248
299
|
planRelation
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/billing/plan-card.tsx"],"sourcesContent":["import { useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Check, Gauge } from \"lucide-react\";\n\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { formatNumber } from \"../lib/format\";\nimport { cn } from \"../lib/cn\";\nimport { ChoiceCard } from \"../components/choice-card\";\nimport type { ChoiceCardProps } from \"../components/choice-card\";\nimport { Chip } from \"../components/chip\";\nimport { ToggleGroup } from \"../components/toggle-group\";\nimport { Button } from \"../components/button\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport { BILLING_INTERVALS, billingCurrencyFor, formatPlanPrice, planPrice } from \"./plan-price\";\nimport type { BillingCurrency, BillingInterval, PlanPrices } from \"./plan-price\";\n\n/**\n * One plan of the app's catalogue (docs/billing-harmonization.md §3.1) as a page shows it:\n * server-kit's `PlanSpec` from `GET /billing/plans`, plus the words, which are the app's\n * (display names and feature lines come from its i18n, never from the provider).\n */\nexport interface BillingPlan {\n /** The plan code, lowercase on the wire (§12.16); compared without case here. */\n code: string;\n /** The app's display name, in the reader's language: \"Pro\". */\n name: string;\n /** A line under the price. */\n description?: ReactNode;\n /** Gross prices in minor units, per currency and period (§3.1, §12.17). None at all —\n * or 0 — is a free plan. */\n prices?: PlanPrices | null;\n /** Dimension → limit; `null` is unlimited (§3.1). Shown in the object's order. */\n limits?: Readonly<Record<string, number | null>>;\n /** The app's feature lines. */\n features?: readonly ReactNode[];\n /** Not choosable — a plan being retired, say. */\n disabled?: boolean;\n}\n\n/**\n * How a plan stands to the one the payer pays for now: `current` is it, `upgrade` comes\n * after it in the catalogue's order (`PlanSpec.sort`), `downgrade` before it.\n */\nexport type PlanRelation = \"current\" | \"upgrade\" | \"downgrade\";\n\n/** The plan's place against `current`, by the order of `plans`; `undefined` without a\n * current plan (a trial, an ended plan) or for a code that is not in the list. */\nexport function planRelation(\n plans: readonly Pick<BillingPlan, \"code\">[],\n code: string,\n current: string | null | undefined,\n): PlanRelation | undefined {\n if (!current) return undefined;\n const index = (c: string) => plans.findIndex((p) => p.code.toLowerCase() === c.toLowerCase());\n const at = index(code);\n const now = index(current);\n if (at < 0 || now < 0) return undefined;\n return at === now ? \"current\" : at > now ? \"upgrade\" : \"downgrade\";\n}\n\n/** What both parts need to draw a plan. */\ninterface PlanDisplayProps {\n /** The currency the prices are shown in (§2.6). */\n currency: BillingCurrency;\n /** The period the prices are shown for. */\n interval: BillingInterval;\n /** The app's name of each limit's dimension (\"Budgets\"). A dimension without one is\n * shown by its key. */\n dimensionLabels?: Readonly<Record<string, string>>;\n /** How a limit's figure is written — kastlan's storage is bytes (`formatFileSize`).\n * Default: a number in the kit's locale. */\n formatLimit?: (dimension: string, value: number) => string;\n /** Over the provider's locale. */\n locale?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\nexport interface PlanCardProps\n extends Omit<ChoiceCardProps, \"type\" | \"title\" | \"description\" | \"icon\" | \"indeterminate\" | \"value\" | \"labels\">,\n PlanDisplayProps {\n plan: BillingPlan;\n /** See {@link PlanRelation}. `current` draws the \"Current plan\" chip — a word in the\n * card's name, never a colour alone. */\n relation?: PlanRelation;\n /**\n * The provider's own localised price in place of the catalogue's figure (§12.17) — a\n * Merchant of Record's price preview for the buyer's country, tax included. The period\n * and \"VAT included\" stay: the law asks for a gross price, and the card says so.\n */\n pricePreview?: ReactNode;\n}\n\n/** A list from spans (`role=\"list\"`): the card is a `<label>`, which may hold phrasing\n * content only, so a `<ul>` there would be invalid HTML. */\nfunction SpanList({ name, items, icon }: { name: string; items: ReactNode[]; icon: ReactNode }) {\n if (items.length === 0) return null;\n return (\n <span role=\"list\" aria-label={name} className=\"block space-y-1\">\n {items.map((item, i) => (\n <span role=\"listitem\" key={i} className=\"flex items-start gap-1.5 text-secondary\">\n <span aria-hidden className=\"mt-[0.0625rem] flex shrink-0 text-muted [&_svg]:size-3.5\">\n {icon}\n </span>\n <span className=\"min-w-0\">\n {item}\n {/* A space between the items' texts in the flattened description. */}{\" \"}\n </span>\n </span>\n ))}\n </span>\n );\n}\n\n/**\n * One plan as a {@link ChoiceCard} radio (§7): the name (and \"Current plan\"), the GROSS\n * price in the chosen currency and period through `formatMoney` (\"CHF 49 per year, VAT\n * included\", §12.17), the limits (\"Budgets: 3\", \"Seats: Unlimited\") and the app's feature\n * lines. A plan with prices but none for this currency and period is shown, \"Not offered\n * for this billing period\", and cannot be picked.\n *\n * A radio of a set: give the cards one `name`, or use {@link PlanPicker}, which does that\n * and owns the choice. Everything else a `ChoiceCard` takes goes to it — `checked`,\n * `onCheckedChange`, `name`, `className`. Carries `data-plan` and `data-relation`.\n */\nexport function PlanCard({\n // The radio `<input>` (ChoiceCard's): a React 19 ref prop since 0.34 (MOD-4, refactor\n // audit 2026-10), where this was `forwardRef`.\n ref,\n plan,\n relation,\n pricePreview,\n currency,\n interval,\n dimensionLabels,\n formatLimit,\n locale: localeProp,\n labels: labelsProp,\n disabled,\n ...rest\n}: PlanCardProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale(localeProp);\n const price = planPrice(plan.prices, currency, interval);\n\n const priced = price.kind === \"price\";\n const figure = priced\n ? pricePreview !== undefined && pricePreview !== null\n ? pricePreview\n : formatPlanPrice(price.minor, currency, { locale })\n : price.kind === \"free\"\n ? labels.free\n : labels.notOffered;\n\n const limitLines = Object.entries(plan.limits ?? {}).map(([dimension, value]) => {\n const name = dimensionLabels?.[dimension] ?? dimension;\n const shown =\n value === null ? labels.unlimited : formatLimit ? formatLimit(dimension, value) : formatNumber(value, { locale });\n return common.fieldValue(name, shown);\n });\n\n const body = (\n <span className=\"mt-1 block space-y-2\">\n <span className=\"block\">\n <span\n data-part=\"price\"\n className={cn(\n \"tabular-nums\",\n price.kind === \"not-offered\"\n ? \"text-xs text-muted\"\n : \"text-lg font-semibold leading-tight text-primary\",\n )}\n >\n {figure}\n </span>\n {priced && (\n <>\n {\" \"}\n <span className=\"text-xs text-secondary\">\n {interval === \"year\" ? labels.perYear : labels.perMonth}\n </span>{\" \"}\n <span className=\"block text-xs text-muted\">{labels.vatIncluded}</span>\n </>\n )}{\" \"}\n </span>\n {plan.description !== undefined && plan.description !== null && (\n <span className=\"block text-secondary\">{plan.description} </span>\n )}\n <SpanList name={labels.limits} items={limitLines} icon={<Gauge />} />\n <SpanList name={labels.features} items={[...(plan.features ?? [])]} icon={<Check />} />\n </span>\n );\n\n return (\n <ChoiceCard\n ref={ref}\n {...rest}\n type=\"radio\"\n value={plan.code}\n disabled={disabled || plan.disabled || price.kind === \"not-offered\"}\n data-plan={plan.code}\n data-relation={relation}\n title={\n <>\n {plan.name}\n {relation === \"current\" && (\n <>\n {\" \"}\n <Chip size=\"xs\" tone=\"brand\" shape=\"square\" caps className=\"ms-1 align-[0.0625rem]\">\n {labels.current}\n </Chip>\n </>\n )}\n </>\n }\n description={body}\n />\n );\n}\n\n/** What {@link PlanPickerProps.onChoose} is handed: everything `POST /billing/checkout`\n * takes (§4). */\nexport interface PlanChoice {\n plan: string;\n interval: BillingInterval;\n currency: BillingCurrency;\n}\n\nexport interface PlanPickerProps extends Omit<PlanDisplayProps, \"currency\" | \"interval\"> {\n /** The catalogue, in its order (`PlanSpec.sort`): cheapest first. The order decides\n * upgrade and downgrade. */\n plans: readonly BillingPlan[];\n /**\n * The plan the payer pays for now (`plan` of the overview while `active` or\n * `past_due`). Leave it out during a trial and after a plan ended: then every plan is\n * \"Choose …\", the trial's plan included, since none is paid for yet.\n */\n current?: string | null;\n /** The picked plan, controlled. `null` while none is. */\n value?: string | null;\n /** The picked plan, uncontrolled. Default: `current`, else nothing. */\n defaultValue?: string | null;\n onValueChange?: (plan: string) => void;\n /** The period, controlled. */\n interval?: BillingInterval;\n /** The period, uncontrolled. Default `year` — lead with yearly (§12.17). */\n defaultInterval?: BillingInterval;\n onIntervalChange?: (interval: BillingInterval) => void;\n /** The currency, controlled. */\n currency?: BillingCurrency;\n /** The currency, uncontrolled. Default: the account's stored currency is the app's to\n * pass here (§12.18); without it, {@link billingCurrencyFor} the kit's locale. */\n defaultCurrency?: BillingCurrency;\n onCurrencyChange?: (currency: BillingCurrency) => void;\n /** The currencies the payer may switch between. Default: every currency a plan has a\n * price in. The switch shows only for more than one. */\n currencies?: readonly BillingCurrency[];\n /** The button: the app starts its checkout (`POST /billing/checkout`, then off to the\n * provider's page). The kit sends no request. */\n onChoose: (choice: PlanChoice) => void;\n /** The checkout is being opened: the button's spinner, no second press. */\n pending?: boolean;\n /** The provider's localised price for a plan, or `undefined` for the catalogue's —\n * see {@link PlanCardProps.pricePreview}. */\n pricePreview?: (plan: BillingPlan, choice: Omit<PlanChoice, \"plan\">) => ReactNode | undefined;\n /** A visible question over the cards. Without one the set is named \"Plans\". */\n legend?: ReactNode;\n /** Shared by the radios. Generated if omitted. */\n name?: string;\n /** Classes for the whole picker. */\n className?: string;\n /** Classes for the card grid. Default: one column, two from `sm`, three from `lg`. */\n gridClassName?: string;\n /** Classes for every card. */\n cardClassName?: string;\n}\n\n/** Every currency some plan has a price in, CHF first. */\nfunction pricedCurrencies(plans: readonly BillingPlan[]): BillingCurrency[] {\n const found = new Set<BillingCurrency>();\n for (const p of plans) for (const c of Object.keys(p.prices ?? {})) found.add(c as BillingCurrency);\n return ([\"CHF\", \"EUR\"] as const).filter((c) => found.has(c));\n}\n\n/** Every period some plan has a price for, yearly first. */\nfunction pricedIntervals(plans: readonly BillingPlan[]): BillingInterval[] {\n const found = new Set<BillingInterval>();\n for (const p of plans)\n for (const byInterval of Object.values(p.prices ?? {}))\n for (const [k, v] of Object.entries(byInterval ?? {})) if (typeof v === \"number\") found.add(k as BillingInterval);\n return BILLING_INTERVALS.filter((i) => found.has(i));\n}\n\n/**\n * The plans to choose from (§7): the period switch — yearly first (§12.17) — and, where\n * the payer may pay in either, the currency switch (§2.6); a {@link PlanCard} per plan in\n * a radio set; and one button that says what pressing it does: \"Upgrade to Pro\", \"Switch\n * to Free\", or \"Choose Pro\" with no plan paid for yet.\n *\n * One button under the set rather than one per card: each card IS a radio — a `<label>`\n * round an `<input>` — and a button inside a label is interactive content inside\n * interactive content. Picking and committing are two steps anyway: the price under the\n * picked card is what checkout will charge.\n *\n * The button never takes the write lock: billing itself always works, a read-only payer\n * must be able to pay (§3.3). It is off, with the reason on hover and focus, while\n * nothing is picked or the current plan is.\n */\nexport function PlanPicker({\n plans,\n current,\n value: valueProp,\n defaultValue,\n onValueChange,\n interval: intervalProp,\n defaultInterval = \"year\",\n onIntervalChange,\n currency: currencyProp,\n defaultCurrency,\n onCurrencyChange,\n currencies: currenciesProp,\n onChoose,\n pending,\n pricePreview,\n legend,\n name,\n className,\n gridClassName,\n cardClassName,\n dimensionLabels,\n formatLimit,\n locale: localeProp,\n labels: labelsProp,\n}: PlanPickerProps) {\n const labels = useBillingLabels(labelsProp);\n const locale = useKitLocale(localeProp);\n const generated = useId();\n const groupName = name ?? generated;\n const legendId = `${generated}-legend`;\n\n const [ownValue, setOwnValue] = useState<string | null>(() => defaultValue ?? current ?? null);\n const [ownInterval, setOwnInterval] = useState<BillingInterval>(defaultInterval);\n const [ownCurrency, setOwnCurrency] = useState<BillingCurrency>(\n () => defaultCurrency ?? billingCurrencyFor(locale),\n );\n const value = valueProp !== undefined ? valueProp : ownValue;\n const interval = intervalProp ?? ownInterval;\n const currency = currencyProp ?? ownCurrency;\n\n const pick = (code: string) => {\n if (valueProp === undefined) setOwnValue(code);\n onValueChange?.(code);\n };\n const setInterval = (next: BillingInterval) => {\n if (intervalProp === undefined) setOwnInterval(next);\n onIntervalChange?.(next);\n };\n const setCurrency = (next: BillingCurrency) => {\n if (currencyProp === undefined) setOwnCurrency(next);\n onCurrencyChange?.(next);\n };\n\n const currencies = currenciesProp ?? pricedCurrencies(plans);\n const intervals = pricedIntervals(plans);\n const picked = value ? plans.find((p) => p.code.toLowerCase() === value.toLowerCase()) : undefined;\n const relation = picked ? planRelation(plans, picked.code, current) : undefined;\n const pickedPrice = picked ? planPrice(picked.prices, currency, interval) : undefined;\n\n const buttonText = !picked\n ? labels.choosePlan\n : relation === \"upgrade\"\n ? labels.upgrade(picked.name)\n : relation === \"downgrade\"\n ? labels.downgrade(picked.name)\n : labels.choose(picked.name);\n const blocked = !picked\n ? labels.pickFirst\n : relation === \"current\"\n ? labels.isCurrent\n : pickedPrice?.kind === \"not-offered\"\n ? labels.notOffered\n : undefined;\n\n return (\n <div className={cn(\"min-w-0 space-y-3\", className)} data-billing-interval={interval} data-billing-currency={currency}>\n {(intervals.length > 1 || currencies.length > 1) && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {intervals.length > 1 && (\n <ToggleGroup\n aria-label={labels.interval}\n // Content-wide, so the two switches share a row and wrap only when narrow.\n className=\"w-auto\"\n options={intervals.map((i) => ({ value: i, label: i === \"year\" ? labels.yearly : labels.monthly }))}\n value={interval}\n onChange={setInterval}\n />\n )}\n {currencies.length > 1 && (\n <ToggleGroup\n aria-label={labels.currency}\n className=\"w-auto\"\n options={currencies.map((c) => ({ value: c, label: c }))}\n value={currency}\n onChange={setCurrency}\n />\n )}\n </div>\n )}\n <fieldset\n className=\"min-w-0\"\n aria-labelledby={legend !== undefined ? legendId : undefined}\n aria-label={legend === undefined ? labels.plans : undefined}\n >\n {legend !== undefined && (\n <legend id={legendId} className=\"mb-2 text-sm font-medium text-primary\">\n {legend}\n </legend>\n )}\n <div className={cn(\"grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3\", gridClassName)}>\n {plans.map((plan) => (\n <PlanCard\n key={plan.code}\n plan={plan}\n name={groupName}\n currency={currency}\n interval={interval}\n relation={planRelation(plans, plan.code, current)}\n pricePreview={pricePreview?.(plan, { interval, currency })}\n checked={picked?.code === plan.code}\n onCheckedChange={(on) => on && pick(plan.code)}\n dimensionLabels={dimensionLabels}\n formatLimit={formatLimit}\n locale={locale}\n labels={labelsProp}\n className={cardClassName}\n />\n ))}\n </div>\n </fieldset>\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant=\"primary\"\n className=\"max-sm:w-full\"\n disabledReason={blocked}\n pending={pending}\n onClick={() => picked && onChoose({ plan: picked.code, interval, currency })}\n >\n {buttonText}\n </Button>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/billing/plan-card.tsx"],"sourcesContent":["import { useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Check, Gauge } from \"lucide-react\";\n\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { formatNumber } from \"../lib/format\";\nimport { cn } from \"../lib/cn\";\nimport { ChoiceCard } from \"../components/choice-card\";\nimport type { ChoiceCardProps } from \"../components/choice-card\";\nimport { Chip } from \"../components/chip\";\nimport { ToggleGroup } from \"../components/toggle-group\";\nimport { Button } from \"../components/button\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport { BILLING_INTERVALS, billingCurrencyFor, formatPlanPrice, planPrice } from \"./plan-price\";\nimport type { BillingCurrency, BillingInterval, PlanPrices } from \"./plan-price\";\n\n/**\n * One plan of the app's catalogue (docs/billing-harmonization.md §3.1) as a page shows it:\n * server-kit's `PlanSpec` from `GET /billing/plans`, plus the words, which are the app's\n * (display names and feature lines come from its i18n, never from the provider).\n */\nexport interface BillingPlan {\n /** The plan code, lowercase on the wire (§12.16); compared without case here. */\n code: string;\n /** The app's display name, in the reader's language: \"Pro\". */\n name: string;\n /** A line under the price. */\n description?: ReactNode;\n /** Gross prices in minor units, per currency and period (§3.1, §12.17). None at all —\n * or 0 — is a free plan. */\n prices?: PlanPrices | null;\n /** Dimension → limit; `null` is unlimited (§3.1). Shown in the object's order. */\n limits?: Readonly<Record<string, number | null>>;\n /** The app's feature lines. */\n features?: readonly ReactNode[];\n /** Not choosable — a plan being retired, say. */\n disabled?: boolean;\n}\n\n/**\n * How a plan stands to the one the payer pays for now: `current` is it, `upgrade` comes\n * after it in the catalogue's order (`PlanSpec.sort`), `downgrade` before it.\n */\nexport type PlanRelation = \"current\" | \"upgrade\" | \"downgrade\";\n\n/** The plan's place against `current`, by the order of `plans`; `undefined` without a\n * current plan (a trial, an ended plan) or for a code that is not in the list. */\nexport function planRelation(\n plans: readonly Pick<BillingPlan, \"code\">[],\n code: string,\n current: string | null | undefined,\n): PlanRelation | undefined {\n if (!current) return undefined;\n const index = (c: string) => plans.findIndex((p) => p.code.toLowerCase() === c.toLowerCase());\n const at = index(code);\n const now = index(current);\n if (at < 0 || now < 0) return undefined;\n return at === now ? \"current\" : at > now ? \"upgrade\" : \"downgrade\";\n}\n\n/** What both parts need to draw a plan. */\ninterface PlanDisplayProps {\n /** The currency the prices are shown in (§2.6). */\n currency: BillingCurrency;\n /** The period the prices are shown for. */\n interval: BillingInterval;\n /** The app's name of each limit's dimension (\"Budgets\"). A dimension without one is\n * shown by its key. */\n dimensionLabels?: Readonly<Record<string, string>>;\n /** How a limit's figure is written — kastlan's storage is bytes (`formatFileSize`).\n * Default: a number in the kit's locale. */\n formatLimit?: (dimension: string, value: number) => string;\n /** Over the provider's locale. */\n locale?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\nexport interface PlanCardProps\n extends Omit<ChoiceCardProps, \"type\" | \"title\" | \"description\" | \"icon\" | \"indeterminate\" | \"value\" | \"labels\">,\n PlanDisplayProps {\n plan: BillingPlan;\n /** See {@link PlanRelation}. `current` draws the \"Current plan\" chip — a word in the\n * card's name, never a colour alone. */\n relation?: PlanRelation;\n /**\n * The provider's own localised price in place of the catalogue's figure (§12.17) — a\n * Merchant of Record's price preview for the buyer's country, tax included. The period\n * and \"VAT included\" stay: the law asks for a gross price, and the card says so.\n */\n pricePreview?: ReactNode;\n}\n\n/** A list from spans (`role=\"list\"`): the card is a `<label>`, which may hold phrasing\n * content only, so a `<ul>` there would be invalid HTML. */\nfunction SpanList({ name, items, icon }: { name: string; items: ReactNode[]; icon: ReactNode }) {\n if (items.length === 0) return null;\n return (\n <span role=\"list\" aria-label={name} className=\"block space-y-1\">\n {items.map((item, i) => (\n <span role=\"listitem\" key={i} className=\"flex items-start gap-1.5 text-secondary\">\n <span aria-hidden className=\"mt-[0.0625rem] flex shrink-0 text-muted [&_svg]:size-3.5\">\n {icon}\n </span>\n <span className=\"min-w-0\">\n {item}\n {/* A space between the items' texts in the flattened description. */}{\" \"}\n </span>\n </span>\n ))}\n </span>\n );\n}\n\n/**\n * One plan as a {@link ChoiceCard} radio (§7): the name (and \"Current plan\"), the GROSS\n * price in the chosen currency and period through `formatMoney` (\"CHF 49 per year, VAT\n * included\", §12.17), the limits (\"Budgets: 3\", \"Seats: Unlimited\") and the app's feature\n * lines. A plan with prices but none for this currency and period is shown, \"Not offered\n * for this billing period\", and cannot be picked.\n *\n * A radio of a set: give the cards one `name`, or use {@link PlanPicker}, which does that\n * and owns the choice. Everything else a `ChoiceCard` takes goes to it — `checked`,\n * `onCheckedChange`, `name`, `className`. Carries `data-plan` and `data-relation`.\n */\nexport function PlanCard({\n // The radio `<input>` (ChoiceCard's): a React 19 ref prop since 0.34 (MOD-4, refactor\n // audit 2026-10), where this was `forwardRef`.\n ref,\n plan,\n relation,\n pricePreview,\n currency,\n interval,\n dimensionLabels,\n formatLimit,\n locale: localeProp,\n labels: labelsProp,\n disabled,\n ...rest\n}: PlanCardProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale(localeProp);\n const price = planPrice(plan.prices, currency, interval);\n\n const priced = price.kind === \"price\";\n const figure = priced\n ? pricePreview !== undefined && pricePreview !== null\n ? pricePreview\n : formatPlanPrice(price.minor, currency, { locale })\n : price.kind === \"free\"\n ? labels.free\n : labels.notOffered;\n\n const limitLines = Object.entries(plan.limits ?? {}).map(([dimension, value]) => {\n const name = dimensionLabels?.[dimension] ?? dimension;\n const shown =\n value === null ? labels.unlimited : formatLimit ? formatLimit(dimension, value) : formatNumber(value, { locale });\n return common.fieldValue(name, shown);\n });\n\n const body = (\n <span className=\"mt-1 block space-y-2\">\n <span className=\"block\">\n <span\n data-part=\"price\"\n className={cn(\n \"tabular-nums\",\n price.kind === \"not-offered\"\n ? \"text-xs text-muted\"\n : \"text-lg font-semibold leading-tight text-primary\",\n )}\n >\n {figure}\n </span>\n {priced && (\n <>\n {\" \"}\n <span className=\"text-xs text-secondary\">\n {interval === \"year\" ? labels.perYear : labels.perMonth}\n </span>{\" \"}\n <span className=\"block text-xs text-muted\">{labels.vatIncluded}</span>\n </>\n )}{\" \"}\n </span>\n {plan.description !== undefined && plan.description !== null && (\n <span className=\"block text-secondary\">{plan.description} </span>\n )}\n <SpanList name={labels.limits} items={limitLines} icon={<Gauge />} />\n <SpanList name={labels.features} items={[...(plan.features ?? [])]} icon={<Check />} />\n </span>\n );\n\n return (\n <ChoiceCard\n ref={ref}\n {...rest}\n type=\"radio\"\n value={plan.code}\n disabled={disabled || plan.disabled || price.kind === \"not-offered\"}\n data-plan={plan.code}\n data-relation={relation}\n title={\n <>\n {plan.name}\n {relation === \"current\" && (\n <>\n {\" \"}\n <Chip size=\"xs\" tone=\"brand\" shape=\"square\" caps className=\"ms-1 align-[0.0625rem]\">\n {labels.current}\n </Chip>\n </>\n )}\n </>\n }\n description={body}\n />\n );\n}\n\n/** What {@link PlanPickerProps.onChoose} is handed: everything `POST /billing/checkout`\n * takes (§4). */\nexport interface PlanChoice {\n plan: string;\n interval: BillingInterval;\n currency: BillingCurrency;\n}\n\nexport interface PlanPickerProps extends Omit<PlanDisplayProps, \"currency\" | \"interval\"> {\n /** The catalogue, in its order (`PlanSpec.sort`): cheapest first. The order decides\n * upgrade and downgrade. */\n plans: readonly BillingPlan[];\n /**\n * The plan the payer pays for now (`plan` of the overview while `active` or\n * `past_due`). Leave it out during a trial and after a plan ended: then every plan is\n * \"Choose …\", the trial's plan included, since none is paid for yet.\n */\n current?: string | null;\n /** The picked plan, controlled. `null` while none is. */\n value?: string | null;\n /** The picked plan, uncontrolled. Default: `current`, else nothing. */\n defaultValue?: string | null;\n onValueChange?: (plan: string) => void;\n /** The period, controlled. */\n interval?: BillingInterval;\n /** The period, uncontrolled. Default `year` — lead with yearly (§12.17). */\n defaultInterval?: BillingInterval;\n onIntervalChange?: (interval: BillingInterval) => void;\n /** The currency, controlled. */\n currency?: BillingCurrency;\n /** The currency, uncontrolled. Default: the account's stored currency is the app's to\n * pass here (§12.18); without it, {@link billingCurrencyFor} the kit's locale. */\n defaultCurrency?: BillingCurrency;\n onCurrencyChange?: (currency: BillingCurrency) => void;\n /** The currencies the payer may switch between. Default: every currency a plan has a\n * price in. The switch shows only for more than one. */\n currencies?: readonly BillingCurrency[];\n /** The button: the app starts its checkout (`POST /billing/checkout`, then off to the\n * provider's page). The kit sends no request. */\n onChoose: (choice: PlanChoice) => void;\n /** The checkout is being opened: the button's spinner, no second press. */\n pending?: boolean;\n /**\n * 0.42 (§16.2 decision 5, §16.8 B): the payer's free grant runs without an end —\n * `isPermanentGrant(overview)`. A purchase under it changes nothing the payer gets\n * while the grant runs (§12.11), so the button first opens a confirm —\n * `grantWarningTitle`, `grantWarningBody`, \"Buy anyway\" (`grantWarningAction`) and\n * \"Back\" — and {@link onChoose} runs only on \"Buy anyway\". Warned, not refused.\n *\n * Not while {@link switching}: then the pick opens `PlanSwitchConfirm`, which takes\n * the same `permanentGrant` and shows the warning above its figures — one warning,\n * in the dialog where the payer agrees, rather than a confirm before a confirm.\n */\n permanentGrant?: boolean;\n /**\n * 0.42: the overview's `switching` — the app sends this pick to `PlanSwitchConfirm`\n * (§15.2.10), not the checkout. Only decides where {@link permanentGrant}'s warning\n * shows; the button and {@link onChoose} are the same either way.\n */\n switching?: boolean;\n /** The provider's localised price for a plan, or `undefined` for the catalogue's —\n * see {@link PlanCardProps.pricePreview}. */\n pricePreview?: (plan: BillingPlan, choice: Omit<PlanChoice, \"plan\">) => ReactNode | undefined;\n /** A visible question over the cards. Without one the set is named \"Plans\". */\n legend?: ReactNode;\n /** Shared by the radios. Generated if omitted. */\n name?: string;\n /** Classes for the whole picker. */\n className?: string;\n /** Classes for the card grid. Default: one column, two from `sm`, three from `lg`. */\n gridClassName?: string;\n /** Classes for every card. */\n cardClassName?: string;\n}\n\n/** Every currency some plan has a price in, CHF first. */\nfunction pricedCurrencies(plans: readonly BillingPlan[]): BillingCurrency[] {\n const found = new Set<BillingCurrency>();\n for (const p of plans) for (const c of Object.keys(p.prices ?? {})) found.add(c as BillingCurrency);\n return ([\"CHF\", \"EUR\"] as const).filter((c) => found.has(c));\n}\n\n/** Every period some plan has a price for, yearly first. */\nfunction pricedIntervals(plans: readonly BillingPlan[]): BillingInterval[] {\n const found = new Set<BillingInterval>();\n for (const p of plans)\n for (const byInterval of Object.values(p.prices ?? {}))\n for (const [k, v] of Object.entries(byInterval ?? {})) if (typeof v === \"number\") found.add(k as BillingInterval);\n return BILLING_INTERVALS.filter((i) => found.has(i));\n}\n\n/**\n * The plans to choose from (§7): the period switch — yearly first (§12.17) — and, where\n * the payer may pay in either, the currency switch (§2.6); a {@link PlanCard} per plan in\n * a radio set; and one button that says what pressing it does: \"Upgrade to Pro\", \"Switch\n * to Free\", or \"Choose Pro\" with no plan paid for yet.\n *\n * One button under the set rather than one per card: each card IS a radio — a `<label>`\n * round an `<input>` — and a button inside a label is interactive content inside\n * interactive content. Picking and committing are two steps anyway: the price under the\n * picked card is what checkout will charge.\n *\n * The button never takes the write lock: billing itself always works, a read-only payer\n * must be able to pay (§3.3). It is off, with the reason on hover and focus, while\n * nothing is picked or the current plan is.\n *\n * 0.42 (§16.8 B): under a grant without an end (`permanentGrant`) the payer is warned\n * before paying for a plan that won't apply — here, in a confirm before `onChoose`, on\n * the way to the checkout; on the way to a switch (`switching`) in `PlanSwitchConfirm`\n * instead, so the warning shows once on either path.\n */\nexport function PlanPicker({\n plans,\n current,\n value: valueProp,\n defaultValue,\n onValueChange,\n interval: intervalProp,\n defaultInterval = \"year\",\n onIntervalChange,\n currency: currencyProp,\n defaultCurrency,\n onCurrencyChange,\n currencies: currenciesProp,\n onChoose,\n pending,\n permanentGrant = false,\n switching = false,\n pricePreview,\n legend,\n name,\n className,\n gridClassName,\n cardClassName,\n dimensionLabels,\n formatLimit,\n locale: localeProp,\n labels: labelsProp,\n}: PlanPickerProps) {\n const labels = useBillingLabels(labelsProp);\n const locale = useKitLocale(localeProp);\n const generated = useId();\n /** The choice the grant warning holds back until \"Buy anyway\". */\n const [warned, setWarned] = useState<PlanChoice | null>(null);\n const groupName = name ?? generated;\n const legendId = `${generated}-legend`;\n\n const [ownValue, setOwnValue] = useState<string | null>(() => defaultValue ?? current ?? null);\n const [ownInterval, setOwnInterval] = useState<BillingInterval>(defaultInterval);\n const [ownCurrency, setOwnCurrency] = useState<BillingCurrency>(\n () => defaultCurrency ?? billingCurrencyFor(locale),\n );\n const value = valueProp !== undefined ? valueProp : ownValue;\n const interval = intervalProp ?? ownInterval;\n const currency = currencyProp ?? ownCurrency;\n\n const pick = (code: string) => {\n if (valueProp === undefined) setOwnValue(code);\n onValueChange?.(code);\n };\n const setInterval = (next: BillingInterval) => {\n if (intervalProp === undefined) setOwnInterval(next);\n onIntervalChange?.(next);\n };\n const setCurrency = (next: BillingCurrency) => {\n if (currencyProp === undefined) setOwnCurrency(next);\n onCurrencyChange?.(next);\n };\n\n const currencies = currenciesProp ?? pricedCurrencies(plans);\n const intervals = pricedIntervals(plans);\n const picked = value ? plans.find((p) => p.code.toLowerCase() === value.toLowerCase()) : undefined;\n const relation = picked ? planRelation(plans, picked.code, current) : undefined;\n const pickedPrice = picked ? planPrice(picked.prices, currency, interval) : undefined;\n\n const buttonText = !picked\n ? labels.choosePlan\n : relation === \"upgrade\"\n ? labels.upgrade(picked.name)\n : relation === \"downgrade\"\n ? labels.downgrade(picked.name)\n : labels.choose(picked.name);\n const blocked = !picked\n ? labels.pickFirst\n : relation === \"current\"\n ? labels.isCurrent\n : pickedPrice?.kind === \"not-offered\"\n ? labels.notOffered\n : undefined;\n\n return (\n <div className={cn(\"min-w-0 space-y-3\", className)} data-billing-interval={interval} data-billing-currency={currency}>\n {(intervals.length > 1 || currencies.length > 1) && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {intervals.length > 1 && (\n <ToggleGroup\n aria-label={labels.interval}\n // Content-wide, so the two switches share a row and wrap only when narrow.\n className=\"w-auto\"\n options={intervals.map((i) => ({ value: i, label: i === \"year\" ? labels.yearly : labels.monthly }))}\n value={interval}\n onChange={setInterval}\n />\n )}\n {currencies.length > 1 && (\n <ToggleGroup\n aria-label={labels.currency}\n className=\"w-auto\"\n options={currencies.map((c) => ({ value: c, label: c }))}\n value={currency}\n onChange={setCurrency}\n />\n )}\n </div>\n )}\n <fieldset\n className=\"min-w-0\"\n aria-labelledby={legend !== undefined ? legendId : undefined}\n aria-label={legend === undefined ? labels.plans : undefined}\n >\n {legend !== undefined && (\n <legend id={legendId} className=\"mb-2 text-sm font-medium text-primary\">\n {legend}\n </legend>\n )}\n <div className={cn(\"grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3\", gridClassName)}>\n {plans.map((plan) => (\n <PlanCard\n key={plan.code}\n plan={plan}\n name={groupName}\n currency={currency}\n interval={interval}\n relation={planRelation(plans, plan.code, current)}\n pricePreview={pricePreview?.(plan, { interval, currency })}\n checked={picked?.code === plan.code}\n onCheckedChange={(on) => on && pick(plan.code)}\n dimensionLabels={dimensionLabels}\n formatLimit={formatLimit}\n locale={locale}\n labels={labelsProp}\n className={cardClassName}\n />\n ))}\n </div>\n </fieldset>\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant=\"primary\"\n className=\"max-sm:w-full\"\n disabledReason={blocked}\n pending={pending}\n onClick={() => {\n if (!picked) return;\n const choice = { plan: picked.code, interval, currency };\n if (permanentGrant && !switching) setWarned(choice);\n else onChoose(choice);\n }}\n >\n {buttonText}\n </Button>\n </div>\n {permanentGrant && !switching && (\n <GrantWarningConfirm\n open={warned !== null}\n onOpenChange={(open) => {\n if (!open) setWarned(null);\n }}\n onConfirm={() => {\n if (warned) onChoose(warned);\n }}\n labels={labelsProp}\n />\n )}\n </div>\n );\n}\n\nexport interface GrantWarningConfirmProps {\n /** Shown, controlled. */\n open: boolean;\n /** `false` on \"Back\", Escape, the backdrop — and after \"Buy anyway\". */\n onOpenChange: (open: boolean) => void;\n /** \"Buy anyway\": the app opens its checkout. The dialog closes itself after it. */\n onConfirm: () => void;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English: `grantWarningTitle`,\n * `grantWarningBody`, `grantWarningAction`. \"Back\" is `common.back`. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * 0.42 (§16.8 B): the permanent-grant warning before a checkout — `grantWarningTitle`,\n * `grantWarningBody`, \"Back\" and \"Buy anyway\" (`grantWarningAction`). {@link PlanPicker}\n * shows it itself under `permanentGrant` when not `switching`. Exported for the path the\n * picker cannot see: an app that meant to switch, learned there is no subscription to\n * switch (keksdose: `/billing/plan/preview` → 409 `billing_not_subscribed`) and falls back\n * to a checkout — show this before that checkout, so a permanent-grant holder is still\n * warned. Carries `data-grant-warning`.\n */\nexport function GrantWarningConfirm({ open, onOpenChange, onConfirm, labels: labelsProp }: GrantWarningConfirmProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n return (\n <DialogFrame\n open={open}\n title={labels.grantWarningTitle}\n description={labels.grantWarningBody}\n onClose={() => onOpenChange(false)}\n data-grant-warning=\"\"\n actions={(close) => (\n <>\n <Button variant=\"secondary\" onClick={close}>\n {common.back}\n </Button>\n <Button\n variant=\"primary\"\n onClick={() => {\n onConfirm();\n onOpenChange(false);\n }}\n >\n {labels.grantWarningAction}\n </Button>\n </>\n )}\n />\n );\n}\n"],"mappings":"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","names":[]}
|