@volter/twin-stripe 3.0.0 → 3.0.2

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.
Files changed (253) hide show
  1. package/README.md +75 -21
  2. package/dist/journeys/coverage.json +596 -0
  3. package/dist/journeys/customer-life.json +6108 -0
  4. package/dist/journeys/customer-life.outline.md +371 -0
  5. package/dist/journeys/decisions.json +677 -0
  6. package/dist/journeys/demand.json +706 -0
  7. package/dist/journeys/score.json +853 -0
  8. package/dist/journeys/unreachable.json +102 -0
  9. package/dist/journeys/vendor-examples.json +8010 -0
  10. package/dist/spec/SOURCE.md +5 -1
  11. package/dist/spec/doc-examples.json +445 -0
  12. package/dist/spec/sources.json +135 -207
  13. package/dist/src/cli.d.ts +0 -1
  14. package/dist/src/cli.js +0 -2
  15. package/dist/src/engine/after-payment.d.ts +0 -1
  16. package/dist/src/engine/after-payment.js +0 -1
  17. package/dist/src/engine/common.d.ts +5 -5
  18. package/dist/src/engine/common.js +13 -11
  19. package/dist/src/engine/connect.d.ts +0 -21
  20. package/dist/src/engine/connect.js +1 -82
  21. package/dist/src/engine/customers.d.ts +0 -8
  22. package/dist/src/engine/customers.js +0 -22
  23. package/dist/src/engine/display.d.ts +0 -3
  24. package/dist/src/engine/display.js +0 -3
  25. package/dist/src/engine/files.d.ts +0 -7
  26. package/dist/src/engine/files.js +0 -10
  27. package/dist/src/engine/invoices.d.ts +6 -9
  28. package/dist/src/engine/invoices.js +56 -29
  29. package/dist/src/engine/issuing.d.ts +6 -8
  30. package/dist/src/engine/issuing.js +19 -10
  31. package/dist/src/engine/ledger.d.ts +0 -1
  32. package/dist/src/engine/ledger.js +0 -1
  33. package/dist/src/engine/oauth.js +1 -2
  34. package/dist/src/engine/refunds.js +2 -27
  35. package/dist/src/engine/stripe.d.ts +2 -7
  36. package/dist/src/engine/stripe.js +34 -83
  37. package/dist/src/engine/subscriptions.d.ts +0 -1
  38. package/dist/src/engine/subscriptions.js +0 -1
  39. package/dist/src/engine/version.d.ts +0 -2
  40. package/dist/src/engine/version.js +17 -6
  41. package/dist/src/fetch.js +2 -2
  42. package/dist/src/generated/surface.gen.json +1 -1
  43. package/dist/src/generated/ui.gen.json +1 -1
  44. package/dist/src/index.js +5 -3
  45. package/dist/src/manifest.js +348 -220
  46. package/dist/src/screens/billing-portal.js +5 -5
  47. package/dist/src/screens/{connect-oauth.d.ts → checkout-js.d.ts} +0 -1
  48. package/dist/src/screens/checkout-js.js +84 -0
  49. package/dist/src/screens/connect-onboarding.js +3 -3
  50. package/dist/src/screens/dashboard.d.ts +1 -1
  51. package/dist/src/screens/dashboard.js +56 -53
  52. package/dist/src/screens/stripe-js.js +53 -93
  53. package/dist/src/semantics/account.d.ts +2 -0
  54. package/dist/src/semantics/account.js +8 -0
  55. package/dist/src/semantics/accounts.js +3 -1
  56. package/dist/src/semantics/around.js +3 -6
  57. package/dist/src/semantics/billing_portal.d.ts +0 -1
  58. package/dist/src/semantics/billing_portal.js +1 -7
  59. package/dist/src/semantics/checkout.d.ts +1 -1
  60. package/dist/src/semantics/checkout.js +18 -15
  61. package/dist/src/semantics/clock.js +1 -7
  62. package/dist/src/semantics/customers.d.ts +1 -2
  63. package/dist/src/semantics/customers.js +32 -38
  64. package/dist/src/semantics/entitlements.d.ts +0 -2
  65. package/dist/src/semantics/entitlements.js +4 -50
  66. package/dist/src/semantics/events.d.ts +2 -3
  67. package/dist/src/semantics/events.js +6 -11
  68. package/dist/src/semantics/index.js +16 -38
  69. package/dist/src/semantics/invoices.d.ts +3 -8
  70. package/dist/src/semantics/invoices.js +134 -127
  71. package/dist/src/semantics/issuing.d.ts +6 -0
  72. package/dist/src/semantics/issuing.js +77 -0
  73. package/dist/src/semantics/payment_intents.d.ts +0 -1
  74. package/dist/src/semantics/payment_intents.js +5 -13
  75. package/dist/src/semantics/payment_methods.d.ts +0 -2
  76. package/dist/src/semantics/payment_methods.js +2 -26
  77. package/dist/src/semantics/payouts.d.ts +1 -0
  78. package/dist/src/semantics/payouts.js +8 -1
  79. package/dist/src/semantics/products.d.ts +1 -3
  80. package/dist/src/semantics/products.js +3 -14
  81. package/dist/src/semantics/refunds.d.ts +1 -2
  82. package/dist/src/semantics/refunds.js +1 -5
  83. package/dist/src/semantics/shared.d.ts +64 -336
  84. package/dist/src/semantics/shared.js +326 -1336
  85. package/dist/src/semantics/states.d.ts +20 -89
  86. package/dist/src/semantics/states.js +116 -334
  87. package/dist/src/semantics/subscriptions.d.ts +1 -2
  88. package/dist/src/semantics/subscriptions.js +23 -145
  89. package/dist/src/semantics/test_helpers.d.ts +2 -5
  90. package/dist/src/semantics/test_helpers.js +121 -43
  91. package/dist/src/semantics/webhook_endpoints.d.ts +2 -0
  92. package/dist/src/semantics/webhook_endpoints.js +26 -0
  93. package/generated/pack-facts.json +3 -3
  94. package/journeys/coverage.json +596 -0
  95. package/journeys/customer-life.json +6108 -0
  96. package/journeys/customer-life.outline.md +371 -0
  97. package/journeys/decisions.json +677 -0
  98. package/journeys/demand.json +706 -0
  99. package/journeys/score.json +853 -0
  100. package/journeys/unreachable.json +102 -0
  101. package/journeys/vendor-examples.json +8010 -0
  102. package/package.json +11 -10
  103. package/spec/SOURCE.md +5 -1
  104. package/spec/doc-examples.json +445 -0
  105. package/spec/sources.json +135 -207
  106. package/src/cli.ts +0 -3
  107. package/src/engine/after-payment.ts +0 -2
  108. package/src/engine/balance.ts +0 -1
  109. package/src/engine/checkout.ts +0 -1
  110. package/src/engine/common.ts +19 -17
  111. package/src/engine/connect.ts +1 -79
  112. package/src/engine/customers.ts +0 -25
  113. package/src/engine/display.ts +2 -3
  114. package/src/engine/files.ts +0 -13
  115. package/src/engine/invoices.ts +64 -33
  116. package/src/engine/issuing.ts +16 -11
  117. package/src/engine/ledger.ts +0 -2
  118. package/src/engine/oauth.ts +1 -2
  119. package/src/engine/onboarding.ts +0 -1
  120. package/src/engine/params.ts +0 -1
  121. package/src/engine/payment-intents.ts +0 -1
  122. package/src/engine/payment-methods.ts +0 -1
  123. package/src/engine/refunds.ts +2 -28
  124. package/src/engine/renewals.ts +0 -1
  125. package/src/engine/stripe.ts +35 -83
  126. package/src/engine/subscriptions.ts +0 -3
  127. package/src/engine/transfers.ts +0 -1
  128. package/src/engine/version.ts +15 -7
  129. package/src/engine/webhooks.ts +0 -2
  130. package/src/fetch.ts +2 -2
  131. package/src/generated/surface.gen.json +1 -1
  132. package/src/generated/ui.gen.json +1 -1
  133. package/src/index.ts +6 -4
  134. package/src/manifest.ts +348 -220
  135. package/src/screens/billing-portal.tsx +5 -7
  136. package/src/screens/checkout-js.ts +80 -0
  137. package/src/screens/checkout.tsx +0 -2
  138. package/src/screens/connect-onboarding.tsx +3 -5
  139. package/src/screens/dashboard.tsx +55 -73
  140. package/src/screens/stripe-js.ts +57 -125
  141. package/src/semantics/account.ts +10 -0
  142. package/src/semantics/account_links.ts +0 -1
  143. package/src/semantics/accounts.ts +8 -7
  144. package/src/semantics/apps.ts +1 -2
  145. package/src/semantics/around.ts +3 -6
  146. package/src/semantics/balance.ts +0 -1
  147. package/src/semantics/balance_transactions.ts +1 -3
  148. package/src/semantics/billing_portal.ts +1 -7
  149. package/src/semantics/charges.ts +1 -6
  150. package/src/semantics/checkout.ts +16 -12
  151. package/src/semantics/clock.ts +1 -8
  152. package/src/semantics/coupons.ts +0 -1
  153. package/src/semantics/customers.ts +32 -35
  154. package/src/semantics/doors.ts +2 -3
  155. package/src/semantics/entitlements.ts +1 -45
  156. package/src/semantics/events.ts +7 -14
  157. package/src/semantics/index.ts +16 -38
  158. package/src/semantics/invoice_payments.ts +1 -3
  159. package/src/semantics/invoices.ts +124 -119
  160. package/src/semantics/issuing.ts +74 -0
  161. package/src/semantics/payment_intents.ts +10 -17
  162. package/src/semantics/payment_methods.ts +2 -25
  163. package/src/semantics/payouts.ts +9 -3
  164. package/src/semantics/prices.ts +0 -1
  165. package/src/semantics/products.ts +4 -14
  166. package/src/semantics/promotion_codes.ts +0 -1
  167. package/src/semantics/radar.ts +1 -3
  168. package/src/semantics/refunds.ts +2 -11
  169. package/src/semantics/shared.ts +350 -1382
  170. package/src/semantics/states.ts +109 -330
  171. package/src/semantics/subscriptions.ts +25 -145
  172. package/src/semantics/test_helpers.ts +109 -43
  173. package/src/semantics/transfers.ts +1 -3
  174. package/src/semantics/webhook_endpoints.ts +27 -0
  175. package/src/server.ts +0 -1
  176. package/dist/generated/pack-facts.json +0 -94
  177. package/dist/src/engine/billing.d.ts +0 -5
  178. package/dist/src/engine/billing.js +0 -6
  179. package/dist/src/engine/charges.d.ts +0 -6
  180. package/dist/src/engine/charges.js +0 -13
  181. package/dist/src/engine/connect-settings.d.ts +0 -16
  182. package/dist/src/engine/connect-settings.js +0 -26
  183. package/dist/src/engine/coupons.d.ts +0 -0
  184. package/dist/src/engine/coupons.js +0 -5
  185. package/dist/src/engine/credit-notes.d.ts +0 -1
  186. package/dist/src/engine/credit-notes.js +0 -5
  187. package/dist/src/engine/disputes.d.ts +0 -0
  188. package/dist/src/engine/disputes.js +0 -9
  189. package/dist/src/engine/entitlements.d.ts +0 -1
  190. package/dist/src/engine/entitlements.js +0 -4
  191. package/dist/src/engine/ephemeral-keys.d.ts +0 -0
  192. package/dist/src/engine/ephemeral-keys.js +0 -4
  193. package/dist/src/engine/front.d.ts +0 -5
  194. package/dist/src/engine/front.js +0 -25
  195. package/dist/src/engine/payment-links.d.ts +0 -1
  196. package/dist/src/engine/payment-links.js +0 -5
  197. package/dist/src/engine/plans.d.ts +0 -6
  198. package/dist/src/engine/plans.js +0 -26
  199. package/dist/src/engine/platform.d.ts +0 -39
  200. package/dist/src/engine/platform.js +0 -34
  201. package/dist/src/engine/products.d.ts +0 -0
  202. package/dist/src/engine/products.js +0 -4
  203. package/dist/src/engine/setup-intents.d.ts +0 -1
  204. package/dist/src/engine/setup-intents.js +0 -4
  205. package/dist/src/engine/subscription-schedules.d.ts +0 -3
  206. package/dist/src/engine/subscription-schedules.js +0 -1
  207. package/dist/src/engine/tax.d.ts +0 -7
  208. package/dist/src/engine/tax.js +0 -28
  209. package/dist/src/engine/terminal.d.ts +0 -4
  210. package/dist/src/engine/terminal.js +0 -20
  211. package/dist/src/engine/test-clocks.d.ts +0 -5
  212. package/dist/src/engine/test-clocks.js +0 -9
  213. package/dist/src/engine/tokens.d.ts +0 -7
  214. package/dist/src/engine/tokens.js +0 -20
  215. package/dist/src/engine/treasury.d.ts +0 -23
  216. package/dist/src/engine/treasury.js +0 -45
  217. package/dist/src/screens/connect-oauth.js +0 -333
  218. package/dist/src/semantics/disputes.d.ts +0 -2
  219. package/dist/src/semantics/disputes.js +0 -5
  220. package/dist/src/semantics/invoiceitems.d.ts +0 -3
  221. package/dist/src/semantics/invoiceitems.js +0 -59
  222. package/dist/src/semantics/setup_intents.d.ts +0 -3
  223. package/dist/src/semantics/setup_intents.js +0 -41
  224. package/dist/src/semantics/subscription_items.d.ts +0 -5
  225. package/dist/src/semantics/subscription_items.js +0 -55
  226. package/dist/src/semantics/subscription_schedules.d.ts +0 -4
  227. package/dist/src/semantics/subscription_schedules.js +0 -60
  228. package/src/engine/billing.ts +0 -16
  229. package/src/engine/charges.ts +0 -19
  230. package/src/engine/connect-settings.ts +0 -23
  231. package/src/engine/coupons.ts +0 -5
  232. package/src/engine/credit-notes.ts +0 -6
  233. package/src/engine/disputes.ts +0 -9
  234. package/src/engine/entitlements.ts +0 -5
  235. package/src/engine/ephemeral-keys.ts +0 -3
  236. package/src/engine/front.ts +0 -31
  237. package/src/engine/payment-links.ts +0 -6
  238. package/src/engine/plans.ts +0 -42
  239. package/src/engine/platform.ts +0 -47
  240. package/src/engine/products.ts +0 -3
  241. package/src/engine/setup-intents.ts +0 -5
  242. package/src/engine/subscription-schedules.ts +0 -10
  243. package/src/engine/tax.ts +0 -39
  244. package/src/engine/terminal.ts +0 -23
  245. package/src/engine/test-clocks.ts +0 -11
  246. package/src/engine/tokens.ts +0 -22
  247. package/src/engine/treasury.ts +0 -47
  248. package/src/screens/connect-oauth.tsx +0 -376
  249. package/src/semantics/disputes.ts +0 -12
  250. package/src/semantics/invoiceitems.ts +0 -59
  251. package/src/semantics/setup_intents.ts +0 -45
  252. package/src/semantics/subscription_items.ts +0 -54
  253. package/src/semantics/subscription_schedules.ts +0 -66
