@fantastic.dev/better-stripe 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (359) hide show
  1. package/CHANGELOG.md +29 -18
  2. package/README.md +123 -91
  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/disputes.d.ts +1 -1
  8. package/dist/client/connect/disputes.d.ts.map +1 -1
  9. package/dist/client/connect/disputes.js +12 -6
  10. package/dist/client/connect/disputes.js.map +1 -1
  11. package/dist/client/connect/paymentMethods.d.ts.map +1 -1
  12. package/dist/client/connect/paymentMethods.js.map +1 -1
  13. package/dist/client/connect/refundActor.d.ts.map +1 -1
  14. package/dist/client/connect/refundActor.js.map +1 -1
  15. package/dist/client/connect/refunds.d.ts.map +1 -1
  16. package/dist/client/connect/refunds.js.map +1 -1
  17. package/dist/client/connect/transfers.d.ts.map +1 -1
  18. package/dist/client/connect/transfers.js +15 -4
  19. package/dist/client/connect/transfers.js.map +1 -1
  20. package/dist/client/core/accountDefaults.d.ts +6 -0
  21. package/dist/client/core/accountDefaults.d.ts.map +1 -0
  22. package/dist/client/core/accountDefaults.js +15 -0
  23. package/dist/client/core/accountDefaults.js.map +1 -0
  24. package/dist/client/core/accounts.d.ts +1 -5
  25. package/dist/client/core/accounts.d.ts.map +1 -1
  26. package/dist/client/core/accounts.js +2 -14
  27. package/dist/client/core/accounts.js.map +1 -1
  28. package/dist/client/core/fees.d.ts.map +1 -1
  29. package/dist/client/core/fees.js +7 -2
  30. package/dist/client/core/fees.js.map +1 -1
  31. package/dist/client/index.d.ts +1 -1
  32. package/dist/client/index.d.ts.map +1 -1
  33. package/dist/client/products/prices.d.ts.map +1 -1
  34. package/dist/client/products/prices.js.map +1 -1
  35. package/dist/client/products/products.d.ts.map +1 -1
  36. package/dist/client/products/products.js +3 -1
  37. package/dist/client/products/products.js.map +1 -1
  38. package/dist/client/utils/owner.d.ts.map +1 -1
  39. package/dist/client/utils/owner.js +3 -1
  40. package/dist/client/utils/owner.js.map +1 -1
  41. package/dist/client/utils/stripeDashboardUrl.d.ts.map +1 -1
  42. package/dist/client/utils/stripeDashboardUrl.js.map +1 -1
  43. package/dist/client/webhooks/helpers.d.ts +10 -0
  44. package/dist/client/webhooks/helpers.d.ts.map +1 -1
  45. package/dist/client/webhooks/helpers.js +18 -0
  46. package/dist/client/webhooks/helpers.js.map +1 -1
  47. package/dist/client/webhooks/processors.d.ts.map +1 -1
  48. package/dist/client/webhooks/processors.js +1 -19
  49. package/dist/client/webhooks/processors.js.map +1 -1
  50. package/dist/component/connect/mutations.d.ts.map +1 -1
  51. package/dist/component/connect/mutations.js +4 -33
  52. package/dist/component/connect/mutations.js.map +1 -1
  53. package/dist/component/connect/schema.d.ts.map +1 -1
  54. package/dist/component/connect/schema.js.map +1 -1
  55. package/dist/component/lib/paymentSeller.d.ts +3 -0
  56. package/dist/component/lib/paymentSeller.d.ts.map +1 -0
  57. package/dist/component/lib/paymentSeller.js +10 -0
  58. package/dist/component/lib/paymentSeller.js.map +1 -0
  59. package/dist/component/lib/reversals.d.ts +10 -0
  60. package/dist/component/lib/reversals.d.ts.map +1 -1
  61. package/dist/component/lib/reversals.js +17 -0
  62. package/dist/component/lib/reversals.js.map +1 -1
  63. package/dist/react/components/ConnectProvider.d.ts +1 -1
  64. package/dist/react/components/ConnectProvider.d.ts.map +1 -1
  65. package/dist/react/components/ConnectProvider.js +18 -4
  66. package/dist/react/components/ConnectProvider.js.map +1 -1
  67. package/dist/react/components/ConnectStatusBadge.d.ts.map +1 -1
  68. package/dist/react/components/ConnectStatusBadge.js.map +1 -1
  69. package/dist/react/components/DisputeDetail.d.ts +4 -1
  70. package/dist/react/components/DisputeDetail.d.ts.map +1 -1
  71. package/dist/react/components/DisputeDetail.js +12 -6
  72. package/dist/react/components/DisputeDetail.js.map +1 -1
  73. package/dist/react/components/DisputesList.d.ts.map +1 -1
  74. package/dist/react/components/DisputesList.js +2 -1
  75. package/dist/react/components/DisputesList.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 +2 -1
  88. package/dist/react/index.d.ts.map +1 -1
  89. package/dist/react/index.js +3 -1
  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 +48 -0
  168. package/dist/react/workflows/Payouts.d.ts.map +1 -0
  169. package/dist/react/workflows/Payouts.js +88 -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 +88 -0
  228. package/docs/react-workflows.md +234 -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/disputes.test.ts +15 -0
  234. package/src/client/connect/disputes.ts +13 -7
  235. package/src/client/connect/paymentMethods.test.ts +3 -1
  236. package/src/client/connect/paymentMethods.ts +4 -1
  237. package/src/client/connect/refundActor.ts +1 -2
  238. package/src/client/connect/refunds.test.ts +35 -10
  239. package/src/client/connect/refunds.ts +4 -1
  240. package/src/client/connect/transfers.test.ts +66 -12
  241. package/src/client/connect/transfers.ts +32 -8
  242. package/src/client/core/accountDefaults.ts +15 -0
  243. package/src/client/core/accounts.ts +14 -17
  244. package/src/client/core/config.test.ts +2 -1
  245. package/src/client/core/descriptors.test.ts +6 -4
  246. package/src/client/core/fees.test.ts +20 -28
  247. package/src/client/core/fees.ts +9 -2
  248. package/src/client/index.test.ts +22 -22
  249. package/src/client/index.ts +1 -1
  250. package/src/client/products/prices.test.ts +8 -2
  251. package/src/client/products/prices.ts +4 -1
  252. package/src/client/products/products.test.ts +61 -20
  253. package/src/client/products/products.ts +3 -1
  254. package/src/client/utils/data.test.ts +3 -1
  255. package/src/client/utils/owner.test.ts +4 -1
  256. package/src/client/utils/owner.ts +5 -1
  257. package/src/client/utils/stripeDashboardUrl.ts +1 -4
  258. package/src/client/webhooks/handler.test.ts +16 -19
  259. package/src/client/webhooks/helpers.test.ts +26 -13
  260. package/src/client/webhooks/helpers.ts +23 -0
  261. package/src/client/webhooks/hooks.test.ts +1 -2
  262. package/src/client/webhooks/processors.test.ts +18 -34
  263. package/src/client/webhooks/processors.ts +1 -23
  264. package/src/client/webhooks.test.ts +1 -2
  265. package/src/component/_generated/api.ts +2 -0
  266. package/src/component/connect/mutations.ts +5 -46
  267. package/src/component/connect/schema.ts +4 -3
  268. package/src/component/connect.test.ts +1 -5
  269. package/src/component/lib/paymentSeller.ts +15 -0
  270. package/src/component/lib/reversals.ts +30 -2
  271. package/src/component/subscription-scoping.test.ts +5 -1
  272. package/src/component/triggers.test.ts +7 -4
  273. package/src/react/components/BuyerBillingView.render.test.tsx +7 -2
  274. package/src/react/components/ConnectProvider.live.test.tsx +61 -0
  275. package/src/react/components/ConnectProvider.tsx +18 -4
  276. package/src/react/components/ConnectStatusBadge.tsx +1 -4
  277. package/src/react/components/DisputeDetail.render.test.tsx +5 -1
  278. package/src/react/components/DisputeDetail.tsx +14 -5
  279. package/src/react/components/DisputesList.render.test.tsx +21 -0
  280. package/src/react/components/DisputesList.tsx +2 -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 +3 -10
  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 +7 -1
  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 +64 -0
  315. package/src/react/workflows/Disputes.tsx +173 -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 +112 -0
  331. package/src/react/workflows/Payouts.tsx +215 -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,48 @@
