@cosmicdrift/kumiko-samples 0.387.0 → 0.389.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 (274) hide show
  1. package/package.json +1 -1
  2. package/packages/bundled-features/package.json +8 -1
  3. package/packages/bundled-features/src/agent-tools/changes.json +12 -0
  4. package/packages/bundled-features/src/agent-tools/index.ts +1 -1
  5. package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +48 -32
  6. package/packages/bundled-features/src/audit/changes.json +12 -0
  7. package/packages/bundled-features/src/audit/feature.ts +1 -0
  8. package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
  9. package/packages/bundled-features/src/auth-email-password/changes.json +12 -0
  10. package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
  11. package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
  12. package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
  13. package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +4 -0
  14. package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +8 -0
  15. package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
  16. package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
  17. package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
  18. package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
  19. package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
  20. package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
  21. package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
  22. package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
  23. package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
  24. package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
  25. package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
  26. package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
  27. package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
  28. package/packages/bundled-features/src/auth-mfa/changes.json +12 -0
  29. package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
  30. package/packages/bundled-features/src/auth-mfa/feature-exports.ts +2 -2
  31. package/packages/bundled-features/src/auth-mfa/feature.ts +6 -0
  32. package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +8 -5
  33. package/packages/bundled-features/src/auth-mfa/web/i18n.ts +6 -1
  34. package/packages/bundled-features/src/auth-mfa/web/mfa-code-or-recovery-field.tsx +68 -0
  35. package/packages/bundled-features/src/auth-mfa/web/mfa-disable-dialog.tsx +23 -13
  36. package/packages/bundled-features/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +23 -13
  37. package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +15 -6
  38. package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +65 -18
  39. package/packages/bundled-features/src/billing-foundation/changes.json +55 -0
  40. package/packages/bundled-features/src/billing-foundation/checkout-core.ts +32 -42
  41. package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +8 -2
  42. package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +2 -0
  43. package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +14 -3
  44. package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +3 -4
  45. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +12 -6
  46. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +35 -19
  47. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +12 -8
  48. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +14 -8
  49. package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +5 -0
  50. package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +8 -10
  51. package/packages/bundled-features/src/billing-foundation/events.ts +1 -0
  52. package/packages/bundled-features/src/billing-foundation/feature.ts +3 -1
  53. package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +71 -6
  54. package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -0
  55. package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts +65 -0
  56. package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +9 -2
  57. package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +44 -8
  58. package/packages/bundled-features/src/billing-foundation/handlers/switch-plan.write.ts +72 -82
  59. package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +14 -1
  60. package/packages/bundled-features/src/billing-foundation/i18n.ts +22 -0
  61. package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
  62. package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +62 -27
  63. package/packages/bundled-features/src/billing-foundation/plan-switch.ts +84 -0
  64. package/packages/bundled-features/src/billing-foundation/types.ts +80 -1
  65. package/packages/bundled-features/src/billing-foundation/validate-options.ts +19 -0
  66. package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +119 -47
  67. package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +89 -42
  68. package/packages/bundled-features/src/billing-foundation/web/constants.ts +4 -0
  69. package/packages/bundled-features/src/billing-foundation/web/plan-price.ts +55 -0
  70. package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +224 -0
  71. package/packages/bundled-features/src/cap-counter/cap-reservation.ts +25 -8
  72. package/packages/bundled-features/src/cap-counter/changes.json +30 -0
  73. package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
  74. package/packages/bundled-features/src/cap-counter/feature.ts +1 -0
  75. package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
  76. package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
  77. package/packages/bundled-features/src/channel-email/changes.json +6 -0
  78. package/packages/bundled-features/src/channel-email/feature.ts +1 -1
  79. package/packages/bundled-features/src/config/changes.json +18 -0
  80. package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -2
  81. package/packages/bundled-features/src/config/handlers/set.write.ts +19 -2
  82. package/packages/bundled-features/src/config/handlers/values.query.ts +4 -2
  83. package/packages/bundled-features/src/config/write-helpers.ts +27 -8
  84. package/packages/bundled-features/src/crypto-shredding/changes.json +18 -0
  85. package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +11 -10
  86. package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
  87. package/packages/bundled-features/src/delivery/changes.json +42 -0
  88. package/packages/bundled-features/src/delivery/channel-context.ts +9 -3
  89. package/packages/bundled-features/src/delivery/db/queries/preferences.ts +2 -2
  90. package/packages/bundled-features/src/delivery/delivery-service.ts +35 -5
  91. package/packages/bundled-features/src/delivery/feature.ts +1 -0
  92. package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +20 -1
  93. package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +1 -1
  94. package/packages/bundled-features/src/delivery/index.ts +5 -0
  95. package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
  96. package/packages/bundled-features/src/delivery/unsubscribe-link.ts +3 -1
  97. package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +1 -0
  98. package/packages/bundled-features/src/delivery/unsubscribe-page.ts +6 -0
  99. package/packages/bundled-features/src/delivery/unsubscribe.ts +36 -15
  100. package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
  101. package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
  102. package/packages/bundled-features/src/document-ingest-foundation/events.ts +15 -0
  103. package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
  104. package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
  105. package/packages/bundled-features/src/file-foundation/index.ts +1 -0
  106. package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -1
  107. package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +0 -2
  108. package/packages/bundled-features/src/files/README.md +1 -1
  109. package/packages/bundled-features/src/form-draft/changes.json +6 -0
  110. package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +2 -2
  111. package/packages/bundled-features/src/form-draft/decrypt-draft.ts +4 -1
  112. package/packages/bundled-features/src/host-routing/README.md +1 -1
  113. package/packages/bundled-features/src/host-routing/changes.json +12 -0
  114. package/packages/bundled-features/src/host-routing/dispatch.ts +16 -17
  115. package/packages/bundled-features/src/host-routing/options.ts +2 -1
  116. package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
  117. package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
  118. package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -0
  119. package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +17 -5
  120. package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +0 -1
  121. package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +0 -3
  122. package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -3
  123. package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +32 -16
  124. package/packages/bundled-features/src/jobs/changes.json +13 -0
  125. package/packages/bundled-features/src/jobs/feature.ts +15 -4
  126. package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +45 -40
  127. package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +63 -59
  128. package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -4
  129. package/packages/bundled-features/src/ledger/web/index.ts +1 -1
  130. package/packages/bundled-features/src/legal-pages/README.md +1 -1
  131. package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
  132. package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
  133. package/packages/bundled-features/src/mail-foundation/feature.ts +5 -3
  134. package/packages/bundled-features/src/mail-transport-smtp/changes.json +6 -0
  135. package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
  136. package/packages/bundled-features/src/mail-transport-smtp/feature.ts +9 -11
  137. package/packages/bundled-features/src/managed-pages/changes.json +6 -0
  138. package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
  139. package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
  140. package/packages/bundled-features/src/managed-pages/index.ts +1 -0
  141. package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
  142. package/packages/bundled-features/src/personal-access-tokens/feature.ts +5 -1
  143. package/packages/bundled-features/src/renderer-pdf/changes.json +6 -0
  144. package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +27 -10
  145. package/packages/bundled-features/src/secrets/changes.json +6 -0
  146. package/packages/bundled-features/src/secrets/write-gate.ts +9 -2
  147. package/packages/bundled-features/src/sessions/changes.json +7 -0
  148. package/packages/bundled-features/src/sessions/feature.ts +8 -2
  149. package/packages/bundled-features/src/sessions/index.ts +1 -1
  150. package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
  151. package/packages/bundled-features/src/shared/decrypt-stored-pii.ts +10 -0
  152. package/packages/bundled-features/src/shared/index.ts +1 -1
  153. package/packages/bundled-features/src/shared/mail-template-body.ts +10 -3
  154. package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +24 -2
  155. package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
  156. package/packages/bundled-features/src/step-dispatcher/changes.json +12 -0
  157. package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
  158. package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
  159. package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
  160. package/packages/bundled-features/src/subscription-dodo/changes.json +14 -0
  161. package/packages/bundled-features/src/subscription-dodo/client.ts +45 -0
  162. package/packages/bundled-features/src/subscription-dodo/constants.ts +24 -0
  163. package/packages/bundled-features/src/subscription-dodo/feature.ts +207 -0
  164. package/packages/bundled-features/src/subscription-dodo/index.ts +16 -0
  165. package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +345 -0
  166. package/packages/bundled-features/src/subscription-dodo/runtime.ts +126 -0
  167. package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +262 -0
  168. package/packages/bundled-features/src/subscription-stripe/changes.json +12 -0
  169. package/packages/bundled-features/src/subscription-stripe/feature.ts +1 -0
  170. package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -6
  171. package/packages/bundled-features/src/tags/feature.ts +1 -0
  172. package/packages/bundled-features/src/template-resolver/api.ts +31 -4
  173. package/packages/bundled-features/src/template-resolver/changes.json +84 -0
  174. package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
  175. package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
  176. package/packages/bundled-features/src/template-resolver/feature.ts +4 -4
  177. package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +3 -2
  178. package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +27 -5
  179. package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +3 -2
  180. package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +3 -2
  181. package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -2
  182. package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
  183. package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +18 -19
  184. package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +45 -10
  185. package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
  186. package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
  187. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +9 -5
  188. package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +8 -6
  189. package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +44 -5
  190. package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +37 -26
  191. package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +7 -2
  192. package/packages/bundled-features/src/template-resolver/seeding.ts +42 -17
  193. package/packages/bundled-features/src/template-resolver/store-template-resource.ts +74 -12
  194. package/packages/bundled-features/src/template-resolver/template-delivery-policy.ts +0 -11
  195. package/packages/bundled-features/src/template-resolver/template-meta.ts +12 -4
  196. package/packages/bundled-features/src/template-resolver/template-preview.ts +11 -3
  197. package/packages/bundled-features/src/template-resolver/web/constants.ts +7 -5
  198. package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
  199. package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
  200. package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
  201. package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
  202. package/packages/bundled-features/src/tenant/changes.json +6 -0
  203. package/packages/bundled-features/src/tenant/feature.ts +1 -0
  204. package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
  205. package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
  206. package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
  207. package/packages/bundled-features/src/tenant-lifecycle/changes.json +12 -0
  208. package/packages/bundled-features/src/tenant-lifecycle/constants.ts +5 -0
  209. package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -1
  210. package/packages/bundled-features/src/tenant-lifecycle/stages.ts +35 -1
  211. package/packages/bundled-features/src/tier-engine/changes.json +6 -0
  212. package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +4 -1
  213. package/packages/bundled-features/src/tier-engine/web/constants.ts +3 -3
  214. package/packages/bundled-features/src/user/feature.ts +1 -0
  215. package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
  216. package/packages/bundled-features/src/user-data-rights/feature.ts +1 -0
  217. package/packages/bundled-features/src/user-data-rights/index.ts +6 -1
  218. package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +3 -4
  219. package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -1
  220. package/packages/bundled-features/src/user-data-rights/web/constants.ts +6 -0
  221. package/packages/bundled-features/src/user-data-rights/web/index.ts +1 -0
  222. package/packages/bundled-features/src/waitlist/changes.json +36 -0
  223. package/packages/bundled-features/src/waitlist/feature.ts +1 -0
  224. package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
  225. package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +28 -9
  226. package/packages/bundled-features/src/waitlist/import.ts +28 -3
  227. package/packages/bundled-features/src/waitlist/legacy-migrate.ts +24 -4
  228. package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
  229. package/packages/bundled-features/src/waitlist/options.ts +5 -1
  230. package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
  231. package/samples/apps/styleguide/src/app/client.tsx +2 -0
  232. package/samples/apps/styleguide/src/app/seed.ts +6 -4
  233. package/samples/apps/styleguide/src/app/server.ts +4 -1
  234. package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
  235. package/samples/apps/styleguide/src/features/widgets/i18n.ts +11 -0
  236. package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +24 -0
  237. package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
  238. package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
  239. package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
  240. package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
  241. package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
  242. package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
  243. package/samples/apps/use-all-bundled/bin/main.ts +2 -0
  244. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +5 -0
  245. package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +90 -15
  246. package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +16 -6
  247. package/samples/apps/use-all-bundled/feature-manifest.json +136 -3
  248. package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
  249. package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
  250. package/samples/apps/use-all-bundled/package.json +2 -1
  251. package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
  252. package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
  253. package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
  254. package/samples/apps/use-all-bundled/src/run-config.ts +9 -3
  255. package/samples/recipes/auth-mfa/README.md +5 -0
  256. package/samples/recipes/auth-mfa/package.json +5 -2
  257. package/samples/recipes/auth-mfa/src/web/confirmation-code-step.tsx +49 -0
  258. package/samples/recipes/auth-mfa/tsconfig.json +8 -0
  259. package/samples/recipes/env-composition/README.md +3 -1
  260. package/samples/recipes/env-composition/src/env.ts +2 -0
  261. package/samples/recipes/golden-path/src/feature.ts +2 -1
  262. package/samples/recipes/legal-pages/README.md +1 -1
  263. package/samples/recipes/lifecycle-hooks/README.md +4 -0
  264. package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
  265. package/samples/recipes/list-table-options/README.md +50 -0
  266. package/samples/recipes/list-table-options/package.json +11 -0
  267. package/samples/recipes/list-table-options/src/feature.ts +221 -0
  268. package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
  269. package/samples/recipes/rate-limiting/README.md +18 -0
  270. package/samples/recipes/rate-limiting/src/feature.ts +12 -0
  271. package/samples/recipes/state-machine/src/events.ts +3 -2
  272. package/samples/recipes/tenant-isolation/README.md +1 -0
  273. package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
  274. package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
