@eifi1/ui-kit 0.41.0 → 0.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +26 -7
  2. package/dist/admin/admin-area-labels.d.ts +59 -0
  3. package/dist/admin/admin-area-labels.js +35 -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 +110 -0
  9. package/dist/admin/admin-overview.js +92 -0
  10. package/dist/admin/admin-overview.js.map +1 -0
  11. package/dist/admin/admin-sections.d.ts +94 -0
  12. package/dist/admin/admin-sections.js +81 -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 +33 -2
  18. package/dist/billing/billing-banner.js +29 -3
  19. package/dist/billing/billing-banner.js.map +1 -1
  20. package/dist/billing/billing-labels.d.ts +36 -0
  21. package/dist/billing/billing-labels.js +10 -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-request.d.ts +53 -0
  29. package/dist/billing/cancel-request.js +21 -0
  30. package/dist/billing/cancel-request.js.map +1 -0
  31. package/dist/billing/cancel-subscription.d.ts +46 -0
  32. package/dist/billing/cancel-subscription.js +57 -0
  33. package/dist/billing/cancel-subscription.js.map +1 -0
  34. package/dist/billing/plan-card.d.ts +45 -1
  35. package/dist/billing/plan-card.js +53 -2
  36. package/dist/billing/plan-card.js.map +1 -1
  37. package/dist/billing/plan-switch.d.ts +10 -1
  38. package/dist/billing/plan-switch.js +22 -13
  39. package/dist/billing/plan-switch.js.map +1 -1
  40. package/dist/billing/provider-page.d.ts +40 -0
  41. package/dist/billing/provider-page.js +33 -0
  42. package/dist/billing/provider-page.js.map +1 -0
  43. package/dist/billing/subscription-actions.d.ts +68 -4
  44. package/dist/billing/subscription-actions.js +65 -5
  45. package/dist/billing/subscription-actions.js.map +1 -1
  46. package/dist/components/account-chips.d.ts +50 -2
  47. package/dist/components/account-chips.js +33 -0
  48. package/dist/components/account-chips.js.map +1 -1
  49. package/dist/components/empty-state.d.ts +2 -1
  50. package/dist/components/empty-state.js.map +1 -1
  51. package/dist/eslint/index.js +1 -1
  52. package/dist/feedback/feedback-menu.d.ts +7 -4
  53. package/dist/feedback/feedback-menu.js +4 -2
  54. package/dist/feedback/feedback-menu.js.map +1 -1
  55. package/dist/hooks/use-search-param-state.js +4 -1
  56. package/dist/hooks/use-search-param-state.js.map +1 -1
  57. package/dist/i18n/defaults.js +7 -1
  58. package/dist/i18n/defaults.js.map +1 -1
  59. package/dist/i18n/german.js +113 -0
  60. package/dist/i18n/german.js.map +1 -1
  61. package/dist/i18n/kit-labels.d.ts +12 -0
  62. package/dist/i18n/kit-labels.js.map +1 -1
  63. package/dist/i18n/locales/en.js +142 -1
  64. package/dist/i18n/locales/en.js.map +3 -3
  65. package/dist/i18n/locales/es.js +110 -0
  66. package/dist/i18n/locales/es.js.map +1 -1
  67. package/dist/i18n/locales/fr.js +109 -0
  68. package/dist/i18n/locales/fr.js.map +1 -1
  69. package/dist/i18n/locales/hu.js +110 -0
  70. package/dist/i18n/locales/hu.js.map +1 -1
  71. package/dist/i18n/locales/it.js +110 -0
  72. package/dist/i18n/locales/it.js.map +1 -1
  73. package/dist/i18n/locales/zh.js +109 -0
  74. package/dist/i18n/locales/zh.js.map +1 -1
  75. package/dist/i18n/review.js +34 -1
  76. package/dist/i18n/review.js.map +1 -1
  77. package/dist/index.d.ts +24 -2
  78. package/dist/index.js +29 -1
  79. package/dist/index.js.map +1 -1
  80. package/dist/search/global-search.d.ts +3 -0
  81. package/dist/search/global-search.js +3 -1
  82. package/dist/search/global-search.js.map +1 -1
  83. package/dist/settings/settings-catalogue.d.ts +9 -1
  84. package/dist/settings/settings-catalogue.js +1 -1
  85. package/dist/settings/settings-catalogue.js.map +1 -1
  86. package/dist/settings/settings-layout.d.ts +7 -1
  87. package/dist/settings/settings-layout.js +35 -27
  88. package/dist/settings/settings-layout.js.map +1 -1
  89. package/dist/shell/account-menu-language.d.ts +46 -0
  90. package/dist/shell/account-menu-language.js +64 -0
  91. package/dist/shell/account-menu-language.js.map +1 -0
  92. package/dist/shell/admin-menu.d.ts +69 -0
  93. package/dist/shell/admin-menu.js +64 -0
  94. package/dist/shell/admin-menu.js.map +1 -0
  95. package/dist/shell/app-shell.d.ts +10 -0
  96. package/dist/shell/app-shell.js +22 -4
  97. package/dist/shell/app-shell.js.map +1 -1
  98. package/dist/shell/dev-switchers.d.ts +53 -0
  99. package/dist/shell/dev-switchers.js +107 -0
  100. package/dist/shell/dev-switchers.js.map +1 -0
  101. package/dist/shell/sync-status-indicator.d.ts +86 -0
  102. package/dist/shell/sync-status-indicator.js +170 -0
  103. package/dist/shell/sync-status-indicator.js.map +1 -0
  104. package/dist/shell/top-bar-actions.d.ts +49 -0
  105. package/dist/shell/top-bar-actions.js +31 -0
  106. package/dist/shell/top-bar-actions.js.map +1 -0
  107. package/dist/shell/top-bar-parts-labels.d.ts +90 -0
  108. package/dist/shell/top-bar-parts-labels.js +51 -0
  109. package/dist/shell/top-bar-parts-labels.js.map +1 -0
  110. package/dist/shell/topbar-action-menu.d.ts +13 -1
  111. package/dist/shell/topbar-action-menu.js +5 -2
  112. package/dist/shell/topbar-action-menu.js.map +1 -1
  113. package/dist/shell/topbar-controls.d.ts +16 -3
  114. package/dist/shell/topbar-controls.js +5 -1
  115. package/dist/shell/topbar-controls.js.map +1 -1
  116. package/dist/shell/tour-menu.d.ts +67 -0
  117. package/dist/shell/tour-menu.js +87 -0
  118. package/dist/shell/tour-menu.js.map +1 -0
  119. package/dist/shell.d.ts +16 -0
  120. package/dist/shell.js +21 -0
  121. package/dist/shell.js.map +1 -1
  122. package/dist/tour/tour-catalogue-labels.d.ts +44 -0
  123. package/dist/tour/tour-catalogue-labels.js +32 -0
  124. package/dist/tour/tour-catalogue-labels.js.map +1 -0
  125. package/dist/tour/tour-catalogue.d.ts +68 -0
  126. package/dist/tour/tour-catalogue.js +189 -0
  127. package/dist/tour/tour-catalogue.js.map +1 -0
  128. package/dist/tour/tour-info.d.ts +112 -0
  129. package/dist/tour/tour-info.js +36 -0
  130. package/dist/tour/tour-info.js.map +1 -0
  131. package/dist/tour/use-tour-progress.d.ts +61 -0
  132. package/dist/tour/use-tour-progress.js +116 -0
  133. package/dist/tour/use-tour-progress.js.map +1 -0
  134. package/dist/tour/use-tour-url-sync.d.ts +58 -0
  135. package/dist/tour/use-tour-url-sync.js +110 -0
  136. package/dist/tour/use-tour-url-sync.js.map +1 -0
  137. package/dist/tour.d.ts +8 -0
  138. package/dist/tour.js +18 -0
  139. package/dist/tour.js.map +1 -1
  140. package/package.json +1 -1
  141. package/src/admin/admin-area-labels.ts +91 -0
  142. package/src/admin/admin-forbidden.tsx +75 -0
  143. package/src/admin/admin-overview.tsx +217 -0
  144. package/src/admin/admin-sections.tsx +198 -0
  145. package/src/admin/admin-tool-links.tsx +84 -0
  146. package/src/billing/billing-banner.tsx +57 -6
  147. package/src/billing/billing-labels.ts +59 -0
  148. package/src/billing/billing-overview.ts +37 -0
  149. package/src/billing/billing-standing.ts +8 -0
  150. package/src/billing/cancel-request.ts +75 -0
  151. package/src/billing/cancel-subscription.tsx +102 -0
  152. package/src/billing/plan-card.tsx +97 -1
  153. package/src/billing/plan-switch.tsx +16 -0
  154. package/src/billing/provider-page.ts +75 -0
  155. package/src/billing/subscription-actions.tsx +155 -20
  156. package/src/components/account-chips.tsx +89 -2
  157. package/src/components/empty-state.tsx +2 -1
  158. package/src/feedback/feedback-menu.tsx +8 -3
  159. package/src/hooks/use-search-param-state.ts +6 -1
  160. package/src/i18n/defaults.ts +6 -0
  161. package/src/i18n/german.ts +127 -0
  162. package/src/i18n/kit-labels.tsx +12 -0
  163. package/src/i18n/locales/en.ts +32 -0
  164. package/src/i18n/locales/es.ts +121 -0
  165. package/src/i18n/locales/fr.ts +121 -0
  166. package/src/i18n/locales/hu.ts +119 -0
  167. package/src/i18n/locales/it.ts +122 -0
  168. package/src/i18n/locales/zh.ts +114 -0
  169. package/src/i18n/review.ts +33 -0
  170. package/src/index.ts +38 -2
  171. package/src/search/global-search.tsx +5 -0
  172. package/src/settings/settings-catalogue.ts +10 -2
  173. package/src/settings/settings-layout.tsx +58 -34
  174. package/src/shell/account-menu-language.tsx +116 -0
  175. package/src/shell/admin-menu.tsx +146 -0
  176. package/src/shell/app-shell.tsx +44 -3
  177. package/src/shell/dev-switchers.tsx +179 -0
  178. package/src/shell/sync-status-indicator.tsx +301 -0
  179. package/src/shell/top-bar-actions.tsx +79 -0
  180. package/src/shell/top-bar-parts-labels.ts +142 -0
  181. package/src/shell/topbar-action-menu.tsx +15 -2
  182. package/src/shell/topbar-controls.tsx +20 -3
  183. package/src/shell/tour-menu.tsx +179 -0
  184. package/src/shell.ts +27 -0
  185. package/src/tour/tour-catalogue-labels.ts +75 -0
  186. package/src/tour/tour-catalogue.tsx +316 -0
  187. package/src/tour/tour-info.ts +144 -0
  188. package/src/tour/use-tour-progress.ts +198 -0
  189. package/src/tour/use-tour-url-sync.ts +221 -0
  190. package/src/tour.ts +11 -0
