@fantastic.dev/better-stripe 0.7.0 → 0.8.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 (343) hide show
  1. package/CHANGELOG.md +23 -18
  2. package/README.md +122 -90
  3. package/dist/client/billing/checkout.d.ts +1 -1
  4. package/dist/client/billing/checkout.d.ts.map +1 -1
  5. package/dist/client/billing/checkout.js +7 -2
  6. package/dist/client/billing/checkout.js.map +1 -1
  7. package/dist/client/connect/paymentMethods.d.ts.map +1 -1
  8. package/dist/client/connect/paymentMethods.js.map +1 -1
  9. package/dist/client/connect/refundActor.d.ts.map +1 -1
  10. package/dist/client/connect/refundActor.js.map +1 -1
  11. package/dist/client/connect/refunds.d.ts.map +1 -1
  12. package/dist/client/connect/refunds.js.map +1 -1
  13. package/dist/client/connect/transfers.d.ts.map +1 -1
  14. package/dist/client/connect/transfers.js +15 -4
  15. package/dist/client/connect/transfers.js.map +1 -1
  16. package/dist/client/core/accountDefaults.d.ts +6 -0
  17. package/dist/client/core/accountDefaults.d.ts.map +1 -0
  18. package/dist/client/core/accountDefaults.js +15 -0
  19. package/dist/client/core/accountDefaults.js.map +1 -0
  20. package/dist/client/core/accounts.d.ts +1 -5
  21. package/dist/client/core/accounts.d.ts.map +1 -1
  22. package/dist/client/core/accounts.js +2 -14
  23. package/dist/client/core/accounts.js.map +1 -1
  24. package/dist/client/core/fees.d.ts.map +1 -1
  25. package/dist/client/core/fees.js +7 -2
  26. package/dist/client/core/fees.js.map +1 -1
  27. package/dist/client/index.d.ts +1 -1
  28. package/dist/client/index.d.ts.map +1 -1
  29. package/dist/client/products/prices.d.ts.map +1 -1
  30. package/dist/client/products/prices.js.map +1 -1
  31. package/dist/client/products/products.d.ts.map +1 -1
  32. package/dist/client/products/products.js +3 -1
  33. package/dist/client/products/products.js.map +1 -1
  34. package/dist/client/utils/owner.d.ts.map +1 -1
  35. package/dist/client/utils/owner.js +3 -1
  36. package/dist/client/utils/owner.js.map +1 -1
  37. package/dist/client/utils/stripeDashboardUrl.d.ts.map +1 -1
  38. package/dist/client/utils/stripeDashboardUrl.js.map +1 -1
  39. package/dist/client/webhooks/helpers.d.ts +10 -0
  40. package/dist/client/webhooks/helpers.d.ts.map +1 -1
  41. package/dist/client/webhooks/helpers.js +18 -0
  42. package/dist/client/webhooks/helpers.js.map +1 -1
  43. package/dist/client/webhooks/processors.d.ts.map +1 -1
  44. package/dist/client/webhooks/processors.js +1 -19
  45. package/dist/client/webhooks/processors.js.map +1 -1
  46. package/dist/component/connect/mutations.d.ts.map +1 -1
  47. package/dist/component/connect/mutations.js +4 -33
  48. package/dist/component/connect/mutations.js.map +1 -1
  49. package/dist/component/connect/schema.d.ts.map +1 -1
  50. package/dist/component/connect/schema.js.map +1 -1
  51. package/dist/component/lib/paymentSeller.d.ts +3 -0
  52. package/dist/component/lib/paymentSeller.d.ts.map +1 -0
  53. package/dist/component/lib/paymentSeller.js +10 -0
  54. package/dist/component/lib/paymentSeller.js.map +1 -0
  55. package/dist/component/lib/reversals.d.ts +10 -0
  56. package/dist/component/lib/reversals.d.ts.map +1 -1
  57. package/dist/component/lib/reversals.js +17 -0
  58. package/dist/component/lib/reversals.js.map +1 -1
  59. package/dist/react/components/ConnectProvider.d.ts +1 -1
  60. package/dist/react/components/ConnectProvider.d.ts.map +1 -1
  61. package/dist/react/components/ConnectProvider.js +18 -4
  62. package/dist/react/components/ConnectProvider.js.map +1 -1
  63. package/dist/react/components/ConnectStatusBadge.d.ts.map +1 -1
  64. package/dist/react/components/ConnectStatusBadge.js.map +1 -1
  65. package/dist/react/components/EmbeddedDisputes.d.ts +1 -1
  66. package/dist/react/components/EmbeddedDisputes.d.ts.map +1 -1
  67. package/dist/react/components/EmbeddedDisputes.js +1 -1
  68. package/dist/react/components/EmbeddedDisputes.js.map +1 -1
  69. package/dist/react/components/SplitBreakdown.d.ts.map +1 -1
  70. package/dist/react/components/SplitBreakdown.js +12 -2
  71. package/dist/react/components/SplitBreakdown.js.map +1 -1
  72. package/dist/react/hooks/useCheckoutSession.d.ts.map +1 -1
  73. package/dist/react/hooks/useCheckoutSession.js.map +1 -1
  74. package/dist/react/hooks/useConfirmPayment.d.ts.map +1 -1
  75. package/dist/react/hooks/useConfirmPayment.js.map +1 -1
  76. package/dist/react/index.d.ts +1 -0
  77. package/dist/react/index.d.ts.map +1 -1
  78. package/dist/react/index.js +2 -0
  79. package/dist/react/index.js.map +1 -1
  80. package/dist/react/workflows/Account.d.ts +67 -0
  81. package/dist/react/workflows/Account.d.ts.map +1 -0
  82. package/dist/react/workflows/Account.js +74 -0
  83. package/dist/react/workflows/Account.js.map +1 -0
  84. package/dist/react/workflows/Billing.d.ts +60 -0
  85. package/dist/react/workflows/Billing.d.ts.map +1 -0
  86. package/dist/react/workflows/Billing.js +84 -0
  87. package/dist/react/workflows/Billing.js.map +1 -0
  88. package/dist/react/workflows/BillingPlans.d.ts +45 -0
  89. package/dist/react/workflows/BillingPlans.d.ts.map +1 -0
  90. package/dist/react/workflows/BillingPlans.js +51 -0
  91. package/dist/react/workflows/BillingPlans.js.map +1 -0
  92. package/dist/react/workflows/Checkout.d.ts +46 -0
  93. package/dist/react/workflows/Checkout.d.ts.map +1 -0
  94. package/dist/react/workflows/Checkout.js +61 -0
  95. package/dist/react/workflows/Checkout.js.map +1 -0
  96. package/dist/react/workflows/CheckoutChooser.d.ts +20 -0
  97. package/dist/react/workflows/CheckoutChooser.d.ts.map +1 -0
  98. package/dist/react/workflows/CheckoutChooser.js +24 -0
  99. package/dist/react/workflows/CheckoutChooser.js.map +1 -0
  100. package/dist/react/workflows/CheckoutForms.d.ts +26 -0
  101. package/dist/react/workflows/CheckoutForms.d.ts.map +1 -0
  102. package/dist/react/workflows/CheckoutForms.js +126 -0
  103. package/dist/react/workflows/CheckoutForms.js.map +1 -0
  104. package/dist/react/workflows/ClipboardButton.d.ts +17 -0
  105. package/dist/react/workflows/ClipboardButton.d.ts.map +1 -0
  106. package/dist/react/workflows/ClipboardButton.js +35 -0
  107. package/dist/react/workflows/ClipboardButton.js.map +1 -0
  108. package/dist/react/workflows/Collection.d.ts +30 -0
  109. package/dist/react/workflows/Collection.d.ts.map +1 -0
  110. package/dist/react/workflows/Collection.js +25 -0
  111. package/dist/react/workflows/Collection.js.map +1 -0
  112. package/dist/react/workflows/Collections.d.ts +56 -0
  113. package/dist/react/workflows/Collections.d.ts.map +1 -0
  114. package/dist/react/workflows/Collections.js +111 -0
  115. package/dist/react/workflows/Collections.js.map +1 -0
  116. package/dist/react/workflows/Connect.d.ts +19 -0
  117. package/dist/react/workflows/Connect.d.ts.map +1 -0
  118. package/dist/react/workflows/Connect.js +43 -0
  119. package/dist/react/workflows/Connect.js.map +1 -0
  120. package/dist/react/workflows/Costs.d.ts +48 -0
  121. package/dist/react/workflows/Costs.d.ts.map +1 -0
  122. package/dist/react/workflows/Costs.js +66 -0
  123. package/dist/react/workflows/Costs.js.map +1 -0
  124. package/dist/react/workflows/Disputes.d.ts +36 -0
  125. package/dist/react/workflows/Disputes.d.ts.map +1 -0
  126. package/dist/react/workflows/Disputes.js +74 -0
  127. package/dist/react/workflows/Disputes.js.map +1 -0
  128. package/dist/react/workflows/Earnings.d.ts +42 -0
  129. package/dist/react/workflows/Earnings.d.ts.map +1 -0
  130. package/dist/react/workflows/Earnings.js +38 -0
  131. package/dist/react/workflows/Earnings.js.map +1 -0
  132. package/dist/react/workflows/Financials.d.ts +43 -0
  133. package/dist/react/workflows/Financials.d.ts.map +1 -0
  134. package/dist/react/workflows/Financials.js +57 -0
  135. package/dist/react/workflows/Financials.js.map +1 -0
  136. package/dist/react/workflows/IntegrationSwitch.d.ts +23 -0
  137. package/dist/react/workflows/IntegrationSwitch.d.ts.map +1 -0
  138. package/dist/react/workflows/IntegrationSwitch.js +31 -0
  139. package/dist/react/workflows/IntegrationSwitch.js.map +1 -0
  140. package/dist/react/workflows/Onboarding.d.ts +56 -0
  141. package/dist/react/workflows/Onboarding.d.ts.map +1 -0
  142. package/dist/react/workflows/Onboarding.js +86 -0
  143. package/dist/react/workflows/Onboarding.js.map +1 -0
  144. package/dist/react/workflows/Overview.d.ts +51 -0
  145. package/dist/react/workflows/Overview.d.ts.map +1 -0
  146. package/dist/react/workflows/Overview.js +63 -0
  147. package/dist/react/workflows/Overview.js.map +1 -0
  148. package/dist/react/workflows/PaymentMethods.d.ts +35 -0
  149. package/dist/react/workflows/PaymentMethods.d.ts.map +1 -0
  150. package/dist/react/workflows/PaymentMethods.js +63 -0
  151. package/dist/react/workflows/PaymentMethods.js.map +1 -0
  152. package/dist/react/workflows/PaymentReceipt.d.ts +20 -0
  153. package/dist/react/workflows/PaymentReceipt.d.ts.map +1 -0
  154. package/dist/react/workflows/PaymentReceipt.js +24 -0
  155. package/dist/react/workflows/PaymentReceipt.js.map +1 -0
  156. package/dist/react/workflows/Payouts.d.ts +47 -0
  157. package/dist/react/workflows/Payouts.d.ts.map +1 -0
  158. package/dist/react/workflows/Payouts.js +87 -0
  159. package/dist/react/workflows/Payouts.js.map +1 -0
  160. package/dist/react/workflows/Products.d.ts +92 -0
  161. package/dist/react/workflows/Products.d.ts.map +1 -0
  162. package/dist/react/workflows/Products.js +179 -0
  163. package/dist/react/workflows/Products.js.map +1 -0
  164. package/dist/react/workflows/ReferralLinks.d.ts +26 -0
  165. package/dist/react/workflows/ReferralLinks.d.ts.map +1 -0
  166. package/dist/react/workflows/ReferralLinks.js +30 -0
  167. package/dist/react/workflows/ReferralLinks.js.map +1 -0
  168. package/dist/react/workflows/Refunds.d.ts +72 -0
  169. package/dist/react/workflows/Refunds.d.ts.map +1 -0
  170. package/dist/react/workflows/Refunds.js +95 -0
  171. package/dist/react/workflows/Refunds.js.map +1 -0
  172. package/dist/react/workflows/Setup.d.ts +50 -0
  173. package/dist/react/workflows/Setup.d.ts.map +1 -0
  174. package/dist/react/workflows/Setup.js +59 -0
  175. package/dist/react/workflows/Setup.js.map +1 -0
  176. package/dist/react/workflows/Storefront.d.ts +53 -0
  177. package/dist/react/workflows/Storefront.d.ts.map +1 -0
  178. package/dist/react/workflows/Storefront.js +44 -0
  179. package/dist/react/workflows/Storefront.js.map +1 -0
  180. package/dist/react/workflows/SubscriptionOverview.d.ts +13 -0
  181. package/dist/react/workflows/SubscriptionOverview.d.ts.map +1 -0
  182. package/dist/react/workflows/SubscriptionOverview.js +26 -0
  183. package/dist/react/workflows/SubscriptionOverview.js.map +1 -0
  184. package/dist/react/workflows/Subscriptions.d.ts +92 -0
  185. package/dist/react/workflows/Subscriptions.d.ts.map +1 -0
  186. package/dist/react/workflows/Subscriptions.js +149 -0
  187. package/dist/react/workflows/Subscriptions.js.map +1 -0
  188. package/dist/react/workflows/Tasks.d.ts +40 -0
  189. package/dist/react/workflows/Tasks.d.ts.map +1 -0
  190. package/dist/react/workflows/Tasks.js +113 -0
  191. package/dist/react/workflows/Tasks.js.map +1 -0
  192. package/dist/react/workflows/checkout-options.d.ts +37 -0
  193. package/dist/react/workflows/checkout-options.d.ts.map +1 -0
  194. package/dist/react/workflows/checkout-options.js +43 -0
  195. package/dist/react/workflows/checkout-options.js.map +1 -0
  196. package/dist/react/workflows/current-subscription.d.ts +11 -0
  197. package/dist/react/workflows/current-subscription.d.ts.map +1 -0
  198. package/dist/react/workflows/current-subscription.js +14 -0
  199. package/dist/react/workflows/current-subscription.js.map +1 -0
  200. package/dist/react/workflows/filters.d.ts +17 -0
  201. package/dist/react/workflows/filters.d.ts.map +1 -0
  202. package/dist/react/workflows/filters.js +23 -0
  203. package/dist/react/workflows/filters.js.map +1 -0
  204. package/dist/react/workflows/index.d.ts +70 -0
  205. package/dist/react/workflows/index.d.ts.map +1 -0
  206. package/dist/react/workflows/index.js +38 -0
  207. package/dist/react/workflows/index.js.map +1 -0
  208. package/dist/react/workflows/useWorkflowAction.d.ts +11 -0
  209. package/dist/react/workflows/useWorkflowAction.d.ts.map +1 -0
  210. package/dist/react/workflows/useWorkflowAction.js +68 -0
  211. package/dist/react/workflows/useWorkflowAction.js.map +1 -0
  212. package/dist/react/workflows/useWorkflowResource.d.ts +9 -0
  213. package/dist/react/workflows/useWorkflowResource.d.ts.map +1 -0
  214. package/dist/react/workflows/useWorkflowResource.js +67 -0
  215. package/dist/react/workflows/useWorkflowResource.js.map +1 -0
  216. package/docs/react-workflows-audit.md +66 -0
  217. package/docs/react-workflows.md +231 -0
  218. package/package.json +19 -3
  219. package/src/client/billing/checkout-ui-modes.test.ts +49 -0
  220. package/src/client/billing/checkout.ts +8 -3
  221. package/src/client/billing/invoices.test.ts +12 -6
  222. package/src/client/connect/paymentMethods.test.ts +3 -1
  223. package/src/client/connect/paymentMethods.ts +4 -1
  224. package/src/client/connect/refundActor.ts +1 -2
  225. package/src/client/connect/refunds.test.ts +35 -10
  226. package/src/client/connect/refunds.ts +4 -1
  227. package/src/client/connect/transfers.test.ts +66 -12
  228. package/src/client/connect/transfers.ts +32 -8
  229. package/src/client/core/accountDefaults.ts +15 -0
  230. package/src/client/core/accounts.ts +14 -17
  231. package/src/client/core/config.test.ts +2 -1
  232. package/src/client/core/descriptors.test.ts +6 -4
  233. package/src/client/core/fees.test.ts +20 -28
  234. package/src/client/core/fees.ts +9 -2
  235. package/src/client/index.test.ts +22 -22
  236. package/src/client/index.ts +1 -1
  237. package/src/client/products/prices.test.ts +8 -2
  238. package/src/client/products/prices.ts +4 -1
  239. package/src/client/products/products.test.ts +61 -20
  240. package/src/client/products/products.ts +3 -1
  241. package/src/client/utils/data.test.ts +3 -1
  242. package/src/client/utils/owner.test.ts +4 -1
  243. package/src/client/utils/owner.ts +5 -1
  244. package/src/client/utils/stripeDashboardUrl.ts +1 -4
  245. package/src/client/webhooks/handler.test.ts +16 -19
  246. package/src/client/webhooks/helpers.test.ts +26 -13
  247. package/src/client/webhooks/helpers.ts +23 -0
  248. package/src/client/webhooks/hooks.test.ts +1 -2
  249. package/src/client/webhooks/processors.test.ts +18 -34
  250. package/src/client/webhooks/processors.ts +1 -23
  251. package/src/client/webhooks.test.ts +1 -2
  252. package/src/component/_generated/api.ts +2 -0
  253. package/src/component/connect/mutations.ts +5 -46
  254. package/src/component/connect/schema.ts +4 -3
  255. package/src/component/connect.test.ts +1 -5
  256. package/src/component/lib/paymentSeller.ts +15 -0
  257. package/src/component/lib/reversals.ts +30 -2
  258. package/src/component/subscription-scoping.test.ts +5 -1
  259. package/src/component/triggers.test.ts +7 -4
  260. package/src/react/components/BuyerBillingView.render.test.tsx +7 -2
  261. package/src/react/components/ConnectProvider.live.test.tsx +61 -0
  262. package/src/react/components/ConnectProvider.tsx +18 -4
  263. package/src/react/components/ConnectStatusBadge.tsx +1 -4
  264. package/src/react/components/DisputeDetail.render.test.tsx +5 -1
  265. package/src/react/components/EarningsSummary.render.test.tsx +2 -6
  266. package/src/react/components/EmbeddedDisputes.render.test.tsx +7 -5
  267. package/src/react/components/EmbeddedDisputes.tsx +4 -1
  268. package/src/react/components/EvidenceForm.render.test.tsx +2 -8
  269. package/src/react/components/SplitBreakdown.render.test.tsx +28 -5
  270. package/src/react/components/SplitBreakdown.tsx +12 -2
  271. package/src/react/hooks/useCheckoutSession.ts +1 -2
  272. package/src/react/hooks/useConfirmPayment.ts +1 -2
  273. package/src/react/hooks/useEarnings.test.ts +43 -8
  274. package/src/react/hooks/useStripeMode.test.ts +5 -8
  275. package/src/react/index.ts +3 -0
  276. package/src/react/workflows/Account.test.tsx +31 -0
  277. package/src/react/workflows/Account.tsx +183 -0
  278. package/src/react/workflows/Billing.test.tsx +45 -0
  279. package/src/react/workflows/Billing.tsx +213 -0
  280. package/src/react/workflows/BillingPlans.test.tsx +31 -0
  281. package/src/react/workflows/BillingPlans.tsx +130 -0
  282. package/src/react/workflows/Checkout.test.tsx +118 -0
  283. package/src/react/workflows/Checkout.tsx +135 -0
  284. package/src/react/workflows/CheckoutChooser.test.tsx +44 -0
  285. package/src/react/workflows/CheckoutChooser.tsx +77 -0
  286. package/src/react/workflows/CheckoutForm.test.tsx +66 -0
  287. package/src/react/workflows/CheckoutForms.test.tsx +123 -0
  288. package/src/react/workflows/CheckoutForms.tsx +247 -0
  289. package/src/react/workflows/ClipboardButton.test.tsx +34 -0
  290. package/src/react/workflows/ClipboardButton.tsx +67 -0
  291. package/src/react/workflows/Collection.tsx +76 -0
  292. package/src/react/workflows/Collections.test.tsx +86 -0
  293. package/src/react/workflows/Collections.tsx +213 -0
  294. package/src/react/workflows/Connect.test.tsx +50 -0
  295. package/src/react/workflows/Connect.tsx +107 -0
  296. package/src/react/workflows/Costs.test.tsx +41 -0
  297. package/src/react/workflows/Costs.tsx +133 -0
  298. package/src/react/workflows/Disputes.test.tsx +25 -0
  299. package/src/react/workflows/Disputes.tsx +164 -0
  300. package/src/react/workflows/Earnings.test.ts +19 -0
  301. package/src/react/workflows/Earnings.tsx +84 -0
  302. package/src/react/workflows/Financials.test.tsx +35 -0
  303. package/src/react/workflows/Financials.tsx +117 -0
  304. package/src/react/workflows/IntegrationSwitch.test.tsx +36 -0
  305. package/src/react/workflows/IntegrationSwitch.tsx +63 -0
  306. package/src/react/workflows/Onboarding.test.tsx +75 -0
  307. package/src/react/workflows/Onboarding.tsx +171 -0
  308. package/src/react/workflows/Overview.test.tsx +57 -0
  309. package/src/react/workflows/Overview.tsx +123 -0
  310. package/src/react/workflows/PaymentMethods.test.tsx +60 -0
  311. package/src/react/workflows/PaymentMethods.tsx +117 -0
  312. package/src/react/workflows/PaymentReceipt.test.tsx +24 -0
  313. package/src/react/workflows/PaymentReceipt.tsx +46 -0
  314. package/src/react/workflows/Payouts.test.tsx +82 -0
  315. package/src/react/workflows/Payouts.tsx +212 -0
  316. package/src/react/workflows/Products.test.tsx +90 -0
  317. package/src/react/workflows/Products.tsx +377 -0
  318. package/src/react/workflows/ReferralLinks.test.tsx +39 -0
  319. package/src/react/workflows/ReferralLinks.tsx +75 -0
  320. package/src/react/workflows/Refunds.test.tsx +75 -0
  321. package/src/react/workflows/Refunds.tsx +223 -0
  322. package/src/react/workflows/Setup.test.tsx +25 -0
  323. package/src/react/workflows/Setup.tsx +166 -0
  324. package/src/react/workflows/Storefront.test.tsx +82 -0
  325. package/src/react/workflows/Storefront.tsx +138 -0
  326. package/src/react/workflows/SubscriptionOverview.test.tsx +27 -0
  327. package/src/react/workflows/SubscriptionOverview.tsx +48 -0
  328. package/src/react/workflows/Subscriptions.test.tsx +61 -0
  329. package/src/react/workflows/Subscriptions.tsx +282 -0
  330. package/src/react/workflows/Tasks.test.tsx +23 -0
  331. package/src/react/workflows/Tasks.tsx +199 -0
  332. package/src/react/workflows/checkout-options.ts +42 -0
  333. package/src/react/workflows/current-subscription.test.ts +29 -0
  334. package/src/react/workflows/current-subscription.ts +25 -0
  335. package/src/react/workflows/filters.test.ts +12 -0
  336. package/src/react/workflows/filters.ts +22 -0
  337. package/src/react/workflows/index.ts +238 -0
  338. package/src/react/workflows/native-interactions.test.tsx +73 -0
  339. package/src/react/workflows/public-workflows.test.tsx +216 -0
  340. package/src/react/workflows/useWorkflowAction.test.tsx +54 -0
  341. package/src/react/workflows/useWorkflowAction.ts +75 -0
  342. package/src/react/workflows/useWorkflowResource.test.tsx +41 -0
  343. package/src/react/workflows/useWorkflowResource.ts +78 -0
