@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
|
@@ -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({
|
|
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 (
|
|
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
|
|
158
|
-
|
|
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
|
|
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
|
-
:
|
|
236
|
+
: shownPrice === null
|
|
218
237
|
? null
|
|
219
238
|
: {
|
|
220
|
-
amount: formatMoney(
|
|
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(
|
|
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 ?
|
|
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
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
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
|
-
{
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
open
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
price
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
-
|
|
28
|
-
readonly
|
|
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
|
|
52
|
-
const legalLinks = consumerProtection
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
98
|
-
<
|
|
99
|
-
{
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
{
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
{
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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={!
|
|
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
|
+
}
|