@eifi1/ui-kit 0.41.0 → 0.42.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.
Files changed (143) hide show
  1. package/README.md +19 -6
  2. package/dist/admin/admin-area-labels.d.ts +55 -0
  3. package/dist/admin/admin-area-labels.js +33 -0
  4. package/dist/admin/admin-area-labels.js.map +1 -0
  5. package/dist/admin/admin-forbidden.d.ts +35 -0
  6. package/dist/admin/admin-forbidden.js +36 -0
  7. package/dist/admin/admin-forbidden.js.map +1 -0
  8. package/dist/admin/admin-overview.d.ts +93 -0
  9. package/dist/admin/admin-overview.js +69 -0
  10. package/dist/admin/admin-overview.js.map +1 -0
  11. package/dist/admin/admin-sections.d.ts +63 -0
  12. package/dist/admin/admin-sections.js +64 -0
  13. package/dist/admin/admin-sections.js.map +1 -0
  14. package/dist/admin/admin-tool-links.d.ts +48 -0
  15. package/dist/admin/admin-tool-links.js +31 -0
  16. package/dist/admin/admin-tool-links.js.map +1 -0
  17. package/dist/billing/billing-banner.d.ts +25 -2
  18. package/dist/billing/billing-banner.js +5 -2
  19. package/dist/billing/billing-banner.js.map +1 -1
  20. package/dist/billing/billing-labels.d.ts +32 -0
  21. package/dist/billing/billing-labels.js +9 -0
  22. package/dist/billing/billing-labels.js.map +1 -1
  23. package/dist/billing/billing-overview.d.ts +33 -0
  24. package/dist/billing/billing-overview.js +6 -0
  25. package/dist/billing/billing-overview.js.map +1 -1
  26. package/dist/billing/billing-standing.d.ts +8 -0
  27. package/dist/billing/billing-standing.js.map +1 -1
  28. package/dist/billing/cancel-subscription.d.ts +46 -0
  29. package/dist/billing/cancel-subscription.js +57 -0
  30. package/dist/billing/cancel-subscription.js.map +1 -0
  31. package/dist/billing/plan-card.d.ts +45 -1
  32. package/dist/billing/plan-card.js +53 -2
  33. package/dist/billing/plan-card.js.map +1 -1
  34. package/dist/billing/plan-switch.d.ts +10 -1
  35. package/dist/billing/plan-switch.js +22 -13
  36. package/dist/billing/plan-switch.js.map +1 -1
  37. package/dist/billing/subscription-actions.d.ts +37 -4
  38. package/dist/billing/subscription-actions.js +10 -3
  39. package/dist/billing/subscription-actions.js.map +1 -1
  40. package/dist/components/account-chips.d.ts +42 -1
  41. package/dist/components/account-chips.js +33 -0
  42. package/dist/components/account-chips.js.map +1 -1
  43. package/dist/components/empty-state.d.ts +2 -1
  44. package/dist/components/empty-state.js.map +1 -1
  45. package/dist/eslint/index.js +1 -1
  46. package/dist/i18n/defaults.js +5 -1
  47. package/dist/i18n/defaults.js.map +1 -1
  48. package/dist/i18n/german.js +79 -0
  49. package/dist/i18n/german.js.map +1 -1
  50. package/dist/i18n/kit-labels.d.ts +9 -0
  51. package/dist/i18n/kit-labels.js.map +1 -1
  52. package/dist/i18n/locales/en.js +93 -1
  53. package/dist/i18n/locales/en.js.map +3 -3
  54. package/dist/i18n/locales/es.js +76 -0
  55. package/dist/i18n/locales/es.js.map +1 -1
  56. package/dist/i18n/locales/fr.js +75 -0
  57. package/dist/i18n/locales/fr.js.map +1 -1
  58. package/dist/i18n/locales/hu.js +76 -0
  59. package/dist/i18n/locales/hu.js.map +1 -1
  60. package/dist/i18n/locales/it.js +76 -0
  61. package/dist/i18n/locales/it.js.map +1 -1
  62. package/dist/i18n/locales/zh.js +75 -0
  63. package/dist/i18n/locales/zh.js.map +1 -1
  64. package/dist/i18n/review.js +20 -1
  65. package/dist/i18n/review.js.map +1 -1
  66. package/dist/index.d.ts +18 -2
  67. package/dist/index.js +23 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/settings/settings-layout.d.ts +7 -1
  70. package/dist/settings/settings-layout.js +35 -27
  71. package/dist/settings/settings-layout.js.map +1 -1
  72. package/dist/shell/account-menu-language.d.ts +46 -0
  73. package/dist/shell/account-menu-language.js +64 -0
  74. package/dist/shell/account-menu-language.js.map +1 -0
  75. package/dist/shell/admin-menu.d.ts +46 -0
  76. package/dist/shell/admin-menu.js +60 -0
  77. package/dist/shell/admin-menu.js.map +1 -0
  78. package/dist/shell/app-shell.d.ts +10 -0
  79. package/dist/shell/app-shell.js +22 -4
  80. package/dist/shell/app-shell.js.map +1 -1
  81. package/dist/shell/dev-switchers.d.ts +53 -0
  82. package/dist/shell/dev-switchers.js +102 -0
  83. package/dist/shell/dev-switchers.js.map +1 -0
  84. package/dist/shell/sync-status-indicator.d.ts +86 -0
  85. package/dist/shell/sync-status-indicator.js +170 -0
  86. package/dist/shell/sync-status-indicator.js.map +1 -0
  87. package/dist/shell/top-bar-actions.d.ts +49 -0
  88. package/dist/shell/top-bar-actions.js +31 -0
  89. package/dist/shell/top-bar-actions.js.map +1 -0
  90. package/dist/shell/top-bar-parts-labels.d.ts +84 -0
  91. package/dist/shell/top-bar-parts-labels.js +48 -0
  92. package/dist/shell/top-bar-parts-labels.js.map +1 -0
  93. package/dist/shell/topbar-action-menu.d.ts +13 -1
  94. package/dist/shell/topbar-action-menu.js +5 -2
  95. package/dist/shell/topbar-action-menu.js.map +1 -1
  96. package/dist/shell/topbar-controls.d.ts +16 -3
  97. package/dist/shell/topbar-controls.js +5 -1
  98. package/dist/shell/topbar-controls.js.map +1 -1
  99. package/dist/shell/tour-menu.d.ts +58 -0
  100. package/dist/shell/tour-menu.js +73 -0
  101. package/dist/shell/tour-menu.js.map +1 -0
  102. package/dist/shell.d.ts +14 -0
  103. package/dist/shell.js +19 -0
  104. package/dist/shell.js.map +1 -1
  105. package/package.json +1 -1
  106. package/src/admin/admin-area-labels.ts +85 -0
  107. package/src/admin/admin-forbidden.tsx +75 -0
  108. package/src/admin/admin-overview.tsx +166 -0
  109. package/src/admin/admin-sections.tsx +137 -0
  110. package/src/admin/admin-tool-links.tsx +84 -0
  111. package/src/billing/billing-banner.tsx +32 -4
  112. package/src/billing/billing-labels.ts +53 -0
  113. package/src/billing/billing-overview.ts +37 -0
  114. package/src/billing/billing-standing.ts +8 -0
  115. package/src/billing/cancel-subscription.tsx +102 -0
  116. package/src/billing/plan-card.tsx +97 -1
  117. package/src/billing/plan-switch.tsx +16 -0
  118. package/src/billing/subscription-actions.tsx +64 -15
  119. package/src/components/account-chips.tsx +81 -1
  120. package/src/components/empty-state.tsx +2 -1
  121. package/src/i18n/defaults.ts +4 -0
  122. package/src/i18n/german.ts +92 -0
  123. package/src/i18n/kit-labels.tsx +9 -0
  124. package/src/i18n/locales/en.ts +19 -0
  125. package/src/i18n/locales/es.ts +86 -0
  126. package/src/i18n/locales/fr.ts +86 -0
  127. package/src/i18n/locales/hu.ts +84 -0
  128. package/src/i18n/locales/it.ts +87 -0
  129. package/src/i18n/locales/zh.ts +79 -0
  130. package/src/i18n/review.ts +19 -0
  131. package/src/index.ts +30 -2
  132. package/src/settings/settings-layout.tsx +58 -34
  133. package/src/shell/account-menu-language.tsx +116 -0
  134. package/src/shell/admin-menu.tsx +117 -0
  135. package/src/shell/app-shell.tsx +44 -3
  136. package/src/shell/dev-switchers.tsx +171 -0
  137. package/src/shell/sync-status-indicator.tsx +301 -0
  138. package/src/shell/top-bar-actions.tsx +79 -0
  139. package/src/shell/top-bar-parts-labels.ts +133 -0
  140. package/src/shell/topbar-action-menu.tsx +15 -2
  141. package/src/shell/topbar-controls.tsx +20 -3
  142. package/src/shell/tour-menu.tsx +144 -0
  143. package/src/shell.ts +24 -0
