@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
|
@@ -23,25 +23,23 @@
|
|
|
23
23
|
// merchant category codes as its list gives them (./industries.ts), and an industry the page does not offer leaves
|
|
24
24
|
// business_profile.mcc due; the states are the fifty and the District of Columbia; a request with no forwarded address
|
|
25
25
|
// records 127.0.0.1. An update link (account_update) shows the same page.
|
|
26
|
-
import {
|
|
26
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
27
27
|
import { CONSENT_CSS, flowPage, Payment, PAYMENT_CSS, type PaymentField } from '@volter/world-ui';
|
|
28
|
-
import
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
|
|
28
|
+
import type { Row } from '../engine/common.ts';
|
|
29
|
+
import { nowUnix } from '../engine/stripe.ts';
|
|
30
|
+
import { formOf, notFound, seeOther, STRIPE_CONSENT_SKIN } from './shared.tsx';
|
|
31
|
+
import { INDUSTRIES as MCCS } from '../engine/industries.ts';
|
|
32
|
+
import { currentlyDue, type Form, type Kind, kindOf, OFFERED } from '../engine/onboarding.ts';
|
|
33
|
+
import { created, syncExternals } from '../semantics/shared.ts';
|
|
34
34
|
const LINK = 'account_link';
|
|
35
|
-
const OPERATION = (surface.operations as Array<{ id: string; method: string; path: string; class: string }>).find((o) => o.id === 'PostAccountsAccount')!;
|
|
36
35
|
|
|
37
36
|
const gone = (): Response => flowPage({ title: 'Stripe', status: 404, css: [CONSENT_CSS, STRIPE_CONSENT_SKIN], body: <main className="consent"><h1 className="consent-heading">This link is invalid.</h1></main> });
|
|
38
37
|
|
|
39
38
|
/** A link that can still be used: not used and not past its expiry. */
|
|
40
|
-
const usable = (ctx:
|
|
39
|
+
const usable = (ctx: HandlerContext, link: Row): boolean => link.used !== true && Number(link.expires_at) > nowUnix(ctx.occurredAt);
|
|
41
40
|
|
|
42
41
|
// the field asks, with no industry chosen until the person chooses one (an unanswered field leaves the MCC due)
|
|
43
42
|
const INDUSTRIES = [{ value: '', label: 'Select your industry' }, ...MCCS.map(([value, label]) => ({ value, label }))];
|
|
44
|
-
const OFFERED = new Set(MCCS.map(([mcc]) => mcc));
|
|
45
43
|
// the fifty states and the District of Columbia, by their USPS codes (the address's `state`, ISO 3166-2 without its
|
|
46
44
|
// country, docs.stripe.com/api/accounts/object)
|
|
47
45
|
const STATES = ([
|
|
@@ -55,49 +53,6 @@ const STATES = ([
|
|
|
55
53
|
['WA', 'Washington'], ['WV', 'West Virginia'], ['WI', 'Wisconsin'], ['WY', 'Wyoming'],
|
|
56
54
|
] as const).map(([value, label]) => ({ value, label }));
|
|
57
55
|
|
|
58
|
-
type Form = Record<string, string>;
|
|
59
|
-
type Kind = 'company' | 'individual';
|
|
60
|
-
const has = (v: Form, k: string): boolean => (v[k] ?? '').trim() !== '';
|
|
61
|
-
const dobPart = (v: Form, i: number): boolean => /^\d{4}-\d{2}-\d{2}$/.test(v.dob ?? '') && Number((v.dob ?? '').split('-')[i]) > 0;
|
|
62
|
-
|
|
63
|
-
/** Each requirement Stripe names, the form's answer that meets it, and which of the two sets it belongs to. */
|
|
64
|
-
const REQUIREMENTS: Array<[string, (v: Form) => boolean, Kind[]]> = [
|
|
65
|
-
['business_profile.mcc', (v) => OFFERED.has((v.mcc ?? '').trim()), ['company', 'individual']],
|
|
66
|
-
['business_profile.url', (v) => has(v, 'url'), ['company', 'individual']],
|
|
67
|
-
['settings.payments.statement_descriptor', (v) => has(v, 'statement_descriptor'), ['company', 'individual']],
|
|
68
|
-
['company.name', (v) => has(v, 'company_name'), ['company']],
|
|
69
|
-
['company.tax_id', (v) => has(v, 'company_tax_id'), ['company']],
|
|
70
|
-
['company.phone', (v) => has(v, 'company_phone'), ['company']],
|
|
71
|
-
['company.address.line1', (v) => has(v, 'company_line1'), ['company']],
|
|
72
|
-
['company.address.city', (v) => has(v, 'company_city'), ['company']],
|
|
73
|
-
['company.address.state', (v) => has(v, 'company_state'), ['company']],
|
|
74
|
-
['company.address.postal_code', (v) => has(v, 'company_postal_code'), ['company']],
|
|
75
|
-
['company.owners_provided', (v) => v.owners_provided === 'provided', ['company']],
|
|
76
|
-
['owners.first_name', (v) => has(v, 'owner_first_name'), ['company']],
|
|
77
|
-
['owners.last_name', (v) => has(v, 'owner_last_name'), ['company']],
|
|
78
|
-
['owners.email', (v) => has(v, 'owner_email'), ['company']],
|
|
79
|
-
...(['first_name', 'last_name', 'email', 'phone'] as const).flatMap((f): Array<[string, (v: Form) => boolean, Kind[]]> => [[`representative.${f}`, (v) => has(v, f), ['company']], [`individual.${f}`, (v) => has(v, f), ['individual']]]),
|
|
80
|
-
...(['day', 'month', 'year'] as const).flatMap((f, i): Array<[string, (v: Form) => boolean, Kind[]]> => [[`representative.dob.${f}`, (v) => dobPart(v, 2 - i), ['company']], [`individual.dob.${f}`, (v) => dobPart(v, 2 - i), ['individual']]]),
|
|
81
|
-
...(['line1', 'city', 'state', 'postal_code'] as const).flatMap((f): Array<[string, (v: Form) => boolean, Kind[]]> => [[`representative.address.${f}`, (v) => has(v, f), ['company']], [`individual.address.${f}`, (v) => has(v, f), ['individual']]]),
|
|
82
|
-
['representative.relationship.title', (v) => has(v, 'title'), ['company']],
|
|
83
|
-
['representative.ssn_last_4', (v) => /^\d{4}$/.test(v.ssn_last_4 ?? ''), ['company']],
|
|
84
|
-
['individual.ssn_last_4', (v) => /^\d{4}$/.test(v.ssn_last_4 ?? ''), ['individual']],
|
|
85
|
-
['tos_acceptance.date', (v) => v.tos === 'accepted', ['company', 'individual']],
|
|
86
|
-
['tos_acceptance.ip', (v) => v.tos === 'accepted', ['company', 'individual']],
|
|
87
|
-
['external_account', (v) => has(v, 'routing_number') && has(v, 'account_number'), ['company', 'individual']],
|
|
88
|
-
];
|
|
89
|
-
|
|
90
|
-
/** The set an account onboards against: the two US sets the twin models, or none. */
|
|
91
|
-
export function kindOf(account: Row): Kind | undefined {
|
|
92
|
-
const type = account.business_type ?? 'individual';
|
|
93
|
-
return (account.country ?? 'US') === 'US' && (type === 'company' || type === 'individual') ? type : undefined;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/** What an account of this kind still owes, given the form's answers (none: all of it). */
|
|
97
|
-
export function currentlyDue(kind: Kind, v: Form = {}): string[] {
|
|
98
|
-
return REQUIREMENTS.filter(([, met, kinds]) => kinds.includes(kind) && !met(v)).map(([req]) => req);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
56
|
const field = (id: string, label: string, v: Form, extra: Partial<PaymentField> = {}): PaymentField => ({ id, label, value: v[id] ?? '', ...extra });
|
|
102
57
|
const addressFields = (prefix: string, v: Form): PaymentField[] => [
|
|
103
58
|
field(`${prefix}line1`, 'Address', v, { autoComplete: 'address-line1' }), field(`${prefix}city`, 'City', v),
|
|
@@ -107,7 +62,7 @@ const addressFields = (prefix: string, v: Form): PaymentField[] => [
|
|
|
107
62
|
/** The industry the field shows: what the form sent, else what the account already holds, when the page offers it. */
|
|
108
63
|
const industryOf = (mcc: unknown): string => (typeof mcc === 'string' && OFFERED.has(mcc.trim()) ? mcc.trim() : '');
|
|
109
64
|
|
|
110
|
-
function page(ctx:
|
|
65
|
+
function page(ctx: HandlerContext, sessionUrl: string, link: Row, kind: Kind, values: Form = {}, error?: string): Response {
|
|
111
66
|
const v = values;
|
|
112
67
|
const business: PaymentField[] = [
|
|
113
68
|
field('mcc', 'Industry', v, { options: INDUSTRIES, value: industryOf(v.mcc ?? (ctx.get('account', String(link.account))?.business_profile as Row | undefined)?.mcc) }),
|
|
@@ -157,19 +112,19 @@ function unmodelled(account: Row): Response {
|
|
|
157
112
|
}
|
|
158
113
|
|
|
159
114
|
/** A submit that leaves something out is refused on the page with what is missing, the owner's answers kept. */
|
|
160
|
-
function incomplete(ctx:
|
|
115
|
+
function incomplete(ctx: HandlerContext, sessionUrl: string, link: Row, kind: Kind, v: Form, missing: string[]): Response {
|
|
161
116
|
return page(ctx, sessionUrl, link, kind, v, `Still required: ${missing.join(', ')}.`);
|
|
162
117
|
}
|
|
163
118
|
|
|
164
119
|
/** The owner submits everything: it is stored on the account, nothing is due, and charges, payouts and the requested
|
|
165
120
|
* capabilities are enabled. */
|
|
166
|
-
async function submit(ctx:
|
|
121
|
+
async function submit(ctx: HandlerContext, link: Row, kind: Kind, v: Form, ip: string): Promise<void> {
|
|
167
122
|
const id = String(link.account);
|
|
168
123
|
const account = ctx.get('account', id);
|
|
169
124
|
if (!account) return;
|
|
170
125
|
const caps: Row = { ...((account.capabilities as Row | undefined) ?? {}) };
|
|
171
126
|
for (const [cap, status] of Object.entries(caps)) {
|
|
172
|
-
if (status !== 'active'
|
|
127
|
+
if (status !== 'active') caps[cap] = 'active';
|
|
173
128
|
}
|
|
174
129
|
const due = currentlyDue(kind, v);
|
|
175
130
|
const requirements = { ...((account.requirements as Row | undefined) ?? {}), currently_due: due, eventually_due: due, past_due: [], disabled_reason: null, current_deadline: null };
|
|
@@ -178,6 +133,7 @@ async function submit(ctx: SemanticsContext, link: Row, kind: Kind, v: Form, ip:
|
|
|
178
133
|
const person = { first_name: v.first_name, last_name: v.last_name, email: v.email, phone: v.phone, dob: { day, month, year }, address: addressOf(''), ssn_last_4_provided: true, id_number_provided: false };
|
|
179
134
|
const company = kind === 'company';
|
|
180
135
|
const settings = (account.settings as Row | undefined) ?? {};
|
|
136
|
+
const individual = company ? null : await created(ctx, 'person', { ...person, account: id }, { relationship: { representative: true }, metadata: {} });
|
|
181
137
|
await ctx.write('account', id, {
|
|
182
138
|
details_submitted: true, charges_enabled: due.length === 0, payouts_enabled: due.length === 0, capabilities: caps, requirements,
|
|
183
139
|
business_type: kind,
|
|
@@ -185,8 +141,8 @@ async function submit(ctx: SemanticsContext, link: Row, kind: Kind, v: Form, ip:
|
|
|
185
141
|
settings: { ...settings, payments: { ...((settings.payments as Row | undefined) ?? {}), statement_descriptor: v.statement_descriptor } },
|
|
186
142
|
...(company
|
|
187
143
|
? { company: { name: v.company_name, phone: v.company_phone, address: addressOf('company_'), tax_id_provided: true, owners_provided: true, directors_provided: false, executives_provided: false } }
|
|
188
|
-
: { individual:
|
|
189
|
-
tos_acceptance: { date:
|
|
144
|
+
: { individual: individual!.id }),
|
|
145
|
+
tos_acceptance: { date: nowUnix(ctx.occurredAt), ip, user_agent: null },
|
|
190
146
|
}, 'account.updated');
|
|
191
147
|
if (company) {
|
|
192
148
|
await created(ctx, 'person', { account: id }, { ...person, relationship: { representative: true, executive: true, director: false, owner: false, percent_ownership: null, title: v.title }, metadata: {} });
|
|
@@ -205,7 +161,7 @@ const SESSIONS = 'https://connect.stripe.com/setup/s/';
|
|
|
205
161
|
|
|
206
162
|
/** Visiting an Account Link consumes it and sends the owner to their onboarding session; a used or expired one sends
|
|
207
163
|
* them to refresh_url for a fresh link. */
|
|
208
|
-
async function visit(ctx:
|
|
164
|
+
async function visit(ctx: HandlerContext, id: string): Promise<Response> {
|
|
209
165
|
const link = ctx.get(LINK, id);
|
|
210
166
|
if (!link) return gone();
|
|
211
167
|
if (!usable(ctx, link)) return seeOther(String(link.refresh_url));
|
|
@@ -215,29 +171,27 @@ async function visit(ctx: SemanticsContext, id: string): Promise<Response> {
|
|
|
215
171
|
}
|
|
216
172
|
|
|
217
173
|
/** connect.stripe.com's onboarding pages, or undefined for any other request. */
|
|
218
|
-
export function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return seeOther(String(link.return_url));
|
|
242
|
-
};
|
|
174
|
+
export async function screen(ctx: HandlerContext): Promise<Response> {
|
|
175
|
+
const request = ctx.call.request;
|
|
176
|
+
const path = new URL(request.url).pathname;
|
|
177
|
+
const entry = /^\/setup\/(acctlink_[A-Za-z0-9_]+)\/?$/.exec(path);
|
|
178
|
+
const at = /^\/setup\/s\/(onbs_[A-Za-z0-9_]+)\/?$/.exec(path);
|
|
179
|
+
if (entry && request.method === 'GET') return visit(ctx, entry[1]!);
|
|
180
|
+
if (!at || (request.method !== 'GET' && request.method !== 'POST')) return notFound();
|
|
181
|
+
const sid = at[1]!;
|
|
182
|
+
const form = request.method === 'POST' ? await formOf(request.clone()) : {};
|
|
183
|
+
// the session the visited link opened, until it is submitted
|
|
184
|
+
const stored = ctx.rowsRaw(LINK).find((l) => l._session === sid && l._submitted !== true);
|
|
185
|
+
const link = stored ? ctx.get(LINK, String(stored.id)) : undefined;
|
|
186
|
+
if (!stored || !link) return gone();
|
|
187
|
+
const sessionUrl = `${SESSIONS}${sid}`;
|
|
188
|
+
const account = ctx.get('account', String(link.account)) ?? {};
|
|
189
|
+
const kind = kindOf(account);
|
|
190
|
+
if (!kind) return unmodelled(account);
|
|
191
|
+
if (request.method === 'GET') return page(ctx, sessionUrl, link, kind);
|
|
192
|
+
const missing = currentlyDue(kind, form);
|
|
193
|
+
if (missing.length) return incomplete(ctx, sessionUrl, link, kind, form, missing);
|
|
194
|
+
await ctx.write(LINK, String(stored.id), { _submitted: true }, 'account_link.submitted');
|
|
195
|
+
await submit(ctx, link, kind, form, request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || '127.0.0.1');
|
|
196
|
+
return seeOther(String(link.return_url));
|
|
243
197
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// THE DASHBOARD — dashboard.stripe.com's pages an operator sets what Stripe's API cannot (docs/contributing/architecture.md,
|
|
2
|
+
// "Screens": a workspace): Public details (/settings/public) and the Connect OAuth settings
|
|
3
|
+
// (/settings/connect/onboarding-options/oauth), Radar lists and test Issuing funding.
|
|
4
|
+
// The operator controls prepare the cited real application calls; other Dashboard pages answer Stripe's 404.
|
|
5
|
+
//
|
|
6
|
+
// THE DASHBOARD'S PUBLIC DETAILS — a workspace page (docs/contributing/architecture.md, "Screens") of the Dashboard
|
|
7
|
+
// (manifest screen `dashboard`), at dashboard.stripe.com/settings/public: Settings → Business → Public details, where
|
|
8
|
+
// the operator sets what their customers see ("Public Details → Business Settings",
|
|
9
|
+
// support.stripe.com/questions/update-account-name-shown-in-the-dashboard). Its business name is the account's
|
|
10
|
+
// `business_profile.name`, "The customer-facing business name" (docs.stripe.com/api/accounts/object), and it is the
|
|
11
|
+
// name Checkout shows: "You can change a Checkout page's name by modifying the Business name field"
|
|
12
|
+
// (docs.stripe.com/payments/checkout/customization/appearance). Stripe's API has no call that changes the platform's
|
|
13
|
+
// own account (POST /v1/accounts/{account} updates a connected one), so this page is the only way to the act, and
|
|
14
|
+
// saving it writes the platform account's stored record (../engine/connect.ts), which GET /v1/account,
|
|
15
|
+
// Checkout and the customer portal then read. The Dashboard's account name (`settings.dashboard.display_name`,
|
|
16
|
+
// Settings → Account) is a separate, internal setting, "for internal use only" (the same support article), and this
|
|
17
|
+
// page does not touch it. Authored from plain markup under Stripe's type; nothing of Stripe's page is copied.
|
|
18
|
+
//
|
|
19
|
+
// Where the documentation stops and the twin decides: the page holds the business name only (the real page's
|
|
20
|
+
// support details, website and statement descriptor are not modelled here); the name is required and at most 5000
|
|
21
|
+
// characters (the spec's bound on business_profile.name), which the browser holds the field to and the page checks
|
|
22
|
+
// again for a client that is not a browser; no sign-in guards the page. The form posts to the page's own address
|
|
23
|
+
// and the save redirects to its own path. Saving is an update of an account that exists (the first save included, though
|
|
24
|
+
// the twin stores the platform's record only then), so every save is sent as account.updated.
|
|
25
|
+
//
|
|
26
|
+
// THE DASHBOARD'S CONNECT OAUTH SETTINGS — a workspace page (docs/contributing/architecture.md, "Screens") of the
|
|
27
|
+
// Dashboard (manifest screen `dashboard`), at dashboard.stripe.com/settings/connect/onboarding-options/oauth, where a
|
|
28
|
+
// platform starts an OAuth integration: "Enable onboarding accounts with OAuth", "Copy your client_id, a unique
|
|
29
|
+
// identifier for your platform that's generated by Stripe", and "Set your redirect_uri ... You must specify all redirect
|
|
30
|
+
// URLs in your platform settings. If you don't include the redirect_uri parameter in your request, Stripe defaults to
|
|
31
|
+
// using the first address you've configured" (docs.stripe.com/connect/oauth-standard-accounts). The test client_id
|
|
32
|
+
// "allows you to: Set your redirect_uri to a non-HTTPS URL; Set your redirect_uri to localhost"
|
|
33
|
+
// (docs.stripe.com/connect/testing#using-oauth). Stripe's API has no call that reads or changes these settings (GET
|
|
34
|
+
// /v1/account does not carry the client_id), so this page is the only way to them, and connect.stripe.com/oauth/authorize
|
|
35
|
+
// (./connect-oauth.tsx) reads what it saves. Authored from plain markup under Stripe's type; nothing of Stripe's page is
|
|
36
|
+
// copied.
|
|
37
|
+
//
|
|
38
|
+
// Where the documentation stops and the twin decides: the World has one platform, and its test client_id is
|
|
39
|
+
// `ca_twin_self` (PLATFORM_CLIENT_ID; the platform account is acct_twin_self), the same in every World, shown on the
|
|
40
|
+
// page in an element a runner reads (data-testid="connect-client-id"). OAuth starts disabled. The page is served at
|
|
41
|
+
// the docs' address, at /test/… (the docs' test-mode link) and at /settings/connect (the address Cal.com's setup
|
|
42
|
+
// guide gives). The redirect URIs are one form field, one per line (or comma-separated, as the reference describes
|
|
43
|
+
// them), saved whole: each an absolute http or https URL without a fragment; http and localhost are allowed, as the
|
|
44
|
+
// test client_id allows them. No sign-in guards the page. The form posts to the
|
|
45
|
+
// page's own address and the save redirects to it.
|
|
46
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
47
|
+
import { flowPage } from '@volter/world-ui';
|
|
48
|
+
import { VL } from '../engine/radar.ts';
|
|
49
|
+
import { nowUnix } from '../engine/stripe.ts';
|
|
50
|
+
import type { Row } from '../engine/common.ts';
|
|
51
|
+
import { platformAccountDefault } from '../engine/connect.ts';
|
|
52
|
+
import { publicBusinessName } from '../engine/display.ts';
|
|
53
|
+
import { PLATFORM_ACCOUNT_ID } from '../engine/stripe.ts';
|
|
54
|
+
import { formOf, notFound } from './shared.tsx';
|
|
55
|
+
import { created } from '../semantics/shared.ts';
|
|
56
|
+
const redirect = (path: string): Response => new Response(null, { status: 303, headers: { location: path } });
|
|
57
|
+
const invalid = (message: string): Response => flowPage({ css: [], title: 'Stripe', status: 400, body: <main><p role="alert">{message}</p></main> });
|
|
58
|
+
|
|
59
|
+
const PUBLIC_DETAILS_PATH = '/settings/public';
|
|
60
|
+
const MAX_NAME = 5000;
|
|
61
|
+
|
|
62
|
+
const DETAILS_CSS = `
|
|
63
|
+
* { box-sizing: border-box; }
|
|
64
|
+
body { margin: 0; background: #f6f8fa; color: #1a1f36; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Ubuntu, sans-serif; font-size: 14px; }
|
|
65
|
+
.pd { max-width: 640px; margin: 48px auto; padding: 0 16px; }
|
|
66
|
+
.pd h1 { font-size: 28px; margin: 16px 0 4px; }
|
|
67
|
+
.pd-lead { color: #697386; margin: 0 0 24px; }
|
|
68
|
+
.pd-card { background: #ffffff; border: 1px solid #e3e8ee; border-radius: 8px; padding: 24px; }
|
|
69
|
+
.pd-card label { display: block; font-weight: 600; margin-bottom: 6px; }
|
|
70
|
+
.pd-card small { display: block; color: #697386; margin-bottom: 8px; }
|
|
71
|
+
.pd-card input { width: 100%; padding: 8px 10px; border: 1px solid #c1c9d2; border-radius: 6px; font: inherit; }
|
|
72
|
+
.pd-actions { margin-top: 20px; display: flex; justify-content: flex-end; }
|
|
73
|
+
.pd-actions button { background: #635bff; color: #ffffff; border: 0; border-radius: 6px; padding: 8px 16px; font: inherit; font-weight: 600; cursor: pointer; }
|
|
74
|
+
.pd-notice { padding: 10px 12px; border-radius: 6px; margin-bottom: 16px; }
|
|
75
|
+
.pd-error { background: #fff0f3; color: #df1b41; }
|
|
76
|
+
.pd-saved { background: #e7f8ef; color: #0e6245; }
|
|
77
|
+
`;
|
|
78
|
+
|
|
79
|
+
function detailsPage(account: Row | undefined, opts: { value?: string; error?: string; saved?: boolean } = {}): Response {
|
|
80
|
+
const named = (account?.business_profile as Row | undefined)?.name;
|
|
81
|
+
const value = opts.value ?? (typeof named === 'string' ? named : '');
|
|
82
|
+
return flowPage({
|
|
83
|
+
title: 'Public details – Stripe',
|
|
84
|
+
css: [DETAILS_CSS],
|
|
85
|
+
...(opts.error ? { status: 400 } : {}),
|
|
86
|
+
body: (
|
|
87
|
+
<main className="pd">
|
|
88
|
+
<h1>Public details</h1>
|
|
89
|
+
<p className="pd-lead">What your customers see on Checkout, the customer portal, receipts and invoices. Currently shown: <strong data-testid="public-business-name">{publicBusinessName(account)}</strong></p>
|
|
90
|
+
{opts.error ? <p className="pd-notice pd-error" role="alert">{opts.error}</p> : null}
|
|
91
|
+
{opts.saved ? <p className="pd-notice pd-saved" role="status">Your public details were saved.</p> : null}
|
|
92
|
+
<form className="pd-card" method="post">
|
|
93
|
+
<label htmlFor="business_name">Business name</label>
|
|
94
|
+
<small>The name customers see when they pay you.</small>
|
|
95
|
+
<input id="business_name" name="business_name" autoComplete="organization" defaultValue={value} required maxLength={MAX_NAME} />
|
|
96
|
+
<div className="pd-actions"><button type="submit">Save</button></div>
|
|
97
|
+
</form>
|
|
98
|
+
</main>
|
|
99
|
+
),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Save the business name on the platform's own account: the stored record, made from Stripe's default the first
|
|
104
|
+
* time anything is saved, and otherwise changed in place (its other public details kept). */
|
|
105
|
+
async function save(ctx: HandlerContext, name: string): Promise<void> {
|
|
106
|
+
const held = ctx.get('account', PLATFORM_ACCOUNT_ID);
|
|
107
|
+
const profile = { business_profile: { ...(((held ?? platformAccountDefault()).business_profile as Row | undefined) ?? {}), name } };
|
|
108
|
+
await (held ? ctx.write('account', PLATFORM_ACCOUNT_ID, profile, 'account.update') : created(ctx, 'account', { ...platformAccountDefault(), ...profile }, {}, { operation: 'account.update' }));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** What the page says of a name no browser sends (the field is required and bounded), for a client that sends it. */
|
|
112
|
+
function nameRefused(name: string): string | undefined {
|
|
113
|
+
return !name ? 'Enter your business name.' : name.length > MAX_NAME ? `Your business name must be at most ${MAX_NAME} characters.` : undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function publicDetails(ctx: HandlerContext): Promise<Response> {
|
|
117
|
+
const request = ctx.call.request;
|
|
118
|
+
const url = new URL(request.url);
|
|
119
|
+
if (request.method !== 'GET' && request.method !== 'POST') return notFound();
|
|
120
|
+
const form = request.method === 'POST' ? await formOf(request.clone()) : {};
|
|
121
|
+
if (request.method === 'GET') return detailsPage(ctx.get('account', PLATFORM_ACCOUNT_ID), { saved: url.searchParams.has('saved') });
|
|
122
|
+
const name = (form.business_name ?? '').trim();
|
|
123
|
+
const refused = nameRefused(name);
|
|
124
|
+
if (refused) return detailsPage(ctx.get('account', PLATFORM_ACCOUNT_ID), { value: form.business_name ?? '', error: refused });
|
|
125
|
+
await save(ctx, name);
|
|
126
|
+
// a query-only reference: the page's own path, wherever it is mounted and with or without its trailing slash
|
|
127
|
+
return new Response(null, { status: 303, headers: { location: '?saved=1' } });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Dashboard settings, Radar list creation and test Issuing funding; other pages answer Stripe's 404. */
|
|
131
|
+
export async function screen(ctx: HandlerContext): Promise<Response> {
|
|
132
|
+
const path = new URL(ctx.call.request.url).pathname.replace(/\/+$/, '');
|
|
133
|
+
// source: https://docs.stripe.com/radar/lists "Use the Dashboard or the API to create lists."
|
|
134
|
+
// source: https://docs.stripe.com/radar/lists "Enter a name for the list"
|
|
135
|
+
// source: https://docs.stripe.com/radar/lists "Select the type of list to create."
|
|
136
|
+
// source: https://docs.stripe.com/radar/lists "Click Add to save your new list."
|
|
137
|
+
// Where the documentation stops: it names Dashboard controls, not their browser request routes or form wire.
|
|
138
|
+
// This authored page uses /radar/lists for GET and POST, /radar/lists/{id} for its redirect/detail address,
|
|
139
|
+
// and a 303 redirect after saving. The HTML field names name, alias and item_type are the twin's choices;
|
|
140
|
+
// item_type values are the pinned API's enums, not published Dashboard form values. The alias fallback's
|
|
141
|
+
// lowercase/underscore algorithm is also authored; Stripe says it generates an alias but gives no algorithm.
|
|
142
|
+
if (/^\/radar\/lists(?:\/[^/]+)?$/.test(path)) {
|
|
143
|
+
if (ctx.call.request.method === 'POST') {
|
|
144
|
+
const form = await formOf(ctx.call.request);
|
|
145
|
+
const types = ['string', 'case_sensitive_string', 'card_fingerprint', 'card_bin', 'customer_id', 'email', 'ip_address', 'country', 'sepa_debit_fingerprint', 'us_bank_account_fingerprint'];
|
|
146
|
+
if (!form.name?.trim() || !types.includes(form.item_type ?? '')) return invalid('Enter a name and select a list type.');
|
|
147
|
+
const alias = form.alias?.trim() || form.name.toLowerCase().replace(/[^a-z0-9]+/g, '_');
|
|
148
|
+
// Where the page stops: an operator has no login identity in the World; created_by is its synthetic operator.
|
|
149
|
+
const list = await created(ctx, VL, { name: form.name.trim(), alias, item_type: form.item_type }, {
|
|
150
|
+
created_by: 'twin', livemode: false, metadata: {},
|
|
151
|
+
list_items: { object: 'list', data: [], has_more: false, url: '/v1/radar/value_list_items' },
|
|
152
|
+
});
|
|
153
|
+
await ctx.write(VL, String(list.id), { list_items: { object: 'list', data: [], has_more: false, url: `/v1/radar/value_list_items?value_list=${list.id}` } }, 'radar.value_list.updated');
|
|
154
|
+
return redirect(`/radar/lists/${list.id}`);
|
|
155
|
+
}
|
|
156
|
+
return flowPage({ css: [], title: 'Lists – Radar – Stripe', body: <main><h1>Radar Lists</h1><form method="post" action="/radar/lists"><h2>New list</h2><label>Name<input name="name" required /></label><label>Alias<input name="alias" /></label><label>List type<select name="item_type"><option value="customer_id">Customer ID</option><option value="email">Email</option><option value="card_fingerprint">Card fingerprint</option><option value="ip_address">IP address</option><option value="string">String</option></select></label><button>Add</button></form><ul>{ctx.rows(VL).map(row => <li key={String(row.id)}>{String(row.name)} ({String(row.alias)})</li>)}</ul></main> });
|
|
157
|
+
}
|
|
158
|
+
// source: https://docs.stripe.com/issuing/testing "Before you create test transactions, you must add test funds to the Issuing balance on your account."
|
|
159
|
+
// source: https://docs.stripe.com/issuing/testing "You can create test top-ups in the Dashboard, or with the Top-ups API"
|
|
160
|
+
// Where the documentation stops: it states the Dashboard funding act, not a browser request path or form wire.
|
|
161
|
+
// The authored GET/POST address and 303 redirect are /test/issuing/balance. The form name amount, its positive
|
|
162
|
+
// integer validation and its units (USD cents for this US sandbox) are the twin's choices, not published form
|
|
163
|
+
// fields. Its POST records the balance change in the vendor ledger, with source=null (permitted by the pinned
|
|
164
|
+
// schema) and immediate availability for this sandbox control; the page states neither that source nor timing.
|
|
165
|
+
// No private Topup object is seeded or answered, and the uncalled Top-ups API remains the gap.
|
|
166
|
+
if (path === '/test/issuing/balance') {
|
|
167
|
+
if (ctx.call.request.method === 'POST') {
|
|
168
|
+
const form = await formOf(ctx.call.request);
|
|
169
|
+
const amount = Number(form.amount);
|
|
170
|
+
if (!Number.isInteger(amount) || amount <= 0) return invalid('Enter a positive amount in cents.');
|
|
171
|
+
await created(ctx, 'balance_transaction', {}, { amount, currency: 'usd', fee: 0, net: amount, type: 'topup', reporting_category: 'topup', source: null, status: 'available', balance_type: 'issuing', available_on: Number(nowUnix(ctx.occurredAt)), fee_details: [] });
|
|
172
|
+
return redirect('/test/issuing/balance');
|
|
173
|
+
}
|
|
174
|
+
return flowPage({ css: [], title: 'Test Issuing balance – Stripe', body: <main><h1>Test Issuing balance</h1><form method="post"><label>Amount<input name="amount" type="number" min="1" required /></label><button>Add test funds</button></form></main> });
|
|
175
|
+
}
|
|
176
|
+
return path === PUBLIC_DETAILS_PATH ? publicDetails(ctx) : notFound();
|
|
177
|
+
}
|
|
178
|
+
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// What Stripe's screens share (docs/contributing/architecture.md, "Pack layout": screens/shared.tsx): the skin over
|
|
2
|
+
// @volter/world-ui's consent piece for the flows where a person agrees to something (connected-account onboarding,
|
|
3
|
+
// identity verification, linking a bank), a page's form, and the card a customer types on Checkout and the portal.
|
|
4
|
+
export const STRIPE_CONSENT_SKIN = `
|
|
5
|
+
body { background: #f6f9fc; color: #1a1f36; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Ubuntu, sans-serif; }
|
|
6
|
+
.consent-badge { background: #ffffff; border: 1px solid #e3e8ee; color: #1a1f36; }
|
|
7
|
+
.consent-vendor { background: #635bff; border-color: #635bff; }
|
|
8
|
+
.consent-link { border-color: #c1c9d2; }
|
|
9
|
+
.consent-box { background: #ffffff; border-color: #e3e8ee; }
|
|
10
|
+
.consent-who small, .consent-permission summary small, .consent-note { color: #697386; }
|
|
11
|
+
.consent-actions { border-color: #e3e8ee; }
|
|
12
|
+
.consent-deny { background: #ffffff; border-color: #c1c9d2; color: #1a1f36; }
|
|
13
|
+
.consent-allow { background: #635bff; border-color: #635bff; color: #ffffff; }
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
/** The form fields a page's POST carries, as a browser posts them. */
|
|
17
|
+
export async function formOf(request: Request): Promise<Record<string, string>> {
|
|
18
|
+
return Object.fromEntries(new URLSearchParams(await request.text()));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const seeOther = (location: string): Response => new Response(null, { status: 302, headers: { location } });
|
|
22
|
+
|
|
23
|
+
export const DECLINES: Record<string, string> = {
|
|
24
|
+
'4000000000000002': 'Your card was declined.',
|
|
25
|
+
'4000000000009995': 'Your card has insufficient funds.',
|
|
26
|
+
'4000000000000069': 'Your card has expired.',
|
|
27
|
+
'4000000000000127': "Your card's security code is incorrect.",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const luhn = (digits: string): boolean => {
|
|
31
|
+
let sum = 0;
|
|
32
|
+
for (let i = 0; i < digits.length; i++) {
|
|
33
|
+
let d = Number(digits[digits.length - 1 - i]);
|
|
34
|
+
if (i % 2 === 1) { d *= 2; if (d > 9) d -= 9; }
|
|
35
|
+
sum += d;
|
|
36
|
+
}
|
|
37
|
+
return digits.length >= 12 && sum % 10 === 0;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const money = (minor: unknown, currency: unknown): string => {
|
|
41
|
+
const c = typeof currency === 'string' ? currency.toUpperCase() : 'USD';
|
|
42
|
+
const zeroDecimal = ['JPY', 'KRW', 'VND', 'CLP', 'ISK', 'UGX', 'XAF', 'XOF'].includes(c);
|
|
43
|
+
return new Intl.NumberFormat('en-US', { style: 'currency', currency: c }).format((Number(minor) || 0) / (zeroDecimal ? 1 : 100));
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
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! }));
|
|
47
|
+
|
|
48
|
+
/** Cards that are saved but decline when charged. */
|
|
49
|
+
export const DECLINES_WHEN_CHARGED: Record<string, string> = { '4000000000000341': 'Your card was declined.' };
|
|
50
|
+
|
|
51
|
+
/** The expiry as the customer typed it. Stripe's field formats itself as it is typed: the digits `1230` read `12 / 30`,
|
|
52
|
+
* and a first digit above 1 is a month on its own (`5` reads `05 / `), so a person types the digits straight through
|
|
53
|
+
* (Checkout's own field, seen on checkout.stripe.com; docs.stripe.com/payments/checkout names no format to type). The
|
|
54
|
+
* page's script (@volter/world-ui Payment, `format: 'card-expiry'`) shows it formatted; this reads what is posted either
|
|
55
|
+
* way: `MM / YY`, `MM/YYYY`, or the bare digits `MMYY`, `MYY` (`530`) and `MMYYYY`; a month past 12 is read, and refused
|
|
56
|
+
* as invalid by cardAnswer. */
|
|
57
|
+
export function parseExpiry(raw: string): { month: number; year: number } | undefined {
|
|
58
|
+
const split = /^\s*(\d{1,2})\s*\/\s*(\d{2}|\d{4})\s*$/.exec(raw);
|
|
59
|
+
const digits = raw.replace(/\s/g, '');
|
|
60
|
+
const bare = split ? undefined : /^(\d{2})(\d{2}|\d{4})$/.exec(digits) ?? /^([2-9])(\d{2})$/.exec(digits);
|
|
61
|
+
const m = split ?? bare;
|
|
62
|
+
if (!m) return undefined;
|
|
63
|
+
return { month: Number(m[1]), year: m[2]!.length === 2 ? 2000 + Number(m[2]) : Number(m[2]) };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The card's answer: undefined when it pays, else what Checkout says under the button. */
|
|
67
|
+
export function cardAnswer(v: Record<string, string>, nowSeconds: number, chargesNow: boolean): string | undefined {
|
|
68
|
+
const digits = (v.cardNumber ?? '').replace(/\D/g, '');
|
|
69
|
+
if (!digits) return 'Your card number is incomplete.';
|
|
70
|
+
if (DECLINES[digits]) return DECLINES[digits];
|
|
71
|
+
if (chargesNow && DECLINES_WHEN_CHARGED[digits]) return DECLINES_WHEN_CHARGED[digits];
|
|
72
|
+
if (!luhn(digits)) return 'Your card number is invalid.';
|
|
73
|
+
const exp = parseExpiry(v.cardExpiry ?? '');
|
|
74
|
+
if (!exp) return "Your card's expiration date is incomplete.";
|
|
75
|
+
const { month, year } = exp;
|
|
76
|
+
if (month < 1 || month > 12) return "Your card's expiration date is invalid.";
|
|
77
|
+
const now = new Date(nowSeconds * 1000);
|
|
78
|
+
if (year < now.getUTCFullYear() || (year === now.getUTCFullYear() && month < now.getUTCMonth() + 1)) return "Your card's expiration year is in the past.";
|
|
79
|
+
if (!/^\d{3,4}$/.test(v.cardCvc ?? '')) return "Your card's security code is incomplete.";
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Stripe's answer for a page of one of its hosts the twin does not serve. */
|
|
84
|
+
export const notFound = (): Response => new Response('Not Found', { status: 404, headers: { 'content-type': 'text/plain; charset=utf-8' } });
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Stripe.js 7.3.1 is Dub’s pinned browser SDK (apps/web/package.json:49 and pnpm-lock.yaml).
|
|
2
|
+
// Its loader requests a release-train script, reads window.Stripe and registers its wrapper.
|
|
3
|
+
// The shipped upgrade button calls redirectToCheckout({ sessionId }); hosted-checkout.d.ts declares that
|
|
4
|
+
// navigation and its promise that settles only on failure. Other SDK flows have no demanded caller.
|
|
5
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
6
|
+
import { notFound } from './shared.tsx';
|
|
7
|
+
|
|
8
|
+
/** The release train a script path names: `/v3`, `/v3/` or `/v3/stripe.js` → 3, `/<train>/stripe.js` → the train. */
|
|
9
|
+
function stripeJsTrain(pathname: string): 3 | string | undefined {
|
|
10
|
+
return /^\/v3(\/|\/stripe\.js)?$/.test(pathname) ? 3 : /^\/([a-z]+)\/stripe\.js$/.exec(pathname)?.[1];
|
|
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 "loadActions: () => Promise<StripeCheckoutLoadActionsResult>"
|
|
14
|
+
function script(version: 3 | string): string {
|
|
15
|
+
return `(function () {
|
|
16
|
+
var version = ${JSON.stringify(version)};
|
|
17
|
+
function gap(name) {
|
|
18
|
+
return function () { throw new Error("Stripe.js (Volter twin): stripe." + name + " is not modelled by this twin yet"); };
|
|
19
|
+
}
|
|
20
|
+
function initCheckout(options) {
|
|
21
|
+
var session, root, fields = {}, listeners = {};
|
|
22
|
+
function emit(name, value) { (listeners[name] || []).forEach(function (fn) { fn(value); }); }
|
|
23
|
+
function call(action, values) {
|
|
24
|
+
var secret = options.clientSecret;
|
|
25
|
+
var id = secret.split("_secret_")[0];
|
|
26
|
+
return fetch("https://api.stripe.com/v1/payment_pages/" + encodeURIComponent(id) + "/" + action, {
|
|
27
|
+
method: "POST", headers: { "content-type": "application/json" },
|
|
28
|
+
body: JSON.stringify(Object.assign({ client_secret: secret }, values || {}))
|
|
29
|
+
}).then(function (r) { return r.json(); }).then(function (result) {
|
|
30
|
+
if (result.type === "success") { session = result.session; emit("change", session); }
|
|
31
|
+
return result;
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
var actions = {
|
|
35
|
+
getSession: function () { return session; },
|
|
36
|
+
applyPromotionCode: function (code) { return call("apply_promotion_code", { promotion_code: code }); },
|
|
37
|
+
removePromotionCode: function () { return call("remove_promotion_code"); },
|
|
38
|
+
confirm: function (params) {
|
|
39
|
+
return call("confirm", Object.assign({}, params || {}, { payment_details: fields })).then(function (result) {
|
|
40
|
+
if (result.type === "success" && (!params || params.redirect !== "if_required") && result.return_url) window.location.assign((params && params.returnUrl) || result.return_url);
|
|
41
|
+
return result;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var elementListeners = {};
|
|
46
|
+
function elementEmit(name, value) { (elementListeners[name] || []).forEach(function (fn) { fn(value); }); }
|
|
47
|
+
var element = {
|
|
48
|
+
on: function (name, fn) { (elementListeners[name] || (elementListeners[name] = [])).push(fn); return element; },
|
|
49
|
+
off: function (name, fn) { elementListeners[name] = (elementListeners[name] || []).filter(function (f) { return f !== fn; }); return element; },
|
|
50
|
+
mount: function (target) {
|
|
51
|
+
root = typeof target === "string" ? document.querySelector(target) : target;
|
|
52
|
+
[["cardNumber", "Card number", "cc-number"], ["cardExpiry", "Expiration", "cc-exp"], ["cardCvc", "CVC", "cc-csc"], ["billingName", "Cardholder name", "cc-name"]].forEach(function (field) {
|
|
53
|
+
var label = document.createElement("label"), input = document.createElement("input");
|
|
54
|
+
label.textContent = field[1]; input.name = field[0]; input.autocomplete = field[2];
|
|
55
|
+
input.addEventListener("input", function () { fields[field[0]] = input.value; elementEmit("change", { empty: !fields.cardNumber, complete: !!(fields.cardNumber && fields.cardExpiry && fields.cardCvc), value: { type: "card" } }); });
|
|
56
|
+
label.appendChild(input); root.appendChild(label);
|
|
57
|
+
});
|
|
58
|
+
Promise.resolve().then(function () { elementEmit("ready", element); });
|
|
59
|
+
},
|
|
60
|
+
unmount: function () { if (root) root.replaceChildren(); root = null; },
|
|
61
|
+
destroy: function () { element.unmount(); elementListeners = {}; },
|
|
62
|
+
update: function () {}
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
on: function (name, fn) { (listeners[name] || (listeners[name] = [])).push(fn); },
|
|
66
|
+
loadActions: function () { return call("init").then(function (r) { return r.type === "success" ? { type: "success", actions: actions } : r; }); },
|
|
67
|
+
createPaymentElement: function () { return element; }, getPaymentElement: function () { return element; },
|
|
68
|
+
changeAppearance: function () {}, loadFonts: function () {}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function Stripe(publishableKey) {
|
|
72
|
+
if (typeof publishableKey !== "string" || !publishableKey) throw new Error("Stripe.js (Volter twin): Stripe() needs a publishable key");
|
|
73
|
+
var known = {
|
|
74
|
+
initCheckout: initCheckout,
|
|
75
|
+
_registerWrapper: function () {},
|
|
76
|
+
redirectToCheckout: function (options) {
|
|
77
|
+
if (!options || typeof options.sessionId !== "string" || !options.sessionId) return gap("redirectToCheckout without a sessionId")();
|
|
78
|
+
window.location.assign("https://checkout.stripe.com/c/pay/" + encodeURIComponent(options.sessionId));
|
|
79
|
+
return new Promise(function () {});
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return new Proxy(known, {
|
|
83
|
+
get: function (target, name) {
|
|
84
|
+
if (name in target) return target[name];
|
|
85
|
+
if (typeof name === "symbol" || name === "then") return undefined;
|
|
86
|
+
return gap(String(name));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
Stripe.version = version;
|
|
91
|
+
window.Stripe = Stripe;
|
|
92
|
+
})();
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** js.stripe.com's script for a GET of its path; Stripe's 404 for anything else there. */
|
|
97
|
+
export async function screen(ctx: HandlerContext): Promise<Response> {
|
|
98
|
+
const request = ctx.call.request;
|
|
99
|
+
const train = request.method === 'GET' || request.method === 'HEAD' ? stripeJsTrain(new URL(request.url).pathname) : undefined;
|
|
100
|
+
if (train === undefined) return notFound();
|
|
101
|
+
return new Response(script(train), { headers: { 'content-type': 'application/javascript; charset=utf-8', 'access-control-allow-origin': '*' } });
|
|
102
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
2
|
+
import { PLATFORM_ACCOUNT_ID } from '../engine/stripe.ts';
|
|
3
|
+
import { actingAccount, accountMissing } from './shared.ts';
|
|
4
|
+
|
|
5
|
+
// source: spec:GetAccount "Retrieves the details of an account."
|
|
6
|
+
export async function GetAccount(ctx: HandlerContext): Promise<Response> {
|
|
7
|
+
const id = actingAccount(ctx) ?? PLATFORM_ACCOUNT_ID;
|
|
8
|
+
const account = ctx.get('account', id);
|
|
9
|
+
return account ? ctx.reply(ctx.expand('account', account)) : accountMissing(ctx, id);
|
|
10
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Stripe's account_links operations (docs/contributing/architecture.md, "What an author writes"): each handler over the
|
|
2
|
+
// handler contract's context, computing with the engine (../engine/).
|
|
3
|
+
import type { HandlerContext } from '@volter/world-core';
|
|
4
|
+
import { nowUnix } from '../engine/stripe.ts';
|
|
5
|
+
import { accountMissing, fail } from './shared.ts';
|
|
6
|
+
// ── hosted onboarding links and embedded-component sessions ──
|
|
7
|
+
export async function PostAccountLinks(ctx: HandlerContext): Promise<Response> {
|
|
8
|
+
const account = typeof ctx.params.account === 'string' ? ctx.params.account : '';
|
|
9
|
+
if (!account) return fail(ctx, 'Missing required param: account.', 400, 'parameter_missing');
|
|
10
|
+
if (!ctx.get('account', account)) return accountMissing(ctx, account, 400);
|
|
11
|
+
const linkType = typeof ctx.params.type === 'string' ? ctx.params.type : '';
|
|
12
|
+
if (linkType !== 'account_onboarding' && linkType !== 'account_update') return fail(ctx, 'Invalid account link type: must be account_onboarding or account_update.', 400, 'parameter_invalid_string_enum');
|
|
13
|
+
// the owner is sent back to return_url when done, and to refresh_url for a new link once this one is used or expired
|
|
14
|
+
for (const k of ['refresh_url', 'return_url']) if (typeof ctx.params[k] !== 'string' || !ctx.params[k]) return fail(ctx, `Missing required param: ${k}.`, 400, 'parameter_missing');
|
|
15
|
+
const now = Number(nowUnix(ctx.occurredAt));
|
|
16
|
+
const id = ctx.mint('account_link');
|
|
17
|
+
const link = { object: 'account_link', created: now, expires_at: now + 300, url: `https://connect.stripe.com/setup/${id}` };
|
|
18
|
+
await ctx.write('account_link', id, { ...link, account, type: linkType, refresh_url: ctx.params.refresh_url, return_url: ctx.params.return_url, used: false }, 'account_link.create');
|
|
19
|
+
return ctx.reply(link);
|
|
20
|
+
}
|