@@ -0,0 +1,118 @@
1
+ // @vitest-environment jsdom
2
+ import { cleanup, render, screen, waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { afterEach, expect, it, vi } from "vitest";
5
+ vi.mock("../components/EmbeddedCheckout.js", () => ({
6
+ EmbeddedCheckout: () => <div>Stripe iframe</div>,
7
+ }));
8
+ vi.mock("./CheckoutForms.js", () => ({
9
+ CheckoutElementsForm: () => <div>Elements form</div>,
10
+ PaymentIntentForm: () => <div>Intent form</div>,
11
+ CheckoutForm: () => <div>Compact form</div>,
12
+ }));
13
+ import { Checkout } from "./Checkout.js";
14
+ afterEach(cleanup);
15
+ it("opens hosted Checkout only on explicit interaction", async () => {
16
+ const createSession = vi.fn().mockResolvedValue({
17
+ stripeSessionId: "cs_test",
18
+ url: "https://checkout.stripe.com/test",
19
+ });
20
+ const navigate = vi.fn();
21
+ render(
22
+ <Checkout
23
+ mode="redirect"
24
+ orderKey="order_one"
25
+ publishableKey="pk_test_example"
26
+ returnUrl="https://app.test/receipt"
27
+ createSession={createSession}
28
+ onNavigate={navigate}
29
+ />,
30
+ );
31
+ await screen.findByRole("button", { name: "Continue to Stripe Checkout" });
32
+ expect(navigate).not.toHaveBeenCalled();
33
+ await userEvent.click(
34
+ screen.getByRole("button", { name: "Continue to Stripe Checkout" }),
35
+ );
36
+ expect(navigate).toHaveBeenCalledWith("https://checkout.stripe.com/test");
37
+ expect(createSession).toHaveBeenCalledWith(
38
+ expect.objectContaining({
39
+ mode: "redirect",
40
+ requestId: expect.any(String),
41
+ }),
42
+ );
43
+ });
44
+ it.each([
45
+ ["embedded", "Stripe iframe"],
46
+ ["custom", "Elements form"],
47
+ ["form", "Compact form"],
48
+ ] as const)("dispatches %s to the correct integration", async (mode, title) => {
49
+ render(
50
+ <Checkout
51
+ mode={mode}
52
+ orderKey="order_one"
53
+ publishableKey="pk_test_example"
54
+ returnUrl="https://app.test/receipt"
55
+ createSession={async () => ({
56
+ stripeSessionId: "cs_test",
57
+ clientSecret: "cs_test_secret",
58
+ })}
59
+ />,
60
+ );
61
+ expect(await screen.findByText(title)).toBeTruthy();
62
+ });
63
+ it("exposes creation errors and retries without losing the selected integration", async () => {
64
+ const createSession = vi
65
+ .fn()
66
+ .mockRejectedValueOnce(new Error("Seller plan expired"))
67
+ .mockResolvedValueOnce({
68
+ stripeSessionId: "cs_test",
69
+ clientSecret: "cs_test_secret",
70
+ });
71
+ render(
72
+ <Checkout
73
+ mode="embedded"
74
+ orderKey="order_one"
75
+ publishableKey="pk_test_example"
76
+ returnUrl="https://app.test/receipt"
77
+ createSession={createSession}
78
+ />,
79
+ );
80
+ expect((await screen.findByRole("alert")).textContent).toContain(
81
+ "Seller plan expired",
82
+ );
83
+ await userEvent.click(screen.getByRole("button", { name: "Retry" }));
84
+ await screen.findByText("Stripe iframe");
85
+ await waitFor(() => expect(createSession).toHaveBeenCalledTimes(2));
86
+ });
87
+ it("rejects missing session secrets and initializes Payment Intents through their own callback", async () => {
88
+ const { unmount } = render(
89
+ <Checkout
90
+ mode="embedded"
91
+ orderKey="order_one"
92
+ publishableKey="pk_test_example"
93
+ returnUrl="https://app.test/receipt"
94
+ createSession={async () => ({ stripeSessionId: "cs_test" })}
95
+ />,
96
+ );
97
+ expect((await screen.findByRole("alert")).textContent).toContain(
98
+ "client secret",
99
+ );
100
+ unmount();
101
+ const createPaymentIntent = vi.fn().mockResolvedValue({
102
+ paymentIntentId: "pi_test",
103
+ clientSecret: "pi_test_secret",
104
+ });
105
+ render(
106
+ <Checkout
107
+ mode="payment-intent"
108
+ orderKey="order_one"
109
+ publishableKey="pk_test_example"
110
+ returnUrl="https://app.test/receipt"
111
+ createPaymentIntent={createPaymentIntent}
112
+ />,
113
+ );
114
+ await screen.findByText("Intent form");
115
+ expect(createPaymentIntent).toHaveBeenCalledWith({
116
+ requestId: expect.any(String),
117
+ });
118
+ });
@@ -0,0 +1,135 @@
1
+ "use client";
2
+ import type { ReactNode } from "react";
3
+ import { EmbeddedCheckout } from "../components/EmbeddedCheckout.js";
4
+ import { useWorkflowResource } from "./useWorkflowResource.js";
5
+ import {
6
+ CheckoutElementsForm,
7
+ PaymentIntentForm,
8
+ CheckoutForm,
9
+ type CheckoutFormOptions,
10
+ } from "./CheckoutForms.js";
11
+ export type CheckoutMode =
12
+ "redirect" | "embedded" | "custom" | "form" | "payment-intent";
13
+ export type CheckoutSessionResult = {
14
+ stripeSessionId: string;
15
+ clientSecret?: string;
16
+ url?: string;
17
+ };
18
+ export type CheckoutIntentResult = {
19
+ paymentIntentId: string;
20
+ clientSecret: string;
21
+ };
22
+ export type CheckoutRequest = {
23
+ mode: Exclude<CheckoutMode, "payment-intent">;
24
+ requestId: string;
25
+ };
26
+ export type CheckoutRenderProps = {
27
+ content: ReactNode;
28
+ isLoading: boolean;
29
+ error: string | null;
30
+ retry: () => void;
31
+ mode: CheckoutMode;
32
+ hostedUrl?: string;
33
+ continueToCheckout?: () => void;
34
+ };
35
+ type Common = CheckoutFormOptions & {
36
+ publishableKey: string;
37
+ /** Changes when the price, signed-in customer, or referral attribution changes. */
38
+ orderKey: string;
39
+ returnUrl: string;
40
+ onNavigate?: (url: string) => void;
41
+ children?: (state: CheckoutRenderProps) => ReactNode;
42
+ };
43
+ export type CheckoutProps = Common &
44
+ (
45
+ | {
46
+ mode: Exclude<CheckoutMode, "payment-intent">;
47
+ createSession: (
48
+ request: CheckoutRequest,
49
+ ) => Promise<CheckoutSessionResult>;
50
+ }
51
+ | {
52
+ mode: "payment-intent";
53
+ createPaymentIntent: (request: {
54
+ requestId: string;
55
+ }) => Promise<CheckoutIntentResult>;
56
+ }
57
+ );
58
+ /** All checkout integrations share one unstyled lifecycle. Callbacks must authenticate and validate prices on the server. */
59
+ export function Checkout(props: CheckoutProps) {
60
+ return <CheckoutBody key={`${props.orderKey}:${props.mode}`} {...props} />;
61
+ }
62
+ function CheckoutBody(props: CheckoutProps) {
63
+ const state = useWorkflowResource(async () => {
64
+ const requestId = crypto.randomUUID();
65
+ const result =
66
+ props.mode === "payment-intent"
67
+ ? await props.createPaymentIntent({ requestId })
68
+ : await props.createSession({ mode: props.mode, requestId });
69
+ if (props.mode === "redirect") {
70
+ if (!("url" in result) || !result.url)
71
+ throw new Error("Stripe did not return a checkout URL.");
72
+ } else if (!result.clientSecret)
73
+ throw new Error("Stripe did not return a checkout client secret.");
74
+ return result;
75
+ });
76
+ const navigate =
77
+ props.onNavigate ?? ((url: string) => window.location.assign(url));
78
+ let content: ReactNode;
79
+ if (state.error)
80
+ content = (
81
+ <div role="alert">
82
+ <p>{state.error}</p>
83
+ <button type="button" onClick={state.refresh}>
84
+ Retry
85
+ </button>
86
+ </div>
87
+ );
88
+ else if (!state.data || state.isLoading)
89
+ content = <p role="status">Creating checkout…</p>;
90
+ else if (props.mode === "redirect" && "url" in state.data) {
91
+ const url = state.data.url!;
92
+ content = (
93
+ <button type="button" onClick={() => navigate(url)}>
94
+ Continue to Stripe Checkout
95
+ </button>
96
+ );
97
+ } else {
98
+ const form = {
99
+ publishableKey: props.publishableKey,
100
+ clientSecret: state.data.clientSecret!,
101
+ returnUrl: props.returnUrl,
102
+ email: props.email,
103
+ appearance: props.appearance,
104
+ renderForm: props.renderForm,
105
+ onNavigate: navigate,
106
+ };
107
+ content =
108
+ props.mode === "embedded" ? (
109
+ <EmbeddedCheckout
110
+ publishableKey={form.publishableKey}
111
+ clientSecret={form.clientSecret}
112
+ />
113
+ ) : props.mode === "custom" ? (
114
+ <CheckoutElementsForm {...form} />
115
+ ) : props.mode === "form" ? (
116
+ <CheckoutForm {...form} />
117
+ ) : (
118
+ <PaymentIntentForm {...form} />
119
+ );
120
+ }
121
+ const hostedUrl =
122
+ props.mode === "redirect" && state.data && "url" in state.data
123
+ ? state.data.url
124
+ : undefined;
125
+ const result: CheckoutRenderProps = {
126
+ content,
127
+ isLoading: state.isLoading,
128
+ error: state.error,
129
+ retry: state.refresh,
130
+ mode: props.mode,
131
+ hostedUrl,
132
+ continueToCheckout: hostedUrl ? () => navigate(hostedUrl) : undefined,
133
+ };
134
+ return <>{props.children ? props.children(result) : content}</>;
135
+ }
@@ -0,0 +1,44 @@
1
+ // @vitest-environment jsdom
2
+ import { cleanup, render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { afterEach, expect, it, vi } from "vitest";
5
+ import { CheckoutChooser } from "./CheckoutChooser.js";
6
+ afterEach(cleanup);
7
+ it("offers five checkout types, explains tradeoffs, and reports the selected mode", async () => {
8
+ const change = vi.fn();
9
+ render(<CheckoutChooser onValueChange={change} />);
10
+ expect(screen.getAllByRole("button")).toHaveLength(5);
11
+ expect(
12
+ screen
13
+ .getByRole("button", { name: "Embedded Checkout" })
14
+ .getAttribute("aria-pressed"),
15
+ ).toBe("true");
16
+ await userEvent.click(
17
+ screen.getByRole("button", { name: "Hosted Checkout" }),
18
+ );
19
+ expect(change).toHaveBeenCalledWith("redirect");
20
+ expect(screen.getByText(/Redirects away from your app/)).toBeTruthy();
21
+ expect(
22
+ screen
23
+ .getByRole("link", { name: "Stripe documentation" })
24
+ .getAttribute("href"),
25
+ ).toBe("https://docs.stripe.com/payments/checkout");
26
+ });
27
+ it("preserves a controlled selection and marks Payment Intents unsupported for recurring products", async () => {
28
+ const change = vi.fn();
29
+ render(
30
+ <CheckoutChooser value="payment-intent" recurring onValueChange={change}>
31
+ {(state) => (
32
+ <>
33
+ <span>{state.selected.id}</span>
34
+ <span>{String(state.unsupported)}</span>
35
+ <button onClick={() => state.select("custom")}>Switch</button>
36
+ </>
37
+ )}
38
+ </CheckoutChooser>,
39
+ );
40
+ expect(screen.getByText("true")).toBeTruthy();
41
+ await userEvent.click(screen.getByRole("button", { name: "Switch" }));
42
+ expect(change).toHaveBeenCalledWith("custom");
43
+ expect(screen.getByText("payment-intent")).toBeTruthy();
44
+ });
@@ -0,0 +1,77 @@
1
+ "use client";
2
+ import { useState, type ReactNode } from "react";
3
+ import { CHECKOUT_OPTIONS } from "./checkout-options.js";
4
+ import type { CheckoutMode } from "./Checkout.js";
5
+ export type CheckoutOption = (typeof CHECKOUT_OPTIONS)[number];
6
+ export type CheckoutChooserState = {
7
+ options: typeof CHECKOUT_OPTIONS;
8
+ selected: CheckoutOption;
9
+ unsupported: boolean;
10
+ select: (mode: CheckoutMode) => void;
11
+ };
12
+ export type CheckoutChooserProps = {
13
+ value?: CheckoutMode;
14
+ defaultValue?: CheckoutMode;
15
+ onValueChange?: (mode: CheckoutMode) => void;
16
+ recurring?: boolean;
17
+ children?: (state: CheckoutChooserState) => ReactNode;
18
+ };
19
+ /** Checkout tradeoffs and mode selection, independent of routing and session creation. */
20
+ export function CheckoutChooser({
21
+ value,
22
+ defaultValue = "embedded",
23
+ onValueChange,
24
+ recurring = false,
25
+ children,
26
+ }: CheckoutChooserProps) {
27
+ const [internal, setInternal] = useState(defaultValue);
28
+ const selected =
29
+ CHECKOUT_OPTIONS.find((option) => option.id === (value ?? internal)) ??
30
+ CHECKOUT_OPTIONS[1];
31
+ const select = (mode: CheckoutMode) => {
32
+ setInternal(mode);
33
+ onValueChange?.(mode);
34
+ };
35
+ const state = {
36
+ options: CHECKOUT_OPTIONS,
37
+ selected,
38
+ select,
39
+ unsupported: recurring && selected.id === "payment-intent",
40
+ };
41
+ if (children) return <>{children(state)}</>;
42
+ return (
43
+ <section data-better-stripe="checkout-chooser">
44
+ <h2>Choose your checkout type</h2>
45
+ <div role="group" aria-label="Checkout types">
46
+ {state.options.map((option) => (
47
+ <button
48
+ key={option.id}
49
+ type="button"
50
+ aria-pressed={selected.id === option.id}
51
+ onClick={() => select(option.id)}
52
+ >
53
+ {option.title}
54
+ </button>
55
+ ))}
56
+ </div>
57
+ <h3>{selected.title}</h3>
58
+ <p>{selected.api}</p>
59
+ <dl>
60
+ <dt>Pros</dt>
61
+ <dd>{selected.pros}</dd>
62
+ <dt>Cons</dt>
63
+ <dd>{selected.cons}</dd>
64
+ </dl>
65
+ <a href={selected.docs} target="_blank" rel="noreferrer">
66
+ Stripe documentation
67
+ </a>
68
+ {selected.id === "form" && <p>Preview</p>}
69
+ {state.unsupported && (
70
+ <p role="status">
71
+ Payment Intents alone do not create subscriptions. Choose a Checkout
72
+ Sessions integration for a recurring product.
73
+ </p>
74
+ )}
75
+ </section>
76
+ );
77
+ }
@@ -0,0 +1,66 @@
1
+ // @vitest-environment jsdom
2
+ import { act, cleanup, render, screen } from "@testing-library/react";
3
+ import { afterEach, expect, it, vi } from "vitest";
4
+ import type { ReactNode } from "react";
5
+ const mocks = vi.hoisted(() => ({
6
+ confirm: vi.fn(),
7
+ onConfirm: undefined as undefined | ((event: object) => unknown),
8
+ }));
9
+ vi.mock("@stripe/stripe-js", () => ({
10
+ loadStripe: () => Promise.resolve(null),
11
+ }));
12
+ vi.mock("@stripe/react-stripe-js/checkout", () => ({
13
+ CheckoutFormProvider: ({ children }: { children: ReactNode }) => children,
14
+ CheckoutForm: ({ onConfirm }: { onConfirm: (event: object) => unknown }) => {
15
+ mocks.onConfirm = onConfirm;
16
+ return <p>Stripe form</p>;
17
+ },
18
+ useCheckoutForm: () => ({
19
+ type: "success",
20
+ checkout: { id: "cs_form", confirm: mocks.confirm },
21
+ }),
22
+ }));
23
+ import { CheckoutForm } from "./CheckoutForms.js";
24
+ afterEach(cleanup);
25
+ it("returns a promise for the Stripe form confirmation lifecycle and uses the actual session ID", async () => {
26
+ mocks.confirm.mockResolvedValue({ type: "success" });
27
+ const navigate = vi.fn();
28
+ render(
29
+ <CheckoutForm
30
+ publishableKey="pk_test_example"
31
+ clientSecret="cs_form_secret"
32
+ returnUrl="https://app.test/receipt?session_id={CHECKOUT_SESSION_ID}"
33
+ onNavigate={navigate}
34
+ />,
35
+ );
36
+ let pending: unknown;
37
+ await act(async () => {
38
+ pending = mocks.onConfirm!({ type: "confirm" });
39
+ await pending;
40
+ });
41
+ expect(pending).toBeInstanceOf(Promise);
42
+ expect(mocks.confirm).toHaveBeenCalledWith({
43
+ formConfirmEvent: { type: "confirm" },
44
+ });
45
+ expect(navigate).toHaveBeenCalledWith(
46
+ "https://app.test/receipt?session_id=cs_form",
47
+ );
48
+ });
49
+ it("reports Stripe form confirmation failures", async () => {
50
+ mocks.confirm.mockResolvedValue({
51
+ type: "error",
52
+ error: { message: "Card declined" },
53
+ });
54
+ render(
55
+ <CheckoutForm
56
+ publishableKey="pk_test_example"
57
+ clientSecret="cs_form_secret"
58
+ returnUrl="/receipt"
59
+ onNavigate={vi.fn()}
60
+ />,
61
+ );
62
+ await act(async () => {
63
+ await mocks.onConfirm!({ type: "confirm" });
64
+ });
65
+ expect(screen.getByRole("alert").textContent).toContain("Card declined");
66
+ });
@@ -0,0 +1,123 @@
1
+ // @vitest-environment jsdom
2
+ import { cleanup, render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import { afterEach, beforeEach, expect, it, vi } from "vitest";
5
+ import type { ReactNode } from "react";
6
+ const mocks = vi.hoisted(() => ({
7
+ updateEmail: vi.fn(),
8
+ confirm: vi.fn(),
9
+ submit: vi.fn(),
10
+ confirmPayment: vi.fn(),
11
+ }));
12
+ vi.mock("../components/CheckoutSessionProvider.js", () => ({
13
+ CheckoutSessionProvider: ({ children }: { children: ReactNode }) => children,
14
+ }));
15
+ vi.mock("../components/StripeProvider.js", () => ({
16
+ StripeProvider: ({ children }: { children: ReactNode }) => children,
17
+ }));
18
+ vi.mock("@stripe/react-stripe-js/checkout", () => ({
19
+ PaymentElement: () => <p>Secure payment details</p>,
20
+ useCheckout: () => ({
21
+ type: "success",
22
+ checkout: {
23
+ id: "cs_test",
24
+ canConfirm: false,
25
+ updateEmail: mocks.updateEmail,
26
+ confirm: mocks.confirm,
27
+ },
28
+ }),
29
+ }));
30
+ vi.mock("@stripe/react-stripe-js", () => ({
31
+ PaymentElement: () => <p>Secure payment details</p>,
32
+ useStripe: () => ({ confirmPayment: mocks.confirmPayment }),
33
+ useElements: () => ({ submit: mocks.submit }),
34
+ }));
35
+ import { CheckoutElementsForm, PaymentIntentForm } from "./CheckoutForms.js";
36
+ afterEach(cleanup);
37
+ beforeEach(() => {
38
+ vi.clearAllMocks();
39
+ mocks.updateEmail.mockResolvedValue({ type: "success" });
40
+ mocks.confirm.mockResolvedValue({
41
+ type: "error",
42
+ error: { message: "Card declined" },
43
+ });
44
+ mocks.submit.mockResolvedValue({});
45
+ mocks.confirmPayment.mockResolvedValue({
46
+ error: { message: "Card declined" },
47
+ });
48
+ });
49
+ const props = {
50
+ publishableKey: "pk_test_example",
51
+ clientSecret: "cs_test_secret",
52
+ returnUrl: "https://app.test/receipt?session_id={CHECKOUT_SESSION_ID}",
53
+ email: "buyer@example.com",
54
+ onNavigate: vi.fn(),
55
+ };
56
+ it("sends the email before confirming, including when canConfirm is initially false", async () => {
57
+ render(<CheckoutElementsForm {...props} />);
58
+ await userEvent.click(screen.getByRole("button", { name: "Pay" }));
59
+ expect(mocks.updateEmail).toHaveBeenCalledWith("buyer@example.com");
60
+ expect(mocks.confirm).toHaveBeenCalledOnce();
61
+ expect(screen.getByRole("alert").textContent).toContain("Card declined");
62
+ });
63
+ it("does not confirm after Stripe rejects the email", async () => {
64
+ mocks.updateEmail.mockResolvedValue({
65
+ type: "error",
66
+ error: { message: "Invalid email" },
67
+ });
68
+ render(<CheckoutElementsForm {...props} />);
69
+ await userEvent.click(screen.getByRole("button", { name: "Pay" }));
70
+ expect(screen.getByRole("alert").textContent).toContain("Invalid email");
71
+ expect(mocks.confirm).not.toHaveBeenCalled();
72
+ });
73
+ it("disables confirmation while Stripe is processing and supports a new attempt after failure", async () => {
74
+ let resolve!: (value: unknown) => void;
75
+ mocks.confirm.mockImplementationOnce(
76
+ () =>
77
+ new Promise((done) => {
78
+ resolve = done;
79
+ }),
80
+ );
81
+ render(<CheckoutElementsForm {...props} />);
82
+ await userEvent.click(screen.getByRole("button", { name: "Pay" }));
83
+ expect(
84
+ (screen.getByRole("button", { name: "Processing…" }) as HTMLButtonElement)
85
+ .disabled,
86
+ ).toBe(true);
87
+ resolve({ type: "error", error: { message: "Try another card" } });
88
+ await screen.findByText("Try another card");
89
+ expect(
90
+ (screen.getByRole("button", { name: "Pay" }) as HTMLButtonElement).disabled,
91
+ ).toBe(false);
92
+ });
93
+ it("validates Elements before confirming a Payment Intent", async () => {
94
+ mocks.submit.mockResolvedValue({ error: { message: "Enter a valid card" } });
95
+ render(<PaymentIntentForm {...props} returnUrl="https://app.test/receipt" />);
96
+ await userEvent.click(screen.getByRole("button", { name: "Pay" }));
97
+ expect(screen.getByRole("alert").textContent).toContain("Enter a valid card");
98
+ expect(mocks.confirmPayment).not.toHaveBeenCalled();
99
+ });
100
+ it("includes the contact email and redirects to a server-confirmed receipt", async () => {
101
+ mocks.confirmPayment.mockResolvedValue({ paymentIntent: { id: "pi_test" } });
102
+ const navigate = vi.fn();
103
+ render(
104
+ <PaymentIntentForm
105
+ {...props}
106
+ returnUrl="https://app.test/receipt?ref=avery"
107
+ onNavigate={navigate}
108
+ />,
109
+ );
110
+ await userEvent.click(screen.getByRole("button", { name: "Pay" }));
111
+ expect(mocks.confirmPayment).toHaveBeenCalledWith(
112
+ expect.objectContaining({
113
+ confirmParams: expect.objectContaining({
114
+ payment_method_data: {
115
+ billing_details: { email: "buyer@example.com" },
116
+ },
117
+ }),
118
+ }),
119
+ );
120
+ expect(navigate).toHaveBeenCalledWith(
121
+ "https://app.test/receipt?ref=avery&payment_intent=pi_test",
122
+ );
123
+ });