@mmailaender/convex-creem 0.1.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 (383) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +1176 -0
  3. package/dist/client/helpers.d.ts +17 -0
  4. package/dist/client/helpers.d.ts.map +1 -0
  5. package/dist/client/helpers.js +43 -0
  6. package/dist/client/helpers.js.map +1 -0
  7. package/dist/client/index.d.ts +1041 -0
  8. package/dist/client/index.d.ts.map +1 -0
  9. package/dist/client/index.js +1068 -0
  10. package/dist/client/index.js.map +1 -0
  11. package/dist/client/parsers.d.ts +45 -0
  12. package/dist/client/parsers.d.ts.map +1 -0
  13. package/dist/client/parsers.js +138 -0
  14. package/dist/client/parsers.js.map +1 -0
  15. package/dist/client/polyfill.d.ts +2 -0
  16. package/dist/client/polyfill.d.ts.map +1 -0
  17. package/dist/client/polyfill.js +3 -0
  18. package/dist/client/polyfill.js.map +1 -0
  19. package/dist/component/_generated/api.d.ts +36 -0
  20. package/dist/component/_generated/api.d.ts.map +1 -0
  21. package/dist/component/_generated/api.js +31 -0
  22. package/dist/component/_generated/api.js.map +1 -0
  23. package/dist/component/_generated/component.d.ts +542 -0
  24. package/dist/component/_generated/component.d.ts.map +1 -0
  25. package/dist/component/_generated/component.js +11 -0
  26. package/dist/component/_generated/component.js.map +1 -0
  27. package/dist/component/_generated/dataModel.d.ts +46 -0
  28. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  29. package/dist/component/_generated/dataModel.js +11 -0
  30. package/dist/component/_generated/dataModel.js.map +1 -0
  31. package/dist/component/_generated/server.d.ts +121 -0
  32. package/dist/component/_generated/server.d.ts.map +1 -0
  33. package/dist/component/_generated/server.js +78 -0
  34. package/dist/component/_generated/server.js.map +1 -0
  35. package/dist/component/convex.config.d.ts +3 -0
  36. package/dist/component/convex.config.d.ts.map +1 -0
  37. package/dist/component/convex.config.js +3 -0
  38. package/dist/component/convex.config.js.map +1 -0
  39. package/dist/component/lib.d.ts +1005 -0
  40. package/dist/component/lib.d.ts.map +1 -0
  41. package/dist/component/lib.js +647 -0
  42. package/dist/component/lib.js.map +1 -0
  43. package/dist/component/schema.d.ts +191 -0
  44. package/dist/component/schema.d.ts.map +1 -0
  45. package/dist/component/schema.js +104 -0
  46. package/dist/component/schema.js.map +1 -0
  47. package/dist/component/util.d.ts +61 -0
  48. package/dist/component/util.d.ts.map +1 -0
  49. package/dist/component/util.js +142 -0
  50. package/dist/component/util.js.map +1 -0
  51. package/dist/core/catalog.d.ts +18 -0
  52. package/dist/core/catalog.d.ts.map +1 -0
  53. package/dist/core/catalog.js +82 -0
  54. package/dist/core/catalog.js.map +1 -0
  55. package/dist/core/index.d.ts +9 -0
  56. package/dist/core/index.d.ts.map +1 -0
  57. package/dist/core/index.js +9 -0
  58. package/dist/core/index.js.map +1 -0
  59. package/dist/core/markdown.d.ts +12 -0
  60. package/dist/core/markdown.d.ts.map +1 -0
  61. package/dist/core/markdown.js +26 -0
  62. package/dist/core/markdown.js.map +1 -0
  63. package/dist/core/payments.d.ts +11 -0
  64. package/dist/core/payments.d.ts.map +1 -0
  65. package/dist/core/payments.js +27 -0
  66. package/dist/core/payments.js.map +1 -0
  67. package/dist/core/pendingCheckout.d.ts +15 -0
  68. package/dist/core/pendingCheckout.d.ts.map +1 -0
  69. package/dist/core/pendingCheckout.js +40 -0
  70. package/dist/core/pendingCheckout.js.map +1 -0
  71. package/dist/core/resolver.d.ts +11 -0
  72. package/dist/core/resolver.d.ts.map +1 -0
  73. package/dist/core/resolver.js +106 -0
  74. package/dist/core/resolver.js.map +1 -0
  75. package/dist/core/selectors.d.ts +12 -0
  76. package/dist/core/selectors.d.ts.map +1 -0
  77. package/dist/core/selectors.js +18 -0
  78. package/dist/core/selectors.js.map +1 -0
  79. package/dist/core/subscriptionUpdate.d.ts +20 -0
  80. package/dist/core/subscriptionUpdate.d.ts.map +1 -0
  81. package/dist/core/subscriptionUpdate.js +64 -0
  82. package/dist/core/subscriptionUpdate.js.map +1 -0
  83. package/dist/core/types.d.ts +170 -0
  84. package/dist/core/types.d.ts.map +1 -0
  85. package/dist/core/types.js +15 -0
  86. package/dist/core/types.js.map +1 -0
  87. package/dist/design-system/colors/color-utils.d.ts +10 -0
  88. package/dist/design-system/colors/color-utils.d.ts.map +1 -0
  89. package/dist/design-system/colors/color-utils.js +91 -0
  90. package/dist/design-system/colors/color-utils.js.map +1 -0
  91. package/dist/design-system/colors/config.d.ts +33 -0
  92. package/dist/design-system/colors/config.d.ts.map +1 -0
  93. package/dist/design-system/colors/config.js +224 -0
  94. package/dist/design-system/colors/config.js.map +1 -0
  95. package/dist/design-system/colors/index.d.ts +3 -0
  96. package/dist/design-system/colors/index.d.ts.map +1 -0
  97. package/dist/design-system/colors/index.js +3 -0
  98. package/dist/design-system/colors/index.js.map +1 -0
  99. package/dist/design-system/rounded/config.d.ts +31 -0
  100. package/dist/design-system/rounded/config.d.ts.map +1 -0
  101. package/dist/design-system/rounded/config.js +76 -0
  102. package/dist/design-system/rounded/config.js.map +1 -0
  103. package/dist/design-system/rounded/index.d.ts +2 -0
  104. package/dist/design-system/rounded/index.d.ts.map +1 -0
  105. package/dist/design-system/rounded/index.js +2 -0
  106. package/dist/design-system/rounded/index.js.map +1 -0
  107. package/dist/design-system/typography/config.d.ts +55 -0
  108. package/dist/design-system/typography/config.d.ts.map +1 -0
  109. package/dist/design-system/typography/config.js +308 -0
  110. package/dist/design-system/typography/config.js.map +1 -0
  111. package/dist/design-system/typography/index.d.ts +3 -0
  112. package/dist/design-system/typography/index.d.ts.map +1 -0
  113. package/dist/design-system/typography/index.js +3 -0
  114. package/dist/design-system/typography/index.js.map +1 -0
  115. package/dist/design-system/typography/tokens.d.ts +23 -0
  116. package/dist/design-system/typography/tokens.d.ts.map +1 -0
  117. package/dist/design-system/typography/tokens.js +99 -0
  118. package/dist/design-system/typography/tokens.js.map +1 -0
  119. package/dist/react/hooks/useCheckoutSuccessParams.d.ts +2 -0
  120. package/dist/react/hooks/useCheckoutSuccessParams.d.ts.map +1 -0
  121. package/dist/react/hooks/useCheckoutSuccessParams.js +5 -0
  122. package/dist/react/hooks/useCheckoutSuccessParams.js.map +1 -0
  123. package/dist/react/index.d.ts +25 -0
  124. package/dist/react/index.d.ts.map +1 -0
  125. package/dist/react/index.js +22 -0
  126. package/dist/react/index.js.map +1 -0
  127. package/dist/react/primitives/BillingGate.d.ts +8 -0
  128. package/dist/react/primitives/BillingGate.d.ts.map +1 -0
  129. package/dist/react/primitives/BillingGate.js +13 -0
  130. package/dist/react/primitives/BillingGate.js.map +1 -0
  131. package/dist/react/primitives/BillingToggle.d.ts +8 -0
  132. package/dist/react/primitives/BillingToggle.d.ts.map +1 -0
  133. package/dist/react/primitives/BillingToggle.js +12 -0
  134. package/dist/react/primitives/BillingToggle.js.map +1 -0
  135. package/dist/react/primitives/CheckoutButton.d.ts +11 -0
  136. package/dist/react/primitives/CheckoutButton.d.ts.map +1 -0
  137. package/dist/react/primitives/CheckoutButton.js +21 -0
  138. package/dist/react/primitives/CheckoutButton.js.map +1 -0
  139. package/dist/react/primitives/CheckoutSuccessSummary.d.ts +7 -0
  140. package/dist/react/primitives/CheckoutSuccessSummary.d.ts.map +1 -0
  141. package/dist/react/primitives/CheckoutSuccessSummary.js +11 -0
  142. package/dist/react/primitives/CheckoutSuccessSummary.js.map +1 -0
  143. package/dist/react/primitives/CustomerPortalButton.d.ts +8 -0
  144. package/dist/react/primitives/CustomerPortalButton.d.ts.map +1 -0
  145. package/dist/react/primitives/CustomerPortalButton.js +21 -0
  146. package/dist/react/primitives/CustomerPortalButton.js.map +1 -0
  147. package/dist/react/primitives/NumberInput.d.ts +11 -0
  148. package/dist/react/primitives/NumberInput.d.ts.map +1 -0
  149. package/dist/react/primitives/NumberInput.js +18 -0
  150. package/dist/react/primitives/NumberInput.js.map +1 -0
  151. package/dist/react/primitives/OneTimeCheckoutButton.d.ts +11 -0
  152. package/dist/react/primitives/OneTimeCheckoutButton.d.ts.map +1 -0
  153. package/dist/react/primitives/OneTimeCheckoutButton.js +4 -0
  154. package/dist/react/primitives/OneTimeCheckoutButton.js.map +1 -0
  155. package/dist/react/primitives/OneTimePaymentStatusBadge.d.ts +6 -0
  156. package/dist/react/primitives/OneTimePaymentStatusBadge.d.ts.map +1 -0
  157. package/dist/react/primitives/OneTimePaymentStatusBadge.js +11 -0
  158. package/dist/react/primitives/OneTimePaymentStatusBadge.js.map +1 -0
  159. package/dist/react/primitives/PaymentWarningBanner.d.ts +7 -0
  160. package/dist/react/primitives/PaymentWarningBanner.d.ts.map +1 -0
  161. package/dist/react/primitives/PaymentWarningBanner.js +18 -0
  162. package/dist/react/primitives/PaymentWarningBanner.js.map +1 -0
  163. package/dist/react/primitives/PricingCard.d.ts +37 -0
  164. package/dist/react/primitives/PricingCard.d.ts.map +1 -0
  165. package/dist/react/primitives/PricingCard.js +125 -0
  166. package/dist/react/primitives/PricingCard.js.map +1 -0
  167. package/dist/react/primitives/PricingSection.d.ts +39 -0
  168. package/dist/react/primitives/PricingSection.d.ts.map +1 -0
  169. package/dist/react/primitives/PricingSection.js +24 -0
  170. package/dist/react/primitives/PricingSection.js.map +1 -0
  171. package/dist/react/primitives/ScheduledChangeBanner.d.ts +8 -0
  172. package/dist/react/primitives/ScheduledChangeBanner.d.ts.map +1 -0
  173. package/dist/react/primitives/ScheduledChangeBanner.js +13 -0
  174. package/dist/react/primitives/ScheduledChangeBanner.js.map +1 -0
  175. package/dist/react/primitives/SegmentControl.d.ts +11 -0
  176. package/dist/react/primitives/SegmentControl.d.ts.map +1 -0
  177. package/dist/react/primitives/SegmentControl.js +8 -0
  178. package/dist/react/primitives/SegmentControl.js.map +1 -0
  179. package/dist/react/primitives/SegmentGroup.d.ts +14 -0
  180. package/dist/react/primitives/SegmentGroup.d.ts.map +1 -0
  181. package/dist/react/primitives/SegmentGroup.js +11 -0
  182. package/dist/react/primitives/SegmentGroup.js.map +1 -0
  183. package/dist/react/primitives/TrialLimitBanner.d.ts +7 -0
  184. package/dist/react/primitives/TrialLimitBanner.d.ts.map +1 -0
  185. package/dist/react/primitives/TrialLimitBanner.js +14 -0
  186. package/dist/react/primitives/TrialLimitBanner.js.map +1 -0
  187. package/dist/react/shared.d.ts +28 -0
  188. package/dist/react/shared.d.ts.map +1 -0
  189. package/dist/react/shared.js +109 -0
  190. package/dist/react/shared.js.map +1 -0
  191. package/dist/react/widgets/BillingPortal.d.ts +9 -0
  192. package/dist/react/widgets/BillingPortal.d.ts.map +1 -0
  193. package/dist/react/widgets/BillingPortal.js +30 -0
  194. package/dist/react/widgets/BillingPortal.js.map +1 -0
  195. package/dist/react/widgets/ProductItem.d.ts +8 -0
  196. package/dist/react/widgets/ProductItem.d.ts.map +1 -0
  197. package/dist/react/widgets/ProductItem.js +14 -0
  198. package/dist/react/widgets/ProductItem.js.map +1 -0
  199. package/dist/react/widgets/ProductRoot.d.ts +16 -0
  200. package/dist/react/widgets/ProductRoot.d.ts.map +1 -0
  201. package/dist/react/widgets/ProductRoot.js +171 -0
  202. package/dist/react/widgets/ProductRoot.js.map +1 -0
  203. package/dist/react/widgets/SubscriptionItem.d.ts +27 -0
  204. package/dist/react/widgets/SubscriptionItem.d.ts.map +1 -0
  205. package/dist/react/widgets/SubscriptionItem.js +32 -0
  206. package/dist/react/widgets/SubscriptionItem.js.map +1 -0
  207. package/dist/react/widgets/SubscriptionRoot.d.ts +16 -0
  208. package/dist/react/widgets/SubscriptionRoot.d.ts.map +1 -0
  209. package/dist/react/widgets/SubscriptionRoot.js +405 -0
  210. package/dist/react/widgets/SubscriptionRoot.js.map +1 -0
  211. package/dist/react/widgets/index.d.ts +19 -0
  212. package/dist/react/widgets/index.d.ts.map +1 -0
  213. package/dist/react/widgets/index.js +16 -0
  214. package/dist/react/widgets/index.js.map +1 -0
  215. package/dist/react/widgets/productGroupContext.d.ts +6 -0
  216. package/dist/react/widgets/productGroupContext.d.ts.map +1 -0
  217. package/dist/react/widgets/productGroupContext.js +3 -0
  218. package/dist/react/widgets/productGroupContext.js.map +1 -0
  219. package/dist/react/widgets/subscriptionContext.d.ts +6 -0
  220. package/dist/react/widgets/subscriptionContext.d.ts.map +1 -0
  221. package/dist/react/widgets/subscriptionContext.js +3 -0
  222. package/dist/react/widgets/subscriptionContext.js.map +1 -0
  223. package/dist/react/widgets/types.d.ts +171 -0
  224. package/dist/react/widgets/types.d.ts.map +1 -0
  225. package/dist/react/widgets/types.js +2 -0
  226. package/dist/react/widgets/types.js.map +1 -0
  227. package/dist/svelte/index.d.ts +22 -0
  228. package/dist/svelte/index.d.ts.map +1 -0
  229. package/dist/svelte/index.js +20 -0
  230. package/dist/svelte/index.js.map +1 -0
  231. package/dist/svelte/primitives/BillingGate.svelte +28 -0
  232. package/dist/svelte/primitives/BillingToggle.svelte +27 -0
  233. package/dist/svelte/primitives/CheckoutButton.svelte +60 -0
  234. package/dist/svelte/primitives/CheckoutSuccessSummary.svelte +34 -0
  235. package/dist/svelte/primitives/CustomerPortalButton.svelte +60 -0
  236. package/dist/svelte/primitives/NumberInput.svelte +71 -0
  237. package/dist/svelte/primitives/OneTimeCheckoutButton.svelte +37 -0
  238. package/dist/svelte/primitives/OneTimePaymentStatusBadge.svelte +20 -0
  239. package/dist/svelte/primitives/PaymentWarningBanner.svelte +30 -0
  240. package/dist/svelte/primitives/PricingCard.svelte +356 -0
  241. package/dist/svelte/primitives/PricingSection.svelte +121 -0
  242. package/dist/svelte/primitives/ScheduledChangeBanner.svelte +46 -0
  243. package/dist/svelte/primitives/SegmentControl.svelte +38 -0
  244. package/dist/svelte/primitives/SegmentGroup.svelte +52 -0
  245. package/dist/svelte/primitives/TrialLimitBanner.svelte +32 -0
  246. package/dist/svelte/primitives/shared.d.ts +13 -0
  247. package/dist/svelte/primitives/shared.d.ts.map +1 -0
  248. package/dist/svelte/primitives/shared.js +87 -0
  249. package/dist/svelte/primitives/shared.js.map +1 -0
  250. package/dist/svelte/widgets/BillingPortal.svelte +55 -0
  251. package/dist/svelte/widgets/Product.svelte +35 -0
  252. package/dist/svelte/widgets/ProductRoot.svelte +428 -0
  253. package/dist/svelte/widgets/Subscription.svelte +52 -0
  254. package/dist/svelte/widgets/SubscriptionRoot.svelte +690 -0
  255. package/dist/svelte/widgets/index.d.ts +19 -0
  256. package/dist/svelte/widgets/index.d.ts.map +1 -0
  257. package/dist/svelte/widgets/index.js +16 -0
  258. package/dist/svelte/widgets/index.js.map +1 -0
  259. package/dist/svelte/widgets/productGroupContext.d.ts +6 -0
  260. package/dist/svelte/widgets/productGroupContext.d.ts.map +1 -0
  261. package/dist/svelte/widgets/productGroupContext.js +2 -0
  262. package/dist/svelte/widgets/productGroupContext.js.map +1 -0
  263. package/dist/svelte/widgets/subscriptionContext.d.ts +6 -0
  264. package/dist/svelte/widgets/subscriptionContext.d.ts.map +1 -0
  265. package/dist/svelte/widgets/subscriptionContext.js +2 -0
  266. package/dist/svelte/widgets/subscriptionContext.js.map +1 -0
  267. package/dist/svelte/widgets/types.d.ts +171 -0
  268. package/dist/svelte/widgets/types.d.ts.map +1 -0
  269. package/dist/svelte/widgets/types.js +2 -0
  270. package/dist/svelte/widgets/types.js.map +1 -0
  271. package/package.json +182 -0
  272. package/src/client/helpers.test.ts +139 -0
  273. package/src/client/helpers.ts +51 -0
  274. package/src/client/index.test.ts +1554 -0
  275. package/src/client/index.ts +1504 -0
  276. package/src/client/parsers.test.ts +1017 -0
  277. package/src/client/parsers.ts +182 -0
  278. package/src/client/polyfill.ts +2 -0
  279. package/src/component/_generated/api.ts +52 -0
  280. package/src/component/_generated/component.ts +619 -0
  281. package/src/component/_generated/dataModel.ts +60 -0
  282. package/src/component/_generated/server.ts +156 -0
  283. package/src/component/convex.config.ts +3 -0
  284. package/src/component/lib.test.ts +1359 -0
  285. package/src/component/lib.ts +726 -0
  286. package/src/component/schema.ts +112 -0
  287. package/src/component/util.test.ts +281 -0
  288. package/src/component/util.ts +228 -0
  289. package/src/core/catalog.test.ts +212 -0
  290. package/src/core/catalog.ts +119 -0
  291. package/src/core/index.ts +8 -0
  292. package/src/core/markdown.test.ts +43 -0
  293. package/src/core/markdown.ts +26 -0
  294. package/src/core/payments.test.ts +69 -0
  295. package/src/core/payments.ts +33 -0
  296. package/src/core/pendingCheckout.test.ts +44 -0
  297. package/src/core/pendingCheckout.ts +40 -0
  298. package/src/core/resolver.test.ts +283 -0
  299. package/src/core/resolver.ts +160 -0
  300. package/src/core/selectors.test.ts +119 -0
  301. package/src/core/selectors.ts +35 -0
  302. package/src/core/subscriptionUpdate.test.ts +164 -0
  303. package/src/core/subscriptionUpdate.ts +102 -0
  304. package/src/core/types.ts +220 -0
  305. package/src/design-system/README.md +40 -0
  306. package/src/design-system/base.css +27 -0
  307. package/src/design-system/colors/color-utils.ts +110 -0
  308. package/src/design-system/colors/config.ts +282 -0
  309. package/src/design-system/colors/index.ts +2 -0
  310. package/src/design-system/colors/utilities.css +2328 -0
  311. package/src/design-system/components/badges.css +65 -0
  312. package/src/design-system/components/buttons.css +256 -0
  313. package/src/design-system/components/dialog.css +218 -0
  314. package/src/design-system/components/icon-buttons.css +115 -0
  315. package/src/design-system/components/inputs.css +94 -0
  316. package/src/design-system/components/links.css +53 -0
  317. package/src/design-system/components/prose.css +67 -0
  318. package/src/design-system/components/segment-control.css +303 -0
  319. package/src/design-system/index.css +21 -0
  320. package/src/design-system/rounded/config.ts +91 -0
  321. package/src/design-system/rounded/index.ts +1 -0
  322. package/src/design-system/rounded/utilities.css +37 -0
  323. package/src/design-system/typography/config.ts +340 -0
  324. package/src/design-system/typography/index.ts +2 -0
  325. package/src/design-system/typography/tokens.ts +148 -0
  326. package/src/design-system/typography/utilities.css +728 -0
  327. package/src/library.css +20 -0
  328. package/src/react/hooks/useCheckoutSuccessParams.ts +7 -0
  329. package/src/react/index.tsx +47 -0
  330. package/src/react/primitives/BillingGate.tsx +26 -0
  331. package/src/react/primitives/BillingToggle.tsx +29 -0
  332. package/src/react/primitives/CheckoutButton.tsx +47 -0
  333. package/src/react/primitives/CheckoutSuccessSummary.tsx +36 -0
  334. package/src/react/primitives/CustomerPortalButton.tsx +50 -0
  335. package/src/react/primitives/NumberInput.tsx +83 -0
  336. package/src/react/primitives/OneTimeCheckoutButton.tsx +27 -0
  337. package/src/react/primitives/OneTimePaymentStatusBadge.tsx +18 -0
  338. package/src/react/primitives/PaymentWarningBanner.tsx +33 -0
  339. package/src/react/primitives/PricingCard.tsx +421 -0
  340. package/src/react/primitives/PricingSection.tsx +129 -0
  341. package/src/react/primitives/ScheduledChangeBanner.tsx +52 -0
  342. package/src/react/primitives/SegmentControl.tsx +32 -0
  343. package/src/react/primitives/SegmentGroup.tsx +53 -0
  344. package/src/react/primitives/TrialLimitBanner.tsx +32 -0
  345. package/src/react/shared.ts +138 -0
  346. package/src/react/widgets/BillingPortal.tsx +56 -0
  347. package/src/react/widgets/ProductItem.tsx +26 -0
  348. package/src/react/widgets/ProductRoot.tsx +441 -0
  349. package/src/react/widgets/SubscriptionItem.tsx +71 -0
  350. package/src/react/widgets/SubscriptionRoot.tsx +759 -0
  351. package/src/react/widgets/index.ts +36 -0
  352. package/src/react/widgets/productGroupContext.ts +10 -0
  353. package/src/react/widgets/subscriptionContext.ts +10 -0
  354. package/src/react/widgets/types.ts +179 -0
  355. package/src/svelte/index.ts +43 -0
  356. package/src/svelte/primitives/BillingGate.svelte +28 -0
  357. package/src/svelte/primitives/BillingToggle.svelte +27 -0
  358. package/src/svelte/primitives/CheckoutButton.svelte +60 -0
  359. package/src/svelte/primitives/CheckoutSuccessSummary.svelte +34 -0
  360. package/src/svelte/primitives/CustomerPortalButton.svelte +60 -0
  361. package/src/svelte/primitives/NumberInput.svelte +71 -0
  362. package/src/svelte/primitives/OneTimeCheckoutButton.svelte +37 -0
  363. package/src/svelte/primitives/OneTimePaymentStatusBadge.svelte +20 -0
  364. package/src/svelte/primitives/PaymentWarningBanner.svelte +30 -0
  365. package/src/svelte/primitives/PricingCard.svelte +356 -0
  366. package/src/svelte/primitives/PricingSection.svelte +121 -0
  367. package/src/svelte/primitives/ScheduledChangeBanner.svelte +46 -0
  368. package/src/svelte/primitives/SegmentControl.svelte +38 -0
  369. package/src/svelte/primitives/SegmentGroup.svelte +52 -0
  370. package/src/svelte/primitives/TrialLimitBanner.svelte +32 -0
  371. package/src/svelte/primitives/shared.ts +113 -0
  372. package/src/svelte/svelte.d.ts +6 -0
  373. package/src/svelte/widgets/BillingPortal.svelte +55 -0
  374. package/src/svelte/widgets/Product.svelte +35 -0
  375. package/src/svelte/widgets/ProductRoot.svelte +428 -0
  376. package/src/svelte/widgets/Subscription.svelte +52 -0
  377. package/src/svelte/widgets/SubscriptionRoot.svelte +690 -0
  378. package/src/svelte/widgets/index.ts +36 -0
  379. package/src/svelte/widgets/productGroupContext.ts +7 -0
  380. package/src/svelte/widgets/subscriptionContext.ts +7 -0
  381. package/src/svelte/widgets/types.ts +179 -0
  382. package/src/tailwind.css +6 -0
  383. package/src/test.ts +18 -0