@@ -51,6 +51,15 @@ export interface PlanSwitchConfirmProps {
51
51
  onConfirm: () => void;
52
52
  /** The figures' and the date's locale. Default: the provider's. */
53
53
  locale?: string;
54
+ /**
55
+ * 0.42 (§16.2 decision 5, §16.8 B): the payer's free grant runs without an end —
56
+ * `isPermanentGrant(overview)`. The switch changes the PAID plan, not the applied one
57
+ * (§12.11), so the dialog says so above its figures (`grantWarningTitle`,
58
+ * `grantWarningBody`) before the payer agrees. Confirm stays Confirm: the warning is
59
+ * this dialog's own, and `PlanPicker` — told `switching` — shows none of its own on
60
+ * the way here, so the payer reads it once.
61
+ */
62
+ permanentGrant?: boolean;
54
63
  /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */
55
64
  labels?: LabelOverride<BillingLabels>;
56
65
  }
@@ -76,4 +85,4 @@ export interface PlanSwitchConfirmProps {
76
85
  * change plan, as they may pay. A demo lock still holds it, as the server's demo guard
77
86
  * does. The kit sends no request.
78
87
  */
79
- export declare function PlanSwitchConfirm({ open, onOpenChange, from, to, preview, pending, planName, dimensionLabels, formatLimit, onConfirm, locale: localeProp, labels: labelsProp, }: PlanSwitchConfirmProps): import("react").JSX.Element;
88
+ export declare function PlanSwitchConfirm({ open, onOpenChange, from, to, preview, pending, planName, dimensionLabels, formatLimit, onConfirm, locale: localeProp, permanentGrant, labels: labelsProp, }: PlanSwitchConfirmProps): import("react").JSX.Element;
@@ -4,6 +4,7 @@ import { useKitLabels, useKitLocale } from "../i18n/kit-labels.js";
4
4
  import { DEFAULT_COMMON_LABELS } from "../i18n/shared-labels.js";
5
5
  import { pluralForm } from "../i18n/plurals.js";
6
6
  import { formatDate, formatMoney, formatNumber } from "../lib/format.js";
7
+ import { AlertBanner } from "../components/alert-banner.js";
7
8
  import { Button } from "../components/button.js";
8
9
  import { DialogFrame } from "../components/dialog-frame.js";
9
10
  import { Spinner } from "../components/spinner.js";
@@ -23,6 +24,7 @@ function PlanSwitchConfirm({
23
24
  formatLimit,
24
25
  onConfirm,
25
26
  locale: localeProp,
27
+ permanentGrant = false,
26
28
  labels: labelsProp
27
29
  }) {
28
30
  const labels = useBillingLabels(labelsProp);
@@ -44,7 +46,7 @@ function PlanSwitchConfirm({
44
46
  });
45
47
  const credit = preview !== void 0 && preview.now_minor < 0;
46
48
  const lines = preview ? overLimit(preview) : [];
47
- return /* @__PURE__ */ jsx(
49
+ return /* @__PURE__ */ jsxs(
48
50
  DialogFrame,
49
51
  {
50
52
  open,
@@ -70,20 +72,27 @@ function PlanSwitchConfirm({
70
72
  }
71
73
  )
72
74
  ] }),
73
- children: preview === void 0 ? /* @__PURE__ */ jsx("div", { className: "flex justify-center py-4", children: /* @__PURE__ */ jsx(Spinner, { showLabel: true }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
74
- /* @__PURE__ */ jsxs("dl", { className: "grid grid-cols-[minmax(0,1fr)_auto] gap-x-4 gap-y-1 text-sm", "data-plan-switch-preview": true, children: [
75
- /* @__PURE__ */ jsx("dt", { className: "text-secondary", children: credit ? labels.switchCredited : labels.switchChargedNow }),
76
- /* @__PURE__ */ jsx("dd", { className: "text-right font-medium tabular-nums", "data-now": credit ? "credit" : "charge", children: money(Math.abs(preview.now_minor), preview.currency) }),
77
- preview.next_billed_at && preview.next_minor != null && /* @__PURE__ */ jsxs(Fragment, { children: [
78
- /* @__PURE__ */ jsx("dt", { className: "text-secondary", children: labels.switchNextCharge(formatDate(preview.next_billed_at, "medium", { locale })) }),
79
- /* @__PURE__ */ jsx("dd", { className: "text-right tabular-nums", children: money(preview.next_minor, preview.currency) })
80
- ] })
75
+ children: [
76
+ permanentGrant && /* @__PURE__ */ jsxs(AlertBanner, { tone: "warning", size: "sm", "data-grant-warning": "", children: [
77
+ /* @__PURE__ */ jsx("strong", { className: "block font-medium", children: labels.grantWarningTitle }),
78
+ " ",
79
+ labels.grantWarningBody
81
80
  ] }),
82
- lines.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-1 text-sm", "data-plan-switch-over-limit": true, children: [
83
- /* @__PURE__ */ jsx("p", { children: planChange.overLimit }),
84
- /* @__PURE__ */ jsx("ul", { className: "list-disc space-y-0.5 ps-5 text-secondary", children: lines.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line)) })
81
+ preview === void 0 ? /* @__PURE__ */ jsx("div", { className: "flex justify-center py-4", children: /* @__PURE__ */ jsx(Spinner, { showLabel: true }) }) : /* @__PURE__ */ jsxs(Fragment, { children: [
82
+ /* @__PURE__ */ jsxs("dl", { className: "grid grid-cols-[minmax(0,1fr)_auto] gap-x-4 gap-y-1 text-sm", "data-plan-switch-preview": true, children: [
83
+ /* @__PURE__ */ jsx("dt", { className: "text-secondary", children: credit ? labels.switchCredited : labels.switchChargedNow }),
84
+ /* @__PURE__ */ jsx("dd", { className: "text-right font-medium tabular-nums", "data-now": credit ? "credit" : "charge", children: money(Math.abs(preview.now_minor), preview.currency) }),
85
+ preview.next_billed_at && preview.next_minor != null && /* @__PURE__ */ jsxs(Fragment, { children: [
86
+ /* @__PURE__ */ jsx("dt", { className: "text-secondary", children: labels.switchNextCharge(formatDate(preview.next_billed_at, "medium", { locale })) }),
87
+ /* @__PURE__ */ jsx("dd", { className: "text-right tabular-nums", children: money(preview.next_minor, preview.currency) })
88
+ ] })
89
+ ] }),
90
+ lines.length > 0 && /* @__PURE__ */ jsxs("div", { className: "space-y-1 text-sm", "data-plan-switch-over-limit": true, children: [
91
+ /* @__PURE__ */ jsx("p", { children: planChange.overLimit }),
92
+ /* @__PURE__ */ jsx("ul", { className: "list-disc space-y-0.5 ps-5 text-secondary", children: lines.map((line) => /* @__PURE__ */ jsx("li", { children: line }, line)) })
93
+ ] })
85
94
  ] })
86
- ] })
95
+ ]
87
96
  }
88
97
  );
89
98
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/billing/plan-switch.tsx"],"sourcesContent":["import type { KeyboardEvent } from \"react\";\n\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_COMMON_LABELS } from \"../i18n/shared-labels\";\nimport { pluralForm } from \"../i18n/plurals\";\nimport type { PluralForms } from \"../i18n/plurals\";\nimport { formatDate, formatMoney, formatNumber } from \"../lib/format\";\nimport { Button } from \"../components/button\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { Spinner } from \"../components/spinner\";\nimport { COMMIT_EXCEPT_BILLING } from \"../components/write-lock\";\nimport { DEFAULT_PLAN_CHANGE_LABELS } from \"../admin/plan-change-labels\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { PlanSwitchPreview } from \"./billing-overview\";\nimport { minorToMajor } from \"./plan-price\";\nimport type { BillingInterval } from \"./plan-price\";\n\n/**\n * The payer's plan switch (docs/billing-harmonization.md §15.2.10, ui-kit 0.39): built\n * once in the kits and used by all three apps, instead of Paddle's portal. While a\n * provider subscription runs, the app sends a `PlanPicker` pick to\n * `POST /billing/plan/preview` and opens this dialog with the answer; Confirm is the\n * app's `POST /billing/plan`. Otherwise the pick goes to the checkout, as before.\n *\n * The switch is immediate and prorated: an upgrade charges the difference now, a\n * downgrade credits the unused part to the customer's balance, which Paddle takes off\n * the next charges. So the payer agrees to a figure — Paddle's preview — and sees the\n * dimensions over the new plan's limit before agreeing (§3.4: a limit gates creation\n * only, so the switch is allowed).\n */\n\n/** One side of a switch: a plan code and its billing period. */\nexport interface PlanSwitchSide {\n plan: string;\n interval: BillingInterval;\n}\n\nexport interface PlanSwitchConfirmProps {\n open: boolean;\n /** `false` when the payer dismisses it — Cancel, Escape, the backdrop. Confirm leaves\n * it open: close it once the switch answers. */\n onOpenChange: (open: boolean) => void;\n /** What the payer pays now: the overview's `paid`. */\n from: PlanSwitchSide;\n /** The pick. */\n to: PlanSwitchSide;\n /** `POST /billing/plan/preview`'s answer. Undefined while it loads: a spinner, and\n * Confirm is held. */\n preview?: PlanSwitchPreview;\n /** `POST /billing/plan` is under way: Confirm's spinner, no second press, and Escape\n * doesn't close. */\n pending?: boolean;\n /** The plan's display name, the app's (§3.1). Default: the code. */\n planName?: (code: string) => string;\n /**\n * The app's word per dimension, for the over-limit lines. A heading's word (\"Budgets\")\n * reads \"Budgets: 7 of 5\" (`billing.limitUsageLine`); its plural forms\n * (`{ one: \"budget\", other: \"budgets\" }`) read \"7 of 5 budgets\" (`planChange.usageOf`,\n * the form the limit takes). A dimension without one is named by its key.\n */\n dimensionLabels?: Readonly<Record<string, string | PluralForms>>;\n /** How a usage or limit figure is written. Default: a number in the locale. */\n formatLimit?: (dimension: string, value: number) => string;\n /** Confirm: the app's `POST /billing/plan` with `{plan, interval}` of `to`. */\n onConfirm: () => void;\n /** The figures' and the date's locale. Default: the provider's. */\n locale?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * The confirm for a plan switch (§15.2.10): from → to, each with its billing period;\n * Paddle's preview — \"Charged now\" (an upgrade) or \"Credited towards your next charges\"\n * (a downgrade), and the next charge with its date — in the subscription's currency;\n * the dimensions over the new plan's limit; and Confirm.\n *\n * const preview = useMutation({ mutationFn: api.previewPlanSwitch });\n * <PlanSwitchConfirm\n * open={pick !== null}\n * onOpenChange={(open) => !open && setPick(null)}\n * from={overview.paid}\n * to={pick}\n * preview={preview.data}\n * pending={switching.isPending}\n * planName={planName}\n * onConfirm={() => switching.mutate(pick)}\n * />\n *\n * It takes no billing lock (`COMMIT_EXCEPT_BILLING` built in): a read-only payer may\n * change plan, as they may pay. A demo lock still holds it, as the server's demo guard\n * does. The kit sends no request.\n */\nexport function PlanSwitchConfirm({\n open,\n onOpenChange,\n from,\n to,\n preview,\n pending = false,\n planName = (code) => code,\n dimensionLabels,\n formatLimit,\n onConfirm,\n locale: localeProp,\n labels: labelsProp,\n}: PlanSwitchConfirmProps) {\n const labels = useBillingLabels(labelsProp);\n const planChange = useKitLabels(\"planChange\", DEFAULT_PLAN_CHANGE_LABELS);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale(localeProp);\n\n const period = (interval: BillingInterval) => (interval === \"year\" ? labels.yearly : labels.monthly);\n const side = (s: PlanSwitchSide) => `${planName(s.plan.toLowerCase())} (${period(s.interval)})`;\n const money = (minor: number, currency: string) =>\n formatMoney(minorToMajor(minor, currency), currency, { locale });\n const figure = (dimension: string, value: number) =>\n formatLimit ? formatLimit(dimension, value) : formatNumber(value, { locale });\n\n /** One line per dimension over the new limit, in the word's own sentence. */\n const overLimit = (p: PlanSwitchPreview): string[] =>\n (p.over_limit ?? []).flatMap((dimension) => {\n const limit = p.limits?.[dimension];\n const used = p.usage?.[dimension];\n if (limit === null || limit === undefined || used === undefined) return [];\n const word = dimensionLabels?.[dimension] ?? dimension;\n return [\n typeof word === \"string\"\n ? labels.limitUsageLine(word, figure(dimension, used), figure(dimension, limit))\n : planChange.usageOf(figure(dimension, used), figure(dimension, limit), pluralForm(limit, word, locale)),\n ];\n });\n\n const credit = preview !== undefined && preview.now_minor < 0;\n const lines = preview ? overLimit(preview) : [];\n\n return (\n <DialogFrame\n open={open}\n title={labels.switchTitle}\n description={labels.switchFromTo(side(from), side(to))}\n onClose={() => onOpenChange(false)}\n onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {\n if (pending && e.key === \"Escape\") e.preventDefault();\n }}\n aria-busy={preview === undefined || pending || undefined}\n actions={(close) => (\n <>\n <Button variant=\"secondary\" onClick={close} disabled={pending}>\n {common.cancel}\n </Button>\n <Button\n commit={COMMIT_EXCEPT_BILLING}\n pending={pending}\n disabled={preview === undefined}\n onClick={() => {\n if (preview !== undefined && !pending) onConfirm();\n }}\n >\n {labels.switchConfirm}\n </Button>\n </>\n )}\n >\n {preview === undefined ? (\n <div className=\"flex justify-center py-4\">\n <Spinner showLabel />\n </div>\n ) : (\n <>\n <dl className=\"grid grid-cols-[minmax(0,1fr)_auto] gap-x-4 gap-y-1 text-sm\" data-plan-switch-preview>\n <dt className=\"text-secondary\">{credit ? labels.switchCredited : labels.switchChargedNow}</dt>\n <dd className=\"text-right font-medium tabular-nums\" data-now={credit ? \"credit\" : \"charge\"}>\n {money(Math.abs(preview.now_minor), preview.currency)}\n </dd>\n {preview.next_billed_at && preview.next_minor != null && (\n <>\n <dt className=\"text-secondary\">\n {labels.switchNextCharge(formatDate(preview.next_billed_at, \"medium\", { locale }))}\n </dt>\n <dd className=\"text-right tabular-nums\">{money(preview.next_minor, preview.currency)}</dd>\n </>\n )}\n </dl>\n {lines.length > 0 && (\n <div className=\"space-y-1 text-sm\" data-plan-switch-over-limit>\n <p>{planChange.overLimit}</p>\n <ul className=\"list-disc space-y-0.5 ps-5 text-secondary\">\n {lines.map((line) => (\n <li key={line}>{line}</li>\n ))}\n </ul>\n </div>\n )}\n </>\n )}\n </DialogFrame>\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/billing/plan-switch.tsx"],"sourcesContent":["import type { KeyboardEvent } from \"react\";\n\nimport { useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_COMMON_LABELS } from \"../i18n/shared-labels\";\nimport { pluralForm } from \"../i18n/plurals\";\nimport type { PluralForms } from \"../i18n/plurals\";\nimport { formatDate, formatMoney, formatNumber } from \"../lib/format\";\nimport { AlertBanner } from \"../components/alert-banner\";\nimport { Button } from \"../components/button\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { Spinner } from \"../components/spinner\";\nimport { COMMIT_EXCEPT_BILLING } from \"../components/write-lock\";\nimport { DEFAULT_PLAN_CHANGE_LABELS } from \"../admin/plan-change-labels\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { PlanSwitchPreview } from \"./billing-overview\";\nimport { minorToMajor } from \"./plan-price\";\nimport type { BillingInterval } from \"./plan-price\";\n\n/**\n * The payer's plan switch (docs/billing-harmonization.md §15.2.10, ui-kit 0.39): built\n * once in the kits and used by all three apps, instead of Paddle's portal. While a\n * provider subscription runs, the app sends a `PlanPicker` pick to\n * `POST /billing/plan/preview` and opens this dialog with the answer; Confirm is the\n * app's `POST /billing/plan`. Otherwise the pick goes to the checkout, as before.\n *\n * The switch is immediate and prorated: an upgrade charges the difference now, a\n * downgrade credits the unused part to the customer's balance, which Paddle takes off\n * the next charges. So the payer agrees to a figure — Paddle's preview — and sees the\n * dimensions over the new plan's limit before agreeing (§3.4: a limit gates creation\n * only, so the switch is allowed).\n */\n\n/** One side of a switch: a plan code and its billing period. */\nexport interface PlanSwitchSide {\n plan: string;\n interval: BillingInterval;\n}\n\nexport interface PlanSwitchConfirmProps {\n open: boolean;\n /** `false` when the payer dismisses it — Cancel, Escape, the backdrop. Confirm leaves\n * it open: close it once the switch answers. */\n onOpenChange: (open: boolean) => void;\n /** What the payer pays now: the overview's `paid`. */\n from: PlanSwitchSide;\n /** The pick. */\n to: PlanSwitchSide;\n /** `POST /billing/plan/preview`'s answer. Undefined while it loads: a spinner, and\n * Confirm is held. */\n preview?: PlanSwitchPreview;\n /** `POST /billing/plan` is under way: Confirm's spinner, no second press, and Escape\n * doesn't close. */\n pending?: boolean;\n /** The plan's display name, the app's (§3.1). Default: the code. */\n planName?: (code: string) => string;\n /**\n * The app's word per dimension, for the over-limit lines. A heading's word (\"Budgets\")\n * reads \"Budgets: 7 of 5\" (`billing.limitUsageLine`); its plural forms\n * (`{ one: \"budget\", other: \"budgets\" }`) read \"7 of 5 budgets\" (`planChange.usageOf`,\n * the form the limit takes). A dimension without one is named by its key.\n */\n dimensionLabels?: Readonly<Record<string, string | PluralForms>>;\n /** How a usage or limit figure is written. Default: a number in the locale. */\n formatLimit?: (dimension: string, value: number) => string;\n /** Confirm: the app's `POST /billing/plan` with `{plan, interval}` of `to`. */\n onConfirm: () => void;\n /** The figures' and the date's locale. Default: the provider's. */\n locale?: string;\n /**\n * 0.42 (§16.2 decision 5, §16.8 B): the payer's free grant runs without an end —\n * `isPermanentGrant(overview)`. The switch changes the PAID plan, not the applied one\n * (§12.11), so the dialog says so above its figures (`grantWarningTitle`,\n * `grantWarningBody`) before the payer agrees. Confirm stays Confirm: the warning is\n * this dialog's own, and `PlanPicker` — told `switching` — shows none of its own on\n * the way here, so the payer reads it once.\n */\n permanentGrant?: boolean;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * The confirm for a plan switch (§15.2.10): from → to, each with its billing period;\n * Paddle's preview — \"Charged now\" (an upgrade) or \"Credited towards your next charges\"\n * (a downgrade), and the next charge with its date — in the subscription's currency;\n * the dimensions over the new plan's limit; and Confirm.\n *\n * const preview = useMutation({ mutationFn: api.previewPlanSwitch });\n * <PlanSwitchConfirm\n * open={pick !== null}\n * onOpenChange={(open) => !open && setPick(null)}\n * from={overview.paid}\n * to={pick}\n * preview={preview.data}\n * pending={switching.isPending}\n * planName={planName}\n * onConfirm={() => switching.mutate(pick)}\n * />\n *\n * It takes no billing lock (`COMMIT_EXCEPT_BILLING` built in): a read-only payer may\n * change plan, as they may pay. A demo lock still holds it, as the server's demo guard\n * does. The kit sends no request.\n */\nexport function PlanSwitchConfirm({\n open,\n onOpenChange,\n from,\n to,\n preview,\n pending = false,\n planName = (code) => code,\n dimensionLabels,\n formatLimit,\n onConfirm,\n locale: localeProp,\n permanentGrant = false,\n labels: labelsProp,\n}: PlanSwitchConfirmProps) {\n const labels = useBillingLabels(labelsProp);\n const planChange = useKitLabels(\"planChange\", DEFAULT_PLAN_CHANGE_LABELS);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale(localeProp);\n\n const period = (interval: BillingInterval) => (interval === \"year\" ? labels.yearly : labels.monthly);\n const side = (s: PlanSwitchSide) => `${planName(s.plan.toLowerCase())} (${period(s.interval)})`;\n const money = (minor: number, currency: string) =>\n formatMoney(minorToMajor(minor, currency), currency, { locale });\n const figure = (dimension: string, value: number) =>\n formatLimit ? formatLimit(dimension, value) : formatNumber(value, { locale });\n\n /** One line per dimension over the new limit, in the word's own sentence. */\n const overLimit = (p: PlanSwitchPreview): string[] =>\n (p.over_limit ?? []).flatMap((dimension) => {\n const limit = p.limits?.[dimension];\n const used = p.usage?.[dimension];\n if (limit === null || limit === undefined || used === undefined) return [];\n const word = dimensionLabels?.[dimension] ?? dimension;\n return [\n typeof word === \"string\"\n ? labels.limitUsageLine(word, figure(dimension, used), figure(dimension, limit))\n : planChange.usageOf(figure(dimension, used), figure(dimension, limit), pluralForm(limit, word, locale)),\n ];\n });\n\n const credit = preview !== undefined && preview.now_minor < 0;\n const lines = preview ? overLimit(preview) : [];\n\n return (\n <DialogFrame\n open={open}\n title={labels.switchTitle}\n description={labels.switchFromTo(side(from), side(to))}\n onClose={() => onOpenChange(false)}\n onKeyDown={(e: KeyboardEvent<HTMLDivElement>) => {\n if (pending && e.key === \"Escape\") e.preventDefault();\n }}\n aria-busy={preview === undefined || pending || undefined}\n actions={(close) => (\n <>\n <Button variant=\"secondary\" onClick={close} disabled={pending}>\n {common.cancel}\n </Button>\n <Button\n commit={COMMIT_EXCEPT_BILLING}\n pending={pending}\n disabled={preview === undefined}\n onClick={() => {\n if (preview !== undefined && !pending) onConfirm();\n }}\n >\n {labels.switchConfirm}\n </Button>\n </>\n )}\n >\n {permanentGrant && (\n <AlertBanner tone=\"warning\" size=\"sm\" data-grant-warning=\"\">\n <strong className=\"block font-medium\">{labels.grantWarningTitle}</strong> {labels.grantWarningBody}\n </AlertBanner>\n )}\n {preview === undefined ? (\n <div className=\"flex justify-center py-4\">\n <Spinner showLabel />\n </div>\n ) : (\n <>\n <dl className=\"grid grid-cols-[minmax(0,1fr)_auto] gap-x-4 gap-y-1 text-sm\" data-plan-switch-preview>\n <dt className=\"text-secondary\">{credit ? labels.switchCredited : labels.switchChargedNow}</dt>\n <dd className=\"text-right font-medium tabular-nums\" data-now={credit ? \"credit\" : \"charge\"}>\n {money(Math.abs(preview.now_minor), preview.currency)}\n </dd>\n {preview.next_billed_at && preview.next_minor != null && (\n <>\n <dt className=\"text-secondary\">\n {labels.switchNextCharge(formatDate(preview.next_billed_at, \"medium\", { locale }))}\n </dt>\n <dd className=\"text-right tabular-nums\">{money(preview.next_minor, preview.currency)}</dd>\n </>\n )}\n </dl>\n {lines.length > 0 && (\n <div className=\"space-y-1 text-sm\" data-plan-switch-over-limit>\n <p>{planChange.overLimit}</p>\n <ul className=\"list-disc space-y-0.5 ps-5 text-secondary\">\n {lines.map((line) => (\n <li key={line}>{line}</li>\n ))}\n </ul>\n </div>\n )}\n </>\n )}\n </DialogFrame>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The provider's hosted portal in a NEW TAB (0.43, docs/billing-harmonization.md §17):
3
+ * "Payment and invoices", the portal's "Cancel subscription" and the payment-failed
4
+ * banner's "Update payment method" open at the provider while the app stays open where
5
+ * it was. kastlan's `usePortal` (feat/kit-0.42, `72c3177`) is the reference.
6
+ *
7
+ * The checkout is NOT one of them (§17.4): it stays in the same tab, since the page waits
8
+ * for the payer's return (`useCheckoutProcessing`).
9
+ */
10
+ /** How the page went: in a tab of its own, or — the browser refused one — in this one. */
11
+ export type ProviderPageWay = "new-tab" | "same-tab";
12
+ export interface OpenProviderPageOptions {
13
+ /** The window to open from and to fall back to. Default the global `window` (a test's
14
+ * or an iframe's own). */
15
+ window?: Window;
16
+ }
17
+ /**
18
+ * Opens the provider's page in a new tab, from INSIDE the click: call it in the click
19
+ * handler itself, before any `await` — a `window.open` after one is blocked as a pop-up.
20
+ *
21
+ * 1. opens `about:blank` synchronously and sets its `opener` to `null`, so the provider's
22
+ * page cannot reach back into the app;
23
+ * 2. awaits `fetchUrl` — the app's `POST /billing/portal` with its target, answering the
24
+ * `url` (the kit sends no request);
25
+ * 3. points the tab at the URL;
26
+ * 4. on a refusal closes the tab again and rethrows, so the app shows its error;
27
+ * 5. where the browser refused the tab (a pop-up blocker), or it was closed while the
28
+ * URL was on its way, the app's own page goes there instead.
29
+ *
30
+ * <SubscriptionActions
31
+ * onPortal={(target) => {
32
+ * openProviderPage(() => api.billingPortal({ target }).then((r) => r.url)).catch(showError);
33
+ * }}
34
+ * providerName="Paddle"
35
+ * appName="Kastlan"
36
+ * />
37
+ *
38
+ * Resolves with the way it went.
39
+ */
40
+ export declare function openProviderPage(fetchUrl: () => Promise<string>, options?: OpenProviderPageOptions): Promise<ProviderPageWay>;
@@ -0,0 +1,33 @@
1
+ "use client";
2
+ async function openProviderPage(fetchUrl, options = {}) {
3
+ const win = options.window ?? window;
4
+ let tab;
5
+ try {
6
+ tab = win.open("about:blank", "_blank");
7
+ } catch {
8
+ tab = null;
9
+ }
10
+ if (tab) {
11
+ try {
12
+ tab.opener = null;
13
+ } catch {
14
+ }
15
+ }
16
+ let url;
17
+ try {
18
+ url = await fetchUrl();
19
+ } catch (error) {
20
+ tab?.close();
21
+ throw error;
22
+ }
23
+ if (tab && !tab.closed) {
24
+ tab.location.href = url;
25
+ return "new-tab";
26
+ }
27
+ win.location.assign(url);
28
+ return "same-tab";
29
+ }
30
+ export {
31
+ openProviderPage
32
+ };
33
+ //# sourceMappingURL=provider-page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/billing/provider-page.ts"],"sourcesContent":["/**\n * The provider's hosted portal in a NEW TAB (0.43, docs/billing-harmonization.md §17):\n * \"Payment and invoices\", the portal's \"Cancel subscription\" and the payment-failed\n * banner's \"Update payment method\" open at the provider while the app stays open where\n * it was. kastlan's `usePortal` (feat/kit-0.42, `72c3177`) is the reference.\n *\n * The checkout is NOT one of them (§17.4): it stays in the same tab, since the page waits\n * for the payer's return (`useCheckoutProcessing`).\n */\n\n/** How the page went: in a tab of its own, or — the browser refused one — in this one. */\nexport type ProviderPageWay = \"new-tab\" | \"same-tab\";\n\nexport interface OpenProviderPageOptions {\n /** The window to open from and to fall back to. Default the global `window` (a test's\n * or an iframe's own). */\n window?: Window;\n}\n\n/**\n * Opens the provider's page in a new tab, from INSIDE the click: call it in the click\n * handler itself, before any `await` — a `window.open` after one is blocked as a pop-up.\n *\n * 1. opens `about:blank` synchronously and sets its `opener` to `null`, so the provider's\n * page cannot reach back into the app;\n * 2. awaits `fetchUrl` — the app's `POST /billing/portal` with its target, answering the\n * `url` (the kit sends no request);\n * 3. points the tab at the URL;\n * 4. on a refusal closes the tab again and rethrows, so the app shows its error;\n * 5. where the browser refused the tab (a pop-up blocker), or it was closed while the\n * URL was on its way, the app's own page goes there instead.\n *\n * <SubscriptionActions\n * onPortal={(target) => {\n * openProviderPage(() => api.billingPortal({ target }).then((r) => r.url)).catch(showError);\n * }}\n * providerName=\"Paddle\"\n * appName=\"Kastlan\"\n * />\n *\n * Resolves with the way it went.\n */\nexport async function openProviderPage(\n fetchUrl: () => Promise<string>,\n options: OpenProviderPageOptions = {},\n): Promise<ProviderPageWay> {\n const win = options.window ?? window;\n let tab: Window | null;\n try {\n tab = win.open(\"about:blank\", \"_blank\");\n } catch {\n tab = null;\n }\n if (tab) {\n try {\n // The provider's page must not reach back into the app.\n tab.opener = null;\n } catch {\n /* a browser that guards `opener`: the tab is still ours to point */\n }\n }\n let url: string;\n try {\n url = await fetchUrl();\n } catch (error) {\n tab?.close();\n throw error;\n }\n if (tab && !tab.closed) {\n tab.location.href = url;\n return \"new-tab\";\n }\n win.location.assign(url);\n return \"same-tab\";\n}\n"],"mappings":";AA0CA,eAAsB,iBACpB,UACA,UAAmC,CAAC,GACV;AAC1B,QAAM,MAAM,QAAQ,UAAU;AAC9B,MAAI;AACJ,MAAI;AACF,UAAM,IAAI,KAAK,eAAe,QAAQ;AAAA,EACxC,QAAQ;AACN,UAAM;AAAA,EACR;AACA,MAAI,KAAK;AACP,QAAI;AAEF,UAAI,SAAS;AAAA,IACf,QAAQ;AAAA,IAER;AAAA,EACF;AACA,MAAI;AACJ,MAAI;AACF,UAAM,MAAM,SAAS;AAAA,EACvB,SAAS,OAAO;AACd,SAAK,MAAM;AACX,UAAM;AAAA,EACR;AACA,MAAI,OAAO,CAAC,IAAI,QAAQ;AACtB,QAAI,SAAS,OAAO;AACpB,WAAO;AAAA,EACT;AACA,MAAI,SAAS,OAAO,GAAG;AACvB,SAAO;AACT;","names":[]}
@@ -1,7 +1,13 @@
1
1
  import type { ComponentPropsWithRef } from "react";
2
2
  import type { LabelOverride } from "../i18n/kit-labels.js";
3
3
  import type { BillingLabels } from "./billing-labels.js";
4
- import type { PortalTarget } from "./billing-overview.js";
4
+ import type { BillingPortalAccess, PortalTarget } from "./billing-overview.js";
5
+ import type { CancelRequested } from "./cancel-request.js";
6
+ /** The portal action's mark (§17.3): the external-link glyph after the words, and "(opens
7
+ * in a new tab)" for a reader — on a link, `Button`'s `external` says the latter. */
8
+ export declare function ExternalMark({ reader }: {
9
+ reader?: string;
10
+ }): import("react").JSX.Element;
5
11
  export interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<"div">, "children"> {
6
12
  /** "Payment and invoices", as a link — or {@link onManage}. Without either, none. */
7
13
  manageHref?: string;
@@ -30,8 +36,54 @@ export interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<"di
30
36
  * (§12.26). Default true (0.32's behaviour).
31
37
  */
32
38
  atProvider?: boolean;
33
- /** Which of the two is under way: its spinner, no second press. */
34
- pending?: "manage" | "cancel" | null;
39
+ /**
40
+ * 0.42 (§16.2, server-kit 0.14): the overview's `portal`. `"buyer_only"` — the row has
41
+ * a buyer and the viewer isn't them, so the portal would answer `403
42
+ * billing_portal_buyer_only`: "Payment and invoices" becomes a caption naming the buyer
43
+ * (`buyerHolds`, no button), and "Cancel subscription" calls {@link onCancelInApp}
44
+ * (no portal at all — `cancelHref`, `onCancel` and `onPortal` are not used). `"open"`
45
+ * (default): as before.
46
+ */
47
+ portal?: BillingPortalAccess;
48
+ /** 0.42: the overview's `buyer_name`, for the caption under `"buyer_only"`. Empty: "…with
49
+ * the admin who bought the subscription". */
50
+ buyerName?: string | null;
51
+ /**
52
+ * 0.42 (§16.2 decision 2): "Cancel subscription" in the app — open
53
+ * `CancelSubscriptionConfirm`, whose Confirm is `POST /billing/cancel`. The cancel under
54
+ * `"buyer_only"`. Under `"open"` it is the last resort, used only when no portal cancel
55
+ * (`cancelHref`, `onCancel`, `onPortal`) is given — a person-payer app that mounts the
56
+ * route and not the portal's cancel.
57
+ */
58
+ onCancelInApp?: () => void;
59
+ /** 0.42 (§16.7 C): the overview's `cancel_at_period_end` — a cancel is scheduled. With
60
+ * {@link onResume}, "Keep subscription" takes the cancel's place. */
61
+ cancelAtPeriodEnd?: boolean;
62
+ /** 0.42 (§16.7 C): "Keep subscription", the app's `POST /billing/resume` — shown in
63
+ * the cancel's place while {@link cancelAtPeriodEnd} is true. Without it, the cancel
64
+ * stays as before. */
65
+ onResume?: () => void;
66
+ /**
67
+ * 0.43 (kastlan `18bf4c0`): what was asked in the app and the overview does not show
68
+ * yet — `useCancelRequest().view(row).requested`. `"cancel"`: shown as scheduled ("Keep
69
+ * subscription" with {@link onResume}; without it, no second "Cancel subscription").
70
+ * `"resume"`: shown as not scheduled. `null` (default): {@link cancelAtPeriodEnd} says.
71
+ */
72
+ cancelRequested?: CancelRequested;
73
+ /**
74
+ * 0.43 (§17): the portal opens in a NEW TAB — the app's handler calls
75
+ * `openProviderPage` — so its actions carry the external-link mark, a link opens with
76
+ * `target="_blank"`, and the `portalOpensNewTab` note shows under them. Default true;
77
+ * `false` for an app whose portal still opens in the same tab.
78
+ */
79
+ portalNewTab?: boolean;
80
+ /** 0.43: the provider's name in the note. Default `"Paddle"` (§2 decision 16). */
81
+ providerName?: string;
82
+ /** 0.43: the app's name in the note ("Kastlan stays open here"). Without it, "this page
83
+ * stays open". */
84
+ appName?: string;
85
+ /** Which one is under way: its spinner, no second press. `"resume"` since 0.42. */
86
+ pending?: "manage" | "cancel" | "resume" | null;
35
87
  /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */
36
88
  labels?: LabelOverride<BillingLabels>;
37
89
  }
@@ -50,5 +102,17 @@ export interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<"di
50
102
  * Before the payer has reached the provider (`atProvider={false}`, a trial without a
51
103
  * card, a beta) there is no portal to open: the part says so in a caption instead of
52
104
  * offering two buttons that could only end in `409 billing_not_at_provider`.
105
+ *
106
+ * 0.42 (§16): where the company's buyer is another admin (`portal="buyer_only"`), the
107
+ * portal stays the buyer's — "Payment and invoices" is a caption that names them, and
108
+ * "Cancel subscription" is the in-app cancel (`onCancelInApp`). While a cancel is
109
+ * scheduled (`cancelAtPeriodEnd`) and `onResume` is given, "Keep subscription" stands in
110
+ * the cancel's place, whoever the viewer is.
111
+ *
112
+ * 0.43 (§17): the portal opens in a new tab (`openProviderPage` in the app's handler):
113
+ * its actions carry the external-link mark, and a muted note says so — "Payment and
114
+ * invoices open at {provider} in a new tab; {app} stays open here." The in-app cancel and
115
+ * "Keep subscription" stay unmarked. After an in-app cancel or resume, `cancelRequested`
116
+ * (`useCancelRequest`) shows what was asked until the provider's webhook lands.
53
117
  */
54
- export declare function SubscriptionActions({ manageHref, onManage, cancelHref, onCancel, onPortal, atProvider, pending, labels: labelsProp, className, ...rest }: SubscriptionActionsProps): import("react").JSX.Element | null;
118
+ export declare function SubscriptionActions({ manageHref, onManage, cancelHref, onCancel, onPortal, atProvider, portal, buyerName, onCancelInApp, cancelAtPeriodEnd, onResume, cancelRequested, portalNewTab, providerName, appName, pending, labels: labelsProp, className, ...rest }: SubscriptionActionsProps): import("react").JSX.Element | null;
@@ -1,8 +1,23 @@
1
1
  "use client";
2
- import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { ExternalLink } from "lucide-react";
4
+ import { DEFAULT_COMMON_LABELS, useKitLabels } from "../i18n/kit-labels.js";
3
5
  import { cn } from "../lib/cn.js";
4
6
  import { Button } from "../components/button.js";
5
7
  import { useBillingLabels } from "./billing-labels.js";
8
+ function ExternalMark({ reader }) {
9
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
10
+ /* @__PURE__ */ jsx(ExternalLink, { "aria-hidden": true, "data-external": "", className: "size-4 shrink-0 rtl:-scale-x-100" }),
11
+ reader !== void 0 && /* @__PURE__ */ jsxs(Fragment, { children: [
12
+ " ",
13
+ /* @__PURE__ */ jsxs("span", { className: "sr-only", children: [
14
+ "(",
15
+ reader,
16
+ ")"
17
+ ] })
18
+ ] })
19
+ ] });
20
+ }
6
21
  function SubscriptionActions({
7
22
  manageHref,
8
23
  onManage,
@@ -10,26 +25,71 @@ function SubscriptionActions({
10
25
  onCancel,
11
26
  onPortal,
12
27
  atProvider = true,
28
+ portal = "open",
29
+ buyerName,
30
+ onCancelInApp,
31
+ cancelAtPeriodEnd = false,
32
+ onResume,
33
+ cancelRequested = null,
34
+ portalNewTab = true,
35
+ providerName = "Paddle",
36
+ appName,
13
37
  pending,
14
38
  labels: labelsProp,
15
39
  className,
16
40
  ...rest
17
41
  }) {
18
42
  const labels = useBillingLabels(labelsProp);
43
+ const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
19
44
  if (!atProvider) {
20
45
  return /* @__PURE__ */ jsx("div", { ...rest, "data-at-provider": "false", className: cn("text-sm text-muted", className), children: /* @__PURE__ */ jsx("p", { children: labels.notAtProvider }) });
21
46
  }
47
+ const buyerOnly = portal === "buyer_only";
22
48
  const manageCall = onManage ?? (onPortal ? () => onPortal("overview") : void 0);
23
- const cancelCall = onCancel ?? (onPortal ? () => onPortal("cancel") : void 0);
24
- const manage = manageHref !== void 0 ? /* @__PURE__ */ jsx(Button, { href: manageHref, variant: "secondary", children: labels.manage }) : manageCall !== void 0 ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "secondary", onClick: manageCall, pending: pending === "manage", children: labels.manage }) : null;
25
- const cancel = cancelHref !== void 0 ? /* @__PURE__ */ jsx(Button, { href: cancelHref, variant: "link", tone: "danger", children: labels.cancel }) : cancelCall !== void 0 ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", tone: "danger", onClick: cancelCall, pending: pending === "cancel", children: labels.cancel }) : null;
49
+ const portalCancel = onCancel ?? (onPortal ? () => onPortal("cancel") : void 0);
50
+ const cancelCall = buyerOnly ? onCancelInApp : portalCancel ?? (cancelHref === void 0 ? onCancelInApp : void 0);
51
+ const scheduled = cancelRequested === "cancel" ? true : cancelRequested === "resume" ? false : cancelAtPeriodEnd;
52
+ const newTab = portalNewTab && !buyerOnly;
53
+ const words = (text, external, link) => external ? /* @__PURE__ */ jsxs(Fragment, { children: [
54
+ text,
55
+ /* @__PURE__ */ jsx(ExternalMark, { reader: link ? void 0 : common.opensInNewTab })
56
+ ] }) : text;
57
+ const portalCancelShown = !buyerOnly && (cancelHref !== void 0 || portalCancel !== void 0);
58
+ const manage = buyerOnly ? /* @__PURE__ */ jsx("p", { className: "basis-full text-sm text-muted", "data-buyer-holds": "", children: labels.buyerHolds(buyerName || void 0) }) : manageHref !== void 0 ? /* @__PURE__ */ jsx(Button, { href: manageHref, variant: "secondary", external: newTab || void 0, "data-portal-action": "manage", children: words(labels.manage, newTab, true) }) : manageCall !== void 0 ? /* @__PURE__ */ jsx(
59
+ Button,
60
+ {
61
+ type: "button",
62
+ variant: "secondary",
63
+ onClick: manageCall,
64
+ pending: pending === "manage",
65
+ className: newTab ? "relative" : void 0,
66
+ "data-portal-action": "manage",
67
+ children: words(labels.manage, newTab, false)
68
+ }
69
+ ) : null;
70
+ const cancel = scheduled && onResume !== void 0 ? /* @__PURE__ */ jsx(Button, { type: "button", variant: "link", onClick: onResume, pending: pending === "resume", "data-keep-subscription": "", children: labels.keepSubscription }) : cancelRequested === "cancel" ? null : cancelHref !== void 0 && !buyerOnly ? /* @__PURE__ */ jsx(Button, { href: cancelHref, variant: "link", tone: "danger", external: newTab || void 0, "data-portal-action": "cancel", children: words(labels.cancel, newTab, true) }) : cancelCall !== void 0 ? /* @__PURE__ */ jsx(
71
+ Button,
72
+ {
73
+ type: "button",
74
+ variant: "link",
75
+ tone: "danger",
76
+ onClick: cancelCall,
77
+ pending: pending === "cancel",
78
+ className: newTab && portalCancelShown ? "relative" : void 0,
79
+ "data-portal-action": portalCancelShown ? "cancel" : void 0,
80
+ children: words(labels.cancel, newTab && portalCancelShown, false)
81
+ }
82
+ ) : null;
26
83
  if (!manage && !cancel) return null;
84
+ const portalShown = !buyerOnly && (manage !== null || cancel !== null && portalCancelShown && !(scheduled && onResume));
27
85
  return /* @__PURE__ */ jsxs("div", { ...rest, className: cn("flex flex-wrap items-center gap-x-4 gap-y-2", className), children: [
28
86
  manage,
29
- cancel
87
+ cancel,
88
+ newTab && portalShown && /* @__PURE__ */ jsx("p", { className: "basis-full text-sm text-muted", "data-portal-note": "", children: labels.portalOpensNewTab(providerName, appName || void 0) })
30
89
  ] });
31
90
  }
