@eifi1/ui-kit 0.52.0 → 0.54.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 (290) hide show
  1. package/README.md +52 -59
  2. package/dist/account/delete-account-setting.d.ts +3 -2
  3. package/dist/account/delete-account-setting.js.map +1 -1
  4. package/dist/auth/auth-errors.d.ts +36 -0
  5. package/dist/auth/auth-errors.js +20 -0
  6. package/dist/auth/auth-errors.js.map +1 -1
  7. package/dist/billing/cancel-subscription.d.ts +5 -14
  8. package/dist/billing/cancel-subscription.js +1 -3
  9. package/dist/billing/cancel-subscription.js.map +1 -1
  10. package/dist/billing/plan-card.d.ts +5 -13
  11. package/dist/billing/plan-card.js +3 -11
  12. package/dist/billing/plan-card.js.map +1 -1
  13. package/dist/billing/plan-switch.d.ts +5 -14
  14. package/dist/billing/plan-switch.js +1 -3
  15. package/dist/billing/plan-switch.js.map +1 -1
  16. package/dist/chart.d.ts +3 -14
  17. package/dist/chart.js +64 -11
  18. package/dist/chart.js.map +1 -1
  19. package/dist/components/amount-input.d.ts +2 -1
  20. package/dist/components/amount-input.js.map +1 -1
  21. package/dist/components/calendar-heatmap.js +4 -2
  22. package/dist/components/calendar-heatmap.js.map +1 -1
  23. package/dist/components/currency-option-list.d.ts +2 -1
  24. package/dist/components/currency-option-list.js +10 -3
  25. package/dist/components/currency-option-list.js.map +1 -1
  26. package/dist/components/currency-select.d.ts +3 -1
  27. package/dist/components/currency-select.js.map +1 -1
  28. package/dist/components/danger-confirm.d.ts +22 -1
  29. package/dist/components/danger-confirm.js +50 -28
  30. package/dist/components/danger-confirm.js.map +1 -1
  31. package/dist/components/data-table-filter-popover.d.ts +2 -1
  32. package/dist/components/data-table-filter-popover.js +3 -2
  33. package/dist/components/data-table-filter-popover.js.map +1 -1
  34. package/dist/components/data-table-filters.d.ts +33 -3
  35. package/dist/components/data-table-filters.js +6 -4
  36. package/dist/components/data-table-filters.js.map +1 -1
  37. package/dist/components/date-field.d.ts +1 -1
  38. package/dist/components/date-field.js.map +1 -1
  39. package/dist/components/date-picker.d.ts +1 -1
  40. package/dist/components/date-picker.js.map +1 -1
  41. package/dist/components/date-range-picker.d.ts +4 -17
  42. package/dist/components/date-range-picker.js +2 -2
  43. package/dist/components/date-range-picker.js.map +1 -1
  44. package/dist/components/dialog-frame.d.ts +3 -1
  45. package/dist/components/dialog-frame.js +6 -2
  46. package/dist/components/dialog-frame.js.map +1 -1
  47. package/dist/components/mini-calendar.js +3 -2
  48. package/dist/components/mini-calendar.js.map +1 -1
  49. package/dist/components/modal.d.ts +27 -0
  50. package/dist/components/modal.js +5 -3
  51. package/dist/components/modal.js.map +1 -1
  52. package/dist/components/nav-pills.d.ts +1 -1
  53. package/dist/components/nav-pills.js.map +1 -1
  54. package/dist/components/number-input.js.map +1 -1
  55. package/dist/components/series-chart-axes.d.ts +1 -1
  56. package/dist/components/series-chart-axes.js.map +1 -1
  57. package/dist/components/series-chart.d.ts +64 -19
  58. package/dist/components/series-chart.js +2 -1
  59. package/dist/components/series-chart.js.map +1 -1
  60. package/dist/components/stat-tile.d.ts +21 -2
  61. package/dist/components/stat-tile.js +4 -2
  62. package/dist/components/stat-tile.js.map +1 -1
  63. package/dist/components/treemap-labels.d.ts +6 -1
  64. package/dist/components/treemap-labels.js +2 -2
  65. package/dist/components/treemap-labels.js.map +1 -1
  66. package/dist/components/treemap.d.ts +29 -4
  67. package/dist/components/treemap.js +22 -9
  68. package/dist/components/treemap.js.map +1 -1
  69. package/dist/currencies.d.ts +16 -2
  70. package/dist/currencies.js +2 -1
  71. package/dist/currencies.js.map +1 -1
  72. package/dist/data-table.d.ts +1 -6
  73. package/dist/data-table.js +1 -9
  74. package/dist/data-table.js.map +1 -1
  75. package/dist/demo/demo-countdown.d.ts +0 -5
  76. package/dist/demo/demo-countdown.js +0 -4
  77. package/dist/demo/demo-countdown.js.map +1 -1
  78. package/dist/eslint/index.js +1 -1
  79. package/dist/feedback/feedback-context-labels.js +2 -1
  80. package/dist/feedback/feedback-context-labels.js.map +1 -1
  81. package/dist/feedback/feedback-context.d.ts +19 -3
  82. package/dist/feedback/feedback-context.js +12 -1
  83. package/dist/feedback/feedback-context.js.map +1 -1
  84. package/dist/feedback/feedback-diagnostics.d.ts +17 -0
  85. package/dist/feedback/feedback-diagnostics.js +27 -0
  86. package/dist/feedback/feedback-diagnostics.js.map +1 -0
  87. package/dist/feedback/feedback-dialog.d.ts +10 -0
  88. package/dist/feedback/feedback-dialog.js +6 -4
  89. package/dist/feedback/feedback-dialog.js.map +1 -1
  90. package/dist/feedback/feedback-mutation.d.ts +11 -23
  91. package/dist/feedback/feedback-mutation.js +1 -9
  92. package/dist/feedback/feedback-mutation.js.map +1 -1
  93. package/dist/feedback/feedback-public-list.d.ts +1 -6
  94. package/dist/feedback/feedback-public-list.js +1 -2
  95. package/dist/feedback/feedback-public-list.js.map +1 -1
  96. package/dist/feedback/feedback-record.d.ts +5 -0
  97. package/dist/feedback/feedback-record.js.map +1 -1
  98. package/dist/feedback/feedback-row-detail-labels.js +1 -8
  99. package/dist/feedback/feedback-row-detail-labels.js.map +1 -1
  100. package/dist/feedback/feedback-row-detail.d.ts +6 -50
  101. package/dist/feedback/feedback-row-detail.js +3 -8
  102. package/dist/feedback/feedback-row-detail.js.map +1 -1
  103. package/dist/feedback/feedback-status-undo.d.ts +3 -19
  104. package/dist/feedback/feedback-status-undo.js.map +1 -1
  105. package/dist/feedback/feedback-submit-dialog.d.ts +5 -1
  106. package/dist/feedback/feedback-submit-dialog.js +15 -3
  107. package/dist/feedback/feedback-submit-dialog.js.map +1 -1
  108. package/dist/feedback/feedback-submit.d.ts +30 -2
  109. package/dist/feedback/feedback-submit.js +5 -2
  110. package/dist/feedback/feedback-submit.js.map +1 -1
  111. package/dist/feedback/feedback-thread.d.ts +0 -9
  112. package/dist/feedback/feedback-thread.js.map +1 -1
  113. package/dist/feedback.d.ts +6 -40
  114. package/dist/feedback.js +88 -46
  115. package/dist/feedback.js.map +1 -1
  116. package/dist/hooks/use-hotkey.js +11 -1
  117. package/dist/hooks/use-hotkey.js.map +1 -1
  118. package/dist/i18n/defaults.js +0 -4
  119. package/dist/i18n/defaults.js.map +1 -1
  120. package/dist/i18n/german.js +5 -20
  121. package/dist/i18n/german.js.map +1 -1
  122. package/dist/i18n/kit-labels.d.ts +5 -14
  123. package/dist/i18n/kit-labels.js.map +1 -1
  124. package/dist/i18n/locales/en.js +5 -31
  125. package/dist/i18n/locales/en.js.map +3 -3
  126. package/dist/i18n/locales/es.js +5 -20
  127. package/dist/i18n/locales/es.js.map +1 -1
  128. package/dist/i18n/locales/fr.js +5 -20
  129. package/dist/i18n/locales/fr.js.map +1 -1
  130. package/dist/i18n/locales/hu.js +5 -20
  131. package/dist/i18n/locales/hu.js.map +1 -1
  132. package/dist/i18n/locales/it.js +5 -21
  133. package/dist/i18n/locales/it.js.map +1 -1
  134. package/dist/i18n/locales/zh.js +5 -20
  135. package/dist/i18n/locales/zh.js.map +1 -1
  136. package/dist/i18n/review.js +9 -4
  137. package/dist/i18n/review.js.map +1 -1
  138. package/dist/i18n/shared-labels.js +0 -1
  139. package/dist/i18n/shared-labels.js.map +1 -1
  140. package/dist/index.d.ts +18 -67
  141. package/dist/index.js +28 -103
  142. package/dist/index.js.map +1 -1
  143. package/dist/lib/dates.d.ts +7 -1
  144. package/dist/lib/dates.js +8 -1
  145. package/dist/lib/dates.js.map +1 -1
  146. package/dist/lib/format.d.ts +37 -2
  147. package/dist/lib/format.js +25 -1
  148. package/dist/lib/format.js.map +1 -1
  149. package/dist/lib/intl-cache.d.ts +4 -2
  150. package/dist/lib/intl-cache.js.map +1 -1
  151. package/dist/lib/translation-review.d.ts +26 -0
  152. package/dist/lib/translation-review.js +14 -0
  153. package/dist/lib/translation-review.js.map +1 -1
  154. package/dist/rhf/fields.d.ts +2 -2
  155. package/dist/rhf/fields.js.map +1 -1
  156. package/dist/search/search-index.d.ts +0 -6
  157. package/dist/search/search-index.js +0 -4
  158. package/dist/search/search-index.js.map +1 -1
  159. package/dist/search.d.ts +1 -8
  160. package/dist/search.js +2 -7
  161. package/dist/search.js.map +1 -1
  162. package/dist/shell/account-menu-language.d.ts +7 -21
  163. package/dist/shell/account-menu-language.js +1 -3
  164. package/dist/shell/account-menu-language.js.map +1 -1
  165. package/dist/shell/account-menu-text-size.d.ts +9 -23
  166. package/dist/shell/account-menu-text-size.js +2 -3
  167. package/dist/shell/account-menu-text-size.js.map +1 -1
  168. package/dist/shell/app-shell.d.ts +0 -10
  169. package/dist/shell/app-shell.js +1 -7
  170. package/dist/shell/app-shell.js.map +1 -1
  171. package/dist/shell/company-switcher.d.ts +1 -1
  172. package/dist/shell/company-switcher.js.map +1 -1
  173. package/dist/shell/dev-menu.d.ts +1 -1
  174. package/dist/shell/dev-menu.js.map +1 -1
  175. package/dist/shell/top-bar-parts-labels.d.ts +1 -1
  176. package/dist/shell/top-bar-parts-labels.js.map +1 -1
  177. package/dist/shell.d.ts +7 -29
  178. package/dist/shell.js +12 -42
  179. package/dist/shell.js.map +1 -1
  180. package/dist/sync.d.ts +2 -6
  181. package/dist/sync.js +2 -15
  182. package/dist/sync.js.map +1 -1
  183. package/dist/theme/chart-palette.d.ts +0 -5
  184. package/dist/theme/chart-palette.js +0 -2
  185. package/dist/theme/chart-palette.js.map +1 -1
  186. package/package.json +1 -1
  187. package/src/account/delete-account-setting.tsx +3 -2
  188. package/src/auth/auth-errors.ts +69 -0
  189. package/src/billing/cancel-subscription.tsx +5 -16
  190. package/src/billing/plan-card.tsx +7 -24
  191. package/src/billing/plan-switch.tsx +5 -16
  192. package/src/chart.ts +62 -13
  193. package/src/components/amount-input.tsx +2 -1
  194. package/src/components/calendar-heatmap.tsx +8 -2
  195. package/src/components/currency-option-list.tsx +12 -4
  196. package/src/components/currency-select.tsx +3 -1
  197. package/src/components/danger-confirm.tsx +80 -27
  198. package/src/components/data-table-filter-popover.tsx +19 -11
  199. package/src/components/data-table-filters.ts +36 -8
  200. package/src/components/date-field.tsx +1 -1
  201. package/src/components/date-picker.tsx +0 -1
  202. package/src/components/date-range-picker.tsx +6 -22
  203. package/src/components/dialog-frame.tsx +12 -4
  204. package/src/components/mini-calendar.tsx +5 -2
  205. package/src/components/modal.tsx +36 -7
  206. package/src/components/nav-pills.tsx +1 -1
  207. package/src/components/number-input.tsx +4 -3
  208. package/src/components/series-chart-axes.ts +6 -1
  209. package/src/components/series-chart.tsx +78 -21
  210. package/src/components/stat-tile.tsx +32 -3
  211. package/src/components/treemap-labels.ts +11 -3
  212. package/src/components/treemap.tsx +65 -12
  213. package/src/currencies.ts +23 -3
  214. package/src/data-table.ts +1 -11
  215. package/src/demo/demo-countdown.ts +3 -8
  216. package/src/feedback/feedback-context-labels.ts +1 -0
  217. package/src/feedback/feedback-context.tsx +34 -2
  218. package/src/feedback/feedback-diagnostics.tsx +41 -0
  219. package/src/feedback/feedback-dialog.tsx +16 -4
  220. package/src/feedback/feedback-mutation.ts +12 -33
  221. package/src/feedback/feedback-public-list.tsx +1 -7
  222. package/src/feedback/feedback-record.ts +5 -0
  223. package/src/feedback/feedback-row-detail-labels.ts +1 -8
  224. package/src/feedback/feedback-row-detail.tsx +21 -58
  225. package/src/feedback/feedback-status-undo.ts +4 -18
  226. package/src/feedback/feedback-submit-dialog.tsx +15 -2
  227. package/src/feedback/feedback-submit.tsx +36 -3
  228. package/src/feedback/feedback-thread.tsx +0 -10
  229. package/src/feedback.ts +89 -48
  230. package/src/hooks/use-hotkey.ts +24 -1
  231. package/src/i18n/defaults.ts +0 -4
  232. package/src/i18n/german.ts +4 -19
  233. package/src/i18n/kit-labels.tsx +5 -14
  234. package/src/i18n/locales/es.ts +4 -19
  235. package/src/i18n/locales/fr.ts +4 -19
  236. package/src/i18n/locales/hu.ts +4 -19
  237. package/src/i18n/locales/it.ts +4 -20
  238. package/src/i18n/locales/zh.ts +4 -19
  239. package/src/i18n/review.ts +9 -4
  240. package/src/i18n/shared-labels.ts +0 -1
  241. package/src/index.ts +44 -114
  242. package/src/lib/dates.ts +24 -2
  243. package/src/lib/format.ts +66 -3
  244. package/src/lib/intl-cache.ts +4 -2
  245. package/src/lib/translation-review.ts +52 -0
  246. package/src/rhf/fields.tsx +2 -2
  247. package/src/search/search-index.ts +0 -15
  248. package/src/search.ts +8 -8
  249. package/src/shell/account-menu-language.tsx +7 -27
  250. package/src/shell/account-menu-text-size.tsx +13 -32
  251. package/src/shell/app-shell.tsx +0 -13
  252. package/src/shell/company-switcher.tsx +1 -1
  253. package/src/shell/dev-menu.tsx +1 -1
  254. package/src/shell/top-bar-parts-labels.ts +1 -1
  255. package/src/shell.ts +17 -44
  256. package/src/sync.ts +2 -15
  257. package/src/theme/chart-palette.ts +0 -2
  258. package/tokens.css +4 -4
  259. package/dist/billing/dialog-close.d.ts +0 -29
  260. package/dist/billing/dialog-close.js +0 -8
  261. package/dist/billing/dialog-close.js.map +0 -1
  262. package/dist/components/field-sync-labels.d.ts +0 -3
  263. package/dist/components/field-sync-labels.js +0 -12
  264. package/dist/components/field-sync-labels.js.map +0 -1
  265. package/dist/components/field-sync.d.ts +0 -186
  266. package/dist/components/field-sync.js +0 -269
  267. package/dist/components/field-sync.js.map +0 -1
  268. package/dist/components/section-nav.d.ts +0 -44
  269. package/dist/components/section-nav.js +0 -33
  270. package/dist/components/section-nav.js.map +0 -1
  271. package/dist/components/sign-chip-labels.d.ts +0 -3
  272. package/dist/components/sign-chip-labels.js +0 -11
  273. package/dist/components/sign-chip-labels.js.map +0 -1
  274. package/dist/components/sign-chip.d.ts +0 -77
  275. package/dist/components/sign-chip.js +0 -47
  276. package/dist/components/sign-chip.js.map +0 -1
  277. package/dist/lib/isin.d.ts +0 -38
  278. package/dist/lib/isin.js +0 -36
  279. package/dist/lib/isin.js.map +0 -1
  280. package/dist/shell/role-switcher.d.ts +0 -28
  281. package/dist/shell/role-switcher.js +0 -31
  282. package/dist/shell/role-switcher.js.map +0 -1
  283. package/src/billing/dialog-close.ts +0 -34
  284. package/src/components/field-sync-labels.ts +0 -14
  285. package/src/components/field-sync.tsx +0 -504
  286. package/src/components/section-nav.tsx +0 -72
  287. package/src/components/sign-chip-labels.ts +0 -13
  288. package/src/components/sign-chip.tsx +0 -133
  289. package/src/lib/isin.ts +0 -72
  290. package/src/shell/role-switcher.tsx +0 -53
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/auth-errors.ts"],"sourcesContent":["import { httpStatusOf } from \"../lib/http-status\";\n\n/**\n * The coded refusals of sign-in and sign-up (docs/auth-harmonization.md §5.2): every\n * refusal the auth routes answer carries a machine-readable `code`, and the pages\n * recognise the CODE, never the server's sentence.\n *\n * Today each app's pages print the server's `detail` string (keksdose's\n * `extractApiErrorMessage`), which is English on every locale and, on a credential\n * check, would be an account-enumeration oracle if it ever said more than one thing.\n * keksdose already keys one refusal on a code (`features/budgets/plan-limit.ts`,\n * `402 {detail, code: \"plan_budget_limit\"}`); §5.2 makes that the rule for auth. Each\n * app switches its server and its pages together (§9).\n *\n * - `invalid_credentials` — unknown address, wrong password AND deactivated account:\n * one answer, in the same time (§2.10). `401`.\n * - `registration_closed` — the sign-up gate refused the address. `403`.\n * - `email_taken` — an account with this address exists. `409`.\n * - `invitation_invalid`, `invitation_expired` — the `invite` token of a sign-up or an\n * accept (§4.4).\n * - `token_invalid` — a one-time token (reset, verification, a sign-in challenge) is\n * unknown, spent or expired.\n * - `account_inactive` — **no server answers it.** §2.10 decided that a deactivated\n * account gets `invalid_credentials`, because a separate answer after a correct\n * password would confirm a leaked one. It stays in the union only for a future server\n * step that is past the password (an admin view, a signed-in refresh) and may say so.\n * - `session_ended` — 0.34.0 (§11.2, decision 7; server-kit 0.8): every definitive\n * refusal of `POST /auth/refresh`, a `401`. No form shows it, so it has no label: the\n * kit's `attachSessionRefresh` decides by the STATUS, so an app whose server has not\n * switched yet keeps working.\n */\nexport type AuthErrorCode =\n | \"invalid_credentials\"\n | \"registration_closed\"\n | \"email_taken\"\n | \"invitation_invalid\"\n | \"invitation_expired\"\n | \"token_invalid\"\n /** 0.30.0: a link that WAS valid and ran out (verification, reset) — keksdose's step-3\n * refusals tell it from `token_invalid`, so a page can say \"expired, ask for a new one\". */\n | \"token_expired\"\n | \"account_inactive\"\n | \"session_ended\";\n\n/**\n * 0.30.0: the coded refusals of user administration and the account's own settings\n * (docs/user-admin-harmonization.md §4, §6) — server-kit 0.4.0's `AccountErrorCode`,\n * value for value. The same family of answer as the sign-in codes (`{detail, code}`,\n * plus `companies` on kastlan's `last_admin`), read by the same {@link authErrorCode}.\n *\n * - `last_admin` — the action would leave no active admin: deactivating, demoting or\n * deleting the last one (in kastlan, of a company; the answer names the `companies`).\n * - `self_action` — an admin acting on their own account where that is never allowed.\n * - `other_companies` — kastlan: the account belongs to other companies too, so a\n * company admin may not deactivate it; \"Remove from company\" instead (§9.6).\n * - `household_has_members` — keksdose's guard on a deletion request (§6.4).\n * - `confirmation_required`, `confirmation_mismatch` — the action's confirmation level\n * asked for a typed address (or a tick), and the request carried none, or another\n * address (§4.2).\n * - `password_incorrect` — the CURRENT password is wrong on a signed-in route (the email\n * change, the deletion request). `400`, not `401`: an app's client reads a 401 as an\n * ended session and signs the user out, the wrong answer to a typo. Every other one\n * is a `409`.\n */\nexport type AccountErrorCode =\n | \"last_admin\"\n | \"self_action\"\n | \"other_companies\"\n | \"household_has_members\"\n | \"confirmation_required\"\n | \"confirmation_mismatch\"\n | \"password_incorrect\";\n\n/**\n * 0.31.0: the demo's coded refusals (docs/landing-demo-harmonization.md §6.2) —\n * server-kit 0.5's `DemoErrorCode`, value for value, answered as `{detail, code}` like\n * the others and read by the same {@link authErrorCode}.\n *\n * - `demo_disabled` — `404`: the demo switch is off (it is \"not there\", not forbidden).\n * - `demo_rate_limited` — `429` with `Retry-After`: the per-IP window of demo starts.\n * - `demo_capacity` — `429`, no `Retry-After`: the cap of live demo users.\n * - `demo_not_ready` — `503`: the demo data has not been seeded yet.\n * - `demo_read_only` — `403`, model R: any write by a demo user. The app's write lock\n * already explains it, so it shows no toast (§5.4).\n * - `demo_refused` — `403`, both models: an action a demo never may — a way in or out,\n * mail, uploads, the account export (§6.4).\n */\nexport type DemoErrorCode =\n \"demo_disabled\" | \"demo_rate_limited\" | \"demo_capacity\" | \"demo_not_ready\" | \"demo_read_only\" | \"demo_refused\";\n\n/**\n * 0.32.0: billing's coded refusals (docs/billing-harmonization.md §3.3, §3.4, §4, §12.3) —\n * server-kit 0.6's `BillingError` and `PlanLimitError`, value for value, answered as\n * `{detail, code}` like the others and read by the same {@link authErrorCode}.\n *\n * - `billing_disabled` — `404`: the billing switch is off (§2.9), so every billing route\n * but `GET /billing/status` is \"not there\", as a switched-off demo is.\n * - `billing_read_only` — `402`: a write by (or into the data of) a payer out of good\n * standing (§3.3, §12.1). Inside a sync reply it marks the changes that were refused\n * and not applied (§12.5). The app's write lock and banner already say why, so — like\n * `demo_read_only` — it needs no toast of its own.\n * - `plan_limit` — `402`: a create past the plan's limit (§3.4), with the extra fields\n * `dimension`, `plan`, `limit` and `used` — read them with `isPlanLimit`. A lapsed payer\n * creating gets `billing_read_only` instead, never this (§12.3).\n * - `billing_not_configured` — billing is switched on but the provider's settings are\n * missing (the secrets, a price id): the operator's mistake, not the payer's.\n *\n * 0.33.0 (server-kit 0.7, §14.2): the provider's refusals, with the `billing_` prefix of\n * the others (decision 22).\n * - `billing_provider_unavailable` — `502`: Paddle didn't answer, or refused the request.\n * Try again.\n * - `billing_not_at_provider` — `409`: a portal request before the payer reached the\n * provider (§14.5) — a stale page; the overview's `at_provider` says it first.\n * - `billing_already_subscribed` — `409`: a checkout while a provider subscription runs\n * (§14.6). Since 0.39 plan changes go through the plan switch (§15.2.10).\n * - `billing_plan_not_sold` — `422`: a checkout for a plan, currency or interval the\n * catalogue doesn't sell — or (0.39) a switch to one.\n *\n * 0.39.0 (server-kit 0.12, §15.2.10): the plan switch's refusals.\n * - `billing_not_subscribed` — `409`: a switch with no running provider subscription;\n * the page offers the checkout instead.\n * - `billing_plan_unchanged` — `409`: a switch to the price the payer pays now.\n */\nexport type BillingErrorCode =\n | \"billing_disabled\"\n | \"billing_read_only\"\n | \"plan_limit\"\n | \"billing_not_configured\"\n | \"billing_provider_unavailable\"\n | \"billing_not_at_provider\"\n | \"billing_already_subscribed\"\n | \"billing_plan_not_sold\"\n | \"billing_not_subscribed\"\n | \"billing_plan_unchanged\";\n\n/** Every code the kit reads: the sign-in refusals, the account ones (0.30.0), the demo's\n * (0.31.0) and billing's (0.32.0). */\nexport type KitErrorCode = AuthErrorCode | AccountErrorCode | DemoErrorCode | BillingErrorCode;\n\nconst BILLING_CODES: ReadonlySet<BillingErrorCode> = new Set<BillingErrorCode>([\n \"billing_disabled\",\n \"billing_read_only\",\n \"plan_limit\",\n \"billing_not_configured\",\n \"billing_provider_unavailable\",\n \"billing_not_at_provider\",\n \"billing_already_subscribed\",\n \"billing_plan_not_sold\",\n \"billing_not_subscribed\",\n \"billing_plan_unchanged\",\n]);\n\nconst CODES: ReadonlySet<string> = new Set<KitErrorCode>([\n \"invalid_credentials\",\n \"registration_closed\",\n \"email_taken\",\n \"invitation_invalid\",\n \"invitation_expired\",\n \"token_invalid\",\n \"token_expired\",\n \"account_inactive\",\n \"session_ended\",\n \"last_admin\",\n \"self_action\",\n \"other_companies\",\n \"household_has_members\",\n \"confirmation_required\",\n \"confirmation_mismatch\",\n \"password_incorrect\",\n \"demo_disabled\",\n \"demo_rate_limited\",\n \"demo_capacity\",\n \"demo_not_ready\",\n \"demo_read_only\",\n \"demo_refused\",\n ...BILLING_CODES,\n]);\n\ntype Bag = Record<string, unknown>;\n\nconst isBag = (value: unknown): value is Bag => typeof value === \"object\" && value !== null;\n\n/** A known code, or undefined. */\nfunction known(value: unknown): KitErrorCode | undefined {\n return typeof value === \"string\" && CODES.has(value) ? (value as KitErrorCode) : undefined;\n}\n\n/** `{code}` on a parsed body, or FastAPI's `{detail: {code}}` — what an\n * `HTTPException(detail={\"code\": …})` serialises to. */\nfunction codeOfBody(body: unknown): KitErrorCode | undefined {\n if (!isBag(body)) return undefined;\n return known(body.code) ?? (isBag(body.detail) ? known(body.detail.code) : undefined);\n}\n\n/**\n * The code an error carries — an {@link AuthErrorCode}, since 0.30.0 an\n * {@link AccountErrorCode}, since 0.31.0 a {@link DemoErrorCode}, since 0.32.0 a\n * {@link BillingErrorCode} — or `undefined` when it carries none.\n *\n * Reads the parsed response body wherever the apps' HTTP clients put it, without\n * depending on any of them:\n *\n * 1. `err.response.data` — axios (keksdose's, kastlan's and Kurvenschmiede's clients;\n * keksdose's `isPlanBudgetLimitRefusal` reads the same path);\n * 2. `err.data` — ofetch's `FetchError`, and most `fetch` wrappers that parse the body;\n * 3. `err.body` — the generated OpenAPI clients' `ApiError`;\n * 4. `err` itself — a body thrown as it came (`throw await res.json()`).\n *\n * In each, `code` or FastAPI's nested `detail.code`. Last, `err.code` — but only when\n * it is one of the codes above: axios writes its OWN codes there (`ERR_NETWORK`,\n * `ECONNABORTED`, `ERR_BAD_REQUEST`), and so does Node, so an unknown value is never\n * read as a refusal. The HTTP status is not checked: the code is the contract, and the\n * status differs per refusal (401, 402, 403, 404, 409, 410).\n */\nexport function authErrorCode(err: unknown): KitErrorCode | undefined {\n if (!isBag(err)) return undefined;\n const response = err.response;\n return (\n (isBag(response) ? codeOfBody(response.data) : undefined) ??\n codeOfBody(err.data) ??\n codeOfBody(err.body) ??\n codeOfBody(err)\n );\n}\n\n/**\n * Whether `err` is a coded auth refusal — any of them, or the one named:\n *\n * ```ts\n * try { await api.register(values) }\n * catch (err) { if (isAuthError(err, \"email_taken\")) … }\n * ```\n *\n * The kit's `SignInForm` and `RegisterForm` call it on what their callbacks reject\n * with, so an app's `onSubmit` can simply let its client's error through. See\n * {@link authErrorCode} for the shapes it reads. Since 0.30.0 it knows the account codes\n * too: `isAuthError(err, \"last_admin\")`; since 0.31.0 the demo's:\n * `isAuthError(err, \"demo_read_only\")`; since 0.32.0 billing's:\n * `isAuthError(err, \"billing_read_only\")` (or {@link isBillingError}).\n */\nexport function isAuthError(err: unknown, code?: KitErrorCode): boolean {\n const found = authErrorCode(err);\n return found !== undefined && (code === undefined || found === code);\n}\n\n/**\n * Whether `err` is one of billing's coded refusals (0.32.0, docs/billing-harmonization.md\n * §10, §14.2) — any of them, or the one named. {@link isAuthError} answers the same for a\n * named code; this one also answers \"is it billing's at all\", which an app's error\n * handler asks before it decides between the write lock, a `PlanLimitNotice` and a toast.\n */\nexport function isBillingError(err: unknown, code?: BillingErrorCode): boolean {\n const found = authErrorCode(err);\n return found !== undefined && BILLING_CODES.has(found as BillingErrorCode) && (code === undefined || found === code);\n}\n\n/**\n * The parsed body that carries a code, wherever {@link authErrorCode} would find it (in\n * the same order) — the object with the `code`, so FastAPI's nested `detail` when the code\n * is there. For a reader of a refusal's EXTRA fields (`isPlanLimit`: `dimension`, `limit`,\n * `used` sit beside the code). `accept` decides which codes count; default, the known ones.\n *\n * @internal Not part of the barrel.\n */\nexport function errorBodyWithCode(\n err: unknown,\n accept: (code: unknown) => boolean = (code) => known(code) !== undefined,\n): Record<string, unknown> | undefined {\n if (!isBag(err)) return undefined;\n const response = err.response;\n for (const body of [isBag(response) ? response.data : undefined, err.data, err.body, err]) {\n if (!isBag(body)) continue;\n if (accept(body.code)) return body;\n if (isBag(body.detail) && accept(body.detail.code)) return body.detail;\n }\n return undefined;\n}\n\n/**\n * The HTTP status a thrown error carries ({@link httpStatusOf}): axios'\n * `response.status`, then `status`, then `statusCode`. Lives in `lib/http-status.ts`\n * since 0.52 (refactor audit 2026-10-30, MOD-6); re-exported here for the modules that\n * import it from auth. Package-private — not re-exported from the barrel.\n */\nexport { httpStatusOf };\n\n/**\n * Whether `err` is a throttled request — HTTP `429 Too Many Requests` (0.30.0, from the\n * apps' 0.29 adoption: every app answered a throttled sign-in, sign-up or reset through\n * its own `describeError`, with three wordings of one sentence).\n *\n * A throttle carries no `code` in the contract — the STATUS is the answer, set by the\n * apps' limiters (server-kit's `AuthLimiters`, keksdose's per-IP limit) — so this reads\n * the status, wherever the apps' HTTP clients put it ({@link httpStatusOf}).\n *\n * There is no `rate_limited` code: keksdose answers its 429s uncoded, with a\n * `Retry-After` header, and the status is the one signal every limiter gives.\n *\n * The kit's `SignInForm`, `RegisterForm`, `ForgotPasswordForm` and `ResetPasswordForm`\n * call it AFTER the app's `describeError` — an app with words of its own keeps them —\n * and before their generic failure, so \"Too many attempts. Try again in 30 s.\" (with\n * {@link retryAfterSeconds}) or \"… Wait a moment and try again.\" needs no app code.\n */\nexport function isRateLimited(err: unknown): boolean {\n return httpStatusOf(err) === 429;\n}\n\n/** A header's value from a `Headers` (fetch, ofetch's `response`), axios' `AxiosHeaders`\n * or a plain record — whose keys may come in any case. */\nfunction headerOf(headers: unknown, name: string): unknown {\n if (!isBag(headers)) return undefined;\n if (typeof headers.get === \"function\") {\n try {\n const value: unknown = (headers.get as (key: string) => unknown).call(headers, name);\n if (value !== undefined && value !== null) return value;\n } catch {\n // A `get` that is not a header lookup; the record below is.\n }\n }\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === name) return value;\n }\n return undefined;\n}\n\n/** `Retry-After` as seconds from `now`: delta-seconds (\"30\"), or an HTTP date\n * (\"Wed, 07 Oct 2026 10:00:00 GMT\") counted from now, never below 0 (RFC 9110 §10.2.3). */\nfunction parseRetryAfter(value: unknown, now: number): number | undefined {\n const raw = Array.isArray(value) ? (value as unknown[])[0] : value;\n if (typeof raw === \"number\") return Number.isFinite(raw) && raw >= 0 ? Math.ceil(raw) : undefined;\n if (typeof raw !== \"string\") return undefined;\n const text = raw.trim();\n if (/^\\d+$/.test(text)) return Number(text);\n // An HTTP date names its day and month (\"Wed, 07 Oct 2026 …\"). Without a letter it is\n // a malformed number (\"-5\", \"1.5\"), which `Date.parse` would happily read as a year.\n if (!/[a-z]/i.test(text)) return undefined;\n const at = Date.parse(text);\n if (Number.isNaN(at)) return undefined;\n return Math.max(0, Math.ceil((at - now) / 1000));\n}\n\n/**\n * How long a throttled request asks to wait, in whole seconds — its `Retry-After`\n * header (0.30.0, keksdose: its 429s carry no code, only the header) — or `undefined`\n * when there is none, or none that parses.\n *\n * Read where the apps' clients keep the response headers, without depending on any of\n * them: `err.response.headers` (axios' `AxiosHeaders` or a plain record; ofetch's\n * `FetchError.response`, a fetch `Headers`), then `err.headers` (a `Response` thrown as\n * it came). The value is delta-seconds, or an HTTP date turned into seconds from `now`\n * (default: the present moment) — never below 0.\n *\n * The forms hand it to their `rateLimited` label when {@link isRateLimited} is true:\n * \"Too many attempts. Try again in 30 s.\" — and, without one, \"… Wait a moment and try\n * again.\" A browser only exposes `Retry-After` to a cross-origin page when the server\n * lists it in `Access-Control-Expose-Headers`.\n */\nexport function retryAfterSeconds(err: unknown, now: number | Date = Date.now()): number | undefined {\n if (!isBag(err)) return undefined;\n const at = typeof now === \"number\" ? now : now.getTime();\n const response = err.response;\n const value =\n (isBag(response) ? headerOf(response.headers, \"retry-after\") : undefined) ?? headerOf(err.headers, \"retry-after\");\n return value === undefined ? undefined : parseRetryAfter(value, at);\n}\n\n// The English of the `rateLimited` labels, in auth-errors-labels since 0.52: the English\n// catalogue reached this module for them, and with it `lib/http-status` (refactor audit\n// 2026-10-30, MOD-6). Re-exported for the modules that import them from here.\nexport { englishRateLimited, englishWait } from \"./auth-errors-labels\";\n"],"mappings":";AAAA,SAAS,oBAAoB;AA2I7B,MAAM,gBAA+C,oBAAI,IAAsB;AAAA,EAC7E;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,MAAM,QAA6B,oBAAI,IAAkB;AAAA,EACvD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,CAAC;AAID,MAAM,QAAQ,CAAC,UAAiC,OAAO,UAAU,YAAY,UAAU;AAGvF,SAAS,MAAM,OAA0C;AACvD,SAAO,OAAO,UAAU,YAAY,MAAM,IAAI,KAAK,IAAK,QAAyB;AACnF;AAIA,SAAS,WAAW,MAAyC;AAC3D,MAAI,CAAC,MAAM,IAAI,EAAG,QAAO;AACzB,SAAO,MAAM,KAAK,IAAI,MAAM,MAAM,KAAK,MAAM,IAAI,MAAM,KAAK,OAAO,IAAI,IAAI;AAC7E;AAsBO,SAAS,cAAc,KAAwC;AACpE,MAAI,CAAC,MAAM,GAAG,EAAG,QAAO;AACxB,QAAM,WAAW,IAAI;AACrB,UACG,MAAM,QAAQ,IAAI,WAAW,SAAS,IAAI,IAAI,WAC/C,WAAW,IAAI,IAAI,KACnB,WAAW,IAAI,IAAI,KACnB,WAAW,GAAG;AAElB;AAiBO,SAAS,YAAY,KAAc,MAA8B;AACtE,QAAM,QAAQ,cAAc,GAAG;AAC/B,SAAO,UAAU,WAAc,SAAS,UAAa,UAAU;AACjE;AAQO,SAAS,eAAe,KAAc,MAAkC;AAC7E,QAAM,QAAQ,cAAc,GAAG;AAC/B,SAAO,UAAU,UAAa,cAAc,IAAI,KAAyB,MAAM,SAAS,UAAa,UAAU;AACjH;AAUO,SAAS,kBACd,KACA,SAAqC,CAAC,SAAS,MAAM,IAAI,MAAM,QAC1B;AACrC,MAAI,CAAC,MAAM,GAAG,EAAG,QAAO;AACxB,QAAM,WAAW,IAAI;AACrB,aAAW,QAAQ,CAAC,MAAM,QAAQ,IAAI,SAAS,OAAO,QAAW,IAAI,MAAM,IAAI,MAAM,GAAG,GAAG;AACzF,QAAI,CAAC,MAAM,IAAI,EAAG;AAClB,QAAI,OAAO,KAAK,IAAI,EAAG,QAAO;AAC9B,QAAI,MAAM,KAAK,MAAM,KAAK,OAAO,KAAK,OAAO,IAAI,EAAG,QAAO,KAAK;AAAA,EAClE;AACA,SAAO;AACT;AA2BO,SAAS,cAAc,KAAuB;AACnD,SAAO,aAAa,GAAG,MAAM;AAC/B;AAIA,SAAS,SAAS,SAAkB,MAAuB;AACzD,MAAI,CAAC,MAAM,OAAO,EAAG,QAAO;AAC5B,MAAI,OAAO,QAAQ,QAAQ,YAAY;AACrC,QAAI;AACF,YAAM,QAAkB,QAAQ,IAAiC,KAAK,SAAS,IAAI;AACnF,UAAI,UAAU,UAAa,UAAU,KAAM,QAAO;AAAA,IACpD,QAAQ;AAAA,IAER;AAAA,EACF;AACA,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AAClD,QAAI,IAAI,YAAY,MAAM,KAAM,QAAO;AAAA,EACzC;AACA,SAAO;AACT;AAIA,SAAS,gBAAgB,OAAgB,KAAiC;AACxE,QAAM,MAAM,MAAM,QAAQ,KAAK,IAAK,MAAoB,CAAC,IAAI;AAC7D,MAAI,OAAO,QAAQ,SAAU,QAAO,OAAO,SAAS,GAAG,KAAK,OAAO,IAAI,KAAK,KAAK,GAAG,IAAI;AACxF,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,QAAM,OAAO,IAAI,KAAK;AACtB,MAAI,QAAQ,KAAK,IAAI,EAAG,QAAO,OAAO,IAAI;AAG1C,MAAI,CAAC,SAAS,KAAK,IAAI,EAAG,QAAO;AACjC,QAAM,KAAK,KAAK,MAAM,IAAI;AAC1B,MAAI,OAAO,MAAM,EAAE,EAAG,QAAO;AAC7B,SAAO,KAAK,IAAI,GAAG,KAAK,MAAM,KAAK,OAAO,GAAI,CAAC;AACjD;AAkBO,SAAS,kBAAkB,KAAc,MAAqB,KAAK,IAAI,GAAuB;AACnG,MAAI,CAAC,MAAM,GAAG,EAAG,QAAO;AACxB,QAAM,KAAK,OAAO,QAAQ,WAAW,MAAM,IAAI,QAAQ;AACvD,QAAM,WAAW,IAAI;AACrB,QAAM,SACH,MAAM,QAAQ,IAAI,SAAS,SAAS,SAAS,aAAa,IAAI,WAAc,SAAS,IAAI,SAAS,aAAa;AAClH,SAAO,UAAU,SAAY,SAAY,gBAAgB,OAAO,EAAE;AACpE;AAKA,SAAS,oBAAoB,mBAAmB;","names":[]}
1
+ {"version":3,"sources":["../../src/auth/auth-errors.ts"],"sourcesContent":["import type { BillingLabels } from \"../billing/billing-labels\";\nimport { httpStatusOf } from \"../lib/http-status\";\n\n/**\n * The coded refusals of sign-in and sign-up (docs/auth-harmonization.md §5.2): every\n * refusal the auth routes answer carries a machine-readable `code`, and the pages\n * recognise the CODE, never the server's sentence.\n *\n * Today each app's pages print the server's `detail` string (keksdose's\n * `extractApiErrorMessage`), which is English on every locale and, on a credential\n * check, would be an account-enumeration oracle if it ever said more than one thing.\n * keksdose already keys one refusal on a code (`features/budgets/plan-limit.ts`,\n * `402 {detail, code: \"plan_budget_limit\"}`); §5.2 makes that the rule for auth. Each\n * app switches its server and its pages together (§9).\n *\n * - `invalid_credentials` — unknown address, wrong password AND deactivated account:\n * one answer, in the same time (§2.10). `401`.\n * - `registration_closed` — the sign-up gate refused the address. `403`.\n * - `email_taken` — an account with this address exists. `409`.\n * - `invitation_invalid`, `invitation_expired` — the `invite` token of a sign-up or an\n * accept (§4.4).\n * - `token_invalid` — a one-time token (reset, verification, a sign-in challenge) is\n * unknown, spent or expired.\n * - `account_inactive` — **no server answers it.** §2.10 decided that a deactivated\n * account gets `invalid_credentials`, because a separate answer after a correct\n * password would confirm a leaked one. It stays in the union only for a future server\n * step that is past the password (an admin view, a signed-in refresh) and may say so.\n * - `session_ended` — 0.34.0 (§11.2, decision 7; server-kit 0.8): every definitive\n * refusal of `POST /auth/refresh`, a `401`. No form shows it, so it has no label: the\n * kit's `attachSessionRefresh` decides by the STATUS, so an app whose server has not\n * switched yet keeps working.\n */\nexport type AuthErrorCode =\n | \"invalid_credentials\"\n | \"registration_closed\"\n | \"email_taken\"\n | \"invitation_invalid\"\n | \"invitation_expired\"\n | \"token_invalid\"\n /** 0.30.0: a link that WAS valid and ran out (verification, reset) — keksdose's step-3\n * refusals tell it from `token_invalid`, so a page can say \"expired, ask for a new one\". */\n | \"token_expired\"\n | \"account_inactive\"\n | \"session_ended\";\n\n/**\n * 0.30.0: the coded refusals of user administration and the account's own settings\n * (docs/user-admin-harmonization.md §4, §6) — server-kit 0.4.0's `AccountErrorCode`,\n * value for value. The same family of answer as the sign-in codes (`{detail, code}`,\n * plus `companies` on kastlan's `last_admin`), read by the same {@link authErrorCode}.\n *\n * - `last_admin` — the action would leave no active admin: deactivating, demoting or\n * deleting the last one (in kastlan, of a company; the answer names the `companies`).\n * - `self_action` — an admin acting on their own account where that is never allowed.\n * - `other_companies` — kastlan: the account belongs to other companies too, so a\n * company admin may not deactivate it; \"Remove from company\" instead (§9.6).\n * - `household_has_members` — keksdose's guard on a deletion request (§6.4).\n * - `confirmation_required`, `confirmation_mismatch` — the action's confirmation level\n * asked for a typed address (or a tick), and the request carried none, or another\n * address (§4.2).\n * - `password_incorrect` — the CURRENT password is wrong on a signed-in route (the email\n * change, the deletion request). `400`, not `401`: an app's client reads a 401 as an\n * ended session and signs the user out, the wrong answer to a typo. Every other one\n * is a `409`.\n */\nexport type AccountErrorCode =\n | \"last_admin\"\n | \"self_action\"\n | \"other_companies\"\n | \"household_has_members\"\n | \"confirmation_required\"\n | \"confirmation_mismatch\"\n | \"password_incorrect\";\n\n/**\n * 0.31.0: the demo's coded refusals (docs/landing-demo-harmonization.md §6.2) —\n * server-kit 0.5's `DemoErrorCode`, value for value, answered as `{detail, code}` like\n * the others and read by the same {@link authErrorCode}.\n *\n * - `demo_disabled` — `404`: the demo switch is off (it is \"not there\", not forbidden).\n * - `demo_rate_limited` — `429` with `Retry-After`: the per-IP window of demo starts.\n * - `demo_capacity` — `429`, no `Retry-After`: the cap of live demo users.\n * - `demo_not_ready` — `503`: the demo data has not been seeded yet.\n * - `demo_read_only` — `403`, model R: any write by a demo user. The app's write lock\n * already explains it, so it shows no toast (§5.4).\n * - `demo_refused` — `403`, both models: an action a demo never may — a way in or out,\n * mail, uploads, the account export (§6.4).\n */\nexport type DemoErrorCode =\n \"demo_disabled\" | \"demo_rate_limited\" | \"demo_capacity\" | \"demo_not_ready\" | \"demo_read_only\" | \"demo_refused\";\n\n/**\n * 0.32.0: billing's coded refusals (docs/billing-harmonization.md §3.3, §3.4, §4, §12.3) —\n * server-kit 0.6's `BillingError` and `PlanLimitError`, value for value, answered as\n * `{detail, code}` like the others and read by the same {@link authErrorCode}.\n *\n * - `billing_disabled` — `404`: the billing switch is off (§2.9), so every billing route\n * but `GET /billing/status` is \"not there\", as a switched-off demo is.\n * - `billing_read_only` — `402`: a write by (or into the data of) a payer out of good\n * standing (§3.3, §12.1). Inside a sync reply it marks the changes that were refused\n * and not applied (§12.5). The app's write lock and banner already say why, so — like\n * `demo_read_only` — it needs no toast of its own.\n * - `plan_limit` — `402`: a create past the plan's limit (§3.4), with the extra fields\n * `dimension`, `plan`, `limit` and `used` — read them with `isPlanLimit`. A lapsed payer\n * creating gets `billing_read_only` instead, never this (§12.3).\n * - `billing_not_configured` — billing is switched on but the provider's settings are\n * missing (the secrets, a price id): the operator's mistake, not the payer's.\n *\n * 0.33.0 (server-kit 0.7, §14.2): the provider's refusals, with the `billing_` prefix of\n * the others (decision 22).\n * - `billing_provider_unavailable` — `502`: Paddle didn't answer, or refused the request.\n * Try again.\n * - `billing_not_at_provider` — `409`: a portal request before the payer reached the\n * provider (§14.5) — a stale page; the overview's `at_provider` says it first.\n * - `billing_already_subscribed` — `409`: a checkout while a provider subscription runs\n * (§14.6). Since 0.39 plan changes go through the plan switch (§15.2.10).\n * - `billing_plan_not_sold` — `422`: a checkout for a plan, currency or interval the\n * catalogue doesn't sell — or (0.39) a switch to one.\n *\n * 0.39.0 (server-kit 0.12, §15.2.10): the plan switch's refusals.\n * - `billing_not_subscribed` — `409`: a switch with no running provider subscription;\n * the page offers the checkout instead.\n * - `billing_plan_unchanged` — `409`: a switch to the price the payer pays now.\n */\nexport type BillingErrorCode =\n | \"billing_disabled\"\n | \"billing_read_only\"\n | \"plan_limit\"\n | \"billing_not_configured\"\n | \"billing_provider_unavailable\"\n | \"billing_not_at_provider\"\n | \"billing_already_subscribed\"\n | \"billing_plan_not_sold\"\n | \"billing_not_subscribed\"\n | \"billing_plan_unchanged\";\n\n/** Every code the kit reads: the sign-in refusals, the account ones (0.30.0), the demo's\n * (0.31.0) and billing's (0.32.0). */\nexport type KitErrorCode = AuthErrorCode | AccountErrorCode | DemoErrorCode | BillingErrorCode;\n\nconst BILLING_CODES: ReadonlySet<BillingErrorCode> = new Set<BillingErrorCode>([\n \"billing_disabled\",\n \"billing_read_only\",\n \"plan_limit\",\n \"billing_not_configured\",\n \"billing_provider_unavailable\",\n \"billing_not_at_provider\",\n \"billing_already_subscribed\",\n \"billing_plan_not_sold\",\n \"billing_not_subscribed\",\n \"billing_plan_unchanged\",\n]);\n\nconst CODES: ReadonlySet<string> = new Set<KitErrorCode>([\n \"invalid_credentials\",\n \"registration_closed\",\n \"email_taken\",\n \"invitation_invalid\",\n \"invitation_expired\",\n \"token_invalid\",\n \"token_expired\",\n \"account_inactive\",\n \"session_ended\",\n \"last_admin\",\n \"self_action\",\n \"other_companies\",\n \"household_has_members\",\n \"confirmation_required\",\n \"confirmation_mismatch\",\n \"password_incorrect\",\n \"demo_disabled\",\n \"demo_rate_limited\",\n \"demo_capacity\",\n \"demo_not_ready\",\n \"demo_read_only\",\n \"demo_refused\",\n ...BILLING_CODES,\n]);\n\ntype Bag = Record<string, unknown>;\n\nconst isBag = (value: unknown): value is Bag => typeof value === \"object\" && value !== null;\n\n/** A known code, or undefined. */\nfunction known(value: unknown): KitErrorCode | undefined {\n return typeof value === \"string\" && CODES.has(value) ? (value as KitErrorCode) : undefined;\n}\n\n/** `{code}` on a parsed body, or FastAPI's `{detail: {code}}` — what an\n * `HTTPException(detail={\"code\": …})` serialises to. */\nfunction codeOfBody(body: unknown): KitErrorCode | undefined {\n if (!isBag(body)) return undefined;\n return known(body.code) ?? (isBag(body.detail) ? known(body.detail.code) : undefined);\n}\n\n/**\n * The code an error carries — an {@link AuthErrorCode}, since 0.30.0 an\n * {@link AccountErrorCode}, since 0.31.0 a {@link DemoErrorCode}, since 0.32.0 a\n * {@link BillingErrorCode} — or `undefined` when it carries none.\n *\n * Reads the parsed response body wherever the apps' HTTP clients put it, without\n * depending on any of them:\n *\n * 1. `err.response.data` — axios (keksdose's, kastlan's and Kurvenschmiede's clients;\n * keksdose's `isPlanBudgetLimitRefusal` reads the same path);\n * 2. `err.data` — ofetch's `FetchError`, and most `fetch` wrappers that parse the body;\n * 3. `err.body` — the generated OpenAPI clients' `ApiError`;\n * 4. `err` itself — a body thrown as it came (`throw await res.json()`).\n *\n * In each, `code` or FastAPI's nested `detail.code`. Last, `err.code` — but only when\n * it is one of the codes above: axios writes its OWN codes there (`ERR_NETWORK`,\n * `ECONNABORTED`, `ERR_BAD_REQUEST`), and so does Node, so an unknown value is never\n * read as a refusal. The HTTP status is not checked: the code is the contract, and the\n * status differs per refusal (401, 402, 403, 404, 409, 410).\n */\nexport function authErrorCode(err: unknown): KitErrorCode | undefined {\n if (!isBag(err)) return undefined;\n const response = err.response;\n return (\n (isBag(response) ? codeOfBody(response.data) : undefined) ??\n codeOfBody(err.data) ??\n codeOfBody(err.body) ??\n codeOfBody(err)\n );\n}\n\n/**\n * Whether `err` is a coded auth refusal — any of them, or the one named:\n *\n * ```ts\n * try { await api.register(values) }\n * catch (err) { if (isAuthError(err, \"email_taken\")) … }\n * ```\n *\n * The kit's `SignInForm` and `RegisterForm` call it on what their callbacks reject\n * with, so an app's `onSubmit` can simply let its client's error through. See\n * {@link authErrorCode} for the shapes it reads. Since 0.30.0 it knows the account codes\n * too: `isAuthError(err, \"last_admin\")`; since 0.31.0 the demo's:\n * `isAuthError(err, \"demo_read_only\")`; since 0.32.0 billing's:\n * `isAuthError(err, \"billing_read_only\")` (or {@link isBillingError}).\n */\nexport function isAuthError(err: unknown, code?: KitErrorCode): boolean {\n const found = authErrorCode(err);\n return found !== undefined && (code === undefined || found === code);\n}\n\n/**\n * Whether `err` is one of billing's coded refusals (0.32.0, docs/billing-harmonization.md\n * §10, §14.2) — any of them, or the one named. {@link isAuthError} answers the same for a\n * named code; this one also answers \"is it billing's at all\", which an app's error\n * handler asks before it decides between the write lock, a `PlanLimitNotice` and a toast.\n */\nexport function isBillingError(err: unknown, code?: BillingErrorCode): boolean {\n const found = authErrorCode(err);\n return found !== undefined && BILLING_CODES.has(found as BillingErrorCode) && (code === undefined || found === code);\n}\n\n/** Who reads a `billing_read_only` refusal — {@link billingRefusalText}'s third argument,\n * `useBillingLockReason`'s two options. */\nexport interface BillingRefusalTextOptions {\n /** The reader is a GUEST in someone else's item (§12.6): the words never say why. */\n guest?: boolean;\n /** A guest's item, by its own name (\"Household 2026\"). Ignored for the payer. */\n item?: string;\n}\n\ntype RefusalWords = (labels: BillingLabels, options: BillingRefusalTextOptions) => string;\n\n/**\n * Each billing refusal's words in the `billing` labels. A `Record` over the whole union, so\n * a code server-kit adds is a type error here until it has words.\n */\nconst BILLING_REFUSAL_WORDS: Record<BillingErrorCode, RefusalWords> = {\n billing_disabled: (labels) => labels.disabled,\n // The lock's own sentence (`useBillingLockReason`): a guest's never says why (§12.6).\n billing_read_only: (labels, options) => (options.guest ? labels.guestReadOnly(options.item) : labels.lockReason),\n // The notice's first line. The figure and the way to more are `isPlanLimit`'s, for\n // `PlanLimitNotice` or `usePlanLimitToast`.\n plan_limit: (labels) => labels.limitReached,\n billing_not_configured: (labels) => labels.notConfigured,\n billing_provider_unavailable: (labels) => labels.providerUnavailable,\n billing_not_at_provider: (labels) => labels.notAtProvider,\n billing_already_subscribed: (labels) => labels.alreadySubscribed,\n billing_plan_not_sold: (labels) => labels.planNotSold,\n billing_not_subscribed: (labels) => labels.notSubscribed,\n billing_plan_unchanged: (labels) => labels.planUnchanged,\n};\n\n/**\n * The words for a billing refusal, in `labels` (an app's `useBillingLabels()`, or its\n * `billing` catalogue outside React), or `null` when `err` is not one — for the toast or\n * the dialog after a billing call failed, with the app's own words for anything else\n * (kastlan reuse audit 2026-11-02, proposal 4).\n *\n * All three apps mapped the codes to the labels by hand: kastlan's\n * `billing-refusal-dialog.tsx`, keksdose's `subscription-cards.tsx` and Kurvenschmiede's\n * `api/client.ts`, the same eight codes each. This follows the most complete of them:\n * every {@link BillingErrorCode}, so it is `null` only for an error that is not billing's.\n *\n * - `billing_read_only` — the write lock's sentence, as `useBillingLockReason` says it:\n * the payer's `lockReason`, or with `{ guest: true, item }` a guest's `guestReadOnly`,\n * which never says why (§12.6) — kastlan's dialog, which passes `guest: !isAdmin`.\n * - `plan_limit` — `limitReached`, \"Plan limit reached\". Where the figure fits, read\n * `isPlanLimit` first and show `PlanLimitNotice` or `usePlanLimitToast`, as kastlan does.\n * - the eight others — their labels: `disabled`, `notConfigured`, `providerUnavailable`,\n * `notAtProvider`, `alreadySubscribed`, `planNotSold`, `notSubscribed`, `planUnchanged`.\n *\n * Reads the code wherever {@link authErrorCode} does (axios, ofetch, a generated client,\n * FastAPI's nested `detail`).\n *\n * catch (err) {\n * toast.error(billingRefusalText(err, labels) ?? describeError(err));\n * }\n */\nexport function billingRefusalText(\n err: unknown,\n labels: BillingLabels,\n options: BillingRefusalTextOptions = {},\n): string | null {\n const found = authErrorCode(err);\n return found !== undefined && BILLING_CODES.has(found as BillingErrorCode)\n ? BILLING_REFUSAL_WORDS[found as BillingErrorCode](labels, options)\n : null;\n}\n\n/**\n * The parsed body that carries a code, wherever {@link authErrorCode} would find it (in\n * the same order) — the object with the `code`, so FastAPI's nested `detail` when the code\n * is there. For a reader of a refusal's EXTRA fields (`isPlanLimit`: `dimension`, `limit`,\n * `used` sit beside the code). `accept` decides which codes count; default, the known ones.\n *\n * @internal Not part of the barrel.\n */\nexport function errorBodyWithCode(\n err: unknown,\n accept: (code: unknown) => boolean = (code) => known(code) !== undefined,\n): Record<string, unknown> | undefined {\n if (!isBag(err)) return undefined;\n const response = err.response;\n for (const body of [isBag(response) ? response.data : undefined, err.data, err.body, err]) {\n if (!isBag(body)) continue;\n if (accept(body.code)) return body;\n if (isBag(body.detail) && accept(body.detail.code)) return body.detail;\n }\n return undefined;\n}\n\n/**\n * The HTTP status a thrown error carries ({@link httpStatusOf}): axios'\n * `response.status`, then `status`, then `statusCode`. Lives in `lib/http-status.ts`\n * since 0.52 (refactor audit 2026-10-30, MOD-6); re-exported here for the modules that\n * import it from auth. Package-private — not re-exported from the barrel.\n */\nexport { httpStatusOf };\n\n/**\n * Whether `err` is a throttled request — HTTP `429 Too Many Requests` (0.30.0, from the\n * apps' 0.29 adoption: every app answered a throttled sign-in, sign-up or reset through\n * its own `describeError`, with three wordings of one sentence).\n *\n * A throttle carries no `code` in the contract — the STATUS is the answer, set by the\n * apps' limiters (server-kit's `AuthLimiters`, keksdose's per-IP limit) — so this reads\n * the status, wherever the apps' HTTP clients put it ({@link httpStatusOf}).\n *\n * There is no `rate_limited` code: keksdose answers its 429s uncoded, with a\n * `Retry-After` header, and the status is the one signal every limiter gives.\n *\n * The kit's `SignInForm`, `RegisterForm`, `ForgotPasswordForm` and `ResetPasswordForm`\n * call it AFTER the app's `describeError` — an app with words of its own keeps them —\n * and before their generic failure, so \"Too many attempts. Try again in 30 s.\" (with\n * {@link retryAfterSeconds}) or \"… Wait a moment and try again.\" needs no app code.\n */\nexport function isRateLimited(err: unknown): boolean {\n return httpStatusOf(err) === 429;\n}\n\n/** A header's value from a `Headers` (fetch, ofetch's `response`), axios' `AxiosHeaders`\n * or a plain record — whose keys may come in any case. */\nfunction headerOf(headers: unknown, name: string): unknown {\n if (!isBag(headers)) return undefined;\n if (typeof headers.get === \"function\") {\n try {\n const value: unknown = (headers.get as (key: string) => unknown).call(headers, name);\n if (value !== undefined && value !== null) return value;\n } catch {\n // A `get` that is not a header lookup; the record below is.\n }\n }\n for (const [key, value] of Object.entries(headers)) {\n if (key.toLowerCase() === name) return value;\n }\n return undefined;\n}\n\n/** `Retry-After` as seconds from `now`: delta-seconds (\"30\"), or an HTTP date\n * (\"Wed, 07 Oct 2026 10:00:00 GMT\") counted from now, never below 0 (RFC 9110 §10.2.3). */\nfunction parseRetryAfter(value: unknown, now: number): number | undefined {\n const raw = Array.isArray(value) ? (value as unknown[])[0] : value;\n if (typeof raw === \"number\") return Number.isFinite(raw) && raw >= 0 ? Math.ceil(raw) : undefined;\n if (typeof raw !== \"string\") return undefined;\n const text = raw.trim();\n if (/^\\d+$/.test(text)) return Number(text);\n // An HTTP date names its day and month (\"Wed, 07 Oct 2026 …\"). Without a letter it is\n // a malformed number (\"-5\", \"1.5\"), which `Date.parse` would happily read as a year.\n if (!/[a-z]/i.test(text)) return undefined;\n const at = Date.parse(text);\n if (Number.isNaN(at)) return undefined;\n return Math.max(0, Math.ceil((at - now) / 1000));\n}\n\n/**\n * How long a throttled request asks to wait, in whole seconds — its `Retry-After`\n * header (0.30.0, keksdose: its 429s carry no code, only the header) — or `undefined`\n * when there is none, or none that parses.\n *\n * Read where the apps' clients keep the response headers, without depending on any of\n * them: `err.response.headers` (axios' `AxiosHeaders` or a plain record; ofetch's\n * `FetchError.response`, a fetch `Headers`), then `err.headers` (a `Response` thrown as\n * it came). The value is delta-seconds, or an HTTP date turned into seconds from `now`\n * (default: the present moment) — never below 0.\n *\n * The forms hand it to their `rateLimited` label when {@link isRateLimited} is true:\n * \"Too many attempts. Try again in 30 s.\" — and, without one, \"… Wait a moment and try\n * again.\" A browser only exposes `Retry-After` to a cross-origin page when the server\n * lists it in `Access-Control-Expose-Headers`.\n */\nexport function retryAfterSeconds(err: unknown, now: number | Date = Date.now()): number | undefined {\n if (!isBag(err)) return undefined;\n const at = typeof now === \"number\" ? now : now.getTime();\n const response = err.response;\n const value =\n (isBag(response) ? headerOf(response.headers, \"retry-after\") : undefined) ?? headerOf(err.headers, \"retry-after\");\n return value === undefined ? undefined : parseRetryAfter(value, at);\n}\n\n// The English of the `rateLimited` labels, in auth-errors-labels since 0.52: the English\n// catalogue reached this module for them, and with it `lib/http-status` (refactor audit\n// 2026-10-30, MOD-6). Re-exported for the modules that import them from here.\nexport { englishRateLimited, englishWait } from \"./auth-errors-labels\";\n"],"mappings":"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","names":[]}
@@ -1,17 +1,13 @@
1
1
  import type { LabelOverride } from "../i18n/kit-labels.js";