@@ -0,0 +1,48 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import type { ListItemLinkProps } from "../components/list.js";
3
+ import type { LabelOverride } from "../i18n/kit-labels.js";
4
+ import type { AdminAreaLabels } from "./admin-area-labels.js";
5
+ /** One link of the Tools list. */
6
+ export interface AdminToolLink {
7
+ /** Unique in the list. */
8
+ key: string;
9
+ label: string;
10
+ href: string;
11
+ /** A lucide element, sized by the row. */
12
+ icon?: ReactNode;
13
+ /** `false` leaves the link out — Translations for an admin who doesn't review
14
+ * (§6.11, ka F7). Default: shown. */
15
+ visible?: boolean;
16
+ }
17
+ export interface AdminToolLinksProps {
18
+ links: readonly AdminToolLink[];
19
+ /**
20
+ * `sidebar` (default): a `<nav>` named and headed "Tools", for `SettingsLayout`'s
21
+ * `sidebarFooter` under the section list on a desktop. `list`: the bare rows, for
22
+ * `SettingsLayout`'s `phoneFooter`, which puts them in a list of their own.
23
+ */
24
+ variant?: "sidebar" | "list";
25
+ /** Your router's link. Default: the `<UiKitProvider linkComponent>`, then `<a>`. */
26
+ renderLink?: (props: ListItemLinkProps) => ReactElement;
27
+ /** Prop > `<UiKitProvider labels={{ adminArea }}>` > English. */
28
+ labels?: LabelOverride<AdminAreaLabels>;
29
+ className?: string;
30
+ }
31
+ /**
32
+ * The admin area's links to the admin tools that are pages of their own (0.42.0,
33
+ * docs/admin-harmonization.md §2.5 decision 16, §6.11): Translations, and the app's task
34
+ * pages (kastlan's Data import). The feedback inbox is not one of them: the top bar's
35
+ * feedback menu reaches it, and the overview's tiles link to it.
36
+ *
37
+ * Draw it twice from one list — at the foot of the section sidebar and in the phone
38
+ * footer:
39
+ *
40
+ * <SettingsLayout
41
+ * sidebarFooter={<AdminToolLinks links={TOOLS} />}
42
+ * phoneFooter={<AdminToolLinks links={TOOLS} variant="list" />}
43
+ * …
44
+ * />
45
+ *
46
+ * With no visible link it renders nothing.
47
+ */
48
+ export declare function AdminToolLinks({ links, variant, renderLink, labels: labelsProp, className }: AdminToolLinksProps): import("react").JSX.Element | null;
@@ -0,0 +1,31 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useId } from "react";
4
+ import { cn } from "../lib/cn.js";
5
+ import { List, ListItem } from "../components/list.js";
6
+ import { useAdminAreaLabels } from "./admin-area-labels.js";
7
+ function AdminToolLinks({ links, variant = "sidebar", renderLink, labels: labelsProp, className }) {
8
+ const labels = useAdminAreaLabels(labelsProp);
9
+ const headingId = useId();
10
+ const shown = links.filter((l) => l.visible !== false);
11
+ if (shown.length === 0) return null;
12
+ const rows = shown.map((l) => /* @__PURE__ */ jsx(
13
+ ListItem,
14
+ {
15
+ href: l.href,
16
+ renderLink,
17
+ title: l.label,
18
+ leading: l.icon === void 0 ? void 0 : /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "flex shrink-0 text-muted [&_svg]:size-4", children: l.icon })
19
+ },
20
+ l.key
21
+ ));
22
+ if (variant === "list") return /* @__PURE__ */ jsx(Fragment, { children: rows });
23
+ return /* @__PURE__ */ jsxs("nav", { "data-slot": "admin-tool-links", "aria-labelledby": headingId, className: cn("space-y-1", className), children: [
24
+ /* @__PURE__ */ jsx("div", { id: headingId, className: "px-3 text-caption font-semibold uppercase tracking-wide text-muted", children: labels.tools }),
25
+ /* @__PURE__ */ jsx(List, { density: "compact", children: rows })
26
+ ] });
27
+ }
28
+ export {
29
+ AdminToolLinks
30
+ };
31
+ //# sourceMappingURL=admin-tool-links.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/admin/admin-tool-links.tsx"],"sourcesContent":["import { useId } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport { cn } from \"../lib/cn\";\nimport { List, ListItem } from \"../components/list\";\nimport type { ListItemLinkProps } from \"../components/list\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { useAdminAreaLabels } from \"./admin-area-labels\";\nimport type { AdminAreaLabels } from \"./admin-area-labels\";\n\n/** One link of the Tools list. */\nexport interface AdminToolLink {\n /** Unique in the list. */\n key: string;\n label: string;\n href: string;\n /** A lucide element, sized by the row. */\n icon?: ReactNode;\n /** `false` leaves the link out — Translations for an admin who doesn't review\n * (§6.11, ka F7). Default: shown. */\n visible?: boolean;\n}\n\nexport interface AdminToolLinksProps {\n links: readonly AdminToolLink[];\n /**\n * `sidebar` (default): a `<nav>` named and headed \"Tools\", for `SettingsLayout`'s\n * `sidebarFooter` under the section list on a desktop. `list`: the bare rows, for\n * `SettingsLayout`'s `phoneFooter`, which puts them in a list of their own.\n */\n variant?: \"sidebar\" | \"list\";\n /** Your router's link. Default: the `<UiKitProvider linkComponent>`, then `<a>`. */\n renderLink?: (props: ListItemLinkProps) => ReactElement;\n /** Prop > `<UiKitProvider labels={{ adminArea }}>` > English. */\n labels?: LabelOverride<AdminAreaLabels>;\n className?: string;\n}\n\n/**\n * The admin area's links to the admin tools that are pages of their own (0.42.0,\n * docs/admin-harmonization.md §2.5 decision 16, §6.11): Translations, and the app's task\n * pages (kastlan's Data import). The feedback inbox is not one of them: the top bar's\n * feedback menu reaches it, and the overview's tiles link to it.\n *\n * Draw it twice from one list — at the foot of the section sidebar and in the phone\n * footer:\n *\n * <SettingsLayout\n * sidebarFooter={<AdminToolLinks links={TOOLS} />}\n * phoneFooter={<AdminToolLinks links={TOOLS} variant=\"list\" />}\n * …\n * />\n *\n * With no visible link it renders nothing.\n */\nexport function AdminToolLinks({ links, variant = \"sidebar\", renderLink, labels: labelsProp, className }: AdminToolLinksProps) {\n const labels = useAdminAreaLabels(labelsProp);\n const headingId = useId();\n const shown = links.filter((l) => l.visible !== false);\n if (shown.length === 0) return null;\n const rows = shown.map((l) => (\n <ListItem\n key={l.key}\n href={l.href}\n renderLink={renderLink}\n title={l.label}\n leading={\n l.icon === undefined ? undefined : (\n <span aria-hidden className=\"flex shrink-0 text-muted [&_svg]:size-4\">\n {l.icon}\n </span>\n )\n }\n />\n ));\n if (variant === \"list\") return <>{rows}</>;\n return (\n <nav data-slot=\"admin-tool-links\" aria-labelledby={headingId} className={cn(\"space-y-1\", className)}>\n <div id={headingId} className=\"px-3 text-caption font-semibold uppercase tracking-wide text-muted\">\n {labels.tools}\n </div>\n <List density=\"compact\">{rows}</List>\n </nav>\n );\n}\n"],"mappings":";AAmEU,SAOuB,UAPvB,KASN,YATM;AAnEV,SAAS,aAAa;AAEtB,SAAS,UAAU;AACnB,SAAS,MAAM,gBAAgB;AAG/B,SAAS,0BAA0B;AAgD5B,SAAS,eAAe,EAAE,OAAO,UAAU,WAAW,YAAY,QAAQ,YAAY,UAAU,GAAwB;AAC7H,QAAM,SAAS,mBAAmB,UAAU;AAC5C,QAAM,YAAY,MAAM;AACxB,QAAM,QAAQ,MAAM,OAAO,CAAC,MAAM,EAAE,YAAY,KAAK;AACrD,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,QAAM,OAAO,MAAM,IAAI,CAAC,MACtB;AAAA,IAAC;AAAA;AAAA,MAEC,MAAM,EAAE;AAAA,MACR;AAAA,MACA,OAAO,EAAE;AAAA,MACT,SACE,EAAE,SAAS,SAAY,SACrB,oBAAC,UAAK,eAAW,MAAC,WAAU,2CACzB,YAAE,MACL;AAAA;AAAA,IARC,EAAE;AAAA,EAWT,CACD;AACD,MAAI,YAAY,OAAQ,QAAO,gCAAG,gBAAK;AACvC,SACE,qBAAC,SAAI,aAAU,oBAAmB,mBAAiB,WAAW,WAAW,GAAG,aAAa,SAAS,GAChG;AAAA,wBAAC,SAAI,IAAI,WAAW,WAAU,sEAC3B,iBAAO,OACV;AAAA,IACA,oBAAC,QAAK,SAAQ,WAAW,gBAAK;AAAA,KAChC;AAEJ;","names":[]}
@@ -2,6 +2,7 @@ import type { ComponentPropsWithRef } from "react";
2
2
  import type { LabelOverride } from "../i18n/kit-labels.js";
3
3
  import type { AlertSize, AlertTone } from "../components/alert-banner.js";
4
4
  import type { BillingLabels } from "./billing-labels.js";
5
+ import type { BillingPortalAccess } from "./billing-overview.js";
5
6
  /**
6
7
  * The billing banners (docs/billing-harmonization.md §3.3, §7, §12.6, §12.21):
7
8
  *
@@ -9,7 +10,8 @@ import type { BillingLabels } from "./billing-labels.js";
9
10
  * - `grant-ending` — from 7 days before `comped_until`: a beta user's 12 months, an
10
11
  * operator's grant;
11
12
  * - `payment-failed` — from the first failed payment (`past_due`), while the provider
12
- * retries;
13
+ * retries; since 0.42, for an admin who isn't the buyer (`portal="buyer_only"`), it
14
+ * names who can update the payment method, with no button;
13
15
  * - `plan-ended` — out of good standing: read-only, never locked (§2.3);
14
16
  * - `guest` — a guest in someone else's read-only item (§12.6);
15
17
  * - `processing` — back from checkout before the webhook landed (§12.21).
@@ -61,16 +63,37 @@ export type BillingBannerProps = BillingBannerBaseProps & ({
61
63
  /** Calendar days left — `billingDaysLeft(trial_ends_at)`. 0 is today. */
62
64
  daysLeft: number;
63
65
  item?: never;
66
+ portal?: never;
67
+ buyerName?: never;
64
68
  } | {
65
69
  kind: "guest";
66
70
  /** The item's own name ("Household 2026"), or nothing. NEVER the owner's status:
67
71
  * the banner says "for now" and nothing more (§12.6). */
68
72
  item?: string;
69
73
  daysLeft?: never;
74
+ portal?: never;
75
+ buyerName?: never;
70
76
  } | {
71
- kind: "payment-failed" | "plan-ended" | "processing";
77
+ kind: "payment-failed";
78
+ /**
79
+ * 0.42 (§16.7 A, §16.8): `GET /billing/status`'s `portal` (`BillingStanding`).
80
+ * `"buyer_only"` — the viewer isn't the buyer, so the portal's page for the card
81
+ * would answer 403: the banner says who can update the payment method
82
+ * (`paymentFailedAskBuyer`) and shows NO button, whatever `actionHref` or
83
+ * `onAction` say. `"open"` (default): as before.
84
+ */
85
+ portal?: BillingPortalAccess;
86
+ /** 0.42: the status's `buyer_name`, under `"buyer_only"`. Empty: "…the admin who
87
+ * bought the subscription". */
88
+ buyerName?: string | null;
89
+ daysLeft?: never;
90
+ item?: never;
91
+ } | {
92
+ kind: "plan-ended" | "processing";
72
93
  daysLeft?: never;
73
94
  item?: never;
95
+ portal?: never;
96
+ buyerName?: never;
74
97
  });
75
98
  /**
76
99
  * One of the billing banners, as a kit `AlertBanner` strip (§7): the kind's sentence, its
@@ -24,6 +24,8 @@ function BillingBanner(props) {
24
24
  kind,
25
25
  daysLeft,
26
26
  item,
27
+ portal,
28
+ buyerName,
27
29
  tone,
28
30
  variant = "strip",
29
31
  size,
@@ -42,6 +44,7 @@ function BillingBanner(props) {
42
44
  let text;
43
45
  let icon;
44
46
  let ownAction;
47
+ const askBuyer = kind === "payment-failed" && portal === "buyer_only";
45
48
  switch (kind) {
46
49
  case "trial-ending":
47
50
  text = labels.trialEnding(days);
@@ -54,7 +57,7 @@ function BillingBanner(props) {
54
57
  ownAction = labels.choosePlan;
55
58
  break;
56
59
  case "payment-failed":
57
- text = labels.paymentFailed;
60
+ text = askBuyer ? labels.paymentFailedAskBuyer(buyerName || void 0) : labels.paymentFailed;
58
61
  icon = /* @__PURE__ */ jsx(CreditCard, {});
59
62
  ownAction = labels.updatePayment;
60
63
  break;
@@ -75,7 +78,7 @@ function BillingBanner(props) {
75
78
  break;
76
79
  }
77
80
  const words = actionLabel ?? ownAction;