32
91
  export {
92
+ ExternalMark,
33
93
  SubscriptionActions
34
94
  };
35
95
  //# sourceMappingURL=subscription-actions.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/billing/subscription-actions.tsx"],"sourcesContent":["import type { ComponentPropsWithRef } from \"react\";\n\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"../components/button\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { PortalTarget } from \"./billing-overview\";\n\nexport interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /** \"Payment and invoices\", as a link — or {@link onManage}. Without either, none. */\n manageHref?: string;\n /** \"Payment and invoices\", as the app's call: `POST /billing/portal` with\n * `{target: \"overview\"}`, then off to its `url` (§4, §14.5). The kit sends no\n * request. */\n onManage?: () => void;\n /** \"Cancel subscription\", as a link — or {@link onCancel}. Without either, none. */\n cancelHref?: string;\n /** \"Cancel subscription\", as the app's call: the portal again, with `{target:\n * \"cancel\"}` — its cancel link, where the provider's cancel and withdrawal functions\n * are (§12.26, §14.5). Not the overview. */\n onCancel?: () => void;\n /**\n * Both, as one call that says where to ({@link PortalTarget}, the body of\n * `POST /billing/portal`): `\"overview\"` for \"Payment and invoices\",\n * `\"cancel\"` for \"Cancel subscription\" — so the target can't be mixed up (kastlan's\n * cancel opened the overview). {@link onManage} and {@link onCancel} win over it.\n */\n onPortal?: (target: PortalTarget) => void;\n /**\n * The overview's `at_provider` (§14.5): whether the payer has reached the provider.\n * `false`: no customer there yet, so nothing to open — a caption (`notAtProvider`)\n * stands in place of both actions. `true`: both show, WHATEVER the status or source —\n * a payer who bought under a grant keeps the way to the invoices and to \"Cancel\"\n * (§12.26). Default true (0.32's behaviour).\n */\n atProvider?: boolean;\n /** Which of the two is under way: its spinner, no second press. */\n pending?: \"manage\" | \"cancel\" | null;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * The two ways to the provider's hosted portal on the subscription page\n * (docs/billing-harmonization.md §2.8, §7, §12.26): \"Payment and invoices\" — the payment\n * method and the invoices live there, never in the app — and a VISIBLE \"Cancel\n * subscription\". Consumer law wants the second in plain sight: Germany's cancellation\n * button (§312k BGB) and the EU withdrawal function (Directive (EU) 2023/2673, from\n * 19 June 2026) are in the portal, and the app's page must not hide the way there behind\n * \"manage\".\n *\n * Neither takes the write lock: a read-only payer must be able to pay, leave or cancel\n * (§3.3).\n *\n * Before the payer has reached the provider (`atProvider={false}`, a trial without a\n * card, a beta) there is no portal to open: the part says so in a caption instead of\n * offering two buttons that could only end in `409 billing_not_at_provider`.\n */\nexport function SubscriptionActions({\n manageHref,\n onManage,\n cancelHref,\n onCancel,\n onPortal,\n atProvider = true,\n pending,\n labels: labelsProp,\n className,\n ...rest\n}: SubscriptionActionsProps) {\n const labels = useBillingLabels(labelsProp);\n if (!atProvider) {\n return (\n <div {...rest} data-at-provider=\"false\" className={cn(\"text-sm text-muted\", className)}>\n <p>{labels.notAtProvider}</p>\n </div>\n );\n }\n const manageCall = onManage ?? (onPortal ? () => onPortal(\"overview\") : undefined);\n const cancelCall = onCancel ?? (onPortal ? () => onPortal(\"cancel\") : undefined);\n const manage =\n manageHref !== undefined ? (\n <Button href={manageHref} variant=\"secondary\">\n {labels.manage}\n </Button>\n ) : manageCall !== undefined ? (\n <Button type=\"button\" variant=\"secondary\" onClick={manageCall} pending={pending === \"manage\"}>\n {labels.manage}\n </Button>\n ) : null;\n const cancel =\n cancelHref !== undefined ? (\n <Button href={cancelHref} variant=\"link\" tone=\"danger\">\n {labels.cancel}\n </Button>\n ) : cancelCall !== undefined ? (\n <Button type=\"button\" variant=\"link\" tone=\"danger\" onClick={cancelCall} pending={pending === \"cancel\"}>\n {labels.cancel}\n </Button>\n ) : null;\n if (!manage && !cancel) return null;\n return (\n <div {...rest} className={cn(\"flex flex-wrap items-center gap-x-4 gap-y-2\", className)}>\n {manage}\n {cancel}\n </div>\n );\n}\n"],"mappings":";AA2EQ,cA4BJ,YA5BI;AAxER,SAAS,UAAU;AACnB,SAAS,cAAc;AACvB,SAAS,wBAAwB;AAsD1B,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAA6B;AAC3B,QAAM,SAAS,iBAAiB,UAAU;AAC1C,MAAI,CAAC,YAAY;AACf,WACE,oBAAC,SAAK,GAAG,MAAM,oBAAiB,SAAQ,WAAW,GAAG,sBAAsB,SAAS,GACnF,8BAAC,OAAG,iBAAO,eAAc,GAC3B;AAAA,EAEJ;AACA,QAAM,aAAa,aAAa,WAAW,MAAM,SAAS,UAAU,IAAI;AACxE,QAAM,aAAa,aAAa,WAAW,MAAM,SAAS,QAAQ,IAAI;AACtE,QAAM,SACJ,eAAe,SACb,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAC/B,iBAAO,QACV,IACE,eAAe,SACjB,oBAAC,UAAO,MAAK,UAAS,SAAQ,aAAY,SAAS,YAAY,SAAS,YAAY,UACjF,iBAAO,QACV,IACE;AACN,QAAM,SACJ,eAAe,SACb,oBAAC,UAAO,MAAM,YAAY,SAAQ,QAAO,MAAK,UAC3C,iBAAO,QACV,IACE,eAAe,SACjB,oBAAC,UAAO,MAAK,UAAS,SAAQ,QAAO,MAAK,UAAS,SAAS,YAAY,SAAS,YAAY,UAC1F,iBAAO,QACV,IACE;AACN,MAAI,CAAC,UAAU,CAAC,OAAQ,QAAO;AAC/B,SACE,qBAAC,SAAK,GAAG,MAAM,WAAW,GAAG,+CAA+C,SAAS,GAClF;AAAA;AAAA,IACA;AAAA,KACH;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/billing/subscription-actions.tsx"],"sourcesContent":["import type { ComponentPropsWithRef, ReactNode } from \"react\";\nimport { ExternalLink } from \"lucide-react\";\n\nimport { DEFAULT_COMMON_LABELS, useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { cn } from \"../lib/cn\";\nimport { Button } from \"../components/button\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport type { BillingPortalAccess, PortalTarget } from \"./billing-overview\";\nimport type { CancelRequested } from \"./cancel-request\";\n\n/** The portal action's mark (§17.3): the external-link glyph after the words, and \"(opens\n * in a new tab)\" for a reader — on a link, `Button`'s `external` says the latter. */\nexport function ExternalMark({ reader }: { reader?: string }) {\n return (\n <>\n <ExternalLink aria-hidden data-external=\"\" className=\"size-4 shrink-0 rtl:-scale-x-100\" />\n {reader !== undefined && (\n <>\n {\" \"}\n <span className=\"sr-only\">({reader})</span>\n </>\n )}\n </>\n );\n}\n\nexport interface SubscriptionActionsProps extends Omit<ComponentPropsWithRef<\"div\">, \"children\"> {\n /** \"Payment and invoices\", as a link — or {@link onManage}. Without either, none. */\n manageHref?: string;\n /** \"Payment and invoices\", as the app's call: `POST /billing/portal` with\n * `{target: \"overview\"}`, then off to its `url` (§4, §14.5). The kit sends no\n * request. */\n onManage?: () => void;\n /** \"Cancel subscription\", as a link — or {@link onCancel}. Without either, none. */\n cancelHref?: string;\n /** \"Cancel subscription\", as the app's call: the portal again, with `{target:\n * \"cancel\"}` — its cancel link, where the provider's cancel and withdrawal functions\n * are (§12.26, §14.5). Not the overview. */\n onCancel?: () => void;\n /**\n * Both, as one call that says where to ({@link PortalTarget}, the body of\n * `POST /billing/portal`): `\"overview\"` for \"Payment and invoices\",\n * `\"cancel\"` for \"Cancel subscription\" — so the target can't be mixed up (kastlan's\n * cancel opened the overview). {@link onManage} and {@link onCancel} win over it.\n */\n onPortal?: (target: PortalTarget) => void;\n /**\n * The overview's `at_provider` (§14.5): whether the payer has reached the provider.\n * `false`: no customer there yet, so nothing to open — a caption (`notAtProvider`)\n * stands in place of both actions. `true`: both show, WHATEVER the status or source —\n * a payer who bought under a grant keeps the way to the invoices and to \"Cancel\"\n * (§12.26). Default true (0.32's behaviour).\n */\n atProvider?: boolean;\n /**\n * 0.42 (§16.2, server-kit 0.14): the overview's `portal`. `\"buyer_only\"` — the row has\n * a buyer and the viewer isn't them, so the portal would answer `403\n * billing_portal_buyer_only`: \"Payment and invoices\" becomes a caption naming the buyer\n * (`buyerHolds`, no button), and \"Cancel subscription\" calls {@link onCancelInApp}\n * (no portal at all — `cancelHref`, `onCancel` and `onPortal` are not used). `\"open\"`\n * (default): as before.\n */\n portal?: BillingPortalAccess;\n /** 0.42: the overview's `buyer_name`, for the caption under `\"buyer_only\"`. Empty: \"…with\n * the admin who bought the subscription\". */\n buyerName?: string | null;\n /**\n * 0.42 (§16.2 decision 2): \"Cancel subscription\" in the app — open\n * `CancelSubscriptionConfirm`, whose Confirm is `POST /billing/cancel`. The cancel under\n * `\"buyer_only\"`. Under `\"open\"` it is the last resort, used only when no portal cancel\n * (`cancelHref`, `onCancel`, `onPortal`) is given — a person-payer app that mounts the\n * route and not the portal's cancel.\n */\n onCancelInApp?: () => void;\n /** 0.42 (§16.7 C): the overview's `cancel_at_period_end` — a cancel is scheduled. With\n * {@link onResume}, \"Keep subscription\" takes the cancel's place. */\n cancelAtPeriodEnd?: boolean;\n /** 0.42 (§16.7 C): \"Keep subscription\", the app's `POST /billing/resume` — shown in\n * the cancel's place while {@link cancelAtPeriodEnd} is true. Without it, the cancel\n * stays as before. */\n onResume?: () => void;\n /**\n * 0.43 (kastlan `18bf4c0`): what was asked in the app and the overview does not show\n * yet — `useCancelRequest().view(row).requested`. `\"cancel\"`: shown as scheduled (\"Keep\n * subscription\" with {@link onResume}; without it, no second \"Cancel subscription\").\n * `\"resume\"`: shown as not scheduled. `null` (default): {@link cancelAtPeriodEnd} says.\n */\n cancelRequested?: CancelRequested;\n /**\n * 0.43 (§17): the portal opens in a NEW TAB — the app's handler calls\n * `openProviderPage` — so its actions carry the external-link mark, a link opens with\n * `target=\"_blank\"`, and the `portalOpensNewTab` note shows under them. Default true;\n * `false` for an app whose portal still opens in the same tab.\n */\n portalNewTab?: boolean;\n /** 0.43: the provider's name in the note. Default `\"Paddle\"` (§2 decision 16). */\n providerName?: string;\n /** 0.43: the app's name in the note (\"Kastlan stays open here\"). Without it, \"this page\n * stays open\". */\n appName?: string;\n /** Which one is under way: its spinner, no second press. `\"resume\"` since 0.42. */\n pending?: \"manage\" | \"cancel\" | \"resume\" | null;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * The two ways to the provider's hosted portal on the subscription page\n * (docs/billing-harmonization.md §2.8, §7, §12.26): \"Payment and invoices\" — the payment\n * method and the invoices live there, never in the app — and a VISIBLE \"Cancel\n * subscription\". Consumer law wants the second in plain sight: Germany's cancellation\n * button (§312k BGB) and the EU withdrawal function (Directive (EU) 2023/2673, from\n * 19 June 2026) are in the portal, and the app's page must not hide the way there behind\n * \"manage\".\n *\n * Neither takes the write lock: a read-only payer must be able to pay, leave or cancel\n * (§3.3).\n *\n * Before the payer has reached the provider (`atProvider={false}`, a trial without a\n * card, a beta) there is no portal to open: the part says so in a caption instead of\n * offering two buttons that could only end in `409 billing_not_at_provider`.\n *\n * 0.42 (§16): where the company's buyer is another admin (`portal=\"buyer_only\"`), the\n * portal stays the buyer's — \"Payment and invoices\" is a caption that names them, and\n * \"Cancel subscription\" is the in-app cancel (`onCancelInApp`). While a cancel is\n * scheduled (`cancelAtPeriodEnd`) and `onResume` is given, \"Keep subscription\" stands in\n * the cancel's place, whoever the viewer is.\n *\n * 0.43 (§17): the portal opens in a new tab (`openProviderPage` in the app's handler):\n * its actions carry the external-link mark, and a muted note says so — \"Payment and\n * invoices open at {provider} in a new tab; {app} stays open here.\" The in-app cancel and\n * \"Keep subscription\" stay unmarked. After an in-app cancel or resume, `cancelRequested`\n * (`useCancelRequest`) shows what was asked until the provider's webhook lands.\n */\nexport function SubscriptionActions({\n manageHref,\n onManage,\n cancelHref,\n onCancel,\n onPortal,\n atProvider = true,\n portal = \"open\",\n buyerName,\n onCancelInApp,\n cancelAtPeriodEnd = false,\n onResume,\n cancelRequested = null,\n portalNewTab = true,\n providerName = \"Paddle\",\n appName,\n pending,\n labels: labelsProp,\n className,\n ...rest\n}: SubscriptionActionsProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n if (!atProvider) {\n return (\n <div {...rest} data-at-provider=\"false\" className={cn(\"text-sm text-muted\", className)}>\n <p>{labels.notAtProvider}</p>\n </div>\n );\n }\n const buyerOnly = portal === \"buyer_only\";\n const manageCall = onManage ?? (onPortal ? () => onPortal(\"overview\") : undefined);\n const portalCancel = onCancel ?? (onPortal ? () => onPortal(\"cancel\") : undefined);\n // Under \"buyer_only\" the in-app cancel only; under \"open\" the portal's first, the\n // in-app cancel only where no way to the portal's is given.\n const cancelCall = buyerOnly ? onCancelInApp : (portalCancel ?? (cancelHref === undefined ? onCancelInApp : undefined));\n // 0.43: what was asked in the app wins over the row until the row moves.\n const scheduled = cancelRequested === \"cancel\" ? true : cancelRequested === \"resume\" ? false : cancelAtPeriodEnd;\n // 0.43 (§17): the portal's actions are marked, a link opens a tab of its own.\n const newTab = portalNewTab && !buyerOnly;\n const words = (text: string, external: boolean, link: boolean): ReactNode =>\n external ? (\n <>\n {text}\n <ExternalMark reader={link ? undefined : common.opensInNewTab} />\n </>\n ) : (\n text\n );\n const portalCancelShown = !buyerOnly && (cancelHref !== undefined || portalCancel !== undefined);\n const manage = buyerOnly ? (\n <p className=\"basis-full text-sm text-muted\" data-buyer-holds=\"\">\n {labels.buyerHolds(buyerName || undefined)}\n </p>\n ) : manageHref !== undefined ? (\n <Button href={manageHref} variant=\"secondary\" external={newTab || undefined} data-portal-action=\"manage\">\n {words(labels.manage, newTab, true)}\n </Button>\n ) : manageCall !== undefined ? (\n <Button\n type=\"button\"\n variant=\"secondary\"\n onClick={manageCall}\n pending={pending === \"manage\"}\n className={newTab ? \"relative\" : undefined}\n data-portal-action=\"manage\"\n >\n {words(labels.manage, newTab, false)}\n </Button>\n ) : null;\n const cancel =\n scheduled && onResume !== undefined ? (\n <Button type=\"button\" variant=\"link\" onClick={onResume} pending={pending === \"resume\"} data-keep-subscription=\"\">\n {labels.keepSubscription}\n </Button>\n ) : cancelRequested === \"cancel\" ? null : cancelHref !== undefined && !buyerOnly ? (\n <Button href={cancelHref} variant=\"link\" tone=\"danger\" external={newTab || undefined} data-portal-action=\"cancel\">\n {words(labels.cancel, newTab, true)}\n </Button>\n ) : cancelCall !== undefined ? (\n <Button\n type=\"button\"\n variant=\"link\"\n tone=\"danger\"\n onClick={cancelCall}\n pending={pending === \"cancel\"}\n className={newTab && portalCancelShown ? \"relative\" : undefined}\n data-portal-action={portalCancelShown ? \"cancel\" : undefined}\n >\n {words(labels.cancel, newTab && portalCancelShown, false)}\n </Button>\n ) : null;\n if (!manage && !cancel) return null;\n // The note while a way to the portal shows: \"Payment and invoices open at Paddle in a\n // new tab; Kastlan stays open here.\"\n const portalShown = !buyerOnly && (manage !== null || (cancel !== null && portalCancelShown && !(scheduled && onResume)));\n return (\n <div {...rest} className={cn(\"flex flex-wrap items-center gap-x-4 gap-y-2\", className)}>\n {manage}\n {cancel}\n {newTab && portalShown && (\n <p className=\"basis-full text-sm text-muted\" data-portal-note=\"\">\n {labels.portalOpensNewTab(providerName, appName || undefined)}\n </p>\n )}\n </div>\n );\n}\n"],"mappings":"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","names":[]}