1
+ "use client";
2
+ import type { ReactNode } from "react";
3
+ import type { SubscriptionRow } from "./Subscriptions.js";
4
+ import { currentSubscriptionFrom } from "./current-subscription.js";
5
+ import { SubscriptionCard } from "../components/SubscriptionCard.js";
6
+ export type SubscriptionOverviewState<Row> = {
7
+ subscription: Row | null;
8
+ isLoading: boolean;
9
+ isEmpty: boolean;
10
+ };
11
+ export type SubscriptionOverviewProps<
12
+ Row extends SubscriptionRow = SubscriptionRow,
13
+ > = {
14
+ subscriptions: Row[] | undefined;
15
+ children?: (state: SubscriptionOverviewState<Row>) => ReactNode;
16
+ };
17
+ export function SubscriptionOverview<Row extends SubscriptionRow>({
18
+ subscriptions,
19
+ children,
20
+ }: SubscriptionOverviewProps<Row>) {
21
+ const subscription = currentSubscriptionFrom(
22
+ (subscriptions ?? []).map((row) => ({
23
+ ...row,
24
+ cancelAtPeriodEnd: row.cancelAtPeriodEnd ?? false,
25
+ })),
26
+ );
27
+ const state = {
28
+ subscription,
29
+ isLoading: subscriptions === undefined,
30
+ isEmpty: subscriptions !== undefined && !subscription,
31
+ };
32
+ if (children) return <>{children(state)}</>;
33
+ if (state.isLoading) return <p role="status">Loading subscription…</p>;
34
+ if (!subscription) return <p>No subscription yet.</p>;
35
+ return (
36
+ <section data-better-stripe="subscription-overview">
37
+ <h2>Your subscription</h2>
38
+ <p>{subscription.stripeSubscriptionId}</p>
39
+ <SubscriptionCard
40
+ subscription={{
41
+ ...subscription,
42
+ isTrialing:
43
+ subscription.isTrialing ?? subscription.status === "trialing",
44
+ }}
45
+ />
46
+ </section>
47
+ );
48
+ }
@@ -0,0 +1,61 @@
1
+ // @vitest-environment jsdom
2
+ import { act, cleanup, renderHook } from "@testing-library/react";
3
+ import { afterEach, expect, it, vi } from "vitest";
4
+ import { useSubscriptionManager } from "./Subscriptions.js";
5
+ afterEach(cleanup);
6
+ it("validates quantity changes and exposes server confirmation as pending webhook synchronization", async () => {
7
+ const onAction = vi.fn().mockResolvedValue(null);
8
+ const { result } = renderHook(() =>
9
+ useSubscriptionManager({
10
+ scopeKey: "buyer",
11
+ subscription: { stripeSubscriptionId: "sub_one", status: "active" },
12
+ onAction,
13
+ }),
14
+ );
15
+ act(() => result.current.setQuantity("1.5"));
16
+ await act(async () => {
17
+ await result.current.run("quantity");
18
+ });
19
+ expect(onAction).not.toHaveBeenCalled();
20
+ expect(result.current.message).toContain("integer");
21
+ act(() => result.current.setQuantity("3"));
22
+ await act(async () => {
23
+ await result.current.run("quantity");
24
+ });
25
+ expect(onAction).toHaveBeenCalledWith({
26
+ type: "quantity",
27
+ stripeSubscriptionId: "sub_one",
28
+ quantity: 3,
29
+ });
30
+ expect(result.current.message).toContain("Waiting for Stripe webhook sync");
31
+ });
32
+
33
+ it("retains the pending subscription action when a second action is attempted", async () => {
34
+ let finish!: () => void;
35
+ const onAction = vi.fn(
36
+ () =>
37
+ new Promise<void>((resolve) => {
38
+ finish = resolve;
39
+ }),
40
+ );
41
+ const { result } = renderHook(() =>
42
+ useSubscriptionManager({
43
+ scopeKey: "buyer",
44
+ subscription: { stripeSubscriptionId: "sub_one", status: "active" },
45
+ onAction,
46
+ }),
47
+ );
48
+ let pending!: Promise<unknown>;
49
+ act(() => {
50
+ pending = result.current.run("pause");
51
+ });
52
+ await act(async () => {
53
+ await result.current.run("cancel");
54
+ });
55
+ expect(result.current.pendingAction).toBe("pause subscription");
56
+ expect(onAction).toHaveBeenCalledTimes(1);
57
+ await act(async () => {
58
+ finish();
59
+ await pending;
60
+ });
61
+ });
@@ -0,0 +1,282 @@
1
+ "use client";
2
+ import { useId, useState, type ReactNode } from "react";
3
+ import { useWorkflowAction } from "./useWorkflowAction.js";
4
+ import {
5
+ Collection,
6
+ type CollectionProps,
7
+ type WorkflowColumn,
8
+ } from "./Collection.js";
9
+ import { SubscriptionCard } from "../components/SubscriptionCard.js";
10
+ export type SubscriptionRow = {
11
+ stripeSubscriptionId: string;
12
+ status: string;
13
+ isTrialing?: boolean;
14
+ cancelAtPeriodEnd?: boolean;
15
+ isCollectionPaused?: boolean;
16
+ currentPeriodEnd?: string | null;
17
+ trialEnd?: string | null;
18
+ quantity?: number;
19
+ userId?: string;
20
+ };
21
+ export type SubscriptionActionType =
22
+ | "pause"
23
+ | "resume"
24
+ | "reactivate"
25
+ | "cancel"
26
+ | "price"
27
+ | "quantity"
28
+ | "trial"
29
+ | "endTrial";
30
+ export type SubscriptionCommand = {
31
+ type: SubscriptionActionType;
32
+ stripeSubscriptionId: string;
33
+ stripePriceId?: string;
34
+ quantity?: number;
35
+ trialEnd?: string;
36
+ };
37
+ export type SubscriptionManagerOptions = {
38
+ scopeKey: string;
39
+ subscription: SubscriptionRow | null | undefined;
40
+ onAction: (command: SubscriptionCommand) => Promise<unknown>;
41
+ };
42
+ const labels: Record<SubscriptionActionType, string> = {
43
+ pause: "pause subscription",
44
+ resume: "resume subscription",
45
+ reactivate: "reactivate subscription",
46
+ cancel: "cancel subscription",
47
+ price: "change plan",
48
+ quantity: "update quantity",
49
+ trial: "update trial end",
50
+ endTrial: "end trial now",
51
+ };
52
+ export function useSubscriptionManager(props: SubscriptionManagerOptions) {
53
+ const scope = `${props.scopeKey}:${props.subscription?.stripeSubscriptionId ?? ""}`;
54
+ const initial = { scope, quantity: "1", selectedPriceId: "", trialEnd: "" };
55
+ const [fields, setFields] = useState(initial);
56
+ if (fields.scope !== scope) setFields(initial);
57
+ const [operation, setOperation] = useState<SubscriptionActionType>("pause");
58
+ const action = useWorkflowAction(async (type: SubscriptionActionType) => {
59
+ setOperation(type);
60
+ if (!props.subscription) throw new Error("Select a subscription first.");
61
+ const request: SubscriptionCommand = {
62
+ type,
63
+ stripeSubscriptionId: props.subscription.stripeSubscriptionId,
64
+ };
65
+ if (type === "quantity") {
66
+ const quantity = Number(fields.quantity);
67
+ if (
68
+ !/^\d+$/.test(fields.quantity) ||
69
+ !Number.isSafeInteger(quantity) ||
70
+ quantity < 1
71
+ )
72
+ throw new Error("Enter a positive integer quantity.");
73
+ request.quantity = quantity;
74
+ }
75
+ if (type === "price") {
76
+ if (!fields.selectedPriceId) throw new Error("Choose a price first.");
77
+ request.stripePriceId = fields.selectedPriceId;
78
+ }
79
+ if (type === "trial") {
80
+ if (!fields.trialEnd || Number.isNaN(new Date(fields.trialEnd).getTime()))
81
+ throw new Error("Enter a valid trial end date.");
82
+ request.trialEnd = fields.trialEnd;
83
+ }
84
+ if (type === "endTrial") request.trialEnd = "now";
85
+ await props.onAction(request);
86
+ return `${labels[type]} requested. Waiting for Stripe webhook sync.`;
87
+ }, scope);
88
+ const active =
89
+ props.subscription?.status === "active" ||
90
+ props.subscription?.status === "trialing";
91
+ return {
92
+ ...fields,
93
+ setQuantity: (quantity: string) =>
94
+ setFields((previous) => ({ ...previous, quantity })),
95
+ setSelectedPriceId: (selectedPriceId: string) =>
96
+ setFields((previous) => ({ ...previous, selectedPriceId })),
97
+ setTrialEnd: (trialEnd: string) =>
98
+ setFields((previous) => ({ ...previous, trialEnd })),
99
+ pendingAction: action.isPending ? labels[operation] : null,
100
+ isPending: action.isPending,
101
+ message: action.error ?? action.result ?? null,
102
+ error: action.error,
103
+ canPause: active && !props.subscription?.isCollectionPaused,
104
+ canResume: !!props.subscription?.isCollectionPaused,
105
+ canReactivate: active && !!props.subscription?.cancelAtPeriodEnd,
106
+ run: action.run,
107
+ };
108
+ }
109
+ export type SubscriptionManagerProps = SubscriptionManagerOptions & {
110
+ children?: (state: ReturnType<typeof useSubscriptionManager>) => ReactNode;
111
+ };
112
+ export function SubscriptionManager(props: SubscriptionManagerProps) {
113
+ const state = useSubscriptionManager(props);
114
+ const id = useId();
115
+ if (props.children) return <>{props.children(state)}</>;
116
+ if (!props.subscription) return <p>No subscription selected.</p>;
117
+ return (
118
+ <section
119
+ data-better-stripe="subscription-manager"
120
+ aria-busy={state.isPending}
121
+ >
122
+ <SubscriptionCard
123
+ subscription={{
124
+ ...props.subscription,
125
+ isTrialing: props.subscription.isTrialing ?? false,
126
+ cancelAtPeriodEnd: props.subscription.cancelAtPeriodEnd ?? false,
127
+ }}
128
+ />
129
+ {state.message && (
130
+ <p role={state.error ? "alert" : "status"}>{state.message}</p>
131
+ )}
132
+ {(
133
+ [
134
+ ["pause", state.canPause],
135
+ ["resume", state.canResume],
136
+ ["reactivate", state.canReactivate],
137
+ [
138
+ "cancel",
139
+ !props.subscription.cancelAtPeriodEnd &&
140
+ props.subscription.status !== "canceled",
141
+ ],
142
+ ] as const
143
+ ).map(([type, enabled]) => (
144
+ <button
145
+ type="button"
146
+ key={type}
147
+ disabled={!enabled || state.isPending}
148
+ onClick={() => {
149
+ void state.run(type);
150
+ }}
151
+ >
152
+ {state.pendingAction === labels[type] ? "Processing…" : labels[type]}
153
+ </button>
154
+ ))}
155
+ <form
156
+ onSubmit={(event) => {
157
+ event.preventDefault();
158
+ void state.run("price");
159
+ }}
160
+ >
161
+ <label htmlFor={`${id}-price`}>Price ID</label>
162
+ <input
163
+ id={`${id}-price`}
164
+ required
165
+ value={state.selectedPriceId}
166
+ onChange={(event) => state.setSelectedPriceId(event.target.value)}
167
+ />
168
+ <button type="submit" disabled={state.isPending}>
169
+ Change plan
170
+ </button>
171
+ </form>
172
+ <form
173
+ onSubmit={(event) => {
174
+ event.preventDefault();
175
+ void state.run("quantity");
176
+ }}
177
+ >
178
+ <label htmlFor={`${id}-quantity`}>Quantity</label>
179
+ <input
180
+ id={`${id}-quantity`}
181
+ type="number"
182
+ min="1"
183
+ step="1"
184
+ required
185
+ value={state.quantity}
186
+ onChange={(event) => state.setQuantity(event.target.value)}
187
+ />
188
+ <button type="submit" disabled={state.isPending}>
189
+ Update quantity
190
+ </button>
191
+ </form>
192
+ <form
193
+ onSubmit={(event) => {
194
+ event.preventDefault();
195
+ void state.run("trial");
196
+ }}
197
+ >
198
+ <label htmlFor={`${id}-trial`}>Trial end</label>
199
+ <input
200
+ id={`${id}-trial`}
201
+ type="datetime-local"
202
+ required
203
+ value={state.trialEnd}
204
+ onChange={(event) => state.setTrialEnd(event.target.value)}
205
+ />
206
+ <button type="submit" disabled={state.isPending}>
207
+ Set trial end
208
+ </button>
209
+ </form>
210
+ <button
211
+ type="button"
212
+ disabled={state.isPending}
213
+ onClick={() => {
214
+ void state.run("endTrial");
215
+ }}
216
+ >
217
+ End trial now
218
+ </button>
219
+ </section>
220
+ );
221
+ }
222
+ export const SUBSCRIPTION_FILTERS = [
223
+ { label: "All", value: "all" },
224
+ { label: "Active", value: "active" },
225
+ { label: "Trialing", value: "trialing" },
226
+ { label: "Past Due", value: "past_due" },
227
+ { label: "Canceled", value: "canceled" },
228
+ ] as const;
229
+ export function useSubscriptionFilter<T extends { status: string }>(
230
+ subscriptions: T[] | undefined,
231
+ ) {
232
+ const [filter, setFilter] = useState("all");
233
+ return {
234
+ filter,
235
+ setFilter,
236
+ filtered:
237
+ filter === "all"
238
+ ? (subscriptions ?? [])
239
+ : (subscriptions?.filter((row) => row.status === filter) ?? []),
240
+ countByStatus: (status: string) =>
241
+ subscriptions?.filter((row) => row.status === status).length ?? 0,
242
+ tabs: SUBSCRIPTION_FILTERS,
243
+ };
244
+ }
245
+ export type SubscriptionsProps<Row extends SubscriptionRow = SubscriptionRow> =
246
+ Omit<CollectionProps<Row>, "getKey" | "columns"> & {
247
+ columns?: WorkflowColumn<Row>[];
248
+ };
249
+ export function Subscriptions<Row extends SubscriptionRow>(
250
+ props: SubscriptionsProps<Row>,
251
+ ) {
252
+ const columns: WorkflowColumn<Row>[] = [
253
+ {
254
+ id: "id",
255
+ label: "Subscription",
256
+ cell: (row) => row.stripeSubscriptionId,
257
+ },
258
+ { id: "status", label: "Status", cell: (row) => row.status },
259
+ {
260
+ id: "period",
261
+ label: "Current period end",
262
+ cell: (row) =>
263
+ row.currentPeriodEnd
264
+ ? new Date(row.currentPeriodEnd).toLocaleDateString()
265
+ : "Not supplied",
266
+ },
267
+ {
268
+ id: "trial",
269
+ label: "Trial end",
270
+ cell: (row) =>
271
+ row.trialEnd ? new Date(row.trialEnd).toLocaleDateString() : "None",
272
+ },
273
+ ];
274
+ return (
275
+ <Collection
276
+ emptyLabel="No subscriptions yet."
277
+ {...props}
278
+ columns={props.columns ?? columns}
279
+ getKey={(row) => row.stripeSubscriptionId}
280
+ />
281
+ );
282
+ }
@@ -0,0 +1,23 @@
1
+ // @vitest-environment jsdom
2
+ import { act, cleanup, renderHook } from "@testing-library/react";
3
+ import { afterEach, expect, it } from "vitest";
4
+ import { useTaskRunner } from "./Tasks.js";
5
+ afterEach(cleanup);
6
+ it("retains per-operation results and errors and keeps only a bounded activity log", async () => {
7
+ const { result } = renderHook(() => useTaskRunner({ maxEntries: 2 }));
8
+ await act(async () => {
9
+ await result.current.run("Products", async () => ({ count: 3 }));
10
+ });
11
+ expect(result.current.results.Products).toBe("Completed");
12
+ await act(async () => {
13
+ await result.current.run("Accounts", async () => {
14
+ throw new Error("Restricted");
15
+ });
16
+ });
17
+ expect(result.current.errors.Accounts).toBe("Restricted");
18
+ await act(async () => {
19
+ await result.current.run("Subscriptions", async () => null);
20
+ });
21
+ expect(result.current.log).toHaveLength(2);
22
+ expect(result.current.results.Products).toBe("Completed");
23
+ });
@@ -0,0 +1,199 @@
1
+ "use client";
2
+ import { useId, useState, type ReactNode } from "react";
3
+ import { useWorkflowAction } from "./useWorkflowAction.js";
4
+ export type WorkflowLogEntry = {
5
+ id: number;
6
+ timestamp: string;
7
+ action: string;
8
+ result: string;
9
+ status: "success" | "error";
10
+ };
11
+ export type TaskRunnerOptions = {
12
+ scopeKey?: string;
13
+ maxEntries?: number;
14
+ /** Explicitly select safe fields for display. Default never serializes returned secrets. */ formatResult?: (
15
+ value: unknown,
16
+ ) => string;
17
+ };
18
+ export function useTaskRunner({
19
+ scopeKey = "tasks",
20
+ maxEntries = 100,
21
+ formatResult = () => "Completed",
22
+ }: TaskRunnerOptions = {}) {
23
+ const initial = {
24
+ scope: scopeKey,
25
+ log: [] as WorkflowLogEntry[],
26
+ results: {} as Record<string, string>,
27
+ errors: {} as Record<string, string>,
28
+ };
29
+ const [state, setState] = useState(initial);
30
+ if (state.scope !== scopeKey) setState(initial);
31
+ const [pendingOperation, setPendingOperation] = useState<string | null>(null);
32
+ const operation = useWorkflowAction(
33
+ async (name: string, task: () => Promise<unknown>) => {
34
+ setPendingOperation(name);
35
+ try {
36
+ return {
37
+ name,
38
+ status: "success" as const,
39
+ value: await task(),
40
+ error: null,
41
+ };
42
+ } catch (error) {
43
+ return {
44
+ name,
45
+ status: "error" as const,
46
+ value: undefined,
47
+ error: error instanceof Error ? error.message : String(error),
48
+ };
49
+ }
50
+ },
51
+ scopeKey,
52
+ );
53
+ const run = async <T,>(
54
+ name: string,
55
+ task: () => Promise<T>,
56
+ ): Promise<T | undefined> => {
57
+ const result = await operation.run(name, task);
58
+ if (!result) return undefined;
59
+ let message: string;
60
+ try {
61
+ message =
62
+ result.status === "error" ? result.error! : formatResult(result.value);
63
+ } catch {
64
+ message = "Completed";
65
+ }
66
+ setState((previous) => ({
67
+ ...previous,
68
+ log: [
69
+ {
70
+ id: (previous.log[0]?.id ?? 0) + 1,
71
+ timestamp: new Date().toLocaleTimeString(),
72
+ action: name,
73
+ result: message,
74
+ status: result.status,
75
+ },
76
+ ...previous.log,
77
+ ].slice(0, Math.max(1, maxEntries)),
78
+ results:
79
+ result.status === "success"
80
+ ? { ...previous.results, [name]: message }
81
+ : previous.results,
82
+ errors: {
83
+ ...previous.errors,
84
+ [name]: result.status === "error" ? message : "",
85
+ },
86
+ }));
87
+ return result.status === "success" ? (result.value as T) : undefined;
88
+ };
89
+ return {
90
+ log: state.log,
91
+ results: state.results,
92
+ errors: state.errors,
93
+ isPending: operation.isPending,
94
+ pendingOperation: operation.isPending ? pendingOperation : null,
95
+ run,
96
+ appendLog: (
97
+ action: string,
98
+ result: string,
99
+ status: "success" | "error" = "success",
100
+ ) =>
101
+ setState((previous) => ({
102
+ ...previous,
103
+ log: [
104
+ {
105
+ id: (previous.log[0]?.id ?? 0) + 1,
106
+ timestamp: new Date().toLocaleTimeString(),
107
+ action,
108
+ result,
109
+ status,
110
+ },
111
+ ...previous.log,
112
+ ].slice(0, Math.max(1, maxEntries)),
113
+ })),
114
+ clearLog: () => setState((previous) => ({ ...previous, log: [] })),
115
+ };
116
+ }
117
+ export type WorkflowTask = {
118
+ id: string;
119
+ label: string;
120
+ description?: string;
121
+ run: () => Promise<unknown>;
122
+ requiresConfirmation?: boolean;
123
+ };
124
+ export type WorkflowTasksProps = TaskRunnerOptions & {
125
+ title?: string;
126
+ tasks: WorkflowTask[];
127
+ children?: (state: ReturnType<typeof useTaskRunner>) => ReactNode;
128
+ };
129
+ /** Unstyled administration/sandbox operations. Authorization and test-mode enforcement belong to the server callbacks. */
130
+ export function WorkflowTasks(props: WorkflowTasksProps) {
131
+ const state = useTaskRunner(props);
132
+ const [confirmed, setConfirmed] = useState<Record<string, boolean>>({});
133
+ const id = useId();
134
+ if (props.children) return <>{props.children(state)}</>;
135
+ return (
136
+ <section data-better-stripe="workflow-tasks" aria-busy={state.isPending}>
137
+ <h2>{props.title ?? "Stripe operations"}</h2>
138
+ {props.tasks.map((task) => (
139
+ <div key={task.id}>
140
+ <h3>{task.label}</h3>
141
+ {task.description && <p>{task.description}</p>}
142
+ {task.requiresConfirmation && (
143
+ <label htmlFor={`${id}-${task.id}`}>
144
+ <input
145
+ id={`${id}-${task.id}`}
146
+ type="checkbox"
147
+ checked={!!confirmed[task.id]}
148
+ onChange={(event) =>
149
+ setConfirmed((previous) => ({
150
+ ...previous,
151
+ [task.id]: event.target.checked,
152
+ }))
153
+ }
154
+ />
155
+ I understand and confirm this action.
156
+ </label>
157
+ )}
158
+ <button
159
+ type="button"
160
+ disabled={
161
+ state.isPending ||
162
+ (task.requiresConfirmation && !confirmed[task.id])
163
+ }
164
+ onClick={() => {
165
+ void state.run(task.id, task.run);
166
+ }}
167
+ >
168
+ {state.pendingOperation === task.id ? "Processing…" : task.label}
169
+ </button>
170
+ {state.errors[task.id] ? (
171
+ <p role="alert">{state.errors[task.id]}</p>
172
+ ) : (
173
+ state.results[task.id] && (
174
+ <p role="status">{state.results[task.id]}</p>
175
+ )
176
+ )}
177
+ </div>
178
+ ))}
179
+ <h3>Activity</h3>
180
+ {!state.log.length ? (
181
+ <p>No operations yet.</p>
182
+ ) : (
183
+ <ol>
184
+ {state.log.map((entry) => (
185
+ <li key={entry.id}>
186
+ {entry.timestamp} · {entry.action} · {entry.result}
187
+ </li>
188
+ ))}
189
+ </ol>
190
+ )}
191
+ </section>
192
+ );
193
+ }
194
+ export function StripeSetup(props: Omit<WorkflowTasksProps, "title">) {
195
+ return <WorkflowTasks {...props} title="Stripe setup" />;
196
+ }
197
+ export function SandboxTesting(props: Omit<WorkflowTasksProps, "title">) {
198
+ return <WorkflowTasks {...props} title="Sandbox testing" />;
199
+ }
@@ -0,0 +1,42 @@
1
+ export const CHECKOUT_OPTIONS = [
2
+ {
3
+ id: "redirect",
4
+ title: "Hosted Checkout",
5
+ api: "Checkout Sessions",
6
+ pros: "Fastest setup. Stripe owns the payment page, order summary, subscriptions, and payment-method UI.",
7
+ cons: "Redirects away from your app. Layout customization is limited.",
8
+ docs: "https://docs.stripe.com/payments/checkout",
9
+ },
10
+ {
11
+ id: "embedded",
12
+ title: "Embedded Checkout",
13
+ api: "Checkout Sessions · embedded_page",
14
+ pros: "Stripe's complete checkout stays inside your app. Less custom checkout code to maintain.",
15
+ cons: "Stripe controls the form layout. The page embeds a Stripe iframe.",
16
+ docs: "https://docs.stripe.com/payments/checkout",
17
+ },
18
+ {
19
+ id: "custom",
20
+ title: "Payment Element + Checkout Sessions",
21
+ api: "Checkout Sessions · elements",
22
+ pros: "Custom page layout with Stripe handling the checkout session and payment state. Recommended for most custom checkouts.",
23
+ cons: "You build the order summary, form controls, and confirmation UI.",
24
+ docs: "https://docs.stripe.com/payments/payment-element",
25
+ },
26
+ {
27
+ id: "payment-intent",
28
+ title: "Payment Element + Payment Intents",
29
+ api: "Payment Intents · Stripe.js",
30
+ pros: "Low-level control over the payment lifecycle, capture, and your own checkout state.",
31
+ cons: "You build pricing, tax, shipping, discounts, and subscription orchestration. This example supports one-time purchases.",
32
+ docs: "https://docs.stripe.com/payments/payment-intents",
33
+ },
34
+ {
35
+ id: "form",
36
+ title: "Embedded Checkout Form",
37
+ api: "Checkout Sessions · form · public preview",
38
+ pros: "Stripe's compact prebuilt checkout form with Appearance API customization.",
39
+ cons: "Preview availability can vary by sandbox. Less layout control than individual Elements.",
40
+ docs: "https://docs.stripe.com/payments/checkout/how-checkout-works?payment-ui=checkout-form",
41
+ },
42
+ ] as const;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Tests for the BTS-84 `currentSubscriptionFrom` fix — a re-subscribed buyer
3
+ * has one canceled-at-period-end sub from their old subscription plus a new
4
+ * active one, and the page must display the active sub rather than the stale
5
+ * canceled one.
6
+ */
7
+ import { describe, expect, it } from "vitest";
8
+
9
+ import { currentSubscriptionFrom } from "./current-subscription.js";
10
+
11
+ describe("currentSubscriptionFrom", () => {
12
+ it("prefers a new active subscription over an old canceled-at-period-end one", () => {
13
+ const subscriptions = [
14
+ { status: "canceled", cancelAtPeriodEnd: true, id: "old" },
15
+ { status: "active", cancelAtPeriodEnd: false, id: "new" },
16
+ ];
17
+
18
+ expect(currentSubscriptionFrom(subscriptions)?.id).toBe("new");
19
+ });
20
+
21
+ it("still prefers a cancel-scheduled subscription while it is active", () => {
22
+ const subscriptions = [
23
+ { status: "active", cancelAtPeriodEnd: true, id: "canceling" },
24
+ { status: "past_due", cancelAtPeriodEnd: false, id: "other" },
25
+ ];
26
+
27
+ expect(currentSubscriptionFrom(subscriptions)?.id).toBe("canceling");
28
+ });
29
+ });