@@ -0,0 +1,1504 @@
1
+ import "./polyfill.js";
2
+ import { Creem as CreemSDK } from "creem";
3
+ import type {
4
+ CheckoutEntity,
5
+ CustomerEntity,
6
+ SubscriptionEntity,
7
+ } from "creem/models/components";
8
+ import { Webhook, WebhookVerificationError } from "standardwebhooks";
9
+ import {
10
+ getEntityId,
11
+ lowerCaseHeaders,
12
+ toHex,
13
+ constantTimeEqual,
14
+ normalizeSignature,
15
+ } from "./helpers.js";
16
+ import {
17
+ type CreemWebhookEvent,
18
+ getEventType,
19
+ getEventData,
20
+ getCustomerId,
21
+ getConvexEntityId,
22
+ parseSubscription,
23
+ parseCheckout,
24
+ parseProduct,
25
+ } from "./parsers.js";
26
+ import {
27
+ type FunctionReference,
28
+ type HttpRouter,
29
+ actionGeneric,
30
+ httpActionGeneric,
31
+ mutationGeneric,
32
+ queryGeneric,
33
+ } from "convex/server";
34
+ import { ConvexError, type Infer, v } from "convex/values";
35
+ import schema from "../component/schema.js";
36
+ import {
37
+ type RunMutationCtx,
38
+ type RunSchedulerMutationCtx,
39
+ type RunQueryCtx,
40
+ convertToDatabaseProduct,
41
+ convertToDatabaseSubscription,
42
+ convertToOrder,
43
+ type RunActionCtx,
44
+ } from "../component/util.js";
45
+ import type { ComponentApi } from "../component/_generated/component.js";
46
+ import { resolveBillingSnapshot as defaultResolveBillingSnapshot } from "../core/resolver.js";
47
+ import type {
48
+ BillingSnapshot,
49
+ PaymentSnapshot,
50
+ SubscriptionSnapshot,
51
+ } from "../core/types.js";
52
+
53
+ export * from "../core/index.js";
54
+ export type { RunSchedulerMutationCtx } from "../component/util.js";
55
+ export {
56
+ getEntityId,
57
+ lowerCaseHeaders,
58
+ toHex,
59
+ constantTimeEqual,
60
+ normalizeSignature,
61
+ } from "./helpers.js";
62
+ export {
63
+ type CreemWebhookEvent,
64
+ getEventType,
65
+ getEventData,
66
+ getCustomerId,
67
+ getConvexEntityId,
68
+ parseSubscription,
69
+ parseCheckout,
70
+ parseProduct,
71
+ manualParseSubscription,
72
+ } from "./parsers.js";
73
+
74
+ /** Convex validator for the `subscriptions` table. Use with `v.object(subscriptionValidator.fields)` in custom functions. */
75
+ export const subscriptionValidator = schema.tables.subscriptions.validator;
76
+ /** TypeScript type for a subscription document (inferred from the Convex schema). */
77
+ export type Subscription = Infer<typeof subscriptionValidator>;
78
+
79
+ // ── Shared arg validators for custom actions / mutations ──────────────
80
+ // Use these when writing your own Convex functions that wrap creem methods
81
+ // (e.g. for RBAC). They match exactly what the connected widgets send.
82
+
83
+ /**
84
+ * Convex arg validator for checkout creation.
85
+ * Matches the args sent by `<Subscription.Root>` and `<Product.Root>` widgets.
86
+ * Use in your own `action()` definitions for custom RBAC wrappers.
87
+ */
88
+ export const checkoutCreateArgs = {
89
+ productId: v.string(),
90
+ successUrl: v.optional(v.string()),
91
+ fallbackSuccessUrl: v.optional(v.string()),
92
+ units: v.optional(v.number()),
93
+ metadata: v.optional(v.record(v.string(), v.string())),
94
+ discountCode: v.optional(v.string()),
95
+ theme: v.optional(v.union(v.literal("light"), v.literal("dark"))),
96
+ };
97
+
98
+ /**
99
+ * Convex arg validator for subscription updates (plan switch or seat change).
100
+ * Matches the args sent by `<Subscription.Root>` widgets.
101
+ */
102
+ export const subscriptionUpdateArgs = {
103
+ subscriptionId: v.optional(v.string()),
104
+ productId: v.optional(v.string()),
105
+ units: v.optional(v.number()),
106
+ updateBehavior: v.optional(
107
+ v.union(
108
+ v.literal("proration-charge-immediately"),
109
+ v.literal("proration-charge"),
110
+ v.literal("proration-none"),
111
+ ),
112
+ ),
113
+ };
114
+
115
+ /**
116
+ * Convex arg validator for subscription cancellation.
117
+ * Matches the args sent by `<Subscription.Root>` cancel button.
118
+ */
119
+ export const subscriptionCancelArgs = {
120
+ subscriptionId: v.optional(v.string()),
121
+ revokeImmediately: v.optional(v.boolean()),
122
+ };
123
+
124
+ /**
125
+ * Convex arg validator for subscription resume.
126
+ * Matches the args sent by `<Subscription.Root>` resume button.
127
+ */
128
+ export const subscriptionResumeArgs = {
129
+ subscriptionId: v.optional(v.string()),
130
+ };
131
+
132
+ /**
133
+ * Convex arg validator for subscription pause.
134
+ * Matches the args sent by `<Subscription.Root>` pause button.
135
+ */
136
+ export const subscriptionPauseArgs = {
137
+ subscriptionId: v.optional(v.string()),
138
+ };
139
+
140
+ /** Function reference type for internal mutations that receive a subscription document. */
141
+ export type SubscriptionHandler = FunctionReference<
142
+ "mutation",
143
+ "internal",
144
+ { subscription: Subscription }
145
+ >;
146
+
147
+ /**
148
+ * Map of webhook event type → handler function.
149
+ * Handlers run **after** the component's built-in processing (customer/subscription/order upserts).
150
+ * The `ctx` is a Convex mutation context — you can read/write to your own tables.
151
+ *
152
+ * @example
153
+ * ```ts
154
+ * creem.registerRoutes(http, {
155
+ * events: {
156
+ * "checkout.completed": async (ctx, event) => {
157
+ * // Grant entitlements, send emails, log analytics
158
+ * },
159
+ * },
160
+ * });
161
+ * ```
162
+ */
163
+ export type WebhookEventHandlers = Record<
164
+ string,
165
+ (ctx: RunMutationCtx, event: CreemWebhookEvent) => Promise<void> | void
166
+ >;
167
+
168
+ /**
169
+ * Callback that resolves the authenticated user for `creem.api({ resolve })`.
170
+ * Called on every generated Convex function to determine the billing entity.
171
+ *
172
+ * - `userId` — your app's user ID (stored in checkout metadata as `convexUserId`)
173
+ * - `email` — user's email (passed to Creem for customer creation)
174
+ * - `entityId` — billing entity ID. For personal billing, same as `userId`.
175
+ * For org billing, return the org ID so all billing scopes to the organization.
176
+ *
177
+ * @example
178
+ * ```ts
179
+ * const resolve: ApiResolver = async (ctx) => {
180
+ * const user = await ctx.runQuery(api.users.currentUser);
181
+ * return { userId: user._id, email: user.email, entityId: user._id };
182
+ * };
183
+ * ```
184
+ */
185
+ export type ApiResolver = (ctx: RunQueryCtx) => Promise<{
186
+ userId: string;
187
+ email: string;
188
+ entityId: string;
189
+ }>;
190
+
191
+ /**
192
+ * Configuration for the Creem Convex component.
193
+ * All fields are optional — environment variables are used as fallbacks.
194
+ */
195
+ type CreemConfig = {
196
+ /**
197
+ * Default cancel mode for subscriptions.
198
+ * - `"immediate"` — cancel and revoke access now
199
+ * - `"scheduled"` — cancel at end of current billing period
200
+ * - Omit to use Creem's store-level default.
201
+ */
202
+ cancelMode?: "immediate" | "scheduled";
203
+ /** Creem API key. Falls back to `CREEM_API_KEY` env var. */
204
+ apiKey?: string;
205
+ /** Creem webhook signing secret. Falls back to `CREEM_WEBHOOK_SECRET` env var. */
206
+ webhookSecret?: string;
207
+ /** Creem SDK server index (for non-default endpoints). Falls back to `CREEM_SERVER_IDX` env var. */
208
+ serverIdx?: number;
209
+ /** Creem SDK server URL override (for test/staging). Falls back to `CREEM_SERVER_URL` env var. */
210
+ serverURL?: string;
211
+ };
212
+
213
+ /**
214
+ * Main entry point for the Creem–Convex integration.
215
+ *
216
+ * Instantiate once in your `convex/billing.ts` and use its methods
217
+ * to manage subscriptions, checkouts, products, customers, and orders.
218
+ *
219
+ * **Two usage patterns:**
220
+ * 1. **Quick start** — call `creem.api({ resolve })` to generate ready-to-export Convex functions
221
+ * 2. **Full control** — use namespace getters (`creem.subscriptions.*`, `creem.checkouts.*`, etc.)
222
+ * directly in your own Convex functions for custom auth/RBAC
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * import { Creem } from "@mmailaender/convex-creem";
227
+ * import { components } from "./_generated/api";
228
+ *
229
+ * export const creem = new Creem(components.creem);
230
+ * ```
231
+ */
232
+ export class Creem {
233
+ /** Direct access to the Creem SDK client, pre-configured with your API key. Use for resources without webhook sync (licenses, discounts, transactions). */
234
+ public sdk: CreemSDK;
235
+ private apiKey: string;
236
+ private webhookSecret: string;
237
+ private serverIdx?: number;
238
+ private serverURL?: string;
239
+
240
+ constructor(
241
+ public component: ComponentApi,
242
+ private config: CreemConfig = {},
243
+ ) {
244
+ this.apiKey = config.apiKey ?? process.env["CREEM_API_KEY"] ?? "";
245
+ this.webhookSecret =
246
+ config.webhookSecret ?? process.env["CREEM_WEBHOOK_SECRET"] ?? "";
247
+ this.serverIdx =
248
+ config.serverIdx ??
249
+ (process.env["CREEM_SERVER_IDX"]
250
+ ? Number(process.env["CREEM_SERVER_IDX"])
251
+ : undefined);
252
+ this.serverURL = config.serverURL ?? process.env["CREEM_SERVER_URL"];
253
+
254
+ this.sdk = new CreemSDK({
255
+ apiKey: this.apiKey,
256
+ ...(this.serverIdx !== undefined ? { serverIdx: this.serverIdx } : {}),
257
+ ...(this.serverURL ? { serverURL: this.serverURL } : {}),
258
+ });
259
+ }
260
+ private getCustomerByEntityId(ctx: RunQueryCtx, entityId: string) {
261
+ return ctx.runQuery(this.component.lib.getCustomerByEntityId, { entityId });
262
+ }
263
+
264
+ /** Pull all products from the Creem API into the Convex database. Typically called once via `internalAction` or the CLI. */
265
+ async syncProducts(ctx: RunActionCtx) {
266
+ await ctx.runAction(this.component.lib.syncProducts, {
267
+ apiKey: this.apiKey,
268
+ serverIdx: this.serverIdx,
269
+ serverURL: this.serverURL,
270
+ });
271
+ }
272
+
273
+ private async createCheckoutSession(
274
+ ctx: RunMutationCtx,
275
+ {
276
+ productId,
277
+ entityId,
278
+ userId,
279
+ email,
280
+ successUrl,
281
+ units,
282
+ metadata,
283
+ }: {
284
+ productId: string;
285
+ entityId: string;
286
+ userId: string;
287
+ email: string;
288
+ successUrl?: string;
289
+ units?: number;
290
+ metadata?: Record<string, string>;
291
+ },
292
+ ): Promise<CheckoutEntity> {
293
+ const dbCustomer = await ctx.runQuery(
294
+ this.component.lib.getCustomerByEntityId,
295
+ {
296
+ entityId,
297
+ },
298
+ );
299
+
300
+ const checkout = await this.sdk.checkouts.create({
301
+ productId,
302
+ ...(successUrl ? { successUrl } : {}),
303
+ units,
304
+ metadata: {
305
+ ...(metadata ?? {}),
306
+ convexUserId: userId,
307
+ convexBillingEntityId: entityId,
308
+ },
309
+ customer: dbCustomer ? { id: dbCustomer.id } : { email },
310
+ });
311
+
312
+ if (!dbCustomer) {
313
+ const customerId = getEntityId(checkout.customer);
314
+ if (customerId) {
315
+ const customerObj =
316
+ typeof checkout.customer === "object" ? checkout.customer : undefined;
317
+ await ctx.runMutation(this.component.lib.insertCustomer, {
318
+ id: customerId,
319
+ entityId,
320
+ email: customerObj?.email,
321
+ name: customerObj?.name ?? undefined,
322
+ country: customerObj?.country,
323
+ mode: customerObj?.mode,
324
+ });
325
+ }
326
+ }
327
+
328
+ return checkout;
329
+ }
330
+
331
+ private async createCustomerPortalSession(
332
+ ctx: RunActionCtx,
333
+ { entityId }: { entityId: string },
334
+ ) {
335
+ const customer = await ctx.runQuery(
336
+ this.component.lib.getCustomerByEntityId,
337
+ { entityId },
338
+ );
339
+
340
+ if (!customer) {
341
+ throw new ConvexError("Customer not found");
342
+ }
343
+
344
+ const portal = await this.sdk.customers.generateBillingLinks({
345
+ customerId: customer.id,
346
+ });
347
+ return { url: portal.customerPortalLink };
348
+ }
349
+
350
+ private listProducts(
351
+ ctx: RunQueryCtx,
352
+ { includeArchived }: { includeArchived?: boolean } = {},
353
+ ) {
354
+ return ctx.runQuery(this.component.lib.listProducts, {
355
+ includeArchived,
356
+ });
357
+ }
358
+ private async getCurrentSubscription(
359
+ ctx: RunQueryCtx,
360
+ { entityId }: { entityId: string },
361
+ ) {
362
+ const subscription = await ctx.runQuery(
363
+ this.component.lib.getCurrentSubscription,
364
+ {
365
+ entityId,
366
+ },
367
+ );
368
+ if (!subscription) {
369
+ return null;
370
+ }
371
+ const product = await ctx.runQuery(this.component.lib.getProduct, {
372
+ id: subscription.productId,
373
+ });
374
+ if (!product) {
375
+ throw new ConvexError("Product not found");
376
+ }
377
+ return {
378
+ ...subscription,
379
+ product,
380
+ };
381
+ }
382
+ /** Return active subscriptions for an entity, excluding ended and expired trials. */
383
+ private listUserSubscriptions(
384
+ ctx: RunQueryCtx,
385
+ { entityId }: { entityId: string },
386
+ ) {
387
+ return ctx.runQuery(this.component.lib.listUserSubscriptions, {
388
+ entityId,
389
+ });
390
+ }
391
+ /** Return paid one-time orders for an entity. */
392
+ private listUserOrders(ctx: RunQueryCtx, { entityId }: { entityId: string }) {
393
+ return ctx.runQuery(this.component.lib.listUserOrders, {
394
+ entityId,
395
+ });
396
+ }
397
+ /** Return all subscriptions for an entity, including ended and expired trials. */
398
+ private listAllUserSubscriptions(
399
+ ctx: RunQueryCtx,
400
+ { entityId }: { entityId: string },
401
+ ) {
402
+ return ctx.runQuery(this.component.lib.listAllUserSubscriptions, {
403
+ entityId,
404
+ });
405
+ }
406
+ private getProduct(ctx: RunQueryCtx, { productId }: { productId: string }) {
407
+ return ctx.runQuery(this.component.lib.getProduct, { id: productId });
408
+ }
409
+ private toSubscriptionSnapshot(
410
+ subscription: Subscription,
411
+ ): SubscriptionSnapshot {
412
+ return {
413
+ id: subscription.id,
414
+ productId: subscription.productId,
415
+ status: subscription.status,
416
+ recurringInterval: subscription.recurringInterval,
417
+ seats: subscription.seats,
418
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
419
+ currentPeriodEnd: subscription.currentPeriodEnd,
420
+ trialEnd: subscription.trialEnd ?? null,
421
+ };
422
+ }
423
+
424
+ /**
425
+ * Resolve the current billing state for a billing entity.
426
+ * Returns plan, status, available actions, subscription metadata, etc.
427
+ * Used internally by `getBillingModel` and exposed for custom billing UIs.
428
+ */
429
+ async getBillingSnapshot(
430
+ ctx: RunQueryCtx,
431
+ {
432
+ entityId,
433
+ payment,
434
+ }: {
435
+ entityId: string;
436
+ payment?: PaymentSnapshot | null;
437
+ },
438
+ ): Promise<BillingSnapshot> {
439
+ const [currentSubscription, allSubscriptions] = await Promise.all([
440
+ this.getCurrentSubscription(ctx, { entityId }),
441
+ this.listAllUserSubscriptions(ctx, { entityId }),
442
+ ]);
443
+
444
+ return defaultResolveBillingSnapshot({
445
+ currentSubscription: currentSubscription
446
+ ? this.toSubscriptionSnapshot(currentSubscription)
447
+ : null,
448
+ allSubscriptions: allSubscriptions.map((subscription) =>
449
+ this.toSubscriptionSnapshot(subscription),
450
+ ),
451
+ payment: payment ?? null,
452
+ userContext: undefined,
453
+ });
454
+ }
455
+
456
+ private async verifyWebhook(body: string, headers: Record<string, string>) {
457
+ if (!this.webhookSecret) {
458
+ throw new ConvexError("Missing CREEM_WEBHOOK_SECRET");
459
+ }
460
+
461
+ const normalized = lowerCaseHeaders(headers);
462
+ const webhookId = normalized["webhook-id"];
463
+ const webhookTimestamp = normalized["webhook-timestamp"];
464
+ const webhookSignature = normalized["webhook-signature"];
465
+
466
+ if (webhookId && webhookTimestamp && webhookSignature) {
467
+ new Webhook(this.webhookSecret).verify(body, {
468
+ "webhook-id": webhookId,
469
+ "webhook-timestamp": webhookTimestamp,
470
+ "webhook-signature": webhookSignature,
471
+ });
472
+ return;
473
+ }
474
+
475
+ const creemSignature =
476
+ normalized["creem-signature"] ?? normalized["x-creem-signature"];
477
+ if (!creemSignature) {
478
+ throw new WebhookVerificationError("Missing webhook signature");
479
+ }
480
+
481
+ const key = await crypto.subtle.importKey(
482
+ "raw",
483
+ new TextEncoder().encode(this.webhookSecret),
484
+ { name: "HMAC", hash: "SHA-256" },
485
+ false,
486
+ ["sign"],
487
+ );
488
+ const digest = await crypto.subtle.sign(
489
+ "HMAC",
490
+ key,
491
+ new TextEncoder().encode(body),
492
+ );
493
+ const expected = toHex(new Uint8Array(digest));
494
+ if (!constantTimeEqual(normalizeSignature(creemSignature), expected)) {
495
+ throw new WebhookVerificationError("Invalid webhook signature");
496
+ }
497
+ }
498
+
499
+ /** Upsert a customer record if we have both entityId and customerId. */
500
+ private async upsertCustomerFromWebhook(
501
+ ctx: RunMutationCtx,
502
+ customerId: string | null,
503
+ entityId: string | null,
504
+ customerEntity?: CustomerEntity | null,
505
+ ) {
506
+ if (!customerId || !entityId) return;
507
+ try {
508
+ await ctx.runMutation(this.component.lib.insertCustomer, {
509
+ id: customerId,
510
+ entityId,
511
+ email: customerEntity?.email,
512
+ name: customerEntity?.name ?? undefined,
513
+ country: customerEntity?.country,
514
+ mode: customerEntity?.mode,
515
+ createdAt: customerEntity?.createdAt
516
+ ? customerEntity.createdAt instanceof Date
517
+ ? customerEntity.createdAt.toISOString()
518
+ : String(customerEntity.createdAt)
519
+ : undefined,
520
+ updatedAt: customerEntity?.updatedAt
521
+ ? customerEntity.updatedAt instanceof Date
522
+ ? customerEntity.updatedAt.toISOString()
523
+ : String(customerEntity.updatedAt)
524
+ : undefined,
525
+ });
526
+ } catch {
527
+ // insertCustomer is idempotent; ignore duplicate errors
528
+ }
529
+ }
530
+
531
+ // ── Namespace getters (public API) ─────────────────────────
532
+
533
+ /**
534
+ * Subscription management namespace.
535
+ *
536
+ * All methods take explicit `entityId` — use them directly in your own
537
+ * Convex functions, or let `creem.api({ resolve })` handle auth for you.
538
+ *
539
+ * - `.getCurrent()` — current active subscription with product join (Convex DB)
540
+ * - `.list()` — active subscriptions, excludes ended + expired trials (Convex DB)
541
+ * - `.listAll()` — all subscriptions including ended (Convex DB)
542
+ * - `.update()` — plan switch (`productId`) or seat change (`units`) (Creem API, optimistic)
543
+ * - `.cancel()` — cancel subscription (Creem API, optimistic)
544
+ * - `.pause()` — pause an active subscription (Creem API, optimistic)
545
+ * - `.resume()` — resume a paused or scheduled-cancel subscription (Creem API, optimistic)
546
+ */
547
+ get subscriptions() {
548
+ type UpdateBehavior =
549
+ | "proration-charge-immediately"
550
+ | "proration-charge"
551
+ | "proration-none";
552
+ return {
553
+ getCurrent: (ctx: RunQueryCtx, { entityId }: { entityId: string }) =>
554
+ this.getCurrentSubscription(ctx, { entityId }),
555
+ list: (ctx: RunQueryCtx, { entityId }: { entityId: string }) =>
556
+ this.listUserSubscriptions(ctx, { entityId }),
557
+ listAll: (ctx: RunQueryCtx, { entityId }: { entityId: string }) =>
558
+ this.listAllUserSubscriptions(ctx, { entityId }),
559
+ update: async (
560
+ ctx: RunSchedulerMutationCtx,
561
+ args: {
562
+ entityId: string;
563
+ subscriptionId?: string;
564
+ productId?: string;
565
+ units?: number;
566
+ updateBehavior?: UpdateBehavior;
567
+ },
568
+ ) => {
569
+ if (args.productId && args.units)
570
+ throw new ConvexError("Provide productId OR units, not both");
571
+ if (!args.productId && !args.units)
572
+ throw new ConvexError("Provide productId or units");
573
+
574
+ // Resolve current subscription
575
+ const subscription = args.subscriptionId
576
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
577
+ id: args.subscriptionId,
578
+ })
579
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
580
+ entityId: args.entityId,
581
+ });
582
+ if (!subscription) throw new ConvexError("Subscription not found");
583
+
584
+ // Write optimistic state
585
+ await ctx.runMutation(this.component.lib.patchSubscription, {
586
+ subscriptionId: subscription.id,
587
+ ...(args.units != null ? { seats: args.units } : {}),
588
+ ...(args.productId ? { productId: args.productId } : {}),
589
+ ...(args.productId && args.units == null
590
+ ? { seats: subscription.seats ?? null }
591
+ : {}),
592
+ });
593
+
594
+ // Schedule the Creem API call (runs async, reverts on error)
595
+ await ctx.scheduler.runAfter(
596
+ 0,
597
+ this.component.lib.executeSubscriptionUpdate,
598
+ {
599
+ apiKey: this.apiKey,
600
+ serverIdx: this.serverIdx,
601
+ serverURL: this.serverURL,
602
+ subscriptionId: subscription.id,
603
+ productId: args.productId,
604
+ units: args.units,
605
+ updateBehavior: args.updateBehavior,
606
+ previousSeats: subscription.seats ?? undefined,
607
+ previousProductId: subscription.productId,
608
+ },
609
+ );
610
+ },
611
+ cancel: async (
612
+ ctx: RunSchedulerMutationCtx,
613
+ args: {
614
+ entityId: string;
615
+ subscriptionId?: string;
616
+ revokeImmediately?: boolean;
617
+ },
618
+ ) => {
619
+ const subscription = args.subscriptionId
620
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
621
+ id: args.subscriptionId,
622
+ })
623
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
624
+ entityId: args.entityId,
625
+ });
626
+ if (!subscription) throw new ConvexError("Subscription not found");
627
+ if (
628
+ subscription.status !== "active" &&
629
+ subscription.status !== "trialing"
630
+ ) {
631
+ throw new ConvexError("Subscription is not active");
632
+ }
633
+
634
+ // Resolve cancel mode: explicit arg > config default > omit (Creem decides)
635
+ const immediate =
636
+ args.revokeImmediately ??
637
+ (this.config.cancelMode === "immediate" ? true : undefined);
638
+ const isImmediate = immediate === true;
639
+
640
+ // Write optimistic state
641
+ await ctx.runMutation(this.component.lib.patchSubscription, {
642
+ subscriptionId: subscription.id,
643
+ ...(isImmediate
644
+ ? { status: "canceled", cancelAtPeriodEnd: false }
645
+ : { cancelAtPeriodEnd: true }),
646
+ });
647
+
648
+ // Resolve cancel mode string for the action
649
+ const cancelMode = isImmediate
650
+ ? "immediate"
651
+ : immediate === false || this.config.cancelMode === "scheduled"
652
+ ? "scheduled"
653
+ : undefined;
654
+
655
+ // Schedule the Creem API call
656
+ await ctx.scheduler.runAfter(
657
+ 0,
658
+ this.component.lib.executeSubscriptionLifecycle,
659
+ {
660
+ apiKey: this.apiKey,
661
+ serverIdx: this.serverIdx,
662
+ serverURL: this.serverURL,
663
+ subscriptionId: subscription.id,
664
+ operation: "cancel",
665
+ cancelMode,
666
+ previousStatus: subscription.status,
667
+ previousCancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
668
+ },
669
+ );
670
+ },
671
+ pause: async (
672
+ ctx: RunSchedulerMutationCtx,
673
+ args: { entityId: string; subscriptionId?: string },
674
+ ) => {
675
+ const subscription = args.subscriptionId
676
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
677
+ id: args.subscriptionId,
678
+ })
679
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
680
+ entityId: args.entityId,
681
+ });
682
+ if (!subscription) throw new ConvexError("Subscription not found");
683
+ if (
684
+ subscription.status !== "active" &&
685
+ subscription.status !== "trialing"
686
+ ) {
687
+ throw new ConvexError("Subscription is not active");
688
+ }
689
+
690
+ // Write optimistic state
691
+ await ctx.runMutation(this.component.lib.patchSubscription, {
692
+ subscriptionId: subscription.id,
693
+ status: "paused",
694
+ });
695
+
696
+ // Schedule the Creem API call
697
+ await ctx.scheduler.runAfter(
698
+ 0,
699
+ this.component.lib.executeSubscriptionLifecycle,
700
+ {
701
+ apiKey: this.apiKey,
702
+ serverIdx: this.serverIdx,
703
+ serverURL: this.serverURL,
704
+ subscriptionId: subscription.id,
705
+ operation: "pause",
706
+ previousStatus: subscription.status,
707
+ },
708
+ );
709
+ },
710
+ resume: async (
711
+ ctx: RunSchedulerMutationCtx,
712
+ args: { entityId: string; subscriptionId?: string },
713
+ ) => {
714
+ const subscription = args.subscriptionId
715
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
716
+ id: args.subscriptionId,
717
+ })
718
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
719
+ entityId: args.entityId,
720
+ });
721
+ if (!subscription) throw new ConvexError("Subscription not found");
722
+ if (
723
+ subscription.status !== "scheduled_cancel" &&
724
+ subscription.status !== "paused"
725
+ ) {
726
+ throw new ConvexError("Subscription is not in a resumable state");
727
+ }
728
+
729
+ // Write optimistic state
730
+ await ctx.runMutation(this.component.lib.patchSubscription, {
731
+ subscriptionId: subscription.id,
732
+ status: "active",
733
+ cancelAtPeriodEnd: false,
734
+ });
735
+
736
+ // Schedule the Creem API call
737
+ await ctx.scheduler.runAfter(
738
+ 0,
739
+ this.component.lib.executeSubscriptionLifecycle,
740
+ {
741
+ apiKey: this.apiKey,
742
+ serverIdx: this.serverIdx,
743
+ serverURL: this.serverURL,
744
+ subscriptionId: subscription.id,
745
+ operation: "resume",
746
+ previousStatus: subscription.status,
747
+ previousCancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
748
+ },
749
+ );
750
+ },
751
+ };
752
+ }
753
+
754
+ /**
755
+ * Checkout namespace.
756
+ *
757
+ * - `.create()` — create a checkout URL with 3-tier `successUrl` resolution and optional `theme` (Creem API)
758
+ */
759
+ get checkouts() {
760
+ return {
761
+ create: async (
762
+ ctx: RunActionCtx,
763
+ args: {
764
+ entityId: string;
765
+ userId: string;
766
+ email: string;
767
+ productId: string;
768
+ successUrl?: string;
769
+ fallbackSuccessUrl?: string;
770
+ units?: number;
771
+ metadata?: Record<string, string>;
772
+ discountCode?: string;
773
+ theme?: "light" | "dark";
774
+ },
775
+ ): Promise<{ url: string }> => {
776
+ // 3-tier successUrl resolution
777
+ let resolvedSuccessUrl = args.successUrl;
778
+ if (!resolvedSuccessUrl) {
779
+ const product = await ctx.runQuery(this.component.lib.getProduct, {
780
+ id: args.productId,
781
+ });
782
+ resolvedSuccessUrl = product?.defaultSuccessUrl ?? undefined;
783
+ }
784
+ if (!resolvedSuccessUrl) {
785
+ resolvedSuccessUrl = args.fallbackSuccessUrl;
786
+ }
787
+
788
+ const checkout = await this.createCheckoutSession(ctx, {
789
+ productId: args.productId,
790
+ entityId: args.entityId,
791
+ userId: args.userId,
792
+ email: args.email,
793
+ ...(resolvedSuccessUrl ? { successUrl: resolvedSuccessUrl } : {}),
794
+ units: args.units,
795
+ metadata: args.metadata,
796
+ });
797
+ let checkoutUrl = checkout.checkoutUrl;
798
+ if (!checkoutUrl)
799
+ throw new ConvexError("Checkout URL missing from Creem response");
800
+ if (args.theme) {
801
+ const separator = checkoutUrl.includes("?") ? "&" : "?";
802
+ checkoutUrl = `${checkoutUrl}${separator}theme=${args.theme}`;
803
+ }
804
+ return { url: checkoutUrl };
805
+ },
806
+ };
807
+ }
808
+
809
+ /**
810
+ * Product namespace. All reads come from the local Convex DB (synced via webhooks).
811
+ *
812
+ * - `.list()` — all synced products (public, no `entityId` needed)
813
+ * - `.get()` — single product by Creem product ID
814
+ */
815
+ get products() {
816
+ return {
817
+ list: (ctx: RunQueryCtx, options?: { includeArchived?: boolean }) =>
818
+ this.listProducts(ctx, options),
819
+ get: (ctx: RunQueryCtx, { productId }: { productId: string }) =>
820
+ this.getProduct(ctx, { productId }),
821
+ };
822
+ }
823
+
824
+ /**
825
+ * Customer namespace.
826
+ *
827
+ * - `.retrieve()` — customer record by billing entity (Convex DB)
828
+ * - `.portalUrl()` — generate a Creem customer billing portal URL (Creem API)
829
+ */
830
+ get customers() {
831
+ return {
832
+ retrieve: (ctx: RunQueryCtx, { entityId }: { entityId: string }) =>
833
+ this.getCustomerByEntityId(ctx, entityId),
834
+ portalUrl: (ctx: RunActionCtx, { entityId }: { entityId: string }) =>
835
+ this.createCustomerPortalSession(ctx, { entityId }),
836
+ };
837
+ }
838
+
839
+ /**
840
+ * Order namespace.
841
+ *
842
+ * - `.list()` — paid one-time orders for a billing entity (Convex DB)
843
+ */
844
+ get orders() {
845
+ return {
846
+ list: (ctx: RunQueryCtx, { entityId }: { entityId: string }) =>
847
+ this.listUserOrders(ctx, { entityId }),
848
+ };
849
+ }
850
+
851
+ // ── Component helpers (public, flat) ──────────────────────
852
+
853
+ /**
854
+ * Composite billing model for connected widgets.
855
+ *
856
+ * Aggregates snapshot + products + subscriptions + orders into a single
857
+ * object that `<Subscription.Root>` and `<Product.Root>` widgets consume.
858
+ *
859
+ * Graceful when `entityId` is `null` — returns public product catalog only
860
+ * (useful for unauthenticated pricing pages).
861
+ *
862
+ * @param ctx - Convex query context
863
+ * @param options.entityId - Billing entity ID, or `null` for public-only data
864
+ * @param options.user - User info for the UI (widgets display email, etc.)
865
+ */
866
+ async getBillingModel(
867
+ ctx: RunQueryCtx,
868
+ {
869
+ entityId,
870
+ user,
871
+ }: {
872
+ entityId: string | null;
873
+ user?: { _id: string; email: string } | null;
874
+ },
875
+ ) {
876
+ const products = await this.listProducts(ctx);
877
+ if (!entityId) {
878
+ return {
879
+ user: user ?? null,
880
+ billingSnapshot: null as BillingSnapshot | null,
881
+ allProducts: products,
882
+ ownedProductIds: [] as string[],
883
+ subscriptionProductId: null as string | null,
884
+ activeSubscriptions: [] as Array<{
885
+ id: string;
886
+ productId: string;
887
+ status: string;
888
+ cancelAtPeriodEnd: boolean;
889
+ currentPeriodEnd: string | null;
890
+ currentPeriodStart: string;
891
+ seats: number | null;
892
+ recurringInterval: string | null;
893
+ trialEnd: string | null;
894
+ }>,
895
+ hasCreemCustomer: false,
896
+ };
897
+ }
898
+ const [
899
+ billingSnapshot,
900
+ subscription,
901
+ activeSubscriptions,
902
+ customer,
903
+ orders,
904
+ ] = await Promise.all([
905
+ this.getBillingSnapshot(ctx, { entityId }),
906
+ this.getCurrentSubscription(ctx, { entityId }),
907
+ this.listUserSubscriptions(ctx, { entityId }),
908
+ this.getCustomerByEntityId(ctx, entityId),
909
+ this.listUserOrders(ctx, { entityId }),
910
+ ]);
911
+ const ownedProductIds = [...new Set(orders.map((o) => o.productId))];
912
+ return {
913
+ user: user ?? null,
914
+ billingSnapshot,
915
+ allProducts: products,
916
+ ownedProductIds,
917
+ subscriptionProductId: subscription?.productId ?? null,
918
+ activeSubscriptions: activeSubscriptions.map((s) => ({
919
+ id: s.id,
920
+ productId: s.productId,
921
+ status: s.status,
922
+ cancelAtPeriodEnd: s.cancelAtPeriodEnd,
923
+ currentPeriodEnd: s.currentPeriodEnd,
924
+ currentPeriodStart: s.currentPeriodStart,
925
+ seats: s.seats,
926
+ recurringInterval: s.recurringInterval,
927
+ trialEnd: s.trialEnd ?? null,
928
+ })),
929
+ hasCreemCustomer: customer != null,
930
+ };
931
+ }
932
+
933
+ // ── api({ resolve }) convenience ──────────────────────────
934
+
935
+ /**
936
+ * Generate ready-to-export Convex function definitions.
937
+ *
938
+ * Each function calls your `resolve` callback to authenticate the user
939
+ * and determine the billing entity, then delegates to the corresponding
940
+ * namespace method. Destructure and re-export in your `convex/billing.ts`.
941
+ *
942
+ * For full control, use the namespace getters directly instead
943
+ * (e.g. `creem.subscriptions.cancel(ctx, { entityId })`).
944
+ *
945
+ * @param options.resolve - Auth callback that returns `{ userId, email, entityId }`
946
+ * @returns Object with `uiModel`, `snapshot`, `checkouts`, `subscriptions`, `products`, `customers`, `orders`
947
+ *
948
+ * @example
949
+ * ```ts
950
+ * const { uiModel, checkouts, subscriptions } = creem.api({ resolve });
951
+ * export { uiModel };
952
+ * export const checkoutsCreate = checkouts.create;
953
+ * ```
954
+ */
955
+ api({ resolve }: { resolve: ApiResolver }) {
956
+ return {
957
+ uiModel: queryGeneric({
958
+ args: {},
959
+ returns: v.any(),
960
+ handler: async (ctx) => {
961
+ let resolved: {
962
+ userId: string;
963
+ email: string;
964
+ entityId: string;
965
+ } | null = null;
966
+ try {
967
+ resolved = await resolve(ctx);
968
+ } catch {
969
+ // No authenticated user — return unauthenticated model
970
+ }
971
+ return await this.getBillingModel(ctx, {
972
+ entityId: resolved?.entityId ?? null,
973
+ user: resolved
974
+ ? { _id: resolved.userId, email: resolved.email }
975
+ : null,
976
+ });
977
+ },
978
+ }),
979
+ snapshot: queryGeneric({
980
+ args: {},
981
+ returns: v.any(),
982
+ handler: async (ctx) => {
983
+ let resolved: { entityId: string } | null = null;
984
+ try {
985
+ resolved = await resolve(ctx);
986
+ } catch {
987
+ return null;
988
+ }
989
+ if (!resolved) return null;
990
+ return await this.getBillingSnapshot(ctx, {
991
+ entityId: resolved.entityId,
992
+ });
993
+ },
994
+ }),
995
+ checkouts: {
996
+ create: actionGeneric({
997
+ args: checkoutCreateArgs,
998
+ returns: v.object({ url: v.string() }),
999
+ handler: async (ctx, args) => {
1000
+ const { entityId, userId, email } = await resolve(ctx);
1001
+ return await this.checkouts.create(ctx, {
1002
+ entityId,
1003
+ userId,
1004
+ email,
1005
+ ...args,
1006
+ });
1007
+ },
1008
+ }),
1009
+ },
1010
+ subscriptions: {
1011
+ update: mutationGeneric({
1012
+ args: subscriptionUpdateArgs,
1013
+ handler: async (ctx, args) => {
1014
+ const { entityId } = await resolve(ctx);
1015
+ if (args.productId && args.units)
1016
+ throw new ConvexError("Provide productId OR units, not both");
1017
+ if (!args.productId && !args.units)
1018
+ throw new ConvexError("Provide productId or units");
1019
+
1020
+ // Resolve current subscription
1021
+ const subscription = args.subscriptionId
1022
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
1023
+ id: args.subscriptionId,
1024
+ })
1025
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
1026
+ entityId,
1027
+ });
1028
+ if (!subscription) throw new ConvexError("Subscription not found");
1029
+
1030
+ // Write optimistic state
1031
+ // For plan switches, also protect current seats from stale webhook data
1032
+ await ctx.runMutation(this.component.lib.patchSubscription, {
1033
+ subscriptionId: subscription.id,
1034
+ ...(args.units != null ? { seats: args.units } : {}),
1035
+ ...(args.productId ? { productId: args.productId } : {}),
1036
+ ...(args.productId && args.units == null
1037
+ ? { seats: subscription.seats ?? null }
1038
+ : {}),
1039
+ });
1040
+
1041
+ // Schedule the Creem API call (runs async, reverts on error)
1042
+ await ctx.scheduler.runAfter(
1043
+ 0,
1044
+ this.component.lib.executeSubscriptionUpdate,
1045
+ {
1046
+ apiKey: this.apiKey,
1047
+ serverIdx: this.serverIdx,
1048
+ serverURL: this.serverURL,
1049
+ subscriptionId: subscription.id,
1050
+ productId: args.productId,
1051
+ units: args.units,
1052
+ updateBehavior: args.updateBehavior,
1053
+ previousSeats: subscription.seats ?? undefined,
1054
+ previousProductId: subscription.productId,
1055
+ },
1056
+ );
1057
+ },
1058
+ }),
1059
+ cancel: mutationGeneric({
1060
+ args: subscriptionCancelArgs,
1061
+ handler: async (ctx, args) => {
1062
+ const { entityId } = await resolve(ctx);
1063
+ const subscription = args.subscriptionId
1064
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
1065
+ id: args.subscriptionId,
1066
+ })
1067
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
1068
+ entityId,
1069
+ });
1070
+ if (!subscription) throw new ConvexError("Subscription not found");
1071
+ if (
1072
+ subscription.status !== "active" &&
1073
+ subscription.status !== "trialing"
1074
+ ) {
1075
+ throw new ConvexError("Subscription is not active");
1076
+ }
1077
+
1078
+ // Resolve cancel mode: explicit arg > config default > omit (Creem decides)
1079
+ const immediate =
1080
+ args.revokeImmediately ??
1081
+ (this.config.cancelMode === "immediate" ? true : undefined);
1082
+ const isImmediate = immediate === true;
1083
+
1084
+ // Write optimistic state
1085
+ await ctx.runMutation(this.component.lib.patchSubscription, {
1086
+ subscriptionId: subscription.id,
1087
+ ...(isImmediate
1088
+ ? { status: "canceled", cancelAtPeriodEnd: false }
1089
+ : { cancelAtPeriodEnd: true }),
1090
+ });
1091
+
1092
+ // Resolve cancel mode string for the action
1093
+ const cancelMode = isImmediate
1094
+ ? "immediate"
1095
+ : immediate === false || this.config.cancelMode === "scheduled"
1096
+ ? "scheduled"
1097
+ : undefined;
1098
+
1099
+ // Schedule the Creem API call
1100
+ await ctx.scheduler.runAfter(
1101
+ 0,
1102
+ this.component.lib.executeSubscriptionLifecycle,
1103
+ {
1104
+ apiKey: this.apiKey,
1105
+ serverIdx: this.serverIdx,
1106
+ serverURL: this.serverURL,
1107
+ subscriptionId: subscription.id,
1108
+ operation: "cancel",
1109
+ cancelMode,
1110
+ previousStatus: subscription.status,
1111
+ previousCancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
1112
+ },
1113
+ );
1114
+ },
1115
+ }),
1116
+ resume: mutationGeneric({
1117
+ args: subscriptionResumeArgs,
1118
+ handler: async (ctx, args) => {
1119
+ const { entityId } = await resolve(ctx);
1120
+ const subscription = args.subscriptionId
1121
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
1122
+ id: args.subscriptionId,
1123
+ })
1124
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
1125
+ entityId,
1126
+ });
1127
+ if (!subscription) throw new ConvexError("Subscription not found");
1128
+ if (
1129
+ subscription.status !== "scheduled_cancel" &&
1130
+ subscription.status !== "paused"
1131
+ ) {
1132
+ throw new ConvexError("Subscription is not in a resumable state");
1133
+ }
1134
+
1135
+ // Write optimistic state
1136
+ await ctx.runMutation(this.component.lib.patchSubscription, {
1137
+ subscriptionId: subscription.id,
1138
+ status: "active",
1139
+ cancelAtPeriodEnd: false,
1140
+ });
1141
+
1142
+ // Schedule the Creem API call
1143
+ await ctx.scheduler.runAfter(
1144
+ 0,
1145
+ this.component.lib.executeSubscriptionLifecycle,
1146
+ {
1147
+ apiKey: this.apiKey,
1148
+ serverIdx: this.serverIdx,
1149
+ serverURL: this.serverURL,
1150
+ subscriptionId: subscription.id,
1151
+ operation: "resume",
1152
+ previousStatus: subscription.status,
1153
+ previousCancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
1154
+ },
1155
+ );
1156
+ },
1157
+ }),
1158
+ pause: mutationGeneric({
1159
+ args: subscriptionPauseArgs,
1160
+ handler: async (ctx, args) => {
1161
+ const { entityId } = await resolve(ctx);
1162
+ const subscription = args.subscriptionId
1163
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
1164
+ id: args.subscriptionId,
1165
+ })
1166
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
1167
+ entityId,
1168
+ });
1169
+ if (!subscription) throw new ConvexError("Subscription not found");
1170
+ if (
1171
+ subscription.status !== "active" &&
1172
+ subscription.status !== "trialing"
1173
+ ) {
1174
+ throw new ConvexError("Subscription is not active");
1175
+ }
1176
+
1177
+ // Write optimistic state
1178
+ await ctx.runMutation(this.component.lib.patchSubscription, {
1179
+ subscriptionId: subscription.id,
1180
+ status: "paused",
1181
+ });
1182
+
1183
+ // Schedule the Creem API call
1184
+ await ctx.scheduler.runAfter(
1185
+ 0,
1186
+ this.component.lib.executeSubscriptionLifecycle,
1187
+ {
1188
+ apiKey: this.apiKey,
1189
+ serverIdx: this.serverIdx,
1190
+ serverURL: this.serverURL,
1191
+ subscriptionId: subscription.id,
1192
+ operation: "pause",
1193
+ previousStatus: subscription.status,
1194
+ },
1195
+ );
1196
+ },
1197
+ }),
1198
+ list: queryGeneric({
1199
+ args: {},
1200
+ returns: v.any(),
1201
+ handler: async (ctx) => {
1202
+ const { entityId } = await resolve(ctx);
1203
+ return await this.subscriptions.list(ctx, { entityId });
1204
+ },
1205
+ }),
1206
+ listAll: queryGeneric({
1207
+ args: {},
1208
+ returns: v.array(
1209
+ v.object({
1210
+ ...schema.tables.subscriptions.validator.fields,
1211
+ product: v.union(schema.tables.products.validator, v.null()),
1212
+ }),
1213
+ ),
1214
+ handler: async (ctx) => {
1215
+ const { entityId } = await resolve(ctx);
1216
+ return await this.subscriptions.listAll(ctx, { entityId });
1217
+ },
1218
+ }),
1219
+ },
1220
+ products: {
1221
+ list: queryGeneric({
1222
+ args: {},
1223
+ handler: async (ctx) => {
1224
+ return await this.products.list(ctx);
1225
+ },
1226
+ }),
1227
+ get: queryGeneric({
1228
+ args: { productId: v.string() },
1229
+ returns: v.union(schema.tables.products.validator, v.null()),
1230
+ handler: async (ctx, args) => {
1231
+ return await this.products.get(ctx, { productId: args.productId });
1232
+ },
1233
+ }),
1234
+ },
1235
+ customers: {
1236
+ retrieve: queryGeneric({
1237
+ args: {},
1238
+ returns: v.union(schema.tables.customers.validator, v.null()),
1239
+ handler: async (ctx) => {
1240
+ const { entityId } = await resolve(ctx);
1241
+ return await this.customers.retrieve(ctx, { entityId });
1242
+ },
1243
+ }),
1244
+ portalUrl: actionGeneric({
1245
+ args: {},
1246
+ returns: v.object({ url: v.string() }),
1247
+ handler: async (ctx) => {
1248
+ const { entityId } = await resolve(ctx);
1249
+ return await this.customers.portalUrl(ctx, { entityId });
1250
+ },
1251
+ }),
1252
+ },
1253
+ orders: {
1254
+ list: queryGeneric({
1255
+ args: {},
1256
+ returns: v.array(schema.tables.orders.validator),
1257
+ handler: async (ctx) => {
1258
+ const { entityId } = await resolve(ctx);
1259
+ return await this.orders.list(ctx, { entityId });
1260
+ },
1261
+ }),
1262
+ },
1263
+ };
1264
+ }
1265
+
1266
+ /**
1267
+ * Register the Creem webhook HTTP route on your Convex `httpRouter`.
1268
+ *
1269
+ * Automatically handles `checkout.completed`, `subscription.*`, and `product.*`
1270
+ * events — upserts customers, subscriptions, orders, and products in the Convex DB.
1271
+ *
1272
+ * @param http - Your Convex HTTP router (from `httpRouter()`)
1273
+ * @param options.path - Webhook endpoint path (default: `"/creem/events"`)
1274
+ * @param options.events - Optional custom handlers that run **after** built-in processing
1275
+ *
1276
+ * @example
1277
+ * ```ts
1278
+ * const http = httpRouter();
1279
+ * creem.registerRoutes(http, {
1280
+ * events: {
1281
+ * "checkout.completed": async (ctx, event) => { ... },
1282
+ * },
1283
+ * });
1284
+ * ```
1285
+ */
1286
+ registerRoutes(
1287
+ http: HttpRouter,
1288
+ {
1289
+ path = "/creem/events",
1290
+ events,
1291
+ }: {
1292
+ path?: string;
1293
+ events?: WebhookEventHandlers;
1294
+ } = {},
1295
+ ) {
1296
+ const mergedEvents: WebhookEventHandlers = { ...events };
1297
+
1298
+ http.route({
1299
+ path,
1300
+ method: "POST",
1301
+ handler: httpActionGeneric(async (ctx, request) => {
1302
+ if (!request.body) {
1303
+ throw new ConvexError("No body");
1304
+ }
1305
+ const body = await request.text();
1306
+ const headers: Record<string, string> = {};
1307
+ request.headers.forEach((value, key) => {
1308
+ headers[key] = value;
1309
+ });
1310
+ try {
1311
+ await this.verifyWebhook(body, headers);
1312
+ const event = JSON.parse(body) as CreemWebhookEvent;
1313
+ const eventType = getEventType(event);
1314
+ const eventData = getEventData(event);
1315
+
1316
+ console.log(
1317
+ `[creem-webhook] eventType=${eventType}`,
1318
+ `body=${JSON.stringify(event)}`,
1319
+ );
1320
+
1321
+ if (
1322
+ eventData &&
1323
+ typeof eventData === "object" &&
1324
+ eventType.startsWith("checkout.")
1325
+ ) {
1326
+ const raw = eventData as Record<string, unknown>;
1327
+ const checkout = parseCheckout(raw);
1328
+ if (checkout && eventType === "checkout.completed") {
1329
+ // Auto-create customer record from checkout metadata
1330
+ const customerObj =
1331
+ typeof checkout.customer === "object"
1332
+ ? checkout.customer
1333
+ : undefined;
1334
+ const customerId = getCustomerId(customerObj);
1335
+ const entityId = getConvexEntityId(checkout.metadata);
1336
+ await this.upsertCustomerFromWebhook(
1337
+ ctx,
1338
+ customerId,
1339
+ entityId,
1340
+ customerObj as CustomerEntity | undefined,
1341
+ );
1342
+
1343
+ // Process embedded subscription if present (recurring checkout).
1344
+ // checkoutEntityFromJSON already parsed it into a typed SubscriptionEntity,
1345
+ // so use it directly — do NOT re-parse through subscriptionEntityFromJSON.
1346
+ if (
1347
+ checkout.subscription &&
1348
+ typeof checkout.subscription === "object"
1349
+ ) {
1350
+ const embeddedSub = checkout.subscription as SubscriptionEntity;
1351
+ // Recover metadata: SDK strips it from SubscriptionEntity.
1352
+ // Use checkout-level metadata as fallback (same convexUserId).
1353
+ const embeddedRaw = (raw.subscription ?? {}) as Record<
1354
+ string,
1355
+ unknown
1356
+ >;
1357
+ const rawMeta = (embeddedRaw.metadata ??
1358
+ checkout.metadata ??
1359
+ {}) as Record<string, unknown>;
1360
+ const subscription = convertToDatabaseSubscription(
1361
+ embeddedSub,
1362
+ { rawMetadata: rawMeta },
1363
+ );
1364
+ await ctx.runMutation(this.component.lib.createSubscription, {
1365
+ subscription,
1366
+ });
1367
+ }
1368
+
1369
+ // Store the order (present for both one-time and subscription checkouts)
1370
+ if (checkout.order && typeof checkout.order === "object") {
1371
+ const o = checkout.order as Record<string, unknown>;
1372
+ const order = convertToOrder(
1373
+ {
1374
+ id: o.id as string,
1375
+ customer: (o.customer as string) ?? null,
1376
+ product: o.product as string,
1377
+ amount: o.amount as number,
1378
+ currency: o.currency as string,
1379
+ status: o.status as string,
1380
+ type: o.type as string,
1381
+ transaction: (o.transaction as string) ?? null,
1382
+ subTotal: o.subTotal as number | undefined,
1383
+ sub_total: o.sub_total as number | undefined,
1384
+ taxAmount: o.taxAmount as number | undefined,
1385
+ tax_amount: o.tax_amount as number | undefined,
1386
+ discountAmount: o.discountAmount as number | undefined,
1387
+ discount_amount: o.discount_amount as number | undefined,
1388
+ amountDue: o.amountDue as number | undefined,
1389
+ amount_due: o.amount_due as number | undefined,
1390
+ amountPaid: o.amountPaid as number | undefined,
1391
+ amount_paid: o.amount_paid as number | undefined,
1392
+ discount: (o.discount as string) ?? null,
1393
+ affiliate: (o.affiliate as string) ?? null,
1394
+ mode: o.mode as string | undefined,
1395
+ createdAt: o.createdAt as Date | string | undefined,
1396
+ created_at: o.created_at as string | undefined,
1397
+ updatedAt: o.updatedAt as Date | string | undefined,
1398
+ updated_at: o.updated_at as string | undefined,
1399
+ },
1400
+ {
1401
+ checkoutId: checkout.id,
1402
+ metadata: checkout.metadata as
1403
+ | Record<string, unknown>
1404
+ | undefined,
1405
+ },
1406
+ );
1407
+ await ctx.runMutation(this.component.lib.createOrder, {
1408
+ order,
1409
+ });
1410
+ }
1411
+ }
1412
+ }
1413
+
1414
+ if (
1415
+ eventData &&
1416
+ typeof eventData === "object" &&
1417
+ eventType.startsWith("subscription.")
1418
+ ) {
1419
+ const raw = eventData as Record<string, unknown>;
1420
+ const parsed = parseSubscription(raw);
1421
+ if (parsed) {
1422
+ // Pass raw metadata since SDK's SubscriptionEntity type strips it
1423
+ const rawMeta = (raw.metadata ?? {}) as Record<string, unknown>;
1424
+ const subscription = convertToDatabaseSubscription(parsed, {
1425
+ rawMetadata: rawMeta,
1426
+ });
1427
+ if (eventType === "subscription.created") {
1428
+ await ctx.runMutation(this.component.lib.createSubscription, {
1429
+ subscription,
1430
+ });
1431
+ } else {
1432
+ await ctx.runMutation(this.component.lib.updateSubscription, {
1433
+ subscription,
1434
+ });
1435
+ }
1436
+
1437
+ // Auto-create customer record from subscription metadata
1438
+ const customerEntity =
1439
+ typeof parsed.customer === "object"
1440
+ ? (parsed.customer as CustomerEntity)
1441
+ : undefined;
1442
+ const customerId = getCustomerId(parsed.customer);
1443
+ const entityId = getConvexEntityId(
1444
+ raw.metadata ??
1445
+ (parsed as unknown as Record<string, unknown>).metadata,
1446
+ );
1447
+ await this.upsertCustomerFromWebhook(
1448
+ ctx,
1449
+ customerId,
1450
+ entityId,
1451
+ customerEntity,
1452
+ );
1453
+ } else {
1454
+ // Fallback: SDK parsing failed (e.g., unknown status)
1455
+ // Still try to extract subscription ID for update events
1456
+ const subId = typeof raw.id === "string" ? raw.id : null;
1457
+ if (subId) {
1458
+ console.warn(
1459
+ `Could not parse subscription for ${eventType}, id: ${subId}`,
1460
+ );
1461
+ }
1462
+ }
1463
+ }
1464
+
1465
+ if (
1466
+ eventData &&
1467
+ typeof eventData === "object" &&
1468
+ eventType.startsWith("product.")
1469
+ ) {
1470
+ const raw = eventData as Record<string, unknown>;
1471
+ const parsed = parseProduct(raw);
1472
+ if (parsed) {
1473
+ const product = convertToDatabaseProduct(parsed);
1474
+ if (eventType === "product.created") {
1475
+ await ctx.runMutation(this.component.lib.createProduct, {
1476
+ product,
1477
+ });
1478
+ } else {
1479
+ await ctx.runMutation(this.component.lib.updateProduct, {
1480
+ product,
1481
+ });
1482
+ }
1483
+ } else {
1484
+ console.warn(`Could not parse product for ${eventType}`);
1485
+ }
1486
+ }
1487
+
1488
+ const handler = mergedEvents[eventType];
1489
+ if (handler) {
1490
+ await handler(ctx, event);
1491
+ }
1492
+
1493
+ return new Response("Accepted", { status: 202 });
1494
+ } catch (error) {
1495
+ if (error instanceof WebhookVerificationError) {
1496
+ console.error(error);
1497
+ return new Response("Forbidden", { status: 403 });
1498
+ }
1499
+ throw error;
1500
+ }
1501
+ }),
1502
+ });
1503
+ }
1504
+ }