@@ -2,7 +2,7 @@ import type { ReactElement, ReactNode } from "react";
2
2
  import type { LucideIcon } from "lucide-react";
3
3
  import type { DateInput, FormatDateStyle, FormatRelativeTimeOptions } from "../lib/format.js";
4
4
  import { type LabelOverride } from "../i18n/kit-labels.js";
5
- import type { ChipShape, ChipSize, ChipTone, ChipVariant } from "./chip.js";
5
+ import type { ChipLinkProps, ChipShape, ChipSize, ChipTone, ChipVariant } from "./chip.js";
6
6
  import type { DataTableColumn } from "./data-table.js";
7
7
  import { DEFAULT_ACCOUNT_STATE_LABELS } from "./account-chips-labels.js";
8
8
  /**
@@ -38,8 +38,29 @@ interface AccountChipLook {
38
38
  export interface RoleDefinition {
39
39
  label: string;
40
40
  tone?: ChipTone;
41
- /** A lucide component or an element — Kurvenschmiede marks its admins with a shield. */
41
+ /**
42
+ * A lucide component or an element — Kurvenschmiede marks its admins with a shield.
43
+ * Unlike {@link href} and {@link variant}, it shows EVERYWHERE the role is drawn: in
44
+ * {@link AccountRoleChip} and in {@link RoleChip} (the roster's column) alike. That is
45
+ * intended (0.43, keksdose's question): the glyph is part of how the role reads, so the
46
+ * shield on the header's chip is the shield in the users list. An app that wants the
47
+ * glyph in one place only hands the other a second vocabulary without `icon`.
48
+ */
42
49
  icon?: LucideIcon | ReactElement;
