@volter/twin-stripe 2.0.38 → 3.0.1
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/README.md +112 -207
- package/dist/generated/pack-facts.json +94 -0
- package/dist/journeys/coverage.json +596 -0
- package/dist/journeys/customer-life.json +6108 -0
- package/dist/journeys/customer-life.outline.md +371 -0
- package/dist/journeys/decisions.json +677 -0
- package/dist/journeys/demand.json +706 -0
- package/dist/journeys/score.json +853 -0
- package/dist/journeys/unreachable.json +102 -0
- package/dist/journeys/vendor-examples.json +8010 -0
- package/dist/spec/SOURCE.md +18 -0
- package/dist/spec/doc-examples.json +445 -0
- package/dist/spec/fixtures3.json.gz +0 -0
- package/dist/spec/openapi.json.gz +0 -0
- package/dist/spec/patches.json +34 -0
- package/dist/spec/sources.json +306 -0
- package/dist/src/cli.d.ts +0 -1
- package/dist/src/cli.js +11 -31
- package/dist/src/engine/after-payment.d.ts +8 -0
- package/dist/src/engine/after-payment.js +27 -0
- package/dist/src/engine/apps-secrets.d.ts +1 -0
- package/dist/src/engine/apps-secrets.js +3 -0
- package/dist/src/engine/balance.d.ts +5 -0
- package/dist/src/engine/balance.js +15 -0
- package/dist/src/engine/checkout.d.ts +24 -0
- package/dist/src/engine/checkout.js +30 -0
- package/dist/src/engine/common.d.ts +19 -0
- package/dist/src/engine/common.js +34 -0
- package/dist/src/engine/connect.d.ts +45 -0
- package/dist/src/engine/connect.js +108 -0
- package/dist/src/engine/customers.d.ts +4 -0
- package/dist/src/engine/customers.js +8 -0
- package/dist/src/engine/display.d.ts +11 -0
- package/dist/src/engine/display.js +13 -0
- package/dist/src/engine/files.d.ts +11 -0
- package/dist/src/engine/files.js +23 -0
- package/dist/src/engine/invoices.d.ts +21 -0
- package/dist/src/engine/invoices.js +84 -0
- package/dist/src/engine/issuing.d.ts +33 -0
- package/dist/src/engine/issuing.js +54 -0
- package/dist/src/engine/ledger.d.ts +8 -0
- package/dist/src/engine/ledger.js +8 -0
- package/dist/src/engine/oauth.d.ts +4 -0
- package/dist/src/engine/oauth.js +24 -0
- package/dist/src/engine/onboarding.d.ts +13 -0
- package/dist/src/engine/onboarding.js +42 -0
- package/dist/src/{stripe-params.d.ts → engine/params.d.ts} +20 -3
- package/dist/src/{stripe-params.js → engine/params.js} +7 -60
- package/dist/src/engine/payment-intents.d.ts +10 -0
- package/dist/src/engine/payment-intents.js +27 -0
- package/dist/src/engine/payment-methods.d.ts +16 -0
- package/dist/src/engine/payment-methods.js +22 -0
- package/dist/src/engine/radar.d.ts +2 -0
- package/dist/src/engine/radar.js +5 -0
- package/dist/src/engine/refunds.d.ts +1 -0
- package/dist/src/engine/refunds.js +3 -0
- package/dist/src/engine/renewals.d.ts +36 -0
- package/dist/src/engine/renewals.js +65 -0
- package/dist/src/{stripe-twin.d.ts → engine/stripe.d.ts} +12 -45
- package/dist/src/{stripe-twin.js → engine/stripe.js} +53 -387
- package/dist/src/engine/subscriptions.d.ts +11 -0
- package/dist/src/engine/subscriptions.js +30 -0
- package/dist/src/engine/transfers.d.ts +2 -0
- package/dist/src/engine/transfers.js +1 -0
- package/dist/src/{stripe-version.d.ts → engine/version.d.ts} +0 -2
- package/dist/src/{stripe-version.js → engine/version.js} +21 -9
- package/dist/src/engine/webhook-endpoints.d.ts +1 -0
- package/dist/src/engine/webhook-endpoints.js +12 -0
- package/dist/src/engine/webhooks.d.ts +31 -0
- package/dist/src/engine/webhooks.js +68 -0
- package/dist/src/fetch.d.ts +2 -0
- package/dist/src/fetch.js +26 -0
- package/dist/src/generated/surface.gen.json +1 -1
- package/dist/src/generated/ui.gen.json +1 -1
- package/dist/src/index.d.ts +5 -14
- package/dist/src/index.js +10 -75
- package/dist/src/manifest.js +478 -876
- package/dist/src/screens/billing-portal.d.ts +3 -0
- package/dist/src/screens/{portal.js → billing-portal.js} +32 -58
- package/dist/src/screens/checkout-js.d.ts +2 -0
- package/dist/src/screens/checkout-js.js +84 -0
- package/dist/src/screens/checkout.d.ts +2 -31
- package/dist/src/screens/checkout.js +32 -121
- package/dist/src/screens/connect-onboarding.d.ts +3 -0
- package/dist/src/screens/{onboarding.js → connect-onboarding.js} +38 -104
- package/dist/src/screens/dashboard.d.ts +3 -0
- package/dist/src/screens/dashboard.js +116 -0
- package/dist/src/screens/shared.d.ts +27 -0
- package/dist/src/screens/shared.js +87 -0
- package/dist/src/screens/stripe-js.d.ts +3 -0
- package/dist/src/screens/stripe-js.js +95 -0
- package/dist/src/semantics/account.d.ts +2 -0
- package/dist/src/semantics/account.js +8 -0
- package/dist/src/semantics/account_links.d.ts +2 -0
- package/dist/src/semantics/account_links.js +22 -0
- package/dist/src/semantics/accounts.d.ts +4 -0
- package/dist/src/semantics/accounts.js +40 -0
- package/dist/src/semantics/apps.d.ts +4 -0
- package/dist/src/semantics/{apps-secrets.js → apps.js} +14 -26
- package/dist/src/semantics/around.d.ts +2 -0
- package/dist/src/semantics/around.js +13 -0
- package/dist/src/semantics/balance.d.ts +2 -11
- package/dist/src/semantics/balance.js +3 -194
- package/dist/src/semantics/balance_transactions.d.ts +2 -0
- package/dist/src/semantics/balance_transactions.js +15 -0
- package/dist/src/semantics/billing_portal.d.ts +4 -0
- package/dist/src/semantics/billing_portal.js +36 -0
- package/dist/src/semantics/charges.d.ts +3 -28
- package/dist/src/semantics/charges.js +5 -202
- package/dist/src/semantics/checkout.d.ts +3 -20
- package/dist/src/semantics/checkout.js +21 -271
- package/dist/src/semantics/clock.d.ts +2 -0
- package/dist/src/semantics/clock.js +15 -0
- package/dist/src/semantics/coupons.d.ts +2 -6
- package/dist/src/semantics/coupons.js +2 -60
- package/dist/src/semantics/customers.d.ts +5 -6
- package/dist/src/semantics/customers.js +47 -413
- package/dist/src/semantics/doors.d.ts +23 -0
- package/dist/src/semantics/doors.js +73 -0
- package/dist/src/semantics/entitlements.d.ts +0 -2
- package/dist/src/semantics/entitlements.js +4 -95
- package/dist/src/semantics/events.d.ts +11 -0
- package/dist/src/semantics/events.js +31 -0
- package/dist/src/semantics/index.d.ts +2 -0
- package/dist/src/semantics/index.js +85 -0
- package/dist/src/semantics/invoice_payments.d.ts +2 -0
- package/dist/src/semantics/invoice_payments.js +12 -0
- package/dist/src/semantics/invoices.d.ts +11 -18
- package/dist/src/semantics/invoices.js +121 -440
- package/dist/src/semantics/issuing.d.ts +6 -13
- package/dist/src/semantics/issuing.js +58 -556
- package/dist/src/semantics/payment_intents.d.ts +3 -0
- package/dist/src/semantics/payment_intents.js +71 -0
- package/dist/src/semantics/payment_methods.d.ts +3 -0
- package/dist/src/semantics/payment_methods.js +15 -0
- package/dist/src/semantics/payouts.d.ts +3 -0
- package/dist/src/semantics/payouts.js +28 -0
- package/dist/src/semantics/prices.d.ts +4 -0
- package/dist/src/semantics/prices.js +37 -0
- package/dist/src/semantics/products.d.ts +4 -2
- package/dist/src/semantics/products.js +12 -138
- package/dist/src/semantics/promotion_codes.d.ts +3 -0
- package/dist/src/semantics/promotion_codes.js +35 -0
- package/dist/src/semantics/radar.d.ts +2 -2
- package/dist/src/semantics/radar.js +5 -74
- package/dist/src/semantics/refunds.d.ts +2 -9
- package/dist/src/semantics/refunds.js +3 -193
- package/dist/src/semantics/shared.d.ts +558 -55
- package/dist/src/semantics/shared.js +2070 -114
- package/dist/src/semantics/states.d.ts +462 -0
- package/dist/src/semantics/states.js +634 -0
- package/dist/src/semantics/subscriptions.d.ts +6 -11
- package/dist/src/semantics/subscriptions.js +37 -530
- package/dist/src/semantics/test_helpers.d.ts +3 -0
- package/dist/src/semantics/test_helpers.js +125 -0
- package/dist/src/semantics/transfers.d.ts +2 -2
- package/dist/src/semantics/transfers.js +6 -101
- package/dist/src/semantics/webhook_endpoints.d.ts +2 -0
- package/dist/src/semantics/webhook_endpoints.js +26 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.js +7 -0
- package/generated/pack-facts.json +94 -0
- package/journeys/coverage.json +596 -0
- package/journeys/customer-life.json +6108 -0
- package/journeys/customer-life.outline.md +371 -0
- package/journeys/decisions.json +677 -0
- package/journeys/demand.json +706 -0
- package/journeys/score.json +853 -0
- package/journeys/unreachable.json +102 -0
- package/journeys/vendor-examples.json +8010 -0
- package/package.json +11 -20
- package/spec/SOURCE.md +18 -0
- package/spec/doc-examples.json +445 -0
- package/spec/fixtures3.json.gz +0 -0
- package/spec/openapi.json.gz +0 -0
- package/spec/patches.json +34 -0
- package/spec/sources.json +306 -0
- package/src/cli.ts +11 -28
- package/src/engine/after-payment.ts +29 -0
- package/src/engine/apps-secrets.ts +4 -0
- package/src/engine/balance.ts +32 -0
- package/src/engine/checkout.ts +39 -0
- package/src/engine/common.ts +49 -0
- package/src/engine/connect.ts +106 -0
- package/src/engine/customers.ts +10 -0
- package/src/engine/display.ts +18 -0
- package/src/engine/files.ts +28 -0
- package/src/engine/invoices.ts +102 -0
- package/src/engine/issuing.ts +66 -0
- package/src/engine/ledger.ts +48 -0
- package/src/engine/oauth.ts +18 -0
- package/src/engine/onboarding.ts +53 -0
- package/src/{stripe-params.ts → engine/params.ts} +7 -56
- package/src/engine/payment-intents.ts +32 -0
- package/src/engine/payment-methods.ts +24 -0
- package/src/engine/radar.ts +6 -0
- package/src/engine/refunds.ts +3 -0
- package/src/engine/renewals.ts +72 -0
- package/src/{stripe-twin.ts → engine/stripe.ts} +57 -400
- package/src/engine/subscriptions.ts +37 -0
- package/src/engine/transfers.ts +22 -0
- package/src/{stripe-version.ts → engine/version.ts} +19 -10
- package/src/engine/webhook-endpoints.ts +14 -0
- package/src/engine/webhooks.ts +70 -0
- package/src/fetch.ts +27 -0
- package/src/generated/surface.gen.json +1 -1
- package/src/generated/ui.gen.json +1 -1
- package/src/index.ts +10 -116
- package/src/manifest.ts +480 -904
- package/src/screens/{portal.tsx → billing-portal.tsx} +35 -42
- package/src/screens/checkout-js.ts +80 -0
- package/src/screens/checkout.tsx +32 -96
- package/src/screens/{onboarding.tsx → connect-onboarding.tsx} +39 -85
- package/src/screens/dashboard.tsx +178 -0
- package/src/screens/shared.tsx +84 -0
- package/src/screens/stripe-js.ts +102 -0
- package/src/semantics/account.ts +10 -0
- package/src/semantics/account_links.ts +20 -0
- package/src/semantics/accounts.ts +44 -0
- package/src/semantics/{apps-secrets.ts → apps.ts} +17 -33
- package/src/semantics/around.ts +17 -0
- package/src/semantics/balance.ts +6 -208
- package/src/semantics/balance_transactions.ts +18 -0
- package/src/semantics/billing_portal.ts +45 -0
- package/src/semantics/charges.ts +8 -212
- package/src/semantics/checkout.ts +25 -276
- package/src/semantics/clock.ts +19 -0
- package/src/semantics/coupons.ts +5 -68
- package/src/semantics/customers.ts +48 -414
- package/src/semantics/doors.ts +79 -0
- package/src/semantics/entitlements.ts +3 -94
- package/src/semantics/events.ts +37 -0
- package/src/semantics/index.ts +89 -0
- package/src/semantics/invoice_payments.ts +16 -0
- package/src/semantics/invoices.ts +112 -441
- package/src/semantics/issuing.ts +57 -573
- package/src/semantics/payment_intents.ts +76 -0
- package/src/semantics/payment_methods.ts +20 -0
- package/src/semantics/payouts.ts +29 -0
- package/src/semantics/prices.ts +41 -0
- package/src/semantics/products.ts +12 -146
- package/src/semantics/promotion_codes.ts +38 -0
- package/src/semantics/radar.ts +8 -79
- package/src/semantics/refunds.ts +4 -216
- package/src/semantics/shared.ts +2089 -135
- package/src/semantics/states.ts +662 -0
- package/src/semantics/subscriptions.ts +39 -542
- package/src/semantics/test_helpers.ts +119 -0
- package/src/semantics/transfers.ts +10 -132
- package/src/semantics/webhook_endpoints.ts +27 -0
- package/src/server.ts +7 -0
- package/client/dashboard-api.ts +0 -286
- package/client/stripe-mirror.css +0 -277
- package/client/stripe-mirror.tsx +0 -1559
- package/dist/client/dashboard-api.d.ts +0 -107
- package/dist/client/dashboard-api.js +0 -238
- package/dist/client/dashboard-api.ts +0 -286
- package/dist/client/stripe-mirror.bundle.js +0 -236
- package/dist/client/stripe-mirror.css +0 -277
- package/dist/client/stripe-mirror.d.ts +0 -134
- package/dist/client/stripe-mirror.js +0 -859
- package/dist/client/stripe-mirror.tsx +0 -1559
- package/dist/src/screens/connect-oauth.d.ts +0 -27
- package/dist/src/screens/connect-oauth.js +0 -414
- package/dist/src/screens/connect-settings.d.ts +0 -22
- package/dist/src/screens/connect-settings.js +0 -103
- package/dist/src/screens/consent-skin.d.ts +0 -4
- package/dist/src/screens/consent-skin.js +0 -18
- package/dist/src/screens/financial-connections.d.ts +0 -5
- package/dist/src/screens/financial-connections.js +0 -90
- package/dist/src/screens/identity.d.ts +0 -5
- package/dist/src/screens/identity.js +0 -86
- package/dist/src/screens/onboarding.d.ts +0 -13
- package/dist/src/screens/portal.d.ts +0 -5
- package/dist/src/screens/public-details.d.ts +0 -5
- package/dist/src/screens/public-details.js +0 -90
- package/dist/src/semantics/after-payment.d.ts +0 -22
- package/dist/src/semantics/after-payment.js +0 -99
- package/dist/src/semantics/apps-secrets.d.ts +0 -2
- package/dist/src/semantics/billing.d.ts +0 -2
- package/dist/src/semantics/billing.js +0 -220
- package/dist/src/semantics/connect.d.ts +0 -5
- package/dist/src/semantics/connect.js +0 -493
- package/dist/src/semantics/credit-notes.d.ts +0 -2
- package/dist/src/semantics/credit-notes.js +0 -172
- package/dist/src/semantics/disputes.d.ts +0 -2
- package/dist/src/semantics/disputes.js +0 -51
- package/dist/src/semantics/ephemeral-keys.d.ts +0 -2
- package/dist/src/semantics/ephemeral-keys.js +0 -34
- package/dist/src/semantics/files.d.ts +0 -2
- package/dist/src/semantics/files.js +0 -125
- package/dist/src/semantics/ledger.d.ts +0 -59
- package/dist/src/semantics/ledger.js +0 -200
- package/dist/src/semantics/payment-intents.d.ts +0 -18
- package/dist/src/semantics/payment-intents.js +0 -452
- package/dist/src/semantics/payment-links.d.ts +0 -2
- package/dist/src/semantics/payment-links.js +0 -133
- package/dist/src/semantics/payment-methods.d.ts +0 -20
- package/dist/src/semantics/payment-methods.js +0 -140
- package/dist/src/semantics/plans.d.ts +0 -5
- package/dist/src/semantics/plans.js +0 -121
- package/dist/src/semantics/platform.d.ts +0 -9
- package/dist/src/semantics/platform.js +0 -206
- package/dist/src/semantics/renewals.d.ts +0 -47
- package/dist/src/semantics/renewals.js +0 -281
- package/dist/src/semantics/setup-intents.d.ts +0 -2
- package/dist/src/semantics/setup-intents.js +0 -84
- package/dist/src/semantics/subscription-schedules.d.ts +0 -2
- package/dist/src/semantics/subscription-schedules.js +0 -119
- package/dist/src/semantics/tax.d.ts +0 -2
- package/dist/src/semantics/tax.js +0 -197
- package/dist/src/semantics/terminal.d.ts +0 -5
- package/dist/src/semantics/terminal.js +0 -182
- package/dist/src/semantics/test-clocks.d.ts +0 -6
- package/dist/src/semantics/test-clocks.js +0 -73
- package/dist/src/semantics/tokens.d.ts +0 -4
- package/dist/src/semantics/tokens.js +0 -44
- package/dist/src/semantics/treasury.d.ts +0 -2
- package/dist/src/semantics/treasury.js +0 -377
- package/dist/src/semantics/webhook-endpoints.d.ts +0 -3
- package/dist/src/semantics/webhook-endpoints.js +0 -85
- package/dist/src/stripe-budget.d.ts +0 -55
- package/dist/src/stripe-budget.js +0 -155
- package/dist/src/stripe-capabilities.d.ts +0 -3
- package/dist/src/stripe-capabilities.js +0 -5836
- package/dist/src/stripe-conformance.d.ts +0 -43
- package/dist/src/stripe-conformance.js +0 -105
- package/dist/src/stripe-connector.d.ts +0 -161
- package/dist/src/stripe-connector.js +0 -414
- package/dist/src/stripe-emit.d.ts +0 -2
- package/dist/src/stripe-emit.js +0 -145
- package/dist/src/stripe-events.d.ts +0 -93
- package/dist/src/stripe-events.js +0 -392
- package/dist/src/stripe-js.d.ts +0 -4
- package/dist/src/stripe-js.js +0 -70
- package/dist/src/stripe-mirror-ui.d.ts +0 -15
- package/dist/src/stripe-mirror-ui.js +0 -87
- package/dist/src/stripe-perform-harness.d.ts +0 -9
- package/dist/src/stripe-perform-harness.js +0 -26
- package/dist/src/stripe-server.d.ts +0 -33
- package/dist/src/stripe-server.js +0 -429
- package/dist/src/stripe-shared.d.ts +0 -109
- package/dist/src/stripe-shared.js +0 -276
- package/dist/src/stripe-ui-conformance.d.ts +0 -5
- package/dist/src/stripe-ui-conformance.js +0 -79
- package/dist/src/stripe-ui-structure.d.ts +0 -3
- package/dist/src/stripe-ui-structure.js +0 -168
- package/dist/test-fixtures/stripe-known-deviations.json +0 -110
- package/dist/test-fixtures/stripe-openapi-operations.SOURCE.md +0 -14
- package/dist/test-fixtures/stripe-openapi-operations.json +0 -4717
- package/dist/test-fixtures/stripe-schemas.SOURCE.md +0 -35
- package/dist/test-fixtures/stripe-schemas.json +0 -3813
- package/src/screens/connect-oauth.tsx +0 -400
- package/src/screens/connect-settings.tsx +0 -121
- package/src/screens/consent-skin.ts +0 -20
- package/src/screens/financial-connections.tsx +0 -101
- package/src/screens/identity.tsx +0 -96
- package/src/screens/public-details.tsx +0 -105
- package/src/semantics/after-payment.ts +0 -118
- package/src/semantics/billing.ts +0 -216
- package/src/semantics/connect.ts +0 -487
- package/src/semantics/credit-notes.ts +0 -168
- package/src/semantics/disputes.ts +0 -62
- package/src/semantics/ephemeral-keys.ts +0 -34
- package/src/semantics/files.ts +0 -143
- package/src/semantics/ledger.ts +0 -253
- package/src/semantics/payment-intents.ts +0 -463
- package/src/semantics/payment-links.ts +0 -148
- package/src/semantics/payment-methods.ts +0 -145
- package/src/semantics/plans.ts +0 -131
- package/src/semantics/platform.ts +0 -220
- package/src/semantics/renewals.ts +0 -279
- package/src/semantics/setup-intents.ts +0 -87
- package/src/semantics/subscription-schedules.ts +0 -129
- package/src/semantics/tax.ts +0 -220
- package/src/semantics/terminal.ts +0 -195
- package/src/semantics/test-clocks.ts +0 -77
- package/src/semantics/tokens.ts +0 -52
- package/src/semantics/treasury.ts +0 -383
- package/src/semantics/webhook-endpoints.ts +0 -87
- package/src/stripe-budget.ts +0 -181
- package/src/stripe-capabilities.ts +0 -5651
- package/src/stripe-conformance.ts +0 -124
- package/src/stripe-connector.ts +0 -462
- package/src/stripe-emit.ts +0 -157
- package/src/stripe-events.ts +0 -434
- package/src/stripe-js.ts +0 -70
- package/src/stripe-mirror-ui.ts +0 -90
- package/src/stripe-perform-harness.ts +0 -29
- package/src/stripe-server.ts +0 -393
- package/src/stripe-shared.ts +0 -297
- package/src/stripe-ui-conformance.ts +0 -88
- package/src/stripe-ui-structure.ts +0 -182
- package/test-fixtures/stripe-known-deviations.json +0 -110
- package/test-fixtures/stripe-openapi-operations.json +0 -4717
- package/test-fixtures/stripe-schemas.json +0 -3813
- /package/dist/src/{screens → engine}/industries.d.ts +0 -0
- /package/dist/src/{screens → engine}/industries.js +0 -0
- /package/dist/src/{semantics → engine}/test-cards.d.ts +0 -0
- /package/dist/src/{semantics → engine}/test-cards.js +0 -0
- /package/src/{screens → engine}/industries.ts +0 -0
- /package/src/{semantics → engine}/test-cards.ts +0 -0
|
@@ -16,20 +16,15 @@
|
|
|
16
16
|
// (the account's default) enables cancellation at period end, updating the payment method and invoice history;
|
|
17
17
|
// changing the plan from the portal is not offered yet. A new card is saved without a charge, so a card that
|
|
18
18
|
// declines only when charged (4000000000000341) is saved and declines when the invoice is paid.
|
|
19
|
-
import {
|
|
20
|
-
import { flowPage, Portal, PORTAL_CSS, type
|
|
21
|
-
import
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { created,
|
|
26
|
-
import { publicBusinessName } from '../stripe-shared.ts';
|
|
27
|
-
import { declineFor, paymentMethodSubObject, PLATFORM_ACCOUNT_ID } from '../stripe-twin.ts';
|
|
28
|
-
import { cardAnswer, COUNTRIES, money } from './checkout.tsx';
|
|
29
|
-
import { formOf } from './consent-skin.ts';
|
|
30
|
-
|
|
19
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
20
|
+
import { flowPage, type PaymentField, Portal, PORTAL_CSS, type PortalItem } from '@volter/world-ui';
|
|
21
|
+
import type { Row } from '../engine/common.ts';
|
|
22
|
+
import { publicBusinessName } from '../engine/display.ts';
|
|
23
|
+
import { declineFor, nowUnix, paymentMethodSubObject, PLATFORM_ACCOUNT_ID } from '../engine/stripe.ts';
|
|
24
|
+
import { cardAnswer, COUNTRIES, formOf, money, notFound } from './shared.tsx';
|
|
25
|
+
import { resourceView, afterSuccessOf, created, payerOf, payOpenInvoice } from '../semantics/shared.ts';
|
|
31
26
|
const SUB = 'subscription';
|
|
32
|
-
const PORTAL_OPERATION =
|
|
27
|
+
const PORTAL_OPERATION = { id: 'PostBillingPortalSessions' };
|
|
33
28
|
|
|
34
29
|
// Stripe's skin: its type, its purple accent, the merchant's side tinted.
|
|
35
30
|
const SKIN = `
|
|
@@ -50,7 +45,7 @@ const day = (epoch: unknown): string => new Date(Number(epoch) * 1000).toLocaleD
|
|
|
50
45
|
const gone = (): Response => flowPage({ title: 'Billing', status: 404, css: [PORTAL_CSS, SKIN], body: <main className="portal-main"><h1>This link has expired.</h1></main> });
|
|
51
46
|
|
|
52
47
|
type Features = { cancel: { enabled: boolean; mode: 'at_period_end' | 'immediately' }; invoices: boolean; paymentMethod: boolean };
|
|
53
|
-
function featuresOf(ctx:
|
|
48
|
+
function featuresOf(ctx: HandlerContext, session: Row): Features {
|
|
54
49
|
const f = (ctx.get('billing_portal.configuration', String(session.configuration))?.features ?? undefined) as Row | undefined;
|
|
55
50
|
const cancel = (f?.subscription_cancel ?? {}) as Row;
|
|
56
51
|
const invoices = (f?.invoice_history ?? {}) as Row;
|
|
@@ -62,7 +57,7 @@ function featuresOf(ctx: SemanticsContext, session: Row): Features {
|
|
|
62
57
|
};
|
|
63
58
|
}
|
|
64
59
|
|
|
65
|
-
function planOf(ctx:
|
|
60
|
+
function planOf(ctx: HandlerContext, sub: Row): { name: string; price: string } {
|
|
66
61
|
const item = (((sub.items as { data?: Row[] } | undefined)?.data) ?? [])[0];
|
|
67
62
|
const price = (typeof item?.price === 'object' ? item.price : ctx.get('price', String(item?.price ?? ''))) as Row | undefined;
|
|
68
63
|
const product = typeof price?.product === 'string' ? ctx.get('product', price.product) : (price?.product as Row | undefined);
|
|
@@ -73,10 +68,10 @@ function planOf(ctx: SemanticsContext, sub: Row): { name: string; price: string
|
|
|
73
68
|
};
|
|
74
69
|
}
|
|
75
70
|
|
|
76
|
-
function page(ctx:
|
|
71
|
+
function page(ctx: HandlerContext, id: string, session: Row, problem?: { error: string; values: Record<string, string> }): Response {
|
|
77
72
|
const features = featuresOf(ctx, session);
|
|
78
73
|
const customer = ctx.get('customer', String(session.customer)) ?? {};
|
|
79
|
-
const subs = ctx.rows(SUB).filter((s) => s.customer === session.customer && s.status !== 'canceled' && s.status !== 'incomplete_expired');
|
|
74
|
+
const subs = ctx.rows(SUB).map((s) => ctx.expand(SUB, s)).filter((s) => s.customer === session.customer && s.status !== 'canceled' && s.status !== 'incomplete_expired');
|
|
80
75
|
const plans: PortalItem[] = subs.map((s) => {
|
|
81
76
|
const plan = planOf(ctx, s);
|
|
82
77
|
const ending = s.cancel_at_period_end === true;
|
|
@@ -106,7 +101,7 @@ function page(ctx: SemanticsContext, id: string, session: Row, problem?: { error
|
|
|
106
101
|
}))
|
|
107
102
|
: [];
|
|
108
103
|
const back = typeof session.return_url === 'string' ? session.return_url : undefined;
|
|
109
|
-
// the portal shows the public business name (
|
|
104
|
+
// the portal shows the public business name (../engine/display.ts, publicBusinessName) of the session's on_behalf_of
|
|
110
105
|
// account ("Use the Accounts API to modify the on_behalf_of account's branding settings, which the portal displays",
|
|
111
106
|
// docs.stripe.com/api/customer_portal/sessions/object), else of the account it was made on (Stripe-Account), else the
|
|
112
107
|
// platform's
|
|
@@ -143,8 +138,8 @@ function page(ctx: SemanticsContext, id: string, session: Row, problem?: { error
|
|
|
143
138
|
}
|
|
144
139
|
|
|
145
140
|
/** The customer's move on one of their subscriptions, then back to the portal. */
|
|
146
|
-
async function move(ctx:
|
|
147
|
-
const sub = ctx
|
|
141
|
+
async function move(ctx: HandlerContext, id: string, session: Row, subscription: string, kind: 'cancel' | 'renew'): Promise<Response> {
|
|
142
|
+
const sub = resourceView(ctx, SUB, subscription);
|
|
148
143
|
if (!sub || sub.customer !== session.customer || sub.status === 'canceled') return gone();
|
|
149
144
|
const back = new Response(null, { status: 303, headers: { location: `/p/session/${id}` } });
|
|
150
145
|
if (kind === 'renew') {
|
|
@@ -153,7 +148,7 @@ async function move(ctx: SemanticsContext, id: string, session: Row, subscriptio
|
|
|
153
148
|
}
|
|
154
149
|
const features = featuresOf(ctx, session);
|
|
155
150
|
if (!features.cancel.enabled) return gone();
|
|
156
|
-
const now =
|
|
151
|
+
const now = nowUnix(ctx.occurredAt);
|
|
157
152
|
const details = { comment: null, feedback: null, reason: 'cancellation_requested' };
|
|
158
153
|
if (features.cancel.mode === 'at_period_end') {
|
|
159
154
|
await ctx.write(SUB, subscription, { cancel_at_period_end: true, cancel_at: sub.current_period_end ?? null, canceled_at: now, cancellation_details: details }, 'subscription.update');
|
|
@@ -166,9 +161,9 @@ async function move(ctx: SemanticsContext, id: string, session: Row, subscriptio
|
|
|
166
161
|
}
|
|
167
162
|
|
|
168
163
|
/** The customer's new card: saved on them (no charge) as the default their invoices and subscriptions charge. */
|
|
169
|
-
async function updatePaymentMethod(ctx:
|
|
164
|
+
async function updatePaymentMethod(ctx: HandlerContext, id: string, session: Row, v: Record<string, string>): Promise<Response> {
|
|
170
165
|
if (!featuresOf(ctx, session).paymentMethod) return gone();
|
|
171
|
-
const refused = cardAnswer(v,
|
|
166
|
+
const refused = cardAnswer(v, nowUnix(ctx.occurredAt), false);
|
|
172
167
|
if (refused) return page(ctx, id, session, { error: refused, values: v });
|
|
173
168
|
const customer = String(session.customer);
|
|
174
169
|
const [month, year] = (v.cardExpiry ?? '').split('/').map((x) => Number(x.trim()));
|
|
@@ -181,17 +176,17 @@ async function updatePaymentMethod(ctx: SemanticsContext, id: string, session: R
|
|
|
181
176
|
});
|
|
182
177
|
const holder = ctx.get('customer', customer) ?? {};
|
|
183
178
|
await ctx.write('customer', customer, { invoice_settings: { ...((holder.invoice_settings as Row | undefined) ?? {}), default_payment_method: pm.id } }, 'customer.update');
|
|
184
|
-
for (const sub of ctx.rows(SUB).filter((s) => s.customer === customer && s.status !== 'canceled' && s.status !== 'incomplete_expired')) {
|
|
179
|
+
for (const sub of ctx.rows(SUB).map((s) => ctx.expand(SUB, s)).filter((s) => s.customer === customer && s.status !== 'canceled' && s.status !== 'incomplete_expired')) {
|
|
185
180
|
await ctx.write(SUB, String(sub.id), { default_payment_method: pm.id }, 'subscription.update');
|
|
186
181
|
}
|
|
187
182
|
return new Response(null, { status: 303, headers: { location: `/p/session/${id}` } });
|
|
188
183
|
}
|
|
189
184
|
|
|
190
185
|
/** The customer pays one of their open invoices with the card on file. */
|
|
191
|
-
async function payInvoice(ctx:
|
|
186
|
+
async function payInvoice(ctx: HandlerContext, id: string, session: Row, invoiceId: string): Promise<Response> {
|
|
192
187
|
const inv = ctx.get('invoice', invoiceId);
|
|
193
188
|
if (!inv || inv.customer !== session.customer || inv.status !== 'open') return gone();
|
|
194
|
-
const sub = typeof inv.subscription === 'string' ? ctx
|
|
189
|
+
const sub = typeof inv.subscription === 'string' ? resourceView(ctx, SUB, inv.subscription) : undefined;
|
|
195
190
|
const holder = ctx.get('customer', String(session.customer)) ?? {};
|
|
196
191
|
const pm = (sub ? payerOf(ctx, sub) : undefined) ?? ((holder.invoice_settings as Row | undefined)?.default_payment_method as string | undefined);
|
|
197
192
|
if (!pm) return page(ctx, id, session, { error: 'Add a payment method to pay this invoice.', values: {} });
|
|
@@ -201,20 +196,18 @@ async function payInvoice(ctx: SemanticsContext, id: string, session: Row, invoi
|
|
|
201
196
|
}
|
|
202
197
|
|
|
203
198
|
/** billing.stripe.com's portal pages, or undefined for any other request. */
|
|
204
|
-
export function
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return move(ctx, id, session, form.subscription ?? '', kind);
|
|
219
|
-
};
|
|
199
|
+
export async function screen(ctx: HandlerContext): Promise<Response> {
|
|
200
|
+
const request = ctx.call.request;
|
|
201
|
+
const m = /^\/p\/session\/(bps_[A-Za-z0-9_]+)(?:\/(cancel|renew|payment_method|pay))?\/?$/.exec(new URL(request.url).pathname);
|
|
202
|
+
if (!m) return notFound();
|
|
203
|
+
const [, id, kind] = m as unknown as [string, string, 'cancel' | 'renew' | 'payment_method' | 'pay' | undefined];
|
|
204
|
+
if ((kind && request.method !== 'POST') || (!kind && request.method !== 'GET')) return notFound();
|
|
205
|
+
// the form is read before the context, which reads the request's body itself
|
|
206
|
+
const form = kind ? await formOf(request.clone()) : {};
|
|
207
|
+
const session = ctx.get('billing_portal.session', id);
|
|
208
|
+
if (!session) return gone();
|
|
209
|
+
if (!kind) return page(ctx, id, session);
|
|
210
|
+
if (kind === 'payment_method') return updatePaymentMethod(ctx, id, session, form);
|
|
211
|
+
if (kind === 'pay') return payInvoice(ctx, id, session, form.invoice ?? '');
|
|
212
|
+
return move(ctx, id, session, form.subscription ?? '', kind);
|
|
220
213
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
2
|
+
import type { Row } from '../engine/common.ts';
|
|
3
|
+
import { newCustomer } from '../engine/customers.ts';
|
|
4
|
+
import { CS } from '../engine/checkout.ts';
|
|
5
|
+
import { applyCouponDiscount, nowUnix, resolveTrial } from '../engine/stripe.ts';
|
|
6
|
+
import { cardAnswer, parseExpiry } from './shared.tsx';
|
|
7
|
+
import { completeSession, discountOf, created } from '../semantics/shared.ts';
|
|
8
|
+
|
|
9
|
+
function amount(value: number, currency: string): Row {
|
|
10
|
+
return { amount: new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(value / 100), minorUnitsAmount: value };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// source: archive:https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.6.0.tgz#sha256=13e61419e9076ab440cc636e34d3ea5df3fc62bbf2d6a80d3fd718e8cd47f0a2!/package/dist/stripe-js/checkout.d.ts "export interface StripeCheckoutSession"
|
|
14
|
+
// The SDK publishes its contract, not its private HTTP wire. This authored payment_pages wire uses the same session store and completion as hosted Checkout.
|
|
15
|
+
function view(ctx: HandlerContext, session: Row): Row {
|
|
16
|
+
const currency = String(session.currency ?? 'usd');
|
|
17
|
+
const lines = ((session.line_items as Row)?.data ?? []) as Row[];
|
|
18
|
+
const stored = ctx.row(CS, String(session.id)) ?? session;
|
|
19
|
+
const trial = session.mode === 'subscription' ? resolveTrial((stored._subscription_data as Row) ?? {}, nowUnix(ctx.occurredAt)) : undefined;
|
|
20
|
+
const customer = typeof session.customer === 'string' ? ctx.get('customer', session.customer) : undefined;
|
|
21
|
+
const discount = stored._discount as Row | undefined;
|
|
22
|
+
const promotion = typeof discount?.promotion_code === 'string' ? ctx.get('promotion_code', discount.promotion_code) : undefined;
|
|
23
|
+
const zero = amount(0, currency);
|
|
24
|
+
return {
|
|
25
|
+
id: session.id, billingAddress: null, businessName: null, canConfirm: session.status === 'open', currency,
|
|
26
|
+
currencyOptions: null, discountAmounts: promotion ? [{ ...amount(Number((session.total_details as Row)?.amount_discount ?? 0), currency), promotionCode: promotion.code }] : [],
|
|
27
|
+
email: customer?.email ?? session.customer_email ?? null, lastPaymentError: null,
|
|
28
|
+
lineItems: lines.map(line => ({ id: line.id, name: line.description, quantity: line.quantity, unitAmount: amount(Number(line.amount_subtotal) / Number(line.quantity || 1), currency), total: amount(Number(line.amount_total), currency) })),
|
|
29
|
+
livemode: false, minorUnitsAmountDivisor: 100, phoneNumber: null,
|
|
30
|
+
recurring: session.mode === 'subscription' ? { trial: trial ? { trialEnd: trial.end, trialPeriodDays: Math.ceil((trial.end - trial.start) / 86400) } : null } : null,
|
|
31
|
+
savedPaymentMethods: [], shipping: null, shippingAddress: null, shippingOptions: [],
|
|
32
|
+
status: session.status === 'complete' ? { type: 'complete', paymentStatus: session.payment_status } : { type: session.status },
|
|
33
|
+
tax: { status: 'ready' }, taxAmounts: [], taxIdInfo: null,
|
|
34
|
+
total: { appliedBalance: zero, balanceAppliedToNextInvoice: false, discount: amount(Number((session.total_details as Row)?.amount_discount ?? 0), currency), shippingRate: zero, subtotal: amount(Number(session.amount_subtotal ?? 0), currency), taxExclusive: zero, taxInclusive: zero, total: amount(trial ? 0 : Number(session.amount_total ?? 0), currency) },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const failed = (message: string, code: string | null = null): Response => Response.json({ type: 'error', error: { message, code } });
|
|
39
|
+
|
|
40
|
+
export async function screen(ctx: HandlerContext): Promise<Response> {
|
|
41
|
+
const path = new URL(ctx.call.request.url).pathname;
|
|
42
|
+
const id = path.split('/')[3]!;
|
|
43
|
+
const action = path.split('/')[4];
|
|
44
|
+
const params = ctx.params;
|
|
45
|
+
const session = ctx.get(CS, id);
|
|
46
|
+
// The private client secret is the browser's authority for this session; it never accepts a secret API key.
|
|
47
|
+
if (!session || session.ui_mode !== 'custom' || params.client_secret !== session.client_secret) return failed('Invalid Checkout Session client secret.');
|
|
48
|
+
if (action === 'init') return Response.json({ type: 'success', session: view(ctx, session) });
|
|
49
|
+
if (session.status !== 'open') return failed('This Checkout Session is no longer active.');
|
|
50
|
+
if (action === 'apply_promotion_code' || action === 'remove_promotion_code') {
|
|
51
|
+
if (action === 'apply_promotion_code' && session.allow_promotion_codes !== true) return failed('Promotion codes are not enabled for this Checkout Session.');
|
|
52
|
+
const promotion = action === 'apply_promotion_code' ? ctx.rows('promotion_code').find(p => p.code === params.promotion_code && p.active === true) : undefined;
|
|
53
|
+
if (action === 'apply_promotion_code' && !promotion) return failed('This promotion code is invalid.', 'invalidPromotionCode');
|
|
54
|
+
const discount = promotion ? discountOf(ctx, { discounts: [{ promotion_code: promotion.id }] }, nowUnix(ctx.occurredAt)) : undefined;
|
|
55
|
+
if (discount instanceof Response) return failed('This promotion code is invalid.', 'invalidPromotionCode');
|
|
56
|
+
const off = discount ? applyCouponDiscount(Number(session.amount_subtotal), discount.coupon) : 0;
|
|
57
|
+
const fields = { amount_total: Number(session.amount_subtotal) - off, total_details: { ...(session.total_details as Row), amount_discount: off }, discounts: discount ? [{ coupon: discount.coupon.id, promotion_code: promotion!.id }] : [], _discount: discount ? { coupon: discount.coupon.id, promotion_code: promotion!.id } : null };
|
|
58
|
+
const updated = await ctx.write(CS, id, fields, 'checkout.session.update');
|
|
59
|
+
return Response.json({ type: 'success', session: view(ctx, updated) });
|
|
60
|
+
}
|
|
61
|
+
if (action !== 'confirm') return failed('Unrecognized Checkout action.');
|
|
62
|
+
const values = (params.payment_details ?? {}) as Record<string, string>;
|
|
63
|
+
const stored = ctx.row(CS, id) ?? session;
|
|
64
|
+
const trial = session.mode === 'subscription' ? resolveTrial((stored._subscription_data as Row) ?? {}, nowUnix(ctx.occurredAt)) : undefined;
|
|
65
|
+
const refusal = cardAnswer(values, nowUnix(ctx.occurredAt), !trial);
|
|
66
|
+
if (refusal) return failed(refusal, 'paymentFailed');
|
|
67
|
+
const customer = typeof session.customer === 'string' ? ctx.get('customer', session.customer) : undefined;
|
|
68
|
+
const email = String(customer?.email ?? params.email ?? session.customer_email ?? '');
|
|
69
|
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return failed('Your email address is incomplete.');
|
|
70
|
+
let existing = session;
|
|
71
|
+
if (!customer && session.mode === 'subscription') {
|
|
72
|
+
const cid = ctx.mint('customer');
|
|
73
|
+
const made = await created(ctx, 'customer', { id: cid, email }, { livemode: false, ...newCustomer(cid) });
|
|
74
|
+
existing = { ...session, customer: made.id };
|
|
75
|
+
}
|
|
76
|
+
const expiry = parseExpiry(values.cardExpiry)!;
|
|
77
|
+
const completed = await completeSession(ctx, id, existing, { customer: existing.customer, customer_details: { email, name: values.billingName ?? null, address: null, phone: null, tax_exempt: 'none', tax_ids: [] } }, { number: values.cardNumber.replace(/\D/g, ''), exp_month: expiry.month, exp_year: expiry.year });
|
|
78
|
+
if (completed instanceof Response) return failed('This Checkout Session is no longer active.');
|
|
79
|
+
return Response.json({ type: 'success', session: view(ctx, completed), return_url: session.return_url });
|
|
80
|
+
}
|
package/src/screens/checkout.tsx
CHANGED
|
@@ -15,41 +15,17 @@
|
|
|
15
15
|
// only sets the card up: setup mode, or a subscription's trial);
|
|
16
16
|
// any other number that passes the Luhn check pays, and a card that asks for authentication pays without
|
|
17
17
|
// the challenge. A subscription session with no customer makes one from the email, as Stripe does.
|
|
18
|
-
import {
|
|
18
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
19
19
|
import { flowPage, Payment, PAYMENT_CSS, type PaymentField } from '@volter/world-ui';
|
|
20
|
-
import
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import { formOf } from './consent-skin.ts';
|
|
28
|
-
|
|
20
|
+
import { isOneTime } from '../engine/checkout.ts';
|
|
21
|
+
import { newCustomer } from '../engine/customers.ts';
|
|
22
|
+
import type { Row } from '../engine/common.ts';
|
|
23
|
+
import { publicBusinessName } from '../engine/display.ts';
|
|
24
|
+
import { applyCouponDiscount, nowUnix, PLATFORM_ACCOUNT_ID, resolveTrial } from '../engine/stripe.ts';
|
|
25
|
+
import { cardAnswer, COUNTRIES, formOf, money, notFound, parseExpiry } from './shared.tsx';
|
|
26
|
+
import { completeSession, created } from '../semantics/shared.ts';
|
|
29
27
|
const CS = 'checkout.session';
|
|
30
|
-
const COMPLETE_OPERATION =
|
|
31
|
-
|
|
32
|
-
const DECLINES: Record<string, string> = {
|
|
33
|
-
'4000000000000002': 'Your card was declined.',
|
|
34
|
-
'4000000000009995': 'Your card has insufficient funds.',
|
|
35
|
-
'4000000000000069': 'Your card has expired.',
|
|
36
|
-
'4000000000000127': "Your card's security code is incorrect.",
|
|
37
|
-
};
|
|
38
|
-
const luhn = (digits: string): boolean => {
|
|
39
|
-
let sum = 0;
|
|
40
|
-
for (let i = 0; i < digits.length; i++) {
|
|
41
|
-
let d = Number(digits[digits.length - 1 - i]);
|
|
42
|
-
if (i % 2 === 1) { d *= 2; if (d > 9) d -= 9; }
|
|
43
|
-
sum += d;
|
|
44
|
-
}
|
|
45
|
-
return digits.length >= 12 && sum % 10 === 0;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export const money = (minor: unknown, currency: unknown): string => {
|
|
49
|
-
const c = typeof currency === 'string' ? currency.toUpperCase() : 'USD';
|
|
50
|
-
const zeroDecimal = ['JPY', 'KRW', 'VND', 'CLP', 'ISK', 'UGX', 'XAF', 'XOF'].includes(c);
|
|
51
|
-
return new Intl.NumberFormat('en-US', { style: 'currency', currency: c }).format((Number(minor) || 0) / (zeroDecimal ? 1 : 100));
|
|
52
|
-
};
|
|
28
|
+
const COMPLETE_OPERATION = { id: 'PostCheckoutSessionsSession' };
|
|
53
29
|
|
|
54
30
|
// Stripe's skin: its type, its blue submit, the summary on a tinted half.
|
|
55
31
|
const SKIN = `
|
|
@@ -61,8 +37,6 @@ body { color: #1a1a1a; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI
|
|
|
61
37
|
.pay-submit { background: #0074d4; color: #ffffff; }
|
|
62
38
|
`;
|
|
63
39
|
|
|
64
|
-
export const COUNTRIES = [['US', 'United States'], ['GB', 'United Kingdom'], ['CA', 'Canada'], ['DE', 'Germany'], ['FR', 'France'], ['AU', 'Australia'], ['JP', 'Japan']].map(([value, label]) => ({ value: value!, label: label! }));
|
|
65
|
-
|
|
66
40
|
/** A subscription session's trial, when its subscription_data gives one: `trial_period_days`, or `trial_end`
|
|
67
41
|
* (docs.stripe.com/payments/checkout/free-trials), as resolveTrial makes the subscription's at completion. */
|
|
68
42
|
function trialOf(session: Row, now: number): { start: number; end: number } | undefined {
|
|
@@ -103,7 +77,7 @@ function trialSummary(session: Row, trial: { start: number; end: number }): { le
|
|
|
103
77
|
|
|
104
78
|
/** What each period after the trial charges, as the subscription's invoices will: its recurring line items (a one-time
|
|
105
79
|
* item is on the first invoice only, semantics/checkout.ts isOneTime), under the session's coupon unless the coupon is `once`, which the trial's own $0
|
|
106
|
-
* invoice takes (
|
|
80
|
+
* invoice takes (../engine/renewals.ts draftSubscriptionInvoice). */
|
|
107
81
|
function afterTrial(session: Row): number {
|
|
108
82
|
const items = (((session.line_items as { data?: Row[] } | undefined)?.data) ?? []).filter((it, i) => !isOneTime(it, inlineOf(session, i)));
|
|
109
83
|
const subtotal = items.reduce((sum, it) => sum + (Number(it.amount_subtotal) || 0), 0);
|
|
@@ -167,52 +141,17 @@ function page(session: Row, id: string, now: number, values: Record<string, stri
|
|
|
167
141
|
const gone = (): Response =>
|
|
168
142
|
flowPage({ title: 'Checkout', status: 404, css: [PAYMENT_CSS, SKIN], body: <main className="pay-form"><h1>This Checkout Session is no longer active.</h1></main> });
|
|
169
143
|
|
|
170
|
-
|
|
171
|
-
const DECLINES_WHEN_CHARGED: Record<string, string> = { '4000000000000341': 'Your card was declined.' };
|
|
172
|
-
|
|
173
|
-
/** The expiry as the customer typed it. Stripe's field formats itself as it is typed: the digits `1230` read `12 / 30`,
|
|
174
|
-
* and a first digit above 1 is a month on its own (`5` reads `05 / `), so a person types the digits straight through
|
|
175
|
-
* (Checkout's own field, seen on checkout.stripe.com; docs.stripe.com/payments/checkout names no format to type). The
|
|
176
|
-
* page's script (@volter/world-ui Payment, `format: 'card-expiry'`) shows it formatted; this reads what is posted either
|
|
177
|
-
* way: `MM / YY`, `MM/YYYY`, or the bare digits `MMYY`, `MYY` (`530`) and `MMYYYY`; a month past 12 is read, and refused
|
|
178
|
-
* as invalid by cardAnswer. */
|
|
179
|
-
export function parseExpiry(raw: string): { month: number; year: number } | undefined {
|
|
180
|
-
const split = /^\s*(\d{1,2})\s*\/\s*(\d{2}|\d{4})\s*$/.exec(raw);
|
|
181
|
-
const digits = raw.replace(/\s/g, '');
|
|
182
|
-
const bare = split ? undefined : /^(\d{2})(\d{2}|\d{4})$/.exec(digits) ?? /^([2-9])(\d{2})$/.exec(digits);
|
|
183
|
-
const m = split ?? bare;
|
|
184
|
-
if (!m) return undefined;
|
|
185
|
-
return { month: Number(m[1]), year: m[2]!.length === 2 ? 2000 + Number(m[2]) : Number(m[2]) };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/** The card's answer: undefined when it pays, else what Checkout says under the button. */
|
|
189
|
-
export function cardAnswer(v: Record<string, string>, nowSeconds: number, chargesNow: boolean): string | undefined {
|
|
190
|
-
const digits = (v.cardNumber ?? '').replace(/\D/g, '');
|
|
191
|
-
if (!digits) return 'Your card number is incomplete.';
|
|
192
|
-
if (DECLINES[digits]) return DECLINES[digits];
|
|
193
|
-
if (chargesNow && DECLINES_WHEN_CHARGED[digits]) return DECLINES_WHEN_CHARGED[digits];
|
|
194
|
-
if (!luhn(digits)) return 'Your card number is invalid.';
|
|
195
|
-
const exp = parseExpiry(v.cardExpiry ?? '');
|
|
196
|
-
if (!exp) return "Your card's expiration date is incomplete.";
|
|
197
|
-
const { month, year } = exp;
|
|
198
|
-
if (month < 1 || month > 12) return "Your card's expiration date is invalid.";
|
|
199
|
-
const now = new Date(nowSeconds * 1000);
|
|
200
|
-
if (year < now.getUTCFullYear() || (year === now.getUTCFullYear() && month < now.getUTCMonth() + 1)) return "Your card's expiration year is in the past.";
|
|
201
|
-
if (!/^\d{3,4}$/.test(v.cardCvc ?? '')) return "Your card's security code is incomplete.";
|
|
202
|
-
return undefined;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async function pay(ctx: SemanticsContext, id: string, session: Row, v: Record<string, string>): Promise<Response> {
|
|
144
|
+
async function pay(ctx: HandlerContext, id: string, session: Row, v: Record<string, string>): Promise<Response> {
|
|
206
145
|
const onFile = customerEmailOf(session);
|
|
207
146
|
const email = onFile ?? (typeof session.customer_email === 'string' ? session.customer_email : (v.email ?? '').trim());
|
|
208
147
|
if (session.mode !== 'setup' || !session.customer) {
|
|
209
|
-
if (!EMAIL.test(email)) return page(session, id,
|
|
148
|
+
if (!EMAIL.test(email)) return page(session, id, nowUnix(ctx.occurredAt), v, 'Your email address is incomplete.');
|
|
210
149
|
}
|
|
211
150
|
// a trial charges nothing now unless a one-time price is on the first invoice
|
|
212
151
|
const oneTimeDue = (((session.line_items as { data?: Row[] } | undefined)?.data) ?? []).some((it, i) => isOneTime(it, inlineOf(session, i)));
|
|
213
|
-
const chargesNow = session.mode === 'payment' || (session.mode === 'subscription' && (!trialOf(session,
|
|
214
|
-
const refused = cardAnswer(v,
|
|
215
|
-
if (refused) return page(session, id,
|
|
152
|
+
const chargesNow = session.mode === 'payment' || (session.mode === 'subscription' && (!trialOf(session, nowUnix(ctx.occurredAt)) || oneTimeDue));
|
|
153
|
+
const refused = cardAnswer(v, nowUnix(ctx.occurredAt), chargesNow);
|
|
154
|
+
if (refused) return page(session, id, nowUnix(ctx.occurredAt), v, refused);
|
|
216
155
|
let existing = session;
|
|
217
156
|
// Checkout always makes a customer for a subscription; a payment makes one only when the session asks
|
|
218
157
|
if (!session.customer && (session.mode === 'subscription' || session.customer_creation === 'always')) {
|
|
@@ -233,35 +172,32 @@ async function pay(ctx: SemanticsContext, id: string, session: Row, v: Record<st
|
|
|
233
172
|
return new Response(null, { status: 303, headers: { location: to } });
|
|
234
173
|
}
|
|
235
174
|
|
|
236
|
-
/** checkout.stripe.com's pages, or undefined for any other request. */
|
|
237
175
|
/** The name the page shows: the session's own `branding_settings.display_name` where the create set one, else the
|
|
238
|
-
* public business name (
|
|
176
|
+
* public business name (../engine/display.ts, publicBusinessName) of the account whose brand Checkout uses: "Checkout uses
|
|
239
177
|
* the brand settings of the connected account for destination charges with on_behalf_of and for platforms performing
|
|
240
178
|
* direct charges" (a session made with Stripe-Account), and otherwise the platform's; "If you omit a field in
|
|
241
179
|
* branding_settings, Checkout applies the default value from the Dashboard"
|
|
242
180
|
* (docs.stripe.com/payments/checkout/customization/appearance). */
|
|
243
|
-
|
|
181
|
+
function merchantOf(ctx: HandlerContext, stored: Row): string {
|
|
244
182
|
const brand = (stored._branding_settings as Row | undefined)?.display_name;
|
|
245
183
|
const behalf = (stored._payment_intent_data as Row | undefined)?.on_behalf_of ?? (stored._subscription_data as Row | undefined)?.on_behalf_of;
|
|
246
184
|
const account = typeof behalf === 'string' ? behalf : typeof stored._brand_account === 'string' ? stored._brand_account : PLATFORM_ACCOUNT_ID;
|
|
247
185
|
return typeof brand === 'string' && brand.trim() ? brand : publicBusinessName(ctx.get('account', account));
|
|
248
186
|
}
|
|
249
187
|
|
|
250
|
-
export function
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
return request.method === 'GET' ? page(full, id, Number(ctx.now())) : pay(ctx, id, full, values);
|
|
266
|
-
};
|
|
188
|
+
export async function screen(ctx: HandlerContext): Promise<Response> {
|
|
189
|
+
const request = ctx.call.request;
|
|
190
|
+
const m = /^\/c\/pay\/(cs_[A-Za-z0-9_]+)\/?$/.exec(new URL(request.url).pathname);
|
|
191
|
+
if (!m || (request.method !== 'GET' && request.method !== 'POST')) return notFound();
|
|
192
|
+
const id = m[1]!;
|
|
193
|
+
// the form is read before the context, which reads the request's body itself
|
|
194
|
+
const values = request.method === 'POST' ? await formOf(request.clone()) : {};
|
|
195
|
+
const session = ctx.get(CS, id);
|
|
196
|
+
if (!session) return gone();
|
|
197
|
+
// an ended session's page is gone; paying on one is the customer's move the machine refuses
|
|
198
|
+
if (session.status !== 'open' && (request.method === 'GET' || ctx.legal(CS, 'status', COMPLETE_OPERATION.id, String(session.status), 'complete', id, 'external'))) return gone();
|
|
199
|
+
const stored = ctx.row(CS, id) ?? {};
|
|
200
|
+
const coupon = (stored._discount as Row | undefined)?.coupon;
|
|
201
|
+
const full = { ...session, _subscription_data: stored._subscription_data, _price_data: stored._price_data, _merchant: merchantOf(ctx, stored), _coupon: typeof coupon === 'string' ? ctx.get('coupon', coupon) : undefined, _customer: typeof session.customer === 'string' ? ctx.get('customer', session.customer) : undefined };
|
|
202
|
+
return request.method === 'GET' ? page(full, id, nowUnix(ctx.occurredAt)) : pay(ctx, id, full, values);
|
|
267
203
|
}
|