@volter/twin-stripe 3.0.0 → 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 +51 -21
- package/dist/generated/pack-facts.json +3 -3
- 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 +5 -1
- package/dist/spec/doc-examples.json +445 -0
- package/dist/spec/sources.json +135 -207
- package/dist/src/cli.d.ts +0 -1
- package/dist/src/cli.js +0 -2
- package/dist/src/engine/after-payment.d.ts +0 -1
- package/dist/src/engine/after-payment.js +0 -1
- package/dist/src/engine/common.d.ts +5 -5
- package/dist/src/engine/common.js +13 -11
- package/dist/src/engine/connect.d.ts +0 -21
- package/dist/src/engine/connect.js +1 -82
- package/dist/src/engine/customers.d.ts +0 -8
- package/dist/src/engine/customers.js +0 -22
- package/dist/src/engine/display.d.ts +0 -3
- package/dist/src/engine/display.js +0 -3
- package/dist/src/engine/files.d.ts +0 -7
- package/dist/src/engine/files.js +0 -10
- package/dist/src/engine/invoices.d.ts +6 -9
- package/dist/src/engine/invoices.js +56 -29
- package/dist/src/engine/issuing.d.ts +6 -8
- package/dist/src/engine/issuing.js +19 -10
- package/dist/src/engine/ledger.d.ts +0 -1
- package/dist/src/engine/ledger.js +0 -1
- package/dist/src/engine/oauth.js +1 -2
- package/dist/src/engine/refunds.js +2 -27
- package/dist/src/engine/stripe.d.ts +2 -7
- package/dist/src/engine/stripe.js +34 -83
- package/dist/src/engine/subscriptions.d.ts +0 -1
- package/dist/src/engine/subscriptions.js +0 -1
- package/dist/src/engine/version.d.ts +0 -2
- package/dist/src/engine/version.js +17 -6
- package/dist/src/fetch.js +2 -2
- package/dist/src/generated/surface.gen.json +1 -1
- package/dist/src/generated/ui.gen.json +1 -1
- package/dist/src/index.js +5 -3
- package/dist/src/manifest.js +348 -220
- package/dist/src/screens/billing-portal.js +5 -5
- package/dist/src/screens/{connect-oauth.d.ts → checkout-js.d.ts} +0 -1
- package/dist/src/screens/checkout-js.js +84 -0
- package/dist/src/screens/connect-onboarding.js +3 -3
- package/dist/src/screens/dashboard.d.ts +1 -1
- package/dist/src/screens/dashboard.js +52 -53
- package/dist/src/screens/stripe-js.js +53 -93
- package/dist/src/semantics/account.d.ts +2 -0
- package/dist/src/semantics/account.js +8 -0
- package/dist/src/semantics/accounts.js +3 -1
- package/dist/src/semantics/around.js +3 -6
- package/dist/src/semantics/billing_portal.d.ts +0 -1
- package/dist/src/semantics/billing_portal.js +1 -7
- package/dist/src/semantics/checkout.d.ts +1 -1
- package/dist/src/semantics/checkout.js +18 -15
- package/dist/src/semantics/clock.js +1 -7
- package/dist/src/semantics/customers.d.ts +1 -2
- package/dist/src/semantics/customers.js +32 -38
- package/dist/src/semantics/entitlements.d.ts +0 -2
- package/dist/src/semantics/entitlements.js +4 -50
- package/dist/src/semantics/events.d.ts +2 -3
- package/dist/src/semantics/events.js +6 -11
- package/dist/src/semantics/index.js +16 -38
- package/dist/src/semantics/invoices.d.ts +3 -8
- package/dist/src/semantics/invoices.js +134 -127
- package/dist/src/semantics/issuing.d.ts +6 -0
- package/dist/src/semantics/issuing.js +77 -0
- package/dist/src/semantics/payment_intents.d.ts +0 -1
- package/dist/src/semantics/payment_intents.js +5 -13
- package/dist/src/semantics/payment_methods.d.ts +0 -2
- package/dist/src/semantics/payment_methods.js +2 -26
- package/dist/src/semantics/payouts.d.ts +1 -0
- package/dist/src/semantics/payouts.js +8 -1
- package/dist/src/semantics/products.d.ts +1 -3
- package/dist/src/semantics/products.js +3 -14
- package/dist/src/semantics/refunds.d.ts +1 -2
- package/dist/src/semantics/refunds.js +1 -5
- package/dist/src/semantics/shared.d.ts +64 -336
- package/dist/src/semantics/shared.js +326 -1336
- package/dist/src/semantics/states.d.ts +20 -89
- package/dist/src/semantics/states.js +116 -334
- package/dist/src/semantics/subscriptions.d.ts +1 -2
- package/dist/src/semantics/subscriptions.js +23 -145
- package/dist/src/semantics/test_helpers.d.ts +2 -5
- package/dist/src/semantics/test_helpers.js +121 -43
- package/dist/src/semantics/webhook_endpoints.d.ts +2 -0
- package/dist/src/semantics/webhook_endpoints.js +26 -0
- package/generated/pack-facts.json +3 -3
- 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 +4 -3
- package/spec/SOURCE.md +5 -1
- package/spec/doc-examples.json +445 -0
- package/spec/sources.json +135 -207
- package/src/cli.ts +0 -3
- package/src/engine/after-payment.ts +0 -2
- package/src/engine/balance.ts +0 -1
- package/src/engine/checkout.ts +0 -1
- package/src/engine/common.ts +19 -17
- package/src/engine/connect.ts +1 -79
- package/src/engine/customers.ts +0 -25
- package/src/engine/display.ts +2 -3
- package/src/engine/files.ts +0 -13
- package/src/engine/invoices.ts +64 -33
- package/src/engine/issuing.ts +16 -11
- package/src/engine/ledger.ts +0 -2
- package/src/engine/oauth.ts +1 -2
- package/src/engine/onboarding.ts +0 -1
- package/src/engine/params.ts +0 -1
- package/src/engine/payment-intents.ts +0 -1
- package/src/engine/payment-methods.ts +0 -1
- package/src/engine/refunds.ts +2 -28
- package/src/engine/renewals.ts +0 -1
- package/src/engine/stripe.ts +35 -83
- package/src/engine/subscriptions.ts +0 -3
- package/src/engine/transfers.ts +0 -1
- package/src/engine/version.ts +15 -7
- package/src/engine/webhooks.ts +0 -2
- package/src/fetch.ts +2 -2
- package/src/generated/surface.gen.json +1 -1
- package/src/generated/ui.gen.json +1 -1
- package/src/index.ts +6 -4
- package/src/manifest.ts +348 -220
- package/src/screens/billing-portal.tsx +5 -7
- package/src/screens/checkout-js.ts +80 -0
- package/src/screens/checkout.tsx +0 -2
- package/src/screens/connect-onboarding.tsx +3 -5
- package/src/screens/dashboard.tsx +53 -73
- package/src/screens/stripe-js.ts +57 -125
- package/src/semantics/account.ts +10 -0
- package/src/semantics/account_links.ts +0 -1
- package/src/semantics/accounts.ts +8 -7
- package/src/semantics/apps.ts +1 -2
- package/src/semantics/around.ts +3 -6
- package/src/semantics/balance.ts +0 -1
- package/src/semantics/balance_transactions.ts +1 -3
- package/src/semantics/billing_portal.ts +1 -7
- package/src/semantics/charges.ts +1 -6
- package/src/semantics/checkout.ts +16 -12
- package/src/semantics/clock.ts +1 -8
- package/src/semantics/coupons.ts +0 -1
- package/src/semantics/customers.ts +32 -35
- package/src/semantics/doors.ts +2 -3
- package/src/semantics/entitlements.ts +1 -45
- package/src/semantics/events.ts +7 -14
- package/src/semantics/index.ts +16 -38
- package/src/semantics/invoice_payments.ts +1 -3
- package/src/semantics/invoices.ts +124 -119
- package/src/semantics/issuing.ts +74 -0
- package/src/semantics/payment_intents.ts +10 -17
- package/src/semantics/payment_methods.ts +2 -25
- package/src/semantics/payouts.ts +9 -3
- package/src/semantics/prices.ts +0 -1
- package/src/semantics/products.ts +4 -14
- package/src/semantics/promotion_codes.ts +0 -1
- package/src/semantics/radar.ts +1 -3
- package/src/semantics/refunds.ts +2 -11
- package/src/semantics/shared.ts +350 -1382
- package/src/semantics/states.ts +109 -330
- package/src/semantics/subscriptions.ts +25 -145
- package/src/semantics/test_helpers.ts +109 -43
- package/src/semantics/transfers.ts +1 -3
- package/src/semantics/webhook_endpoints.ts +27 -0
- package/src/server.ts +0 -1
- package/dist/src/engine/billing.d.ts +0 -5
- package/dist/src/engine/billing.js +0 -6
- package/dist/src/engine/charges.d.ts +0 -6
- package/dist/src/engine/charges.js +0 -13
- package/dist/src/engine/connect-settings.d.ts +0 -16
- package/dist/src/engine/connect-settings.js +0 -26
- package/dist/src/engine/coupons.d.ts +0 -0
- package/dist/src/engine/coupons.js +0 -5
- package/dist/src/engine/credit-notes.d.ts +0 -1
- package/dist/src/engine/credit-notes.js +0 -5
- package/dist/src/engine/disputes.d.ts +0 -0
- package/dist/src/engine/disputes.js +0 -9
- package/dist/src/engine/entitlements.d.ts +0 -1
- package/dist/src/engine/entitlements.js +0 -4
- package/dist/src/engine/ephemeral-keys.d.ts +0 -0
- package/dist/src/engine/ephemeral-keys.js +0 -4
- package/dist/src/engine/front.d.ts +0 -5
- package/dist/src/engine/front.js +0 -25
- package/dist/src/engine/payment-links.d.ts +0 -1
- package/dist/src/engine/payment-links.js +0 -5
- package/dist/src/engine/plans.d.ts +0 -6
- package/dist/src/engine/plans.js +0 -26
- package/dist/src/engine/platform.d.ts +0 -39
- package/dist/src/engine/platform.js +0 -34
- package/dist/src/engine/products.d.ts +0 -0
- package/dist/src/engine/products.js +0 -4
- package/dist/src/engine/setup-intents.d.ts +0 -1
- package/dist/src/engine/setup-intents.js +0 -4
- package/dist/src/engine/subscription-schedules.d.ts +0 -3
- package/dist/src/engine/subscription-schedules.js +0 -1
- package/dist/src/engine/tax.d.ts +0 -7
- package/dist/src/engine/tax.js +0 -28
- package/dist/src/engine/terminal.d.ts +0 -4
- package/dist/src/engine/terminal.js +0 -20
- package/dist/src/engine/test-clocks.d.ts +0 -5
- package/dist/src/engine/test-clocks.js +0 -9
- package/dist/src/engine/tokens.d.ts +0 -7
- package/dist/src/engine/tokens.js +0 -20
- package/dist/src/engine/treasury.d.ts +0 -23
- package/dist/src/engine/treasury.js +0 -45
- package/dist/src/screens/connect-oauth.js +0 -333
- package/dist/src/semantics/disputes.d.ts +0 -2
- package/dist/src/semantics/disputes.js +0 -5
- package/dist/src/semantics/invoiceitems.d.ts +0 -3
- package/dist/src/semantics/invoiceitems.js +0 -59
- package/dist/src/semantics/setup_intents.d.ts +0 -3
- package/dist/src/semantics/setup_intents.js +0 -41
- package/dist/src/semantics/subscription_items.d.ts +0 -5
- package/dist/src/semantics/subscription_items.js +0 -55
- package/dist/src/semantics/subscription_schedules.d.ts +0 -4
- package/dist/src/semantics/subscription_schedules.js +0 -60
- package/src/engine/billing.ts +0 -16
- package/src/engine/charges.ts +0 -19
- package/src/engine/connect-settings.ts +0 -23
- package/src/engine/coupons.ts +0 -5
- package/src/engine/credit-notes.ts +0 -6
- package/src/engine/disputes.ts +0 -9
- package/src/engine/entitlements.ts +0 -5
- package/src/engine/ephemeral-keys.ts +0 -3
- package/src/engine/front.ts +0 -31
- package/src/engine/payment-links.ts +0 -6
- package/src/engine/plans.ts +0 -42
- package/src/engine/platform.ts +0 -47
- package/src/engine/products.ts +0 -3
- package/src/engine/setup-intents.ts +0 -5
- package/src/engine/subscription-schedules.ts +0 -10
- package/src/engine/tax.ts +0 -39
- package/src/engine/terminal.ts +0 -23
- package/src/engine/test-clocks.ts +0 -11
- package/src/engine/tokens.ts +0 -22
- package/src/engine/treasury.ts +0 -47
- package/src/screens/connect-oauth.tsx +0 -376
- package/src/semantics/disputes.ts +0 -12
- package/src/semantics/invoiceitems.ts +0 -59
- package/src/semantics/setup_intents.ts +0 -45
- package/src/semantics/subscription_items.ts +0 -54
- package/src/semantics/subscription_schedules.ts +0 -66
|
@@ -3,7 +3,7 @@ import { flowPage, Portal, PORTAL_CSS } from '@volter/world-ui';
|
|
|
3
3
|
import { publicBusinessName } from "../engine/display.js";
|
|
4
4
|
import { declineFor, nowUnix, paymentMethodSubObject, PLATFORM_ACCOUNT_ID } from "../engine/stripe.js";
|
|
5
5
|
import { cardAnswer, COUNTRIES, formOf, money, notFound } from "./shared.js";
|
|
6
|
-
import { afterSuccessOf, created, payerOf, payOpenInvoice } from "../semantics/shared.js";
|
|
6
|
+
import { resourceView, afterSuccessOf, created, payerOf, payOpenInvoice } from "../semantics/shared.js";
|
|
7
7
|
const SUB = 'subscription';
|
|
8
8
|
const PORTAL_OPERATION = { id: 'PostBillingPortalSessions' };
|
|
9
9
|
// Stripe's skin: its type, its purple accent, the merchant's side tinted.
|
|
@@ -46,7 +46,7 @@ function planOf(ctx, sub) {
|
|
|
46
46
|
function page(ctx, id, session, problem) {
|
|
47
47
|
const features = featuresOf(ctx, session);
|
|
48
48
|
const customer = ctx.get('customer', String(session.customer)) ?? {};
|
|
49
|
-
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');
|
|
50
50
|
const plans = subs.map((s) => {
|
|
51
51
|
const plan = planOf(ctx, s);
|
|
52
52
|
const ending = s.cancel_at_period_end === true;
|
|
@@ -105,7 +105,7 @@ function page(ctx, id, session, problem) {
|
|
|
105
105
|
}
|
|
106
106
|
/** The customer's move on one of their subscriptions, then back to the portal. */
|
|
107
107
|
async function move(ctx, id, session, subscription, kind) {
|
|
108
|
-
const sub = ctx
|
|
108
|
+
const sub = resourceView(ctx, SUB, subscription);
|
|
109
109
|
if (!sub || sub.customer !== session.customer || sub.status === 'canceled')
|
|
110
110
|
return gone();
|
|
111
111
|
const back = new Response(null, { status: 303, headers: { location: `/p/session/${id}` } });
|
|
@@ -146,7 +146,7 @@ async function updatePaymentMethod(ctx, id, session, v) {
|
|
|
146
146
|
});
|
|
147
147
|
const holder = ctx.get('customer', customer) ?? {};
|
|
148
148
|
await ctx.write('customer', customer, { invoice_settings: { ...(holder.invoice_settings ?? {}), default_payment_method: pm.id } }, 'customer.update');
|
|
149
|
-
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')) {
|
|
150
150
|
await ctx.write(SUB, String(sub.id), { default_payment_method: pm.id }, 'subscription.update');
|
|
151
151
|
}
|
|
152
152
|
return new Response(null, { status: 303, headers: { location: `/p/session/${id}` } });
|
|
@@ -156,7 +156,7 @@ async function payInvoice(ctx, id, session, invoiceId) {
|
|
|
156
156
|
const inv = ctx.get('invoice', invoiceId);
|
|
157
157
|
if (!inv || inv.customer !== session.customer || inv.status !== 'open')
|
|
158
158
|
return gone();
|
|
159
|
-
const sub = typeof inv.subscription === 'string' ? ctx
|
|
159
|
+
const sub = typeof inv.subscription === 'string' ? resourceView(ctx, SUB, inv.subscription) : undefined;
|
|
160
160
|
const holder = ctx.get('customer', String(session.customer)) ?? {};
|
|
161
161
|
const pm = (sub ? payerOf(ctx, sub) : undefined) ?? holder.invoice_settings?.default_payment_method;
|
|
162
162
|
if (!pm)
|
|
@@ -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
|
+
}
|
|
@@ -6,7 +6,6 @@ import { INDUSTRIES as MCCS } from "../engine/industries.js";
|
|
|
6
6
|
import { currentlyDue, kindOf, OFFERED } from "../engine/onboarding.js";
|
|
7
7
|
import { created, syncExternals } from "../semantics/shared.js";
|
|
8
8
|
const LINK = 'account_link';
|
|
9
|
-
const OPERATION = { id: 'PostAccountsAccount' };
|
|
10
9
|
const gone = () => flowPage({ title: 'Stripe', status: 404, css: [CONSENT_CSS, STRIPE_CONSENT_SKIN], body: _jsx("main", { className: "consent", children: _jsx("h1", { className: "consent-heading", children: "This link is invalid." }) }) });
|
|
11
10
|
/** A link that can still be used: not used and not past its expiry. */
|
|
12
11
|
const usable = (ctx, link) => link.used !== true && Number(link.expires_at) > nowUnix(ctx.occurredAt);
|
|
@@ -81,7 +80,7 @@ async function submit(ctx, link, kind, v, ip) {
|
|
|
81
80
|
return;
|
|
82
81
|
const caps = { ...(account.capabilities ?? {}) };
|
|
83
82
|
for (const [cap, status] of Object.entries(caps)) {
|
|
84
|
-
if (status !== 'active'
|
|
83
|
+
if (status !== 'active')
|
|
85
84
|
caps[cap] = 'active';
|
|
86
85
|
}
|
|
87
86
|
const due = currentlyDue(kind, v);
|
|
@@ -91,6 +90,7 @@ async function submit(ctx, link, kind, v, ip) {
|
|
|
91
90
|
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 };
|
|
92
91
|
const company = kind === 'company';
|
|
93
92
|
const settings = account.settings ?? {};
|
|
93
|
+
const individual = company ? null : await created(ctx, 'person', { ...person, account: id }, { relationship: { representative: true }, metadata: {} });
|
|
94
94
|
await ctx.write('account', id, {
|
|
95
95
|
details_submitted: true, charges_enabled: due.length === 0, payouts_enabled: due.length === 0, capabilities: caps, requirements,
|
|
96
96
|
business_type: kind,
|
|
@@ -98,7 +98,7 @@ async function submit(ctx, link, kind, v, ip) {
|
|
|
98
98
|
settings: { ...settings, payments: { ...(settings.payments ?? {}), statement_descriptor: v.statement_descriptor } },
|
|
99
99
|
...(company
|
|
100
100
|
? { 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 } }
|
|
101
|
-
: { individual:
|
|
101
|
+
: { individual: individual.id }),
|
|
102
102
|
tos_acceptance: { date: nowUnix(ctx.occurredAt), ip, user_agent: null },
|
|
103
103
|
}, 'account.updated');
|
|
104
104
|
if (company) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { HandlerContext } from '@volter/world-core';
|
|
2
|
-
/**
|
|
2
|
+
/** Dashboard settings, Radar list creation and test Issuing funding; other pages answer Stripe's 404. */
|
|
3
3
|
export declare function screen(ctx: HandlerContext): Promise<Response>;
|
|
@@ -1,42 +1,16 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { flowPage } from '@volter/world-ui';
|
|
3
|
+
import { VL } from "../engine/radar.js";
|
|
4
|
+
import { nowUnix } from "../engine/stripe.js";
|
|
3
5
|
import { platformAccountDefault } from "../engine/connect.js";
|
|
4
|
-
import { CONNECT_SETTINGS_PATH, PLATFORM_CLIENT_ID, redirectUris, SETTINGS } from "../engine/connect-settings.js";
|
|
5
6
|
import { publicBusinessName } from "../engine/display.js";
|
|
6
7
|
import { PLATFORM_ACCOUNT_ID } from "../engine/stripe.js";
|
|
7
8
|
import { formOf, notFound } from "./shared.js";
|
|
8
|
-
import {
|
|
9
|
+
import { created } from "../semantics/shared.js";
|
|
10
|
+
const redirect = (path) => new Response(null, { status: 303, headers: { location: path } });
|
|
11
|
+
const invalid = (message) => flowPage({ css: [], title: 'Stripe', status: 400, body: _jsx("main", { children: _jsx("p", { role: "alert", children: message }) }) });
|
|
9
12
|
const PUBLIC_DETAILS_PATH = '/settings/public';
|
|
10
13
|
const MAX_NAME = 5000;
|
|
11
|
-
const SETTINGS_CSS = `
|
|
12
|
-
* { box-sizing: border-box; }
|
|
13
|
-
body { margin: 0; background: #f6f8fa; color: #1a1f36; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Ubuntu, sans-serif; font-size: 14px; }
|
|
14
|
-
.co { max-width: 680px; margin: 48px auto; padding: 0 16px; }
|
|
15
|
-
.co h1 { font-size: 28px; margin: 16px 0 4px; }
|
|
16
|
-
.co-lead { color: #697386; margin: 0 0 24px; }
|
|
17
|
-
.co-card { background: #ffffff; border: 1px solid #e3e8ee; border-radius: 8px; padding: 24px; margin-bottom: 16px; }
|
|
18
|
-
.co-card h2 { font-size: 16px; margin: 0 0 12px; }
|
|
19
|
-
.co-card label { display: block; font-weight: 600; margin-bottom: 6px; }
|
|
20
|
-
.co-card small { display: block; color: #697386; margin-bottom: 8px; }
|
|
21
|
-
.co-card textarea { width: 100%; min-height: 96px; padding: 8px 10px; border: 1px solid #c1c9d2; border-radius: 6px; font: 13px ui-monospace, Menlo, monospace; }
|
|
22
|
-
.co-id { font: 14px ui-monospace, Menlo, monospace; background: #f6f8fa; border: 1px solid #e3e8ee; border-radius: 6px; padding: 6px 10px; display: inline-block; }
|
|
23
|
-
.co-toggle { display: flex; gap: 8px; align-items: center; font-weight: 600; }
|
|
24
|
-
.co-actions { display: flex; justify-content: flex-end; }
|
|
25
|
-
.co-actions button { background: #635bff; color: #ffffff; border: 0; border-radius: 6px; padding: 8px 16px; font: inherit; font-weight: 600; cursor: pointer; }
|
|
26
|
-
.co-notice { padding: 10px 12px; border-radius: 6px; margin-bottom: 16px; }
|
|
27
|
-
.co-error { background: #fff0f3; color: #df1b41; }
|
|
28
|
-
.co-saved { background: #e7f8ef; color: #0e6245; }
|
|
29
|
-
.co-uris { margin: 0; padding-left: 18px; }
|
|
30
|
-
`;
|
|
31
|
-
function settingsPage(s, opts = {}) {
|
|
32
|
-
const enabled = opts.enabled ?? s.oauth_enabled;
|
|
33
|
-
return flowPage({
|
|
34
|
-
title: 'OAuth – Connect settings – Stripe',
|
|
35
|
-
css: [SETTINGS_CSS],
|
|
36
|
-
...(opts.error ? { status: 400 } : {}),
|
|
37
|
-
body: (_jsxs("main", { className: "co", children: [_jsx("h1", { children: "OAuth" }), _jsx("p", { className: "co-lead", children: "Onboard Standard accounts by sending them to Stripe's OAuth flow. Test mode." }), opts.error ? _jsx("p", { className: "co-notice co-error", role: "alert", children: opts.error }) : null, opts.saved ? _jsx("p", { className: "co-notice co-saved", role: "status", children: "Your OAuth settings were saved." }) : null, _jsxs("section", { className: "co-card", children: [_jsx("h2", { children: "Test mode client ID" }), _jsx("small", { children: "The client_id your platform sends to connect.stripe.com/oauth/authorize." }), _jsx("code", { className: "co-id", "data-testid": "connect-client-id", children: PLATFORM_CLIENT_ID }), _jsx("p", { "data-testid": "connect-oauth-status", children: s.oauth_enabled ? 'OAuth is enabled.' : 'OAuth is disabled.' }), s.redirect_uris.length ? _jsx("ul", { className: "co-uris", children: s.redirect_uris.map((u) => _jsx("li", { "data-testid": "connect-redirect-uri", children: u }, u)) }) : _jsx("p", { children: "No redirect URIs." })] }), _jsxs("form", { className: "co-card", method: "post", children: [_jsxs("label", { className: "co-toggle", htmlFor: "oauth_enabled", children: [_jsx("input", { type: "checkbox", id: "oauth_enabled", name: "oauth_enabled", value: "on", defaultChecked: enabled }), " Enable OAuth for Standard accounts"] }), _jsx("label", { htmlFor: "redirect_uris", children: "Redirect URIs" }), _jsx("small", { children: "One per line. The first is used when a request names none. http and localhost are allowed in test mode." }), _jsx("textarea", { id: "redirect_uris", name: "redirect_uris", defaultValue: opts.value ?? s.redirect_uris.join('\n') }), _jsx("div", { className: "co-actions", children: _jsx("button", { type: "submit", children: "Save" }) })] })] })),
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
14
|
const DETAILS_CSS = `
|
|
41
15
|
* { box-sizing: border-box; }
|
|
42
16
|
body { margin: 0; background: #f6f8fa; color: #1a1f36; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Ubuntu, sans-serif; font-size: 14px; }
|
|
@@ -74,22 +48,6 @@ async function save(ctx, name) {
|
|
|
74
48
|
function nameRefused(name) {
|
|
75
49
|
return !name ? 'Enter your business name.' : name.length > MAX_NAME ? `Your business name must be at most ${MAX_NAME} characters.` : undefined;
|
|
76
50
|
}
|
|
77
|
-
async function settings(ctx) {
|
|
78
|
-
const request = ctx.call.request;
|
|
79
|
-
const url = new URL(request.url);
|
|
80
|
-
if (request.method !== 'GET' && request.method !== 'POST')
|
|
81
|
-
return notFound();
|
|
82
|
-
const form = request.method === 'POST' ? await formOf(request.clone()) : {};
|
|
83
|
-
const held = connectOAuthSettings(ctx);
|
|
84
|
-
if (request.method === 'GET')
|
|
85
|
-
return settingsPage(held, { saved: url.searchParams.has('saved') });
|
|
86
|
-
const enabled = form.oauth_enabled === 'on' || form.oauth_enabled === 'true';
|
|
87
|
-
const parsed = redirectUris(form.redirect_uris ?? '');
|
|
88
|
-
if ('refused' in parsed)
|
|
89
|
-
return settingsPage(held, { value: form.redirect_uris ?? '', enabled, error: parsed.refused });
|
|
90
|
-
await ctx.record(SETTINGS, { oauth_enabled: enabled, redirect_uris: parsed.uris }, PLATFORM_CLIENT_ID);
|
|
91
|
-
return new Response(null, { status: 303, headers: { location: '?saved=1' } });
|
|
92
|
-
}
|
|
93
51
|
async function publicDetails(ctx) {
|
|
94
52
|
const request = ctx.call.request;
|
|
95
53
|
const url = new URL(request.url);
|
|
@@ -106,12 +64,53 @@ async function publicDetails(ctx) {
|
|
|
106
64
|
// a query-only reference: the page's own path, wherever it is mounted and with or without its trailing slash
|
|
107
65
|
return new Response(null, { status: 303, headers: { location: '?saved=1' } });
|
|
108
66
|
}
|
|
109
|
-
/**
|
|
67
|
+
/** Dashboard settings, Radar list creation and test Issuing funding; other pages answer Stripe's 404. */
|
|
110
68
|
export async function screen(ctx) {
|
|
111
69
|
const path = new URL(ctx.call.request.url).pathname.replace(/\/+$/, '');
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
70
|
+
// source: https://docs.stripe.com/radar/lists "Use the Dashboard or the API to create lists."
|
|
71
|
+
// source: https://docs.stripe.com/radar/lists "Enter a name for the list"
|
|
72
|
+
// source: https://docs.stripe.com/radar/lists "Select the type of list to create."
|
|
73
|
+
// source: https://docs.stripe.com/radar/lists "Click Add to save your new list."
|
|
74
|
+
// Where the documentation stops: it names Dashboard controls, not their browser request routes or form wire.
|
|
75
|
+
// This authored page uses /radar/lists for GET and POST, /radar/lists/{id} for its redirect/detail address,
|
|
76
|
+
// and a 303 redirect after saving. The HTML field names name, alias and item_type are the twin's choices;
|
|
77
|
+
// item_type values are the pinned API's enums, not published Dashboard form values. The alias fallback's
|
|
78
|
+
// lowercase/underscore algorithm is also authored; Stripe says it generates an alias but gives no algorithm.
|
|
79
|
+
if (/^\/radar\/lists(?:\/[^/]+)?$/.test(path)) {
|
|
80
|
+
if (ctx.call.request.method === 'POST') {
|
|
81
|
+
const form = await formOf(ctx.call.request);
|
|
82
|
+
const types = ['string', 'case_sensitive_string', 'card_fingerprint', 'card_bin', 'customer_id', 'email', 'ip_address', 'country', 'sepa_debit_fingerprint', 'us_bank_account_fingerprint'];
|
|
83
|
+
if (!form.name?.trim() || !types.includes(form.item_type ?? ''))
|
|
84
|
+
return invalid('Enter a name and select a list type.');
|
|
85
|
+
const alias = form.alias?.trim() || form.name.toLowerCase().replace(/[^a-z0-9]+/g, '_');
|
|
86
|
+
// Where the page stops: an operator has no login identity in the World; created_by is its synthetic operator.
|
|
87
|
+
const list = await created(ctx, VL, { name: form.name.trim(), alias, item_type: form.item_type }, {
|
|
88
|
+
created_by: 'twin', livemode: false, metadata: {},
|
|
89
|
+
list_items: { object: 'list', data: [], has_more: false, url: '/v1/radar/value_list_items' },
|
|
90
|
+
});
|
|
91
|
+
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');
|
|
92
|
+
return redirect(`/radar/lists/${list.id}`);
|
|
93
|
+
}
|
|
94
|
+
return flowPage({ css: [], title: 'Lists – Radar – Stripe', body: _jsxs("main", { children: [_jsx("h1", { children: "Radar Lists" }), _jsxs("form", { method: "post", action: "/radar/lists", children: [_jsx("h2", { children: "New list" }), _jsxs("label", { children: ["Name", _jsx("input", { name: "name", required: true })] }), _jsxs("label", { children: ["Alias", _jsx("input", { name: "alias" })] }), _jsxs("label", { children: ["List type", _jsxs("select", { name: "item_type", children: [_jsx("option", { value: "customer_id", children: "Customer ID" }), _jsx("option", { value: "email", children: "Email" }), _jsx("option", { value: "card_fingerprint", children: "Card fingerprint" }), _jsx("option", { value: "ip_address", children: "IP address" }), _jsx("option", { value: "string", children: "String" })] })] }), _jsx("button", { children: "Add" })] }), _jsx("ul", { children: ctx.rows(VL).map(row => _jsxs("li", { children: [String(row.name), " (", String(row.alias), ")"] }, String(row.id))) })] }) });
|
|
95
|
+
}
|
|
96
|
+
// source: https://docs.stripe.com/issuing/testing "Before you create test transactions, you must add test funds to the Issuing balance on your account."
|
|
97
|
+
// source: https://docs.stripe.com/issuing/testing "You can create test top-ups in the Dashboard, or with the Top-ups API"
|
|
98
|
+
// Where the documentation stops: it states the Dashboard funding act, not a browser request path or form wire.
|
|
99
|
+
// The authored GET/POST address and 303 redirect are /test/issuing/balance. The form name amount, its positive
|
|
100
|
+
// integer validation and its units (USD cents for this US sandbox) are the twin's choices, not published form
|
|
101
|
+
// fields. Its POST records the balance change in the vendor ledger, with source=null (permitted by the pinned
|
|
102
|
+
// schema) and immediate availability for this sandbox control; the page states neither that source nor timing.
|
|
103
|
+
// No private Topup object is seeded or answered, and the uncalled Top-ups API remains the gap.
|
|
104
|
+
if (path === '/test/issuing/balance') {
|
|
105
|
+
if (ctx.call.request.method === 'POST') {
|
|
106
|
+
const form = await formOf(ctx.call.request);
|
|
107
|
+
const amount = Number(form.amount);
|
|
108
|
+
if (!Number.isInteger(amount) || amount <= 0)
|
|
109
|
+
return invalid('Enter a positive amount in cents.');
|
|
110
|
+
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: [] });
|
|
111
|
+
return redirect('/test/issuing/balance');
|
|
112
|
+
}
|
|
113
|
+
return flowPage({ css: [], title: 'Test Issuing balance – Stripe', body: _jsxs("main", { children: [_jsx("h1", { children: "Test Issuing balance" }), _jsxs("form", { method: "post", children: [_jsxs("label", { children: ["Amount", _jsx("input", { name: "amount", type: "number", min: "1", required: true })] }), _jsx("button", { children: "Add test funds" })] })] }) });
|
|
114
|
+
}
|
|
115
|
+
return path === PUBLIC_DETAILS_PATH ? publicDetails(ctx) : notFound();
|
|
117
116
|
}
|
|
@@ -1,115 +1,75 @@
|
|
|
1
1
|
import { notFound } from "./shared.js";
|
|
2
2
|
/** The release train a script path names: `/v3`, `/v3/` or `/v3/stripe.js` → 3, `/<train>/stripe.js` → the train. */
|
|
3
3
|
function stripeJsTrain(pathname) {
|
|
4
|
-
|
|
5
|
-
return 3;
|
|
6
|
-
return /^\/([a-z]+)\/stripe\.js$/.exec(pathname)?.[1];
|
|
4
|
+
return /^\/v3(\/|\/stripe\.js)?$/.test(pathname) ? 3 : /^\/([a-z]+)\/stripe\.js$/.exec(pathname)?.[1];
|
|
7
5
|
}
|
|
6
|
+
// 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>"
|
|
8
7
|
function script(version) {
|
|
9
8
|
return `(function () {
|
|
10
9
|
var version = ${JSON.stringify(version)};
|
|
11
10
|
function gap(name) {
|
|
12
11
|
return function () { throw new Error("Stripe.js (Volter twin): stripe." + name + " is not modelled by this twin yet"); };
|
|
13
12
|
}
|
|
14
|
-
function
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
function cardOf(elements) {
|
|
20
|
-
var el = (elements && elements._mounted && elements._mounted[elements._mounted.length - 1]) || mounted[mounted.length - 1];
|
|
21
|
-
if (!el) return null;
|
|
22
|
-
var exp = String(el.expiry.value || "").split("/");
|
|
23
|
-
return { number: String(el.number.value || "").replace(/\s+/g, ""), exp_month: exp[0] ? String(Number(exp[0])) : "", exp_year: exp[1] ? String(2000 + Number(exp[1]) % 100) : "", cvc: String(el.cvc.value || "") };
|
|
24
|
-
}
|
|
25
|
-
function form(fields) {
|
|
26
|
-
return Object.keys(fields).filter(function (k) { return fields[k] !== undefined && fields[k] !== ""; }).map(function (k) { return encodeURIComponent(k) + "=" + encodeURIComponent(fields[k]); }).join("&");
|
|
27
|
-
}
|
|
28
|
-
function confirm(kind, options) {
|
|
29
|
-
options = options || {};
|
|
30
|
-
var elements = options.elements;
|
|
31
|
-
var secret = options.clientSecret || (elements && elements._options && elements._options.clientSecret);
|
|
32
|
-
if (typeof secret !== "string" || secret.indexOf("_secret_") < 0) return Promise.resolve({ error: { type: "invalid_request_error", message: "A clientSecret is required." } });
|
|
13
|
+
function initCheckout(options) {
|
|
14
|
+
var session, root, fields = {}, listeners = {};
|
|
15
|
+
function emit(name, value) { (listeners[name] || []).forEach(function (fn) { fn(value); }); }
|
|
16
|
+
function call(action, values) {
|
|
17
|
+
var secret = options.clientSecret;
|
|
33
18
|
var id = secret.split("_secret_")[0];
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
.then(function (res) { return res.json().then(function (body) { return { ok: res.ok, body: body }; }); })
|
|
42
|
-
.then(function (r) {
|
|
43
|
-
if (!r.ok) return { error: r.body.error || { type: "api_error", message: "The confirmation failed." } };
|
|
44
|
-
var key = kind === "payment" ? "paymentIntent" : "setupIntent";
|
|
45
|
-
var out = {}; out[key] = r.body;
|
|
46
|
-
if (options.redirect === "if_required" || !params.return_url) return out;
|
|
47
|
-
var q = kind === "payment" ? "payment_intent" : "setup_intent";
|
|
48
|
-
var target = new URL(params.return_url);
|
|
49
|
-
target.searchParams.set(q, r.body.id);
|
|
50
|
-
target.searchParams.set(q + "_client_secret", secret);
|
|
51
|
-
target.searchParams.set("redirect_status", r.body.status === "succeeded" ? "succeeded" : r.body.status === "processing" ? "processing" : "failed");
|
|
52
|
-
window.location.assign(target.toString());
|
|
53
|
-
return new Promise(function () {});
|
|
54
|
-
});
|
|
19
|
+
return fetch("https://api.stripe.com/v1/payment_pages/" + encodeURIComponent(id) + "/" + action, {
|
|
20
|
+
method: "POST", headers: { "content-type": "application/json" },
|
|
21
|
+
body: JSON.stringify(Object.assign({ client_secret: secret }, values || {}))
|
|
22
|
+
}).then(function (r) { return r.json(); }).then(function (result) {
|
|
23
|
+
if (result.type === "success") { session = result.session; emit("change", session); }
|
|
24
|
+
return result;
|
|
25
|
+
});
|
|
55
26
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
var wrap = document.createElement("div");
|
|
66
|
-
wrap.setAttribute("data-volter-twin-element", type);
|
|
67
|
-
function input(name, placeholder) { var i = document.createElement("input"); i.name = name; i.placeholder = placeholder; i.autocomplete = "off"; i.addEventListener("input", change); wrap.appendChild(i); return i; }
|
|
68
|
-
el.number = input("cardnumber", "Card number");
|
|
69
|
-
el.expiry = input("exp-date", "MM / YY");
|
|
70
|
-
el.cvc = input("cvc", "CVC");
|
|
71
|
-
node.appendChild(wrap);
|
|
72
|
-
mounted.push(el); group._mounted.push(el);
|
|
73
|
-
setTimeout(function () { emit("ready", { elementType: type }); }, 0);
|
|
74
|
-
},
|
|
75
|
-
on: function (name, handler) { (handlers[name] = handlers[name] || []).push(handler); return el; },
|
|
76
|
-
off: function (name, handler) { handlers[name] = (handlers[name] || []).filter(function (h) { return h !== handler; }); return el; },
|
|
77
|
-
update: function () {}, focus: function () { if (el.number) el.number.focus(); }, blur: function () {},
|
|
78
|
-
clear: function () { [el.number, el.expiry, el.cvc].forEach(function (i) { if (i) i.value = ""; }); change(); },
|
|
79
|
-
unmount: function () {}, destroy: function () { mounted = mounted.filter(function (m) { return m !== el; }); }
|
|
80
|
-
};
|
|
81
|
-
function change() {
|
|
82
|
-
var c = { number: el.number && el.number.value, expiry: el.expiry && el.expiry.value, cvc: el.cvc && el.cvc.value };
|
|
83
|
-
emit("change", { elementType: type, empty: !c.number && !c.expiry && !c.cvc, complete: !!(c.number && c.expiry && c.cvc), value: { type: "card" } });
|
|
27
|
+
var actions = {
|
|
28
|
+
getSession: function () { return session; },
|
|
29
|
+
applyPromotionCode: function (code) { return call("apply_promotion_code", { promotion_code: code }); },
|
|
30
|
+
removePromotionCode: function () { return call("remove_promotion_code"); },
|
|
31
|
+
confirm: function (params) {
|
|
32
|
+
return call("confirm", Object.assign({}, params || {}, { payment_details: fields })).then(function (result) {
|
|
33
|
+
if (result.type === "success" && (!params || params.redirect !== "if_required") && result.return_url) window.location.assign((params && params.returnUrl) || result.return_url);
|
|
34
|
+
return result;
|
|
35
|
+
});
|
|
84
36
|
}
|
|
85
|
-
|
|
86
|
-
}
|
|
37
|
+
};
|
|
38
|
+
var elementListeners = {};
|
|
39
|
+
function elementEmit(name, value) { (elementListeners[name] || []).forEach(function (fn) { fn(value); }); }
|
|
40
|
+
var element = {
|
|
41
|
+
on: function (name, fn) { (elementListeners[name] || (elementListeners[name] = [])).push(fn); return element; },
|
|
42
|
+
off: function (name, fn) { elementListeners[name] = (elementListeners[name] || []).filter(function (f) { return f !== fn; }); return element; },
|
|
43
|
+
mount: function (target) {
|
|
44
|
+
root = typeof target === "string" ? document.querySelector(target) : target;
|
|
45
|
+
[["cardNumber", "Card number", "cc-number"], ["cardExpiry", "Expiration", "cc-exp"], ["cardCvc", "CVC", "cc-csc"], ["billingName", "Cardholder name", "cc-name"]].forEach(function (field) {
|
|
46
|
+
var label = document.createElement("label"), input = document.createElement("input");
|
|
47
|
+
label.textContent = field[1]; input.name = field[0]; input.autocomplete = field[2];
|
|
48
|
+
input.addEventListener("input", function () { fields[field[0]] = input.value; elementEmit("change", { empty: !fields.cardNumber, complete: !!(fields.cardNumber && fields.cardExpiry && fields.cardCvc), value: { type: "card" } }); });
|
|
49
|
+
label.appendChild(input); root.appendChild(label);
|
|
50
|
+
});
|
|
51
|
+
Promise.resolve().then(function () { elementEmit("ready", element); });
|
|
52
|
+
},
|
|
53
|
+
unmount: function () { if (root) root.replaceChildren(); root = null; },
|
|
54
|
+
destroy: function () { element.unmount(); elementListeners = {}; },
|
|
55
|
+
update: function () {}
|
|
56
|
+
};
|
|
57
|
+
return {
|
|
58
|
+
on: function (name, fn) { (listeners[name] || (listeners[name] = [])).push(fn); },
|
|
59
|
+
loadActions: function () { return call("init").then(function (r) { return r.type === "success" ? { type: "success", actions: actions } : r; }); },
|
|
60
|
+
createPaymentElement: function () { return element; }, getPaymentElement: function () { return element; },
|
|
61
|
+
changeAppearance: function () {}, loadFonts: function () {}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function Stripe(publishableKey) {
|
|
65
|
+
if (typeof publishableKey !== "string" || !publishableKey) throw new Error("Stripe.js (Volter twin): Stripe() needs a publishable key");
|
|
87
66
|
var known = {
|
|
67
|
+
initCheckout: initCheckout,
|
|
88
68
|
_registerWrapper: function () {},
|
|
89
|
-
elements: function (options) {
|
|
90
|
-
var group = { _options: options || {}, _mounted: [], _created: {} };
|
|
91
|
-
group.create = function (type) { var e = element(type, group); group._created[type] = e; return e; };
|
|
92
|
-
group.getElement = function (type) { return group._created[typeof type === "string" ? type : "card"] || null; };
|
|
93
|
-
group.update = function (next) { group._options = Object.assign({}, group._options, next || {}); };
|
|
94
|
-
group.fetchUpdates = function () { return Promise.resolve({}); };
|
|
95
|
-
group.submit = function () {
|
|
96
|
-
var card = cardOf(group);
|
|
97
|
-
if (!card || !card.number || !card.exp_month || !card.cvc) return Promise.resolve({ error: { type: "validation_error", message: "Your card details are incomplete." } });
|
|
98
|
-
return Promise.resolve({});
|
|
99
|
-
};
|
|
100
|
-
return group;
|
|
101
|
-
},
|
|
102
|
-
confirmPayment: function (options) { return confirm("payment", options); },
|
|
103
|
-
confirmSetup: function (options) { return confirm("setup", options); },
|
|
104
69
|
redirectToCheckout: function (options) {
|
|
105
70
|
if (!options || typeof options.sessionId !== "string" || !options.sessionId) return gap("redirectToCheckout without a sessionId")();
|
|
106
71
|
window.location.assign("https://checkout.stripe.com/c/pay/" + encodeURIComponent(options.sessionId));
|
|
107
72
|
return new Promise(function () {});
|
|
108
|
-
},
|
|
109
|
-
collectFinancialConnectionsAccounts: function (options) {
|
|
110
|
-
if (!options || typeof options.clientSecret !== "string" || !options.clientSecret) return gap("collectFinancialConnectionsAccounts without a clientSecret")();
|
|
111
|
-
window.location.assign("https://js.stripe.com/v3/financial-connections/" + encodeURIComponent(options.clientSecret));
|
|
112
|
-
return new Promise(function () {});
|
|
113
73
|
}
|
|
114
74
|
};
|
|
115
75
|
return new Proxy(known, {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { PLATFORM_ACCOUNT_ID } from "../engine/stripe.js";
|
|
2
|
+
import { actingAccount, accountMissing } from "./shared.js";
|
|
3
|
+
// source: spec:GetAccount "Retrieves the details of an account."
|
|
4
|
+
export async function GetAccount(ctx) {
|
|
5
|
+
const id = actingAccount(ctx) ?? PLATFORM_ACCOUNT_ID;
|
|
6
|
+
const account = ctx.get('account', id);
|
|
7
|
+
return account ? ctx.reply(ctx.expand('account', account)) : accountMissing(ctx, id);
|
|
8
|
+
}
|
|
@@ -27,7 +27,7 @@ export async function PostAccounts(ctx) {
|
|
|
27
27
|
tos_acceptance: { date: null, ip: null, user_agent: null },
|
|
28
28
|
});
|
|
29
29
|
await connectReview(ctx, String(made.id));
|
|
30
|
-
return ctx.reply(ctx.get('account', String(made.id)));
|
|
30
|
+
return ctx.reply(ctx.expand('account', ctx.get('account', String(made.id))));
|
|
31
31
|
}
|
|
32
32
|
// a single-use Express dashboard link, not stored
|
|
33
33
|
export async function PostAccountsAccountLoginLinks(ctx) {
|
|
@@ -36,3 +36,5 @@ export async function PostAccountsAccountLoginLinks(ctx) {
|
|
|
36
36
|
return accountMissing(ctx, id);
|
|
37
37
|
return ctx.reply({ object: 'login_link', created: nowUnix(ctx.occurredAt), url: `https://connect.twin.local/express/${id}` });
|
|
38
38
|
}
|
|
39
|
+
// source: spec:PostAccountsAccount "left unchanged."
|
|
40
|
+
// source: https://docs.stripe.com/connect/account-capabilities "If a connected account has both card_payments and transfers"
|
|
@@ -1,16 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { platformKeyRefused, publishableKeyRefused, refuseParameters, rendered, withMetadata } from "./shared.js";
|
|
2
2
|
export async function around(ctx, next) {
|
|
3
3
|
const arrived = ctx.call.request;
|
|
4
4
|
const unkeyed = platformKeyRefused(ctx, arrived);
|
|
5
5
|
if (unkeyed)
|
|
6
6
|
return unkeyed;
|
|
7
|
-
const acting = await actingAsOAuthKey(ctx, arrived.clone());
|
|
8
|
-
if (acting instanceof Response)
|
|
9
|
-
return acting;
|
|
10
7
|
if (ctx.call.operation.id !== 'unmatched') {
|
|
11
|
-
const refused = publishableKeyRefused(ctx) ??
|
|
8
|
+
const refused = publishableKeyRefused(ctx) ?? refuseParameters(ctx);
|
|
12
9
|
if (refused)
|
|
13
10
|
return refused;
|
|
14
11
|
}
|
|
15
|
-
return rendered(arrived, await next(withMetadata(ctx,
|
|
12
|
+
return rendered(arrived, await next(withMetadata(ctx, arrived.clone())));
|
|
16
13
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { HandlerContext } from '@volter/world-core';
|
|
2
2
|
export declare function GetBillingPortalConfigurations(ctx: HandlerContext): Promise<Response>;
|
|
3
3
|
export declare function PostBillingPortalConfigurations(ctx: HandlerContext): Promise<Response>;
|
|
4
|
-
export declare function PostBillingPortalConfigurationsConfiguration(ctx: HandlerContext): Promise<Response>;
|
|
5
4
|
export declare function PostBillingPortalSessions(ctx: HandlerContext): Promise<Response>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { filled, PBC, PORTAL_FEATURES } from "../engine/files.js";
|
|
2
2
|
import { asBool, nowUnix } from "../engine/stripe.js";
|
|
3
|
-
import { actingAccount,
|
|
3
|
+
import { actingAccount, created, fail, list, newest, where } from "./shared.js";
|
|
4
4
|
export async function GetBillingPortalConfigurations(ctx) {
|
|
5
5
|
return list(ctx, PBC, where(ctx, newest(ctx, PBC), {
|
|
6
6
|
active: (c, v) => asBool(c.active) === asBool(v),
|
|
@@ -15,12 +15,6 @@ export async function PostBillingPortalConfigurations(ctx) {
|
|
|
15
15
|
updated: nowUnix(ctx.occurredAt),
|
|
16
16
|
}));
|
|
17
17
|
}
|
|
18
|
-
export async function PostBillingPortalConfigurationsConfiguration(ctx) {
|
|
19
|
-
const id = at(ctx, 'configuration');
|
|
20
|
-
if (!ctx.get(PBC, id))
|
|
21
|
-
return ctx.notFound(PBC, id);
|
|
22
|
-
return ctx.reply(await ctx.write(PBC, id, mergeUpdate(ctx, PBC, id, { ...ctx.params, updated: nowUnix(ctx.occurredAt) }), 'billing_portal.configuration.update'));
|
|
23
|
-
}
|
|
24
18
|
// ── the customer portal ──
|
|
25
19
|
// the configuration defaults to the account's default one
|
|
26
20
|
export async function PostBillingPortalSessions(ctx) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import type { HandlerContext } from '@volter/world-core';
|
|
2
|
+
export declare function GetCheckoutSessionsSessionLineItems(ctx: HandlerContext): Promise<Response>;
|
|
2
3
|
export declare function PostCheckoutSessions(ctx: HandlerContext): Promise<Response>;
|
|
3
|
-
export declare function PostCheckoutSessionsSessionExpire(ctx: HandlerContext): Promise<Response>;
|