50
+ /**
51
+ * 0.42.0 (docs/admin-harmonization.md §2.1, settings §12.3): the role's tool — an
52
+ * admin's `/admin`, a reviewer's `/translations`. {@link AccountRoleChip} is a link to
53
+ * it; a role without one gets a plain chip. {@link RoleChip} ignores it: a roster's
54
+ * chips name a role, they are not a way in.
55
+ */
56
+ href?: string;
57
+ /**
58
+ * 0.42.0 (keksdose build review): this role's look in {@link AccountRoleChip} — keksdose
59
+ * draws only ADMIN `solid` and its REVIEWER and MEMBER `soft`. Default `solid`, the
60
+ * header's look; the chip's own `variant` prop wins over it. {@link RoleChip} ignores
61
+ * it: a roster's column keeps one look, set on the column.
62
+ */
63
+ variant?: ChipVariant;
43
64
  }
44
65
  /**
45
66
  * Every role an app has, keyed by the value its API sends. Annotate the app's table
@@ -67,6 +88,33 @@ export interface RoleChipProps<R extends string = string> extends AccountChipLoo
67
88
  * style hook.
68
89
  */
69
90
  export declare function RoleChip<R extends string = string>({ value: role, roles, size, variant, shape, caps, className, ...rest }: RoleChipProps<R>): import("react").JSX.Element | null;
