@fantastic.dev/better-stripe 0.4.0 → 0.5.0

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 (162) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/MIGRATION.md +135 -0
  3. package/README.md +23 -2
  4. package/dist/client/billing/checkout.d.ts.map +1 -1
  5. package/dist/client/billing/checkout.js +22 -8
  6. package/dist/client/billing/checkout.js.map +1 -1
  7. package/dist/client/billing/subscriptionRouting.d.ts +24 -0
  8. package/dist/client/billing/subscriptionRouting.d.ts.map +1 -0
  9. package/dist/client/billing/subscriptionRouting.js +19 -0
  10. package/dist/client/billing/subscriptionRouting.js.map +1 -0
  11. package/dist/client/billing/subscriptions.d.ts +6 -6
  12. package/dist/client/billing/subscriptions.d.ts.map +1 -1
  13. package/dist/client/billing/subscriptions.js +31 -12
  14. package/dist/client/billing/subscriptions.js.map +1 -1
  15. package/dist/client/connect/disputes.js.map +1 -1
  16. package/dist/client/constants.js +1 -1
  17. package/dist/client/core/accountLinks.d.ts +1 -0
  18. package/dist/client/core/accountLinks.d.ts.map +1 -1
  19. package/dist/client/core/accountLinks.js +0 -6
  20. package/dist/client/core/accountLinks.js.map +1 -1
  21. package/dist/client/core/accountStatus.d.ts.map +1 -1
  22. package/dist/client/core/accountStatus.js +23 -8
  23. package/dist/client/core/accountStatus.js.map +1 -1
  24. package/dist/client/index.d.ts +5 -2
  25. package/dist/client/index.d.ts.map +1 -1
  26. package/dist/client/index.js.map +1 -1
  27. package/dist/client/types/documents.d.ts +1 -0
  28. package/dist/client/types/documents.d.ts.map +1 -1
  29. package/dist/client/types/events.d.ts +2 -1
  30. package/dist/client/types/events.d.ts.map +1 -1
  31. package/dist/client/types/options.d.ts +7 -0
  32. package/dist/client/types/options.d.ts.map +1 -1
  33. package/dist/client/utils/stripeDashboardUrl.d.ts.map +1 -1
  34. package/dist/client/utils/webhookEndpoints.d.ts.map +1 -1
  35. package/dist/client/webhooks/handler.d.ts.map +1 -1
  36. package/dist/client/webhooks/handler.js +35 -1
  37. package/dist/client/webhooks/handler.js.map +1 -1
  38. package/dist/client/webhooks/hooks.d.ts +1 -1
  39. package/dist/client/webhooks/hooks.d.ts.map +1 -1
  40. package/dist/client/webhooks/hooks.js +5 -1
  41. package/dist/client/webhooks/hooks.js.map +1 -1
  42. package/dist/client/webhooks/processors.d.ts.map +1 -1
  43. package/dist/client/webhooks/processors.js +12 -2
  44. package/dist/client/webhooks/processors.js.map +1 -1
  45. package/dist/client/webhooks/v2.d.ts.map +1 -1
  46. package/dist/client/webhooks/v2.js +11 -3
  47. package/dist/client/webhooks/v2.js.map +1 -1
  48. package/dist/component/_generated/component.d.ts +8 -0
  49. package/dist/component/_generated/component.d.ts.map +1 -1
  50. package/dist/component/_generated/server.d.ts +7 -1
  51. package/dist/component/_generated/server.d.ts.map +1 -1
  52. package/dist/component/_generated/server.js +2 -1
  53. package/dist/component/_generated/server.js.map +1 -1
  54. package/dist/component/billing/mutations.d.ts +29 -28
  55. package/dist/component/billing/mutations.d.ts.map +1 -1
  56. package/dist/component/billing/mutations.js +1 -0
  57. package/dist/component/billing/mutations.js.map +1 -1
  58. package/dist/component/billing/queries.d.ts +138 -131
  59. package/dist/component/billing/queries.d.ts.map +1 -1
  60. package/dist/component/billing/schema.d.ts +67 -65
  61. package/dist/component/billing/schema.d.ts.map +1 -1
  62. package/dist/component/billing/validators.d.ts +94 -91
  63. package/dist/component/billing/validators.d.ts.map +1 -1
  64. package/dist/component/billing/validators.js +1 -0
  65. package/dist/component/billing/validators.js.map +1 -1
  66. package/dist/component/connect/mutations.d.ts +37 -37
  67. package/dist/component/connect/queries.d.ts +115 -115
  68. package/dist/component/connect/queries.d.ts.map +1 -1
  69. package/dist/component/connect/queries.js +34 -71
  70. package/dist/component/connect/queries.js.map +1 -1
  71. package/dist/component/connect/schema.d.ts +73 -73
  72. package/dist/component/connect/validators.d.ts +119 -119
  73. package/dist/component/connect/validators.d.ts.map +1 -1
  74. package/dist/component/convex.config.d.ts +1 -0
  75. package/dist/component/convex.config.d.ts.map +1 -1
  76. package/dist/component/convex.config.js +2 -1
  77. package/dist/component/convex.config.js.map +1 -1
  78. package/dist/component/core/mutations.d.ts +12 -12
  79. package/dist/component/core/queries.d.ts +34 -34
  80. package/dist/component/core/schema.d.ts +11 -11
  81. package/dist/component/core/validators.d.ts +14 -14
  82. package/dist/component/core/validators.d.ts.map +1 -1
  83. package/dist/component/lib/fees.d.ts +12 -12
  84. package/dist/component/lib/fees.d.ts.map +1 -1
  85. package/dist/component/lib/fees.js +1 -1
  86. package/dist/component/lib/reversals.js.map +1 -1
  87. package/dist/component/products/mutations.d.ts +7 -7
  88. package/dist/component/products/queries.d.ts +33 -33
  89. package/dist/component/products/schema.d.ts +15 -15
  90. package/dist/component/products/validators.d.ts +16 -16
  91. package/dist/component/products/validators.d.ts.map +1 -1
  92. package/dist/component/schema.d.ts +173 -171
  93. package/dist/component/schema.d.ts.map +1 -1
  94. package/dist/component/webhooks/mutations.d.ts +3 -3
  95. package/dist/component/webhooks/queries.d.ts +12 -12
  96. package/dist/component/webhooks/schema.d.ts +7 -7
  97. package/dist/component/webhooks/validators.d.ts +10 -10
  98. package/dist/component/webhooks/validators.d.ts.map +1 -1
  99. package/dist/react/components/AccountCreateCard.js.map +1 -1
  100. package/dist/react/components/BuyerBillingView.js +1 -1
  101. package/dist/react/components/CheckoutSessionProviderWithKey.d.ts +1 -1
  102. package/dist/react/components/DisputeDetail.js +1 -1
  103. package/dist/react/components/DisputesList.js +1 -1
  104. package/dist/react/components/EarningsSummary.js +1 -1
  105. package/dist/react/components/EmbeddedCheckout.d.ts.map +1 -1
  106. package/dist/react/components/EmbeddedCheckout.js +1 -1
  107. package/dist/react/components/EmbeddedCheckout.js.map +1 -1
  108. package/dist/react/components/PayoutSchedule.js +1 -1
  109. package/dist/react/components/SplitBreakdown.js +1 -1
  110. package/dist/react/components/StripeProvider.d.ts +1 -1
  111. package/dist/react/components/StripeProviderWithKey.d.ts +1 -1
  112. package/dist/react/hooks/useAccount.d.ts.map +1 -1
  113. package/dist/react/hooks/useAccountOnboarding.d.ts.map +1 -1
  114. package/dist/react/hooks/useCheckout.d.ts.map +1 -1
  115. package/dist/react/hooks/useDisputeWithCountdown.d.ts.map +1 -1
  116. package/dist/react/hooks/useDisputes.d.ts.map +1 -1
  117. package/dist/react/hooks/useEarnings.d.ts.map +1 -1
  118. package/dist/react/hooks/useInvoices.d.ts.map +1 -1
  119. package/dist/react/hooks/usePaymentMethodActions.js.map +1 -1
  120. package/dist/react/hooks/usePaymentMethods.d.ts.map +1 -1
  121. package/dist/react/hooks/usePrices.d.ts.map +1 -1
  122. package/dist/react/hooks/useProducts.d.ts.map +1 -1
  123. package/dist/react/hooks/useSplitBreakdown.d.ts.map +1 -1
  124. package/dist/react/hooks/useSubscription.d.ts.map +1 -1
  125. package/dist/react/hooks/useSubscriptions.d.ts.map +1 -1
  126. package/dist/react/lib/stripe-element-styles.d.ts.map +1 -1
  127. package/dist/react/lib/stripe-element-styles.js.map +1 -1
  128. package/package.json +25 -23
  129. package/src/client/billing/checkout.test.ts +62 -14
  130. package/src/client/billing/checkout.ts +26 -10
  131. package/src/client/billing/subscriptionRouting.ts +25 -0
  132. package/src/client/billing/subscriptions.test.ts +90 -28
  133. package/src/client/billing/subscriptions.ts +53 -18
  134. package/src/client/constants.ts +1 -1
  135. package/src/client/core/accountLinks.test.ts +20 -13
  136. package/src/client/core/accountLinks.ts +2 -10
  137. package/src/client/core/accountStatus.test.ts +67 -0
  138. package/src/client/core/accountStatus.ts +30 -13
  139. package/src/client/helpers.test.ts +2 -2
  140. package/src/client/index.ts +49 -44
  141. package/src/client/stripe-version.test.ts +47 -0
  142. package/src/client/types/documents.ts +1 -0
  143. package/src/client/types/events.ts +2 -1
  144. package/src/client/types/options.ts +7 -0
  145. package/src/client/webhooks/handler.ts +38 -1
  146. package/src/client/webhooks/hooks.ts +3 -0
  147. package/src/client/webhooks/processors.test.ts +201 -40
  148. package/src/client/webhooks/processors.ts +21 -17
  149. package/src/client/webhooks/retry.integration.test.ts +196 -0
  150. package/src/client/webhooks/v2.test.ts +36 -0
  151. package/src/client/webhooks/v2.ts +14 -4
  152. package/src/component/_generated/component.ts +15 -28
  153. package/src/component/_generated/server.ts +9 -2
  154. package/src/component/billing/mutations.ts +1 -0
  155. package/src/component/billing/validators.ts +1 -0
  156. package/src/component/config-queries.test.ts +8 -4
  157. package/src/component/connect/queries.ts +31 -66
  158. package/src/component/convex.config.ts +2 -1
  159. package/src/component/earnings-totals.test.ts +27 -1
  160. package/src/component/lib/fees.ts +1 -1
  161. package/src/react/components/EmbeddedCheckout.render.test.tsx +57 -0
  162. package/src/react/components/EmbeddedCheckout.tsx +1 -0
