@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
|
@@ -1,35 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
// STRIPE'S CUSTOMER PORTAL — a hosted flow (docs/contributing/architecture.md, "Screens"): an application creates
|
|
3
|
-
// a portal session for its customer and sends them to the session's `url` at billing.stripe.com/p/session/{id};
|
|
4
|
-
// there the customer sees their plans, payment method, billing details and invoices, and makes the moves the
|
|
5
|
-
// portal configuration allows, then returns to return_url (docs.stripe.com/customer-management). Canceling is the
|
|
6
|
-
// customer's own move (the subscription machine's external actor): at the period's end by default, or at once
|
|
7
|
-
// when the configuration's subscription_cancel.mode says immediately, and a plan set to cancel can be renewed.
|
|
8
|
-
// Each move goes through the same handler context the API's handlers use, so the application sees it as it
|
|
9
|
-
// would from Stripe: on the subscription, and in its events. Authored from @volter/world-ui's portal piece under
|
|
10
|
-
// Stripe's skin; nothing of Stripe's page is copied.
|
|
11
|
-
//
|
|
12
|
-
// The customer also replaces their card, which becomes the default for their invoices and subscriptions, and pays
|
|
13
|
-
// an open invoice with it, which makes a subscription waiting on it (past_due) active again
|
|
14
|
-
// (docs.stripe.com/customer-management/configure-portal: payment_method_update, invoice_history).
|
|
15
|
-
//
|
|
16
|
-
// Where the documentation stops and the twin decides: a session's configuration that the World never created
|
|
17
|
-
// (the account's default) enables cancellation at period end, updating the payment method and invoice history;
|
|
18
|
-
// changing the plan from the portal is not offered yet. A new card is saved without a charge, so a card that
|
|
19
|
-
// declines only when charged (4000000000000341) is saved and declines when the invoice is paid.
|
|
20
|
-
import { semanticsContext } from '@volter/world-core';
|
|
21
2
|
import { flowPage, Portal, PORTAL_CSS } from '@volter/world-ui';
|
|
22
|
-
import
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import { afterSuccessOf } from "../semantics/
|
|
26
|
-
import { created } from "../semantics/shared.js";
|
|
27
|
-
import { publicBusinessName } from "../stripe-shared.js";
|
|
28
|
-
import { declineFor, paymentMethodSubObject, PLATFORM_ACCOUNT_ID } from "../stripe-twin.js";
|
|
29
|
-
import { cardAnswer, COUNTRIES, money } from "./checkout.js";
|
|
30
|
-
import { formOf } from "./consent-skin.js";
|
|
3
|
+
import { publicBusinessName } from "../engine/display.js";
|
|
4
|
+
import { declineFor, nowUnix, paymentMethodSubObject, PLATFORM_ACCOUNT_ID } from "../engine/stripe.js";
|
|
5
|
+
import { cardAnswer, COUNTRIES, formOf, money, notFound } from "./shared.js";
|
|
6
|
+
import { resourceView, afterSuccessOf, created, payerOf, payOpenInvoice } from "../semantics/shared.js";
|
|
31
7
|
const SUB = 'subscription';
|
|
32
|
-
const PORTAL_OPERATION =
|
|
8
|
+
const PORTAL_OPERATION = { id: 'PostBillingPortalSessions' };
|
|
33
9
|
// Stripe's skin: its type, its purple accent, the merchant's side tinted.
|
|
34
10
|
const SKIN = `
|
|
35
11
|
body { color: #1a1a1a; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Ubuntu, sans-serif; }
|
|
@@ -70,7 +46,7 @@ function planOf(ctx, sub) {
|
|
|
70
46
|
function page(ctx, id, session, problem) {
|
|
71
47
|
const features = featuresOf(ctx, session);
|
|
72
48
|
const customer = ctx.get('customer', String(session.customer)) ?? {};
|
|
73
|
-
const subs = ctx.rows(SUB).filter((s) => s.customer === session.customer && s.status !== 'canceled' && s.status !== 'incomplete_expired');
|
|
49
|
+
const subs = ctx.rows(SUB).map((s) => ctx.expand(SUB, s)).filter((s) => s.customer === session.customer && s.status !== 'canceled' && s.status !== 'incomplete_expired');
|
|
74
50
|
const plans = subs.map((s) => {
|
|
75
51
|
const plan = planOf(ctx, s);
|
|
76
52
|
const ending = s.cancel_at_period_end === true;
|
|
@@ -100,7 +76,7 @@ function page(ctx, id, session, problem) {
|
|
|
100
76
|
}))
|
|
101
77
|
: [];
|
|
102
78
|
const back = typeof session.return_url === 'string' ? session.return_url : undefined;
|
|
103
|
-
// the portal shows the public business name (
|
|
79
|
+
// the portal shows the public business name (../engine/display.ts, publicBusinessName) of the session's on_behalf_of
|
|
104
80
|
// account ("Use the Accounts API to modify the on_behalf_of account's branding settings, which the portal displays",
|
|
105
81
|
// docs.stripe.com/api/customer_portal/sessions/object), else of the account it was made on (Stripe-Account), else the
|
|
106
82
|
// platform's
|
|
@@ -129,7 +105,7 @@ function page(ctx, id, session, problem) {
|
|
|
129
105
|
}
|
|
130
106
|
/** The customer's move on one of their subscriptions, then back to the portal. */
|
|
131
107
|
async function move(ctx, id, session, subscription, kind) {
|
|
132
|
-
const sub = ctx
|
|
108
|
+
const sub = resourceView(ctx, SUB, subscription);
|
|
133
109
|
if (!sub || sub.customer !== session.customer || sub.status === 'canceled')
|
|
134
110
|
return gone();
|
|
135
111
|
const back = new Response(null, { status: 303, headers: { location: `/p/session/${id}` } });
|
|
@@ -140,7 +116,7 @@ async function move(ctx, id, session, subscription, kind) {
|
|
|
140
116
|
const features = featuresOf(ctx, session);
|
|
141
117
|
if (!features.cancel.enabled)
|
|
142
118
|
return gone();
|
|
143
|
-
const now =
|
|
119
|
+
const now = nowUnix(ctx.occurredAt);
|
|
144
120
|
const details = { comment: null, feedback: null, reason: 'cancellation_requested' };
|
|
145
121
|
if (features.cancel.mode === 'at_period_end') {
|
|
146
122
|
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');
|
|
@@ -156,7 +132,7 @@ async function move(ctx, id, session, subscription, kind) {
|
|
|
156
132
|
async function updatePaymentMethod(ctx, id, session, v) {
|
|
157
133
|
if (!featuresOf(ctx, session).paymentMethod)
|
|
158
134
|
return gone();
|
|
159
|
-
const refused = cardAnswer(v,
|
|
135
|
+
const refused = cardAnswer(v, nowUnix(ctx.occurredAt), false);
|
|
160
136
|
if (refused)
|
|
161
137
|
return page(ctx, id, session, { error: refused, values: v });
|
|
162
138
|
const customer = String(session.customer);
|
|
@@ -170,7 +146,7 @@ async function updatePaymentMethod(ctx, id, session, v) {
|
|
|
170
146
|
});
|
|
171
147
|
const holder = ctx.get('customer', customer) ?? {};
|
|
172
148
|
await ctx.write('customer', customer, { invoice_settings: { ...(holder.invoice_settings ?? {}), default_payment_method: pm.id } }, 'customer.update');
|
|
173
|
-
for (const sub of ctx.rows(SUB).filter((s) => s.customer === customer && s.status !== 'canceled' && s.status !== 'incomplete_expired')) {
|
|
149
|
+
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')) {
|
|
174
150
|
await ctx.write(SUB, String(sub.id), { default_payment_method: pm.id }, 'subscription.update');
|
|
175
151
|
}
|
|
176
152
|
return new Response(null, { status: 303, headers: { location: `/p/session/${id}` } });
|
|
@@ -180,7 +156,7 @@ async function payInvoice(ctx, id, session, invoiceId) {
|
|
|
180
156
|
const inv = ctx.get('invoice', invoiceId);
|
|
181
157
|
if (!inv || inv.customer !== session.customer || inv.status !== 'open')
|
|
182
158
|
return gone();
|
|
183
|
-
const sub = typeof inv.subscription === 'string' ? ctx
|
|
159
|
+
const sub = typeof inv.subscription === 'string' ? resourceView(ctx, SUB, inv.subscription) : undefined;
|
|
184
160
|
const holder = ctx.get('customer', String(session.customer)) ?? {};
|
|
185
161
|
const pm = (sub ? payerOf(ctx, sub) : undefined) ?? holder.invoice_settings?.default_payment_method;
|
|
186
162
|
if (!pm)
|
|
@@ -191,26 +167,24 @@ async function payInvoice(ctx, id, session, invoiceId) {
|
|
|
191
167
|
return new Response(null, { status: 303, headers: { location: `/p/session/${id}` } });
|
|
192
168
|
}
|
|
193
169
|
/** billing.stripe.com's portal pages, or undefined for any other request. */
|
|
194
|
-
export function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return move(ctx, id, session, form.subscription ?? '', kind);
|
|
215
|
-
};
|
|
170
|
+
export async function screen(ctx) {
|
|
171
|
+
const request = ctx.call.request;
|
|
172
|
+
const m = /^\/p\/session\/(bps_[A-Za-z0-9_]+)(?:\/(cancel|renew|payment_method|pay))?\/?$/.exec(new URL(request.url).pathname);
|
|
173
|
+
if (!m)
|
|
174
|
+
return notFound();
|
|
175
|
+
const [, id, kind] = m;
|
|
176
|
+
if ((kind && request.method !== 'POST') || (!kind && request.method !== 'GET'))
|
|
177
|
+
return notFound();
|
|
178
|
+
// the form is read before the context, which reads the request's body itself
|
|
179
|
+
const form = kind ? await formOf(request.clone()) : {};
|
|
180
|
+
const session = ctx.get('billing_portal.session', id);
|
|
181
|
+
if (!session)
|
|
182
|
+
return gone();
|
|
183
|
+
if (!kind)
|
|
184
|
+
return page(ctx, id, session);
|
|
185
|
+
if (kind === 'payment_method')
|
|
186
|
+
return updatePaymentMethod(ctx, id, session, form);
|
|
187
|
+
if (kind === 'pay')
|
|
188
|
+
return payInvoice(ctx, id, session, form.invoice ?? '');
|
|
189
|
+
return move(ctx, id, session, form.subscription ?? '', kind);
|
|
216
190
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { newCustomer } from "../engine/customers.js";
|
|
2
|
+
import { CS } from "../engine/checkout.js";
|
|
3
|
+
import { applyCouponDiscount, nowUnix, resolveTrial } from "../engine/stripe.js";
|
|
4
|
+
import { cardAnswer, parseExpiry } from "./shared.js";
|
|
5
|
+
import { completeSession, discountOf, created } from "../semantics/shared.js";
|
|
6
|
+
function amount(value, currency) {
|
|
7
|
+
return { amount: new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(value / 100), minorUnitsAmount: value };
|
|
8
|
+
}
|
|
9
|
+
// 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"
|
|
10
|
+
// 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.
|
|
11
|
+
function view(ctx, session) {
|
|
12
|
+
const currency = String(session.currency ?? 'usd');
|
|
13
|
+
const lines = (session.line_items?.data ?? []);
|
|
14
|
+
const stored = ctx.row(CS, String(session.id)) ?? session;
|
|
15
|
+
const trial = session.mode === 'subscription' ? resolveTrial(stored._subscription_data ?? {}, nowUnix(ctx.occurredAt)) : undefined;
|
|
16
|
+
const customer = typeof session.customer === 'string' ? ctx.get('customer', session.customer) : undefined;
|
|
17
|
+
const discount = stored._discount;
|
|
18
|
+
const promotion = typeof discount?.promotion_code === 'string' ? ctx.get('promotion_code', discount.promotion_code) : undefined;
|
|
19
|
+
const zero = amount(0, currency);
|
|
20
|
+
return {
|
|
21
|
+
id: session.id, billingAddress: null, businessName: null, canConfirm: session.status === 'open', currency,
|
|
22
|
+
currencyOptions: null, discountAmounts: promotion ? [{ ...amount(Number(session.total_details?.amount_discount ?? 0), currency), promotionCode: promotion.code }] : [],
|
|
23
|
+
email: customer?.email ?? session.customer_email ?? null, lastPaymentError: null,
|
|
24
|
+
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) })),
|
|
25
|
+
livemode: false, minorUnitsAmountDivisor: 100, phoneNumber: null,
|
|
26
|
+
recurring: session.mode === 'subscription' ? { trial: trial ? { trialEnd: trial.end, trialPeriodDays: Math.ceil((trial.end - trial.start) / 86400) } : null } : null,
|
|
27
|
+
savedPaymentMethods: [], shipping: null, shippingAddress: null, shippingOptions: [],
|
|
28
|
+
status: session.status === 'complete' ? { type: 'complete', paymentStatus: session.payment_status } : { type: session.status },
|
|
29
|
+
tax: { status: 'ready' }, taxAmounts: [], taxIdInfo: null,
|
|
30
|
+
total: { appliedBalance: zero, balanceAppliedToNextInvoice: false, discount: amount(Number(session.total_details?.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) },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const failed = (message, code = null) => Response.json({ type: 'error', error: { message, code } });
|
|
34
|
+
export async function screen(ctx) {
|
|
35
|
+
const path = new URL(ctx.call.request.url).pathname;
|
|
36
|
+
const id = path.split('/')[3];
|
|
37
|
+
const action = path.split('/')[4];
|
|
38
|
+
const params = ctx.params;
|
|
39
|
+
const session = ctx.get(CS, id);
|
|
40
|
+
// The private client secret is the browser's authority for this session; it never accepts a secret API key.
|
|
41
|
+
if (!session || session.ui_mode !== 'custom' || params.client_secret !== session.client_secret)
|
|
42
|
+
return failed('Invalid Checkout Session client secret.');
|
|
43
|
+
if (action === 'init')
|
|
44
|
+
return Response.json({ type: 'success', session: view(ctx, session) });
|
|
45
|
+
if (session.status !== 'open')
|
|
46
|
+
return failed('This Checkout Session is no longer active.');
|
|
47
|
+
if (action === 'apply_promotion_code' || action === 'remove_promotion_code') {
|
|
48
|
+
if (action === 'apply_promotion_code' && session.allow_promotion_codes !== true)
|
|
49
|
+
return failed('Promotion codes are not enabled for this Checkout Session.');
|
|
50
|
+
const promotion = action === 'apply_promotion_code' ? ctx.rows('promotion_code').find(p => p.code === params.promotion_code && p.active === true) : undefined;
|
|
51
|
+
if (action === 'apply_promotion_code' && !promotion)
|
|
52
|
+
return failed('This promotion code is invalid.', 'invalidPromotionCode');
|
|
53
|
+
const discount = promotion ? discountOf(ctx, { discounts: [{ promotion_code: promotion.id }] }, nowUnix(ctx.occurredAt)) : undefined;
|
|
54
|
+
if (discount instanceof Response)
|
|
55
|
+
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, 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')
|
|
62
|
+
return failed('Unrecognized Checkout action.');
|
|
63
|
+
const values = (params.payment_details ?? {});
|
|
64
|
+
const stored = ctx.row(CS, id) ?? session;
|
|
65
|
+
const trial = session.mode === 'subscription' ? resolveTrial(stored._subscription_data ?? {}, nowUnix(ctx.occurredAt)) : undefined;
|
|
66
|
+
const refusal = cardAnswer(values, nowUnix(ctx.occurredAt), !trial);
|
|
67
|
+
if (refusal)
|
|
68
|
+
return failed(refusal, 'paymentFailed');
|
|
69
|
+
const customer = typeof session.customer === 'string' ? ctx.get('customer', session.customer) : undefined;
|
|
70
|
+
const email = String(customer?.email ?? params.email ?? session.customer_email ?? '');
|
|
71
|
+
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email))
|
|
72
|
+
return failed('Your email address is incomplete.');
|
|
73
|
+
let existing = session;
|
|
74
|
+
if (!customer && session.mode === 'subscription') {
|
|
75
|
+
const cid = ctx.mint('customer');
|
|
76
|
+
const made = await created(ctx, 'customer', { id: cid, email }, { livemode: false, ...newCustomer(cid) });
|
|
77
|
+
existing = { ...session, customer: made.id };
|
|
78
|
+
}
|
|
79
|
+
const expiry = parseExpiry(values.cardExpiry);
|
|
80
|
+
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 });
|
|
81
|
+
if (completed instanceof Response)
|
|
82
|
+
return failed('This Checkout Session is no longer active.');
|
|
83
|
+
return Response.json({ type: 'success', session: view(ctx, completed), return_url: session.return_url });
|
|
84
|
+
}
|
|
@@ -1,31 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
export declare const money: (minor: unknown, currency: unknown) => string;
|
|
4
|
-
export declare const COUNTRIES: {
|
|
5
|
-
value: string;
|
|
6
|
-
label: string;
|
|
7
|
-
}[];
|
|
8
|
-
/** The expiry as the customer typed it. Stripe's field formats itself as it is typed: the digits `1230` read `12 / 30`,
|
|
9
|
-
* and a first digit above 1 is a month on its own (`5` reads `05 / `), so a person types the digits straight through
|
|
10
|
-
* (Checkout's own field, seen on checkout.stripe.com; docs.stripe.com/payments/checkout names no format to type). The
|
|
11
|
-
* page's script (@volter/world-ui Payment, `format: 'card-expiry'`) shows it formatted; this reads what is posted either
|
|
12
|
-
* way: `MM / YY`, `MM/YYYY`, or the bare digits `MMYY`, `MYY` (`530`) and `MMYYYY`; a month past 12 is read, and refused
|
|
13
|
-
* as invalid by cardAnswer. */
|
|
14
|
-
export declare function parseExpiry(raw: string): {
|
|
15
|
-
month: number;
|
|
16
|
-
year: number;
|
|
17
|
-
} | undefined;
|
|
18
|
-
/** The card's answer: undefined when it pays, else what Checkout says under the button. */
|
|
19
|
-
export declare function cardAnswer(v: Record<string, string>, nowSeconds: number, chargesNow: boolean): string | undefined;
|
|
20
|
-
/** checkout.stripe.com's pages, or undefined for any other request. */
|
|
21
|
-
/** The name the page shows: the session's own `branding_settings.display_name` where the create set one, else the
|
|
22
|
-
* public business name (stripe-shared.ts, publicBusinessName) of the account whose brand Checkout uses: "Checkout uses
|
|
23
|
-
* the brand settings of the connected account for destination charges with on_behalf_of and for platforms performing
|
|
24
|
-
* direct charges" (a session made with Stripe-Account), and otherwise the platform's; "If you omit a field in
|
|
25
|
-
* branding_settings, Checkout applies the default value from the Dashboard"
|
|
26
|
-
* (docs.stripe.com/payments/checkout/customization/appearance). */
|
|
27
|
-
export declare function merchantOf(ctx: SemanticsContext, stored: Row): string;
|
|
28
|
-
export declare function stripeCheckoutFlow(scope: {
|
|
29
|
-
root?: string;
|
|
30
|
-
clock?: () => string;
|
|
31
|
-
}): (request: Request) => Promise<Response | undefined>;
|
|
1
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
2
|
+
export declare function screen(ctx: HandlerContext): Promise<Response>;
|
|
@@ -1,57 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
// STRIPE CHECKOUT — a hosted flow (docs/contributing/architecture.md, "Screens"): an application creates a
|
|
3
|
-
// Checkout Session and sends its customer to the session's `url` at checkout.stripe.com/c/pay/{id}; the
|
|
4
|
-
// customer pays there, the session completes (what the payment made is linked on it and
|
|
5
|
-
// checkout.session.completed is delivered), and Checkout redirects to success_url with
|
|
6
|
-
// {CHECKOUT_SESSION_ID} filled in, or the customer goes back to cancel_url
|
|
7
|
-
// (docs.stripe.com/payments/checkout/how-checkout-works). Paying is a move no API call makes: the external
|
|
8
|
-
// actor's move on the session's machine, taken here through the same handler context the API's handlers use.
|
|
9
|
-
// The page is authored from @volter/world-ui's payment piece under Stripe's skin, nothing of Stripe's page
|
|
10
|
-
// copied; its fields carry the ids tests of Checkout fill (email, cardNumber, cardExpiry, cardCvc,
|
|
11
|
-
// billingName, billingCountry, billingPostalCode) and its button the test id Stripe gives it.
|
|
12
|
-
//
|
|
13
|
-
// Where the documentation stops and the twin decides: test cards answer as docs.stripe.com/testing lists
|
|
14
|
-
// (4242… pays; 4000000000000002 declines; …9995 insufficient funds; …0069 expired; …0127 incorrect CVC;
|
|
15
|
-
// …0341 is saved but declines when charged, so it declines on a page that charges now and is saved on one that
|
|
16
|
-
// only sets the card up: setup mode, or a subscription's trial);
|
|
17
|
-
// any other number that passes the Luhn check pays, and a card that asks for authentication pays without
|
|
18
|
-
// the challenge. A subscription session with no customer makes one from the email, as Stripe does.
|
|
19
|
-
import { semanticsContext } from '@volter/world-core';
|
|
20
2
|
import { flowPage, Payment, PAYMENT_CSS } from '@volter/world-ui';
|
|
21
|
-
import
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import { applyCouponDiscount, PLATFORM_ACCOUNT_ID, resolveTrial } from "../stripe-twin.js";
|
|
28
|
-
import { formOf } from "./consent-skin.js";
|
|
3
|
+
import { isOneTime } from "../engine/checkout.js";
|
|
4
|
+
import { newCustomer } from "../engine/customers.js";
|
|
5
|
+
import { publicBusinessName } from "../engine/display.js";
|
|
6
|
+
import { applyCouponDiscount, nowUnix, PLATFORM_ACCOUNT_ID, resolveTrial } from "../engine/stripe.js";
|
|
7
|
+
import { cardAnswer, COUNTRIES, formOf, money, notFound, parseExpiry } from "./shared.js";
|
|
8
|
+
import { completeSession, created } from "../semantics/shared.js";
|
|
29
9
|
const CS = 'checkout.session';
|
|
30
|
-
const COMPLETE_OPERATION =
|
|
31
|
-
const DECLINES = {
|
|
32
|
-
'4000000000000002': 'Your card was declined.',
|
|
33
|
-
'4000000000009995': 'Your card has insufficient funds.',
|
|
34
|
-
'4000000000000069': 'Your card has expired.',
|
|
35
|
-
'4000000000000127': "Your card's security code is incorrect.",
|
|
36
|
-
};
|
|
37
|
-
const luhn = (digits) => {
|
|
38
|
-
let sum = 0;
|
|
39
|
-
for (let i = 0; i < digits.length; i++) {
|
|
40
|
-
let d = Number(digits[digits.length - 1 - i]);
|
|
41
|
-
if (i % 2 === 1) {
|
|
42
|
-
d *= 2;
|
|
43
|
-
if (d > 9)
|
|
44
|
-
d -= 9;
|
|
45
|
-
}
|
|
46
|
-
sum += d;
|
|
47
|
-
}
|
|
48
|
-
return digits.length >= 12 && sum % 10 === 0;
|
|
49
|
-
};
|
|
50
|
-
export const money = (minor, currency) => {
|
|
51
|
-
const c = typeof currency === 'string' ? currency.toUpperCase() : 'USD';
|
|
52
|
-
const zeroDecimal = ['JPY', 'KRW', 'VND', 'CLP', 'ISK', 'UGX', 'XAF', 'XOF'].includes(c);
|
|
53
|
-
return new Intl.NumberFormat('en-US', { style: 'currency', currency: c }).format((Number(minor) || 0) / (zeroDecimal ? 1 : 100));
|
|
54
|
-
};
|
|
10
|
+
const COMPLETE_OPERATION = { id: 'PostCheckoutSessionsSession' };
|
|
55
11
|
// Stripe's skin: its type, its blue submit, the summary on a tinted half.
|
|
56
12
|
const SKIN = `
|
|
57
13
|
body { color: #1a1a1a; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Ubuntu, sans-serif; }
|
|
@@ -61,7 +17,6 @@ body { color: #1a1a1a; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI
|
|
|
61
17
|
.pay-error { color: #df1b41; }
|
|
62
18
|
.pay-submit { background: #0074d4; color: #ffffff; }
|
|
63
19
|
`;
|
|
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
20
|
/** A subscription session's trial, when its subscription_data gives one: `trial_period_days`, or `trial_end`
|
|
66
21
|
* (docs.stripe.com/payments/checkout/free-trials), as resolveTrial makes the subscription's at completion. */
|
|
67
22
|
function trialOf(session, now) {
|
|
@@ -97,7 +52,7 @@ function trialSummary(session, trial) {
|
|
|
97
52
|
}
|
|
98
53
|
/** What each period after the trial charges, as the subscription's invoices will: its recurring line items (a one-time
|
|
99
54
|
* 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
|
|
100
|
-
* invoice takes (
|
|
55
|
+
* invoice takes (../engine/renewals.ts draftSubscriptionInvoice). */
|
|
101
56
|
function afterTrial(session) {
|
|
102
57
|
const items = ((session.line_items?.data) ?? []).filter((it, i) => !isOneTime(it, inlineOf(session, i)));
|
|
103
58
|
const subtotal = items.reduce((sum, it) => sum + (Number(it.amount_subtotal) || 0), 0);
|
|
@@ -144,60 +99,19 @@ function page(session, id, now, values = {}, error) {
|
|
|
144
99
|
});
|
|
145
100
|
}
|
|
146
101
|
const gone = () => flowPage({ title: 'Checkout', status: 404, css: [PAYMENT_CSS, SKIN], body: _jsx("main", { className: "pay-form", children: _jsx("h1", { children: "This Checkout Session is no longer active." }) }) });
|
|
147
|
-
/** Cards that are saved but decline when charged. */
|
|
148
|
-
const DECLINES_WHEN_CHARGED = { '4000000000000341': 'Your card was declined.' };
|
|
149
|
-
/** The expiry as the customer typed it. Stripe's field formats itself as it is typed: the digits `1230` read `12 / 30`,
|
|
150
|
-
* and a first digit above 1 is a month on its own (`5` reads `05 / `), so a person types the digits straight through
|
|
151
|
-
* (Checkout's own field, seen on checkout.stripe.com; docs.stripe.com/payments/checkout names no format to type). The
|
|
152
|
-
* page's script (@volter/world-ui Payment, `format: 'card-expiry'`) shows it formatted; this reads what is posted either
|
|
153
|
-
* way: `MM / YY`, `MM/YYYY`, or the bare digits `MMYY`, `MYY` (`530`) and `MMYYYY`; a month past 12 is read, and refused
|
|
154
|
-
* as invalid by cardAnswer. */
|
|
155
|
-
export function parseExpiry(raw) {
|
|
156
|
-
const split = /^\s*(\d{1,2})\s*\/\s*(\d{2}|\d{4})\s*$/.exec(raw);
|
|
157
|
-
const digits = raw.replace(/\s/g, '');
|
|
158
|
-
const bare = split ? undefined : /^(\d{2})(\d{2}|\d{4})$/.exec(digits) ?? /^([2-9])(\d{2})$/.exec(digits);
|
|
159
|
-
const m = split ?? bare;
|
|
160
|
-
if (!m)
|
|
161
|
-
return undefined;
|
|
162
|
-
return { month: Number(m[1]), year: m[2].length === 2 ? 2000 + Number(m[2]) : Number(m[2]) };
|
|
163
|
-
}
|
|
164
|
-
/** The card's answer: undefined when it pays, else what Checkout says under the button. */
|
|
165
|
-
export function cardAnswer(v, nowSeconds, chargesNow) {
|
|
166
|
-
const digits = (v.cardNumber ?? '').replace(/\D/g, '');
|
|
167
|
-
if (!digits)
|
|
168
|
-
return 'Your card number is incomplete.';
|
|
169
|
-
if (DECLINES[digits])
|
|
170
|
-
return DECLINES[digits];
|
|
171
|
-
if (chargesNow && DECLINES_WHEN_CHARGED[digits])
|
|
172
|
-
return DECLINES_WHEN_CHARGED[digits];
|
|
173
|
-
if (!luhn(digits))
|
|
174
|
-
return 'Your card number is invalid.';
|
|
175
|
-
const exp = parseExpiry(v.cardExpiry ?? '');
|
|
176
|
-
if (!exp)
|
|
177
|
-
return "Your card's expiration date is incomplete.";
|
|
178
|
-
const { month, year } = exp;
|
|
179
|
-
if (month < 1 || month > 12)
|
|
180
|
-
return "Your card's expiration date is invalid.";
|
|
181
|
-
const now = new Date(nowSeconds * 1000);
|
|
182
|
-
if (year < now.getUTCFullYear() || (year === now.getUTCFullYear() && month < now.getUTCMonth() + 1))
|
|
183
|
-
return "Your card's expiration year is in the past.";
|
|
184
|
-
if (!/^\d{3,4}$/.test(v.cardCvc ?? ''))
|
|
185
|
-
return "Your card's security code is incomplete.";
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
188
102
|
async function pay(ctx, id, session, v) {
|
|
189
103
|
const onFile = customerEmailOf(session);
|
|
190
104
|
const email = onFile ?? (typeof session.customer_email === 'string' ? session.customer_email : (v.email ?? '').trim());
|
|
191
105
|
if (session.mode !== 'setup' || !session.customer) {
|
|
192
106
|
if (!EMAIL.test(email))
|
|
193
|
-
return page(session, id,
|
|
107
|
+
return page(session, id, nowUnix(ctx.occurredAt), v, 'Your email address is incomplete.');
|
|
194
108
|
}
|
|
195
109
|
// a trial charges nothing now unless a one-time price is on the first invoice
|
|
196
110
|
const oneTimeDue = ((session.line_items?.data) ?? []).some((it, i) => isOneTime(it, inlineOf(session, i)));
|
|
197
|
-
const chargesNow = session.mode === 'payment' || (session.mode === 'subscription' && (!trialOf(session,
|
|
198
|
-
const refused = cardAnswer(v,
|
|
111
|
+
const chargesNow = session.mode === 'payment' || (session.mode === 'subscription' && (!trialOf(session, nowUnix(ctx.occurredAt)) || oneTimeDue));
|
|
112
|
+
const refused = cardAnswer(v, nowUnix(ctx.occurredAt), chargesNow);
|
|
199
113
|
if (refused)
|
|
200
|
-
return page(session, id,
|
|
114
|
+
return page(session, id, nowUnix(ctx.occurredAt), v, refused);
|
|
201
115
|
let existing = session;
|
|
202
116
|
// Checkout always makes a customer for a subscription; a payment makes one only when the session asks
|
|
203
117
|
if (!session.customer && (session.mode === 'subscription' || session.customer_creation === 'always')) {
|
|
@@ -219,37 +133,34 @@ async function pay(ctx, id, session, v) {
|
|
|
219
133
|
const to = String(session.success_url).replaceAll('{CHECKOUT_SESSION_ID}', id);
|
|
220
134
|
return new Response(null, { status: 303, headers: { location: to } });
|
|
221
135
|
}
|
|
222
|
-
/** checkout.stripe.com's pages, or undefined for any other request. */
|
|
223
136
|
/** The name the page shows: the session's own `branding_settings.display_name` where the create set one, else the
|
|
224
|
-
* public business name (
|
|
137
|
+
* public business name (../engine/display.ts, publicBusinessName) of the account whose brand Checkout uses: "Checkout uses
|
|
225
138
|
* the brand settings of the connected account for destination charges with on_behalf_of and for platforms performing
|
|
226
139
|
* direct charges" (a session made with Stripe-Account), and otherwise the platform's; "If you omit a field in
|
|
227
140
|
* branding_settings, Checkout applies the default value from the Dashboard"
|
|
228
141
|
* (docs.stripe.com/payments/checkout/customization/appearance). */
|
|
229
|
-
|
|
142
|
+
function merchantOf(ctx, stored) {
|
|
230
143
|
const brand = stored._branding_settings?.display_name;
|
|
231
144
|
const behalf = stored._payment_intent_data?.on_behalf_of ?? stored._subscription_data?.on_behalf_of;
|
|
232
145
|
const account = typeof behalf === 'string' ? behalf : typeof stored._brand_account === 'string' ? stored._brand_account : PLATFORM_ACCOUNT_ID;
|
|
233
146
|
return typeof brand === 'string' && brand.trim() ? brand : publicBusinessName(ctx.get('account', account));
|
|
234
147
|
}
|
|
235
|
-
export function
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return request.method === 'GET' ? page(full, id, Number(ctx.now())) : pay(ctx, id, full, values);
|
|
254
|
-
};
|
|
148
|
+
export async function screen(ctx) {
|
|
149
|
+
const request = ctx.call.request;
|
|
150
|
+
const m = /^\/c\/pay\/(cs_[A-Za-z0-9_]+)\/?$/.exec(new URL(request.url).pathname);
|
|
151
|
+
if (!m || (request.method !== 'GET' && request.method !== 'POST'))
|
|
152
|
+
return notFound();
|
|
153
|
+
const id = m[1];
|
|
154
|
+
// the form is read before the context, which reads the request's body itself
|
|
155
|
+
const values = request.method === 'POST' ? await formOf(request.clone()) : {};
|
|
156
|
+
const session = ctx.get(CS, id);
|
|
157
|
+
if (!session)
|
|
158
|
+
return gone();
|
|
159
|
+
// an ended session's page is gone; paying on one is the customer's move the machine refuses
|
|
160
|
+
if (session.status !== 'open' && (request.method === 'GET' || ctx.legal(CS, 'status', COMPLETE_OPERATION.id, String(session.status), 'complete', id, 'external')))
|
|
161
|
+
return gone();
|
|
162
|
+
const stored = ctx.row(CS, id) ?? {};
|
|
163
|
+
const coupon = stored._discount?.coupon;
|
|
164
|
+
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 };
|
|
165
|
+
return request.method === 'GET' ? page(full, id, nowUnix(ctx.occurredAt)) : pay(ctx, id, full, values);
|
|
255
166
|
}
|