@@ -24,24 +24,13 @@ import {
24
24
  SubscriptionFoundationQueries,
25
25
  SubscriptionStatuses,
26
26
  } from "./constants.ts";
27
+ import { resolveConsentLocale } from "./consumer-protection/consent-locale.ts";
27
28
  import { localizedParams } from "./localized-params.ts";
29
+ import { cheapestPrice, intervalKey, pricesDiffer, renewalKey, tierPrices } from "./plan-price.ts";
30
+ import { PlanSwitchConfirmDialog } from "./plan-switch-confirm-dialog.tsx";
28
31
 
29
32
  type UseTranslation = ReturnType<typeof useTranslation>;
30
33
 
31
- function intervalKey(price: BillingPlanView["price"]): string | undefined {
32
- if (price === null || price.interval === null) return undefined;
33
- return price.intervalCount !== null && price.intervalCount > 1
34
- ? `billing-foundation.plans.everyInterval.${price.interval}`
35
- : `billing-foundation.plans.perInterval.${price.interval}`;
36
- }
37
-
38
- function renewalKey(price: BillingPlanView["price"]): string | undefined {
39
- if (price === null || price.interval === null) return undefined;
40
- return price.intervalCount !== null && price.intervalCount > 1
41
- ? `billing-foundation.consent.renewsEvery.${price.interval}`
42
- : `billing-foundation.consent.renews.${price.interval}`;
43
- }
44
-
45
34
  /** Which action the plan "would" offer if it weren't blocked — mirrors
46
35
  * plan-catalog.ts's own decision tree, since `unavailable` collapses both
47
36
  * "would checkout" and "would switch" into one action and the disabled CTA
@@ -135,12 +124,25 @@ export function BillingPlansPanel(_props: ExtensionSectionProps): ReactNode {
135
124
  const [redirecting, setRedirecting] = useState(false);
136
125
  const [consentTier, setConsentTier] = useState<string | null>(null);
137
126
  const [cancelOpen, setCancelOpen] = useState(false);
127
+ const [chosenProvider, setChosenProvider] = useState<string | null>(null);
128
+ const [switchTier, setSwitchTier] = useState<string | null>(null);
129
+ const [switchRequested, setSwitchRequested] = useState(false);
130
+
131
+ // Only present when the tenant can choose (several offered providers) or
132
+ // the single offered provider has a note; absent means the server decides.
133
+ const providers = query.data?.providers;
134
+ const activeProvider = providers?.find((p) => p.name === chosenProvider) ?? providers?.[0];
135
+ const providerArg = activeProvider ? { providerName: activeProvider.name } : {};
138
136
 
139
137
  async function orderWithConsent(
140
138
  tier: string,
141
- consent: CheckoutConsentPayload,
139
+ consent: CheckoutConsentPayload | undefined,
142
140
  ): Promise<WriteResult<{ readonly url: string }>> {
143
- const result = await checkoutMutation.mutate({ tier, consent });
141
+ const result = await checkoutMutation.mutate({
142
+ tier,
143
+ ...(consent !== undefined && { consent }),
144
+ ...providerArg,
145
+ });
144
146
  if (result.isSuccess) {
145
147
  setRedirecting(true);
146
148
  window.location.assign(result.data.url);
@@ -149,13 +151,23 @@ export function BillingPlansPanel(_props: ExtensionSectionProps): ReactNode {
149
151
  }
150
152
 
151
153
  async function handleAction(tier: string, kind: "checkout" | "switch"): Promise<void> {
152
- if (kind === "checkout" && query.data?.consumerProtection !== undefined) {
154
+ if (
155
+ kind === "checkout" &&
156
+ (query.data?.consumerProtection !== undefined || (providers?.length ?? 0) > 1)
157
+ ) {
158
+ setChosenProvider(null);
153
159
  setConsentTier(tier);
154
160
  return;
155
161
  }
162
+ if (kind === "switch" && query.data?.inAppPlanSwitch) {
163
+ setSwitchTier(tier);
164
+ return;
165
+ }
156
166
  setRedirecting(true);
157
- const mutation = kind === "checkout" ? checkoutMutation : switchMutation;
158
- const result = await mutation.mutate({ tier });
167
+ const result =
168
+ kind === "checkout"
169
+ ? await checkoutMutation.mutate({ tier, ...providerArg })
170
+ : await switchMutation.mutate({ tier });
159
171
  if (result.isSuccess) {
160
172
  window.location.assign(result.data.url);
161
173
  } else {
@@ -189,6 +201,9 @@ export function BillingPlansPanel(_props: ExtensionSectionProps): ReactNode {
189
201
  }
190
202
  if (!query.data) return null;
191
203
  const result: BillingPlansResult = query.data;
204
+ // Cards show the first provider's price; the dialog then follows the chosen one.
205
+ const shownPlans = providers?.[0]?.plans ?? result.plans;
206
+ const dialogPlans = activeProvider?.plans ?? result.plans;
192
207
 
193
208
  // The consent dialog shows the checkout error itself.
194
209
  const mutationError =
@@ -199,7 +214,11 @@ export function BillingPlansPanel(_props: ExtensionSectionProps): ReactNode {
199
214
  result.subscription !== null && !result.subscription.terminal && result.canPurchase;
200
215
 
201
216
  function toCardProps(plan: BillingPlanView) {
202
- const key = intervalKey(plan.price);
217
+ const comparable =
218
+ providers !== undefined && providers.length > 1 ? tierPrices(providers, plan.tier) : [];
219
+ const fromPrice = pricesDiffer(comparable) ? cheapestPrice(comparable) : undefined;
220
+ const shownPrice = fromPrice ?? plan.price;
221
+ const key = intervalKey(shownPrice);
203
222
  return {
204
223
  title: t(plan.labelKey),
205
224
  description:
@@ -214,13 +233,14 @@ export function BillingPlansPanel(_props: ExtensionSectionProps): ReactNode {
214
233
  price:
215
234
  !result.enabled && plan.isCurrent
216
235
  ? undefined
217
- : plan.price === null
236
+ : shownPrice === null
218
237
  ? null
219
238
  : {
220
- amount: formatMoney(plan.price.unitAmount, plan.price.currency, locale),
239
+ amount: formatMoney(shownPrice.unitAmount, shownPrice.currency, locale),
240
+ ...(fromPrice !== undefined && { prefix: t("billing-foundation.plans.priceFrom") }),
221
241
  ...(key !== undefined && {
222
242
  period: t(key, {
223
- count: new Intl.NumberFormat(locale).format(plan.price.intervalCount ?? 1),
243
+ count: new Intl.NumberFormat(locale).format(shownPrice.intervalCount ?? 1),
224
244
  }),
225
245
  }),
226
246
  },
@@ -239,12 +259,41 @@ export function BillingPlansPanel(_props: ExtensionSectionProps): ReactNode {
239
259
  // rendered plans is the current one, a synthetic current-tier card (no
240
260
  // price, no cta) keeps it visible regardless of enabled/disabled.
241
261
  const consentPlan =
242
- consentTier !== null ? result.plans.find((plan) => plan.tier === consentTier) : undefined;
262
+ consentTier !== null ? dialogPlans.find((plan) => plan.tier === consentTier) : undefined;
263
+ const switchPlan =
264
+ switchTier !== null ? shownPlans.find((plan) => plan.tier === switchTier) : undefined;
243
265
  const consumerProtection = result.consumerProtection;
244
- const currentTierInPlans = result.plans.some((plan) => plan.isCurrent);
245
- const visiblePlans = result.enabled
246
- ? result.plans
247
- : result.plans.filter((plan) => plan.isCurrent);
266
+ const noteLocale = resolveConsentLocale(locale);
267
+ const noteOf = (provider: { readonly note?: Readonly<Record<string, string>> }) =>
268
+ provider.note?.[noteLocale] ?? provider.note?.["en"];
269
+ const singleProviderNote =
270
+ providers !== undefined && providers.length === 1 && providers[0] !== undefined
271
+ ? noteOf(providers[0])
272
+ : undefined;
273
+ function providerChoiceFor(tier: string) {
274
+ if (providers === undefined || providers.length < 2 || activeProvider === undefined) {
275
+ return undefined;
276
+ }
277
+ const prices = tierPrices(providers, tier);
278
+ const showPrices = pricesDiffer(prices);
279
+ return {
280
+ value: activeProvider.name,
281
+ onChange: setChosenProvider,
282
+ options: providers.map((provider) => {
283
+ const price = provider.plans.find((plan) => plan.tier === tier)?.price;
284
+ const note = noteOf(provider);
285
+ return {
286
+ value: provider.name,
287
+ title: provider.displayName,
288
+ ...(note !== undefined && { description: note }),
289
+ ...(showPrices &&
290
+ price && { aside: formatMoney(price.unitAmount, price.currency, locale) }),
291
+ };
292
+ }),
293
+ };
294
+ }
295
+ const currentTierInPlans = shownPlans.some((plan) => plan.isCurrent);
296
+ const visiblePlans = result.enabled ? shownPlans : shownPlans.filter((plan) => plan.isCurrent);
248
297
 
249
298
  return (
250
299
  <div className="flex flex-col gap-4" data-testid="billing-plans-panel">
@@ -258,6 +307,11 @@ export function BillingPlansPanel(_props: ExtensionSectionProps): ReactNode {
258
307
  {t("billing-foundation.plans.purchaseNotAllowed")}
259
308
  </Banner>
260
309
  )}
310
+ {switchRequested && (
311
+ <Banner variant="info" testId="billing-plans-panel-switch-requested">
312
+ {t("billing-foundation.plans.planSwitchRequested")}
313
+ </Banner>
314
+ )}
261
315
  {mutationError !== null && (
262
316
  <Banner variant="error" testId="billing-plans-panel-mutation-error">
263
317
  {t(mutationError.i18nKey, mutationError.i18nParams)}
@@ -318,25 +372,43 @@ export function BillingPlansPanel(_props: ExtensionSectionProps): ReactNode {
318
372
  ))}
319
373
  {result.contact !== undefined && <BillingContactPlan contact={result.contact} />}
320
374
  </PlanGrid>
321
- {consumerProtection !== undefined &&
322
- consentPlan !== undefined &&
323
- consentPlan.price !== null && (
324
- <CheckoutConsentDialog
325
- open
326
- onOpenChange={(open) => {
327
- if (!open) setConsentTier(null);
328
- }}
329
- planName={t(consentPlan.labelKey)}
330
- price={{
331
- amount: formatMoney(consentPlan.price.unitAmount, consentPlan.price.currency, locale),
332
- renewalKey: renewalKey(consentPlan.price),
333
- count: new Intl.NumberFormat(locale).format(consentPlan.price.intervalCount ?? 1),
334
- }}
335
- consumerProtection={consumerProtection}
336
- onOrder={(consent) => orderWithConsent(consentPlan.tier, consent)}
337
- onConsentTextOutdated={() => void query.refetch()}
338
- />
339
- )}
375
+ {consentPlan !== undefined && consentPlan.price !== null && (
376
+ <CheckoutConsentDialog
377
+ open
378
+ onOpenChange={(open) => {
379
+ if (!open) setConsentTier(null);
380
+ }}
381
+ planName={t(consentPlan.labelKey)}
382
+ price={{
383
+ amount: formatMoney(consentPlan.price.unitAmount, consentPlan.price.currency, locale),
384
+ renewalKey: renewalKey(consentPlan.price),
385
+ count: new Intl.NumberFormat(locale).format(consentPlan.price.intervalCount ?? 1),
386
+ }}
387
+ consumerProtection={consumerProtection}
388
+ providerChoice={providerChoiceFor(consentPlan.tier)}
389
+ note={singleProviderNote}
390
+ onOrder={(consent) => orderWithConsent(consentPlan.tier, consent)}
391
+ onConsentTextOutdated={() => void query.refetch()}
392
+ />
393
+ )}
394
+ {switchPlan !== undefined && (
395
+ <PlanSwitchConfirmDialog
396
+ open
397
+ onOpenChange={(open) => {
398
+ if (!open) setSwitchTier(null);
399
+ }}
400
+ tier={switchPlan.tier}
401
+ fromPlanName={t(result.currentTier.labelKey)}
402
+ toPlanName={t(switchPlan.labelKey)}
403
+ consumerProtection={consumerProtection}
404
+ onApplied={() => {
405
+ setSwitchTier(null);
406
+ setSwitchRequested(true);
407
+ void query.refetch();
408
+ }}
409
+ onConsentTextOutdated={() => void query.refetch()}
410
+ />
411
+ )}
340
412
  {consumerProtection !== undefined && canManage && (
341
413
  <CancelContractDialog
342
414
  open={cancelOpen}
@@ -1,5 +1,6 @@
1
1
  import { useLocale, usePrimitives, useTranslation } from "@cosmicdrift/kumiko-renderer";
2
2
  import type { WriteResult } from "@cosmicdrift/kumiko-renderer/headless";
3
+ import { ChoiceCards } from "@cosmicdrift/kumiko-renderer/web";
3
4
  import { type ReactNode, useState } from "react";
4
5
  import type { BillingPlansResult } from "../types.ts";
5
6
  import { resolveConsentLocale } from "./consumer-protection/consent-locale.ts";
@@ -24,8 +25,24 @@ export type CheckoutConsentDialogProps = {
24
25
  readonly renewalKey?: string;
25
26
  readonly count?: string;
26
27
  };
27
- readonly consumerProtection: ConsumerProtectionView;
28
- readonly onOrder: (consent: CheckoutConsentPayload) => Promise<WriteResult<{ url: string }>>;
28
+ /** Without it the dialog shows no legal links and no consent checkboxes and orders without a consent. */
29
+ readonly consumerProtection?: ConsumerProtectionView;
30
+ /** Several providers to pick from; the parent follows `value` for price and order. */
31
+ readonly providerChoice?: {
32
+ readonly options: readonly {
33
+ readonly value: string;
34
+ readonly title: string;
35
+ readonly description?: string;
36
+ readonly aside?: string;
37
+ }[];
38
+ readonly value: string;
39
+ readonly onChange: (value: string) => void;
40
+ };
41
+ /** Provider note under the price line when there is no provider choice. */
42
+ readonly note?: string;
43
+ readonly onOrder: (
44
+ consent: CheckoutConsentPayload | undefined,
45
+ ) => Promise<WriteResult<{ url: string }>>;
29
46
  readonly onConsentTextOutdated: () => void;
