@fantastic.dev/better-stripe 0.6.1 → 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 (359) hide show
  1. package/CHANGELOG.md +28 -16
  2. package/README.md +130 -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 +10 -24
  45. package/dist/client/webhooks/processors.js.map +1 -1
  46. package/dist/component/_generated/component.d.ts +34 -0
  47. package/dist/component/_generated/component.d.ts.map +1 -1
  48. package/dist/component/billing/queries.d.ts +36 -0
  49. package/dist/component/billing/queries.d.ts.map +1 -1
  50. package/dist/component/billing/queries.js +10 -0
  51. package/dist/component/billing/queries.js.map +1 -1
  52. package/dist/component/billing/schema.d.ts +1 -0
  53. package/dist/component/billing/schema.d.ts.map +1 -1
  54. package/dist/component/billing/schema.js +1 -0
  55. package/dist/component/billing/schema.js.map +1 -1
  56. package/dist/component/connect/mutations.d.ts.map +1 -1
  57. package/dist/component/connect/mutations.js +28 -28
  58. package/dist/component/connect/mutations.js.map +1 -1
  59. package/dist/component/connect/schema.d.ts.map +1 -1
  60. package/dist/component/connect/schema.js.map +1 -1
  61. package/dist/component/lib/paymentSeller.d.ts +3 -0
  62. package/dist/component/lib/paymentSeller.d.ts.map +1 -0
  63. package/dist/component/lib/paymentSeller.js +10 -0
  64. package/dist/component/lib/paymentSeller.js.map +1 -0
  65. package/dist/component/lib/reversals.d.ts +10 -0
  66. package/dist/component/lib/reversals.d.ts.map +1 -1
  67. package/dist/component/lib/reversals.js +17 -0
  68. package/dist/component/lib/reversals.js.map +1 -1
  69. package/dist/component/schema.d.ts +1 -0
  70. package/dist/react/components/ConnectProvider.d.ts +1 -1
  71. package/dist/react/components/ConnectProvider.d.ts.map +1 -1
  72. package/dist/react/components/ConnectProvider.js +18 -4
  73. package/dist/react/components/ConnectProvider.js.map +1 -1
  74. package/dist/react/components/ConnectStatusBadge.d.ts.map +1 -1
  75. package/dist/react/components/ConnectStatusBadge.js.map +1 -1
  76. package/dist/react/components/EmbeddedDisputes.d.ts +1 -1
  77. package/dist/react/components/EmbeddedDisputes.d.ts.map +1 -1
  78. package/dist/react/components/EmbeddedDisputes.js +1 -1
  79. package/dist/react/components/EmbeddedDisputes.js.map +1 -1
  80. package/dist/react/components/SplitBreakdown.d.ts.map +1 -1
  81. package/dist/react/components/SplitBreakdown.js +12 -2
  82. package/dist/react/components/SplitBreakdown.js.map +1 -1
  83. package/dist/react/hooks/useCheckoutSession.d.ts.map +1 -1
  84. package/dist/react/hooks/useCheckoutSession.js.map +1 -1
  85. package/dist/react/hooks/useConfirmPayment.d.ts.map +1 -1
  86. package/dist/react/hooks/useConfirmPayment.js.map +1 -1
  87. package/dist/react/index.d.ts +1 -0
  88. package/dist/react/index.d.ts.map +1 -1
  89. package/dist/react/index.js +2 -0
  90. package/dist/react/index.js.map +1 -1
  91. package/dist/react/workflows/Account.d.ts +67 -0
  92. package/dist/react/workflows/Account.d.ts.map +1 -0
  93. package/dist/react/workflows/Account.js +74 -0
  94. package/dist/react/workflows/Account.js.map +1 -0
  95. package/dist/react/workflows/Billing.d.ts +60 -0
  96. package/dist/react/workflows/Billing.d.ts.map +1 -0
  97. package/dist/react/workflows/Billing.js +84 -0
  98. package/dist/react/workflows/Billing.js.map +1 -0
  99. package/dist/react/workflows/BillingPlans.d.ts +45 -0
  100. package/dist/react/workflows/BillingPlans.d.ts.map +1 -0
  101. package/dist/react/workflows/BillingPlans.js +51 -0
  102. package/dist/react/workflows/BillingPlans.js.map +1 -0
  103. package/dist/react/workflows/Checkout.d.ts +46 -0
  104. package/dist/react/workflows/Checkout.d.ts.map +1 -0
  105. package/dist/react/workflows/Checkout.js +61 -0
  106. package/dist/react/workflows/Checkout.js.map +1 -0
  107. package/dist/react/workflows/CheckoutChooser.d.ts +20 -0
  108. package/dist/react/workflows/CheckoutChooser.d.ts.map +1 -0
  109. package/dist/react/workflows/CheckoutChooser.js +24 -0
  110. package/dist/react/workflows/CheckoutChooser.js.map +1 -0
  111. package/dist/react/workflows/CheckoutForms.d.ts +26 -0
  112. package/dist/react/workflows/CheckoutForms.d.ts.map +1 -0
  113. package/dist/react/workflows/CheckoutForms.js +126 -0
  114. package/dist/react/workflows/CheckoutForms.js.map +1 -0
  115. package/dist/react/workflows/ClipboardButton.d.ts +17 -0
  116. package/dist/react/workflows/ClipboardButton.d.ts.map +1 -0
  117. package/dist/react/workflows/ClipboardButton.js +35 -0
  118. package/dist/react/workflows/ClipboardButton.js.map +1 -0
  119. package/dist/react/workflows/Collection.d.ts +30 -0
  120. package/dist/react/workflows/Collection.d.ts.map +1 -0
  121. package/dist/react/workflows/Collection.js +25 -0
  122. package/dist/react/workflows/Collection.js.map +1 -0
  123. package/dist/react/workflows/Collections.d.ts +56 -0
  124. package/dist/react/workflows/Collections.d.ts.map +1 -0
  125. package/dist/react/workflows/Collections.js +111 -0
  126. package/dist/react/workflows/Collections.js.map +1 -0
  127. package/dist/react/workflows/Connect.d.ts +19 -0
  128. package/dist/react/workflows/Connect.d.ts.map +1 -0
  129. package/dist/react/workflows/Connect.js +43 -0
  130. package/dist/react/workflows/Connect.js.map +1 -0
  131. package/dist/react/workflows/Costs.d.ts +48 -0
  132. package/dist/react/workflows/Costs.d.ts.map +1 -0
  133. package/dist/react/workflows/Costs.js +66 -0
  134. package/dist/react/workflows/Costs.js.map +1 -0
  135. package/dist/react/workflows/Disputes.d.ts +36 -0
  136. package/dist/react/workflows/Disputes.d.ts.map +1 -0
  137. package/dist/react/workflows/Disputes.js +74 -0
  138. package/dist/react/workflows/Disputes.js.map +1 -0
  139. package/dist/react/workflows/Earnings.d.ts +42 -0
  140. package/dist/react/workflows/Earnings.d.ts.map +1 -0
  141. package/dist/react/workflows/Earnings.js +38 -0
  142. package/dist/react/workflows/Earnings.js.map +1 -0
  143. package/dist/react/workflows/Financials.d.ts +43 -0
  144. package/dist/react/workflows/Financials.d.ts.map +1 -0
  145. package/dist/react/workflows/Financials.js +57 -0
  146. package/dist/react/workflows/Financials.js.map +1 -0
  147. package/dist/react/workflows/IntegrationSwitch.d.ts +23 -0
  148. package/dist/react/workflows/IntegrationSwitch.d.ts.map +1 -0
  149. package/dist/react/workflows/IntegrationSwitch.js +31 -0
  150. package/dist/react/workflows/IntegrationSwitch.js.map +1 -0
  151. package/dist/react/workflows/Onboarding.d.ts +56 -0
  152. package/dist/react/workflows/Onboarding.d.ts.map +1 -0
  153. package/dist/react/workflows/Onboarding.js +86 -0
  154. package/dist/react/workflows/Onboarding.js.map +1 -0
  155. package/dist/react/workflows/Overview.d.ts +51 -0
  156. package/dist/react/workflows/Overview.d.ts.map +1 -0
  157. package/dist/react/workflows/Overview.js +63 -0
  158. package/dist/react/workflows/Overview.js.map +1 -0
  159. package/dist/react/workflows/PaymentMethods.d.ts +35 -0
  160. package/dist/react/workflows/PaymentMethods.d.ts.map +1 -0
  161. package/dist/react/workflows/PaymentMethods.js +63 -0
  162. package/dist/react/workflows/PaymentMethods.js.map +1 -0
  163. package/dist/react/workflows/PaymentReceipt.d.ts +20 -0
  164. package/dist/react/workflows/PaymentReceipt.d.ts.map +1 -0
  165. package/dist/react/workflows/PaymentReceipt.js +24 -0
  166. package/dist/react/workflows/PaymentReceipt.js.map +1 -0
  167. package/dist/react/workflows/Payouts.d.ts +47 -0
  168. package/dist/react/workflows/Payouts.d.ts.map +1 -0
  169. package/dist/react/workflows/Payouts.js +87 -0
  170. package/dist/react/workflows/Payouts.js.map +1 -0
  171. package/dist/react/workflows/Products.d.ts +92 -0
  172. package/dist/react/workflows/Products.d.ts.map +1 -0
  173. package/dist/react/workflows/Products.js +179 -0
  174. package/dist/react/workflows/Products.js.map +1 -0
  175. package/dist/react/workflows/ReferralLinks.d.ts +26 -0
  176. package/dist/react/workflows/ReferralLinks.d.ts.map +1 -0
  177. package/dist/react/workflows/ReferralLinks.js +30 -0
  178. package/dist/react/workflows/ReferralLinks.js.map +1 -0
  179. package/dist/react/workflows/Refunds.d.ts +72 -0
  180. package/dist/react/workflows/Refunds.d.ts.map +1 -0
  181. package/dist/react/workflows/Refunds.js +95 -0
  182. package/dist/react/workflows/Refunds.js.map +1 -0
  183. package/dist/react/workflows/Setup.d.ts +50 -0
  184. package/dist/react/workflows/Setup.d.ts.map +1 -0
  185. package/dist/react/workflows/Setup.js +59 -0
  186. package/dist/react/workflows/Setup.js.map +1 -0
  187. package/dist/react/workflows/Storefront.d.ts +53 -0
  188. package/dist/react/workflows/Storefront.d.ts.map +1 -0
  189. package/dist/react/workflows/Storefront.js +44 -0
  190. package/dist/react/workflows/Storefront.js.map +1 -0
  191. package/dist/react/workflows/SubscriptionOverview.d.ts +13 -0
  192. package/dist/react/workflows/SubscriptionOverview.d.ts.map +1 -0
  193. package/dist/react/workflows/SubscriptionOverview.js +26 -0
  194. package/dist/react/workflows/SubscriptionOverview.js.map +1 -0
  195. package/dist/react/workflows/Subscriptions.d.ts +92 -0
  196. package/dist/react/workflows/Subscriptions.d.ts.map +1 -0
  197. package/dist/react/workflows/Subscriptions.js +149 -0
  198. package/dist/react/workflows/Subscriptions.js.map +1 -0
  199. package/dist/react/workflows/Tasks.d.ts +40 -0
  200. package/dist/react/workflows/Tasks.d.ts.map +1 -0
  201. package/dist/react/workflows/Tasks.js +113 -0
  202. package/dist/react/workflows/Tasks.js.map +1 -0
  203. package/dist/react/workflows/checkout-options.d.ts +37 -0
  204. package/dist/react/workflows/checkout-options.d.ts.map +1 -0
  205. package/dist/react/workflows/checkout-options.js +43 -0
  206. package/dist/react/workflows/checkout-options.js.map +1 -0
  207. package/dist/react/workflows/current-subscription.d.ts +11 -0
  208. package/dist/react/workflows/current-subscription.d.ts.map +1 -0
  209. package/dist/react/workflows/current-subscription.js +14 -0
  210. package/dist/react/workflows/current-subscription.js.map +1 -0
  211. package/dist/react/workflows/filters.d.ts +17 -0
  212. package/dist/react/workflows/filters.d.ts.map +1 -0
  213. package/dist/react/workflows/filters.js +23 -0
  214. package/dist/react/workflows/filters.js.map +1 -0
  215. package/dist/react/workflows/index.d.ts +70 -0
  216. package/dist/react/workflows/index.d.ts.map +1 -0
  217. package/dist/react/workflows/index.js +38 -0
  218. package/dist/react/workflows/index.js.map +1 -0
  219. package/dist/react/workflows/useWorkflowAction.d.ts +11 -0
  220. package/dist/react/workflows/useWorkflowAction.d.ts.map +1 -0
  221. package/dist/react/workflows/useWorkflowAction.js +68 -0
  222. package/dist/react/workflows/useWorkflowAction.js.map +1 -0
  223. package/dist/react/workflows/useWorkflowResource.d.ts +9 -0
  224. package/dist/react/workflows/useWorkflowResource.d.ts.map +1 -0
  225. package/dist/react/workflows/useWorkflowResource.js +67 -0
  226. package/dist/react/workflows/useWorkflowResource.js.map +1 -0
  227. package/docs/react-workflows-audit.md +66 -0
  228. package/docs/react-workflows.md +231 -0
  229. package/package.json +19 -3
  230. package/src/client/billing/checkout-ui-modes.test.ts +49 -0
  231. package/src/client/billing/checkout.ts +8 -3
  232. package/src/client/billing/invoices.test.ts +12 -6
  233. package/src/client/connect/paymentMethods.test.ts +3 -1
  234. package/src/client/connect/paymentMethods.ts +4 -1
  235. package/src/client/connect/refundActor.ts +1 -2
  236. package/src/client/connect/refunds.test.ts +35 -10
  237. package/src/client/connect/refunds.ts +4 -1
  238. package/src/client/connect/transfers.test.ts +66 -12
  239. package/src/client/connect/transfers.ts +32 -8
  240. package/src/client/core/accountDefaults.ts +15 -0
  241. package/src/client/core/accounts.ts +14 -17
  242. package/src/client/core/config.test.ts +2 -1
  243. package/src/client/core/descriptors.test.ts +6 -4
  244. package/src/client/core/fees.test.ts +20 -28
  245. package/src/client/core/fees.ts +9 -2
  246. package/src/client/index.test.ts +22 -22
  247. package/src/client/index.ts +1 -1
  248. package/src/client/products/prices.test.ts +8 -2
  249. package/src/client/products/prices.ts +4 -1
  250. package/src/client/products/products.test.ts +61 -20
  251. package/src/client/products/products.ts +3 -1
  252. package/src/client/utils/data.test.ts +3 -1
  253. package/src/client/utils/owner.test.ts +4 -1
  254. package/src/client/utils/owner.ts +5 -1
  255. package/src/client/utils/stripeDashboardUrl.ts +1 -4
  256. package/src/client/webhooks/handler.test.ts +16 -19
  257. package/src/client/webhooks/helpers.test.ts +26 -13
  258. package/src/client/webhooks/helpers.ts +23 -0
  259. package/src/client/webhooks/hooks.test.ts +1 -2
  260. package/src/client/webhooks/processors.test.ts +34 -34
  261. package/src/client/webhooks/processors.ts +9 -26
  262. package/src/client/webhooks.test.ts +1 -2
  263. package/src/component/_generated/api.ts +2 -0
  264. package/src/component/_generated/component.ts +46 -0
  265. package/src/component/billing/queries.ts +12 -0
  266. package/src/component/billing/schema.ts +1 -0
  267. package/src/component/connect/mutations.ts +34 -35
  268. package/src/component/connect/schema.ts +4 -3
  269. package/src/component/connect.test.ts +1 -5
  270. package/src/component/lib/paymentSeller.ts +15 -0
  271. package/src/component/lib/reversals.ts +30 -2
  272. package/src/component/seller-case-ownership.test.ts +61 -0
  273. package/src/component/store-subscriptions.test.ts +32 -0
  274. package/src/component/subscription-scoping.test.ts +5 -1
  275. package/src/component/triggers.test.ts +7 -4
  276. package/src/react/components/BuyerBillingView.render.test.tsx +7 -2
  277. package/src/react/components/ConnectProvider.live.test.tsx +61 -0
  278. package/src/react/components/ConnectProvider.tsx +18 -4
  279. package/src/react/components/ConnectStatusBadge.tsx +1 -4
  280. package/src/react/components/DisputeDetail.render.test.tsx +5 -1
  281. package/src/react/components/EarningsSummary.render.test.tsx +2 -6
  282. package/src/react/components/EmbeddedDisputes.render.test.tsx +7 -5
  283. package/src/react/components/EmbeddedDisputes.tsx +4 -1
  284. package/src/react/components/EvidenceForm.render.test.tsx +2 -8
  285. package/src/react/components/SplitBreakdown.render.test.tsx +28 -5
  286. package/src/react/components/SplitBreakdown.tsx +12 -2
  287. package/src/react/hooks/useCheckoutSession.ts +1 -2
  288. package/src/react/hooks/useConfirmPayment.ts +1 -2
  289. package/src/react/hooks/useEarnings.test.ts +43 -8
  290. package/src/react/hooks/useStripeMode.test.ts +5 -8
  291. package/src/react/index.ts +3 -0
  292. package/src/react/workflows/Account.test.tsx +31 -0
  293. package/src/react/workflows/Account.tsx +183 -0
  294. package/src/react/workflows/Billing.test.tsx +45 -0
  295. package/src/react/workflows/Billing.tsx +213 -0
  296. package/src/react/workflows/BillingPlans.test.tsx +31 -0
  297. package/src/react/workflows/BillingPlans.tsx +130 -0
  298. package/src/react/workflows/Checkout.test.tsx +118 -0
  299. package/src/react/workflows/Checkout.tsx +135 -0
  300. package/src/react/workflows/CheckoutChooser.test.tsx +44 -0
  301. package/src/react/workflows/CheckoutChooser.tsx +77 -0
  302. package/src/react/workflows/CheckoutForm.test.tsx +66 -0
  303. package/src/react/workflows/CheckoutForms.test.tsx +123 -0
  304. package/src/react/workflows/CheckoutForms.tsx +247 -0
  305. package/src/react/workflows/ClipboardButton.test.tsx +34 -0
  306. package/src/react/workflows/ClipboardButton.tsx +67 -0
  307. package/src/react/workflows/Collection.tsx +76 -0
  308. package/src/react/workflows/Collections.test.tsx +86 -0
  309. package/src/react/workflows/Collections.tsx +213 -0
  310. package/src/react/workflows/Connect.test.tsx +50 -0
  311. package/src/react/workflows/Connect.tsx +107 -0
  312. package/src/react/workflows/Costs.test.tsx +41 -0
  313. package/src/react/workflows/Costs.tsx +133 -0
  314. package/src/react/workflows/Disputes.test.tsx +25 -0
  315. package/src/react/workflows/Disputes.tsx +164 -0
  316. package/src/react/workflows/Earnings.test.ts +19 -0
  317. package/src/react/workflows/Earnings.tsx +84 -0
  318. package/src/react/workflows/Financials.test.tsx +35 -0
  319. package/src/react/workflows/Financials.tsx +117 -0
  320. package/src/react/workflows/IntegrationSwitch.test.tsx +36 -0
  321. package/src/react/workflows/IntegrationSwitch.tsx +63 -0
  322. package/src/react/workflows/Onboarding.test.tsx +75 -0
  323. package/src/react/workflows/Onboarding.tsx +171 -0
  324. package/src/react/workflows/Overview.test.tsx +57 -0
  325. package/src/react/workflows/Overview.tsx +123 -0
  326. package/src/react/workflows/PaymentMethods.test.tsx +60 -0
  327. package/src/react/workflows/PaymentMethods.tsx +117 -0
  328. package/src/react/workflows/PaymentReceipt.test.tsx +24 -0
  329. package/src/react/workflows/PaymentReceipt.tsx +46 -0
  330. package/src/react/workflows/Payouts.test.tsx +82 -0
  331. package/src/react/workflows/Payouts.tsx +212 -0
  332. package/src/react/workflows/Products.test.tsx +90 -0
  333. package/src/react/workflows/Products.tsx +377 -0
  334. package/src/react/workflows/ReferralLinks.test.tsx +39 -0
  335. package/src/react/workflows/ReferralLinks.tsx +75 -0
  336. package/src/react/workflows/Refunds.test.tsx +75 -0
  337. package/src/react/workflows/Refunds.tsx +223 -0
  338. package/src/react/workflows/Setup.test.tsx +25 -0
  339. package/src/react/workflows/Setup.tsx +166 -0
  340. package/src/react/workflows/Storefront.test.tsx +82 -0
  341. package/src/react/workflows/Storefront.tsx +138 -0
  342. package/src/react/workflows/SubscriptionOverview.test.tsx +27 -0
  343. package/src/react/workflows/SubscriptionOverview.tsx +48 -0
  344. package/src/react/workflows/Subscriptions.test.tsx +61 -0
  345. package/src/react/workflows/Subscriptions.tsx +282 -0
  346. package/src/react/workflows/Tasks.test.tsx +23 -0
  347. package/src/react/workflows/Tasks.tsx +199 -0
  348. package/src/react/workflows/checkout-options.ts +42 -0
  349. package/src/react/workflows/current-subscription.test.ts +29 -0
  350. package/src/react/workflows/current-subscription.ts +25 -0
  351. package/src/react/workflows/filters.test.ts +12 -0
  352. package/src/react/workflows/filters.ts +22 -0
  353. package/src/react/workflows/index.ts +238 -0
  354. package/src/react/workflows/native-interactions.test.tsx +73 -0
  355. package/src/react/workflows/public-workflows.test.tsx +216 -0
  356. package/src/react/workflows/useWorkflowAction.test.tsx +54 -0
  357. package/src/react/workflows/useWorkflowAction.ts +75 -0
  358. package/src/react/workflows/useWorkflowResource.test.tsx +41 -0
  359. package/src/react/workflows/useWorkflowResource.ts +78 -0
