@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,171 @@
1
+ "use client";
2
+ import { useId, useState, type ReactNode } from "react";
3
+ import { useWorkflowAction } from "./useWorkflowAction.js";
4
+ import { Connect, type ConnectProps } from "./Connect.js";
5
+ import { AccountOnboardingCard } from "../components/AccountOnboardingCard.js";
6
+ export const DEFAULT_CONNECT_COUNTRIES = [
7
+ { code: "US", name: "United States" },
8
+ { code: "GB", name: "United Kingdom" },
9
+ { code: "AU", name: "Australia" },
10
+ { code: "CA", name: "Canada" },
11
+ { code: "DE", name: "Germany" },
12
+ { code: "FR", name: "France" },
13
+ { code: "JP", name: "Japan" },
14
+ { code: "SG", name: "Singapore" },
15
+ ];
16
+ export type OnboardingActions = {
17
+ scopeKey: string;
18
+ initialCountry?: string;
19
+ createAccount: (country: string) => Promise<{ onboardingUrl?: string }>;
20
+ continueOnboarding: () => Promise<{ url?: string }>;
21
+ closeAccount?: () => Promise<unknown>;
22
+ onNavigate?: (url: string) => void;
23
+ };
24
+ export function useOnboarding(props: OnboardingActions) {
25
+ const [fields, setFields] = useState({
26
+ scope: props.scopeKey,
27
+ country: props.initialCountry ?? "US",
28
+ });
29
+ if (fields.scope !== props.scopeKey)
30
+ setFields({ scope: props.scopeKey, country: props.initialCountry ?? "US" });
31
+ const [operation, setOperation] = useState<"create" | "link" | "close">(
32
+ "create",
33
+ );
34
+ const action = useWorkflowAction(
35
+ async (type: "create" | "link" | "close") => {
36
+ setOperation(type);
37
+ if (type === "close") {
38
+ if (!props.closeAccount)
39
+ throw new Error("Account closing is not configured.");
40
+ await props.closeAccount();
41
+ return true;
42
+ }
43
+ const result =
44
+ type === "create"
45
+ ? await props.createAccount(fields.country)
46
+ : await props.continueOnboarding();
47
+ const url =
48
+ "onboardingUrl" in result
49
+ ? result.onboardingUrl
50
+ : "url" in result
51
+ ? result.url
52
+ : undefined;
53
+ if (!url) throw new Error("Stripe did not return an onboarding URL.");
54
+ (props.onNavigate ?? ((url) => window.location.assign(url)))(url);
55
+ return true;
56
+ },
57
+ props.scopeKey,
58
+ );
59
+ const run = action.run;
60
+ return {
61
+ selectedCountry: fields.country,
62
+ setSelectedCountry: (country: string) =>
63
+ setFields((previous) => ({ ...previous, country })),
64
+ isPending: action.isPending,
65
+ isCreating: action.isPending && operation === "create",
66
+ isLinking: action.isPending && operation === "link",
67
+ isRestarting: action.isPending && operation === "close",
68
+ error: action.error,
69
+ createError: operation === "create" ? action.error : null,
70
+ linkError: operation === "link" ? action.error : null,
71
+ restartError: operation === "close" ? action.error : null,
72
+ create: () => run("create"),
73
+ continue: () => run("link"),
74
+ close: () => run("close"),
75
+ };
76
+ }
77
+ export type OnboardingAccount = {
78
+ stripeAccountId: string;
79
+ onboardingStatus?: string;
80
+ missingRequirements?: string[];
81
+ email?: string;
82
+ country?: string;
83
+ };
84
+ export type OnboardingRenderProps = ReturnType<typeof useOnboarding> & {
85
+ account: OnboardingAccount | null | undefined;
86
+ };
87
+ export type OnboardingProps =
88
+ | (OnboardingActions & {
89
+ presentation?: "headless";
90
+ account: OnboardingAccount | null | undefined;
91
+ countries?: { code: string; name: string }[];
92
+ children?: (state: OnboardingRenderProps) => ReactNode;
93
+ })
94
+ | (Omit<ConnectProps, "component"> & { presentation: "embedded" });
95
+ export function Onboarding(props: OnboardingProps) {
96
+ return props.presentation === "embedded" ? (
97
+ <Connect {...props} component="onboarding" />
98
+ ) : (
99
+ <OnboardingBody key={props.scopeKey} {...props} />
100
+ );
101
+ }
102
+ function OnboardingBody(props: Extract<OnboardingProps, { account: unknown }>) {
103
+ const state = useOnboarding(props);
104
+ const id = useId();
105
+ if (props.children)
106
+ return <>{props.children({ ...state, account: props.account })}</>;
107
+ if (props.account === undefined) return <p role="status">Loading account…</p>;
108
+ const status = props.account?.onboardingStatus;
109
+ const normalized =
110
+ status === "complete" || status === "in_progress" || status === "restricted"
111
+ ? status
112
+ : "pending";
113
+ return (
114
+ <section data-better-stripe="onboarding" aria-busy={state.isPending}>
115
+ <h2>Account setup</h2>
116
+ {state.error && <p role="alert">{state.error}</p>}
117
+ {!props.account ? (
118
+ <form
119
+ onSubmit={(event) => {
120
+ event.preventDefault();
121
+ void state.create();
122
+ }}
123
+ >
124
+ <label htmlFor={id}>Country</label>
125
+ <select
126
+ id={id}
127
+ required
128
+ value={state.selectedCountry}
129
+ onChange={(event) => state.setSelectedCountry(event.target.value)}
130
+ >
131
+ {(props.countries ?? DEFAULT_CONNECT_COUNTRIES).map((country) => (
132
+ <option key={country.code} value={country.code}>
133
+ {country.name}
134
+ </option>
135
+ ))}
136
+ </select>
137
+ <button type="submit" disabled={state.isPending}>
138
+ {state.isCreating ? "Creating account…" : "Create account"}
139
+ </button>
140
+ </form>
141
+ ) : (
142
+ <>
143
+ <p>{props.account.stripeAccountId}</p>
144
+ <AccountOnboardingCard
145
+ status={normalized}
146
+ missingRequirements={props.account.missingRequirements}
147
+ onContinue={() => {
148
+ void state.continue();
149
+ }}
150
+ isLoading={state.isPending}
151
+ />
152
+ {props.closeAccount && (
153
+ <details>
154
+ <summary>Close account</summary>
155
+ <p>Closing a Stripe account is irreversible.</p>
156
+ <button
157
+ type="button"
158
+ disabled={state.isPending}
159
+ onClick={() => {
160
+ void state.close();
161
+ }}
162
+ >
163
+ {state.isRestarting ? "Closing…" : "Close account"}
164
+ </button>
165
+ </details>
166
+ )}
167
+ </>
168
+ )}
169
+ </section>
170
+ );
171
+ }
@@ -0,0 +1,57 @@
1
+ // @vitest-environment jsdom
2
+ import { cleanup, render, screen } from "@testing-library/react";
3
+ import { afterEach, expect, it } from "vitest";
4
+ import { Overview, getPayoutTotals } from "./Overview.js";
5
+ afterEach(cleanup);
6
+ it("does not claim live totals until both datasets have loaded", () => {
7
+ const { rerender } = render(
8
+ <Overview products={undefined} subscriptions={[]} />,
9
+ );
10
+ expect(screen.getAllByText("Loading…").length).toBeGreaterThan(0);
11
+ rerender(
12
+ <Overview
13
+ products={[{}]}
14
+ subscriptions={[{ status: "active" }, { status: "canceled" }]}
15
+ />,
16
+ );
17
+ expect(screen.getByText("Total Subscriptions")).toBeTruthy();
18
+ expect(screen.getByText("2")).toBeTruthy();
19
+ });
20
+ it("does not combine payouts in different currencies", () => {
21
+ const result = getPayoutTotals(
22
+ [
23
+ { amount: 100, status: "paid", currency: "usd" },
24
+ { amount: 900, status: "paid", currency: "eur" },
25
+ { amount: 200, status: "in_transit", currency: "usd" },
26
+ ],
27
+ "usd",
28
+ );
29
+ expect(result.totalPayouts).toBe(100);
30
+ expect(result.pendingBalance).toBe(200);
31
+ expect(result.totalEarned).toBe(300);
32
+ });
33
+ it("renders payout activity separately from Stripe account balances", async () => {
34
+ const { PayoutOverview } = await import("./Overview.js");
35
+ render(
36
+ <PayoutOverview
37
+ currency="usd"
38
+ payouts={[
39
+ {
40
+ stripePayoutId: "po_one",
41
+ amount: 1000,
42
+ status: "paid",
43
+ currency: "usd",
44
+ },
45
+ {
46
+ stripePayoutId: "po_two",
47
+ amount: 200,
48
+ status: "pending",
49
+ currency: "usd",
50
+ },
51
+ ]}
52
+ />,
53
+ );
54
+ expect(screen.getByText("Upcoming payouts")).toBeTruthy();
55
+ expect(screen.getByText("$2.00")).toBeTruthy();
56
+ expect(screen.queryByText("Pending Balance")).toBeNull();
57
+ });
@@ -0,0 +1,123 @@
1
+ "use client";
2
+ import { formatPrice } from "../lib/price-helpers.js";
3
+ import type { ReactNode } from "react";
4
+ export type OverviewState = {
5
+ isLoading: boolean;
6
+ totalProducts: number;
7
+ totalSubscriptions: number;
8
+ activeSubscriptions: number;
9
+ metrics: { label: string; value: number | string }[];
10
+ };
11
+ export type OverviewProps = {
12
+ products: unknown[] | undefined;
13
+ subscriptions: { status: string }[] | undefined;
14
+ children?: (state: OverviewState) => ReactNode;
15
+ };
16
+ export function Overview({ products, subscriptions, children }: OverviewProps) {
17
+ const isLoading = products === undefined || subscriptions === undefined;
18
+ const totalProducts = products?.length ?? 0;
19
+ const totalSubscriptions = subscriptions?.length ?? 0;
20
+ const activeSubscriptions =
21
+ subscriptions?.filter((row) => row.status === "active").length ?? 0;
22
+ const state = {
23
+ isLoading,
24
+ totalProducts,
25
+ totalSubscriptions,
26
+ activeSubscriptions,
27
+ metrics: [
28
+ { label: "Total Subscriptions", value: totalSubscriptions },
29
+ { label: "Active Subscriptions", value: activeSubscriptions },
30
+ { label: "Total Products", value: totalProducts },
31
+ { label: "Data Status", value: isLoading ? "Loading…" : "Live" },
32
+ ],
33
+ };
34
+ if (children) return <>{children(state)}</>;
35
+ return (
36
+ <section data-better-stripe="overview">
37
+ <h2>Overview</h2>
38
+ <dl>
39
+ {state.metrics.map((metric) => (
40
+ <div key={metric.label}>
41
+ <dt>{metric.label}</dt>
42
+ <dd>{isLoading ? "Loading…" : metric.value}</dd>
43
+ </div>
44
+ ))}
45
+ </dl>
46
+ </section>
47
+ );
48
+ }
49
+ /** Totals for one currency only. Upcoming payout amounts are not an account's available/pending Stripe balance. */
50
+ export function getPayoutTotals<
51
+ T extends { amount?: number; status: string; currency: string },
52
+ >(payouts: T[] | undefined, currency: string) {
53
+ const rows = payouts?.filter((row) => row.currency === currency) ?? [];
54
+ const totalPayouts = rows
55
+ .filter((row) => row.status === "paid")
56
+ .reduce((sum, row) => sum + (row.amount ?? 0), 0);
57
+ const pendingBalance = rows
58
+ .filter((row) => row.status === "pending" || row.status === "in_transit")
59
+ .reduce((sum, row) => sum + (row.amount ?? 0), 0);
60
+ return {
61
+ totalPayouts,
62
+ pendingBalance,
63
+ totalEarned: totalPayouts + pendingBalance,
64
+ recentPayouts: rows.slice(0, 3),
65
+ };
66
+ }
67
+
68
+ export type PayoutOverviewRow = {
69
+ stripePayoutId: string;
70
+ amount?: number;
71
+ currency: string;
72
+ status: string;
73
+ arrivalDate?: string | null;
74
+ };
75
+ export type PayoutOverviewProps<
76
+ Row extends PayoutOverviewRow = PayoutOverviewRow,
77
+ > = {
78
+ payouts: Row[] | undefined;
79
+ currency: string;
80
+ children?: (
81
+ state: ReturnType<typeof getPayoutTotals<Row>> & {
82
+ isLoading: boolean;
83
+ metrics: { label: string; value: string }[];
84
+ },
85
+ ) => ReactNode;
86
+ };
87
+ /** Payout activity totals. These amounts are not the available or pending account balance. */
88
+ export function PayoutOverview<Row extends PayoutOverviewRow>({
89
+ payouts,
90
+ currency,
91
+ children,
92
+ }: PayoutOverviewProps<Row>) {
93
+ const totals = getPayoutTotals(payouts, currency);
94
+ const state = {
95
+ ...totals,
96
+ isLoading: payouts === undefined,
97
+ metrics: [
98
+ {
99
+ label: "Total payouts tracked",
100
+ value: formatPrice(totals.totalEarned, currency),
101
+ },
102
+ {
103
+ label: "Upcoming payouts",
104
+ value: formatPrice(totals.pendingBalance, currency),
105
+ },
106
+ { label: "Paid out", value: formatPrice(totals.totalPayouts, currency) },
107
+ ],
108
+ };
109
+ if (children) return <>{children(state)}</>;
110
+ return (
111
+ <section data-better-stripe="payout-overview">
112
+ <h2>Payout activity</h2>
113
+ <dl>
114
+ {state.metrics.map((metric) => (
115
+ <div key={metric.label}>
116
+ <dt>{metric.label}</dt>
117
+ <dd>{state.isLoading ? "Loading…" : metric.value}</dd>
118
+ </div>
119
+ ))}
120
+ </dl>
121
+ </section>
122
+ );
123
+ }
@@ -0,0 +1,60 @@
1
+ // @vitest-environment jsdom
2
+ import { act, cleanup, renderHook, waitFor } from "@testing-library/react";
3
+ import { afterEach, expect, it, vi } from "vitest";
4
+ import { usePaymentMethodsManager } from "./PaymentMethods.js";
5
+ afterEach(cleanup);
6
+ it("refreshes saved methods after a successful attach and reports detach failures", async () => {
7
+ const load = vi
8
+ .fn()
9
+ .mockResolvedValueOnce([])
10
+ .mockResolvedValueOnce([{ id: "pm_one", type: "card" }]);
11
+ const attach = vi.fn().mockResolvedValue(null);
12
+ const detach = vi.fn().mockRejectedValue(new Error("Card is still in use"));
13
+ const { result } = renderHook(() =>
14
+ usePaymentMethodsManager({ scopeKey: "buyer", load, attach, detach }),
15
+ );
16
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
17
+ await act(async () => {
18
+ await result.current.attach("pm_one");
19
+ });
20
+ await waitFor(() => expect(result.current.methods).toHaveLength(1));
21
+ expect(attach).toHaveBeenCalledWith("pm_one");
22
+ await act(async () => {
23
+ await result.current.detach("pm_one");
24
+ });
25
+ expect(result.current.deleteError).toBe("Card is still in use");
26
+ expect(result.current.methods).toHaveLength(1);
27
+ });
28
+ it("keeps a pending detach error associated with detach when another action is attempted", async () => {
29
+ let reject!: (error: Error) => void;
30
+ const detach = vi.fn(
31
+ () =>
32
+ new Promise<void>((_, fail) => {
33
+ reject = fail;
34
+ }),
35
+ );
36
+ const attach = vi.fn();
37
+ const { result } = renderHook(() =>
38
+ usePaymentMethodsManager({
39
+ scopeKey: "buyer",
40
+ load: async () => [],
41
+ attach,
42
+ detach,
43
+ }),
44
+ );
45
+ await waitFor(() => expect(result.current.isLoading).toBe(false));
46
+ let pending!: Promise<void>;
47
+ act(() => {
48
+ pending = result.current.detach("pm_one");
49
+ });
50
+ await act(async () => {
51
+ await result.current.attach("pm_two");
52
+ });
53
+ expect(attach).not.toHaveBeenCalled();
54
+ await act(async () => {
55
+ reject(new Error("Detach failed"));
56
+ await pending;
57
+ });
58
+ expect(result.current.deleteError).toBe("Detach failed");
59
+ expect(result.current.attachError).toBeNull();
60
+ });
@@ -0,0 +1,117 @@
1
+ "use client";
2
+ import { useState, type ReactNode } from "react";
3
+ import {
4
+ PaymentMethodsList,
5
+ type PaymentMethodItem,
6
+ } from "../components/PaymentMethodsList.js";
7
+ import { AddCardForm } from "../components/AddCardForm.js";
8
+ import { StripeProvider } from "../components/StripeProvider.js";
9
+ import { useWorkflowAction } from "./useWorkflowAction.js";
10
+ import { useWorkflowResource } from "./useWorkflowResource.js";
11
+ export type PaymentMethodsActions = {
12
+ scopeKey: string;
13
+ load: () => Promise<PaymentMethodItem[]>;
14
+ attach: (paymentMethodId: string) => Promise<unknown>;
15
+ detach: (paymentMethodId: string) => Promise<unknown>;
16
+ };
17
+ export function usePaymentMethodsManager(props: PaymentMethodsActions) {
18
+ const resource = useWorkflowResource(props.load, props.scopeKey);
19
+ const [kind, setKind] = useState<"attach" | "detach">("attach");
20
+ const action = useWorkflowAction(
21
+ async (kind: "attach" | "detach", id: string) => {
22
+ setKind(kind);
23
+ if (!id.trim()) throw new Error("A payment method ID is required.");
24
+ await props[kind](id);
25
+ return true;
26
+ },
27
+ props.scopeKey,
28
+ );
29
+ const run = async (kind: "attach" | "detach", id: string) => {
30
+ if (await action.run(kind, id)) resource.refresh();
31
+ };
32
+ return {
33
+ methods: resource.data ?? [],
34
+ isLoading: resource.isLoading,
35
+ isPending: action.isPending,
36
+ loadError: resource.error,
37
+ attachError: kind === "attach" ? action.error : null,
38
+ deleteError: kind === "detach" ? action.error : null,
39
+ reload: resource.refresh,
40
+ attach: (id: string) => run("attach", id),
41
+ detach: (id: string) => run("detach", id),
42
+ };
43
+ }
44
+ export function toPaymentMethodItem(
45
+ method: {
46
+ id: string;
47
+ type: string;
48
+ card?: {
49
+ brand: string;
50
+ last4: string;
51
+ exp_month: number;
52
+ exp_year: number;
53
+ } | null;
54
+ },
55
+ defaultId?: string,
56
+ ): PaymentMethodItem {
57
+ return {
58
+ id: method.id,
59
+ type: method.type,
60
+ card: method.card
61
+ ? {
62
+ brand: method.card.brand,
63
+ last4: method.card.last4,
64
+ expMonth: method.card.exp_month,
65
+ expYear: method.card.exp_year,
66
+ }
67
+ : undefined,
68
+ isDefault: method.id === defaultId,
69
+ };
70
+ }
71
+ export type PaymentMethodsProps = PaymentMethodsActions & {
72
+ publishableKey: string;
73
+ children?: (state: ReturnType<typeof usePaymentMethodsManager>) => ReactNode;
74
+ };
75
+ export function PaymentMethods(props: PaymentMethodsProps) {
76
+ return <PaymentMethodsBody key={props.scopeKey} {...props} />;
77
+ }
78
+ function PaymentMethodsBody(props: PaymentMethodsProps) {
79
+ const state = usePaymentMethodsManager(props);
80
+ if (props.children) return <>{props.children(state)}</>;
81
+ return (
82
+ <section
83
+ data-better-stripe="payment-methods"
84
+ aria-busy={state.isPending || state.isLoading}
85
+ >
86
+ <h2>Payment methods</h2>
87
+ <StripeProvider publishableKey={props.publishableKey}>
88
+ <fieldset disabled={state.isPending}>
89
+ <legend>Add a card</legend>
90
+ <AddCardForm
91
+ onSuccess={(id) => {
92
+ void state.attach(id);
93
+ }}
94
+ />
95
+ </fieldset>
96
+ </StripeProvider>
97
+ {(state.loadError || state.attachError || state.deleteError) && (
98
+ <p role="alert">
99
+ {state.loadError || state.attachError || state.deleteError}
100
+ </p>
101
+ )}
102
+ <button type="button" disabled={state.isLoading} onClick={state.reload}>
103
+ {state.isLoading ? "Loading…" : "Refresh"}
104
+ </button>
105
+ {state.isLoading ? (
106
+ <p role="status">Loading payment methods…</p>
107
+ ) : (
108
+ <PaymentMethodsList
109
+ methods={state.methods}
110
+ onDelete={(id) => {
111
+ void state.detach(id);
112
+ }}
113
+ />
114
+ )}
115
+ </section>
116
+ );
117
+ }
@@ -0,0 +1,24 @@
1
+ // @vitest-environment jsdom
2
+ import { cleanup, render, screen } from "@testing-library/react";
3
+ import { afterEach, expect, it } from "vitest";
4
+ import { PaymentReceipt } from "./PaymentReceipt.js";
5
+ afterEach(cleanup);
6
+ it("shows success only after the application supplies a confirmed server payment", () => {
7
+ const { rerender } = render(<PaymentReceipt payment={undefined} />);
8
+ expect(screen.getByRole("status").textContent).toContain(
9
+ "Waiting for Stripe",
10
+ );
11
+ rerender(
12
+ <PaymentReceipt
13
+ payment={{ status: "processing", amount: 2900, currency: "usd" }}
14
+ />,
15
+ );
16
+ expect(screen.queryByText("Payment successful!")).toBeNull();
17
+ rerender(
18
+ <PaymentReceipt
19
+ payment={{ status: "succeeded", amount: 2900, currency: "usd" }}
20
+ />,
21
+ );
22
+ expect(screen.getByText("Payment successful!")).toBeTruthy();
23
+ expect(screen.getByText("$29.00")).toBeTruthy();
24
+ });
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import type { ReactNode } from "react";
3
+ import { formatPrice } from "../lib/price-helpers.js";
4
+ export type ReceiptPayment = {
5
+ status: string;
6
+ amount: number;
7
+ currency: string;
8
+ };
9
+ export type PaymentReceiptState = {
10
+ payment: ReceiptPayment | null | undefined;
11
+ isLoading: boolean;
12
+ isConfirmed: boolean;
13
+ title: string;
14
+ message: string;
15
+ amountLabel?: string;
16
+ };
17
+ export type PaymentReceiptProps = {
18
+ /** Supply authoritative server/webhook data, never a client-confirmation result. */ payment:
19
+ ReceiptPayment | null | undefined;
20
+ children?: (state: PaymentReceiptState) => ReactNode;
21
+ };
22
+ export function PaymentReceipt({ payment, children }: PaymentReceiptProps) {
23
+ const isConfirmed = payment?.status === "succeeded";
24
+ const state = {
25
+ payment,
26
+ isLoading: !payment,
27
+ isConfirmed,
28
+ title: isConfirmed ? "Payment successful!" : "Payment status",
29
+ message: !payment
30
+ ? "Waiting for Stripe payment confirmation…"
31
+ : isConfirmed
32
+ ? "Your purchase is confirmed by Stripe."
33
+ : `Stripe payment status: ${payment.status}`,
34
+ amountLabel: payment
35
+ ? formatPrice(payment.amount, payment.currency)
36
+ : undefined,
37
+ };
38
+ if (children) return <>{children(state)}</>;
39
+ return (
40
+ <section data-better-stripe="payment-receipt">
41
+ <h2>{state.title}</h2>
42
+ <p role="status">{state.message}</p>
43
+ {state.amountLabel && <p>{state.amountLabel}</p>}
44
+ </section>
45
+ );
46
+ }
@@ -0,0 +1,82 @@
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 { Payouts } from "./Payouts.js";
6
+ afterEach(cleanup);
7
+ const row = (id: string, status = "paid") => ({
8
+ id,
9
+ status,
10
+ amount: 5000,
11
+ currency: "usd",
12
+ arrivalDate: 1800000000000,
13
+ created: 1799990000000,
14
+ });
15
+ it("shows balances, the actual next payout/date, and merges paginated history without duplicates", async () => {
16
+ const first = row("po_first");
17
+ const next = row("po_next", "pending");
18
+ const load = vi
19
+ .fn()
20
+ .mockResolvedValueOnce({
21
+ balances: [{ currency: "usd", available: 12000, pending: 2000 }],
22
+ upcoming: [next],
23
+ payouts: [next, first],
24
+ nextCursor: "po_first",
25
+ })
26
+ .mockResolvedValueOnce({
27
+ balances: [],
28
+ upcoming: [next],
29
+ payouts: [first, row("po_older")],
30
+ nextCursor: null,
31
+ });
32
+ render(<Payouts scopeKey="acct_one" load={load} />);
33
+ await screen.findByText("$120.00");
34
+ expect(screen.getByText("$20.00")).toBeTruthy();
35
+ expect(screen.getByText("po_next")).toBeTruthy();
36
+ await userEvent.click(
37
+ screen.getByRole("button", { name: "Load more payouts" }),
38
+ );
39
+ await screen.findByText("po_older");
40
+ expect(screen.getAllByText("po_first")).toHaveLength(1);
41
+ expect(screen.getByText("$120.00")).toBeTruthy();
42
+ expect(load).toHaveBeenLastCalledWith("po_first");
43
+ });
44
+ it("shows a load error with a working retry", async () => {
45
+ const load = vi
46
+ .fn()
47
+ .mockRejectedValueOnce(new Error("No balance access"))
48
+ .mockResolvedValueOnce({
49
+ balances: [],
50
+ upcoming: [],
51
+ payouts: [],
52
+ nextCursor: null,
53
+ });
54
+ render(<Payouts scopeKey="acct_one" load={load} />);
55
+ expect((await screen.findByRole("alert")).textContent).toContain(
56
+ "No balance access",
57
+ );
58
+ await userEvent.click(screen.getByRole("button", { name: "Refresh" }));
59
+ await screen.findByText("No previous payouts yet.");
60
+ });
61
+ it("exports payout history with explicit empty/loading states and date formatting", async () => {
62
+ const { PayoutHistory } = await import("./Payouts.js");
63
+ const { rerender } = render(<PayoutHistory rows={undefined} />);
64
+ expect(screen.getByRole("status")).toBeTruthy();
65
+ rerender(<PayoutHistory rows={[]} />);
66
+ expect(screen.getByText("No payouts yet.")).toBeTruthy();
67
+ rerender(
68
+ <PayoutHistory
69
+ rows={[
70
+ {
71
+ id: "po_history",
72
+ amount: 900,
73
+ currency: "usd",
74
+ status: "paid",
75
+ arrivalDate: null,
76
+ },
77
+ ]}
78
+ />,
79
+ );
80
+ expect(screen.getByText("po_history")).toBeTruthy();
81
+ expect(screen.getByText("Not supplied by Stripe")).toBeTruthy();
82
+ });