@@ -45,7 +45,8 @@ function makeStripe() {
45
45
  type MockStripe = ReturnType<typeof makeStripe>;
46
46
 
47
47
  // Cast helper — the mock only implements the slice of Stripe these functions use.
48
- const asStripe = (s: MockStripe) => s as unknown as Parameters<typeof createAccountLink>[0];
48
+ const asStripe = (s: MockStripe) =>
49
+ s as unknown as Parameters<typeof createAccountLink>[0];
49
50
 
50
51
  describe("createAccountLink", () => {
51
52
  it("passes through params and returns the url", async () => {
@@ -94,7 +95,6 @@ describe("createV2AccountLink", () => {
94
95
  use_case: {
95
96
  type: "account_onboarding",
96
97
  account_onboarding: {
97
- configurations: ["merchant"],
98
98
  refresh_url: "https://refresh",
99
99
  return_url: "https://return",
100
100
  },
@@ -122,7 +122,6 @@ describe("createV2AccountLink", () => {
122
122
  use_case: {
123
123
  type: "account_update",
124
124
  account_update: {
125
- configurations: ["recipient"],
126
125
  refresh_url: "https://refresh",
127
126
  return_url: "https://return",
128
127
  },
@@ -130,7 +129,7 @@ describe("createV2AccountLink", () => {
130
129
  });
131
130
  });
132
131
 
133
- it("defaults configurations to an empty array for account_update too", async () => {
132
+ it("omits configurations for account_update", async () => {
134
133
  const stripe = makeStripe();
135
134
  stripe.v2.core.accountLinks.create.mockResolvedValue({
136
135
  url: "https://v2",
@@ -145,10 +144,10 @@ describe("createV2AccountLink", () => {
145
144
  });
146
145
 
147
146
  const arg = stripe.v2.core.accountLinks.create.mock.calls[0][0];
148
- expect(arg.use_case.account_update.configurations).toEqual([]);
147
+ expect(arg.use_case.account_update).not.toHaveProperty("configurations");
149
148
  });
150
149
 
151
- it("defaults configurations to an empty array when omitted", async () => {
150
+ it("omits configurations for account_onboarding", async () => {
152
151
  const stripe = makeStripe();
153
152
  stripe.v2.core.accountLinks.create.mockResolvedValue({
154
153
  url: "https://v2",
@@ -163,7 +162,9 @@ describe("createV2AccountLink", () => {
163
162
  });
164
163
 
165
164
  const arg = stripe.v2.core.accountLinks.create.mock.calls[0][0];
166
- expect(arg.use_case.account_onboarding.configurations).toEqual([]);
165
+ expect(arg.use_case.account_onboarding).not.toHaveProperty(
166
+ "configurations",
167
+ );
167
168
  });
168
169
  });
169
170
 
@@ -283,9 +284,11 @@ describe("getAccountLinkWithStatus", () => {
283
284
  expect(result).toEqual({ url: "https://onboard", linkType: "onboarding" });
284
285
  // login link must NOT be used while requirements are outstanding
285
286
  expect(stripe.accounts.createLoginLink).not.toHaveBeenCalled();
286
- // applied_configurations should flow into the onboarding link
287
+ // Stripe 23 collects requirements without a configurations parameter.
287
288
  const arg = stripe.v2.core.accountLinks.create.mock.calls[0][0];
288
- expect(arg.use_case.account_onboarding.configurations).toEqual(["merchant"]);
289
+ expect(arg.use_case.account_onboarding).not.toHaveProperty(
290
+ "configurations",
291
+ );
289
292
  });
290
293
 
291
294
  it("returns a login link for an express dashboard account with no requirements", async () => {
@@ -352,15 +355,19 @@ describe("getAccountLinkWithStatus", () => {
352
355
 
353
356
  expect(result.linkType).toBe("onboarding");
354
357
  const arg = stripe.v2.core.accountLinks.create.mock.calls[0][0];
355
- // applied_configurations absent → defaults to []
356
- expect(arg.use_case.account_onboarding.configurations).toEqual([]);
358
+ // No configurations parameter is sent.
359
+ expect(arg.use_case.account_onboarding).not.toHaveProperty(
360
+ "configurations",
361
+ );
357
362
  });
358
363
  });
359
364
 
360
365
  describe("createDisputeSession (BTS-30)", () => {
361
366
  it("creates an account session with the disputes components for the seller", async () => {
362
367
  const stripe = makeStripe();
363
- stripe.accountSessions.create.mockResolvedValue({ client_secret: "acs_secret" });
368
+ stripe.accountSessions.create.mockResolvedValue({
369
+ client_secret: "acs_secret",
370
+ });
364
371
 
365
372
  const result = await createDisputeSession(asStripe(stripe), {} as RunCtx, {
366
373
  stripeAccountId: "acct_seller",
@@ -375,4 +382,4 @@ describe("createDisputeSession (BTS-30)", () => {
375
382
  });
376
383
  expect(result).toEqual({ clientSecret: "acs_secret" });
377
384
  });
378
- })
385
+ });
@@ -2,10 +2,7 @@ import type Stripe from "stripe";
2
2
 
3
3
  import { throwStripeError } from "../errors.js";
4
4
  import type { RunCtx } from "../helpers.js";
5
- import type {
6
- V2AccountLinkCreateParams,
7
- V2AppliedConfiguration,
8
- } from "../stripe-types.js";
5
+ import type { V2AccountLinkCreateParams } from "../stripe-types.js";
9
6
  import type { AccountLinkWithStatus } from "../types.js";
10
7
 
11
8
  // =============================================================================
@@ -39,6 +36,7 @@ export async function createV2AccountLink(
39
36
  type: "account_onboarding" | "account_update";
40
37
  refreshUrl: string;
41
38
  returnUrl: string;
39
+ /** @deprecated Endive collects requirements without configurations. Ignored. */
42
40
  configurations?: string[];
43
41
  },
44
42
  ) {
@@ -47,8 +45,6 @@ export async function createV2AccountLink(
47
45
  ? {
48
46
  type: "account_update",
49
47
  account_update: {
50
- configurations: (opts.configurations ??
51
- []) as V2AppliedConfiguration[],
52
48
  refresh_url: opts.refreshUrl,
53
49
  return_url: opts.returnUrl,
54
50
  },
@@ -56,8 +52,6 @@ export async function createV2AccountLink(
56
52
  : {
57
53
  type: "account_onboarding",
58
54
  account_onboarding: {
59
- configurations: (opts.configurations ??
60
- []) as V2AppliedConfiguration[],
61
55
  refresh_url: opts.refreshUrl,
62
56
  return_url: opts.returnUrl,
63
57
  },
@@ -154,11 +148,9 @@ export async function getAccountLinkWithStatus(
154
148
  });
155
149
 
156
150
  const getOnboardingLink = async (): Promise<AccountLinkWithStatus> => {
157
- const configs = account.applied_configurations ?? [];
158
151
  const link = await createV2AccountLink(stripe, ctx, {
159
152
  stripeAccountId: opts.stripeAccountId,
160
153
  type: "account_onboarding",
161
- configurations: configs,
162
154
  refreshUrl: opts.refreshUrl,
163
155
  returnUrl: opts.returnUrl,
164
156
  });
@@ -28,6 +28,73 @@ function makeEntry(
28
28
  }
29
29
 
30
30
  describe("deriveAccountStatus", () => {
31
+ it.each(["rejected", "restricted"] as const)(
32
+ "maps an Endive %s capability to restricted even when the config is applied",
33
+ (status) => {
34
+ expect(
35
+ deriveAccountStatus({
36
+ configuration: {
37
+ merchant: {
38
+ applied: true,
39
+ capabilities: { card_payments: { status } },
40
+ } as V2AccountConfigMerchant,
41
+ },
42
+ }).onboardingStatus,
43
+ ).toBe("restricted");
44
+ },
45
+ );
46
+
47
+ it("keeps pending nested capabilities in progress", () => {
48
+ expect(
49
+ deriveAccountStatus({
50
+ configuration: {
51
+ merchant: {
52
+ applied: true,
53
+ capabilities: {
54
+ stripe_balance: { payouts: { status: "pending" } },
55
+ },
56
+ } as V2AccountConfigMerchant,
57
+ },
58
+ }).onboardingStatus,
59
+ ).toBe("in_progress");
60
+ });
61
+
62
+ it("prioritizes rejected capabilities over requirements awaiting Stripe", () => {
63
+ expect(
64
+ deriveAccountStatus({
65
+ requirements: {
66
+ entries: [
67
+ makeEntry("identity_document", { awaiting_action_from: "stripe" }),
68
+ ],
69
+ },
70
+ configuration: {
71
+ merchant: {
72
+ applied: true,
73
+ capabilities: { card_payments: { status: "rejected" } },
74
+ } as V2AccountConfigMerchant,
75
+ },
76
+ }),
77
+ ).toMatchObject({
78
+ onboardingStatus: "restricted",
79
+ missingRequirements: ["identity_document"],
80
+ });
81
+ });
82
+
83
+ it("permits active capabilities and ignores unsupported optional methods", () => {
84
+ expect(
85
+ deriveAccountStatus({
86
+ configuration: {
87
+ merchant: {
88
+ applied: true,
89
+ capabilities: {
90
+ card_payments: { status: "active" },
91
+ ach_debit_payments: { status: "unsupported" },
92
+ },
93
+ } as V2AccountConfigMerchant,
94
+ },
95
+ }).onboardingStatus,
96
+ ).toBe("complete");
97
+ });
31
98
  it("returns pending when no requirements and no config", () => {
32
99
  const status = deriveAccountStatus({});
33
100
  expect(status.onboardingStatus).toBe("pending");
@@ -1,10 +1,7 @@
1
1
  import type Stripe from "stripe";
2
2
 
3
3
  export type BetterStripeOnboardingStatus =
4
- | "pending"
5
- | "in_progress"
6
- | "complete"
7
- | "restricted";
4
+ "pending" | "in_progress" | "complete" | "restricted";
8
5
 
9
6
  /**
10
7
  * Accepts a V2 Account (or partial shape with configuration/requirements).
@@ -14,6 +11,14 @@ type StripeAccountLike = Pick<
14
11
  "configuration" | "requirements"
15
12
  >;
16
13
 
14
+ /** Capability groups such as stripe_balance nest their leaf statuses. */
15
+ function capabilityStatuses(value: unknown): string[] {
16
+ if (!value || typeof value !== "object") return [];
17
+ const record = value as Record<string, unknown>;
18
+ if (typeof record.status === "string") return [record.status];
19
+ return Object.values(record).flatMap(capabilityStatuses);
20
+ }
21
+
17
22
  export function deriveAccountStatus(account: StripeAccountLike): {
18
23
  onboardingStatus: BetterStripeOnboardingStatus;
19
24
  missingRequirements: string[];
@@ -53,6 +58,25 @@ export function deriveAccountStatus(account: StripeAccountLike): {
53
58
  };
54
59
  }
55
60
 
61
+ const configuration = account.configuration;
62
+ const configs = [
63
+ configuration?.customer,
64
+ configuration?.merchant,
65
+ configuration?.recipient,
66
+ ].filter(Boolean);
67
+ const statuses = configs.flatMap((cfg) =>
68
+ capabilityStatuses(cfg?.capabilities),
69
+ );
70
+
71
+ // An applied configuration does not imply that its capabilities are active.
72
+ // Endive adds rejected to the V2 capability status enum. A blocked capability
73
+ // takes precedence over requirements still awaiting Stripe verification.
74
+ if (
75
+ statuses.some((status) => status === "restricted" || status === "rejected")
76
+ ) {
77
+ return { onboardingStatus: "restricted", missingRequirements };
78
+ }
79
+
56
80
  if (missingRequirements.length > 0) {
57
81
  return {
58
82
  onboardingStatus: "in_progress",
@@ -60,7 +84,6 @@ export function deriveAccountStatus(account: StripeAccountLike): {
60
84
  };
61
85
  }
62
86
 
63
- const configuration = account.configuration;
64
87
  if (!configuration) {
65
88
  return {
66
89
  onboardingStatus: "pending",
@@ -68,13 +91,6 @@ export function deriveAccountStatus(account: StripeAccountLike): {
68
91
  };
69
92
  }
70
93
 
71
- // V2 configurations use `applied: boolean` to indicate active state
72
- const configs = [
73
- configuration.customer,
74
- configuration.merchant,
75
- configuration.recipient,
76
- ].filter(Boolean);
77
-
78
94
  if (configs.length === 0) {
79
95
  return {
80
96
  onboardingStatus: "pending",
@@ -85,7 +101,8 @@ export function deriveAccountStatus(account: StripeAccountLike): {
85
101
  const allApplied = configs.every((cfg) => cfg?.applied === true);
86
102
 
87
103
  return {
88
- onboardingStatus: allApplied ? "complete" : "in_progress",
104
+ onboardingStatus:
105
+ allApplied && !statuses.includes("pending") ? "complete" : "in_progress",
89
106
  missingRequirements,
90
107
  };
91
108
  }
@@ -4,8 +4,8 @@ import { STRIPE_API_VERSION } from "./constants.js";
4
4
  import { epochToIso, getStripeClient, runMutationOrThrow } from "./helpers.js";
5
5
 
6
6
  describe("STRIPE_API_VERSION", () => {
7
- it("is a dahlia API version", () => {
8
- expect(STRIPE_API_VERSION).toMatch(/\.dahlia$/);
7
+ it("matches the installed SDK API version", () => {
8
+ expect(STRIPE_API_VERSION).toBe("2026-09-30.endive");
9
9
  });
10
10
  });
11
11
 
@@ -679,15 +679,20 @@ export class BetterStripe {
679
679
  },
680
680
  ) {
681
681
  const { fee, ...rest } = opts;
682
- return checkoutImpl.createCheckoutSession(this.stripe(), this.component, ctx, {
683
- ...rest,
684
- // Fees apply to destination or split sales; resolve override → default.
685
- feeConfig:
686
- opts.destinationAccountId || opts.split?.length
687
- ? this.resolveFee(fee)
688
- : undefined,
689
- defaultStatementDescriptorSuffix: this._statementDescriptorSuffix,
690
- });
682
+ return checkoutImpl.createCheckoutSession(
683
+ this.stripe(),
684
+ this.component,
685
+ ctx,
686
+ {
687
+ ...rest,
688
+ // Fees apply to destination or split sales; resolve override → default.
689
+ feeConfig:
690
+ opts.destinationAccountId || opts.split?.length
691
+ ? this.resolveFee(fee)
692
+ : undefined,
693
+ defaultStatementDescriptorSuffix: this._statementDescriptorSuffix,
694
+ },
695
+ );
691
696
  }
692
697
 
693
698
  async getCheckoutSession(
@@ -760,20 +765,27 @@ export class BetterStripe {
760
765
  split?: SplitRecipient[];
761
766
  fee?: FeeOverride;
762
767
  trialDays?: number;
768
+ /** Attached payment method to use for this subscription. */
769
+ defaultPaymentMethod?: string;
763
770
  metadata?: Record<string, string>;
764
771
  /** Stripe idempotency key for retry-safe subscription creation. */
765
772
  idempotencyKey?: string;
766
773
  },
767
774
  ) {
768
775
  const { fee, ...rest } = opts;
769
- return subscriptionsImpl.createSubscription(this.stripe(), this.component, ctx, {
770
- ...rest,
771
- feeConfig:
772
- opts.destinationAccountId || opts.split?.length
773
- ? this.resolveFee(fee)
774
- : undefined,
775
- defaultStatementDescriptorSuffix: this._statementDescriptorSuffix,
776
- });
776
+ return subscriptionsImpl.createSubscription(
777
+ this.stripe(),
778
+ this.component,
779
+ ctx,
780
+ {
781
+ ...rest,
782
+ feeConfig:
783
+ opts.destinationAccountId || opts.split?.length
784
+ ? this.resolveFee(fee)
785
+ : undefined,
786
+ defaultStatementDescriptorSuffix: this._statementDescriptorSuffix,
787
+ },
788
+ );
777
789
  }
778
790
 
779
791
  async getSubscription(
@@ -1083,6 +1095,7 @@ export class BetterStripe {
1083
1095
  type: "account_onboarding" | "account_update";
1084
1096
  refreshUrl: string;
1085
1097
  returnUrl: string;
1098
+ /** @deprecated Endive collects requirements without configurations. Ignored. */
1086
1099
  configurations?: string[];
1087
1100
  },
1088
1101
  ) {
@@ -1169,7 +1182,12 @@ export class BetterStripe {
1169
1182
  operationId?: string;
1170
1183
  },
1171
1184
  ) {
1172
- return transfersImpl.reverseTransfers(this.stripe(), this.component, ctx, opts);
1185
+ return transfersImpl.reverseTransfers(
1186
+ this.stripe(),
1187
+ this.component,
1188
+ ctx,
1189
+ opts,
1190
+ );
1173
1191
  }
1174
1192
 
1175
1193
  /**
@@ -1799,44 +1817,31 @@ export class BetterStripe {
1799
1817
  ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined
1800
1818
  > = {
1801
1819
  afterAccountUpdated: hooks?.onAccountUpdated as
1802
- | ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>)
1803
- | undefined,
1820
+ ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
1804
1821
  afterCheckoutCompleted: hooks?.onCheckoutCompleted as
1805
- | ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>)
1806
- | undefined,
1822
+ ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
1807
1823
  afterSubscriptionUpdated: hooks?.onSubscriptionUpdated as
1808
- | ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>)
1809
- | undefined,
1824
+ ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
1810
1825
  afterSubscriptionCanceled: hooks?.onSubscriptionCanceled as
1811
- | ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>)
1812
- | undefined,
1826
+ ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
1813
1827
  afterTrialEnding: hooks?.onTrialEnding as
1814
- | ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>)
1815
- | undefined,
1828
+ ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
1816
1829
  afterInvoicePaid: hooks?.onInvoicePaid as
1817
- | ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>)
1818
- | undefined,
1830
+ ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
1819
1831
  afterInvoicePaymentFailed: hooks?.onInvoicePaymentFailed as
1820
- | ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>)
1821
- | undefined,
1832
+ ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
1822
1833
  afterPaymentSucceeded: hooks?.onPaymentSucceeded as
1823
- | ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>)
1824
- | undefined,
1834
+ ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
1825
1835
  afterPaymentFailed: hooks?.onPaymentFailed as
1826
- | ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>)
1827
- | undefined,
1836
+ ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
1828
1837
  afterPayoutCompleted: hooks?.onPayoutCompleted as
1829
- | ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>)
1830
- | undefined,
1838
+ ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
1831
1839
  afterRefundCreated: hooks?.onRefundCreated as
1832
- | ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>)
1833
- | undefined,
1840
+ ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
1834
1841
  afterDisputeCreated: hooks?.onDisputeCreated as
1835
- | ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>)
1836
- | undefined,
1842
+ ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
1837
1843
  afterDisputeClosed: hooks?.onDisputeClosed as
1838
- | ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>)
1839
- | undefined,
1844
+ ((ctx: AsyncHookCtx, doc: unknown) => Promise<void>) | undefined,
1840
1845
  };
1841
1846
 
1842
1847
  const syncWebhook = internalMutationGeneric({
@@ -0,0 +1,47 @@
1
+ import Stripe from "stripe";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import { getStripe } from "../component/lib/stripeFactory.js";
5
+ import { STRIPE_API_VERSION } from "./constants.js";
6
+ import { getStripeClient } from "./helpers.js";
7
+
8
+ describe("Stripe version pin", () => {
9
+ it("matches the supported stripe-node API, including the full endive suffix", () => {
10
+ expect(STRIPE_API_VERSION).toBe("2026-09-30.endive");
11
+ expect(Stripe.API_VERSION).toBe(STRIPE_API_VERSION);
12
+ });
13
+
14
+ it.each([getStripeClient, getStripe])(
15
+ "pins outgoing API requests from each factory",
16
+ async (factory) => {
17
+ const stripe = factory("sk_test_version_transport_only");
18
+ let version: string | undefined;
19
+ // Factories deliberately expose no transport override. Replace only the
20
+ // SDK's transport slot, leaving its request serializer/headers untouched.
21
+ const transportSlot = stripe as unknown as {
22
+ _setApiField: (field: string, value: unknown) => void;
23
+ };
24
+ transportSlot._setApiField("httpClient", {
25
+ getClientName: () => "test",
26
+ makeRequest: async (
27
+ ...[, , , , headers]: Parameters<
28
+ ReturnType<typeof Stripe.createNodeHttpClient>["makeRequest"]
29
+ >
30
+ ) => {
31
+ version = String(headers["Stripe-Version"]);
32
+ return {
33
+ getStatusCode: () => 200,
34
+ getHeaders: () => ({}),
35
+ getRawResponse: () => ({}),
36
+ toJSON: async () => ({ id: "prod_local", object: "product" }),
37
+ toStream: () => {
38
+ throw new Error("Unexpected stream");
39
+ },
40
+ };
41
+ },
42
+ });
43
+ await stripe.products.retrieve("prod_local");
44
+ expect(version).toBe(STRIPE_API_VERSION);
45
+ },
46
+ );
47
+ });
@@ -178,6 +178,7 @@ export type StripeComponentSubscription = {
178
178
  currentPeriodStart?: string;
179
179
  currentPeriodEnd?: string;
180
180
  cancelAtPeriodEnd: boolean;
181
+ isCollectionPaused?: boolean;
181
182
  canceledAt?: string;
182
183
  isTrialing: boolean;
183
184
  trialStart?: string;
@@ -26,7 +26,8 @@ export type V2ThinEvent = {
26
26
  created: string;
27
27
  livemode?: boolean;
28
28
  related_object?: {
29
- id: string;
29
+ /** Singleton related objects may omit their id (stripe-node 23). */
30
+ id?: string;
30
31
  type: string;
31
32
  url: string;
32
33
  };
@@ -79,6 +79,13 @@ export type RegisterRoutesConfig = {
79
79
  webhookSecretV2?: string;
80
80
  onEvent?: StripeEventHandler<StripeWebhookEvent>;
81
81
  events?: StripeEventHandlers;
82
+ /**
83
+ * Select verified V1 events whose onEvent/event hooks must succeed before
84
+ * delivery is acknowledged. Failures return 500 and permit redelivery;
85
+ * an in-flight delivery returns 503. Hooks must be idempotent by event.id.
86
+ * V2 events and scheduled asyncWebhook hooks retain best-effort behavior.
87
+ */
88
+ retryOnHookError?: (event: StripeWebhookEvent) => boolean;
82
89
  /**
83
90
  * Function references to the app's exported `webhookHandlers()` pair,
84
91
  * e.g. `webhooks: internal.stripe` (no cast needed). When provided,
@@ -179,6 +179,13 @@ export async function handleWebhookRequest(
179
179
  );
180
180
  }
181
181
 
182
+ let requiresHookSuccess: boolean;
183
+ try {
184
+ requiresHookSuccess = config?.retryOnHookError?.(event) ?? false;
185
+ } catch (error) {
186
+ console.error("[better-stripe] retryOnHookError selector failed:", error);
187
+ return jsonResponse({ error: "Webhook retry policy failed" }, 500);
188
+ }
182
189
  // --- Atomic webhook dedup via event ledger ---
183
190
  const eventId = event.id;
184
191
  console.info(
@@ -195,6 +202,9 @@ export async function handleWebhookRequest(
195
202
  },
196
203
  );
197
204
  if (dedupStatus !== "inserted") {
205
+ if (requiresHookSuccess && dedupStatus === "processing") {
206
+ return jsonResponse({ error: "Event processing in progress" }, 503);
207
+ }
198
208
  console.info(`[better-stripe] ↳ deduplicated (already processed)`);
199
209
  return jsonResponse({ success: true, deduplicated: true }, 200);
200
210
  }
@@ -204,6 +214,8 @@ export async function handleWebhookRequest(
204
214
  `[better-stripe] Webhook ledger insert failed for ${eventId}:`,
205
215
  error,
206
216
  );
217
+ if (requiresHookSuccess)
218
+ return jsonResponse({ error: "Webhook ledger unavailable" }, 500);
207
219
  }
208
220
 
209
221
  // --- Process event (sync) ---
@@ -226,6 +238,29 @@ export async function handleWebhookRequest(
226
238
  return jsonResponse({ error: "Sync processing failed" }, 500);
227
239
  }
228
240
 
241
+ if (requiresHookSuccess) {
242
+ try {
243
+ await runHooks(ctx, config, event, true);
244
+ } catch (error) {
245
+ console.error(
246
+ `[better-stripe] Required hook failed for ${event.type}:`,
247
+ error,
248
+ );
249
+ try {
250
+ await ctx.runMutation(
251
+ componentRef(component, "webhooks/mutations/markWebhookEventFailed"),
252
+ {
253
+ stripeEventId: eventId,
254
+ error: error instanceof Error ? error.message : String(error),
255
+ },
256
+ );
257
+ } catch {
258
+ // A stale processing row is recoverable on a later delivery.
259
+ }
260
+ return jsonResponse({ error: "Application fulfillment failed" }, 500);
261
+ }
262
+ }
263
+
229
264
  // --- Mark processed ---
230
265
  try {
231
266
  await ctx.runMutation(
@@ -235,6 +270,8 @@ export async function handleWebhookRequest(
235
270
  },
236
271
  );
237
272
  } catch {
273
+ if (requiresHookSuccess)
274
+ return jsonResponse({ error: "Webhook completion unavailable" }, 500);
238
275
  /* ledger update best-effort */
239
276
  }
240
277
 
@@ -244,7 +281,7 @@ export async function handleWebhookRequest(
244
281
 
245
282
  // --- Run async hooks (errors must not crash the handler) ---
246
283
  try {
247
- await runHooks(ctx, config, event);
284
+ if (!requiresHookSuccess) await runHooks(ctx, config, event);
248
285
  } catch (error) {
249
286
  console.error(
250
287
  `[better-stripe] Hook runner error for ${event.type}:`,
@@ -14,11 +14,13 @@ export async function runHooks(
14
14
  ctx: WebhookActionCtx,
15
15
  config: RegisterRoutesConfig | undefined,
16
16
  event: StripeWebhookEvent,
17
+ shouldThrow = false,
17
18
  ): Promise<void> {
18
19
  if (config?.onEvent) {
19
20
  try {
20
21
  await config.onEvent(ctx, event);
21
22
  } catch (error) {
23
+ if (shouldThrow) throw error;
22
24
  console.error(
23
25
  `[better-stripe] onEvent hook error for ${event.type}:`,
24
26
  error,
@@ -31,6 +33,7 @@ export async function runHooks(
31
33
  try {
32
34
  await handler(ctx, event);
33
35
  } catch (error) {
36
+ if (shouldThrow) throw error;
34
37
  console.error(
35
38
  `[better-stripe] Event hook error for ${event.type}:`,
36
39
  error,