78
- const action = words === void 0 || actionHref === void 0 && onAction === void 0 ? void 0 : actionHref !== void 0 ? /* @__PURE__ */ jsx(Button, { href: actionHref, variant: "secondary", size: "sm", children: words }) : /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: onAction, pending: actionPending, children: words });
81
+ const action = askBuyer || words === void 0 || actionHref === void 0 && onAction === void 0 ? void 0 : actionHref !== void 0 ? /* @__PURE__ */ jsx(Button, { href: actionHref, variant: "secondary", size: "sm", children: words }) : /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", size: "sm", onClick: onAction, pending: actionPending, children: words });
79
82
  return /* @__PURE__ */ jsx(
80
83
  AlertBanner,
81
84
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/billing/billing-banner.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { CreditCard, Hourglass, PencilOff } from \"lucide-react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport type { AlertSize, AlertTone } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { Spinner } from \"../components/spinner\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\n\n/**\n * The billing banners (docs/billing-harmonization.md §3.3, §7, §12.6, §12.21):\n *\n * - `trial-ending` — from 7 days before `trial_ends_at` (§3.3);\n * - `grant-ending` — from 7 days before `comped_until`: a beta user's 12 months, an\n * operator's grant;\n * - `payment-failed` — from the first failed payment (`past_due`), while the provider\n * retries;\n * - `plan-ended` — out of good standing: read-only, never locked (§2.3);\n * - `guest` — a guest in someone else's read-only item (§12.6);\n * - `processing` — back from checkout before the webhook landed (§12.21).\n */\nexport type BillingBannerKind =\n | \"trial-ending\"\n | \"grant-ending\"\n | \"payment-failed\"\n | \"plan-ended\"\n | \"guest\"\n | \"processing\";\n\n/** A `div`'s props, `ref` included since 0.35 (round 0.35 R4, the follow-up to API-2): they\n * go on to the {@link AlertBanner}, so the ref is the banner's `<div>` — with an × too,\n * since the banner has no `href` or `onClick` that would make its row a link or a button. */\ninterface BillingBannerBaseProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"onClick\"> {\n /**\n * Over the kind's tone: `trial-ending` and `grant-ending` are `info` until the last\n * three days and `warning` in them; `payment-failed` is `danger` (money did not\n * arrive — kastlan's past-due strip was red); `plan-ended` is `warning`, a state with\n * everything still there rather than an error; `guest` and `processing` are `info`.\n */\n tone?: AlertTone;\n /** `strip` (default): the band under the top bar, on every page. `box`: the same\n * message on the subscription page itself. */\n variant?: \"strip\" | \"box\";\n /** Default `sm` for a strip (the demo's banner), `md` for a box. */\n size?: AlertSize;\n /**\n * Where the action goes — the subscription page, say. The app's action, as an `href`\n * or as {@link onAction}; without either there is no button. Never a write-locked\n * commit: billing itself always works (§3.3).\n */\n actionHref?: string;\n /** What the action does — the app's portal call (`POST /billing/portal`, then off to\n * its `url`): for `payment-failed` with `{target: \"payment_method\"}`, the portal's\n * page for the card (§14.5); for `processing`, `useCheckoutProcessing`'s\n * `checkAgain`. The kit sends no request. */\n onAction?: () => void;\n /**\n * Over the kind's action words: \"Choose a plan\" (trial, grant, plan ended), \"Update\n * payment method\" (payment failed), \"Check again\" (processing). The guest banner has\n * none of its own: an action there needs this.\n */\n actionLabel?: string;\n /** The action is under way: the button's spinner, no second press. */\n actionPending?: boolean;\n /** An × that calls this; the app removes the banner and remembers that it did. */\n onDismiss?: () => void;\n dismissLabel?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\nexport type BillingBannerProps = BillingBannerBaseProps &\n (\n | {\n kind: \"trial-ending\" | \"grant-ending\";\n /** Calendar days left — `billingDaysLeft(trial_ends_at)`. 0 is today. */\n daysLeft: number;\n item?: never;\n }\n | {\n kind: \"guest\";\n /** The item's own name (\"Household 2026\"), or nothing. NEVER the owner's status:\n * the banner says \"for now\" and nothing more (§12.6). */\n item?: string;\n daysLeft?: never;\n }\n | {\n kind: \"payment-failed\" | \"plan-ended\" | \"processing\";\n daysLeft?: never;\n item?: never;\n }\n );\n\n/** The last days, in which an ending trial or grant turns from news into a warning. */\nconst LAST_DAYS = 3;\n\nfunction defaultTone(kind: BillingBannerKind, daysLeft: number | undefined): AlertTone {\n switch (kind) {\n case \"trial-ending\":\n case \"grant-ending\":\n return (daysLeft ?? 0) > LAST_DAYS ? \"info\" : \"warning\";\n case \"payment-failed\":\n return \"danger\";\n case \"plan-ended\":\n return \"warning\";\n default:\n return \"info\";\n }\n}\n\n/**\n * One of the billing banners, as a kit `AlertBanner` strip (§7): the kind's sentence, its\n * glyph and tone, and the app's action. Carries `data-billing-banner` with the kind.\n *\n * The app decides WHICH banner shows, from `GET /billing/overview` (and hides all of them\n * while billing is off, §4); the kit only says it. A guest's banner takes no status at\n * all — the type does not let one in — so the owner's payment state cannot leak into it\n * by a wrong prop (§12.6).\n *\n * `processing` is a live region (`role=\"status\"`): it appears when the person comes back\n * from checkout and goes when the overview catches up (§12.21), and a screen reader\n * should hear both. The others are page furniture, role-less as the strip is.\n */\nexport function BillingBanner(props: BillingBannerProps) {\n const {\n kind,\n daysLeft,\n item,\n tone,\n variant = \"strip\",\n size,\n actionHref,\n onAction,\n actionLabel,\n actionPending,\n onDismiss,\n dismissLabel,\n labels: labelsProp,\n role,\n ...rest\n } = props;\n const labels = useBillingLabels(labelsProp);\n const days = Math.max(0, Math.floor(daysLeft ?? 0));\n\n let text: string;\n let icon: ReactNode;\n let ownAction: string | undefined;\n switch (kind) {\n case \"trial-ending\":\n text = labels.trialEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"grant-ending\":\n text = labels.grantEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"payment-failed\":\n text = labels.paymentFailed;\n icon = <CreditCard />;\n ownAction = labels.updatePayment;\n break;\n case \"plan-ended\":\n text = labels.planEnded;\n icon = <PencilOff />;\n ownAction = labels.choosePlan;\n break;\n case \"guest\":\n text = labels.guestReadOnly(item);\n icon = <PencilOff />;\n ownAction = undefined;\n break;\n case \"processing\":\n text = labels.processing;\n icon = <Spinner label={null} />;\n ownAction = labels.checkAgain;\n break;\n }\n\n const words = actionLabel ?? ownAction;\n const action =\n words === undefined || (actionHref === undefined && onAction === undefined) ? undefined : actionHref !== undefined ? (\n <Button href={actionHref} variant=\"secondary\" size=\"sm\">\n {words}\n </Button>\n ) : (\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={onAction} pending={actionPending}>\n {words}\n </Button>\n );\n\n return (\n <AlertBanner\n {...rest}\n role={role ?? (kind === \"processing\" ? \"status\" : undefined)}\n variant={variant}\n size={size ?? (variant === \"strip\" ? \"sm\" : \"md\")}\n tone={tone ?? defaultTone(kind, days)}\n icon={icon}\n action={action}\n onDismiss={onDismiss}\n dismissLabel={dismissLabel}\n data-billing-banner={kind}\n >\n {text}\n </AlertBanner>\n );\n}\n"],"mappings":";AAwJa;AAvJb,SAAS,YAAY,WAAW,iBAAiB;AAGjD,SAAS,mBAAmB;AAE5B,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,wBAAwB;AAwFjC,MAAM,YAAY;AAElB,SAAS,YAAY,MAAyB,UAAyC;AACrF,UAAQ,MAAM;AAAA,IACZ,KAAK;AAAA,IACL,KAAK;AACH,cAAQ,YAAY,KAAK,YAAY,SAAS;AAAA,IAChD,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAeO,SAAS,cAAc,OAA2B;AACvD,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AACJ,QAAM,SAAS,iBAAiB,UAAU;AAC1C,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,YAAY,CAAC,CAAC;AAElD,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,OAAO,YAAY,IAAI;AAC9B,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO,YAAY,IAAI;AAC9B,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO;AACd,aAAO,oBAAC,cAAW;AACnB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO;AACd,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO,cAAc,IAAI;AAChC,aAAO,oBAAC,aAAU;AAClB,kBAAY;AACZ;AAAA,IACF,KAAK;AACH,aAAO,OAAO;AACd,aAAO,oBAAC,WAAQ,OAAO,MAAM;AAC7B,kBAAY,OAAO;AACnB;AAAA,EACJ;AAEA,QAAM,QAAQ,eAAe;AAC7B,QAAM,SACJ,UAAU,UAAc,eAAe,UAAa,aAAa,SAAa,SAAY,eAAe,SACvG,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAAY,MAAK,MAChD,iBACH,IAEA,oBAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,MAAK,MAAK,SAAS,UAAU,SAAS,eAC7E,iBACH;AAGJ,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAM,SAAS,SAAS,eAAe,WAAW;AAAA,MAClD;AAAA,MACA,MAAM,SAAS,YAAY,UAAU,OAAO;AAAA,MAC5C,MAAM,QAAQ,YAAY,MAAM,IAAI;AAAA,MACpC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,uBAAqB;AAAA,MAEpB;AAAA;AAAA,EACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/billing/billing-banner.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { CreditCard, Hourglass, PencilOff } from \"lucide-react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport type { AlertSize, AlertTone } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { Spinner } from \"../components/spinner\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { BillingPortalAccess } from \"./billing-overview\";\n\n/**\n * The billing banners (docs/billing-harmonization.md §3.3, §7, §12.6, §12.21):\n *\n * - `trial-ending` — from 7 days before `trial_ends_at` (§3.3);\n * - `grant-ending` — from 7 days before `comped_until`: a beta user's 12 months, an\n * operator's grant;\n * - `payment-failed` — from the first failed payment (`past_due`), while the provider\n * retries; since 0.42, for an admin who isn't the buyer (`portal=\"buyer_only\"`), it\n * names who can update the payment method, with no button;\n * - `plan-ended` — out of good standing: read-only, never locked (§2.3);\n * - `guest` — a guest in someone else's read-only item (§12.6);\n * - `processing` — back from checkout before the webhook landed (§12.21).\n */\nexport type BillingBannerKind =\n | \"trial-ending\"\n | \"grant-ending\"\n | \"payment-failed\"\n | \"plan-ended\"\n | \"guest\"\n | \"processing\";\n\n/** A `div`'s props, `ref` included since 0.35 (round 0.35 R4, the follow-up to API-2): they\n * go on to the {@link AlertBanner}, so the ref is the banner's `<div>` — with an × too,\n * since the banner has no `href` or `onClick` that would make its row a link or a button. */\ninterface BillingBannerBaseProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\" | \"onClick\"> {\n /**\n * Over the kind's tone: `trial-ending` and `grant-ending` are `info` until the last\n * three days and `warning` in them; `payment-failed` is `danger` (money did not\n * arrive — kastlan's past-due strip was red); `plan-ended` is `warning`, a state with\n * everything still there rather than an error; `guest` and `processing` are `info`.\n */\n tone?: AlertTone;\n /** `strip` (default): the band under the top bar, on every page. `box`: the same\n * message on the subscription page itself. */\n variant?: \"strip\" | \"box\";\n /** Default `sm` for a strip (the demo's banner), `md` for a box. */\n size?: AlertSize;\n /**\n * Where the action goes — the subscription page, say. The app's action, as an `href`\n * or as {@link onAction}; without either there is no button. Never a write-locked\n * commit: billing itself always works (§3.3).\n */\n actionHref?: string;\n /** What the action does — the app's portal call (`POST /billing/portal`, then off to\n * its `url`): for `payment-failed` with `{target: \"payment_method\"}`, the portal's\n * page for the card (§14.5); for `processing`, `useCheckoutProcessing`'s\n * `checkAgain`. The kit sends no request. */\n onAction?: () => void;\n /**\n * Over the kind's action words: \"Choose a plan\" (trial, grant, plan ended), \"Update\n * payment method\" (payment failed), \"Check again\" (processing). The guest banner has\n * none of its own: an action there needs this.\n */\n actionLabel?: string;\n /** The action is under way: the button's spinner, no second press. */\n actionPending?: boolean;\n /** An × that calls this; the app removes the banner and remembers that it did. */\n onDismiss?: () => void;\n dismissLabel?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\nexport type BillingBannerProps = BillingBannerBaseProps &\n (\n | {\n kind: \"trial-ending\" | \"grant-ending\";\n /** Calendar days left — `billingDaysLeft(trial_ends_at)`. 0 is today. */\n daysLeft: number;\n item?: never;\n portal?: never;\n buyerName?: never;\n }\n | {\n kind: \"guest\";\n /** The item's own name (\"Household 2026\"), or nothing. NEVER the owner's status:\n * the banner says \"for now\" and nothing more (§12.6). */\n item?: string;\n daysLeft?: never;\n portal?: never;\n buyerName?: never;\n }\n | {\n kind: \"payment-failed\";\n /**\n * 0.42 (§16.7 A, §16.8): `GET /billing/status`'s `portal` (`BillingStanding`).\n * `\"buyer_only\"` — the viewer isn't the buyer, so the portal's page for the card\n * would answer 403: the banner says who can update the payment method\n * (`paymentFailedAskBuyer`) and shows NO button, whatever `actionHref` or\n * `onAction` say. `\"open\"` (default): as before.\n */\n portal?: BillingPortalAccess;\n /** 0.42: the status's `buyer_name`, under `\"buyer_only\"`. Empty: \"…the admin who\n * bought the subscription\". */\n buyerName?: string | null;\n daysLeft?: never;\n item?: never;\n }\n | {\n kind: \"plan-ended\" | \"processing\";\n daysLeft?: never;\n item?: never;\n portal?: never;\n buyerName?: never;\n }\n );\n\n/** The last days, in which an ending trial or grant turns from news into a warning. */\nconst LAST_DAYS = 3;\n\nfunction defaultTone(kind: BillingBannerKind, daysLeft: number | undefined): AlertTone {\n switch (kind) {\n case \"trial-ending\":\n case \"grant-ending\":\n return (daysLeft ?? 0) > LAST_DAYS ? \"info\" : \"warning\";\n case \"payment-failed\":\n return \"danger\";\n case \"plan-ended\":\n return \"warning\";\n default:\n return \"info\";\n }\n}\n\n/**\n * One of the billing banners, as a kit `AlertBanner` strip (§7): the kind's sentence, its\n * glyph and tone, and the app's action. Carries `data-billing-banner` with the kind.\n *\n * The app decides WHICH banner shows, from `GET /billing/overview` (and hides all of them\n * while billing is off, §4); the kit only says it. A guest's banner takes no status at\n * all — the type does not let one in — so the owner's payment state cannot leak into it\n * by a wrong prop (§12.6).\n *\n * `processing` is a live region (`role=\"status\"`): it appears when the person comes back\n * from checkout and goes when the overview catches up (§12.21), and a screen reader\n * should hear both. The others are page furniture, role-less as the strip is.\n */\nexport function BillingBanner(props: BillingBannerProps) {\n const {\n kind,\n daysLeft,\n item,\n portal,\n buyerName,\n tone,\n variant = \"strip\",\n size,\n actionHref,\n onAction,\n actionLabel,\n actionPending,\n onDismiss,\n dismissLabel,\n labels: labelsProp,\n role,\n ...rest\n } = props;\n const labels = useBillingLabels(labelsProp);\n const days = Math.max(0, Math.floor(daysLeft ?? 0));\n\n let text: string;\n let icon: ReactNode;\n let ownAction: string | undefined;\n // 0.42 (§16.7 A): only the buyer can reach the portal's card page.\n const askBuyer = kind === \"payment-failed\" && portal === \"buyer_only\";\n switch (kind) {\n case \"trial-ending\":\n text = labels.trialEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"grant-ending\":\n text = labels.grantEnding(days);\n icon = <Hourglass />;\n ownAction = labels.choosePlan;\n break;\n case \"payment-failed\":\n text = askBuyer ? labels.paymentFailedAskBuyer(buyerName || undefined) : labels.paymentFailed;\n icon = <CreditCard />;\n ownAction = labels.updatePayment;\n break;\n case \"plan-ended\":\n text = labels.planEnded;\n icon = <PencilOff />;\n ownAction = labels.choosePlan;\n break;\n case \"guest\":\n text = labels.guestReadOnly(item);\n icon = <PencilOff />;\n ownAction = undefined;\n break;\n case \"processing\":\n text = labels.processing;\n icon = <Spinner label={null} />;\n ownAction = labels.checkAgain;\n break;\n }\n\n const words = actionLabel ?? ownAction;\n const action =\n askBuyer || words === undefined || (actionHref === undefined && onAction === undefined) ? undefined : actionHref !== undefined ? (\n <Button href={actionHref} variant=\"secondary\" size=\"sm\">\n {words}\n </Button>\n ) : (\n <Button type=\"button\" variant=\"secondary\" size=\"sm\" onClick={onAction} pending={actionPending}>\n {words}\n </Button>\n );\n\n return (\n <AlertBanner\n {...rest}\n role={role ?? (kind === \"processing\" ? \"status\" : undefined)}\n variant={variant}\n size={size ?? (variant === \"strip\" ? \"sm\" : \"md\")}\n tone={tone ?? defaultTone(kind, days)}\n icon={icon}\n action={action}\n onDismiss={onDismiss}\n dismissLabel={dismissLabel}\n data-billing-banner={kind}\n >\n {text}\n </AlertBanner>\n );\n}\n"],"mappings":";AAoLa;AAnLb,SAAS,YAAY,WAAW,iBAAiB;AAGjD,SAAS,mBAAmB;AAE5B,SAAS,cAAc;AACvB,SAAS,eAAe;AACxB,SAAS,wBAAwB;AAgHjC,MAAM,YAAY;AAElB,SAAS,YAAY,MAAyB,UAAyC;AACrF,UAAQ,MAAM;AAAA,IACZ,KAAK;AAAA,IACL,KAAK;AACH,cAAQ,YAAY,KAAK,YAAY,SAAS;AAAA,IAChD,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO;AAAA,EACX;AACF;AAeO,SAAS,cAAc,OAA2B;AACvD,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,UAAU;AAAA,IACV;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA,GAAG;AAAA,EACL,IAAI;AACJ,QAAM,SAAS,iBAAiB,UAAU;AAC1C,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,YAAY,CAAC,CAAC;AAElD,MAAI;AACJ,MAAI;AACJ,MAAI;AAEJ,QAAM,WAAW,SAAS,oBAAoB,WAAW;AACzD,UAAQ,MAAM;AAAA,IACZ,KAAK;AACH,aAAO,OAAO,YAAY,IAAI;AAC9B,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO,YAAY,IAAI;AAC9B,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,WAAW,OAAO,sBAAsB,aAAa,MAAS,IAAI,OAAO;AAChF,aAAO,oBAAC,cAAW;AACnB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO;AACd,aAAO,oBAAC,aAAU;AAClB,kBAAY,OAAO;AACnB;AAAA,IACF,KAAK;AACH,aAAO,OAAO,cAAc,IAAI;AAChC,aAAO,oBAAC,aAAU;AAClB,kBAAY;AACZ;AAAA,IACF,KAAK;AACH,aAAO,OAAO;AACd,aAAO,oBAAC,WAAQ,OAAO,MAAM;AAC7B,kBAAY,OAAO;AACnB;AAAA,EACJ;AAEA,QAAM,QAAQ,eAAe;AAC7B,QAAM,SACJ,YAAY,UAAU,UAAc,eAAe,UAAa,aAAa,SAAa,SAAY,eAAe,SACnH,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAAY,MAAK,MAChD,iBACH,IAEA,oBAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,MAAK,MAAK,SAAS,UAAU,SAAS,eAC7E,iBACH;AAGJ,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ,MAAM,SAAS,SAAS,eAAe,WAAW;AAAA,MAClD;AAAA,MACA,MAAM,SAAS,YAAY,UAAU,OAAO;AAAA,MAC5C,MAAM,QAAQ,YAAY,MAAM,IAAI;AAAA,MACpC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,uBAAqB;AAAA,MAEpB;AAAA;AAAA,EACH;AAEJ;","names":[]}
@@ -91,6 +91,13 @@ export interface BillingLabels {
91
91
  choosePlan: string;
92
92
  /** The payment-failed banner's action, to the provider's portal: "Update payment method". */
93
93
  updatePayment: string;
94
+ /**
95
+ * 0.42 (§16.7 A, §16.8): the payment-failed banner for an admin who isn't the buyer
96
+ * (`portal: "buyer_only"`), with no button — only the buyer can change the card.
97
+ * `buyer` is their display name, or `undefined` when they are gone ("the admin who
98
+ * bought the subscription").
99
+ */
100
+ paymentFailedAskBuyer: (buyer?: string) => string;
94
101
  /** The processing banner's action, when the app gives one: "Check again". */
95
102
  checkAgain: string;
96
103
  /** The `WriteLockProvider` reason for the payer (§3.3, verbatim). A guest's reason is
@@ -129,6 +136,24 @@ export interface BillingLabels {
129
136
  * the words for `409 billing_not_at_provider`, a stale page that asked anyway.
130
137
  */
131
138
  notAtProvider: string;
139
+ /**
140
+ * In place of "Payment and invoices" for an admin who isn't the buyer (`portal:
141
+ * "buyer_only"`, §16.2 decision 3): the provider's portal opens only for the buyer.
142
+ * `buyer` is their display name — or `undefined` when they are gone or have left the
143
+ * company, and the words then say "the admin who bought the subscription".
144
+ */
145
+ buyerHolds: (buyer?: string) => string;
146
+ /** `CancelSubscriptionConfirm`'s title: "Cancel subscription". */
147
+ cancelConfirmTitle: string;
148
+ /** Its body; `date` is the period's end, formatted: "Your subscription ends on
149
+ * 9 Nov 2026. Until then everything works as before." */
150
+ cancelConfirmBody: (date: string) => string;
151
+ /** Its confirm button (§16.4): "Cancel subscription now". German: "Jetzt kündigen",
152
+ * the confirmation button's words in §312k BGB. */
153
+ cancelConfirmAction: string;
154
+ /** In place of "Cancel subscription" while a cancel is scheduled (§16.7 C, `POST
155
+ * /billing/resume`): "Keep subscription". */
156
+ keepSubscription: string;
132
157
  /** While the checkout opens. */
133
158
  payOpening: string;
134
159
  /** A link without a transaction, or billing switched off: nothing to pay. */
@@ -154,6 +179,13 @@ export interface BillingLabels {
154
179
  switchNextCharge: (date: string) => string;
155
180
  /** The confirm button: "Change plan". */
156
181
  switchConfirm: string;
182
+ /** The warning's title, before a checkout (`PlanPicker`) or a switch
183
+ * (`PlanSwitchConfirm`) under a grant without an end: "Your free access continues". */
184
+ grantWarningTitle: string;
185
+ /** What the purchase does under it: nothing to the plan while the grant runs. */
186
+ grantWarningBody: string;
187
+ /** `PlanPicker`'s button that goes on to the checkout: "Buy anyway". */
188
+ grantWarningAction: string;
157
189
  /** Changes refused as `billing_read_only` inside a sync reply, kept queued and sent
158
190
  * after payment: "3 changes waiting for a plan". */
159
191
  waitingChanges: (count: number) => string;
@@ -50,6 +50,7 @@ const DEFAULT_BILLING_LABELS = {
50
50
  processing: "Your payment is being processed. Your plan starts as soon as it\u2019s confirmed; this page updates by itself.",
51
51
  choosePlan: "Choose a plan",
52
52
  updatePayment: "Update payment method",
53
+ paymentFailedAskBuyer: (buyer) => buyer ? `The payment failed. ${buyer} can update the payment method.` : "The payment failed. The admin who bought the subscription can update the payment method.",
53
54
  checkAgain: "Check again",
54
55
  lockReason: "Your plan has ended. Choose a plan to make changes again.",
55
56
  limitReached: "Plan limit reached",
@@ -62,6 +63,11 @@ const DEFAULT_BILLING_LABELS = {
62
63
  manage: "Payment and invoices",
63
64
  cancel: "Cancel subscription",
64
65
  notAtProvider: "Nothing to manage yet. Payments and invoices appear here once you have a paid plan.",
66
+ buyerHolds: (buyer) => buyer ? `The payment method and the invoices are with ${buyer}, who bought the subscription.` : "The payment method and the invoices are with the admin who bought the subscription.",
67
+ cancelConfirmTitle: "Cancel subscription",
68
+ cancelConfirmBody: (date) => `Your subscription ends on ${date}. Until then everything works as before.`,
69
+ cancelConfirmAction: "Cancel subscription now",
70
+ keepSubscription: "Keep subscription",
65
71
  payOpening: "Opening the secure checkout\u2026",
66
72
  payNothing: "There\u2019s no payment to make here. To choose a plan, go back to the app.",
67
73
  payFailed: "The checkout couldn\u2019t be opened. Please go back and try again in a moment.",
@@ -73,6 +79,9 @@ const DEFAULT_BILLING_LABELS = {
73
79
  switchCredited: "Credited towards your next charges",
74
80
  switchNextCharge: (date) => `Next charge on ${date}`,
75
81
  switchConfirm: "Change plan",
82
+ grantWarningTitle: "Your free access continues",
83
+ grantWarningBody: "You have free access with no end date. A purchase won\u2019t change your plan while the free access runs: you would pay for a plan that doesn\u2019t apply.",
84
+ grantWarningAction: "Buy anyway",
76
85
  waitingChanges: (count) => count === 1 ? "1 change waiting for a plan" : `${count} changes waiting for a plan`,
77
86
  discardWaiting: "Discard waiting changes",
78
87
  notConfigured: "Payments aren't set up yet. Please try again later.",
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/billing/billing-labels.ts"],"sourcesContent":["import { useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\n/**\n * The subscription's status vocabulary (docs/billing-harmonization.md §3.2) — server-kit's\n * `SubscriptionStatus`, value for value:\n *\n * - `trialing` — the 30-day trial without a card (§2.7), or a pure guest's row whose trial\n * has not started (§12.9, `trial_ends_at` empty);\n * - `active` — paid;\n * - `past_due` — a payment failed; in good standing during the provider's retries (§3.3);\n * - `canceled` — the payer cancelled; the American spelling is the wire's, the word shown\n * is \"Cancelled\";\n * - `expired` — a trial, a grant or a failed subscription ran out: read-only (§2.3);\n * - `comped` — free by the operator's grant or as a beta user (§3.2, `comped_until`).\n */\nexport type SubscriptionStatus = \"trialing\" | \"active\" | \"past_due\" | \"canceled\" | \"expired\" | \"comped\";\n\n/** Every status, in the order a lifecycle reads: trial, paid, trouble, ended, granted. */\nexport const SUBSCRIPTION_STATUSES: readonly SubscriptionStatus[] = Object.freeze([\n \"trialing\",\n \"active\",\n \"past_due\",\n \"canceled\",\n \"expired\",\n \"comped\",\n]);\n\n/**\n * The words of the subscription parts — the `billing` namespace of `<UiKitProvider labels>`\n * (docs/billing-harmonization.md §7, §10; ui-kit 0.32.0).\n *\n * WORDING (§12.24): the English group is \"Subscription\", never \"Plan\" — keksdose's \"plan\"\n * is the month's budget plan (\"One plan per month\"), and two meanings of one word on one\n * settings page is the clash the review found. \"Plan\" stays the word for WHAT is bought\n * (Free, Pro), \"subscription\" for the standing arrangement. German says \"Tarif\" for the\n * plan and \"Abonnement\" for the subscription (§10).\n *\n * Prices are GROSS (§12.17): Swiss consumer law (PBV) wants a consumer price with the\n * MWST in it, so the cards say so (`vatIncluded`).\n */\nexport interface BillingLabels {\n /** The settings group or admin section the parts live on (§7, §12.24): \"Subscription\". */\n group: string;\n\n /* ── PlanPicker / PlanCard (§7) ── */\n /** The picker's name when it has no visible `legend`: \"Plans\". */\n plans: string;\n /** The name of the billing-period switch: \"Billing period\". */\n interval: string;\n /** The switch's two options — yearly first (§12.17). */\n yearly: string;\n monthly: string;\n /** The name of the currency switch: \"Currency\". */\n currency: string;\n /** After a price: \"per year\", \"per month\". */\n perYear: string;\n perMonth: string;\n /** Under a price (§12.17): \"VAT included\". German: \"inkl. MWST\". */\n vatIncluded: string;\n /** A plan with no price, or a price of 0: \"Free\". */\n free: string;\n /** A plan with prices, but none for this currency and period. */\n notOffered: string;\n /** A limit of `null` (§3.1): \"Unlimited\". */\n unlimited: string;\n /** The accessible names of a card's two lists. */\n limits: string;\n features: string;\n /** The chip on the payer's current plan. */\n current: string;\n /** The picker's button for `plan` (the plan's display name) with no current plan —\n * during a trial, or after a plan ended: \"Choose Pro\". */\n choose: (plan: string) => string;\n /** …for a plan after the current one in the catalogue's order: \"Upgrade to Pro\". */\n upgrade: (plan: string) => string;\n /** …for a plan before it: \"Switch to Free\". */\n downgrade: (plan: string) => string;\n /** Why the button is off while the current plan is the one picked. */\n isCurrent: string;\n /** Why it is off while nothing is picked. */\n pickFirst: string;\n\n /* ── SubscriptionStatusChip (§6) ── */\n /** One word per status. */\n status: Record<SubscriptionStatus, string>;\n\n /* ── BillingBanner (§3.3, §12.6, §12.21) ── */\n /** A trial ends in `days` calendar days — 0 is today, 1 tomorrow. */\n trialEnding: (days: number) => string;\n /** A free grant (`comped_until`: a beta user's 12 months, an operator's grant) ends in\n * `days` calendar days. */\n grantEnding: (days: number) => string;\n /** The first failed payment (`past_due`), while the provider retries. */\n paymentFailed: string;\n /** Out of good standing: read-only, never locked (§2.3). */\n planEnded: string;\n /**\n * A guest in someone else's item that is read-only for billing (§12.6). It never says\n * WHY beyond \"for now\": the owner's payment status is the owner's personal data.\n * `item` is the item's own name (\"Household 2026\"), shown in quotes — or `undefined`.\n */\n guestReadOnly: (item?: string) => string;\n /** Back from checkout before the provider's webhook landed (§12.21). */\n processing: string;\n /** The action of the trial, grant and plan-ended banners, and of `PlanLimitNotice`'s\n * `upgrade` mode: \"Choose a plan\". */\n choosePlan: string;\n /** The payment-failed banner's action, to the provider's portal: \"Update payment method\". */\n updatePayment: string;\n /** The processing banner's action, when the app gives one: \"Check again\". */\n checkAgain: string;\n\n /* ── The read-only lock (§3.3, §12.6) ── */\n /** The `WriteLockProvider` reason for the payer (§3.3, verbatim). A guest's reason is\n * {@link guestReadOnly}. */\n lockReason: string;\n\n /* ── PlanLimitNotice (§3.4) ── */\n /** The notice's first line: \"Plan limit reached\". */\n limitReached: string;\n /** Mode `upgrade`: what the action does. */\n limitUpgrade: string;\n /** Mode `contact` (the operator grants plans by hand, keksdose's today). */\n limitContact: string;\n /** Mode `contact`'s action, a mail to support: \"Ask for more\". */\n askForMore: string;\n /** The meter's value: \"3 of 3\". Both figures are formatted already. */\n usage: (used: string, limit: string) => string;\n /** The subject of the `contact` mail; `dimension` is the app's name for it (\"Budgets\"). */\n contactSubject: (dimension: string) => string;\n /**\n * The figure line of the limit's toast (`usePlanLimitToast`, §14.11) and of an\n * operator's plan change over the new limit (§14.12): \"Budgets: 3 of 3\". A key of its\n * own rather than `${dimension}: ${usage}`, because French puts a space before the\n * colon. Both figures are formatted already.\n */\n limitUsageLine: (dimension: string, used: string, limit: string) => string;\n\n /* ── The provider's portal (§7, §12.26) ── */\n /** The portal: payment method and invoices (§2.8, the provider hosts both). */\n manage: string;\n /**\n * The visible cancel link on the app's subscription page (§12.26). German: §312k BGB\n * wants the button that leads to the cancellation labelled \"Verträge hier kündigen\" or\n * as unambiguously — a translator should use exactly that.\n */\n cancel: string;\n /**\n * In place of both while the payer hasn't reached the provider (the overview's\n * `at_provider` is false, §14.5): there is no customer to open a portal for yet. Also\n * the words for `409 billing_not_at_provider`, a stale page that asked anyway.\n */\n notAtProvider: string;\n\n /* ── The pay page (§14.4) ── */\n /* Built into the static page `dist/pay/` at the kit's build: it has no React and no\n provider, so an app's override never reaches it. */\n /** While the checkout opens. */\n payOpening: string;\n /** A link without a transaction, or billing switched off: nothing to pay. */\n payNothing: string;\n /** Paddle's script didn't load, or the page's configuration is wrong. */\n payFailed: string;\n /** The buyer closed the checkout without paying. */\n payBack: string;\n /** 0.39 (§15.2.5): paid, for a transaction that names no app the page knows (a\n * payment-method update from Paddle's e-mail) — no way back to offer. */\n payDone: string;\n\n /* ── PlanSwitchConfirm (0.39, §15.2.10) ── */\n /** The dialog's title: \"Change plan\". */\n switchTitle: string;\n /** The line naming both plans, each with its billing period: \"From Pro (Monthly) to\n * Business (Yearly)\". */\n switchFromTo: (from: string, to: string) => string;\n /** What an upgrade charges at once (Paddle's preview, prorated): \"Charged now\". */\n switchChargedNow: string;\n /** What a downgrade credits — the customer's balance, which Paddle takes off the next\n * charges: \"Credited towards your next charges\". */\n switchCredited: string;\n /** The next regular charge; `date` is formatted: \"Next charge on 9 Nov 2026\". */\n switchNextCharge: (date: string) => string;\n /** The confirm button: \"Change plan\". */\n switchConfirm: string;\n\n /* ── Offline (§12.5) ── */\n /** Changes refused as `billing_read_only` inside a sync reply, kept queued and sent\n * after payment: \"3 changes waiting for a plan\". */\n waitingChanges: (count: number) => string;\n /** The sign-out guard's way out for someone who will not pay. */\n discardWaiting: string;\n\n /* ── Refusals with no part of their own (0.32.1, kastlan's 0.32 report) ── */\n /**\n * `billing_not_configured`: billing is on but the provider's settings are missing — the\n * operator's mistake, so the words ask the payer to wait, not to act. For the toast or\n * alert after \"Choose a plan\" or \"Payment and invoices\" (`isBillingError(err,\n * \"billing_not_configured\")`). `billing_read_only` and `plan_limit` need none: the\n * write lock, the banner and `PlanLimitNotice` say them.\n */\n notConfigured: string;\n /** `billing_disabled`: billing is switched off on this server (§2.9), for a page or a\n * link that reached it anyway. */\n disabled: string;\n /* 0.33.0 — server-kit 0.7's refusals (§14.2). `billing_not_at_provider` is\n {@link notAtProvider}'s: a stale page that asked for the portal anyway. */\n /** `billing_provider_unavailable` (502): the provider didn't answer. Try again. */\n providerUnavailable: string;\n /** `billing_already_subscribed` (409): a second checkout while a subscription runs\n * (§14.6). Since 0.39 plan changes go through the app's own switch (§15.2.10), not the\n * portal, and the words say so. */\n alreadySubscribed: string;\n /** `billing_plan_not_sold` (422): the catalogue doesn't sell this plan with that\n * period or currency — at a checkout, or (0.39) a switch to a plan no longer sold. */\n planNotSold: string;\n /** 0.39 (§15.2.10): `billing_not_subscribed` (409), a plan switch with no running\n * provider subscription — the page offers the checkout instead. */\n notSubscribed: string;\n /** 0.39: `billing_plan_unchanged` (409), a switch to the price the payer pays now. */\n planUnchanged: string;\n}\n\n/** \"today\", \"tomorrow\", \"in 5 days\". */\nfunction inDays(days: number): string {\n if (days <= 0) return \"today\";\n if (days === 1) return \"tomorrow\";\n return `in ${days} days`;\n}\n\nexport const DEFAULT_BILLING_LABELS: BillingLabels = {\n group: \"Subscription\",\n\n plans: \"Plans\",\n interval: \"Billing period\",\n yearly: \"Yearly\",\n monthly: \"Monthly\",\n currency: \"Currency\",\n perYear: \"per year\",\n perMonth: \"per month\",\n vatIncluded: \"VAT included\",\n free: \"Free\",\n notOffered: \"Not offered for this billing period\",\n unlimited: \"Unlimited\",\n limits: \"Limits\",\n features: \"Included\",\n current: \"Current plan\",\n choose: (plan) => `Choose ${plan}`,\n upgrade: (plan) => `Upgrade to ${plan}`,\n downgrade: (plan) => `Switch to ${plan}`,\n isCurrent: \"This is your current plan.\",\n pickFirst: \"Pick a plan first.\",\n\n status: {\n trialing: \"Trial\",\n active: \"Active\",\n past_due: \"Payment overdue\",\n canceled: \"Cancelled\",\n expired: \"Expired\",\n comped: \"Complimentary\",\n },\n\n trialEnding: (days) =>\n `Your trial ends ${inDays(days)}. After that you can still view everything, but changes need a plan.`,\n grantEnding: (days) =>\n `Your free access ends ${inDays(days)}. After that you can still view everything, but changes need a plan.`,\n paymentFailed: \"Your last payment didn’t go through. Update your payment method to keep your plan.\",\n planEnded: \"Your plan has ended. You can still view and export everything; choose a plan to make changes again.\",\n guestReadOnly: (item) =>\n item ? `“${item}” is read-only for now; its owner can lift that.` : \"This is read-only for now; its owner can lift that.\",\n processing: \"Your payment is being processed. Your plan starts as soon as it’s confirmed; this page updates by itself.\",\n choosePlan: \"Choose a plan\",\n updatePayment: \"Update payment method\",\n checkAgain: \"Check again\",\n\n lockReason: \"Your plan has ended. Choose a plan to make changes again.\",\n\n limitReached: \"Plan limit reached\",\n limitUpgrade: \"To add more, choose a plan with a higher limit.\",\n limitContact: \"To add more, ask us for a higher limit.\",\n askForMore: \"Ask for more\",\n usage: (used, limit) => `${used} of ${limit}`,\n contactSubject: (dimension) => `Plan limit: ${dimension}`,\n limitUsageLine: (dimension, used, limit) => `${dimension}: ${used} of ${limit}`,\n\n manage: \"Payment and invoices\",\n cancel: \"Cancel subscription\",\n notAtProvider: \"Nothing to manage yet. Payments and invoices appear here once you have a paid plan.\",\n\n payOpening: \"Opening the secure checkout…\",\n payNothing: \"There’s no payment to make here. To choose a plan, go back to the app.\",\n payFailed: \"The checkout couldn’t be opened. Please go back and try again in a moment.\",\n payBack: \"The checkout was closed. You can go back, or reload this page to open it again.\",\n payDone: \"Payment complete. You can close this page.\",\n\n switchTitle: \"Change plan\",\n switchFromTo: (from, to) => `From ${from} to ${to}`,\n switchChargedNow: \"Charged now\",\n switchCredited: \"Credited towards your next charges\",\n switchNextCharge: (date) => `Next charge on ${date}`,\n switchConfirm: \"Change plan\",\n\n waitingChanges: (count) =>\n count === 1 ? \"1 change waiting for a plan\" : `${count} changes waiting for a plan`,\n discardWaiting: \"Discard waiting changes\",\n\n notConfigured: \"Payments aren't set up yet. Please try again later.\",\n disabled: \"Subscriptions aren't available here.\",\n providerUnavailable: \"The payment provider can’t be reached right now. Please try again in a moment.\",\n alreadySubscribed: \"You already have a subscription. Change your plan above.\",\n planNotSold: \"This plan isn’t offered for that billing period or currency. Please choose another.\",\n notSubscribed: \"There’s no running subscription to change. Choose a plan to subscribe.\",\n planUnchanged: \"That’s already your plan. Pick another plan or billing period.\",\n};\n\n/** The `billing` namespace, resolved: English, then the provider, then `labels` — which\n * may be partial at every depth (`{ status: { comped: \"Beta\" } }`). */\nexport function useBillingLabels(labels?: LabelOverride<BillingLabels>): BillingLabels {\n return useKitLabels(\"billing\", DEFAULT_BILLING_LABELS, labels);\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;AAmBtB,MAAM,wBAAuD,OAAO,OAAO;AAAA,EAChF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAsMD,SAAS,OAAO,MAAsB;AACpC,MAAI,QAAQ,EAAG,QAAO;AACtB,MAAI,SAAS,EAAG,QAAO;AACvB,SAAO,MAAM,IAAI;AACnB;AAEO,MAAM,yBAAwC;AAAA,EACnD,OAAO;AAAA,EAEP,OAAO;AAAA,EACP,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AAAA,EACV,SAAS;AAAA,EACT,UAAU;AAAA,EACV,aAAa;AAAA,EACb,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA,EACT,QAAQ,CAAC,SAAS,UAAU,IAAI;AAAA,EAChC,SAAS,CAAC,SAAS,cAAc,IAAI;AAAA,EACrC,WAAW,CAAC,SAAS,aAAa,IAAI;AAAA,EACtC,WAAW;AAAA,EACX,WAAW;AAAA,EAEX,QAAQ;AAAA,IACN,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,UAAU;AAAA,IACV,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AAAA,EAEA,aAAa,CAAC,SACZ,mBAAmB,OAAO,IAAI,CAAC;AAAA,EACjC,aAAa,CAAC,SACZ,yBAAyB,OAAO,IAAI,CAAC;AAAA,EACvC,eAAe;AAAA,EACf,WAAW;AAAA,EACX,eAAe,CAAC,SACd,OAAO,SAAI,IAAI,0DAAqD;AAAA,EACtE,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,YAAY;AAAA,EAEZ,YAAY;AAAA,EAEZ,cAAc;AAAA,EACd,cAAc;AAAA,EACd,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,OAAO,CAAC,MAAM,UAAU,GAAG,IAAI,OAAO,KAAK;AAAA,EAC3C,gBAAgB,CAAC,cAAc,eAAe,SAAS;AAAA,EACvD,gBAAgB,CAAC,WAAW,MAAM,UAAU,GAAG,SAAS,KAAK,IAAI,OAAO,KAAK;AAAA,EAE7E,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,eAAe;AAAA,EAEf,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,SAAS;AAAA,EACT,SAAS;AAAA,EAET,aAAa;AAAA,EACb,cAAc,CAAC,MAAM,OAAO,QAAQ,IAAI,OAAO,EAAE;AAAA,EACjD,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,kBAAkB,CAAC,SAAS,kBAAkB,IAAI;AAAA,EAClD,eAAe;AAAA,EAEf,gBAAgB,CAAC,UACf,UAAU,IAAI,gCAAgC,GAAG,KAAK;AAAA,EACxD,gBAAgB;AAAA,EAEhB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,qBAAqB;AAAA,EACrB,mBAAmB;AAAA,EACnB,aAAa;AAAA,EACb,eAAe;AAAA,EACf,eAAe;AACjB;AAIO,SAAS,iBAAiB,QAAsD;AACrF,SAAO,aAAa,WAAW,wBAAwB,MAAM;AAC/D;","names":[]}
1
+ {"version":3,"sources":["../../src/billing/billing-labels.ts"],"sourcesContent":["import { useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\n\n/**\n * The subscription's status vocabulary (docs/billing-harmonization.md §3.2) — server-kit's\n * `SubscriptionStatus`, value for value:\n *\n * - `trialing` — the 30-day trial without a card (§2.7), or a pure guest's row whose trial\n * has not started (§12.9, `trial_ends_at` empty);\n * - `active` — paid;\n * - `past_due` — a payment failed; in good standing during the provider's retries (§3.3);\n * - `canceled` — the payer cancelled; the American spelling is the wire's, the word shown\n * is \"Cancelled\";\n * - `expired` — a trial, a grant or a failed subscription ran out: read-only (§2.3);\n * - `comped` — free by the operator's grant or as a beta user (§3.2, `comped_until`).\n */\nexport type SubscriptionStatus = \"trialing\" | \"active\" | \"past_due\" | \"canceled\" | \"expired\" | \"comped\";\n\n/** Every status, in the order a lifecycle reads: trial, paid, trouble, ended, granted. */\nexport const SUBSCRIPTION_STATUSES: readonly SubscriptionStatus[] = Object.freeze([\n \"trialing\",\n \"active\",\n \"past_due\",\n \"canceled\",\n \"expired\",\n \"comped\",\n]);\n\n/**\n * The words of the subscription parts — the `billing` namespace of `<UiKitProvider labels>`\n * (docs/billing-harmonization.md §7, §10; ui-kit 0.32.0).\n *\n * WORDING (§12.24): the English group is \"Subscription\", never \"Plan\" — keksdose's \"plan\"\n * is the month's budget plan (\"One plan per month\"), and two meanings of one word on one\n * settings page is the clash the review found. \"Plan\" stays the word for WHAT is bought\n * (Free, Pro), \"subscription\" for the standing arrangement. German says \"Tarif\" for the\n * plan and \"Abonnement\" for the subscription (§10).\n *\n * Prices are GROSS (§12.17): Swiss consumer law (PBV) wants a consumer price with the\n * MWST in it, so the cards say so (`vatIncluded`).\n */\nexport interface BillingLabels {\n /** The settings group or admin section the parts live on (§7, §12.24): \"Subscription\". */\n group: string;\n\n /* ── PlanPicker / PlanCard (§7) ── */\n /** The picker's name when it has no visible `legend`: \"Plans\". */\n plans: string;\n /** The name of the billing-period switch: \"Billing period\". */\n interval: string;\n /** The switch's two options — yearly first (§12.17). */\n yearly: string;\n monthly: string;\n /** The name of the currency switch: \"Currency\". */\n currency: string;\n /** After a price: \"per year\", \"per month\". */\n perYear: string;\n perMonth: string;\n /** Under a price (§12.17): \"VAT included\". German: \"inkl. MWST\". */\n vatIncluded: string;\n /** A plan with no price, or a price of 0: \"Free\". */\n free: string;\n /** A plan with prices, but none for this currency and period. */\n notOffered: string;\n /** A limit of `null` (§3.1): \"Unlimited\". */\n unlimited: string;\n /** The accessible names of a card's two lists. */\n limits: string;\n features: string;\n /** The chip on the payer's current plan. */\n current: string;\n /** The picker's button for `plan` (the plan's display name) with no current plan —\n * during a trial, or after a plan ended: \"Choose Pro\". */\n choose: (plan: string) => string;\n /** …for a plan after the current one in the catalogue's order: \"Upgrade to Pro\". */\n upgrade: (plan: string) => string;\n /** …for a plan before it: \"Switch to Free\". */\n downgrade: (plan: string) => string;\n /** Why the button is off while the current plan is the one picked. */\n isCurrent: string;\n /** Why it is off while nothing is picked. */\n pickFirst: string;\n\n /* ── SubscriptionStatusChip (§6) ── */\n /** One word per status. */\n status: Record<SubscriptionStatus, string>;\n\n /* ── BillingBanner (§3.3, §12.6, §12.21) ── */\n /** A trial ends in `days` calendar days — 0 is today, 1 tomorrow. */\n trialEnding: (days: number) => string;\n /** A free grant (`comped_until`: a beta user's 12 months, an operator's grant) ends in\n * `days` calendar days. */\n grantEnding: (days: number) => string;\n /** The first failed payment (`past_due`), while the provider retries. */\n paymentFailed: string;\n /** Out of good standing: read-only, never locked (§2.3). */\n planEnded: string;\n /**\n * A guest in someone else's item that is read-only for billing (§12.6). It never says\n * WHY beyond \"for now\": the owner's payment status is the owner's personal data.\n * `item` is the item's own name (\"Household 2026\"), shown in quotes — or `undefined`.\n */\n guestReadOnly: (item?: string) => string;\n /** Back from checkout before the provider's webhook landed (§12.21). */\n processing: string;\n /** The action of the trial, grant and plan-ended banners, and of `PlanLimitNotice`'s\n * `upgrade` mode: \"Choose a plan\". */\n choosePlan: string;\n /** The payment-failed banner's action, to the provider's portal: \"Update payment method\". */\n updatePayment: string;\n /**\n * 0.42 (§16.7 A, §16.8): the payment-failed banner for an admin who isn't the buyer\n * (`portal: \"buyer_only\"`), with no button — only the buyer can change the card.\n * `buyer` is their display name, or `undefined` when they are gone (\"the admin who\n * bought the subscription\").\n */\n paymentFailedAskBuyer: (buyer?: string) => string;\n /** The processing banner's action, when the app gives one: \"Check again\". */\n checkAgain: string;\n\n /* ── The read-only lock (§3.3, §12.6) ── */\n /** The `WriteLockProvider` reason for the payer (§3.3, verbatim). A guest's reason is\n * {@link guestReadOnly}. */\n lockReason: string;\n\n /* ── PlanLimitNotice (§3.4) ── */\n /** The notice's first line: \"Plan limit reached\". */\n limitReached: string;\n /** Mode `upgrade`: what the action does. */\n limitUpgrade: string;\n /** Mode `contact` (the operator grants plans by hand, keksdose's today). */\n limitContact: string;\n /** Mode `contact`'s action, a mail to support: \"Ask for more\". */\n askForMore: string;\n /** The meter's value: \"3 of 3\". Both figures are formatted already. */\n usage: (used: string, limit: string) => string;\n /** The subject of the `contact` mail; `dimension` is the app's name for it (\"Budgets\"). */\n contactSubject: (dimension: string) => string;\n /**\n * The figure line of the limit's toast (`usePlanLimitToast`, §14.11) and of an\n * operator's plan change over the new limit (§14.12): \"Budgets: 3 of 3\". A key of its\n * own rather than `${dimension}: ${usage}`, because French puts a space before the\n * colon. Both figures are formatted already.\n */\n limitUsageLine: (dimension: string, used: string, limit: string) => string;\n\n /* ── The provider's portal (§7, §12.26) ── */\n /** The portal: payment method and invoices (§2.8, the provider hosts both). */\n manage: string;\n /**\n * The visible cancel link on the app's subscription page (§12.26). German: §312k BGB\n * wants the button that leads to the cancellation labelled \"Verträge hier kündigen\" or\n * as unambiguously — a translator should use exactly that.\n */\n cancel: string;\n /**\n * In place of both while the payer hasn't reached the provider (the overview's\n * `at_provider` is false, §14.5): there is no customer to open a portal for yet. Also\n * the words for `409 billing_not_at_provider`, a stale page that asked anyway.\n */\n notAtProvider: string;\n\n /* ── The portal for the buyer only, the in-app cancel (0.42, §16) ── */\n /**\n * In place of \"Payment and invoices\" for an admin who isn't the buyer (`portal:\n * \"buyer_only\"`, §16.2 decision 3): the provider's portal opens only for the buyer.\n * `buyer` is their display name — or `undefined` when they are gone or have left the\n * company, and the words then say \"the admin who bought the subscription\".\n */\n buyerHolds: (buyer?: string) => string;\n /** `CancelSubscriptionConfirm`'s title: \"Cancel subscription\". */\n cancelConfirmTitle: string;\n /** Its body; `date` is the period's end, formatted: \"Your subscription ends on\n * 9 Nov 2026. Until then everything works as before.\" */\n cancelConfirmBody: (date: string) => string;\n /** Its confirm button (§16.4): \"Cancel subscription now\". German: \"Jetzt kündigen\",\n * the confirmation button's words in §312k BGB. */\n cancelConfirmAction: string;\n /** In place of \"Cancel subscription\" while a cancel is scheduled (§16.7 C, `POST\n * /billing/resume`): \"Keep subscription\". */\n keepSubscription: string;\n\n /* ── The pay page (§14.4) ── */\n /* Built into the static page `dist/pay/` at the kit's build: it has no React and no\n provider, so an app's override never reaches it. */\n /** While the checkout opens. */\n payOpening: string;\n /** A link without a transaction, or billing switched off: nothing to pay. */\n payNothing: string;\n /** Paddle's script didn't load, or the page's configuration is wrong. */\n payFailed: string;\n /** The buyer closed the checkout without paying. */\n payBack: string;\n /** 0.39 (§15.2.5): paid, for a transaction that names no app the page knows (a\n * payment-method update from Paddle's e-mail) — no way back to offer. */\n payDone: string;\n\n /* ── PlanSwitchConfirm (0.39, §15.2.10) ── */\n /** The dialog's title: \"Change plan\". */\n switchTitle: string;\n /** The line naming both plans, each with its billing period: \"From Pro (Monthly) to\n * Business (Yearly)\". */\n switchFromTo: (from: string, to: string) => string;\n /** What an upgrade charges at once (Paddle's preview, prorated): \"Charged now\". */\n switchChargedNow: string;\n /** What a downgrade credits — the customer's balance, which Paddle takes off the next\n * charges: \"Credited towards your next charges\". */\n switchCredited: string;\n /** The next regular charge; `date` is formatted: \"Next charge on 9 Nov 2026\". */\n switchNextCharge: (date: string) => string;\n /** The confirm button: \"Change plan\". */\n switchConfirm: string;\n\n /* ── The permanent-grant warning (0.42, §16.2 decision 5, §16.8 B) ── */\n /** The warning's title, before a checkout (`PlanPicker`) or a switch\n * (`PlanSwitchConfirm`) under a grant without an end: \"Your free access continues\". */\n grantWarningTitle: string;\n /** What the purchase does under it: nothing to the plan while the grant runs. */\n grantWarningBody: string;\n /** `PlanPicker`'s button that goes on to the checkout: \"Buy anyway\". */\n grantWarningAction: string;\n\n /* ── Offline (§12.5) ── */\n /** Changes refused as `billing_read_only` inside a sync reply, kept queued and sent\n * after payment: \"3 changes waiting for a plan\". */\n waitingChanges: (count: number) => string;\n /** The sign-out guard's way out for someone who will not pay. */\n discardWaiting: string;\n\n /* ── Refusals with no part of their own (0.32.1, kastlan's 0.32 report) ── */\n /**\n * `billing_not_configured`: billing is on but the provider's settings are missing — the\n * operator's mistake, so the words ask the payer to wait, not to act. For the toast or\n * alert after \"Choose a plan\" or \"Payment and invoices\" (`isBillingError(err,\n * \"billing_not_configured\")`). `billing_read_only` and `plan_limit` need none: the\n * write lock, the banner and `PlanLimitNotice` say them.\n */\n notConfigured: string;\n /** `billing_disabled`: billing is switched off on this server (§2.9), for a page or a\n * link that reached it anyway. */\n disabled: string;\n /* 0.33.0 — server-kit 0.7's refusals (§14.2). `billing_not_at_provider` is\n {@link notAtProvider}'s: a stale page that asked for the portal anyway. */\n /** `billing_provider_unavailable` (502): the provider didn't answer. Try again. */\n providerUnavailable: string;\n /** `billing_already_subscribed` (409): a second checkout while a subscription runs\n * (§14.6). Since 0.39 plan changes go through the app's own switch (§15.2.10), not the\n * portal, and the words say so. */\n alreadySubscribed: string;\n /** `billing_plan_not_sold` (422): the catalogue doesn't sell this plan with that\n * period or currency — at a checkout, or (0.39) a switch to a plan no longer sold. */\n planNotSold: string;\n /** 0.39 (§15.2.10): `billing_not_subscribed` (409), a plan switch with no running\n * provider subscription — the page offers the checkout instead. */\n notSubscribed: string;\n /** 0.39: `billing_plan_unchanged` (409), a switch to the price the payer pays now. */\n planUnchanged: string;\n}\n\n/** \"today\", \"tomorrow\", \"in 5 days\". */\nfunction inDays(days: number): string {\n if (days <= 0) return \"today\";\n if (days === 1) return \"tomorrow\";\n return `in ${days} days`;\n}\n\nexport const DEFAULT_BILLING_LABELS: BillingLabels = {\n group: \"Subscription\",\n\n plans: \"Plans\",\n interval: \"Billing period\",\n yearly: \"Yearly\",\n monthly: \"Monthly\",\n currency: \"Currency\",\n perYear: \"per year\",\n perMonth: \"per month\",\n vatIncluded: \"VAT included\",\n free: \"Free\",\n notOffered: \"Not offered for this billing period\",\n unlimited: \"Unlimited\",\n limits: \"Limits\",\n features: \"Included\",\n current: \"Current plan\",\n choose: (plan) => `Choose ${plan}`,\n upgrade: (plan) => `Upgrade to ${plan}`,\n downgrade: (plan) => `Switch to ${plan}`,\n isCurrent: \"This is your current plan.\",\n pickFirst: \"Pick a plan first.\",\n\n status: {\n trialing: \"Trial\",\n active: \"Active\",\n past_due: \"Payment overdue\",\n canceled: \"Cancelled\",\n expired: \"Expired\",\n comped: \"Complimentary\",\n },\n\n trialEnding: (days) =>\n `Your trial ends ${inDays(days)}. After that you can still view everything, but changes need a plan.`,\n grantEnding: (days) =>\n `Your free access ends ${inDays(days)}. After that you can still view everything, but changes need a plan.`,\n paymentFailed: \"Your last payment didn’t go through. Update your payment method to keep your plan.\",\n planEnded: \"Your plan has ended. You can still view and export everything; choose a plan to make changes again.\",\n guestReadOnly: (item) =>\n item ? `“${item}” is read-only for now; its owner can lift that.` : \"This is read-only for now; its owner can lift that.\",\n processing: \"Your payment is being processed. Your plan starts as soon as it’s confirmed; this page updates by itself.\",\n choosePlan: \"Choose a plan\",\n updatePayment: \"Update payment method\",\n paymentFailedAskBuyer: (buyer) =>\n buyer\n ? `The payment failed. ${buyer} can update the payment method.`\n : \"The payment failed. The admin who bought the subscription can update the payment method.\",\n checkAgain: \"Check again\",\n\n lockReason: \"Your plan has ended. Choose a plan to make changes again.\",\n\n limitReached: \"Plan limit reached\",\n limitUpgrade: \"To add more, choose a plan with a higher limit.\",\n limitContact: \"To add more, ask us for a higher limit.\",\n askForMore: \"Ask for more\",\n usage: (used, limit) => `${used} of ${limit}`,\n contactSubject: (dimension) => `Plan limit: ${dimension}`,\n limitUsageLine: (dimension, used, limit) => `${dimension}: ${used} of ${limit}`,\n\n manage: \"Payment and invoices\",\n cancel: \"Cancel subscription\",\n notAtProvider: \"Nothing to manage yet. Payments and invoices appear here once you have a paid plan.\",\n buyerHolds: (buyer) =>\n buyer\n ? `The payment method and the invoices are with ${buyer}, who bought the subscription.`\n : \"The payment method and the invoices are with the admin who bought the subscription.\",\n cancelConfirmTitle: \"Cancel subscription\",\n cancelConfirmBody: (date) => `Your subscription ends on ${date}. Until then everything works as before.`,\n cancelConfirmAction: \"Cancel subscription now\",\n keepSubscription: \"Keep subscription\",\n\n payOpening: \"Opening the secure checkout…\",\n payNothing: \"There’s no payment to make here. To choose a plan, go back to the app.\",\n payFailed: \"The checkout couldn’t be opened. Please go back and try again in a moment.\",\n payBack: \"The checkout was closed. You can go back, or reload this page to open it again.\",\n payDone: \"Payment complete. You can close this page.\",\n\n switchTitle: \"Change plan\",\n switchFromTo: (from, to) => `From ${from} to ${to}`,\n switchChargedNow: \"Charged now\",\n switchCredited: \"Credited towards your next charges\",\n switchNextCharge: (date) => `Next charge on ${date}`,\n switchConfirm: \"Change plan\",\n\n grantWarningTitle: \"Your free access continues\",\n grantWarningBody:\n \"You have free access with no end date. A purchase won’t change your plan while the free access runs: you would pay for a plan that doesn’t apply.\",\n grantWarningAction: \"Buy anyway\",\n\n waitingChanges: (count) =>\n count === 1 ? \"1 change waiting for a plan\" : `${count} changes waiting for a plan`,\n discardWaiting: \"Discard waiting changes\",\n\n notConfigured: \"Payments aren't set up yet. Please try again later.\",\n disabled: \"Subscriptions aren't available here.\",\n providerUnavailable: \"The payment provider can’t be reached right now. Please try again in a moment.\",\n alreadySubscribed: \"You already have a subscription. Change your plan above.\",\n planNotSold: \"This plan isn’t offered for that billing period or currency. Please choose another.\",\n notSubscribed: \"There’s no running subscription to change. Choose a plan to subscribe.\",\n planUnchanged: \"That’s already your plan. Pick another plan or billing period.\",\n};\n\n/** The `billing` namespace, resolved: English, then the provider, then `labels` — which\n * may be partial at every depth (`{ status: { comped: \"Beta\" } }`). */\nexport function useBillingLabels(labels?: LabelOverride<BillingLabels>): BillingLabels {\n return useKitLabels(\"billing\", DEFAULT_BILLING_LABELS, labels);\n}\n"],"mappings":";AAAA,SAAS,oBAAoB;AAmBtB,MAAM,wBAAuD,OAAO,OAAO;AAAA,EAChF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AA0OD,SAAS,OAAO,MAAsB;AACpC,MAAI,QAAQ,EAAG,QAAO;AACtB,MAAI,SAAS,EAAG,QAAO;AACvB,SAAO,MAAM,IAAI;AACnB;AAEO,MAAM,yBAAwC;AAAA,EACnD,OAAO;AAAA,EAEP,OAAO;AAAA,EACP,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,UAAU;AAAA,EACV,SAAS;AAAA,EACT,UAAU;AAAA,EACV,aAAa;AAAA,EACb,MAAM;AAAA,EACN,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,SAAS;AAAA,EACT,QAAQ,CAAC,SAAS,UAAU,IAAI;AAAA,EAChC,SAAS,CAAC,SAAS,cAAc,IAAI;AAAA,EACrC,WAAW,CAAC,SAAS,aAAa,IAAI;AAAA,EACtC,WAAW;AAAA,EACX,WAAW;AAAA,EAEX,QAAQ;AAAA,IACN,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,UAAU;AAAA,IACV,SAAS;AAAA,IACT,QAAQ;AAAA,EACV;AAAA,EAEA,aAAa,CAAC,SACZ,mBAAmB,OAAO,IAAI,CAAC;AAAA,EACjC,aAAa,CAAC,SACZ,yBAAyB,OAAO,IAAI,CAAC;AAAA,EACvC,eAAe;AAAA,EACf,WAAW;AAAA,EACX,eAAe,CAAC,SACd,OAAO,SAAI,IAAI,0DAAqD;AAAA,EACtE,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,uBAAuB,CAAC,UACtB,QACI,uBAAuB,KAAK,oCAC5B;AAAA,EACN,YAAY;AAAA,EAEZ,YAAY;AAAA,EAEZ,cAAc;AAAA,EACd,cAAc;AAAA,EACd,cAAc;AAAA,EACd,YAAY;AAAA,EACZ,OAAO,CAAC,MAAM,UAAU,GAAG,IAAI,OAAO,KAAK;AAAA,EAC3C,gBAAgB,CAAC,cAAc,eAAe,SAAS;AAAA,EACvD,gBAAgB,CAAC,WAAW,MAAM,UAAU,GAAG,SAAS,KAAK,IAAI,OAAO,KAAK;AAAA,EAE7E,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,eAAe;AAAA,EACf,YAAY,CAAC,UACX,QACI,gDAAgD,KAAK,mCACrD;AAAA,EACN,oBAAoB;AAAA,EACpB,mBAAmB,CAAC,SAAS,6BAA6B,IAAI;AAAA,EAC9D,qBAAqB;AAAA,EACrB,kBAAkB;AAAA,EAElB,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,WAAW;AAAA,EACX,SAAS;AAAA,EACT,SAAS;AAAA,EAET,aAAa;AAAA,EACb,cAAc,CAAC,MAAM,OAAO,QAAQ,IAAI,OAAO,EAAE;AAAA,EACjD,kBAAkB;AAAA,EAClB,gBAAgB;AAAA,EAChB,kBAAkB,CAAC,SAAS,kBAAkB,IAAI;AAAA,EAClD,eAAe;AAAA,EAEf,mBAAmB;AAAA,EACnB,kBACE;AAAA,EACF,oBAAoB;AAAA,EAEpB,gBAAgB,CAAC,UACf,UAAU,IAAI,gCAAgC,GAAG,KAAK;AAAA,EACxD,gBAAgB;AAAA,EAEhB,eAAe;AAAA,EACf,UAAU;AAAA,EACV,qBAAqB;AAAA,EACrB,mBAAmB;AAAA,EACnB,aAAa;AAAA,EACb,eAAe;AAAA,EACf,eAAe;AACjB;AAIO,SAAS,iBAAiB,QAAsD;AACrF,SAAO,aAAa,WAAW,wBAAwB,MAAM;AAC/D;","names":[]}
@@ -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,2 +1,8 @@
1
1
  "use client";
2
+ function isPermanentGrant(overview) {
3
+ return overview.status === "comped" && overview.source === "manual" && overview.comped_until == null;
4
+ }
5
+ export {
6
+ isPermanentGrant
7
+ };
2
8
  //# sourceMappingURL=billing-overview.js.map
@@ -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;AA+BhB,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":[]}
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,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;