30
47
  };
31
48
 
@@ -36,6 +53,8 @@ export function CheckoutConsentDialog({
36
53
  planName,
37
54
  price,
38
55
  consumerProtection,
56
+ providerChoice,
57
+ note,
39
58
  onOrder,
40
59
  onConsentTextOutdated,
41
60
  }: CheckoutConsentDialogProps): ReactNode {
@@ -48,18 +67,23 @@ export function CheckoutConsentDialog({
48
67
  const [errorText, setErrorText] = useState<string | null>(null);
49
68
 
50
69
  const consentLocale = resolveConsentLocale(uiLocale);
51
- const texts = consumerProtection.consentTexts[consentLocale];
52
- const legalLinks = consumerProtection.legalLinks[consentLocale];
70
+ const texts = consumerProtection?.consentTexts[consentLocale];
71
+ const legalLinks = consumerProtection?.legalLinks[consentLocale];
72
+ const consentGiven = consumerProtection === undefined || (earlyPerformance && withdrawalLoss);
53
73
 
54
74
  async function order(): Promise<void> {
55
75
  setSubmitting(true);
56
76
  setErrorText(null);
57
- const result = await onOrder({
58
- earlyPerformanceRequested: true,
59
- withdrawalLossAcknowledged: true,
60
- consentTextVersion: texts.consentTextVersion,
61
- locale: consentLocale,
62
- });
77
+ const result = await onOrder(
78
+ texts === undefined
79
+ ? undefined
80
+ : {
81
+ earlyPerformanceRequested: true,
82
+ withdrawalLossAcknowledged: true,
83
+ consentTextVersion: texts.consentTextVersion,
84
+ locale: consentLocale,
85
+ },
86
+ );
63
87
  if (result.isSuccess) return;
64
88
  setSubmitting(false);
65
89
  setErrorText(t(result.error.i18nKey, result.error.i18nParams));
@@ -78,6 +102,18 @@ export function CheckoutConsentDialog({
78
102
  testId="checkout-consent-dialog"
79
103
  >
80
104
  <div className="flex flex-col gap-4">
105
+ {providerChoice !== undefined && (
106
+ <div className="flex flex-col gap-2">
107
+ <Text variant="muted">{t("billing-foundation.consent.chooseProvider")}</Text>
108
+ <ChoiceCards
109
+ value={providerChoice.value}
110
+ options={providerChoice.options}
111
+ onChange={providerChoice.onChange}
112
+ ariaLabel={t("billing-foundation.consent.chooseProvider")}
113
+ testId="checkout-provider-choice"
114
+ />
115
+ </div>
116
+ )}
81
117
  <div className="flex flex-col gap-1" data-testid="checkout-consent-summary">
82
118
  <Text>
83
119
  {planName} — {price.amount}
@@ -93,38 +129,49 @@ export function CheckoutConsentDialog({
93
129
  <Text variant="muted">{t("billing-foundation.consent.cancelAnytime")}</Text>
94
130
  </>
95
131
  )}
132
+ {note !== undefined && (
133
+ <Text variant="muted" testId="checkout-consent-note">
134
+ {note}
135
+ </Text>
136
+ )}
96
137
  </div>
97
- <div className="flex flex-wrap gap-4">
98
- <Link href={legalLinks.terms} target="_blank">
99
- {t("billing-foundation.consent.link.terms")}
100
- </Link>
101
- <Link href={legalLinks.withdrawal} target="_blank">
102
- {t("billing-foundation.consent.link.withdrawal")}
103
- </Link>
104
- <Link href={legalLinks.privacy} target="_blank">
105
- {t("billing-foundation.consent.link.privacy")}
106
- </Link>
107
- </div>
108
- <Field id="consent-early-performance" label={texts.earlyPerformance} layout="inline">
109
- <Input
110
- kind="boolean"
111
- id="consent-early-performance"
112
- name="consent-early-performance"
113
- value={earlyPerformance}
114
- onChange={setEarlyPerformance}
115
- disabled={submitting}
116
- />
117
- </Field>
118
- <Field id="consent-withdrawal-loss" label={texts.withdrawalLoss} layout="inline">
119
- <Input
120
- kind="boolean"
121
- id="consent-withdrawal-loss"
122
- name="consent-withdrawal-loss"
123
- value={withdrawalLoss}
124
- onChange={setWithdrawalLoss}
125
- disabled={submitting}
126
- />
127
- </Field>
138
+ {legalLinks !== undefined && (
139
+ <div className="flex flex-wrap gap-4">
140
+ <Link href={legalLinks.terms} target="_blank">
141
+ {t("billing-foundation.consent.link.terms")}
142
+ </Link>
143
+ <Link href={legalLinks.withdrawal} target="_blank">
144
+ {t("billing-foundation.consent.link.withdrawal")}
145
+ </Link>
146
+ <Link href={legalLinks.privacy} target="_blank">
147
+ {t("billing-foundation.consent.link.privacy")}
148
+ </Link>
149
+ </div>
150
+ )}
151
+ {texts !== undefined && (
152
+ <>
153
+ <Field id="consent-early-performance" label={texts.earlyPerformance} layout="inline">
154
+ <Input
155
+ kind="boolean"
156
+ id="consent-early-performance"
157
+ name="consent-early-performance"
158
+ value={earlyPerformance}
159
+ onChange={setEarlyPerformance}
160
+ disabled={submitting}
161
+ />
162
+ </Field>
163
+ <Field id="consent-withdrawal-loss" label={texts.withdrawalLoss} layout="inline">
164
+ <Input
165
+ kind="boolean"
166
+ id="consent-withdrawal-loss"
167
+ name="consent-withdrawal-loss"
168
+ value={withdrawalLoss}
169
+ onChange={setWithdrawalLoss}
170
+ disabled={submitting}
171
+ />
172
+ </Field>
173
+ </>
174
+ )}
128
175
  {errorText !== null && (
129
176
  <Banner variant="error" testId="checkout-consent-error">
130
177
  {errorText}
@@ -140,7 +187,7 @@ export function CheckoutConsentDialog({
140
187
  {t("billing-foundation.consent.back")}
141
188
  </Button>
142
189
  <Button
143
- disabled={!earlyPerformance || !withdrawalLoss || submitting}
190
+ disabled={!consentGiven || submitting}
144
191
  loading={submitting}
145
192
  onClick={order}
146
193
  testId="checkout-consent-order"
@@ -95,6 +95,10 @@ export const SubscriptionFoundationQueries = {
95
95
  * current tier and per-plan action. Only registered when a `catalog` is
96
96
  * configured. */
97
97
  billingPlans: "billing-foundation:query:billing-plans",
98
+ /** What switching to a tier would cost right now, for providers that switch
99
+ * plans in-app (preview + apply). Only registered when a `catalog` is
100
+ * configured. */
101
+ planSwitchPreview: "billing-foundation:query:plan-switch-preview",
98
102
  /** System-only probe: does the caller tenant hold a non-terminal
99
103
  * subscription (consumerProtection only). */
100
104
  terminableSubscription: "billing-foundation:query:terminable-subscription",
@@ -0,0 +1,55 @@
1
+ import type { BillingPlanView } from "../types.ts";
2
+
3
+ type Recurrence = {
4
+ readonly interval: string | null;
5
+ readonly intervalCount: number | null;
6
+ };
7
+
8
+ export function intervalKey(price: Recurrence | null): string | undefined {
9
+ if (price === null || price.interval === null) return undefined;
10
+ return price.intervalCount !== null && price.intervalCount > 1
11
+ ? `billing-foundation.plans.everyInterval.${price.interval}`
12
+ : `billing-foundation.plans.perInterval.${price.interval}`;
13
+ }
14
+
15
+ export function renewalKey(price: Recurrence | null): string | undefined {
16
+ if (price === null || price.interval === null) return undefined;
17
+ return price.intervalCount !== null && price.intervalCount > 1
18
+ ? `billing-foundation.consent.renewsEvery.${price.interval}`
19
+ : `billing-foundation.consent.renews.${price.interval}`;
20
+ }
21
+
22
+ type PlanPrice = NonNullable<BillingPlanView["price"]>;
23
+
24
+ export function pricesDiffer(prices: readonly PlanPrice[]): boolean {
25
+ const [first, ...rest] = prices;
26
+ return (
27
+ first !== undefined &&
28
+ rest.some(
29
+ (price) =>
30
+ price.unitAmount !== first.unitAmount ||
31
+ price.currency !== first.currency ||
32
+ price.interval !== first.interval ||
33
+ price.intervalCount !== first.intervalCount,
34
+ )
35
+ );
36
+ }
37
+
38
+ /** Price of `tier` at every provider that offers it, in provider order. */
39
+ export function tierPrices(
40
+ providers: readonly { readonly plans: readonly BillingPlanView[] }[],
41
+ tier: string,
42
+ ): PlanPrice[] {
43
+ return providers.flatMap((provider) => {
44
+ const price = provider.plans.find((plan) => plan.tier === tier)?.price;
45
+ return price ? [price] : [];
46
+ });
47
+ }
48
+
49
+ export function cheapestPrice(prices: readonly PlanPrice[]): PlanPrice | undefined {
50
+ return prices.reduce<PlanPrice | undefined>(
51
+ (lowest, price) =>
52
+ lowest === undefined || price.unitAmount < lowest.unitAmount ? price : lowest,
53
+ undefined,
54
+ );
55
+ }