@@ -0,0 +1,213 @@
1
+ "use client";
2
+ import { useId, useState, type ReactNode } from "react";
3
+ import { useWorkflowAction } from "./useWorkflowAction.js";
4
+ import { formatPrice } from "../lib/price-helpers.js";
5
+ export type EstimatedInvoice = {
6
+ currency: string;
7
+ total: number;
8
+ nextBillingAt: number;
9
+ lines: {
10
+ id: string;
11
+ description: string;
12
+ amount: number;
13
+ sourceType?: string;
14
+ }[];
15
+ };
16
+ export function useInvoiceEstimate(
17
+ load: () => Promise<EstimatedInvoice>,
18
+ scopeKey: string,
19
+ ) {
20
+ const action = useWorkflowAction(load, scopeKey);
21
+ return {
22
+ invoice: action.result,
23
+ isPending: action.isPending,
24
+ error: action.error,
25
+ refresh: action.run,
26
+ };
27
+ }
28
+ export type InvoiceEstimateProps = {
29
+ scopeKey: string;
30
+ load: () => Promise<EstimatedInvoice>;
31
+ children?: (state: ReturnType<typeof useInvoiceEstimate>) => ReactNode;
32
+ };
33
+ export function InvoiceEstimate({
34
+ scopeKey,
35
+ load,
36
+ children,
37
+ }: InvoiceEstimateProps) {
38
+ const state = useInvoiceEstimate(load, scopeKey);
39
+ if (children) return <>{children(state)}</>;
40
+ return (
41
+ <section data-better-stripe="invoice-estimate">
42
+ <h2>Estimated next invoice</h2>
43
+ <button
44
+ type="button"
45
+ disabled={state.isPending}
46
+ onClick={() => {
47
+ void state.refresh();
48
+ }}
49
+ >
50
+ {state.isPending
51
+ ? "Loading…"
52
+ : state.invoice
53
+ ? "Refresh estimate"
54
+ : "View estimate"}
55
+ </button>
56
+ {state.error && <p role="alert">{state.error}</p>}
57
+ {state.invoice && (
58
+ <>
59
+ <p>
60
+ Next billing date:{" "}
61
+ {new Date(state.invoice.nextBillingAt).toLocaleDateString()}
62
+ </p>
63
+ <ul>
64
+ {state.invoice.lines.map((line) => (
65
+ <li key={line.id}>
66
+ {line.description}{" "}
67
+ <span>{formatPrice(line.amount, state.invoice!.currency)}</span>
68
+ </li>
69
+ ))}
70
+ </ul>
71
+ <p>
72
+ Estimated total:{" "}
73
+ <span>
74
+ {formatPrice(state.invoice.total, state.invoice.currency)}
75
+ </span>
76
+ </p>
77
+ </>
78
+ )}
79
+ </section>
80
+ );
81
+ }
82
+ export type BalanceFundingActions = {
83
+ scopeKey: string;
84
+ change: (args: { enabled: boolean; consent?: true }) => Promise<unknown>;
85
+ };
86
+ export function useBalanceFunding({ scopeKey, change }: BalanceFundingActions) {
87
+ const [state, setState] = useState({ scope: scopeKey, consent: false });
88
+ if (state.scope !== scopeKey) setState({ scope: scopeKey, consent: false });
89
+ const action = useWorkflowAction(async (enabled: boolean) => {
90
+ if (enabled && !state.consent)
91
+ throw new Error(
92
+ "Explicit consent is required to debit a Stripe balance.",
93
+ );
94
+ await change({ enabled, consent: enabled ? true : undefined });
95
+ return true;
96
+ }, scopeKey);
97
+ return {
98
+ consent: state.consent,
99
+ setConsent: (consent: boolean) =>
100
+ setState((previous) => ({ ...previous, consent })),
101
+ error: action.error,
102
+ isPending: action.isPending,
103
+ confirm: async (enabled: boolean) => {
104
+ if (await action.run(enabled))
105
+ setState((previous) => ({ ...previous, consent: false }));
106
+ },
107
+ };
108
+ }
109
+ export type BalancePaymentSourceProps = BalanceFundingActions & {
110
+ enabled: boolean | undefined;
111
+ consentLabel?: string;
112
+ children?: (
113
+ state: ReturnType<typeof useBalanceFunding> & {
114
+ enabled: boolean | undefined;
115
+ },
116
+ ) => ReactNode;
117
+ };
118
+ export function BalancePaymentSource(props: BalancePaymentSourceProps) {
119
+ const state = useBalanceFunding(props);
120
+ const id = useId();
121
+ if (props.children)
122
+ return <>{props.children({ ...state, enabled: props.enabled })}</>;
123
+ return (
124
+ <section data-better-stripe="balance-payment-source">
125
+ <h2>Payment source</h2>
126
+ <p role="status">
127
+ {props.enabled === undefined
128
+ ? "Loading…"
129
+ : props.enabled
130
+ ? "Pay from Stripe balance"
131
+ : "Pay with saved payment method"}
132
+ </p>
133
+ <p>
134
+ If your Stripe balance is insufficient, payment can fail. Switch to a
135
+ saved payment method to pay instead.
136
+ </p>
137
+ {state.error && <p role="alert">{state.error}</p>}
138
+ {props.enabled === false && (
139
+ <>
140
+ <label htmlFor={id}>
141
+ <input
142
+ id={id}
143
+ type="checkbox"
144
+ checked={state.consent}
145
+ onChange={(event) => state.setConsent(event.target.checked)}
146
+ />
147
+ {props.consentLabel ??
148
+ "I authorize recurring platform charges from my Stripe balance."}
149
+ </label>
150
+ <button
151
+ type="button"
152
+ disabled={!state.consent || state.isPending}
153
+ onClick={() => {
154
+ void state.confirm(true);
155
+ }}
156
+ >
157
+ {state.isPending ? "Saving…" : "Confirm balance payments"}
158
+ </button>
159
+ </>
160
+ )}
161
+ {props.enabled && (
162
+ <button
163
+ type="button"
164
+ disabled={state.isPending}
165
+ onClick={() => {
166
+ void state.confirm(false);
167
+ }}
168
+ >
169
+ Use saved payment method
170
+ </button>
171
+ )}
172
+ </section>
173
+ );
174
+ }
175
+ export type BillingPortalProps = {
176
+ scopeKey?: string;
177
+ createSession: () => Promise<string | { url: string }>;
178
+ onNavigate?: (url: string) => void;
179
+ children?: (state: {
180
+ open: () => Promise<true | undefined>;
181
+ isPending: boolean;
182
+ error: string | null;
183
+ }) => ReactNode;
184
+ };
185
+ export function BillingPortal(props: BillingPortalProps) {
186
+ const action = useWorkflowAction(async () => {
187
+ const result = await props.createSession();
188
+ const url = typeof result === "string" ? result : result.url;
189
+ if (!url) throw new Error("Stripe did not return a portal URL.");
190
+ (props.onNavigate ?? ((url) => window.location.assign(url)))(url);
191
+ return true as const;
192
+ }, props.scopeKey);
193
+ const state = {
194
+ open: action.run,
195
+ isPending: action.isPending,
196
+ error: action.error,
197
+ };
198
+ if (props.children) return <>{props.children(state)}</>;
199
+ return (
200
+ <div data-better-stripe="billing-portal">
201
+ {action.error && <p role="alert">{action.error}</p>}
202
+ <button
203
+ type="button"
204
+ disabled={action.isPending}
205
+ onClick={() => {
206
+ void action.run();
207
+ }}
208
+ >
209
+ {action.isPending ? "Opening portal…" : "Open Stripe Billing Portal"}
210
+ </button>
211
+ </div>
212
+ );
213
+ }
@@ -0,0 +1,31 @@
1
+ // @vitest-environment jsdom
2
+ import { act, cleanup, renderHook, waitFor } from "@testing-library/react";
3
+ import { afterEach, expect, it, vi } from "vitest";
4
+ import { usePlanBilling } from "./BillingPlans.js";
5
+ afterEach(cleanup);
6
+ it("confirms a return session once and creates plan checkout only on explicit selection", async () => {
7
+ const confirm = vi.fn().mockResolvedValue(null);
8
+ const createCheckout = vi.fn().mockResolvedValue({
9
+ stripeSessionId: "cs_plan",
10
+ clientSecret: "cs_plan_secret",
11
+ });
12
+ const { result, rerender } = renderHook(
13
+ ({ scope }) =>
14
+ usePlanBilling({
15
+ scopeKey: scope,
16
+ sessionId: "cs_return",
17
+ confirm,
18
+ createCheckout,
19
+ createPortal: async () => "https://billing.stripe.com/session",
20
+ }),
21
+ { initialProps: { scope: "seller_one" } },
22
+ );
23
+ await waitFor(() => expect(confirm).toHaveBeenCalledWith("cs_return"));
24
+ expect(createCheckout).not.toHaveBeenCalled();
25
+ await act(async () => {
26
+ await result.current.choose("growth");
27
+ });
28
+ expect(result.current.clientSecret).toBe("cs_plan_secret");
29
+ rerender({ scope: "seller_two" });
30
+ expect(result.current.clientSecret).toBeUndefined();
31
+ });
@@ -0,0 +1,130 @@
1
+ "use client";
2
+ import type { ReactNode } from "react";
3
+ import { EmbeddedCheckout } from "../components/EmbeddedCheckout.js";
4
+ import { formatPrice } from "../lib/price-helpers.js";
5
+ import { useWorkflowAction } from "./useWorkflowAction.js";
6
+ import { useWorkflowResource } from "./useWorkflowResource.js";
7
+ import type { CheckoutSessionResult } from "./Checkout.js";
8
+ export type PlanBillingOptions<PlanId extends string = string> = {
9
+ scopeKey: string;
10
+ sessionId?: string | null;
11
+ confirm?: (sessionId: string) => Promise<unknown>;
12
+ createCheckout: (planId: PlanId) => Promise<CheckoutSessionResult>;
13
+ createPortal: () => Promise<string | { url: string }>;
14
+ onNavigate?: (url: string) => void;
15
+ };
16
+ export function usePlanBilling<PlanId extends string = string>(
17
+ props: PlanBillingOptions<PlanId>,
18
+ ) {
19
+ const confirmation = useWorkflowResource(
20
+ () =>
21
+ props.sessionId && props.confirm
22
+ ? props.confirm(props.sessionId)
23
+ : Promise.resolve(null),
24
+ `${props.scopeKey}:${props.sessionId ?? ""}`,
25
+ );
26
+ const action = useWorkflowAction(
27
+ async (
28
+ operation: { type: "checkout"; planId: PlanId } | { type: "portal" },
29
+ ) => {
30
+ if (operation.type === "checkout") {
31
+ const session = await props.createCheckout(operation.planId);
32
+ if (!session.clientSecret)
33
+ throw new Error("Stripe did not return a checkout form.");
34
+ return {
35
+ type: "checkout" as const,
36
+ clientSecret: session.clientSecret,
37
+ };
38
+ }
39
+ const result = await props.createPortal();
40
+ const url = typeof result === "string" ? result : result.url;
41
+ if (!url) throw new Error("Stripe did not return a billing portal URL.");
42
+ return { type: "portal" as const, url };
43
+ },
44
+ props.scopeKey,
45
+ );
46
+ return {
47
+ clientSecret:
48
+ action.result?.type === "checkout"
49
+ ? action.result.clientSecret
50
+ : undefined,
51
+ isPending: action.isPending,
52
+ error: action.error ?? confirmation.error,
53
+ choose: (planId: PlanId) => action.run({ type: "checkout", planId }),
54
+ openPortal: async () => {
55
+ const result = await action.run({ type: "portal" });
56
+ if (result?.type === "portal")
57
+ (props.onNavigate ?? ((url) => window.location.assign(url)))(
58
+ result.url,
59
+ );
60
+ },
61
+ };
62
+ }
63
+ export type BillingPlan<PlanId extends string = string> = {
64
+ id: PlanId;
65
+ name: string;
66
+ monthlyAmount: number;
67
+ currency: string;
68
+ description?: string;
69
+ feeDescription?: string;
70
+ };
71
+ export type BillingPlansProps<PlanId extends string = string> =
72
+ PlanBillingOptions<PlanId> & {
73
+ plans: BillingPlan<PlanId>[];
74
+ publishableKey: string;
75
+ currentPlanId?: PlanId;
76
+ children?: (
77
+ state: ReturnType<typeof usePlanBilling<PlanId>> & {
78
+ plans: BillingPlan<PlanId>[];
79
+ },
80
+ ) => ReactNode;
81
+ };
82
+ export function BillingPlans<PlanId extends string = string>(
83
+ props: BillingPlansProps<PlanId>,
84
+ ) {
85
+ const state = usePlanBilling(props);
86
+ if (props.children)
87
+ return <>{props.children({ ...state, plans: props.plans })}</>;
88
+ return (
89
+ <section data-better-stripe="billing-plans" aria-busy={state.isPending}>
90
+ <h2>Billing</h2>
91
+ {state.error && <p role="alert">{state.error}</p>}
92
+ <button
93
+ type="button"
94
+ disabled={state.isPending}
95
+ onClick={() => {
96
+ void state.openPortal();
97
+ }}
98
+ >
99
+ {state.isPending ? "Loading…" : "Manage billing"}
100
+ </button>
101
+ {props.plans.length ? (
102
+ props.plans.map((plan) => (
103
+ <article key={plan.id}>
104
+ <h3>{plan.name}</h3>
105
+ <p>{formatPrice(plan.monthlyAmount, plan.currency)}/month</p>
106
+ {plan.description && <p>{plan.description}</p>}
107
+ {plan.feeDescription && <p>{plan.feeDescription}</p>}
108
+ <button
109
+ type="button"
110
+ disabled={state.isPending || plan.id === props.currentPlanId}
111
+ onClick={() => {
112
+ void state.choose(plan.id);
113
+ }}
114
+ >
115
+ {plan.id === props.currentPlanId ? "Current plan" : "Choose plan"}
116
+ </button>
117
+ </article>
118
+ ))
119
+ ) : (
120
+ <p>No billing plans configured.</p>
121
+ )}
122
+ {state.clientSecret && (
123
+ <EmbeddedCheckout
124
+ publishableKey={props.publishableKey}
125
+ clientSecret={state.clientSecret}
126
+ />
127
+ )}
128
+ </section>
129
+ );
130
+ }
@@ -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
+ });