2
2
  import type { DateInput } from "../lib/format.js";
3
3
  import type { BillingLabels } from "./billing-labels.js";
4
- import type { WithDialogClose } from "./dialog-close.js";
5
- /** {@link CancelSubscriptionConfirmProps} before one of its two close props is required. */
6
- interface CancelSubscriptionConfirmFields {
4
+ export interface CancelSubscriptionConfirmProps {
7
5
  open: boolean;
8
6
  /** The admin dismissed it — Back, Escape, the backdrop. Confirm leaves it open: close it
9
7
  * once `POST /billing/cancel` answers. 0.52: every other kit dialog's `onClose`
10
- * (CONTRIBUTING §4; refactor audit 2026-10-30, API-5). */
11
- onClose?: () => void;
12
- /** @deprecated 0.52 — `onClose`, as every other kit dialog; removed in 0.53. Called with
13
- * `false` when there is no `onClose`. */
14
- onOpenChange?: (open: boolean) => void;
8
+ * (CONTRIBUTING §4; refactor audit 2026-10-30, API-5); required since 0.53 removed
9
+ * `onOpenChange` (deprecated in 0.52). */
10
+ onClose: () => void;
15
11
  /** The overview's `current_period_end`: the day the subscription ends, in a sentence —
16
12
  * the `<UiKitProvider formatDate>` (the app's date preference) when it set one, else the
17
13
  * kit's `formatDate`, `"medium"`, in the provider's locale and `timeZone` (0.52). */
@@ -26,10 +22,6 @@ interface CancelSubscriptionConfirmFields {
26
22
  /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */
27
23
  labels?: LabelOverride<BillingLabels>;
28
24
  }
29
- /** What {@link CancelSubscriptionConfirm} takes: `onClose`, or alone the deprecated
30
- * `onOpenChange`, is required (0.52 code review: with neither it compiled, and nothing
31
- * could dismiss it). */
32
- export type CancelSubscriptionConfirmProps = WithDialogClose<CancelSubscriptionConfirmFields>;
33
25
  /**
34
26
  * The in-app cancel's confirm (docs/billing-harmonization.md §16.2 decision 2, §16.4,
35
27
  * ui-kit 0.42): "Cancel subscription" for an admin the provider's portal doesn't open for
@@ -54,5 +46,4 @@ export type CancelSubscriptionConfirmProps = WithDialogClose<CancelSubscriptionC
54
46
  * may always cancel. A demo lock still holds it, as the server's demo guard does. The kit
55
47
  * sends no request.
56
48
  */
57
- export declare function CancelSubscriptionConfirm({ open, onClose, onOpenChange, periodEnd, onConfirm, pending, locale: localeProp, labels: labelsProp, }: CancelSubscriptionConfirmProps): import("react").JSX.Element;
58
- export {};
49
+ export declare function CancelSubscriptionConfirm({ open, onClose, periodEnd, onConfirm, pending, locale: localeProp, labels: labelsProp, }: CancelSubscriptionConfirmProps): import("react").JSX.Element;
@@ -7,11 +7,9 @@ import { Button } from "../components/button.js";
7
7
  import { DialogFrame } from "../components/dialog-frame.js";
8
8
  import { COMMIT_EXCEPT_BILLING } from "../components/write-lock.js";
9
9
  import { useBillingLabels } from "./billing-labels.js";
10
- import { dialogClose } from "./dialog-close.js";
11
10
  function CancelSubscriptionConfirm({
12
11
  open,
13
12
  onClose,
14
- onOpenChange,
15
13
  periodEnd,
16
14
  onConfirm,
17
15
  pending = false,
@@ -27,7 +25,7 @@ function CancelSubscriptionConfirm({
27
25
  open,
28
26
  title: labels.cancelConfirmTitle,
29
27
  description: labels.cancelConfirmBody(day(periodEnd)),
30
- onClose: dialogClose({ onClose, onOpenChange }),
28
+ onClose,
31
29
  busy: pending,
32
30
  "data-cancel-subscription-confirm": "",
33
31
  actions: (close) => /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/billing/cancel-subscription.tsx"],"sourcesContent":["import { useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_COMMON_LABELS } from \"../i18n/shared-labels\";\nimport { useDayFormatter } from \"../lib/day-format\";\nimport type { DateInput } from \"../lib/format\";\nimport { Button } from \"../components/button\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { COMMIT_EXCEPT_BILLING } from \"../components/write-lock\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport { dialogClose } from \"./dialog-close\";\nimport type { WithDialogClose } from \"./dialog-close\";\n\n/** {@link CancelSubscriptionConfirmProps} before one of its two close props is required. */\ninterface CancelSubscriptionConfirmFields {\n open: boolean;\n /** The admin dismissed it — Back, Escape, the backdrop. Confirm leaves it open: close it\n * once `POST /billing/cancel` answers. 0.52: every other kit dialog's `onClose`\n * (CONTRIBUTING §4; refactor audit 2026-10-30, API-5). */\n onClose?: () => void;\n /** @deprecated 0.52 — `onClose`, as every other kit dialog; removed in 0.53. Called with\n * `false` when there is no `onClose`. */\n onOpenChange?: (open: boolean) => void;\n /** The overview's `current_period_end`: the day the subscription ends, in a sentence —\n * the `<UiKitProvider formatDate>` (the app's date preference) when it set one, else the\n * kit's `formatDate`, `\"medium\"`, in the provider's locale and `timeZone` (0.52). */\n periodEnd: DateInput;\n /** Confirm: the app's `POST /billing/cancel` (server-kit 0.14, §16.3). */\n onConfirm: () => void;\n /** The cancel is under way: Confirm's spinner, no second press, and neither Escape,\n * the backdrop nor Back closes it. */\n pending?: boolean;\n /** The date's locale. Default: the provider's. */\n locale?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/** What {@link CancelSubscriptionConfirm} takes: `onClose`, or alone the deprecated\n * `onOpenChange`, is required (0.52 code review: with neither it compiled, and nothing\n * could dismiss it). */\nexport type CancelSubscriptionConfirmProps = WithDialogClose<CancelSubscriptionConfirmFields>;\n\n/**\n * The in-app cancel's confirm (docs/billing-harmonization.md §16.2 decision 2, §16.4,\n * ui-kit 0.42): \"Cancel subscription\" for an admin the provider's portal doesn't open for\n * (`SubscriptionActions`' `portal=\"buyer_only\"` → `onCancelInApp`). It names the day the\n * subscription ends — \"Your subscription ends on 9 Nov 2026. Until then everything works\n * as before.\" — and its button reads \"Cancel subscription now\" (German \"Jetzt kündigen\",\n * the confirmation button of §312k BGB).\n *\n * <CancelSubscriptionConfirm\n * open={asking}\n * onClose={() => setAsking(false)}\n * periodEnd={overview.current_period_end}\n * pending={cancel.isPending}\n * onConfirm={() => cancel.mutate()}\n * />\n *\n * The cancel is at the period's end (Paddle's `cancel`, not immediately); the row follows\n * by webhook (`cancel_at_period_end`), as after a cancel in the portal, and\n * `SubscriptionActions` then offers \"Keep subscription\" (`onResume`).\n *\n * It takes no billing lock (`COMMIT_EXCEPT_BILLING` built in, §3.3): a read-only payer\n * may always cancel. A demo lock still holds it, as the server's demo guard does. The kit\n * sends no request.\n */\nexport function CancelSubscriptionConfirm({\n open,\n onClose,\n onOpenChange,\n periodEnd,\n onConfirm,\n pending = false,\n locale: localeProp,\n labels: labelsProp,\n}: CancelSubscriptionConfirmProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const day = useDayFormatter(undefined, localeProp);\n\n return (\n <DialogFrame\n open={open}\n title={labels.cancelConfirmTitle}\n description={labels.cancelConfirmBody(day(periodEnd))}\n onClose={dialogClose({ onClose, onOpenChange })}\n busy={pending}\n data-cancel-subscription-confirm=\"\"\n actions={(close) => (\n <>\n <Button variant=\"secondary\" onClick={close} disabled={pending}>\n {common.back}\n </Button>\n <Button\n variant=\"danger\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={pending}\n onClick={() => {\n if (!pending) onConfirm();\n }}\n >\n {labels.cancelConfirmAction}\n </Button>\n </>\n )}\n />\n );\n}\n"],"mappings":";AA0FQ,mBACE,KADF;AA1FR,SAAS,oBAAoB;AAE7B,SAAS,6BAA6B;AACtC,SAAS,uBAAuB;AAEhC,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B,SAAS,6BAA6B;AACtC,SAAS,wBAAwB;AAEjC,SAAS,mBAAmB;AAyDrB,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,QAAQ;AACV,GAAmC;AACjC,QAAM,SAAS,iBAAiB,UAAU;AAC1C,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,MAAM,gBAAgB,QAAW,UAAU;AAEjD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,OAAO;AAAA,MACd,aAAa,OAAO,kBAAkB,IAAI,SAAS,CAAC;AAAA,MACpD,SAAS,YAAY,EAAE,SAAS,aAAa,CAAC;AAAA,MAC9C,MAAM;AAAA,MACN,oCAAiC;AAAA,MACjC,SAAS,CAAC,UACR,iCACE;AAAA,4BAAC,UAAO,SAAQ,aAAY,SAAS,OAAO,UAAU,SACnD,iBAAO,MACV;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,QAAQ;AAAA,YACR;AAAA,YACA,SAAS,MAAM;AACb,kBAAI,CAAC,QAAS,WAAU;AAAA,YAC1B;AAAA,YAEC,iBAAO;AAAA;AAAA,QACV;AAAA,SACF;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/billing/cancel-subscription.tsx"],"sourcesContent":["import { useKitLabels } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { DEFAULT_COMMON_LABELS } from \"../i18n/shared-labels\";\nimport { useDayFormatter } from \"../lib/day-format\";\nimport type { DateInput } from \"../lib/format\";\nimport { Button } from \"../components/button\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { COMMIT_EXCEPT_BILLING } from \"../components/write-lock\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\n\nexport interface CancelSubscriptionConfirmProps {\n open: boolean;\n /** The admin dismissed it — Back, Escape, the backdrop. Confirm leaves it open: close it\n * once `POST /billing/cancel` answers. 0.52: every other kit dialog's `onClose`\n * (CONTRIBUTING §4; refactor audit 2026-10-30, API-5); required since 0.53 removed\n * `onOpenChange` (deprecated in 0.52). */\n onClose: () => void;\n /** The overview's `current_period_end`: the day the subscription ends, in a sentence —\n * the `<UiKitProvider formatDate>` (the app's date preference) when it set one, else the\n * kit's `formatDate`, `\"medium\"`, in the provider's locale and `timeZone` (0.52). */\n periodEnd: DateInput;\n /** Confirm: the app's `POST /billing/cancel` (server-kit 0.14, §16.3). */\n onConfirm: () => void;\n /** The cancel is under way: Confirm's spinner, no second press, and neither Escape,\n * the backdrop nor Back closes it. */\n pending?: boolean;\n /** The date's locale. Default: the provider's. */\n locale?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * The in-app cancel's confirm (docs/billing-harmonization.md §16.2 decision 2, §16.4,\n * ui-kit 0.42): \"Cancel subscription\" for an admin the provider's portal doesn't open for\n * (`SubscriptionActions`' `portal=\"buyer_only\"` → `onCancelInApp`). It names the day the\n * subscription ends — \"Your subscription ends on 9 Nov 2026. Until then everything works\n * as before.\" — and its button reads \"Cancel subscription now\" (German \"Jetzt kündigen\",\n * the confirmation button of §312k BGB).\n *\n * <CancelSubscriptionConfirm\n * open={asking}\n * onClose={() => setAsking(false)}\n * periodEnd={overview.current_period_end}\n * pending={cancel.isPending}\n * onConfirm={() => cancel.mutate()}\n * />\n *\n * The cancel is at the period's end (Paddle's `cancel`, not immediately); the row follows\n * by webhook (`cancel_at_period_end`), as after a cancel in the portal, and\n * `SubscriptionActions` then offers \"Keep subscription\" (`onResume`).\n *\n * It takes no billing lock (`COMMIT_EXCEPT_BILLING` built in, §3.3): a read-only payer\n * may always cancel. A demo lock still holds it, as the server's demo guard does. The kit\n * sends no request.\n */\nexport function CancelSubscriptionConfirm({\n open,\n onClose,\n periodEnd,\n onConfirm,\n pending = false,\n locale: localeProp,\n labels: labelsProp,\n}: CancelSubscriptionConfirmProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const day = useDayFormatter(undefined, localeProp);\n\n return (\n <DialogFrame\n open={open}\n title={labels.cancelConfirmTitle}\n description={labels.cancelConfirmBody(day(periodEnd))}\n onClose={onClose}\n busy={pending}\n data-cancel-subscription-confirm=\"\"\n actions={(close) => (\n <>\n <Button variant=\"secondary\" onClick={close} disabled={pending}>\n {common.back}\n </Button>\n <Button\n variant=\"danger\"\n commit={COMMIT_EXCEPT_BILLING}\n pending={pending}\n onClick={() => {\n if (!pending) onConfirm();\n }}\n >\n {labels.cancelConfirmAction}\n </Button>\n </>\n )}\n />\n );\n}\n"],"mappings":";AA+EQ,mBACE,KADF;AA/ER,SAAS,oBAAoB;AAE7B,SAAS,6BAA6B;AACtC,SAAS,uBAAuB;AAEhC,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B,SAAS,6BAA6B;AACtC,SAAS,wBAAwB;AAiD1B,SAAS,0BAA0B;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,QAAQ;AACV,GAAmC;AACjC,QAAM,SAAS,iBAAiB,UAAU;AAC1C,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,MAAM,gBAAgB,QAAW,UAAU;AAEjD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,OAAO;AAAA,MACd,aAAa,OAAO,kBAAkB,IAAI,SAAS,CAAC;AAAA,MACpD;AAAA,MACA,MAAM;AAAA,MACN,oCAAiC;AAAA,MACjC,SAAS,CAAC,UACR,iCACE;AAAA,4BAAC,UAAO,SAAQ,aAAY,SAAS,OAAO,UAAU,SACnD,iBAAO,MACV;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,QAAQ;AAAA,YACR;AAAA,YACA,SAAS,MAAM;AACb,kBAAI,CAAC,QAAS,WAAU;AAAA,YAC1B;AAAA,YAEC,iBAAO;AAAA;AAAA,QACV;AAAA,SACF;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -2,7 +2,6 @@ import type { ReactNode } from "react";
2
2
  import type { LabelOverride } from "../i18n/kit-labels.js";
3
3
  import type { ChoiceCardProps } from "../components/choice-card.js";
4
4
  import type { BillingLabels } from "./billing-labels.js";
5
- import type { WithDialogClose } from "./dialog-close.js";
6
5
  import type { BillingCurrency, BillingInterval, PlanPrices } from "./plan-price.js";
7
6
  /**
8
7
  * One plan of the app's catalogue (docs/billing-harmonization.md §3.1) as a page shows it:
@@ -169,26 +168,19 @@ export interface PlanPickerProps extends Omit<PlanDisplayProps, "currency" | "in
169
168
  * instead, so the warning shows once on either path.
170
169
  */
171
170
  export declare function PlanPicker({ plans, current, value: valueProp, defaultValue, onValueChange, interval: intervalProp, defaultInterval, onIntervalChange, currency: currencyProp, defaultCurrency, onCurrencyChange, currencies: currenciesProp, onChoose, pending, permanentGrant, switching, pricePreview, legend, name, className, gridClassName, cardClassName, dimensionLabels, formatLimit, locale: localeProp, labels: labelsProp, }: PlanPickerProps): import("react").JSX.Element;
172
- /** {@link GrantWarningConfirmProps} before one of its two close props is required. */
173
- interface GrantWarningConfirmFields {
171
+ export interface GrantWarningConfirmProps {
174
172
  /** Shown, controlled. */
175
173
  open: boolean;
176
174
  /** "Back", Escape, the backdrop — and after "Buy anyway". 0.52: every other kit
177
- * dialog's `onClose` (CONTRIBUTING §4; refactor audit 2026-10-30, API-5). */
178
- onClose?: () => void;
179
- /** @deprecated 0.52 — `onClose`, as every other kit dialog; removed in 0.53. Called with
180
- * `false` when there is no `onClose`. */
181
- onOpenChange?: (open: boolean) => void;
175
+ * dialog's `onClose` (CONTRIBUTING §4; refactor audit 2026-10-30, API-5); required
176
+ * since 0.53 removed `onOpenChange` (deprecated in 0.52). */
177
+ onClose: () => void;
182
178
  /** "Buy anyway": the app opens its checkout. The dialog closes itself after it. */
183
179
  onConfirm: () => void;
184
180
  /** Prop > `<UiKitProvider labels={{ billing }}>` > English: `grantWarningTitle`,
185
181
  * `grantWarningBody`, `grantWarningAction`. "Back" is `common.back`. */
186
182
  labels?: LabelOverride<BillingLabels>;
187
183
  }
188
- /** What {@link GrantWarningConfirm} takes: `onClose`, or alone the deprecated
189
- * `onOpenChange`, is required (0.52 code review: with neither it compiled, and nothing
190
- * could dismiss it). */
191
- export type GrantWarningConfirmProps = WithDialogClose<GrantWarningConfirmFields>;
192
184
  /**
193
185
  * 0.42 (§16.8 B): the permanent-grant warning before a checkout — `grantWarningTitle`,
194
186
  * `grantWarningBody`, "Back" and "Buy anyway" (`grantWarningAction`). {@link PlanPicker}
@@ -198,5 +190,5 @@ export type GrantWarningConfirmProps = WithDialogClose<GrantWarningConfirmFields
198
190
  * to a checkout — show this before that checkout, so a permanent-grant holder is still
199
191
  * warned. Carries `data-grant-warning`.
200
192
  */
201
- export declare function GrantWarningConfirm({ open, onClose, onOpenChange, onConfirm, labels: labelsProp, }: GrantWarningConfirmProps): import("react").JSX.Element;
193
+ export declare function GrantWarningConfirm({ open, onClose, onConfirm, labels: labelsProp }: GrantWarningConfirmProps): import("react").JSX.Element;
202
194
  export {};
@@ -11,7 +11,6 @@ import { ToggleGroup } from "../components/toggle-group.js";
11
11
  import { Button } from "../components/button.js";
12
12
  import { DialogFrame } from "../components/dialog-frame.js";
13
13
  import { useBillingLabels } from "./billing-labels.js";
14
- import { dialogClose } from "./dialog-close.js";
15
14
  import { BILLING_INTERVALS, billingCurrencyFor, formatPlanPrice, planPrice } from "./plan-price.js";
16
15
  function planRelation(plans, code, current) {
17
16
  if (!current) return void 0;
@@ -269,23 +268,16 @@ function PlanPicker({
269
268
  }
270
269
  );
271
270
  }
272
- function GrantWarningConfirm({
273
- open,
274
- onClose,
275
- onOpenChange,
276
- onConfirm,
277
- labels: labelsProp
278
- }) {
271
+ function GrantWarningConfirm({ open, onClose, onConfirm, labels: labelsProp }) {
279
272
  const labels = useBillingLabels(labelsProp);
280
273
  const common = useKitLabels("common", DEFAULT_COMMON_LABELS);
281
- const close = dialogClose({ onClose, onOpenChange });
282
274
  return /* @__PURE__ */ jsx(
283
275
  DialogFrame,
284
276
  {
285
277
  open,
286
278
  title: labels.grantWarningTitle,
287
279
  description: labels.grantWarningBody,
288
- onClose: close,
280
+ onClose,
289
281
  "data-grant-warning": "",
290
282
  actions: (lower) => /* @__PURE__ */ jsxs(Fragment, { children: [
291
283
  /* @__PURE__ */ jsx(Button, { variant: "secondary", onClick: lower, children: common.back }),
@@ -296,7 +288,7 @@ function GrantWarningConfirm({
296
288
  onClick: () => {
297
289
  if (!open) return;
298
290
  onConfirm();
299
- close();
291
+ onClose();
300
292
  },
301
293
  children: labels.grantWarningAction
302
294
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/billing/plan-card.tsx"],"sourcesContent":["import { useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Check, Gauge } from \"lucide-react\";\n\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { formatNumber } from \"../lib/format\";\nimport { cn } from \"../lib/cn\";\nimport { ChoiceCard } from \"../components/choice-card\";\nimport type { ChoiceCardProps } from \"../components/choice-card\";\nimport { Chip } from \"../components/chip\";\nimport { ToggleGroup } from \"../components/toggle-group\";\nimport { Button } from \"../components/button\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport { dialogClose } from \"./dialog-close\";\nimport type { WithDialogClose } from \"./dialog-close\";\nimport { BILLING_INTERVALS, billingCurrencyFor, formatPlanPrice, planPrice } from \"./plan-price\";\nimport type { BillingCurrency, BillingInterval, PlanPrices } from \"./plan-price\";\n\n/**\n * One plan of the app's catalogue (docs/billing-harmonization.md §3.1) as a page shows it:\n * server-kit's `PlanSpec` from `GET /billing/plans`, plus the words, which are the app's\n * (display names and feature lines come from its i18n, never from the provider).\n */\nexport interface BillingPlan {\n /** The plan code, lowercase on the wire (§12.16); compared without case here. */\n code: string;\n /** The app's display name, in the reader's language: \"Pro\". */\n name: string;\n /** A line under the price. */\n description?: ReactNode;\n /** Gross prices in minor units, per currency and period (§3.1, §12.17). None at all —\n * or 0 — is a free plan. */\n prices?: PlanPrices | null;\n /** Dimension → limit; `null` is unlimited (§3.1). Shown in the object's order. */\n limits?: Readonly<Record<string, number | null>>;\n /** The app's feature lines. */\n features?: readonly ReactNode[];\n /** Not choosable — a plan being retired, say. */\n disabled?: boolean;\n}\n\n/**\n * How a plan stands to the one the payer pays for now: `current` is it, `upgrade` comes\n * after it in the catalogue's order (`PlanSpec.sort`), `downgrade` before it.\n */\nexport type PlanRelation = \"current\" | \"upgrade\" | \"downgrade\";\n\n/** The plan's place against `current`, by the order of `plans`; `undefined` without a\n * current plan (a trial, an ended plan) or for a code that is not in the list. */\nexport function planRelation(\n plans: readonly Pick<BillingPlan, \"code\">[],\n code: string,\n current: string | null | undefined,\n): PlanRelation | undefined {\n if (!current) return undefined;\n const index = (c: string) => plans.findIndex((p) => p.code.toLowerCase() === c.toLowerCase());\n const at = index(code);\n const now = index(current);\n if (at < 0 || now < 0) return undefined;\n return at === now ? \"current\" : at > now ? \"upgrade\" : \"downgrade\";\n}\n\n/** What both parts need to draw a plan. */\ninterface PlanDisplayProps {\n /** The currency the prices are shown in (§2.6). */\n currency: BillingCurrency;\n /** The period the prices are shown for. */\n interval: BillingInterval;\n /** The app's name of each limit's dimension (\"Budgets\"). A dimension without one is\n * shown by its key. */\n dimensionLabels?: Readonly<Record<string, string>>;\n /** How a limit's figure is written — kastlan's storage is bytes (`formatFileSize`).\n * Default: a number in the kit's locale. */\n formatLimit?: (dimension: string, value: number) => string;\n /** Over the provider's locale. */\n locale?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\nexport interface PlanCardProps\n extends\n Omit<ChoiceCardProps, \"type\" | \"title\" | \"description\" | \"icon\" | \"indeterminate\" | \"value\" | \"labels\">,\n PlanDisplayProps {\n plan: BillingPlan;\n /** See {@link PlanRelation}. `current` draws the \"Current plan\" chip — a word in the\n * card's name, never a colour alone. */\n relation?: PlanRelation;\n /**\n * The provider's own localised price in place of the catalogue's figure (§12.17) — a\n * Merchant of Record's price preview for the buyer's country, tax included. The period\n * and \"VAT included\" stay: the law asks for a gross price, and the card says so.\n */\n pricePreview?: ReactNode;\n}\n\n/** A list from spans (`role=\"list\"`): the card is a `<label>`, which may hold phrasing\n * content only, so a `<ul>` there would be invalid HTML. */\nfunction SpanList({ name, items, icon }: { name: string; items: ReactNode[]; icon: ReactNode }) {\n if (items.length === 0) return null;\n return (\n <span role=\"list\" aria-label={name} className=\"block space-y-1\">\n {items.map((item, i) => (\n <span role=\"listitem\" key={i} className=\"flex items-start gap-1.5 text-secondary\">\n <span aria-hidden className=\"mt-[0.0625rem] flex shrink-0 text-muted [&_svg]:size-3.5\">\n {icon}\n </span>\n <span className=\"min-w-0\">\n {item}\n {/* A space between the items' texts in the flattened description. */}{\" \"}\n </span>\n </span>\n ))}\n </span>\n );\n}\n\n/**\n * One plan as a {@link ChoiceCard} radio (§7): the name (and \"Current plan\"), the GROSS\n * price in the chosen currency and period through `formatMoney` (\"CHF 49 per year, VAT\n * included\", §12.17), the limits (\"Budgets: 3\", \"Seats: Unlimited\") and the app's feature\n * lines. A plan with prices but none for this currency and period is shown, \"Not offered\n * for this billing period\", and cannot be picked.\n *\n * A radio of a set: give the cards one `name`, or use {@link PlanPicker}, which does that\n * and owns the choice. Everything else a `ChoiceCard` takes goes to it — `checked`,\n * `onCheckedChange`, `name`, `className`. Carries `data-plan` and `data-relation`.\n */\nexport function PlanCard({\n // The radio `<input>` (ChoiceCard's): a React 19 ref prop since 0.34 (MOD-4, refactor\n // audit 2026-10), where this was `forwardRef`.\n ref,\n plan,\n relation,\n pricePreview,\n currency,\n interval,\n dimensionLabels,\n formatLimit,\n locale: localeProp,\n labels: labelsProp,\n disabled,\n ...rest\n}: PlanCardProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale(localeProp);\n const price = planPrice(plan.prices, currency, interval);\n\n const priced = price.kind === \"price\";\n const figure = priced\n ? pricePreview !== undefined && pricePreview !== null\n ? pricePreview\n : formatPlanPrice(price.minor, currency, { locale })\n : price.kind === \"free\"\n ? labels.free\n : labels.notOffered;\n\n const limitLines = Object.entries(plan.limits ?? {}).map(([dimension, value]) => {\n const name = dimensionLabels?.[dimension] ?? dimension;\n const shown =\n value === null ? labels.unlimited : formatLimit ? formatLimit(dimension, value) : formatNumber(value, { locale });\n return common.fieldValue(name, shown);\n });\n\n const body = (\n <span className=\"mt-1 block space-y-2\">\n <span className=\"block\">\n <span\n data-part=\"price\"\n className={cn(\n \"tabular-nums\",\n price.kind === \"not-offered\" ? \"text-xs text-muted\" : \"text-lg font-semibold leading-tight text-primary\",\n )}\n >\n {figure}\n </span>\n {priced && (\n <>\n {\" \"}\n <span className=\"text-xs text-secondary\">\n {interval === \"year\" ? labels.perYear : labels.perMonth}\n </span>{\" \"}\n <span className=\"block text-xs text-muted\">{labels.vatIncluded}</span>\n </>\n )}{\" \"}\n </span>\n {plan.description !== undefined && plan.description !== null && (\n <span className=\"block text-secondary\">{plan.description} </span>\n )}\n <SpanList name={labels.limits} items={limitLines} icon={<Gauge />} />\n <SpanList name={labels.features} items={[...(plan.features ?? [])]} icon={<Check />} />\n </span>\n );\n\n return (\n <ChoiceCard\n ref={ref}\n {...rest}\n type=\"radio\"\n value={plan.code}\n disabled={disabled || plan.disabled || price.kind === \"not-offered\"}\n data-plan={plan.code}\n data-relation={relation}\n title={\n <>\n {plan.name}\n {relation === \"current\" && (\n <>\n {\" \"}\n <Chip size=\"xs\" tone=\"brand\" shape=\"square\" caps className=\"ms-1 align-[0.0625rem]\">\n {labels.current}\n </Chip>\n </>\n )}\n </>\n }\n description={body}\n />\n );\n}\n\n/** What {@link PlanPickerProps.onChoose} is handed: everything `POST /billing/checkout`\n * takes (§4). */\nexport interface PlanChoice {\n plan: string;\n interval: BillingInterval;\n currency: BillingCurrency;\n}\n\nexport interface PlanPickerProps extends Omit<PlanDisplayProps, \"currency\" | \"interval\"> {\n /** The catalogue, in its order (`PlanSpec.sort`): cheapest first. The order decides\n * upgrade and downgrade. */\n plans: readonly BillingPlan[];\n /**\n * The plan the payer pays for now (`plan` of the overview while `active` or\n * `past_due`). Leave it out during a trial and after a plan ended: then every plan is\n * \"Choose …\", the trial's plan included, since none is paid for yet.\n */\n current?: string | null;\n /** The picked plan, controlled. `null` while none is. */\n value?: string | null;\n /** The picked plan, uncontrolled. Default: `current`, else nothing. */\n defaultValue?: string | null;\n onValueChange?: (plan: string) => void;\n /** The period, controlled. */\n interval?: BillingInterval;\n /** The period, uncontrolled. Default `year` — lead with yearly (§12.17). */\n defaultInterval?: BillingInterval;\n onIntervalChange?: (interval: BillingInterval) => void;\n /** The currency, controlled. */\n currency?: BillingCurrency;\n /** The currency, uncontrolled. Default: the account's stored currency is the app's to\n * pass here (§12.18); without it, {@link billingCurrencyFor} the kit's locale. */\n defaultCurrency?: BillingCurrency;\n onCurrencyChange?: (currency: BillingCurrency) => void;\n /** The currencies the payer may switch between. Default: every currency a plan has a\n * price in. The switch shows only for more than one. */\n currencies?: readonly BillingCurrency[];\n /** The button: the app starts its checkout (`POST /billing/checkout`, then off to the\n * provider's page). The kit sends no request. */\n onChoose: (choice: PlanChoice) => void;\n /** The checkout is being opened: the button's spinner, no second press. */\n pending?: boolean;\n /**\n * 0.42 (§16.2 decision 5, §16.8 B): the payer's free grant runs without an end —\n * `isPermanentGrant(overview)`. A purchase under it changes nothing the payer gets\n * while the grant runs (§12.11), so the button first opens a confirm —\n * `grantWarningTitle`, `grantWarningBody`, \"Buy anyway\" (`grantWarningAction`) and\n * \"Back\" — and {@link onChoose} runs only on \"Buy anyway\". Warned, not refused.\n *\n * Not while {@link switching}: then the pick opens `PlanSwitchConfirm`, which takes\n * the same `permanentGrant` and shows the warning above its figures — one warning,\n * in the dialog where the payer agrees, rather than a confirm before a confirm.\n */\n permanentGrant?: boolean;\n /**\n * 0.42: the overview's `switching` — the app sends this pick to `PlanSwitchConfirm`\n * (§15.2.10), not the checkout. Only decides where {@link permanentGrant}'s warning\n * shows; the button and {@link onChoose} are the same either way.\n */\n switching?: boolean;\n /** The provider's localised price for a plan, or `undefined` for the catalogue's —\n * see {@link PlanCardProps.pricePreview}. */\n pricePreview?: (plan: BillingPlan, choice: Omit<PlanChoice, \"plan\">) => ReactNode | undefined;\n /** A visible question over the cards. Without one the set is named \"Plans\". */\n legend?: ReactNode;\n /** Shared by the radios. Generated if omitted. */\n name?: string;\n /** Classes for the whole picker. */\n className?: string;\n /** Classes for the card grid. Default: one column, two from `sm`, three from `lg`. */\n gridClassName?: string;\n /** Classes for every card. */\n cardClassName?: string;\n}\n\n/** Every currency some plan has a price in, CHF first. */\nfunction pricedCurrencies(plans: readonly BillingPlan[]): BillingCurrency[] {\n const found = new Set<BillingCurrency>();\n for (const p of plans) for (const c of Object.keys(p.prices ?? {})) found.add(c as BillingCurrency);\n return ([\"CHF\", \"EUR\"] as const).filter((c) => found.has(c));\n}\n\n/** Every period some plan has a price for, yearly first. */\nfunction pricedIntervals(plans: readonly BillingPlan[]): BillingInterval[] {\n const found = new Set<BillingInterval>();\n for (const p of plans)\n for (const byInterval of Object.values(p.prices ?? {}))\n for (const [k, v] of Object.entries(byInterval ?? {})) if (typeof v === \"number\") found.add(k as BillingInterval);\n return BILLING_INTERVALS.filter((i) => found.has(i));\n}\n\n/**\n * The plans to choose from (§7): the period switch — yearly first (§12.17) — and, where\n * the payer may pay in either, the currency switch (§2.6); a {@link PlanCard} per plan in\n * a radio set; and one button that says what pressing it does: \"Upgrade to Pro\", \"Switch\n * to Free\", or \"Choose Pro\" with no plan paid for yet.\n *\n * One button under the set rather than one per card: each card IS a radio — a `<label>`\n * round an `<input>` — and a button inside a label is interactive content inside\n * interactive content. Picking and committing are two steps anyway: the price under the\n * picked card is what checkout will charge.\n *\n * The button never takes the write lock: billing itself always works, a read-only payer\n * must be able to pay (§3.3). It is off, with the reason on hover and focus, while\n * nothing is picked or the current plan is.\n *\n * 0.42 (§16.8 B): under a grant without an end (`permanentGrant`) the payer is warned\n * before paying for a plan that won't apply — here, in a confirm before `onChoose`, on\n * the way to the checkout; on the way to a switch (`switching`) in `PlanSwitchConfirm`\n * instead, so the warning shows once on either path.\n */\nexport function PlanPicker({\n plans,\n current,\n value: valueProp,\n defaultValue,\n onValueChange,\n interval: intervalProp,\n defaultInterval = \"year\",\n onIntervalChange,\n currency: currencyProp,\n defaultCurrency,\n onCurrencyChange,\n currencies: currenciesProp,\n onChoose,\n pending,\n permanentGrant = false,\n switching = false,\n pricePreview,\n legend,\n name,\n className,\n gridClassName,\n cardClassName,\n dimensionLabels,\n formatLimit,\n locale: localeProp,\n labels: labelsProp,\n}: PlanPickerProps) {\n const labels = useBillingLabels(labelsProp);\n const locale = useKitLocale(localeProp);\n const generated = useId();\n /** The choice the grant warning holds back until \"Buy anyway\". */\n const [warned, setWarned] = useState<PlanChoice | null>(null);\n const groupName = name ?? generated;\n const legendId = `${generated}-legend`;\n\n const [ownValue, setOwnValue] = useState<string | null>(() => defaultValue ?? current ?? null);\n const [ownInterval, setOwnInterval] = useState<BillingInterval>(defaultInterval);\n const [ownCurrency, setOwnCurrency] = useState<BillingCurrency>(() => defaultCurrency ?? billingCurrencyFor(locale));\n const value = valueProp !== undefined ? valueProp : ownValue;\n const interval = intervalProp ?? ownInterval;\n const currency = currencyProp ?? ownCurrency;\n\n const pick = (code: string) => {\n if (valueProp === undefined) setOwnValue(code);\n onValueChange?.(code);\n };\n const setInterval = (next: BillingInterval) => {\n if (intervalProp === undefined) setOwnInterval(next);\n onIntervalChange?.(next);\n };\n const setCurrency = (next: BillingCurrency) => {\n if (currencyProp === undefined) setOwnCurrency(next);\n onCurrencyChange?.(next);\n };\n\n const currencies = currenciesProp ?? pricedCurrencies(plans);\n const intervals = pricedIntervals(plans);\n const picked = value ? plans.find((p) => p.code.toLowerCase() === value.toLowerCase()) : undefined;\n const relation = picked ? planRelation(plans, picked.code, current) : undefined;\n const pickedPrice = picked ? planPrice(picked.prices, currency, interval) : undefined;\n\n const buttonText = !picked\n ? labels.choosePlan\n : relation === \"upgrade\"\n ? labels.upgrade(picked.name)\n : relation === \"downgrade\"\n ? labels.downgrade(picked.name)\n : labels.choose(picked.name);\n const blocked = !picked\n ? labels.pickFirst\n : relation === \"current\"\n ? labels.isCurrent\n : pickedPrice?.kind === \"not-offered\"\n ? labels.notOffered\n : undefined;\n\n return (\n <div\n className={cn(\"min-w-0 space-y-3\", className)}\n data-billing-interval={interval}\n data-billing-currency={currency}\n >\n {(intervals.length > 1 || currencies.length > 1) && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {intervals.length > 1 && (\n <ToggleGroup\n aria-label={labels.interval}\n // Content-wide, so the two switches share a row and wrap only when narrow.\n className=\"w-auto\"\n options={intervals.map((i) => ({ value: i, label: i === \"year\" ? labels.yearly : labels.monthly }))}\n value={interval}\n onChange={setInterval}\n />\n )}\n {currencies.length > 1 && (\n <ToggleGroup\n aria-label={labels.currency}\n className=\"w-auto\"\n options={currencies.map((c) => ({ value: c, label: c }))}\n value={currency}\n onChange={setCurrency}\n />\n )}\n </div>\n )}\n <fieldset\n className=\"min-w-0\"\n aria-labelledby={legend !== undefined ? legendId : undefined}\n aria-label={legend === undefined ? labels.plans : undefined}\n >\n {legend !== undefined && (\n <legend id={legendId} className=\"mb-2 text-sm font-medium text-primary\">\n {legend}\n </legend>\n )}\n <div className={cn(\"grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3\", gridClassName)}>\n {plans.map((plan) => (\n <PlanCard\n key={plan.code}\n plan={plan}\n name={groupName}\n currency={currency}\n interval={interval}\n relation={planRelation(plans, plan.code, current)}\n pricePreview={pricePreview?.(plan, { interval, currency })}\n checked={picked?.code === plan.code}\n onCheckedChange={(on) => on && pick(plan.code)}\n dimensionLabels={dimensionLabels}\n formatLimit={formatLimit}\n locale={locale}\n labels={labelsProp}\n className={cardClassName}\n />\n ))}\n </div>\n </fieldset>\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant=\"default\"\n className=\"max-sm:w-full\"\n disabledReason={blocked}\n pending={pending}\n onClick={() => {\n if (!picked) return;\n const choice = { plan: picked.code, interval, currency };\n if (permanentGrant && !switching) setWarned(choice);\n else onChoose(choice);\n }}\n >\n {buttonText}\n </Button>\n </div>\n {permanentGrant && !switching && (\n <GrantWarningConfirm\n open={warned !== null}\n onClose={() => setWarned(null)}\n onConfirm={() => {\n if (warned) onChoose(warned);\n }}\n labels={labelsProp}\n />\n )}\n </div>\n );\n}\n\n/** {@link GrantWarningConfirmProps} before one of its two close props is required. */\ninterface GrantWarningConfirmFields {\n /** Shown, controlled. */\n open: boolean;\n /** \"Back\", Escape, the backdrop — and after \"Buy anyway\". 0.52: every other kit\n * dialog's `onClose` (CONTRIBUTING §4; refactor audit 2026-10-30, API-5). */\n onClose?: () => void;\n /** @deprecated 0.52 — `onClose`, as every other kit dialog; removed in 0.53. Called with\n * `false` when there is no `onClose`. */\n onOpenChange?: (open: boolean) => void;\n /** \"Buy anyway\": the app opens its checkout. The dialog closes itself after it. */\n onConfirm: () => void;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English: `grantWarningTitle`,\n * `grantWarningBody`, `grantWarningAction`. \"Back\" is `common.back`. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/** What {@link GrantWarningConfirm} takes: `onClose`, or alone the deprecated\n * `onOpenChange`, is required (0.52 code review: with neither it compiled, and nothing\n * could dismiss it). */\nexport type GrantWarningConfirmProps = WithDialogClose<GrantWarningConfirmFields>;\n\n/**\n * 0.42 (§16.8 B): the permanent-grant warning before a checkout — `grantWarningTitle`,\n * `grantWarningBody`, \"Back\" and \"Buy anyway\" (`grantWarningAction`). {@link PlanPicker}\n * shows it itself under `permanentGrant` when not `switching`. Exported for the path the\n * picker cannot see: an app that meant to switch, learned there is no subscription to\n * switch (keksdose: `/billing/plan/preview` → 409 `billing_not_subscribed`) and falls back\n * to a checkout — show this before that checkout, so a permanent-grant holder is still\n * warned. Carries `data-grant-warning`.\n */\nexport function GrantWarningConfirm({\n open,\n onClose,\n onOpenChange,\n onConfirm,\n labels: labelsProp,\n}: GrantWarningConfirmProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const close = dialogClose({ onClose, onOpenChange });\n return (\n <DialogFrame\n open={open}\n title={labels.grantWarningTitle}\n description={labels.grantWarningBody}\n onClose={close}\n data-grant-warning=\"\"\n actions={(lower) => (\n <>\n <Button variant=\"secondary\" onClick={lower}>\n {common.back}\n </Button>\n <Button\n variant=\"default\"\n onClick={() => {\n // Already closed, and lowering: a second press is no second checkout, for an\n // app that shows this dialog itself as much as for PlanPicker.\n if (!open) return;\n onConfirm();\n // The caller's close, at once — not `lower`, the panel's animated one, which\n // reports the close 220 ms later. PlanPicker clears the warned choice in it,\n // and the lowering panel is not inert: a double click on \"Buy anyway\" opened\n // the checkout twice (0.52 code review, plan-card.tsx). The caller's `open`\n // turning false still plays the exit.\n close();\n }}\n >\n {labels.grantWarningAction}\n </Button>\n </>\n )}\n />\n );\n}\n"],"mappings":";AA2GU,SA0EA,UA1EA,KAGA,YAHA;AA3GV,SAAS,OAAO,gBAAgB;AAEhC,SAAS,OAAO,aAAa;AAE7B,SAAS,uBAAuB,cAAc,oBAAoB;AAElE,SAAS,oBAAoB;AAC7B,SAAS,UAAU;AACnB,SAAS,kBAAkB;AAE3B,SAAS,YAAY;AACrB,SAAS,mBAAmB;AAC5B,SAAS,cAAc;AACvB,SAAS,mBAAmB;AAC5B,SAAS,wBAAwB;AAEjC,SAAS,mBAAmB;AAE5B,SAAS,mBAAmB,oBAAoB,iBAAiB,iBAAiB;AAkC3E,SAAS,aACd,OACA,MACA,SAC0B;AAC1B,MAAI,CAAC,QAAS,QAAO;AACrB,QAAM,QAAQ,CAAC,MAAc,MAAM,UAAU,CAAC,MAAM,EAAE,KAAK,YAAY,MAAM,EAAE,YAAY,CAAC;AAC5F,QAAM,KAAK,MAAM,IAAI;AACrB,QAAM,MAAM,MAAM,OAAO;AACzB,MAAI,KAAK,KAAK,MAAM,EAAG,QAAO;AAC9B,SAAO,OAAO,MAAM,YAAY,KAAK,MAAM,YAAY;AACzD;AAsCA,SAAS,SAAS,EAAE,MAAM,OAAO,KAAK,GAA0D;AAC9F,MAAI,MAAM,WAAW,EAAG,QAAO;AAC/B,SACE,oBAAC,UAAK,MAAK,QAAO,cAAY,MAAM,WAAU,mBAC3C,gBAAM,IAAI,CAAC,MAAM,MAChB,qBAAC,UAAK,MAAK,YAAmB,WAAU,2CACtC;AAAA,wBAAC,UAAK,eAAW,MAAC,WAAU,4DACzB,gBACH;AAAA,IACA,qBAAC,UAAK,WAAU,WACb;AAAA;AAAA,MACsE;AAAA,OACzE;AAAA,OAPyB,CAQ3B,CACD,GACH;AAEJ;AAaO,SAAS,SAAS;AAAA;AAAA;AAAA,EAGvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,SAAS,iBAAiB,UAAU;AAC1C,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,SAAS,aAAa,UAAU;AACtC,QAAM,QAAQ,UAAU,KAAK,QAAQ,UAAU,QAAQ;AAEvD,QAAM,SAAS,MAAM,SAAS;AAC9B,QAAM,SAAS,SACX,iBAAiB,UAAa,iBAAiB,OAC7C,eACA,gBAAgB,MAAM,OAAO,UAAU,EAAE,OAAO,CAAC,IACnD,MAAM,SAAS,SACb,OAAO,OACP,OAAO;AAEb,QAAM,aAAa,OAAO,QAAQ,KAAK,UAAU,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,WAAW,KAAK,MAAM;AAC/E,UAAM,OAAO,kBAAkB,SAAS,KAAK;AAC7C,UAAM,QACJ,UAAU,OAAO,OAAO,YAAY,cAAc,YAAY,WAAW,KAAK,IAAI,aAAa,OAAO,EAAE,OAAO,CAAC;AAClH,WAAO,OAAO,WAAW,MAAM,KAAK;AAAA,EACtC,CAAC;AAED,QAAM,OACJ,qBAAC,UAAK,WAAU,wBACd;AAAA,yBAAC,UAAK,WAAU,SACd;AAAA;AAAA,QAAC;AAAA;AAAA,UACC,aAAU;AAAA,UACV,WAAW;AAAA,YACT;AAAA,YACA,MAAM,SAAS,gBAAgB,uBAAuB;AAAA,UACxD;AAAA,UAEC;AAAA;AAAA,MACH;AAAA,MACC,UACC,iCACG;AAAA;AAAA,QACD,oBAAC,UAAK,WAAU,0BACb,uBAAa,SAAS,OAAO,UAAU,OAAO,UACjD;AAAA,QAAQ;AAAA,QACR,oBAAC,UAAK,WAAU,4BAA4B,iBAAO,aAAY;AAAA,SACjE;AAAA,MACC;AAAA,OACL;AAAA,IACC,KAAK,gBAAgB,UAAa,KAAK,gBAAgB,QACtD,qBAAC,UAAK,WAAU,wBAAwB;AAAA,WAAK;AAAA,MAAY;AAAA,OAAC;AAAA,IAE5D,oBAAC,YAAS,MAAM,OAAO,QAAQ,OAAO,YAAY,MAAM,oBAAC,SAAM,GAAI;AAAA,IACnE,oBAAC,YAAS,MAAM,OAAO,UAAU,OAAO,CAAC,GAAI,KAAK,YAAY,CAAC,CAAE,GAAG,MAAM,oBAAC,SAAM,GAAI;AAAA,KACvF;AAGF,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACC,GAAG;AAAA,MACJ,MAAK;AAAA,MACL,OAAO,KAAK;AAAA,MACZ,UAAU,YAAY,KAAK,YAAY,MAAM,SAAS;AAAA,MACtD,aAAW,KAAK;AAAA,MAChB,iBAAe;AAAA,MACf,OACE,iCACG;AAAA,aAAK;AAAA,QACL,aAAa,aACZ,iCACG;AAAA;AAAA,UACD,oBAAC,QAAK,MAAK,MAAK,MAAK,SAAQ,OAAM,UAAS,MAAI,MAAC,WAAU,0BACxD,iBAAO,SACV;AAAA,WACF;AAAA,SAEJ;AAAA,MAEF,aAAa;AAAA;AAAA,EACf;AAEJ;AA8EA,SAAS,iBAAiB,OAAkD;AAC1E,QAAM,QAAQ,oBAAI,IAAqB;AACvC,aAAW,KAAK,MAAO,YAAW,KAAK,OAAO,KAAK,EAAE,UAAU,CAAC,CAAC,EAAG,OAAM,IAAI,CAAoB;AAClG,SAAQ,CAAC,OAAO,KAAK,EAAY,OAAO,CAAC,MAAM,MAAM,IAAI,CAAC,CAAC;AAC7D;AAGA,SAAS,gBAAgB,OAAkD;AACzE,QAAM,QAAQ,oBAAI,IAAqB;AACvC,aAAW,KAAK;AACd,eAAW,cAAc,OAAO,OAAO,EAAE,UAAU,CAAC,CAAC;AACnD,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,cAAc,CAAC,CAAC,EAAG,KAAI,OAAO,MAAM,SAAU,OAAM,IAAI,CAAoB;AACpH,SAAO,kBAAkB,OAAO,CAAC,MAAM,MAAM,IAAI,CAAC,CAAC;AACrD;AAsBO,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA,OAAO;AAAA,EACP;AAAA,EACA;AAAA,EACA,UAAU;AAAA,EACV,kBAAkB;AAAA,EAClB;AAAA,EACA,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,YAAY;AAAA,EACZ;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR,QAAQ;AACV,GAAoB;AAClB,QAAM,SAAS,iBAAiB,UAAU;AAC1C,QAAM,SAAS,aAAa,UAAU;AACtC,QAAM,YAAY,MAAM;AAExB,QAAM,CAAC,QAAQ,SAAS,IAAI,SAA4B,IAAI;AAC5D,QAAM,YAAY,QAAQ;AAC1B,QAAM,WAAW,GAAG,SAAS;AAE7B,QAAM,CAAC,UAAU,WAAW,IAAI,SAAwB,MAAM,gBAAgB,WAAW,IAAI;AAC7F,QAAM,CAAC,aAAa,cAAc,IAAI,SAA0B,eAAe;AAC/E,QAAM,CAAC,aAAa,cAAc,IAAI,SAA0B,MAAM,mBAAmB,mBAAmB,MAAM,CAAC;AACnH,QAAM,QAAQ,cAAc,SAAY,YAAY;AACpD,QAAM,WAAW,gBAAgB;AACjC,QAAM,WAAW,gBAAgB;AAEjC,QAAM,OAAO,CAAC,SAAiB;AAC7B,QAAI,cAAc,OAAW,aAAY,IAAI;AAC7C,oBAAgB,IAAI;AAAA,EACtB;AACA,QAAM,cAAc,CAAC,SAA0B;AAC7C,QAAI,iBAAiB,OAAW,gBAAe,IAAI;AACnD,uBAAmB,IAAI;AAAA,EACzB;AACA,QAAM,cAAc,CAAC,SAA0B;AAC7C,QAAI,iBAAiB,OAAW,gBAAe,IAAI;AACnD,uBAAmB,IAAI;AAAA,EACzB;AAEA,QAAM,aAAa,kBAAkB,iBAAiB,KAAK;AAC3D,QAAM,YAAY,gBAAgB,KAAK;AACvC,QAAM,SAAS,QAAQ,MAAM,KAAK,CAAC,MAAM,EAAE,KAAK,YAAY,MAAM,MAAM,YAAY,CAAC,IAAI;AACzF,QAAM,WAAW,SAAS,aAAa,OAAO,OAAO,MAAM,OAAO,IAAI;AACtE,QAAM,cAAc,SAAS,UAAU,OAAO,QAAQ,UAAU,QAAQ,IAAI;AAE5E,QAAM,aAAa,CAAC,SAChB,OAAO,aACP,aAAa,YACX,OAAO,QAAQ,OAAO,IAAI,IAC1B,aAAa,cACX,OAAO,UAAU,OAAO,IAAI,IAC5B,OAAO,OAAO,OAAO,IAAI;AACjC,QAAM,UAAU,CAAC,SACb,OAAO,YACP,aAAa,YACX,OAAO,YACP,aAAa,SAAS,gBACpB,OAAO,aACP;AAER,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAW,GAAG,qBAAqB,SAAS;AAAA,MAC5C,yBAAuB;AAAA,MACvB,yBAAuB;AAAA,MAErB;AAAA,mBAAU,SAAS,KAAK,WAAW,SAAS,MAC5C,qBAAC,SAAI,WAAU,qCACZ;AAAA,oBAAU,SAAS,KAClB;AAAA,YAAC;AAAA;AAAA,cACC,cAAY,OAAO;AAAA,cAEnB,WAAU;AAAA,cACV,SAAS,UAAU,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,MAAM,SAAS,OAAO,SAAS,OAAO,QAAQ,EAAE;AAAA,cAClG,OAAO;AAAA,cACP,UAAU;AAAA;AAAA,UACZ;AAAA,UAED,WAAW,SAAS,KACnB;AAAA,YAAC;AAAA;AAAA,cACC,cAAY,OAAO;AAAA,cACnB,WAAU;AAAA,cACV,SAAS,WAAW,IAAI,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,EAAE,EAAE;AAAA,cACvD,OAAO;AAAA,cACP,UAAU;AAAA;AAAA,UACZ;AAAA,WAEJ;AAAA,QAEF;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,mBAAiB,WAAW,SAAY,WAAW;AAAA,YACnD,cAAY,WAAW,SAAY,OAAO,QAAQ;AAAA,YAEjD;AAAA,yBAAW,UACV,oBAAC,YAAO,IAAI,UAAU,WAAU,yCAC7B,kBACH;AAAA,cAEF,oBAAC,SAAI,WAAW,GAAG,wDAAwD,aAAa,GACrF,gBAAM,IAAI,CAAC,SACV;AAAA,gBAAC;AAAA;AAAA,kBAEC;AAAA,kBACA,MAAM;AAAA,kBACN;AAAA,kBACA;AAAA,kBACA,UAAU,aAAa,OAAO,KAAK,MAAM,OAAO;AAAA,kBAChD,cAAc,eAAe,MAAM,EAAE,UAAU,SAAS,CAAC;AAAA,kBACzD,SAAS,QAAQ,SAAS,KAAK;AAAA,kBAC/B,iBAAiB,CAAC,OAAO,MAAM,KAAK,KAAK,IAAI;AAAA,kBAC7C;AAAA,kBACA;AAAA,kBACA;AAAA,kBACA,QAAQ;AAAA,kBACR,WAAW;AAAA;AAAA,gBAbN,KAAK;AAAA,cAcZ,CACD,GACH;AAAA;AAAA;AAAA,QACF;AAAA,QACA,oBAAC,SAAI,WAAU,oBACb;AAAA,UAAC;AAAA;AAAA,YACC,MAAK;AAAA,YACL,SAAQ;AAAA,YACR,WAAU;AAAA,YACV,gBAAgB;AAAA,YAChB;AAAA,YACA,SAAS,MAAM;AACb,kBAAI,CAAC,OAAQ;AACb,oBAAM,SAAS,EAAE,MAAM,OAAO,MAAM,UAAU,SAAS;AACvD,kBAAI,kBAAkB,CAAC,UAAW,WAAU,MAAM;AAAA,kBAC7C,UAAS,MAAM;AAAA,YACtB;AAAA,YAEC;AAAA;AAAA,QACH,GACF;AAAA,QACC,kBAAkB,CAAC,aAClB;AAAA,UAAC;AAAA;AAAA,YACC,MAAM,WAAW;AAAA,YACjB,SAAS,MAAM,UAAU,IAAI;AAAA,YAC7B,WAAW,MAAM;AACf,kBAAI,OAAQ,UAAS,MAAM;AAAA,YAC7B;AAAA,YACA,QAAQ;AAAA;AAAA,QACV;AAAA;AAAA;AAAA,EAEJ;AAEJ;AAiCO,SAAS,oBAAoB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,QAAQ;AACV,GAA6B;AAC3B,QAAM,SAAS,iBAAiB,UAAU;AAC1C,QAAM,SAAS,aAAa,UAAU,qBAAqB;AAC3D,QAAM,QAAQ,YAAY,EAAE,SAAS,aAAa,CAAC;AACnD,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,OAAO,OAAO;AAAA,MACd,aAAa,OAAO;AAAA,MACpB,SAAS;AAAA,MACT,sBAAmB;AAAA,MACnB,SAAS,CAAC,UACR,iCACE;AAAA,4BAAC,UAAO,SAAQ,aAAY,SAAS,OAClC,iBAAO,MACV;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,SAAS,MAAM;AAGb,kBAAI,CAAC,KAAM;AACX,wBAAU;AAMV,oBAAM;AAAA,YACR;AAAA,YAEC,iBAAO;AAAA;AAAA,QACV;AAAA,SACF;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
1
+ {"version":3,"sources":["../../src/billing/plan-card.tsx"],"sourcesContent":["import { useId, useState } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Check, Gauge } from \"lucide-react\";\n\nimport { DEFAULT_COMMON_LABELS, useKitLabels, useKitLocale } from \"../i18n/kit-labels\";\nimport type { LabelOverride } from \"../i18n/kit-labels\";\nimport { formatNumber } from \"../lib/format\";\nimport { cn } from \"../lib/cn\";\nimport { ChoiceCard } from \"../components/choice-card\";\nimport type { ChoiceCardProps } from \"../components/choice-card\";\nimport { Chip } from \"../components/chip\";\nimport { ToggleGroup } from \"../components/toggle-group\";\nimport { Button } from \"../components/button\";\nimport { DialogFrame } from \"../components/dialog-frame\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport { BILLING_INTERVALS, billingCurrencyFor, formatPlanPrice, planPrice } from \"./plan-price\";\nimport type { BillingCurrency, BillingInterval, PlanPrices } from \"./plan-price\";\n\n/**\n * One plan of the app's catalogue (docs/billing-harmonization.md §3.1) as a page shows it:\n * server-kit's `PlanSpec` from `GET /billing/plans`, plus the words, which are the app's\n * (display names and feature lines come from its i18n, never from the provider).\n */\nexport interface BillingPlan {\n /** The plan code, lowercase on the wire (§12.16); compared without case here. */\n code: string;\n /** The app's display name, in the reader's language: \"Pro\". */\n name: string;\n /** A line under the price. */\n description?: ReactNode;\n /** Gross prices in minor units, per currency and period (§3.1, §12.17). None at all —\n * or 0 — is a free plan. */\n prices?: PlanPrices | null;\n /** Dimension → limit; `null` is unlimited (§3.1). Shown in the object's order. */\n limits?: Readonly<Record<string, number | null>>;\n /** The app's feature lines. */\n features?: readonly ReactNode[];\n /** Not choosable — a plan being retired, say. */\n disabled?: boolean;\n}\n\n/**\n * How a plan stands to the one the payer pays for now: `current` is it, `upgrade` comes\n * after it in the catalogue's order (`PlanSpec.sort`), `downgrade` before it.\n */\nexport type PlanRelation = \"current\" | \"upgrade\" | \"downgrade\";\n\n/** The plan's place against `current`, by the order of `plans`; `undefined` without a\n * current plan (a trial, an ended plan) or for a code that is not in the list. */\nexport function planRelation(\n plans: readonly Pick<BillingPlan, \"code\">[],\n code: string,\n current: string | null | undefined,\n): PlanRelation | undefined {\n if (!current) return undefined;\n const index = (c: string) => plans.findIndex((p) => p.code.toLowerCase() === c.toLowerCase());\n const at = index(code);\n const now = index(current);\n if (at < 0 || now < 0) return undefined;\n return at === now ? \"current\" : at > now ? \"upgrade\" : \"downgrade\";\n}\n\n/** What both parts need to draw a plan. */\ninterface PlanDisplayProps {\n /** The currency the prices are shown in (§2.6). */\n currency: BillingCurrency;\n /** The period the prices are shown for. */\n interval: BillingInterval;\n /** The app's name of each limit's dimension (\"Budgets\"). A dimension without one is\n * shown by its key. */\n dimensionLabels?: Readonly<Record<string, string>>;\n /** How a limit's figure is written — kastlan's storage is bytes (`formatFileSize`).\n * Default: a number in the kit's locale. */\n formatLimit?: (dimension: string, value: number) => string;\n /** Over the provider's locale. */\n locale?: string;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */\n labels?: LabelOverride<BillingLabels>;\n}\n\nexport interface PlanCardProps\n extends\n Omit<ChoiceCardProps, \"type\" | \"title\" | \"description\" | \"icon\" | \"indeterminate\" | \"value\" | \"labels\">,\n PlanDisplayProps {\n plan: BillingPlan;\n /** See {@link PlanRelation}. `current` draws the \"Current plan\" chip — a word in the\n * card's name, never a colour alone. */\n relation?: PlanRelation;\n /**\n * The provider's own localised price in place of the catalogue's figure (§12.17) — a\n * Merchant of Record's price preview for the buyer's country, tax included. The period\n * and \"VAT included\" stay: the law asks for a gross price, and the card says so.\n */\n pricePreview?: ReactNode;\n}\n\n/** A list from spans (`role=\"list\"`): the card is a `<label>`, which may hold phrasing\n * content only, so a `<ul>` there would be invalid HTML. */\nfunction SpanList({ name, items, icon }: { name: string; items: ReactNode[]; icon: ReactNode }) {\n if (items.length === 0) return null;\n return (\n <span role=\"list\" aria-label={name} className=\"block space-y-1\">\n {items.map((item, i) => (\n <span role=\"listitem\" key={i} className=\"flex items-start gap-1.5 text-secondary\">\n <span aria-hidden className=\"mt-[0.0625rem] flex shrink-0 text-muted [&_svg]:size-3.5\">\n {icon}\n </span>\n <span className=\"min-w-0\">\n {item}\n {/* A space between the items' texts in the flattened description. */}{\" \"}\n </span>\n </span>\n ))}\n </span>\n );\n}\n\n/**\n * One plan as a {@link ChoiceCard} radio (§7): the name (and \"Current plan\"), the GROSS\n * price in the chosen currency and period through `formatMoney` (\"CHF 49 per year, VAT\n * included\", §12.17), the limits (\"Budgets: 3\", \"Seats: Unlimited\") and the app's feature\n * lines. A plan with prices but none for this currency and period is shown, \"Not offered\n * for this billing period\", and cannot be picked.\n *\n * A radio of a set: give the cards one `name`, or use {@link PlanPicker}, which does that\n * and owns the choice. Everything else a `ChoiceCard` takes goes to it — `checked`,\n * `onCheckedChange`, `name`, `className`. Carries `data-plan` and `data-relation`.\n */\nexport function PlanCard({\n // The radio `<input>` (ChoiceCard's): a React 19 ref prop since 0.34 (MOD-4, refactor\n // audit 2026-10), where this was `forwardRef`.\n ref,\n plan,\n relation,\n pricePreview,\n currency,\n interval,\n dimensionLabels,\n formatLimit,\n locale: localeProp,\n labels: labelsProp,\n disabled,\n ...rest\n}: PlanCardProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n const locale = useKitLocale(localeProp);\n const price = planPrice(plan.prices, currency, interval);\n\n const priced = price.kind === \"price\";\n const figure = priced\n ? pricePreview !== undefined && pricePreview !== null\n ? pricePreview\n : formatPlanPrice(price.minor, currency, { locale })\n : price.kind === \"free\"\n ? labels.free\n : labels.notOffered;\n\n const limitLines = Object.entries(plan.limits ?? {}).map(([dimension, value]) => {\n const name = dimensionLabels?.[dimension] ?? dimension;\n const shown =\n value === null ? labels.unlimited : formatLimit ? formatLimit(dimension, value) : formatNumber(value, { locale });\n return common.fieldValue(name, shown);\n });\n\n const body = (\n <span className=\"mt-1 block space-y-2\">\n <span className=\"block\">\n <span\n data-part=\"price\"\n className={cn(\n \"tabular-nums\",\n price.kind === \"not-offered\" ? \"text-xs text-muted\" : \"text-lg font-semibold leading-tight text-primary\",\n )}\n >\n {figure}\n </span>\n {priced && (\n <>\n {\" \"}\n <span className=\"text-xs text-secondary\">\n {interval === \"year\" ? labels.perYear : labels.perMonth}\n </span>{\" \"}\n <span className=\"block text-xs text-muted\">{labels.vatIncluded}</span>\n </>\n )}{\" \"}\n </span>\n {plan.description !== undefined && plan.description !== null && (\n <span className=\"block text-secondary\">{plan.description} </span>\n )}\n <SpanList name={labels.limits} items={limitLines} icon={<Gauge />} />\n <SpanList name={labels.features} items={[...(plan.features ?? [])]} icon={<Check />} />\n </span>\n );\n\n return (\n <ChoiceCard\n ref={ref}\n {...rest}\n type=\"radio\"\n value={plan.code}\n disabled={disabled || plan.disabled || price.kind === \"not-offered\"}\n data-plan={plan.code}\n data-relation={relation}\n title={\n <>\n {plan.name}\n {relation === \"current\" && (\n <>\n {\" \"}\n <Chip size=\"xs\" tone=\"brand\" shape=\"square\" caps className=\"ms-1 align-[0.0625rem]\">\n {labels.current}\n </Chip>\n </>\n )}\n </>\n }\n description={body}\n />\n );\n}\n\n/** What {@link PlanPickerProps.onChoose} is handed: everything `POST /billing/checkout`\n * takes (§4). */\nexport interface PlanChoice {\n plan: string;\n interval: BillingInterval;\n currency: BillingCurrency;\n}\n\nexport interface PlanPickerProps extends Omit<PlanDisplayProps, \"currency\" | \"interval\"> {\n /** The catalogue, in its order (`PlanSpec.sort`): cheapest first. The order decides\n * upgrade and downgrade. */\n plans: readonly BillingPlan[];\n /**\n * The plan the payer pays for now (`plan` of the overview while `active` or\n * `past_due`). Leave it out during a trial and after a plan ended: then every plan is\n * \"Choose …\", the trial's plan included, since none is paid for yet.\n */\n current?: string | null;\n /** The picked plan, controlled. `null` while none is. */\n value?: string | null;\n /** The picked plan, uncontrolled. Default: `current`, else nothing. */\n defaultValue?: string | null;\n onValueChange?: (plan: string) => void;\n /** The period, controlled. */\n interval?: BillingInterval;\n /** The period, uncontrolled. Default `year` — lead with yearly (§12.17). */\n defaultInterval?: BillingInterval;\n onIntervalChange?: (interval: BillingInterval) => void;\n /** The currency, controlled. */\n currency?: BillingCurrency;\n /** The currency, uncontrolled. Default: the account's stored currency is the app's to\n * pass here (§12.18); without it, {@link billingCurrencyFor} the kit's locale. */\n defaultCurrency?: BillingCurrency;\n onCurrencyChange?: (currency: BillingCurrency) => void;\n /** The currencies the payer may switch between. Default: every currency a plan has a\n * price in. The switch shows only for more than one. */\n currencies?: readonly BillingCurrency[];\n /** The button: the app starts its checkout (`POST /billing/checkout`, then off to the\n * provider's page). The kit sends no request. */\n onChoose: (choice: PlanChoice) => void;\n /** The checkout is being opened: the button's spinner, no second press. */\n pending?: boolean;\n /**\n * 0.42 (§16.2 decision 5, §16.8 B): the payer's free grant runs without an end —\n * `isPermanentGrant(overview)`. A purchase under it changes nothing the payer gets\n * while the grant runs (§12.11), so the button first opens a confirm —\n * `grantWarningTitle`, `grantWarningBody`, \"Buy anyway\" (`grantWarningAction`) and\n * \"Back\" — and {@link onChoose} runs only on \"Buy anyway\". Warned, not refused.\n *\n * Not while {@link switching}: then the pick opens `PlanSwitchConfirm`, which takes\n * the same `permanentGrant` and shows the warning above its figures — one warning,\n * in the dialog where the payer agrees, rather than a confirm before a confirm.\n */\n permanentGrant?: boolean;\n /**\n * 0.42: the overview's `switching` — the app sends this pick to `PlanSwitchConfirm`\n * (§15.2.10), not the checkout. Only decides where {@link permanentGrant}'s warning\n * shows; the button and {@link onChoose} are the same either way.\n */\n switching?: boolean;\n /** The provider's localised price for a plan, or `undefined` for the catalogue's —\n * see {@link PlanCardProps.pricePreview}. */\n pricePreview?: (plan: BillingPlan, choice: Omit<PlanChoice, \"plan\">) => ReactNode | undefined;\n /** A visible question over the cards. Without one the set is named \"Plans\". */\n legend?: ReactNode;\n /** Shared by the radios. Generated if omitted. */\n name?: string;\n /** Classes for the whole picker. */\n className?: string;\n /** Classes for the card grid. Default: one column, two from `sm`, three from `lg`. */\n gridClassName?: string;\n /** Classes for every card. */\n cardClassName?: string;\n}\n\n/** Every currency some plan has a price in, CHF first. */\nfunction pricedCurrencies(plans: readonly BillingPlan[]): BillingCurrency[] {\n const found = new Set<BillingCurrency>();\n for (const p of plans) for (const c of Object.keys(p.prices ?? {})) found.add(c as BillingCurrency);\n return ([\"CHF\", \"EUR\"] as const).filter((c) => found.has(c));\n}\n\n/** Every period some plan has a price for, yearly first. */\nfunction pricedIntervals(plans: readonly BillingPlan[]): BillingInterval[] {\n const found = new Set<BillingInterval>();\n for (const p of plans)\n for (const byInterval of Object.values(p.prices ?? {}))\n for (const [k, v] of Object.entries(byInterval ?? {})) if (typeof v === \"number\") found.add(k as BillingInterval);\n return BILLING_INTERVALS.filter((i) => found.has(i));\n}\n\n/**\n * The plans to choose from (§7): the period switch — yearly first (§12.17) — and, where\n * the payer may pay in either, the currency switch (§2.6); a {@link PlanCard} per plan in\n * a radio set; and one button that says what pressing it does: \"Upgrade to Pro\", \"Switch\n * to Free\", or \"Choose Pro\" with no plan paid for yet.\n *\n * One button under the set rather than one per card: each card IS a radio — a `<label>`\n * round an `<input>` — and a button inside a label is interactive content inside\n * interactive content. Picking and committing are two steps anyway: the price under the\n * picked card is what checkout will charge.\n *\n * The button never takes the write lock: billing itself always works, a read-only payer\n * must be able to pay (§3.3). It is off, with the reason on hover and focus, while\n * nothing is picked or the current plan is.\n *\n * 0.42 (§16.8 B): under a grant without an end (`permanentGrant`) the payer is warned\n * before paying for a plan that won't apply — here, in a confirm before `onChoose`, on\n * the way to the checkout; on the way to a switch (`switching`) in `PlanSwitchConfirm`\n * instead, so the warning shows once on either path.\n */\nexport function PlanPicker({\n plans,\n current,\n value: valueProp,\n defaultValue,\n onValueChange,\n interval: intervalProp,\n defaultInterval = \"year\",\n onIntervalChange,\n currency: currencyProp,\n defaultCurrency,\n onCurrencyChange,\n currencies: currenciesProp,\n onChoose,\n pending,\n permanentGrant = false,\n switching = false,\n pricePreview,\n legend,\n name,\n className,\n gridClassName,\n cardClassName,\n dimensionLabels,\n formatLimit,\n locale: localeProp,\n labels: labelsProp,\n}: PlanPickerProps) {\n const labels = useBillingLabels(labelsProp);\n const locale = useKitLocale(localeProp);\n const generated = useId();\n /** The choice the grant warning holds back until \"Buy anyway\". */\n const [warned, setWarned] = useState<PlanChoice | null>(null);\n const groupName = name ?? generated;\n const legendId = `${generated}-legend`;\n\n const [ownValue, setOwnValue] = useState<string | null>(() => defaultValue ?? current ?? null);\n const [ownInterval, setOwnInterval] = useState<BillingInterval>(defaultInterval);\n const [ownCurrency, setOwnCurrency] = useState<BillingCurrency>(() => defaultCurrency ?? billingCurrencyFor(locale));\n const value = valueProp !== undefined ? valueProp : ownValue;\n const interval = intervalProp ?? ownInterval;\n const currency = currencyProp ?? ownCurrency;\n\n const pick = (code: string) => {\n if (valueProp === undefined) setOwnValue(code);\n onValueChange?.(code);\n };\n const setInterval = (next: BillingInterval) => {\n if (intervalProp === undefined) setOwnInterval(next);\n onIntervalChange?.(next);\n };\n const setCurrency = (next: BillingCurrency) => {\n if (currencyProp === undefined) setOwnCurrency(next);\n onCurrencyChange?.(next);\n };\n\n const currencies = currenciesProp ?? pricedCurrencies(plans);\n const intervals = pricedIntervals(plans);\n const picked = value ? plans.find((p) => p.code.toLowerCase() === value.toLowerCase()) : undefined;\n const relation = picked ? planRelation(plans, picked.code, current) : undefined;\n const pickedPrice = picked ? planPrice(picked.prices, currency, interval) : undefined;\n\n const buttonText = !picked\n ? labels.choosePlan\n : relation === \"upgrade\"\n ? labels.upgrade(picked.name)\n : relation === \"downgrade\"\n ? labels.downgrade(picked.name)\n : labels.choose(picked.name);\n const blocked = !picked\n ? labels.pickFirst\n : relation === \"current\"\n ? labels.isCurrent\n : pickedPrice?.kind === \"not-offered\"\n ? labels.notOffered\n : undefined;\n\n return (\n <div\n className={cn(\"min-w-0 space-y-3\", className)}\n data-billing-interval={interval}\n data-billing-currency={currency}\n >\n {(intervals.length > 1 || currencies.length > 1) && (\n <div className=\"flex flex-wrap items-center gap-2\">\n {intervals.length > 1 && (\n <ToggleGroup\n aria-label={labels.interval}\n // Content-wide, so the two switches share a row and wrap only when narrow.\n className=\"w-auto\"\n options={intervals.map((i) => ({ value: i, label: i === \"year\" ? labels.yearly : labels.monthly }))}\n value={interval}\n onChange={setInterval}\n />\n )}\n {currencies.length > 1 && (\n <ToggleGroup\n aria-label={labels.currency}\n className=\"w-auto\"\n options={currencies.map((c) => ({ value: c, label: c }))}\n value={currency}\n onChange={setCurrency}\n />\n )}\n </div>\n )}\n <fieldset\n className=\"min-w-0\"\n aria-labelledby={legend !== undefined ? legendId : undefined}\n aria-label={legend === undefined ? labels.plans : undefined}\n >\n {legend !== undefined && (\n <legend id={legendId} className=\"mb-2 text-sm font-medium text-primary\">\n {legend}\n </legend>\n )}\n <div className={cn(\"grid grid-cols-1 gap-2 sm:grid-cols-2 lg:grid-cols-3\", gridClassName)}>\n {plans.map((plan) => (\n <PlanCard\n key={plan.code}\n plan={plan}\n name={groupName}\n currency={currency}\n interval={interval}\n relation={planRelation(plans, plan.code, current)}\n pricePreview={pricePreview?.(plan, { interval, currency })}\n checked={picked?.code === plan.code}\n onCheckedChange={(on) => on && pick(plan.code)}\n dimensionLabels={dimensionLabels}\n formatLimit={formatLimit}\n locale={locale}\n labels={labelsProp}\n className={cardClassName}\n />\n ))}\n </div>\n </fieldset>\n <div className=\"flex justify-end\">\n <Button\n type=\"button\"\n variant=\"default\"\n className=\"max-sm:w-full\"\n disabledReason={blocked}\n pending={pending}\n onClick={() => {\n if (!picked) return;\n const choice = { plan: picked.code, interval, currency };\n if (permanentGrant && !switching) setWarned(choice);\n else onChoose(choice);\n }}\n >\n {buttonText}\n </Button>\n </div>\n {permanentGrant && !switching && (\n <GrantWarningConfirm\n open={warned !== null}\n onClose={() => setWarned(null)}\n onConfirm={() => {\n if (warned) onChoose(warned);\n }}\n labels={labelsProp}\n />\n )}\n </div>\n );\n}\n\nexport interface GrantWarningConfirmProps {\n /** Shown, controlled. */\n open: boolean;\n /** \"Back\", Escape, the backdrop — and after \"Buy anyway\". 0.52: every other kit\n * dialog's `onClose` (CONTRIBUTING §4; refactor audit 2026-10-30, API-5); required\n * since 0.53 removed `onOpenChange` (deprecated in 0.52). */\n onClose: () => void;\n /** \"Buy anyway\": the app opens its checkout. The dialog closes itself after it. */\n onConfirm: () => void;\n /** Prop > `<UiKitProvider labels={{ billing }}>` > English: `grantWarningTitle`,\n * `grantWarningBody`, `grantWarningAction`. \"Back\" is `common.back`. */\n labels?: LabelOverride<BillingLabels>;\n}\n\n/**\n * 0.42 (§16.8 B): the permanent-grant warning before a checkout — `grantWarningTitle`,\n * `grantWarningBody`, \"Back\" and \"Buy anyway\" (`grantWarningAction`). {@link PlanPicker}\n * shows it itself under `permanentGrant` when not `switching`. Exported for the path the\n * picker cannot see: an app that meant to switch, learned there is no subscription to\n * switch (keksdose: `/billing/plan/preview` → 409 `billing_not_subscribed`) and falls back\n * to a checkout — show this before that checkout, so a permanent-grant holder is still\n * warned. Carries `data-grant-warning`.\n */\nexport function GrantWarningConfirm({ open, onClose, onConfirm, labels: labelsProp }: GrantWarningConfirmProps) {\n const labels = useBillingLabels(labelsProp);\n const common = useKitLabels(\"common\", DEFAULT_COMMON_LABELS);\n return (\n <DialogFrame\n open={open}\n title={labels.grantWarningTitle}\n description={labels.grantWarningBody}\n onClose={onClose}\n data-grant-warning=\"\"\n actions={(lower) => (\n <>\n <Button variant=\"secondary\" onClick={lower}>\n {common.back}\n </Button>\n <Button\n variant=\"default\"\n onClick={() => {\n // Already closed, and lowering: a second press is no second checkout, for an\n // app that shows this dialog itself as much as for PlanPicker.\n if (!open) return;\n onConfirm();\n // The caller's close, at once — not `lower`, the panel's animated one, which\n // reports the close 220 ms later. PlanPicker clears the warned choice in it,\n // and the lowering panel is not inert: a double click on \"Buy anyway\" opened\n // the checkout twice (0.52 code review, plan-card.tsx). The caller's `open`\n // turning false still plays the exit.\n onClose();\n }}\n >\n {labels.grantWarningAction}\n </Button>\n </>\n )}\n />\n );\n}\n"],"mappings":"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","names":[]}
@@ -1,7 +1,6 @@
1
1
  import type { LabelOverride } from "../i18n/kit-labels.js";
2
2
  import type { PluralForms } from "../i18n/plurals.js";
3
3
  import type { BillingLabels } from "./billing-labels.js";
4
- import type { WithDialogClose } from "./dialog-close.js";
5
4
  import type { PlanSwitchPreview } from "./billing-overview.js";
6
5
  import type { BillingInterval } from "./plan-price.js";
7
6
  /**
@@ -22,16 +21,13 @@ export interface PlanSwitchSide {
22
21
  plan: string;
23
22
  interval: BillingInterval;
24
23
  }
25
- /** {@link PlanSwitchConfirmProps} before one of its two close props is required. */
26
- interface PlanSwitchConfirmFields {
24
+ export interface PlanSwitchConfirmProps {
27
25
  open: boolean;
28
26
  /** The payer dismissed it — Cancel, Escape, the backdrop. Confirm leaves it open: close
29
27
  * it once the switch answers. 0.52: every other kit dialog's `onClose` (CONTRIBUTING
30
- * §4; refactor audit 2026-10-30, API-5). */
31
- onClose?: () => void;
32
- /** @deprecated 0.52 — `onClose`, as every other kit dialog; removed in 0.53. Called with
33
- * `false` when there is no `onClose`. */
34
- onOpenChange?: (open: boolean) => void;
28
+ * §4; refactor audit 2026-10-30, API-5); required since 0.53 removed `onOpenChange`
29
+ * (deprecated in 0.52). */
30
+ onClose: () => void;
35
31
  /** What the payer pays now: the overview's `paid`. */
36
32
  from: PlanSwitchSide;
37
33
  /** The pick. */
@@ -70,10 +66,6 @@ interface PlanSwitchConfirmFields {
70
66
  /** Prop > `<UiKitProvider labels={{ billing }}>` > English. */
71
67
  labels?: LabelOverride<BillingLabels>;
72
68
  }
73
- /** What {@link PlanSwitchConfirm} takes: `onClose`, or alone the deprecated
74
- * `onOpenChange`, is required (0.52 code review: with neither it compiled, and nothing
75
- * could dismiss it). */
76
- export type PlanSwitchConfirmProps = WithDialogClose<PlanSwitchConfirmFields>;
77
69
  /**
78
70
  * The confirm for a plan switch (§15.2.10): from → to, each with its billing period;
79
71
  * Paddle's preview — "Charged now" (an upgrade) or "Credited towards your next charges"
@@ -96,5 +88,4 @@ export type PlanSwitchConfirmProps = WithDialogClose<PlanSwitchConfirmFields>;
96
88
  * change plan, as they may pay. A demo lock still holds it, as the server's demo guard
97
89
  * does. The kit sends no request.
98
90
  */
99
- export declare function PlanSwitchConfirm({ open, onClose, onOpenChange, from, to, preview, pending, planName, dimensionLabels, formatLimit, onConfirm, locale: localeProp, permanentGrant, labels: labelsProp, }: PlanSwitchConfirmProps): import("react").JSX.Element;
100
- export {};
91
+ export declare function PlanSwitchConfirm({ open, onClose, from, to, preview, pending, planName, dimensionLabels, formatLimit, onConfirm, locale: localeProp, permanentGrant, labels: labelsProp, }: PlanSwitchConfirmProps): import("react").JSX.Element;
@@ -12,12 +12,10 @@ import { Spinner } from "../components/spinner.js";
12
12
  import { COMMIT_EXCEPT_BILLING } from "../components/write-lock.js";
13
13
  import { DEFAULT_PLAN_CHANGE_LABELS } from "./plan-change-labels.js";
14
14
  import { useBillingLabels } from "./billing-labels.js";
15
- import { dialogClose } from "./dialog-close.js";
16
15
  import { minorToMajor } from "./plan-price.js";
17
16
  function PlanSwitchConfirm({
18
17
  open,
19
18
  onClose,
20
- onOpenChange,
21
19
  from,
22
20
  to,
23
21
  preview,
@@ -56,7 +54,7 @@ function PlanSwitchConfirm({
56
54
  open,
57
55
  title: labels.switchTitle,
58
56
  description: labels.switchFromTo(side(from), side(to)),
59
- onClose: dialogClose({ onClose, onOpenChange }),
57
+ onClose,
60
58
  busy: pending,
61
59
  "aria-busy": preview === void 0 || void 0,
62
60
  actions: (close) => /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/billing/plan-switch.tsx"],"sourcesContent":["import { 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 { useDayFormatter } from \"../lib/day-format\";\nimport { 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 \"./plan-change-labels\";\nimport { useBillingLabels } from \"./billing-labels\";\nimport type { BillingLabels } from \"./billing-labels\";\nimport { dialogClose } from \"./dialog-close\";\nimport type { WithDialogClose } from \"./dialog-close\";\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\n/** {@link PlanSwitchConfirmProps} before one of its two close props is required. */\ninterface PlanSwitchConfirmFields {\n open: boolean;\n /** The payer dismissed it — Cancel, Escape, the backdrop. Confirm leaves it open: close\n * it once the switch answers. 0.52: every other kit dialog's `onClose` (CONTRIBUTING\n * §4; refactor audit 2026-10-30, API-5). */\n onClose?: () => void;\n /** @deprecated 0.52 — `onClose`, as every other kit dialog; removed in 0.53. Called with\n * `false` when there is no `onClose`. */\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 neither\n * Escape, the backdrop nor Back closes it. */\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. The next charge's date\n * is written by the `<UiKitProvider formatDate>` when the app set one (0.52). */\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/** What {@link PlanSwitchConfirm} takes: `onClose`, or alone the deprecated\n * `onOpenChange`, is required (0.52 code review: with neither it compiled, and nothing\n * could dismiss it). */\nexport type PlanSwitchConfirmProps = WithDialogClose<PlanSwitchConfirmFields>;\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 * onClose={() => 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 onClose,\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 const day = useDayFormatter(undefined, 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) => 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={dialogClose({ onClose, onOpenChange })}\n busy={pending}\n // A preview that loads is busy too, but holds nothing: Cancel stays a way out.\n aria-busy={preview === undefined || 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\">{labels.switchNextCharge(day(preview.next_billed_at))}</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 { 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 { useDayFormatter } from \"../lib/day-format\";\nimport { 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 \"./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 /** The payer dismissed it — Cancel, Escape, the backdrop. Confirm leaves it open: close\n * it once the switch answers. 0.52: every other kit dialog's `onClose` (CONTRIBUTING\n * §4; refactor audit 2026-10-30, API-5); required since 0.53 removed `onOpenChange`\n * (deprecated in 0.52). */\n onClose: () => 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 neither\n * Escape, the backdrop nor Back closes it. */\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. The next charge's date\n * is written by the `<UiKitProvider formatDate>` when the app set one (0.52). */\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 * onClose={() => 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 onClose,\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 const day = useDayFormatter(undefined, 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) => 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={onClose}\n busy={pending}\n // A preview that loads is busy too, but holds nothing: Cancel stays a way out.\n aria-busy={preview === undefined || 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\">{labels.switchNextCharge(day(preview.next_billed_at))}</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":[]}