@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.
- package/package.json +1 -1
- package/packages/bundled-features/package.json +8 -1
- package/packages/bundled-features/src/agent-tools/changes.json +12 -0
- package/packages/bundled-features/src/agent-tools/index.ts +1 -1
- package/packages/bundled-features/src/agent-tools/tool-dispatch.ts +48 -32
- package/packages/bundled-features/src/audit/changes.json +12 -0
- package/packages/bundled-features/src/audit/feature.ts +1 -0
- package/packages/bundled-features/src/audit/handlers/audit-events.ts +1 -1
- package/packages/bundled-features/src/auth-email-password/changes.json +12 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-account-unlock.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-email-verification.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/request-password-reset.write.ts +1 -0
- package/packages/bundled-features/src/auth-email-password/handlers/signup-request.write.ts +4 -0
- package/packages/bundled-features/src/auth-email-password/handlers/token-request-handler.ts +8 -0
- package/packages/bundled-features/src/auth-email-password/magic-link-mail.ts +16 -3
- package/packages/bundled-features/src/auth-email-password/signup-activation.ts +2 -0
- package/packages/bundled-features/src/auth-email-password/web/auth-client.ts +8 -2
- package/packages/bundled-features/src/auth-email-password/web/auth-redirect.ts +23 -21
- package/packages/bundled-features/src/auth-email-password/web/default-auth-pages-gate.tsx +6 -3
- package/packages/bundled-features/src/auth-email-password/web/forgot-password-screen.tsx +11 -1
- package/packages/bundled-features/src/auth-email-password/web/index.ts +5 -0
- package/packages/bundled-features/src/auth-email-password/web/login-screen.tsx +16 -5
- package/packages/bundled-features/src/auth-email-password/web/reset-password-screen.tsx +7 -1
- package/packages/bundled-features/src/auth-email-password/web/return-path.ts +35 -0
- package/packages/bundled-features/src/auth-email-password/web/session.tsx +6 -1
- package/packages/bundled-features/src/auth-email-password/web/signup-complete-screen.tsx +11 -3
- package/packages/bundled-features/src/auth-email-password/web/signup-screen.tsx +12 -2
- package/packages/bundled-features/src/auth-mfa/changes.json +12 -0
- package/packages/bundled-features/src/auth-mfa/config.ts +3 -7
- package/packages/bundled-features/src/auth-mfa/feature-exports.ts +2 -2
- package/packages/bundled-features/src/auth-mfa/feature.ts +6 -0
- package/packages/bundled-features/src/auth-mfa/mfa-status-checker.ts +8 -5
- package/packages/bundled-features/src/auth-mfa/web/i18n.ts +6 -1
- package/packages/bundled-features/src/auth-mfa/web/mfa-code-or-recovery-field.tsx +68 -0
- package/packages/bundled-features/src/auth-mfa/web/mfa-disable-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-regenerate-recovery-dialog.tsx +23 -13
- package/packages/bundled-features/src/auth-mfa/web/mfa-setup-preauth-screen.tsx +15 -6
- package/packages/bundled-features/src/auth-mfa/web/mfa-verify-screen.tsx +65 -18
- package/packages/bundled-features/src/billing-foundation/changes.json +55 -0
- package/packages/bundled-features/src/billing-foundation/checkout-core.ts +32 -42
- package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts +8 -2
- package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts +14 -3
- package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts +3 -4
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-receipt-limit.ts +12 -6
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts +35 -19
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts +12 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-revoke.ts +14 -8
- package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-texts.ts +5 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/withdrawal-period.ts +8 -10
- package/packages/bundled-features/src/billing-foundation/events.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/feature.ts +3 -1
- package/packages/bundled-features/src/billing-foundation/handlers/billing-plans.query.ts +71 -6
- package/packages/bundled-features/src/billing-foundation/handlers/create-checkout-session.write.ts +1 -0
- package/packages/bundled-features/src/billing-foundation/handlers/plan-switch-preview.query.ts +65 -0
- package/packages/bundled-features/src/billing-foundation/handlers/process-event.write.ts +9 -2
- package/packages/bundled-features/src/billing-foundation/handlers/start-plan-checkout.write.ts +44 -8
- package/packages/bundled-features/src/billing-foundation/handlers/switch-plan.write.ts +72 -82
- package/packages/bundled-features/src/billing-foundation/handlers/sync-subscription.write.ts +14 -1
- package/packages/bundled-features/src/billing-foundation/i18n.ts +22 -0
- package/packages/bundled-features/src/billing-foundation/index.ts +2 -0
- package/packages/bundled-features/src/billing-foundation/plan-catalog.ts +62 -27
- package/packages/bundled-features/src/billing-foundation/plan-switch.ts +84 -0
- package/packages/bundled-features/src/billing-foundation/types.ts +80 -1
- package/packages/bundled-features/src/billing-foundation/validate-options.ts +19 -0
- package/packages/bundled-features/src/billing-foundation/web/billing-plans-panel.tsx +119 -47
- package/packages/bundled-features/src/billing-foundation/web/checkout-consent-dialog.tsx +89 -42
- package/packages/bundled-features/src/billing-foundation/web/constants.ts +4 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-price.ts +55 -0
- package/packages/bundled-features/src/billing-foundation/web/plan-switch-confirm-dialog.tsx +224 -0
- package/packages/bundled-features/src/cap-counter/cap-reservation.ts +25 -8
- package/packages/bundled-features/src/cap-counter/changes.json +30 -0
- package/packages/bundled-features/src/cap-counter/enforce-cap.ts +2 -1
- package/packages/bundled-features/src/cap-counter/feature.ts +1 -0
- package/packages/bundled-features/src/cap-counter/stock-cap-guard.ts +8 -1
- package/packages/bundled-features/src/cap-overview/feature.ts +1 -0
- package/packages/bundled-features/src/channel-email/changes.json +6 -0
- package/packages/bundled-features/src/channel-email/feature.ts +1 -1
- package/packages/bundled-features/src/config/changes.json +18 -0
- package/packages/bundled-features/src/config/handlers/cascade.query.ts +4 -2
- package/packages/bundled-features/src/config/handlers/set.write.ts +19 -2
- package/packages/bundled-features/src/config/handlers/values.query.ts +4 -2
- package/packages/bundled-features/src/config/write-helpers.ts +27 -8
- package/packages/bundled-features/src/crypto-shredding/changes.json +18 -0
- package/packages/bundled-features/src/crypto-shredding/handlers/forget-subject.write.ts +11 -10
- package/packages/bundled-features/src/custom-fields/lib/parse-serialized-field.ts +1 -1
- package/packages/bundled-features/src/delivery/changes.json +42 -0
- package/packages/bundled-features/src/delivery/channel-context.ts +9 -3
- package/packages/bundled-features/src/delivery/db/queries/preferences.ts +2 -2
- package/packages/bundled-features/src/delivery/delivery-service.ts +35 -5
- package/packages/bundled-features/src/delivery/feature.ts +1 -0
- package/packages/bundled-features/src/delivery/handlers/preferences.query.ts +20 -1
- package/packages/bundled-features/src/delivery/handlers/unsubscribe-page-chrome.query.ts +1 -1
- package/packages/bundled-features/src/delivery/index.ts +5 -0
- package/packages/bundled-features/src/delivery/system-channel-context.ts +33 -0
- package/packages/bundled-features/src/delivery/unsubscribe-link.ts +3 -1
- package/packages/bundled-features/src/delivery/unsubscribe-page-strings.ts +1 -0
- package/packages/bundled-features/src/delivery/unsubscribe-page.ts +6 -0
- package/packages/bundled-features/src/delivery/unsubscribe.ts +36 -15
- package/packages/bundled-features/src/document-ingest-foundation/append-missing-extract-event.ts +84 -0
- package/packages/bundled-features/src/document-ingest-foundation/changes.json +6 -0
- package/packages/bundled-features/src/document-ingest-foundation/events.ts +15 -0
- package/packages/bundled-features/src/document-ingest-foundation/index.ts +11 -0
- package/packages/bundled-features/src/feature-toggles/feature.ts +1 -0
- package/packages/bundled-features/src/file-foundation/index.ts +1 -0
- package/packages/bundled-features/src/file-foundation/read-file-ref-content.ts +7 -1
- package/packages/bundled-features/src/file-provider-s3/env-named-feature.ts +0 -2
- package/packages/bundled-features/src/files/README.md +1 -1
- package/packages/bundled-features/src/form-draft/changes.json +6 -0
- package/packages/bundled-features/src/form-draft/db/queries/cleanup.ts +2 -2
- package/packages/bundled-features/src/form-draft/decrypt-draft.ts +4 -1
- package/packages/bundled-features/src/host-routing/README.md +1 -1
- package/packages/bundled-features/src/host-routing/changes.json +12 -0
- package/packages/bundled-features/src/host-routing/dispatch.ts +16 -17
- package/packages/bundled-features/src/host-routing/options.ts +2 -1
- package/packages/bundled-features/src/host-routing/validate-options.ts +5 -0
- package/packages/bundled-features/src/inbound-mail-foundation/changes.json +18 -0
- package/packages/bundled-features/src/inbound-mail-foundation/feature.ts +1 -0
- package/packages/bundled-features/src/inbound-mail-foundation/handlers/update-account.write.ts +17 -5
- package/packages/bundled-features/src/inbound-mail-foundation/i18n.ts +0 -1
- package/packages/bundled-features/src/inbound-mail-foundation/screens.ts +0 -3
- package/packages/bundled-features/src/inbound-mail-foundation/types.ts +4 -3
- package/packages/bundled-features/src/inbound-mail-foundation/watch-supervisor.ts +32 -16
- package/packages/bundled-features/src/jobs/changes.json +13 -0
- package/packages/bundled-features/src/jobs/feature.ts +15 -4
- package/packages/bundled-features/src/jobs/handlers/tenant-failures.query.ts +45 -40
- package/packages/bundled-features/src/jobs/handlers/tenant-runs.query.ts +63 -59
- package/packages/bundled-features/src/jobs/job-run-logger.ts +5 -4
- package/packages/bundled-features/src/ledger/web/index.ts +1 -1
- package/packages/bundled-features/src/legal-pages/README.md +1 -1
- package/packages/bundled-features/src/legal-pages/seed.ts +5 -2
- package/packages/bundled-features/src/mail-foundation/changes.json +6 -0
- package/packages/bundled-features/src/mail-foundation/feature.ts +5 -3
- package/packages/bundled-features/src/mail-transport-smtp/changes.json +6 -0
- package/packages/bundled-features/src/mail-transport-smtp/config-keys.ts +1 -1
- package/packages/bundled-features/src/mail-transport-smtp/feature.ts +9 -11
- package/packages/bundled-features/src/managed-pages/changes.json +6 -0
- package/packages/bundled-features/src/managed-pages/constants.ts +1 -0
- package/packages/bundled-features/src/managed-pages/feature.ts +3 -1
- package/packages/bundled-features/src/managed-pages/index.ts +1 -0
- package/packages/bundled-features/src/notes-history/handlers/add-note.write.ts +1 -1
- package/packages/bundled-features/src/personal-access-tokens/feature.ts +5 -1
- package/packages/bundled-features/src/renderer-pdf/changes.json +6 -0
- package/packages/bundled-features/src/renderer-pdf/render-table-pdf.ts +27 -10
- package/packages/bundled-features/src/secrets/changes.json +6 -0
- package/packages/bundled-features/src/secrets/write-gate.ts +9 -2
- package/packages/bundled-features/src/sessions/changes.json +7 -0
- package/packages/bundled-features/src/sessions/feature.ts +8 -2
- package/packages/bundled-features/src/sessions/index.ts +1 -1
- package/packages/bundled-features/src/sessions/session-callbacks.ts +1 -9
- package/packages/bundled-features/src/shared/decrypt-stored-pii.ts +10 -0
- package/packages/bundled-features/src/shared/index.ts +1 -1
- package/packages/bundled-features/src/shared/mail-template-body.ts +10 -3
- package/packages/bundled-features/src/shared/purge-files-reaching-subject.ts +24 -2
- package/packages/bundled-features/src/shared/run-in-sub-transaction.ts +5 -5
- package/packages/bundled-features/src/step-dispatcher/changes.json +12 -0
- package/packages/bundled-features/src/step-dispatcher/deliver-dispatch-request.ts +17 -3
- package/packages/bundled-features/src/step-dispatcher/feature.ts +1 -0
- package/packages/bundled-features/src/step-dispatcher/mail-runner.ts +2 -1
- package/packages/bundled-features/src/subscription-dodo/changes.json +14 -0
- package/packages/bundled-features/src/subscription-dodo/client.ts +45 -0
- package/packages/bundled-features/src/subscription-dodo/constants.ts +24 -0
- package/packages/bundled-features/src/subscription-dodo/feature.ts +207 -0
- package/packages/bundled-features/src/subscription-dodo/index.ts +16 -0
- package/packages/bundled-features/src/subscription-dodo/plugin-methods.ts +345 -0
- package/packages/bundled-features/src/subscription-dodo/runtime.ts +126 -0
- package/packages/bundled-features/src/subscription-dodo/verify-webhook.ts +262 -0
- package/packages/bundled-features/src/subscription-stripe/changes.json +12 -0
- package/packages/bundled-features/src/subscription-stripe/feature.ts +1 -0
- package/packages/bundled-features/src/subscription-stripe/runtime.ts +7 -6
- package/packages/bundled-features/src/tags/feature.ts +1 -0
- package/packages/bundled-features/src/template-resolver/api.ts +31 -4
- package/packages/bundled-features/src/template-resolver/changes.json +84 -0
- package/packages/bundled-features/src/template-resolver/compile-document-body.ts +1 -0
- package/packages/bundled-features/src/template-resolver/document-ops.ts +20 -1
- package/packages/bundled-features/src/template-resolver/feature.ts +4 -4
- package/packages/bundled-features/src/template-resolver/handlers/bundle-import.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/copy-locale.write.ts +27 -5
- package/packages/bundled-features/src/template-resolver/handlers/diff-to-system.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/document-base.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/find-by-id.query.ts +3 -2
- package/packages/bundled-features/src/template-resolver/handlers/layout-source.query.ts +36 -11
- package/packages/bundled-features/src/template-resolver/handlers/list.query.ts +18 -19
- package/packages/bundled-features/src/template-resolver/handlers/overview.query.ts +45 -10
- package/packages/bundled-features/src/template-resolver/handlers/patch-draft.write.ts +1 -1
- package/packages/bundled-features/src/template-resolver/handlers/send-test-status.query.ts +22 -19
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-counts.query.ts +9 -5
- package/packages/bundled-features/src/template-resolver/handlers/tenant-override-selection.ts +8 -6
- package/packages/bundled-features/src/template-resolver/handlers/tenant-overrides.query.ts +44 -5
- package/packages/bundled-features/src/template-resolver/handlers/usage.query.ts +37 -26
- package/packages/bundled-features/src/template-resolver/handlers/validate.query.ts +7 -2
- package/packages/bundled-features/src/template-resolver/seeding.ts +42 -17
- package/packages/bundled-features/src/template-resolver/store-template-resource.ts +74 -12
- package/packages/bundled-features/src/template-resolver/template-delivery-policy.ts +0 -11
- package/packages/bundled-features/src/template-resolver/template-meta.ts +12 -4
- package/packages/bundled-features/src/template-resolver/template-preview.ts +11 -3
- package/packages/bundled-features/src/template-resolver/web/constants.ts +7 -5
- package/packages/bundled-features/src/template-resolver/web/i18n.ts +6 -0
- package/packages/bundled-features/src/template-resolver/web/index.ts +7 -0
- package/packages/bundled-features/src/template-resolver/web/mail-layout-constants.ts +8 -0
- package/packages/bundled-features/src/template-resolver/web/qualified-names.ts +1 -0
- package/packages/bundled-features/src/tenant/changes.json +6 -0
- package/packages/bundled-features/src/tenant/feature.ts +1 -0
- package/packages/bundled-features/src/tenant/handlers/member-directory.query.ts +9 -3
- package/packages/bundled-features/src/tenant/handlers/team-list.query.ts +3 -3
- package/packages/bundled-features/src/tenant/handlers/tenant-directory.query.ts +7 -4
- package/packages/bundled-features/src/tenant-lifecycle/changes.json +12 -0
- package/packages/bundled-features/src/tenant-lifecycle/constants.ts +5 -0
- package/packages/bundled-features/src/tenant-lifecycle/run-tenant-destroy.ts +5 -1
- package/packages/bundled-features/src/tenant-lifecycle/stages.ts +35 -1
- package/packages/bundled-features/src/tier-engine/changes.json +6 -0
- package/packages/bundled-features/src/tier-engine/handlers/set-tenant-tier.write.ts +4 -1
- package/packages/bundled-features/src/tier-engine/web/constants.ts +3 -3
- package/packages/bundled-features/src/user/feature.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/changes.json +12 -0
- package/packages/bundled-features/src/user-data-rights/feature.ts +1 -0
- package/packages/bundled-features/src/user-data-rights/index.ts +6 -1
- package/packages/bundled-features/src/user-data-rights/run-export-jobs.ts +3 -4
- package/packages/bundled-features/src/user-data-rights/run-forget-cleanup.ts +2 -1
- package/packages/bundled-features/src/user-data-rights/web/constants.ts +6 -0
- package/packages/bundled-features/src/user-data-rights/web/index.ts +1 -0
- package/packages/bundled-features/src/waitlist/changes.json +36 -0
- package/packages/bundled-features/src/waitlist/feature.ts +1 -0
- package/packages/bundled-features/src/waitlist/handlers/export-purge.write.ts +2 -1
- package/packages/bundled-features/src/waitlist/handlers/submit.write.ts +28 -9
- package/packages/bundled-features/src/waitlist/import.ts +28 -3
- package/packages/bundled-features/src/waitlist/legacy-migrate.ts +24 -4
- package/packages/bundled-features/src/waitlist/link-on-signup.ts +20 -10
- package/packages/bundled-features/src/waitlist/options.ts +5 -1
- package/packages/bundled-features/src/workflow-runner/handlers/resume-run.write.ts +1 -1
- package/samples/apps/styleguide/src/app/client.tsx +2 -0
- package/samples/apps/styleguide/src/app/seed.ts +6 -4
- package/samples/apps/styleguide/src/app/server.ts +4 -1
- package/samples/apps/styleguide/src/features/widgets/feature.ts +15 -4
- package/samples/apps/styleguide/src/features/widgets/i18n.ts +11 -0
- package/samples/apps/styleguide/src/features/widgets/web/Widgets.tsx +24 -0
- package/samples/apps/styleguide/src/features/workboard/feature.ts +185 -0
- package/samples/apps/styleguide/src/features/workboard/i18n.ts +27 -0
- package/samples/apps/styleguide/src/features/workboard/seed.ts +121 -0
- package/samples/apps/styleguide/src/features/workboard/web.ts +13 -0
- package/samples/apps/ui-walkthrough/src/features/rental/i18n.ts +2 -0
- package/samples/apps/ui-walkthrough/src/features/rental/screens.ts +9 -0
- package/samples/apps/use-all-bundled/bin/main.ts +2 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-constants.ts +5 -0
- package/samples/apps/use-all-bundled/e2e-billing-gallery/gallery-features.ts +90 -15
- package/samples/apps/use-all-bundled/e2e-billing-gallery/server.ts +16 -6
- package/samples/apps/use-all-bundled/feature-manifest.json +136 -3
- package/samples/apps/use-all-bundled/kumiko/schema.ts +2 -0
- package/samples/apps/use-all-bundled/kumiko.config.ts +2 -0
- package/samples/apps/use-all-bundled/package.json +2 -1
- package/samples/apps/use-all-bundled/src/app/dev-app-options.ts +2 -0
- package/samples/apps/use-all-bundled/src/app/notes-feature.ts +2 -0
- package/samples/apps/use-all-bundled/src/bundled-lists.ts +15 -0
- package/samples/apps/use-all-bundled/src/run-config.ts +9 -3
- package/samples/recipes/auth-mfa/README.md +5 -0
- package/samples/recipes/auth-mfa/package.json +5 -2
- package/samples/recipes/auth-mfa/src/web/confirmation-code-step.tsx +49 -0
- package/samples/recipes/auth-mfa/tsconfig.json +8 -0
- package/samples/recipes/env-composition/README.md +3 -1
- package/samples/recipes/env-composition/src/env.ts +2 -0
- package/samples/recipes/golden-path/src/feature.ts +2 -1
- package/samples/recipes/legal-pages/README.md +1 -1
- package/samples/recipes/lifecycle-hooks/README.md +4 -0
- package/samples/recipes/lifecycle-hooks/src/feature.ts +26 -1
- package/samples/recipes/list-table-options/README.md +50 -0
- package/samples/recipes/list-table-options/package.json +11 -0
- package/samples/recipes/list-table-options/src/feature.ts +221 -0
- package/samples/recipes/pdf-audit-export/src/feature.ts +2 -1
- package/samples/recipes/rate-limiting/README.md +18 -0
- package/samples/recipes/rate-limiting/src/feature.ts +12 -0
- package/samples/recipes/state-machine/src/events.ts +3 -2
- package/samples/recipes/tenant-isolation/README.md +1 -0
- package/samples/recipes/tenant-isolation/src/feature.ts +36 -0
- package/packages/bundled-features/src/billing-foundation/consumer-protection/stored-user-email.ts +0 -12
|
@@ -79,6 +79,7 @@ export function MfaSetupPreauthScreen({
|
|
|
79
79
|
const [setup, setSetup] = useState<SetupState | null>(null);
|
|
80
80
|
const [acknowledged, setAcknowledged] = useState(false);
|
|
81
81
|
const [code, setCode] = useState("");
|
|
82
|
+
const [attempt, setAttempt] = useState(0);
|
|
82
83
|
const [busy, setBusy] = useState(false);
|
|
83
84
|
const [error, setError] = useState<string | null>(null);
|
|
84
85
|
const [retryAfterSeconds, setRetryAfterSeconds] = useState<number | null>(null);
|
|
@@ -109,13 +110,13 @@ export function MfaSetupPreauthScreen({
|
|
|
109
110
|
}
|
|
110
111
|
};
|
|
111
112
|
|
|
112
|
-
const confirmSetup = async (): Promise<void> => {
|
|
113
|
-
if (!setup) return;
|
|
113
|
+
const confirmSetup = async (submittedCode: string): Promise<void> => {
|
|
114
|
+
if (!setup || busy) return;
|
|
114
115
|
setBusy(true);
|
|
115
116
|
setError(null);
|
|
116
117
|
setRetryAfterSeconds(null);
|
|
117
118
|
try {
|
|
118
|
-
const res = await confirmMfaSetupPreauth(setup.setupToken,
|
|
119
|
+
const res = await confirmMfaSetupPreauth(setup.setupToken, submittedCode);
|
|
119
120
|
if (res.kind !== "success") {
|
|
120
121
|
setError(res.error.reason);
|
|
121
122
|
setRetryAfterSeconds(res.error.retryAfterSeconds ?? null);
|
|
@@ -130,7 +131,11 @@ export function MfaSetupPreauthScreen({
|
|
|
130
131
|
setSetup(null);
|
|
131
132
|
setAcknowledged(false);
|
|
132
133
|
setCode("");
|
|
134
|
+
return;
|
|
133
135
|
}
|
|
136
|
+
// Remount: the field was disabled during confirm and lost focus.
|
|
137
|
+
setCode("");
|
|
138
|
+
setAttempt((n) => n + 1);
|
|
134
139
|
return;
|
|
135
140
|
}
|
|
136
141
|
onSuccess?.(res.data.landingPath);
|
|
@@ -143,7 +148,7 @@ export function MfaSetupPreauthScreen({
|
|
|
143
148
|
|
|
144
149
|
const onSubmit = (e?: FormEvent): void => {
|
|
145
150
|
e?.preventDefault();
|
|
146
|
-
void confirmSetup();
|
|
151
|
+
void confirmSetup(code);
|
|
147
152
|
};
|
|
148
153
|
|
|
149
154
|
return (
|
|
@@ -226,13 +231,17 @@ export function MfaSetupPreauthScreen({
|
|
|
226
231
|
|
|
227
232
|
<Field id="mfa-setup-preauth-code" label={t("auth.mfa.enable.code")} required>
|
|
228
233
|
<Input
|
|
229
|
-
|
|
234
|
+
key={attempt}
|
|
235
|
+
kind="code"
|
|
236
|
+
length={6}
|
|
230
237
|
id="mfa-setup-preauth-code"
|
|
231
238
|
name="mfa-setup-preauth-code"
|
|
232
239
|
value={code}
|
|
233
240
|
onChange={setCode}
|
|
241
|
+
onComplete={(next) => void confirmSetup(next)}
|
|
234
242
|
disabled={busy || !acknowledged}
|
|
235
|
-
|
|
243
|
+
hasError={error !== null}
|
|
244
|
+
autoFocus={attempt > 0}
|
|
236
245
|
/>
|
|
237
246
|
</Field>
|
|
238
247
|
|
|
@@ -27,6 +27,10 @@ export type MfaVerifyScreenProps = {
|
|
|
27
27
|
readonly onCancel?: () => void;
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
type CodeEntryMode = "authenticator" | "recovery";
|
|
31
|
+
|
|
32
|
+
const MFA_CODE_LENGTH = 6;
|
|
33
|
+
|
|
30
34
|
function reasonToKey(reason: string): string {
|
|
31
35
|
switch (reason) {
|
|
32
36
|
case "invalid_totp_code":
|
|
@@ -56,14 +60,17 @@ export function MfaVerifyScreen({
|
|
|
56
60
|
const t = useTranslation();
|
|
57
61
|
const { Form, Field, Input, Button, Banner } = usePrimitives();
|
|
58
62
|
const session = useSession();
|
|
63
|
+
const [mode, setMode] = useState<CodeEntryMode>("authenticator");
|
|
59
64
|
const [code, setCode] = useState("");
|
|
65
|
+
const [attempt, setAttempt] = useState(0);
|
|
60
66
|
const [submitting, setSubmitting] = useState(false);
|
|
61
67
|
const [error, setError] = useState<string | null>(null);
|
|
62
68
|
|
|
63
|
-
const doSubmit = async (): Promise<void> => {
|
|
69
|
+
const doSubmit = async (submittedCode: string): Promise<void> => {
|
|
70
|
+
if (submitting) return;
|
|
64
71
|
setSubmitting(true);
|
|
65
72
|
setError(null);
|
|
66
|
-
const res = await verifyMfaChallenge(challengeToken,
|
|
73
|
+
const res = await verifyMfaChallenge(challengeToken, submittedCode);
|
|
67
74
|
if (res.kind === "success") {
|
|
68
75
|
await session.refresh();
|
|
69
76
|
setSubmitting(false);
|
|
@@ -72,30 +79,65 @@ export function MfaVerifyScreen({
|
|
|
72
79
|
}
|
|
73
80
|
setSubmitting(false);
|
|
74
81
|
setError(res.error.reason);
|
|
82
|
+
// Remount: the input was disabled during submit and lost focus.
|
|
83
|
+
setAttempt((n) => n + 1);
|
|
84
|
+
if (mode === "authenticator") setCode("");
|
|
75
85
|
};
|
|
76
86
|
|
|
77
87
|
const onSubmit = (e?: FormEvent): void => {
|
|
78
88
|
e?.preventDefault();
|
|
79
|
-
void doSubmit();
|
|
89
|
+
void doSubmit(code);
|
|
80
90
|
};
|
|
81
91
|
|
|
92
|
+
const toggleMode = (): void => {
|
|
93
|
+
setMode(mode === "authenticator" ? "recovery" : "authenticator");
|
|
94
|
+
setCode("");
|
|
95
|
+
setError(null);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const isRecovery = mode === "recovery";
|
|
99
|
+
const fieldId = isRecovery ? "mfa-verify-recovery-code" : "mfa-verify-code";
|
|
100
|
+
const defaultSubtitle = isRecovery
|
|
101
|
+
? t("auth.mfa.verify.recoverySubtitle")
|
|
102
|
+
: t("auth.mfa.verify.subtitle");
|
|
103
|
+
|
|
82
104
|
return (
|
|
83
|
-
<AuthCard
|
|
84
|
-
title={title ?? t("auth.mfa.verify.title")}
|
|
85
|
-
subtitle={subtitle ?? t("auth.mfa.verify.subtitle")}
|
|
86
|
-
>
|
|
105
|
+
<AuthCard title={title ?? t("auth.mfa.verify.title")} subtitle={subtitle ?? defaultSubtitle}>
|
|
87
106
|
<Form onSubmit={onSubmit}>
|
|
88
|
-
<Field
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
107
|
+
<Field
|
|
108
|
+
id={fieldId}
|
|
109
|
+
label={isRecovery ? t("auth.mfa.verify.recoveryCode") : t("auth.mfa.verify.code")}
|
|
110
|
+
required
|
|
111
|
+
>
|
|
112
|
+
{isRecovery ? (
|
|
113
|
+
<Input
|
|
114
|
+
key={`${mode}-${attempt}`}
|
|
115
|
+
kind="text"
|
|
116
|
+
id={fieldId}
|
|
117
|
+
name={fieldId}
|
|
118
|
+
value={code}
|
|
119
|
+
onChange={setCode}
|
|
120
|
+
disabled={submitting}
|
|
121
|
+
required
|
|
122
|
+
autoFocus
|
|
123
|
+
autoComplete="off"
|
|
124
|
+
/>
|
|
125
|
+
) : (
|
|
126
|
+
<Input
|
|
127
|
+
key={`${mode}-${attempt}`}
|
|
128
|
+
kind="code"
|
|
129
|
+
length={MFA_CODE_LENGTH}
|
|
130
|
+
id={fieldId}
|
|
131
|
+
name={fieldId}
|
|
132
|
+
value={code}
|
|
133
|
+
onChange={setCode}
|
|
134
|
+
onComplete={(next) => void doSubmit(next)}
|
|
135
|
+
disabled={submitting}
|
|
136
|
+
hasError={error !== null}
|
|
137
|
+
required
|
|
138
|
+
autoFocus
|
|
139
|
+
/>
|
|
140
|
+
)}
|
|
99
141
|
</Field>
|
|
100
142
|
{error !== null ? <Banner variant="error">{t(reasonToKey(error))}</Banner> : null}
|
|
101
143
|
<Button type="submit" loading={submitting} disabled={submitting}>
|
|
@@ -103,6 +145,11 @@ export function MfaVerifyScreen({
|
|
|
103
145
|
? t("auth.mfa.verify.submitting")
|
|
104
146
|
: (submitLabel ?? t("auth.mfa.verify.submit"))}
|
|
105
147
|
</Button>
|
|
148
|
+
<Button type="button" variant="link" onClick={toggleMode} disabled={submitting}>
|
|
149
|
+
{isRecovery
|
|
150
|
+
? t("auth.mfa.verify.useAuthenticatorCode")
|
|
151
|
+
: t("auth.mfa.verify.useRecoveryCode")}
|
|
152
|
+
</Button>
|
|
106
153
|
{onCancel ? (
|
|
107
154
|
<Button type="button" variant="secondary" onClick={onCancel} disabled={submitting}>
|
|
108
155
|
{t("auth.mfa.verify.backToLogin")}
|
|
@@ -1,4 +1,59 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.388.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Concurrent termination runs, revocation resume and sync ordering",
|
|
6
|
+
"detail": "record-contract-termination re-checks the requestId after the provider call so a concurrent run appends nothing. Revocation resumes a period-end cancel that was recorded between its two stream reads. sync-subscription stamps the snapshot time as providerEventAtIso so a late older webhook cannot reactivate a synced cancellation."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.388.0",
|
|
10
|
+
"type": "fix",
|
|
11
|
+
"title": "Ordered terms lists keep their numbers in the confirmation mail",
|
|
12
|
+
"detail": "The contract confirmation mail prefixes each item of an ordered Markdown list with its number, so references such as \"see no. 3\" in terms and cancellation policies stay intact."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"version": "0.388.0",
|
|
16
|
+
"type": "fix",
|
|
17
|
+
"title": "Operator notice tells an unavailable receipt limiter from a reached limit",
|
|
18
|
+
"detail": "When the termination receipt mail is withheld because the rate limiter failed or has no digest key, the operator notice reports \"receipt_not_sent\" instead of the per-recipient limit reason."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"version": "0.388.0",
|
|
22
|
+
"type": "fix",
|
|
23
|
+
"title": "An abandoned plan-change checkout no longer restarts the withdrawal period",
|
|
24
|
+
"detail": "The withdrawal period now runs from the latest subscription start only; a checkout consent without a following subscription start does not reopen it, so a later withdrawal of an old contract is cancelled at period end."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"version": "0.388.0",
|
|
28
|
+
"type": "fix",
|
|
29
|
+
"title": "Same-second provider update cannot revive a canceled subscription",
|
|
30
|
+
"detail": "When a non-terminal state event carries the same provider timestamp as a stored cancellation, it is treated as late and discarded."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"version": "0.388.0",
|
|
34
|
+
"type": "improvement",
|
|
35
|
+
"title": "Several subscription providers in one catalog",
|
|
36
|
+
"detail": "The billing-plans query returns providers (name, displayName, plans, note) when more than one provider is offered, and start-plan-checkout takes providerName. consumerProtection.providerTexts overrides checkoutNote and vatNote per provider, and the consent record carries providerName. switch-plan always uses the provider of the existing subscription."
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"version": "0.388.0",
|
|
40
|
+
"type": "improvement",
|
|
41
|
+
"title": "In-app plan switch with a preview and consent",
|
|
42
|
+
"detail": "A provider plugin that implements previewPlanSwitch and applyPlanSwitch switches plans without a hosted portal. The new plan-switch-preview query returns the proration, the billing-plans result carries inAppPlanSwitch, and the panel shows PlanSwitchConfirmDialog with the withdrawal consent checkboxes. The confirmation mail is sent when the plan_changed webhook arrives with the consent id."
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"version": "0.388.0",
|
|
46
|
+
"type": "breaking",
|
|
47
|
+
"title": "A catalog with several providers no longer throws an ambiguous-provider error",
|
|
48
|
+
"detail": "With more than one subscription provider mounted, billing-plans and start-plan-checkout used to throw. They now list the providers; start-plan-checkout answers with errors.providerRequired when several are offered and no providerName is given, and errors.providerNotOffered for an unknown one. Plugins have to set displayName.",
|
|
49
|
+
"migration": "Set displayName on custom subscriptionProvider plugins. Callers that send start-plan-checkout with several providers mounted must pass providerName."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"version": "0.388.0",
|
|
53
|
+
"type": "improvement",
|
|
54
|
+
"title": "Provider choice moved into the checkout dialog",
|
|
55
|
+
"detail": "The plan panel no longer shows a provider switch. Cards show the first provider's price, or the cheapest one with a \"from\" prefix when providers disagree. With several providers, or with consumerProtection, choosing a plan opens CheckoutConsentDialog, which now has an optional providerChoice (ChoiceCards with the checkout note and, when prices differ, the price), an optional note, and an optional consumerProtection (without it there are no checkboxes and the order carries no consent). The plan switch dialog shows the recurring price with its interval."
|
|
56
|
+
},
|
|
2
57
|
{
|
|
3
58
|
"version": "0.379.0",
|
|
4
59
|
"type": "fix",
|
|
@@ -36,17 +36,22 @@ export type ResolvedProvider = {
|
|
|
36
36
|
readonly plugin: SubscriptionProviderPlugin;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
+
/** Every registered provider plugin, in registration order. */
|
|
40
|
+
function listProviderPlugins(ctx: HandlerContext): readonly ResolvedProvider[] {
|
|
41
|
+
return ctx.registry.getExtensionUsages(SUBSCRIPTION_PROVIDER_EXTENSION).map((usage) => ({
|
|
42
|
+
name: usage.entityName,
|
|
43
|
+
// @cast-boundary engine-payload — extension-usage carries unknown options
|
|
44
|
+
plugin: usage.options as SubscriptionProviderPlugin,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
39
48
|
/** Non-throwing provider lookup by name — null when no plugin with that
|
|
40
49
|
* entityName is registered. */
|
|
41
50
|
export function findProviderPlugin(
|
|
42
51
|
ctx: HandlerContext,
|
|
43
52
|
providerName: string,
|
|
44
53
|
): ResolvedProvider | null {
|
|
45
|
-
|
|
46
|
-
.getExtensionUsages(SUBSCRIPTION_PROVIDER_EXTENSION)
|
|
47
|
-
.find((u) => u.entityName === providerName);
|
|
48
|
-
// @cast-boundary engine-payload — extension-usage carries unknown options
|
|
49
|
-
return usage ? { name: providerName, plugin: usage.options as SubscriptionProviderPlugin } : null;
|
|
54
|
+
return listProviderPlugins(ctx).find((provider) => provider.name === providerName) ?? null;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
export function resolveProviderPlugin(ctx: HandlerContext, providerName: string): ResolvedProvider {
|
|
@@ -64,17 +69,15 @@ export function resolveProviderPlugin(ctx: HandlerContext, providerName: string)
|
|
|
64
69
|
return found;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
|
|
73
|
-
* `UnconfiguredError`. */
|
|
74
|
-
export function findCatalogProvider(
|
|
72
|
+
/** The providers a catalog sells through, in registration order: an explicit
|
|
73
|
+
* `catalog.providerName` (only that one), otherwise every registered provider
|
|
74
|
+
* exposing `priceToTier`. Empty when none qualifies, a state `billing-plans`
|
|
75
|
+
* renders as `enabled: false` instead of erroring. An explicit `providerName`
|
|
76
|
+
* that is not registered is a config bug and throws `UnconfiguredError`. */
|
|
77
|
+
export function listCatalogProviders(
|
|
75
78
|
ctx: HandlerContext,
|
|
76
79
|
catalog: BillingPlanCatalog,
|
|
77
|
-
): ResolvedProvider
|
|
80
|
+
): readonly ResolvedProvider[] {
|
|
78
81
|
if (catalog.providerName) {
|
|
79
82
|
const found = findProviderPlugin(ctx, catalog.providerName);
|
|
80
83
|
if (!found) {
|
|
@@ -84,40 +87,27 @@ export function findCatalogProvider(
|
|
|
84
87
|
hint: `provider "${catalog.providerName}" not registered`,
|
|
85
88
|
});
|
|
86
89
|
}
|
|
87
|
-
return found;
|
|
90
|
+
return [found];
|
|
88
91
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
return listProviderPlugins(ctx).filter(({ plugin }) => plugin.priceToTier !== undefined);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function filterOfferedProviders(
|
|
96
|
+
ctx: HandlerContext,
|
|
97
|
+
providers: readonly ResolvedProvider[],
|
|
98
|
+
): Promise<readonly ResolvedProvider[]> {
|
|
99
|
+
const enabledFlags = await Promise.all(
|
|
100
|
+
providers.map(({ plugin }) => isPluginBillingEnabled(ctx, plugin)),
|
|
92
101
|
);
|
|
93
|
-
|
|
94
|
-
throw new UnconfiguredError({
|
|
95
|
-
feature: "billing-foundation",
|
|
96
|
-
key: "catalog.providerName",
|
|
97
|
-
hint: `ambiguous — ${withPriceCatalog.length} registered plugins expose priceToTier, set catalog.providerName explicitly`,
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
const usage = withPriceCatalog[0];
|
|
101
|
-
if (!usage) return null;
|
|
102
|
-
// @cast-boundary engine-payload — extension-usage carries unknown options
|
|
103
|
-
return { name: usage.entityName, plugin: usage.options as SubscriptionProviderPlugin };
|
|
102
|
+
return providers.filter((_, index) => enabledFlags[index]);
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
* `UnconfiguredError`) when none is found: an unconfigured provider is
|
|
109
|
-
* the same "billing isn't live yet" state as `isPluginBillingEnabled`
|
|
110
|
-
* returning false, and the panel already renders both the same way. */
|
|
111
|
-
export function resolveCatalogProvider(
|
|
105
|
+
/** The catalog providers that are live right now: what a buyer can choose from. */
|
|
106
|
+
export async function listOfferedCatalogProviders(
|
|
112
107
|
ctx: HandlerContext,
|
|
113
108
|
catalog: BillingPlanCatalog,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const found = findCatalogProvider(ctx, catalog);
|
|
117
|
-
if (!found) {
|
|
118
|
-
throw new FeatureDisabledError(BILLING_FOUNDATION_FEATURE, handlerName);
|
|
119
|
-
}
|
|
120
|
-
return found;
|
|
109
|
+
): Promise<readonly ResolvedProvider[]> {
|
|
110
|
+
return filterOfferedProviders(ctx, listCatalogProviders(ctx, catalog));
|
|
121
111
|
}
|
|
122
112
|
|
|
123
113
|
/** `plugin.isBillingEnabled?.(ctx) ?? true` — the one place this fallback is
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/confirmation-mail.ts
CHANGED
|
@@ -166,8 +166,14 @@ inlineHtmlMarkdown.use({
|
|
|
166
166
|
blockquote({ tokens }) {
|
|
167
167
|
return this.parser.parse(tokens);
|
|
168
168
|
},
|
|
169
|
-
list({ items }) {
|
|
170
|
-
|
|
169
|
+
list({ items, ordered, start }) {
|
|
170
|
+
const firstNumber = typeof start === "number" ? start : 1;
|
|
171
|
+
return items
|
|
172
|
+
.map((item, index) => {
|
|
173
|
+
const marker = ordered ? `${firstNumber + index}. ` : "";
|
|
174
|
+
return `${marker}${this.parser.parse(item.tokens)}`;
|
|
175
|
+
})
|
|
176
|
+
.join("");
|
|
171
177
|
},
|
|
172
178
|
hr: () => "",
|
|
173
179
|
space: () => "",
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/consent-gate.ts
CHANGED
|
@@ -79,6 +79,7 @@ export type ConsentPriceDetails = {
|
|
|
79
79
|
"unitAmount" | "currency" | "interval" | "intervalCount"
|
|
80
80
|
> | null;
|
|
81
81
|
readonly orderItem?: OrderItem;
|
|
82
|
+
readonly providerName?: string;
|
|
82
83
|
};
|
|
83
84
|
|
|
84
85
|
export async function prepareConsent(
|
|
@@ -163,6 +164,7 @@ export async function recordConsent(
|
|
|
163
164
|
termsTemplateVersion: prepared.termsTemplateVersion,
|
|
164
165
|
locale: prepared.locale,
|
|
165
166
|
actorUserId: String(ctx.user.id),
|
|
167
|
+
...(details.providerName !== undefined && { providerName: details.providerName }),
|
|
166
168
|
...(details.orderItem && {
|
|
167
169
|
itemLabelKey: details.orderItem.labelKey,
|
|
168
170
|
...(details.orderItem.params && { itemLabelParams: details.orderItem.params }),
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/issue-confirmation.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { InternalError, UnprocessableError } from "@cosmicdrift/kumiko-framework
|
|
|
12
12
|
import { createI18n } from "@cosmicdrift/kumiko-framework/i18n";
|
|
13
13
|
import { isValidIanaTimeZone } from "@cosmicdrift/kumiko-framework/time";
|
|
14
14
|
import * as z from "zod";
|
|
15
|
+
import { decryptStoredUserEmail } from "../../shared/index.ts";
|
|
15
16
|
import { requireTemplateResolver, TEXT_BLOCK_KIND } from "../../template-resolver/index.ts";
|
|
16
17
|
import { UserQueries } from "../../user/index.ts";
|
|
17
18
|
import { paymentAggregateId, subscriptionAggregateId } from "../aggregate-id.ts";
|
|
@@ -29,7 +30,6 @@ import type { ConsumerProtectionOptions, ResolvedBillingFoundationOptions } from
|
|
|
29
30
|
import { CONTRACT_CONFIRMATION_NOTIFICATION_TYPE } from "../web/constants.ts";
|
|
30
31
|
import { contractConfirmationVariables } from "./confirmation-mail.ts";
|
|
31
32
|
import { resolveConsentLocale } from "./consent-text.ts";
|
|
32
|
-
import { openStoredUserEmail } from "./stored-user-email.ts";
|
|
33
33
|
|
|
34
34
|
export const issueContractConfirmationSchema = z
|
|
35
35
|
.object({
|
|
@@ -58,6 +58,17 @@ function translatedOrUndefined(translation: string, key: string): string | undef
|
|
|
58
58
|
return translation === key ? undefined : translation;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
function vatNoteFor(
|
|
62
|
+
consumerProtection: ConsumerProtectionOptions,
|
|
63
|
+
providerName: string | undefined,
|
|
64
|
+
): Readonly<Record<string, string>> {
|
|
65
|
+
const providerNote =
|
|
66
|
+
providerName === undefined
|
|
67
|
+
? undefined
|
|
68
|
+
: consumerProtection.providerTexts?.[providerName]?.vatNote;
|
|
69
|
+
return providerNote ?? consumerProtection.vatNote;
|
|
70
|
+
}
|
|
71
|
+
|
|
61
72
|
function pickTimeZone(userZone: string | null | undefined, tenantZone: string): string {
|
|
62
73
|
if (userZone && isValidIanaTimeZone(userZone)) return userZone;
|
|
63
74
|
return isValidIanaTimeZone(tenantZone) ? tenantZone : "UTC";
|
|
@@ -145,7 +156,7 @@ export function createIssueContractConfirmationHandler(
|
|
|
145
156
|
);
|
|
146
157
|
const recipientEmail =
|
|
147
158
|
recipient.success && recipient.data.isDeleted !== true
|
|
148
|
-
? await
|
|
159
|
+
? await decryptStoredUserEmail(
|
|
149
160
|
recipient.data.email,
|
|
150
161
|
"billing-foundation:issue-contract-confirmation",
|
|
151
162
|
)
|
|
@@ -203,7 +214,7 @@ export function createIssueContractConfirmationHandler(
|
|
|
203
214
|
...(payload.currentPeriodEndIso !== undefined && {
|
|
204
215
|
currentPeriodEndIso: payload.currentPeriodEndIso,
|
|
205
216
|
}),
|
|
206
|
-
vatNote: consumerProtection.
|
|
217
|
+
vatNote: vatNoteFor(consumerProtection, consent.providerName),
|
|
207
218
|
operatorEmail: consumerProtection.operatorEmail,
|
|
208
219
|
termsContent: terms.content,
|
|
209
220
|
});
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/mail-notifications.ts
CHANGED
|
@@ -69,10 +69,9 @@ export function terminationReceiptTemplate(): NotificationLocalizedTemplate {
|
|
|
69
69
|
mailRepeatedRows("details", "text"),
|
|
70
70
|
mailParagraph(texts.receiptNextSteps),
|
|
71
71
|
mailParagraph(texts.receiptRevoke(OPERATOR_EMAIL_SLOT)),
|
|
72
|
-
mailParagraphWithLink(texts.receiptNotMe, texts.receiptNotMeLink, "revokeUrl"
|
|
73
|
-
"
|
|
74
|
-
|
|
75
|
-
),
|
|
72
|
+
mailParagraphWithLink(texts.receiptNotMe, texts.receiptNotMeLink, "revokeUrl", {
|
|
73
|
+
showIf: "revokeUrl",
|
|
74
|
+
}),
|
|
76
75
|
mailParagraph(`${texts.contact} {operatorEmail}`),
|
|
77
76
|
].join(""),
|
|
78
77
|
};
|
|
@@ -14,28 +14,34 @@ function receiptBucketKey(ctx: HandlerContext, email: string): string | undefine
|
|
|
14
14
|
return digest === undefined ? undefined : `${RECEIPT_BUCKET_PREFIX}:${digest}`;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export type ReceiptMailVerdict = "allowed" | "limited" | "unavailable";
|
|
18
|
+
|
|
17
19
|
/** Consumes one receipt token for the recipient. Fails closed when the limiter
|
|
18
|
-
* errors or no digest key is wired: the declaration is already
|
|
19
|
-
|
|
20
|
+
* errors or no digest key is wired ("unavailable"): the declaration is already
|
|
21
|
+
* recorded, a missing receipt is the lesser harm. */
|
|
22
|
+
export async function mayMailReceipt(
|
|
23
|
+
ctx: HandlerContext,
|
|
24
|
+
email: string,
|
|
25
|
+
): Promise<ReceiptMailVerdict> {
|
|
20
26
|
// skip: no rate limiter wired, nothing to count against
|
|
21
|
-
if (!ctx.rateLimit) return
|
|
27
|
+
if (!ctx.rateLimit) return "allowed";
|
|
22
28
|
try {
|
|
23
29
|
const bucketKey = receiptBucketKey(ctx, email);
|
|
24
30
|
if (bucketKey === undefined) {
|
|
25
31
|
ctx.log?.warn(
|
|
26
32
|
"[billing-foundation:termination] no rate-limit digest available, receipt mail withheld",
|
|
27
33
|
);
|
|
28
|
-
return
|
|
34
|
+
return "unavailable";
|
|
29
35
|
}
|
|
30
36
|
const decision = await ctx.rateLimit.check(bucketKey, {
|
|
31
37
|
limit: RECEIPT_MAILS_PER_RECIPIENT,
|
|
32
38
|
windowSeconds: RECEIPT_WINDOW_SECONDS,
|
|
33
39
|
});
|
|
34
|
-
return decision.allowed;
|
|
40
|
+
return decision.allowed ? "allowed" : "limited";
|
|
35
41
|
} catch (error) {
|
|
36
42
|
ctx.log?.warn(
|
|
37
43
|
`[billing-foundation:termination] receipt rate limiter failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
38
44
|
);
|
|
39
|
-
return
|
|
45
|
+
return "unavailable";
|
|
40
46
|
}
|
|
41
47
|
}
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-record.ts
CHANGED
|
@@ -249,25 +249,8 @@ export function createRecordContractTerminationHandler(
|
|
|
249
249
|
|
|
250
250
|
// The public job runs at least once: a second run must not call the
|
|
251
251
|
// provider or append again.
|
|
252
|
-
const alreadyRecorded = stream.events
|
|
253
|
-
|
|
254
|
-
e.type === CONTRACT_TERMINATION_REQUESTED_EVENT_QN
|
|
255
|
-
? contractTerminationRequestedPayloadSchema.safeParse(e.payload).data
|
|
256
|
-
: undefined,
|
|
257
|
-
)
|
|
258
|
-
.find((recordedPayload) => recordedPayload?.requestId === payload.requestId);
|
|
259
|
-
if (alreadyRecorded) {
|
|
260
|
-
return {
|
|
261
|
-
isSuccess: true as const,
|
|
262
|
-
data: {
|
|
263
|
-
receivedAtIso: alreadyRecorded.receivedAtIso,
|
|
264
|
-
effectiveAtIso: alreadyRecorded.effectiveAtIso,
|
|
265
|
-
providerCancel: alreadyRecorded.providerCancel,
|
|
266
|
-
noticeReason: null,
|
|
267
|
-
withdrawalPeriodExpired: false,
|
|
268
|
-
} satisfies RecordContractTerminationResult,
|
|
269
|
-
};
|
|
270
|
-
}
|
|
252
|
+
const alreadyRecorded = findRecordedTermination(stream.events, payload.requestId);
|
|
253
|
+
if (alreadyRecorded) return recordedTerminationResult(alreadyRecorded);
|
|
271
254
|
|
|
272
255
|
// A revoked public declaration never reaches the provider.
|
|
273
256
|
if (hasRevocation(stream.events, payload.requestId)) {
|
|
@@ -301,6 +284,13 @@ export function createRecordContractTerminationHandler(
|
|
|
301
284
|
aggregateId: subscriptionAggregateId(tenantId),
|
|
302
285
|
aggregateType: SUBSCRIPTION_AGGREGATE_TYPE,
|
|
303
286
|
});
|
|
287
|
+
// A concurrent run with the same requestId may have recorded while we were
|
|
288
|
+
// at the provider; a second append would duplicate the event and the notice.
|
|
289
|
+
const recordedMeanwhile = findRecordedTermination(
|
|
290
|
+
streamAfterProviderCall.events,
|
|
291
|
+
payload.requestId,
|
|
292
|
+
);
|
|
293
|
+
if (recordedMeanwhile) return recordedTerminationResult(recordedMeanwhile);
|
|
304
294
|
// A revocation that landed during the provider call found no cancel to take back.
|
|
305
295
|
if (hasRevocation(streamAfterProviderCall.events, payload.requestId)) {
|
|
306
296
|
await takeBackLateRevokedCancel(ctx, consumerProtection, {
|
|
@@ -348,6 +338,32 @@ export function createRecordContractTerminationHandler(
|
|
|
348
338
|
};
|
|
349
339
|
}
|
|
350
340
|
|
|
341
|
+
export function findRecordedTermination(
|
|
342
|
+
events: readonly { type: string; payload: unknown }[],
|
|
343
|
+
requestId: string,
|
|
344
|
+
): ContractTerminationRequestedPayload | undefined {
|
|
345
|
+
return events
|
|
346
|
+
.map((e) =>
|
|
347
|
+
e.type === CONTRACT_TERMINATION_REQUESTED_EVENT_QN
|
|
348
|
+
? contractTerminationRequestedPayloadSchema.safeParse(e.payload).data
|
|
349
|
+
: undefined,
|
|
350
|
+
)
|
|
351
|
+
.find((recordedPayload) => recordedPayload?.requestId === requestId);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function recordedTerminationResult(recorded: ContractTerminationRequestedPayload) {
|
|
355
|
+
return {
|
|
356
|
+
isSuccess: true as const,
|
|
357
|
+
data: {
|
|
358
|
+
receivedAtIso: recorded.receivedAtIso,
|
|
359
|
+
effectiveAtIso: recorded.effectiveAtIso,
|
|
360
|
+
providerCancel: recorded.providerCancel,
|
|
361
|
+
noticeReason: null,
|
|
362
|
+
withdrawalPeriodExpired: false,
|
|
363
|
+
} satisfies RecordContractTerminationResult,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
351
367
|
export function hasRevocation(
|
|
352
368
|
events: readonly { type: string; payload: unknown }[],
|
|
353
369
|
requestId: string,
|
package/packages/bundled-features/src/billing-foundation/consumer-protection/termination-request.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
|
|
14
14
|
import { generateId } from "@cosmicdrift/kumiko-framework/utils";
|
|
15
15
|
import * as z from "zod";
|
|
16
|
+
import { decryptStoredUserEmail } from "../../shared/index.ts";
|
|
16
17
|
import { TenantQueries } from "../../tenant/index.ts";
|
|
17
18
|
import { UserQueries } from "../../user/index.ts";
|
|
18
19
|
import {
|
|
@@ -31,10 +32,9 @@ import {
|
|
|
31
32
|
SubscriptionFoundationQueries,
|
|
32
33
|
} from "../web/constants.ts";
|
|
33
34
|
import { resolveConsentLocale } from "./consent-text.ts";
|
|
34
|
-
import { openStoredUserEmail } from "./stored-user-email.ts";
|
|
35
35
|
import { type TerminationDeclaration, terminationReceiptVariables } from "./termination-mail.ts";
|
|
36
36
|
import { notifyOperator, type OperatorNoticePlan, requireNotify } from "./termination-notify.ts";
|
|
37
|
-
import { mayMailReceipt } from "./termination-receipt-limit.ts";
|
|
37
|
+
import { mayMailReceipt, type ReceiptMailVerdict } from "./termination-receipt-limit.ts";
|
|
38
38
|
import type { RecordContractTerminationResult } from "./termination-record.ts";
|
|
39
39
|
import { createRevocationToken, revocationUrl } from "./termination-revoke.ts";
|
|
40
40
|
import type { OperatorNoticeReason } from "./termination-texts.ts";
|
|
@@ -109,13 +109,19 @@ type TerminationMailPlan = OperatorNoticePlan & {
|
|
|
109
109
|
readonly revokeUrl?: string;
|
|
110
110
|
};
|
|
111
111
|
|
|
112
|
+
function receiptWithheldReasons(verdict: ReceiptMailVerdict): readonly OperatorNoticeReason[] {
|
|
113
|
+
if (verdict === "limited") return ["receipt_rate_limited"];
|
|
114
|
+
if (verdict === "unavailable") return ["receipt_not_sent"];
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
|
|
112
118
|
async function sendTerminationMails(
|
|
113
119
|
ctx: HandlerContext,
|
|
114
120
|
consumerProtection: ConsumerProtectionOptions,
|
|
115
121
|
plan: TerminationMailPlan,
|
|
116
122
|
): Promise<void> {
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
123
|
+
const receiptVerdict = await mayMailReceipt(ctx, plan.declaration.email);
|
|
124
|
+
if (receiptVerdict === "allowed") {
|
|
119
125
|
await requireNotify(ctx)(CONTRACT_TERMINATION_RECEIPT_NOTIFICATION_TYPE, {
|
|
120
126
|
route: { email: plan.declaration.email },
|
|
121
127
|
data: terminationReceiptVariables({
|
|
@@ -133,9 +139,7 @@ async function sendTerminationMails(
|
|
|
133
139
|
priority: "critical",
|
|
134
140
|
});
|
|
135
141
|
}
|
|
136
|
-
const reasons =
|
|
137
|
-
? plan.reasons
|
|
138
|
-
: [...plan.reasons, "receipt_rate_limited" as const];
|
|
142
|
+
const reasons = [...plan.reasons, ...receiptWithheldReasons(receiptVerdict)];
|
|
139
143
|
// skip: no operator notice needed for this declaration
|
|
140
144
|
if (reasons.length === 0) return;
|
|
141
145
|
const { revokeUrl: _revokeUrl, ...noticePlan } = plan;
|
|
@@ -320,7 +324,7 @@ export function createTerminateContractHandler(
|
|
|
320
324
|
);
|
|
321
325
|
const callerEmail =
|
|
322
326
|
caller.success && caller.data.email && caller.data.isDeleted !== true
|
|
323
|
-
? await
|
|
327
|
+
? await decryptStoredUserEmail(caller.data.email, "billing-foundation:terminate-contract")
|
|
324
328
|
: null;
|
|
325
329
|
if (callerEmail === null) {
|
|
326
330
|
throw new InternalError({
|