91
+ export interface AccountRoleChipProps<R extends string = string> extends RoleChipProps<R> {
92
+ /**
93
+ * Runs on the way to the role's tool — the account menu's `close`, so the panel does
94
+ * not stay open over the page it just navigated to (keksdose F7). Only called on a
95
+ * link chip.
96
+ */
97
+ onNavigate?: () => void;
98
+ /** Your router's link for the role's `href`. Default: the `<UiKitProvider
99
+ * linkComponent>`, then `<a>` — {@link Chip}'s `renderLink`. */
100
+ renderLink?: (props: ChipLinkProps) => ReactElement;
101
+ }
102
+ /**
103
+ * The role in the account menu's header (0.42.0, docs/admin-harmonization.md §2.1, §7):
104
+ * {@link RoleChip} in keksdose's look — `solid`, `sm`, `square`, `caps`, the role's icon
105
+ * and tone — made for `TopBarActionMenu`'s `header.extra`:
106
+ *
107
+ * extra: (close) => <AccountRoleChip value={me.role} roles={ROLES} onNavigate={close} />
108
+ *
109
+ * A role whose {@link RoleDefinition.href} is set is a shortcut to its tool: the chip is a
110
+ * link (through `renderLink`, else the provider's link, as {@link Chip} draws links) and
111
+ * calls `onNavigate` on the click. Any other role is a plain chip — every account shows
112
+ * its role (§7, "Always a badge"). The words and tones are the app's vocabulary; the kit
113
+ * adds none. `solid` stays the default, so a role the app gives no look still reads as
114
+ * the header's tag; a role that should sit quieter (keksdose's MEMBER and REVIEWER) sets
115
+ * {@link RoleDefinition.variant} `soft` in the vocabulary. Carries `data-role` with the key.
116
+ */
117
+ export declare function AccountRoleChip<R extends string = string>({ value: role, roles, onNavigate, renderLink, size, variant, shape, caps, className, ...rest }: AccountRoleChipProps<R>): import("react").JSX.Element | null;
70
118
  /**
71
119
  * The states both rosters show, each as the app's own flag maps onto it:
72
120
  *
@@ -34,6 +34,38 @@ function RoleChip({
34
34
  }
35
35
  );
36
36
  }
37
+ function AccountRoleChip({
38
+ value: role,
39
+ roles,
40
+ onNavigate,
41
+ renderLink,
42
+ size = "sm",
43
+ variant,
44
+ shape = "square",
45
+ caps = true,
46
+ className,
47
+ ...rest
48
+ }) {
49
+ if (role === null || role === void 0 || role === "") return null;
50
+ const def = roles[role];
51
+ const look = {
52
+ ...rest,
53
+ "data-role": role,
54
+ tone: def?.tone ?? "neutral",
55
+ icon: def?.icon,
56
+ size,
57
+ // The prop, then the role's own look, then the header's `solid`.
58
+ variant: variant ?? def?.variant ?? "solid",
59
+ shape,
60
+ caps,
61
+ className
62
+ };
63
+ const label = def?.label ?? role;
64
+ if (def?.href) {
65
+ return /* @__PURE__ */ jsx(Chip, { ...look, href: def.href, renderLink, onClick: onNavigate ? () => onNavigate() : void 0, children: label });
66
+ }
67
+ return /* @__PURE__ */ jsx(Chip, { ...look, children: label });
68
+ }
37
69
  const ACCOUNT_STATE_TONES = {
38
70
  active: "success",
39
71
  inactive: "danger",
@@ -158,6 +190,7 @@ function dateColumn({
158
190
  }
159
191
  export {
160
192
  ACCOUNT_STATE_TONES,
193
+ AccountRoleChip,
161
194
  AccountStateChip,
162
195
  DEFAULT_ACCOUNT_STATE_LABELS,
163
196
  DateMark,