@@ -22,8 +22,7 @@ import type { Row } from '../engine/common.ts';
22
22
  import { publicBusinessName } from '../engine/display.ts';
23
23
  import { declineFor, nowUnix, paymentMethodSubObject, PLATFORM_ACCOUNT_ID } from '../engine/stripe.ts';
24
24
  import { cardAnswer, COUNTRIES, formOf, money, notFound } from './shared.tsx';
25
- import { afterSuccessOf, created, payerOf, payOpenInvoice } from '../semantics/shared.ts';
26
-
25
+ import { resourceView, afterSuccessOf, created, payerOf, payOpenInvoice } from '../semantics/shared.ts';
27
26
  const SUB = 'subscription';
28
27
  const PORTAL_OPERATION = { id: 'PostBillingPortalSessions' };
29
28
 
@@ -72,7 +71,7 @@ function planOf(ctx: HandlerContext, sub: Row): { name: string; price: string }
72
71
  function page(ctx: HandlerContext, id: string, session: Row, problem?: { error: string; values: Record<string, string> }): Response {
73
72
  const features = featuresOf(ctx, session);
74
73
  const customer = ctx.get('customer', String(session.customer)) ?? {};
75
- const subs = ctx.rows(SUB).filter((s) => s.customer === session.customer && s.status !== 'canceled' && s.status !== 'incomplete_expired');
74
+ const subs = ctx.rows(SUB).map((s) => ctx.expand(SUB, s)).filter((s) => s.customer === session.customer && s.status !== 'canceled' && s.status !== 'incomplete_expired');
76
75
  const plans: PortalItem[] = subs.map((s) => {
77
76
  const plan = planOf(ctx, s);
78
77
  const ending = s.cancel_at_period_end === true;
@@ -140,7 +139,7 @@ function page(ctx: HandlerContext, id: string, session: Row, problem?: { error:
140
139
 
141
140
  /** The customer's move on one of their subscriptions, then back to the portal. */
142
141
  async function move(ctx: HandlerContext, id: string, session: Row, subscription: string, kind: 'cancel' | 'renew'): Promise<Response> {
143
- const sub = ctx.get(SUB, subscription);
142
+ const sub = resourceView(ctx, SUB, subscription);
144
143
  if (!sub || sub.customer !== session.customer || sub.status === 'canceled') return gone();
145
144
  const back = new Response(null, { status: 303, headers: { location: `/p/session/${id}` } });
146
145
  if (kind === 'renew') {
@@ -177,7 +176,7 @@ async function updatePaymentMethod(ctx: HandlerContext, id: string, session: Row
177
176
  });
178
177
  const holder = ctx.get('customer', customer) ?? {};
179
178
  await ctx.write('customer', customer, { invoice_settings: { ...((holder.invoice_settings as Row | undefined) ?? {}), default_payment_method: pm.id } }, 'customer.update');
180
- for (const sub of ctx.rows(SUB).filter((s) => s.customer === customer && s.status !== 'canceled' && s.status !== 'incomplete_expired')) {
179
+ for (const sub of ctx.rows(SUB).map((s) => ctx.expand(SUB, s)).filter((s) => s.customer === customer && s.status !== 'canceled' && s.status !== 'incomplete_expired')) {
181
180
  await ctx.write(SUB, String(sub.id), { default_payment_method: pm.id }, 'subscription.update');
182
181
  }
183
182
  return new Response(null, { status: 303, headers: { location: `/p/session/${id}` } });
@@ -187,7 +186,7 @@ async function updatePaymentMethod(ctx: HandlerContext, id: string, session: Row
187
186
  async function payInvoice(ctx: HandlerContext, id: string, session: Row, invoiceId: string): Promise<Response> {
188
187
  const inv = ctx.get('invoice', invoiceId);
189
188
  if (!inv || inv.customer !== session.customer || inv.status !== 'open') return gone();
190
- const sub = typeof inv.subscription === 'string' ? ctx.get(SUB, inv.subscription) : undefined;
189
+ const sub = typeof inv.subscription === 'string' ? resourceView(ctx, SUB, inv.subscription) : undefined;
191
190
  const holder = ctx.get('customer', String(session.customer)) ?? {};
192
191
  const pm = (sub ? payerOf(ctx, sub) : undefined) ?? ((holder.invoice_settings as Row | undefined)?.default_payment_method as string | undefined);
193
192
  if (!pm) return page(ctx, id, session, { error: 'Add a payment method to pay this invoice.', values: {} });
@@ -212,4 +211,3 @@ export async function screen(ctx: HandlerContext): Promise<Response> {
212
211
  if (kind === 'pay') return payInvoice(ctx, id, session, form.invoice ?? '');
213
212
  return move(ctx, id, session, form.subscription ?? '', kind);
214
213
  }
215
-
@@ -0,0 +1,80 @@
1
+ import type { HandlerContext } from '@volter/world-core';
2
+ import type { Row } from '../engine/common.ts';
3
+ import { newCustomer } from '../engine/customers.ts';
4
+ import { CS } from '../engine/checkout.ts';
5
+ import { applyCouponDiscount, nowUnix, resolveTrial } from '../engine/stripe.ts';
6
+ import { cardAnswer, parseExpiry } from './shared.tsx';
7
+ import { completeSession, discountOf, created } from '../semantics/shared.ts';
8
+
9
+ function amount(value: number, currency: string): Row {
10
+ return { amount: new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(value / 100), minorUnitsAmount: value };
11
+ }
12
+
13
+ // source: archive:https://registry.npmjs.org/@stripe/stripe-js/-/stripe-js-8.6.0.tgz#sha256=13e61419e9076ab440cc636e34d3ea5df3fc62bbf2d6a80d3fd718e8cd47f0a2!/package/dist/stripe-js/checkout.d.ts "export interface StripeCheckoutSession"
14
+ // The SDK publishes its contract, not its private HTTP wire. This authored payment_pages wire uses the same session store and completion as hosted Checkout.
15
+ function view(ctx: HandlerContext, session: Row): Row {
16
+ const currency = String(session.currency ?? 'usd');
17
+ const lines = ((session.line_items as Row)?.data ?? []) as Row[];
18
+ const stored = ctx.row(CS, String(session.id)) ?? session;
19
+ const trial = session.mode === 'subscription' ? resolveTrial((stored._subscription_data as Row) ?? {}, nowUnix(ctx.occurredAt)) : undefined;
20
+ const customer = typeof session.customer === 'string' ? ctx.get('customer', session.customer) : undefined;
21
+ const discount = stored._discount as Row | undefined;
22
+ const promotion = typeof discount?.promotion_code === 'string' ? ctx.get('promotion_code', discount.promotion_code) : undefined;
23
+ const zero = amount(0, currency);
24
+ return {
25
+ id: session.id, billingAddress: null, businessName: null, canConfirm: session.status === 'open', currency,
26
+ currencyOptions: null, discountAmounts: promotion ? [{ ...amount(Number((session.total_details as Row)?.amount_discount ?? 0), currency), promotionCode: promotion.code }] : [],
27
+ email: customer?.email ?? session.customer_email ?? null, lastPaymentError: null,
28
+ lineItems: lines.map(line => ({ id: line.id, name: line.description, quantity: line.quantity, unitAmount: amount(Number(line.amount_subtotal) / Number(line.quantity || 1), currency), total: amount(Number(line.amount_total), currency) })),
29
+ livemode: false, minorUnitsAmountDivisor: 100, phoneNumber: null,
30
+ recurring: session.mode === 'subscription' ? { trial: trial ? { trialEnd: trial.end, trialPeriodDays: Math.ceil((trial.end - trial.start) / 86400) } : null } : null,
31
+ savedPaymentMethods: [], shipping: null, shippingAddress: null, shippingOptions: [],
32
+ status: session.status === 'complete' ? { type: 'complete', paymentStatus: session.payment_status } : { type: session.status },
33
+ tax: { status: 'ready' }, taxAmounts: [], taxIdInfo: null,
34
+ total: { appliedBalance: zero, balanceAppliedToNextInvoice: false, discount: amount(Number((session.total_details as Row)?.amount_discount ?? 0), currency), shippingRate: zero, subtotal: amount(Number(session.amount_subtotal ?? 0), currency), taxExclusive: zero, taxInclusive: zero, total: amount(trial ? 0 : Number(session.amount_total ?? 0), currency) },
35
+ };
36
+ }
37
+
38
+ const failed = (message: string, code: string | null = null): Response => Response.json({ type: 'error', error: { message, code } });
39
+
40
+ export async function screen(ctx: HandlerContext): Promise<Response> {
41
+ const path = new URL(ctx.call.request.url).pathname;
42
+ const id = path.split('/')[3]!;
43
+ const action = path.split('/')[4];
44
+ const params = ctx.params;
45
+ const session = ctx.get(CS, id);
46
+ // The private client secret is the browser's authority for this session; it never accepts a secret API key.
47
+ if (!session || session.ui_mode !== 'custom' || params.client_secret !== session.client_secret) return failed('Invalid Checkout Session client secret.');
48
+ if (action === 'init') return Response.json({ type: 'success', session: view(ctx, session) });
49
+ if (session.status !== 'open') return failed('This Checkout Session is no longer active.');
50
+ if (action === 'apply_promotion_code' || action === 'remove_promotion_code') {
51
+ if (action === 'apply_promotion_code' && session.allow_promotion_codes !== true) return failed('Promotion codes are not enabled for this Checkout Session.');
52
+ const promotion = action === 'apply_promotion_code' ? ctx.rows('promotion_code').find(p => p.code === params.promotion_code && p.active === true) : undefined;
53
+ if (action === 'apply_promotion_code' && !promotion) return failed('This promotion code is invalid.', 'invalidPromotionCode');
54
+ const discount = promotion ? discountOf(ctx, { discounts: [{ promotion_code: promotion.id }] }, nowUnix(ctx.occurredAt)) : undefined;
55
+ if (discount instanceof Response) return failed('This promotion code is invalid.', 'invalidPromotionCode');
56
+ const off = discount ? applyCouponDiscount(Number(session.amount_subtotal), discount.coupon) : 0;
57
+ const fields = { amount_total: Number(session.amount_subtotal) - off, total_details: { ...(session.total_details as Row), amount_discount: off }, discounts: discount ? [{ coupon: discount.coupon.id, promotion_code: promotion!.id }] : [], _discount: discount ? { coupon: discount.coupon.id, promotion_code: promotion!.id } : null };
58
+ const updated = await ctx.write(CS, id, fields, 'checkout.session.update');
59
+ return Response.json({ type: 'success', session: view(ctx, updated) });
60
+ }
61
+ if (action !== 'confirm') return failed('Unrecognized Checkout action.');
62
+ const values = (params.payment_details ?? {}) as Record<string, string>;
63
+ const stored = ctx.row(CS, id) ?? session;
64
+ const trial = session.mode === 'subscription' ? resolveTrial((stored._subscription_data as Row) ?? {}, nowUnix(ctx.occurredAt)) : undefined;
65
+ const refusal = cardAnswer(values, nowUnix(ctx.occurredAt), !trial);
66
+ if (refusal) return failed(refusal, 'paymentFailed');
67
+ const customer = typeof session.customer === 'string' ? ctx.get('customer', session.customer) : undefined;
68
+ const email = String(customer?.email ?? params.email ?? session.customer_email ?? '');
69
+ if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return failed('Your email address is incomplete.');
70
+ let existing = session;
71
+ if (!customer && session.mode === 'subscription') {
72
+ const cid = ctx.mint('customer');
73
+ const made = await created(ctx, 'customer', { id: cid, email }, { livemode: false, ...newCustomer(cid) });
74
+ existing = { ...session, customer: made.id };
75
+ }
76
+ const expiry = parseExpiry(values.cardExpiry)!;
77
+ const completed = await completeSession(ctx, id, existing, { customer: existing.customer, customer_details: { email, name: values.billingName ?? null, address: null, phone: null, tax_exempt: 'none', tax_ids: [] } }, { number: values.cardNumber.replace(/\D/g, ''), exp_month: expiry.month, exp_year: expiry.year });
78
+ if (completed instanceof Response) return failed('This Checkout Session is no longer active.');
79
+ return Response.json({ type: 'success', session: view(ctx, completed), return_url: session.return_url });
80
+ }
@@ -24,7 +24,6 @@ import { publicBusinessName } from '../engine/display.ts';
24
24
  import { applyCouponDiscount, nowUnix, PLATFORM_ACCOUNT_ID, resolveTrial } from '../engine/stripe.ts';
25
25
  import { cardAnswer, COUNTRIES, formOf, money, notFound, parseExpiry } from './shared.tsx';
26
26
  import { completeSession, created } from '../semantics/shared.ts';
27
-
28
27
  const CS = 'checkout.session';
29
28
  const COMPLETE_OPERATION = { id: 'PostCheckoutSessionsSession' };
30
29
 
@@ -202,4 +201,3 @@ export async function screen(ctx: HandlerContext): Promise<Response> {
202
201
  const full = { ...session, _subscription_data: stored._subscription_data, _price_data: stored._price_data, _merchant: merchantOf(ctx, stored), _coupon: typeof coupon === 'string' ? ctx.get('coupon', coupon) : undefined, _customer: typeof session.customer === 'string' ? ctx.get('customer', session.customer) : undefined };
203
202
  return request.method === 'GET' ? page(full, id, nowUnix(ctx.occurredAt)) : pay(ctx, id, full, values);
204
203
  }
205
-
@@ -31,9 +31,7 @@ import { formOf, notFound, seeOther, STRIPE_CONSENT_SKIN } from './shared.tsx';
31
31
  import { INDUSTRIES as MCCS } from '../engine/industries.ts';
32
32
  import { currentlyDue, type Form, type Kind, kindOf, OFFERED } from '../engine/onboarding.ts';
33
33
  import { created, syncExternals } from '../semantics/shared.ts';
34
-
35
34
  const LINK = 'account_link';
36
- const OPERATION = { id: 'PostAccountsAccount' };
37
35
 
38
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> });
39
37
 
@@ -126,7 +124,7 @@ async function submit(ctx: HandlerContext, link: Row, kind: Kind, v: Form, ip: s
126
124
  if (!account) return;
127
125
  const caps: Row = { ...((account.capabilities as Row | undefined) ?? {}) };
128
126
  for (const [cap, status] of Object.entries(caps)) {
129
- if (status !== 'active' && !ctx.legal('capability', 'status', OPERATION.id, String(status), 'active', cap, 'vendor')) caps[cap] = 'active';
127
+ if (status !== 'active') caps[cap] = 'active';
130
128
  }
131
129
  const due = currentlyDue(kind, v);
132
130
  const requirements = { ...((account.requirements as Row | undefined) ?? {}), currently_due: due, eventually_due: due, past_due: [], disabled_reason: null, current_deadline: null };
@@ -135,6 +133,7 @@ async function submit(ctx: HandlerContext, link: Row, kind: Kind, v: Form, ip: s
135
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 };
136
134
  const company = kind === 'company';
137
135
  const settings = (account.settings as Row | undefined) ?? {};
136
+ const individual = company ? null : await created(ctx, 'person', { ...person, account: id }, { relationship: { representative: true }, metadata: {} });
138
137
  await ctx.write('account', id, {
139
138
  details_submitted: true, charges_enabled: due.length === 0, payouts_enabled: due.length === 0, capabilities: caps, requirements,
140
139
  business_type: kind,
@@ -142,7 +141,7 @@ async function submit(ctx: HandlerContext, link: Row, kind: Kind, v: Form, ip: s
142
141
  settings: { ...settings, payments: { ...((settings.payments as Row | undefined) ?? {}), statement_descriptor: v.statement_descriptor } },
143
142
  ...(company
144
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 } }
145
- : { individual: { ...person, id: `person_${id}` } }),
144
+ : { individual: individual!.id }),
146
145
  tos_acceptance: { date: nowUnix(ctx.occurredAt), ip, user_agent: null },
147
146
  }, 'account.updated');
148
147
  if (company) {
@@ -196,4 +195,3 @@ export async function screen(ctx: HandlerContext): Promise<Response> {
196
195
  await submit(ctx, link, kind, form, request.headers.get('x-forwarded-for')?.split(',')[0]?.trim() || '127.0.0.1');
197
196
  return seeOther(String(link.return_url));
198
197
  }
199
-
@@ -1,6 +1,7 @@
1
1
  // THE DASHBOARD — dashboard.stripe.com's pages an operator sets what Stripe's API cannot (docs/contributing/architecture.md,
2
2
  // "Screens": a workspace): Public details (/settings/public) and the Connect OAuth settings
3
- // (/settings/connect/onboarding-options/oauth). Stripe's other Dashboard pages answer Stripe's 404 here.
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.
4
5
  //
5
6
  // THE DASHBOARD'S PUBLIC DETAILS — a workspace page (docs/contributing/architecture.md, "Screens") of the Dashboard
6
7
  // (manifest screen `dashboard`), at dashboard.stripe.com/settings/public: Settings → Business → Public details, where
@@ -44,69 +45,20 @@
44
45
  // page's own address and the save redirects to it.
45
46
  import type { HandlerContext } from '@volter/world-core';
46
47
  import { flowPage } from '@volter/world-ui';
48
+ import { VL } from '../engine/radar.ts';
49
+ import { nowUnix } from '../engine/stripe.ts';
47
50
  import type { Row } from '../engine/common.ts';
48
51
  import { platformAccountDefault } from '../engine/connect.ts';
49
- import { CONNECT_SETTINGS_PATH, type ConnectOAuthSettings, PLATFORM_CLIENT_ID, redirectUris, SETTINGS } from '../engine/connect-settings.ts';
50
52
  import { publicBusinessName } from '../engine/display.ts';
51
53
  import { PLATFORM_ACCOUNT_ID } from '../engine/stripe.ts';
52
54
  import { formOf, notFound } from './shared.tsx';
53
- import { connectOAuthSettings, created } from '../semantics/shared.ts';
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> });
54
58
 
55
59
  const PUBLIC_DETAILS_PATH = '/settings/public';
56
60
  const MAX_NAME = 5000;
57
61
 
58
- const SETTINGS_CSS = `
59
- * { box-sizing: border-box; }
60
- body { margin: 0; background: #f6f8fa; color: #1a1f36; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Ubuntu, sans-serif; font-size: 14px; }
61
- .co { max-width: 680px; margin: 48px auto; padding: 0 16px; }
62
- .co h1 { font-size: 28px; margin: 16px 0 4px; }
63
- .co-lead { color: #697386; margin: 0 0 24px; }
64
- .co-card { background: #ffffff; border: 1px solid #e3e8ee; border-radius: 8px; padding: 24px; margin-bottom: 16px; }
65
- .co-card h2 { font-size: 16px; margin: 0 0 12px; }
66
- .co-card label { display: block; font-weight: 600; margin-bottom: 6px; }
67
- .co-card small { display: block; color: #697386; margin-bottom: 8px; }
68
- .co-card textarea { width: 100%; min-height: 96px; padding: 8px 10px; border: 1px solid #c1c9d2; border-radius: 6px; font: 13px ui-monospace, Menlo, monospace; }
69
- .co-id { font: 14px ui-monospace, Menlo, monospace; background: #f6f8fa; border: 1px solid #e3e8ee; border-radius: 6px; padding: 6px 10px; display: inline-block; }
70
- .co-toggle { display: flex; gap: 8px; align-items: center; font-weight: 600; }
71
- .co-actions { display: flex; justify-content: flex-end; }
72
- .co-actions button { background: #635bff; color: #ffffff; border: 0; border-radius: 6px; padding: 8px 16px; font: inherit; font-weight: 600; cursor: pointer; }
73
- .co-notice { padding: 10px 12px; border-radius: 6px; margin-bottom: 16px; }
74
- .co-error { background: #fff0f3; color: #df1b41; }
75
- .co-saved { background: #e7f8ef; color: #0e6245; }
76
- .co-uris { margin: 0; padding-left: 18px; }
77
- `;
78
-
79
- function settingsPage(s: ConnectOAuthSettings, opts: { value?: string; enabled?: boolean; error?: string; saved?: boolean } = {}): Response {
80
- const enabled = opts.enabled ?? s.oauth_enabled;
81
- return flowPage({
82
- title: 'OAuth – Connect settings – Stripe',
83
- css: [SETTINGS_CSS],
84
- ...(opts.error ? { status: 400 } : {}),
85
- body: (
86
- <main className="co">
87
- <h1>OAuth</h1>
88
- <p className="co-lead">Onboard Standard accounts by sending them to Stripe's OAuth flow. Test mode.</p>
89
- {opts.error ? <p className="co-notice co-error" role="alert">{opts.error}</p> : null}
90
- {opts.saved ? <p className="co-notice co-saved" role="status">Your OAuth settings were saved.</p> : null}
91
- <section className="co-card">
92
- <h2>Test mode client ID</h2>
93
- <small>The client_id your platform sends to connect.stripe.com/oauth/authorize.</small>
94
- <code className="co-id" data-testid="connect-client-id">{PLATFORM_CLIENT_ID}</code>
95
- <p data-testid="connect-oauth-status">{s.oauth_enabled ? 'OAuth is enabled.' : 'OAuth is disabled.'}</p>
96
- {s.redirect_uris.length ? <ul className="co-uris">{s.redirect_uris.map((u) => <li key={u} data-testid="connect-redirect-uri">{u}</li>)}</ul> : <p>No redirect URIs.</p>}
97
- </section>
98
- <form className="co-card" method="post">
99
- <label className="co-toggle" htmlFor="oauth_enabled"><input type="checkbox" id="oauth_enabled" name="oauth_enabled" value="on" defaultChecked={enabled} /> Enable OAuth for Standard accounts</label>
100
- <label htmlFor="redirect_uris">Redirect URIs</label>
101
- <small>One per line. The first is used when a request names none. http and localhost are allowed in test mode.</small>
102
- <textarea id="redirect_uris" name="redirect_uris" defaultValue={opts.value ?? s.redirect_uris.join('\n')} />
103
- <div className="co-actions"><button type="submit">Save</button></div>
104
- </form>
105
- </main>
106
- ),
107
- });
108
- }
109
-
110
62
  const DETAILS_CSS = `
111
63
  * { box-sizing: border-box; }
112
64
  body { margin: 0; background: #f6f8fa; color: #1a1f36; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Ubuntu, sans-serif; font-size: 14px; }
@@ -161,20 +113,6 @@ function nameRefused(name: string): string | undefined {
161
113
  return !name ? 'Enter your business name.' : name.length > MAX_NAME ? `Your business name must be at most ${MAX_NAME} characters.` : undefined;
162
114
  }
163
115
 
164
- async function settings(ctx: HandlerContext): Promise<Response> {
165
- const request = ctx.call.request;
166
- const url = new URL(request.url);
167
- if (request.method !== 'GET' && request.method !== 'POST') return notFound();
168
- const form = request.method === 'POST' ? await formOf(request.clone()) : {};
169
- const held = connectOAuthSettings(ctx);
170
- if (request.method === 'GET') return settingsPage(held, { saved: url.searchParams.has('saved') });
171
- const enabled = form.oauth_enabled === 'on' || form.oauth_enabled === 'true';
172
- const parsed = redirectUris(form.redirect_uris ?? '');
173
- if ('refused' in parsed) return settingsPage(held, { value: form.redirect_uris ?? '', enabled, error: parsed.refused });
174
- await ctx.record(SETTINGS, { oauth_enabled: enabled, redirect_uris: parsed.uris }, PLATFORM_CLIENT_ID);
175
- return new Response(null, { status: 303, headers: { location: '?saved=1' } });
176
- }
177
-
178
116
  async function publicDetails(ctx: HandlerContext): Promise<Response> {
179
117
  const request = ctx.call.request;
180
118
  const url = new URL(request.url);
@@ -189,10 +127,54 @@ async function publicDetails(ctx: HandlerContext): Promise<Response> {
189
127
  return new Response(null, { status: 303, headers: { location: '?saved=1' } });
190
128
  }
191
129
 
192
- /** dashboard.stripe.com's pages: Public details and the Connect OAuth settings; Stripe's 404 for any other. */
130
+ /** Dashboard settings, Radar list creation and test Issuing funding; other pages answer Stripe's 404. */
193
131
  export async function screen(ctx: HandlerContext): Promise<Response> {
194
132
  const path = new URL(ctx.call.request.url).pathname.replace(/\/+$/, '');
195
- if (path === PUBLIC_DETAILS_PATH) return publicDetails(ctx);
196
- if (CONNECT_SETTINGS_PATH.test(path)) return settings(ctx);
197
- return notFound();
133
+ // The twin's declared workspace entry opens its implemented Public details page; this authored choice does not
134
+ // reproduce Stripe's Dashboard home. The request's public base retains the World mount when the console frames it.
135
+ if (path === '' && ['GET', 'HEAD'].includes(ctx.call.request.method)) return redirect(`${ctx.publicBase}${PUBLIC_DETAILS_PATH}`);
136
+ // source: https://docs.stripe.com/radar/lists "Use the Dashboard or the API to create lists."
137
+ // source: https://docs.stripe.com/radar/lists "Enter a name for the list"
138
+ // source: https://docs.stripe.com/radar/lists "Select the type of list to create."
139
+ // source: https://docs.stripe.com/radar/lists "Click Add to save your new list."
140
+ // Where the documentation stops: it names Dashboard controls, not their browser request routes or form wire.
141
+ // This authored page uses /radar/lists for GET and POST, /radar/lists/{id} for its redirect/detail address,
142
+ // and a 303 redirect after saving. The HTML field names name, alias and item_type are the twin's choices;
143
+ // item_type values are the pinned API's enums, not published Dashboard form values. The alias fallback's
144
+ // lowercase/underscore algorithm is also authored; Stripe says it generates an alias but gives no algorithm.
145
+ if (/^\/radar\/lists(?:\/[^/]+)?$/.test(path)) {
146
+ if (ctx.call.request.method === 'POST') {
147
+ const form = await formOf(ctx.call.request);
148
+ const types = ['string', 'case_sensitive_string', 'card_fingerprint', 'card_bin', 'customer_id', 'email', 'ip_address', 'country', 'sepa_debit_fingerprint', 'us_bank_account_fingerprint'];
149
+ if (!form.name?.trim() || !types.includes(form.item_type ?? '')) return invalid('Enter a name and select a list type.');
150
+ const alias = form.alias?.trim() || form.name.toLowerCase().replace(/[^a-z0-9]+/g, '_');
151
+ // Where the page stops: an operator has no login identity in the World; created_by is its synthetic operator.
152
+ const list = await created(ctx, VL, { name: form.name.trim(), alias, item_type: form.item_type }, {
153
+ created_by: 'twin', livemode: false, metadata: {},
154
+ list_items: { object: 'list', data: [], has_more: false, url: '/v1/radar/value_list_items' },
155
+ });
156
+ 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');
157
+ return redirect(`/radar/lists/${list.id}`);
158
+ }
159
+ 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> });
160
+ }
161
+ // source: https://docs.stripe.com/issuing/testing "Before you create test transactions, you must add test funds to the Issuing balance on your account."
162
+ // source: https://docs.stripe.com/issuing/testing "You can create test top-ups in the Dashboard, or with the Top-ups API"
163
+ // Where the documentation stops: it states the Dashboard funding act, not a browser request path or form wire.
164
+ // The authored GET/POST address and 303 redirect are /test/issuing/balance. The form name amount, its positive
165
+ // integer validation and its units (USD cents for this US sandbox) are the twin's choices, not published form
166
+ // fields. Its POST records the balance change in the vendor ledger, with source=null (permitted by the pinned
167
+ // schema) and immediate availability for this sandbox control; the page states neither that source nor timing.
168
+ // No private Topup object is seeded or answered, and the uncalled Top-ups API remains the gap.
169
+ if (path === '/test/issuing/balance') {
170
+ if (ctx.call.request.method === 'POST') {
171
+ const form = await formOf(ctx.call.request);
172
+ const amount = Number(form.amount);
173
+ if (!Number.isInteger(amount) || amount <= 0) return invalid('Enter a positive amount in cents.');
174
+ 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: [] });
175
+ return redirect('/test/issuing/balance');
176
+ }
177
+ 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> });
178
+ }
179
+ return path === PUBLIC_DETAILS_PATH ? publicDetails(ctx) : notFound();
198
180
  }
@@ -1,150 +1,82 @@
1
- // STRIPE.JS — the client library an application's page loads from js.stripe.com, served as a content screen
2
- // (docs/contributing/architecture.md, "Screens": a widget the vendor ships into the application's own page "is not a screen: it is a client library,
3
- // twinned from its published types as an SDK surface is"). Written from @stripe/stripe-js's published loader and types
4
- // (7.x: dist/index.mjs, dist/stripe-js/stripe.d.ts, dist/stripe-js/hosted-checkout.d.ts), nothing of Stripe's script
5
- // copied:
6
- // - the loader injects https://js.stripe.com/<release train>/stripe.js (or /v3/), and accepts a script already on the
7
- // page whose src matches `/^https:\/\/js\.stripe\.com\/(v3|[a-z]+)\/stripe\.js(\?.*)?$/` (so /v3/stripe.js too);
8
- // it resolves `window.Stripe`, reads `Stripe.version` (3 is the v3 URL, otherwise the train's name) and calls the
9
- // instance's `_registerWrapper` when it has one;
10
- // - `stripe.redirectToCheckout({ sessionId })` sends the customer to that Checkout Session's page (hosted-checkout.d.ts,
11
- // RedirectToCheckoutServerOptions), which this twin serves at checkout.stripe.com/c/pay/{id} (screens/checkout.tsx),
12
- // and its promise settles only on failure (`Promise<never | {error}>`);
13
- // - `stripe.collectFinancialConnectionsAccounts({ clientSecret })` loads the Financial Connections authentication
14
- // flow for the session whose client_secret it is (stripe.d.ts; docs.stripe.com/js/financial_connections/
15
- // collect_financial_connections_accounts: "it will load the Authentication Flow, an on-page modal UI"). Where the
16
- // twin decides: it navigates to the flow's page, js.stripe.com/v3/financial-connections/{client_secret}
17
- // (screens/financial-connections.tsx), instead of a modal, so its promise does not settle; the flow returns the
18
- // holder to the session's return_url, where the application reads the session's accounts.
19
- // - `stripe.elements({ clientSecret?, ... })` groups elements; `elements.create('payment' | 'card')` makes one, which
20
- // `mount`s into the page and emits `ready` and `change` ({ complete, empty }); `elements.submit()` checks the fields
21
- // (stripe.d.ts: StripeElements, StripePaymentElement). Where the twin decides: an element is the page's own card
22
- // fields (number, expiry, CVC), not Stripe's iframe;
23
- // - `stripe.confirmPayment({ elements, clientSecret?, confirmParams: { return_url }, redirect? })` and
24
- // `stripe.confirmSetup(...)` confirm the intent whose client secret it is with the card the element holds
25
- // (`POST /v1/payment_intents/{id}/confirm`, `/v1/setup_intents/{id}/confirm`, with the publishable key), then send
26
- // the customer to `return_url` with `payment_intent` (or `setup_intent`), its client secret and `redirect_status`,
27
- // or, with `redirect: 'if_required'`, resolve `{ paymentIntent }` (`{ setupIntent }`); a refusal resolves
28
- // `{ error }` (docs.stripe.com/js/payment_intents/confirm_payment).
29
- // Demand: Dub's upgrade button (apps/web/ui/workspaces/upgrade-plan-button.tsx) and cal.com's sign-up call
30
- // redirectToCheckout; cal.com's booking payment form (packages/platform/atoms/event-types/payments/StripePaymentForm.tsx)
31
- // and Twenty's billing forms (AddPaymentMethodForm.tsx, useSubmitSubscriptionPayment.ts) confirm intents through
32
- // Elements. Every other member of the library throws, naming itself, when called: an application reaching one finds the
33
- // gap instead of a silent no-op.
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.
34
5
  import type { HandlerContext } from '@volter/world-core';
35
6
  import { notFound } from './shared.tsx';
36
7
 
37
8
  /** The release train a script path names: `/v3`, `/v3/` or `/v3/stripe.js` → 3, `/<train>/stripe.js` → the train. */
38
9
  function stripeJsTrain(pathname: string): 3 | string | undefined {
39
- if (/^\/v3(\/|\/stripe\.js)?$/.test(pathname)) return 3;
40
- return /^\/([a-z]+)\/stripe\.js$/.exec(pathname)?.[1];
10
+ return /^\/v3(\/|\/stripe\.js)?$/.test(pathname) ? 3 : /^\/([a-z]+)\/stripe\.js$/.exec(pathname)?.[1];
41
11
  }
42
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>"
43
14
  function script(version: 3 | string): string {
44
15
  return `(function () {
45
16
  var version = ${JSON.stringify(version)};
46
17
  function gap(name) {
47
18
  return function () { throw new Error("Stripe.js (Volter twin): stripe." + name + " is not modelled by this twin yet"); };
48
19
  }
49
- function Stripe(publishableKey) {
50
- if (typeof publishableKey !== "string" || !publishableKey) throw new Error("Stripe.js (Volter twin): Stripe() needs a publishable key");
51
- var api = "https://api.stripe.com/v1";
52
- // the card fields every mounted element holds, the last mounted first
53
- var mounted = [];
54
- function cardOf(elements) {
55
- var el = (elements && elements._mounted && elements._mounted[elements._mounted.length - 1]) || mounted[mounted.length - 1];
56
- if (!el) return null;
57
- var exp = String(el.expiry.value || "").split("/");
58
- 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 || "") };
59
- }
60
- function form(fields) {
61
- return Object.keys(fields).filter(function (k) { return fields[k] !== undefined && fields[k] !== ""; }).map(function (k) { return encodeURIComponent(k) + "=" + encodeURIComponent(fields[k]); }).join("&");
62
- }
63
- function confirm(kind, options) {
64
- options = options || {};
65
- var elements = options.elements;
66
- var secret = options.clientSecret || (elements && elements._options && elements._options.clientSecret);
67
- if (typeof secret !== "string" || secret.indexOf("_secret_") < 0) return Promise.resolve({ error: { type: "invalid_request_error", message: "A clientSecret is required." } });
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;
68
25
  var id = secret.split("_secret_")[0];
69
- var card = cardOf(elements);
70
- var params = options.confirmParams || {};
71
- var fields = { client_secret: secret };
72
- if (card) { fields["payment_method_data[type]"] = "card"; fields["payment_method_data[card][number]"] = card.number; fields["payment_method_data[card][exp_month]"] = card.exp_month; fields["payment_method_data[card][exp_year]"] = card.exp_year; fields["payment_method_data[card][cvc]"] = card.cvc; }
73
- if (params.return_url) fields.return_url = params.return_url;
74
- var path = kind === "payment" ? "/payment_intents/" : "/setup_intents/";
75
- return fetch(api + path + encodeURIComponent(id) + "/confirm", { method: "POST", headers: { authorization: "Bearer " + publishableKey, "content-type": "application/x-www-form-urlencoded" }, body: form(fields) })
76
- .then(function (res) { return res.json().then(function (body) { return { ok: res.ok, body: body }; }); })
77
- .then(function (r) {
78
- if (!r.ok) return { error: r.body.error || { type: "api_error", message: "The confirmation failed." } };
79
- var key = kind === "payment" ? "paymentIntent" : "setupIntent";
80
- var out = {}; out[key] = r.body;
81
- if (options.redirect === "if_required" || !params.return_url) return out;
82
- var q = kind === "payment" ? "payment_intent" : "setup_intent";
83
- var target = new URL(params.return_url);
84
- target.searchParams.set(q, r.body.id);
85
- target.searchParams.set(q + "_client_secret", secret);
86
- target.searchParams.set("redirect_status", r.body.status === "succeeded" ? "succeeded" : r.body.status === "processing" ? "processing" : "failed");
87
- window.location.assign(target.toString());
88
- return new Promise(function () {});
89
- });
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
+ });
90
33
  }
91
- function element(type, group) {
92
- var handlers = {};
93
- function emit(name, event) { (handlers[name] || []).forEach(function (h) { h(event); }); }
94
- var el = {
95
- _type: type,
96
- mount: function (target) {
97
- var node = typeof target === "string" ? document.querySelector(target) : target;
98
- if (!node) throw new Error("Stripe.js (Volter twin): mount() found no element " + String(target));
99
- node.innerHTML = "";
100
- var wrap = document.createElement("div");
101
- wrap.setAttribute("data-volter-twin-element", type);
102
- 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; }
103
- el.number = input("cardnumber", "Card number");
104
- el.expiry = input("exp-date", "MM / YY");
105
- el.cvc = input("cvc", "CVC");
106
- node.appendChild(wrap);
107
- mounted.push(el); group._mounted.push(el);
108
- setTimeout(function () { emit("ready", { elementType: type }); }, 0);
109
- },
110
- on: function (name, handler) { (handlers[name] = handlers[name] || []).push(handler); return el; },
111
- off: function (name, handler) { handlers[name] = (handlers[name] || []).filter(function (h) { return h !== handler; }); return el; },
112
- update: function () {}, focus: function () { if (el.number) el.number.focus(); }, blur: function () {},
113
- clear: function () { [el.number, el.expiry, el.cvc].forEach(function (i) { if (i) i.value = ""; }); change(); },
114
- unmount: function () {}, destroy: function () { mounted = mounted.filter(function (m) { return m !== el; }); }
115
- };
116
- function change() {
117
- var c = { number: el.number && el.number.value, expiry: el.expiry && el.expiry.value, cvc: el.cvc && el.cvc.value };
118
- emit("change", { elementType: type, empty: !c.number && !c.expiry && !c.cvc, complete: !!(c.number && c.expiry && c.cvc), value: { type: "card" } });
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
+ });
119
43
  }
120
- return el;
121
- }
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");
122
73
  var known = {
74
+ initCheckout: initCheckout,
123
75
  _registerWrapper: function () {},
124
- elements: function (options) {
125
- var group = { _options: options || {}, _mounted: [], _created: {} };
126
- group.create = function (type) { var e = element(type, group); group._created[type] = e; return e; };
127
- group.getElement = function (type) { return group._created[typeof type === "string" ? type : "card"] || null; };
128
- group.update = function (next) { group._options = Object.assign({}, group._options, next || {}); };
129
- group.fetchUpdates = function () { return Promise.resolve({}); };
130
- group.submit = function () {
131
- var card = cardOf(group);
132
- if (!card || !card.number || !card.exp_month || !card.cvc) return Promise.resolve({ error: { type: "validation_error", message: "Your card details are incomplete." } });
133
- return Promise.resolve({});
134
- };
135
- return group;
136
- },
137
- confirmPayment: function (options) { return confirm("payment", options); },
138
- confirmSetup: function (options) { return confirm("setup", options); },
139
76
  redirectToCheckout: function (options) {
140
77
  if (!options || typeof options.sessionId !== "string" || !options.sessionId) return gap("redirectToCheckout without a sessionId")();
141
78
  window.location.assign("https://checkout.stripe.com/c/pay/" + encodeURIComponent(options.sessionId));
142
79
  return new Promise(function () {});
143
- },
144
- collectFinancialConnectionsAccounts: function (options) {
145
- if (!options || typeof options.clientSecret !== "string" || !options.clientSecret) return gap("collectFinancialConnectionsAccounts without a clientSecret")();
146
- window.location.assign("https://js.stripe.com/v3/financial-connections/" + encodeURIComponent(options.clientSecret));
147
- return new Promise(function () {});
148
80
  }
149
81
  };
150
82
  return new Proxy(known, {
@@ -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
+ }
@@ -3,7 +3,6 @@
3
3
  import type { HandlerContext } from '@volter/world-core';
4
4
  import { nowUnix } from '../engine/stripe.ts';
5
5
  import { accountMissing, fail } from './shared.ts';
6
-
7
6
  // ── hosted onboarding links and embedded-component sessions ──
8
7
  export async function PostAccountLinks(ctx: HandlerContext): Promise<Response> {
9
8
  const account = typeof ctx.params.account === 'string' ? ctx.params.account : '';