@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,1068 @@
1
+ import "./polyfill.js";
2
+ import { Creem as CreemSDK } from "creem";
3
+ import { Webhook, WebhookVerificationError } from "standardwebhooks";
4
+ import { getEntityId, lowerCaseHeaders, toHex, constantTimeEqual, normalizeSignature, } from "./helpers.js";
5
+ import { getEventType, getEventData, getCustomerId, getConvexEntityId, parseSubscription, parseCheckout, parseProduct, } from "./parsers.js";
6
+ import { actionGeneric, httpActionGeneric, mutationGeneric, queryGeneric, } from "convex/server";
7
+ import { ConvexError, v } from "convex/values";
8
+ import schema from "../component/schema.js";
9
+ import { convertToDatabaseProduct, convertToDatabaseSubscription, convertToOrder, } from "../component/util.js";
10
+ import { resolveBillingSnapshot as defaultResolveBillingSnapshot } from "../core/resolver.js";
11
+ export * from "../core/index.js";
12
+ export { getEntityId, lowerCaseHeaders, toHex, constantTimeEqual, normalizeSignature, } from "./helpers.js";
13
+ export { getEventType, getEventData, getCustomerId, getConvexEntityId, parseSubscription, parseCheckout, parseProduct, manualParseSubscription, } from "./parsers.js";
14
+ /** Convex validator for the `subscriptions` table. Use with `v.object(subscriptionValidator.fields)` in custom functions. */
15
+ export const subscriptionValidator = schema.tables.subscriptions.validator;
16
+ // ── Shared arg validators for custom actions / mutations ──────────────
17
+ // Use these when writing your own Convex functions that wrap creem methods
18
+ // (e.g. for RBAC). They match exactly what the connected widgets send.
19
+ /**
20
+ * Convex arg validator for checkout creation.
21
+ * Matches the args sent by `<Subscription.Root>` and `<Product.Root>` widgets.
22
+ * Use in your own `action()` definitions for custom RBAC wrappers.
23
+ */
24
+ export const checkoutCreateArgs = {
25
+ productId: v.string(),
26
+ successUrl: v.optional(v.string()),
27
+ fallbackSuccessUrl: v.optional(v.string()),
28
+ units: v.optional(v.number()),
29
+ metadata: v.optional(v.record(v.string(), v.string())),
30
+ discountCode: v.optional(v.string()),
31
+ theme: v.optional(v.union(v.literal("light"), v.literal("dark"))),
32
+ };
33
+ /**
34
+ * Convex arg validator for subscription updates (plan switch or seat change).
35
+ * Matches the args sent by `<Subscription.Root>` widgets.
36
+ */
37
+ export const subscriptionUpdateArgs = {
38
+ subscriptionId: v.optional(v.string()),
39
+ productId: v.optional(v.string()),
40
+ units: v.optional(v.number()),
41
+ updateBehavior: v.optional(v.union(v.literal("proration-charge-immediately"), v.literal("proration-charge"), v.literal("proration-none"))),
42
+ };
43
+ /**
44
+ * Convex arg validator for subscription cancellation.
45
+ * Matches the args sent by `<Subscription.Root>` cancel button.
46
+ */
47
+ export const subscriptionCancelArgs = {
48
+ subscriptionId: v.optional(v.string()),
49
+ revokeImmediately: v.optional(v.boolean()),
50
+ };
51
+ /**
52
+ * Convex arg validator for subscription resume.
53
+ * Matches the args sent by `<Subscription.Root>` resume button.
54
+ */
55
+ export const subscriptionResumeArgs = {
56
+ subscriptionId: v.optional(v.string()),
57
+ };
58
+ /**
59
+ * Convex arg validator for subscription pause.
60
+ * Matches the args sent by `<Subscription.Root>` pause button.
61
+ */
62
+ export const subscriptionPauseArgs = {
63
+ subscriptionId: v.optional(v.string()),
64
+ };
65
+ /**
66
+ * Main entry point for the Creem–Convex integration.
67
+ *
68
+ * Instantiate once in your `convex/billing.ts` and use its methods
69
+ * to manage subscriptions, checkouts, products, customers, and orders.
70
+ *
71
+ * **Two usage patterns:**
72
+ * 1. **Quick start** — call `creem.api({ resolve })` to generate ready-to-export Convex functions
73
+ * 2. **Full control** — use namespace getters (`creem.subscriptions.*`, `creem.checkouts.*`, etc.)
74
+ * directly in your own Convex functions for custom auth/RBAC
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * import { Creem } from "@mmailaender/convex-creem";
79
+ * import { components } from "./_generated/api";
80
+ *
81
+ * export const creem = new Creem(components.creem);
82
+ * ```
83
+ */
84
+ export class Creem {
85
+ component;
86
+ config;
87
+ /** Direct access to the Creem SDK client, pre-configured with your API key. Use for resources without webhook sync (licenses, discounts, transactions). */
88
+ sdk;
89
+ apiKey;
90
+ webhookSecret;
91
+ serverIdx;
92
+ serverURL;
93
+ constructor(component, config = {}) {
94
+ this.component = component;
95
+ this.config = config;
96
+ this.apiKey = config.apiKey ?? process.env["CREEM_API_KEY"] ?? "";
97
+ this.webhookSecret =
98
+ config.webhookSecret ?? process.env["CREEM_WEBHOOK_SECRET"] ?? "";
99
+ this.serverIdx =
100
+ config.serverIdx ??
101
+ (process.env["CREEM_SERVER_IDX"]
102
+ ? Number(process.env["CREEM_SERVER_IDX"])
103
+ : undefined);
104
+ this.serverURL = config.serverURL ?? process.env["CREEM_SERVER_URL"];
105
+ this.sdk = new CreemSDK({
106
+ apiKey: this.apiKey,
107
+ ...(this.serverIdx !== undefined ? { serverIdx: this.serverIdx } : {}),
108
+ ...(this.serverURL ? { serverURL: this.serverURL } : {}),
109
+ });
110
+ }
111
+ getCustomerByEntityId(ctx, entityId) {
112
+ return ctx.runQuery(this.component.lib.getCustomerByEntityId, { entityId });
113
+ }
114
+ /** Pull all products from the Creem API into the Convex database. Typically called once via `internalAction` or the CLI. */
115
+ async syncProducts(ctx) {
116
+ await ctx.runAction(this.component.lib.syncProducts, {
117
+ apiKey: this.apiKey,
118
+ serverIdx: this.serverIdx,
119
+ serverURL: this.serverURL,
120
+ });
121
+ }
122
+ async createCheckoutSession(ctx, { productId, entityId, userId, email, successUrl, units, metadata, }) {
123
+ const dbCustomer = await ctx.runQuery(this.component.lib.getCustomerByEntityId, {
124
+ entityId,
125
+ });
126
+ const checkout = await this.sdk.checkouts.create({
127
+ productId,
128
+ ...(successUrl ? { successUrl } : {}),
129
+ units,
130
+ metadata: {
131
+ ...(metadata ?? {}),
132
+ convexUserId: userId,
133
+ convexBillingEntityId: entityId,
134
+ },
135
+ customer: dbCustomer ? { id: dbCustomer.id } : { email },
136
+ });
137
+ if (!dbCustomer) {
138
+ const customerId = getEntityId(checkout.customer);
139
+ if (customerId) {
140
+ const customerObj = typeof checkout.customer === "object" ? checkout.customer : undefined;
141
+ await ctx.runMutation(this.component.lib.insertCustomer, {
142
+ id: customerId,
143
+ entityId,
144
+ email: customerObj?.email,
145
+ name: customerObj?.name ?? undefined,
146
+ country: customerObj?.country,
147
+ mode: customerObj?.mode,
148
+ });
149
+ }
150
+ }
151
+ return checkout;
152
+ }
153
+ async createCustomerPortalSession(ctx, { entityId }) {
154
+ const customer = await ctx.runQuery(this.component.lib.getCustomerByEntityId, { entityId });
155
+ if (!customer) {
156
+ throw new ConvexError("Customer not found");
157
+ }
158
+ const portal = await this.sdk.customers.generateBillingLinks({
159
+ customerId: customer.id,
160
+ });
161
+ return { url: portal.customerPortalLink };
162
+ }
163
+ listProducts(ctx, { includeArchived } = {}) {
164
+ return ctx.runQuery(this.component.lib.listProducts, {
165
+ includeArchived,
166
+ });
167
+ }
168
+ async getCurrentSubscription(ctx, { entityId }) {
169
+ const subscription = await ctx.runQuery(this.component.lib.getCurrentSubscription, {
170
+ entityId,
171
+ });
172
+ if (!subscription) {
173
+ return null;
174
+ }
175
+ const product = await ctx.runQuery(this.component.lib.getProduct, {
176
+ id: subscription.productId,
177
+ });
178
+ if (!product) {
179
+ throw new ConvexError("Product not found");
180
+ }
181
+ return {
182
+ ...subscription,
183
+ product,
184
+ };
185
+ }
186
+ /** Return active subscriptions for an entity, excluding ended and expired trials. */
187
+ listUserSubscriptions(ctx, { entityId }) {
188
+ return ctx.runQuery(this.component.lib.listUserSubscriptions, {
189
+ entityId,
190
+ });
191
+ }
192
+ /** Return paid one-time orders for an entity. */
193
+ listUserOrders(ctx, { entityId }) {
194
+ return ctx.runQuery(this.component.lib.listUserOrders, {
195
+ entityId,
196
+ });
197
+ }
198
+ /** Return all subscriptions for an entity, including ended and expired trials. */
199
+ listAllUserSubscriptions(ctx, { entityId }) {
200
+ return ctx.runQuery(this.component.lib.listAllUserSubscriptions, {
201
+ entityId,
202
+ });
203
+ }
204
+ getProduct(ctx, { productId }) {
205
+ return ctx.runQuery(this.component.lib.getProduct, { id: productId });
206
+ }
207
+ toSubscriptionSnapshot(subscription) {
208
+ return {
209
+ id: subscription.id,
210
+ productId: subscription.productId,
211
+ status: subscription.status,
212
+ recurringInterval: subscription.recurringInterval,
213
+ seats: subscription.seats,
214
+ cancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
215
+ currentPeriodEnd: subscription.currentPeriodEnd,
216
+ trialEnd: subscription.trialEnd ?? null,
217
+ };
218
+ }
219
+ /**
220
+ * Resolve the current billing state for a billing entity.
221
+ * Returns plan, status, available actions, subscription metadata, etc.
222
+ * Used internally by `getBillingModel` and exposed for custom billing UIs.
223
+ */
224
+ async getBillingSnapshot(ctx, { entityId, payment, }) {
225
+ const [currentSubscription, allSubscriptions] = await Promise.all([
226
+ this.getCurrentSubscription(ctx, { entityId }),
227
+ this.listAllUserSubscriptions(ctx, { entityId }),
228
+ ]);
229
+ return defaultResolveBillingSnapshot({
230
+ currentSubscription: currentSubscription
231
+ ? this.toSubscriptionSnapshot(currentSubscription)
232
+ : null,
233
+ allSubscriptions: allSubscriptions.map((subscription) => this.toSubscriptionSnapshot(subscription)),
234
+ payment: payment ?? null,
235
+ userContext: undefined,
236
+ });
237
+ }
238
+ async verifyWebhook(body, headers) {
239
+ if (!this.webhookSecret) {
240
+ throw new ConvexError("Missing CREEM_WEBHOOK_SECRET");
241
+ }
242
+ const normalized = lowerCaseHeaders(headers);
243
+ const webhookId = normalized["webhook-id"];
244
+ const webhookTimestamp = normalized["webhook-timestamp"];
245
+ const webhookSignature = normalized["webhook-signature"];
246
+ if (webhookId && webhookTimestamp && webhookSignature) {
247
+ new Webhook(this.webhookSecret).verify(body, {
248
+ "webhook-id": webhookId,
249
+ "webhook-timestamp": webhookTimestamp,
250
+ "webhook-signature": webhookSignature,
251
+ });
252
+ return;
253
+ }
254
+ const creemSignature = normalized["creem-signature"] ?? normalized["x-creem-signature"];
255
+ if (!creemSignature) {
256
+ throw new WebhookVerificationError("Missing webhook signature");
257
+ }
258
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(this.webhookSecret), { name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
259
+ const digest = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(body));
260
+ const expected = toHex(new Uint8Array(digest));
261
+ if (!constantTimeEqual(normalizeSignature(creemSignature), expected)) {
262
+ throw new WebhookVerificationError("Invalid webhook signature");
263
+ }
264
+ }
265
+ /** Upsert a customer record if we have both entityId and customerId. */
266
+ async upsertCustomerFromWebhook(ctx, customerId, entityId, customerEntity) {
267
+ if (!customerId || !entityId)
268
+ return;
269
+ try {
270
+ await ctx.runMutation(this.component.lib.insertCustomer, {
271
+ id: customerId,
272
+ entityId,
273
+ email: customerEntity?.email,
274
+ name: customerEntity?.name ?? undefined,
275
+ country: customerEntity?.country,
276
+ mode: customerEntity?.mode,
277
+ createdAt: customerEntity?.createdAt
278
+ ? customerEntity.createdAt instanceof Date
279
+ ? customerEntity.createdAt.toISOString()
280
+ : String(customerEntity.createdAt)
281
+ : undefined,
282
+ updatedAt: customerEntity?.updatedAt
283
+ ? customerEntity.updatedAt instanceof Date
284
+ ? customerEntity.updatedAt.toISOString()
285
+ : String(customerEntity.updatedAt)
286
+ : undefined,
287
+ });
288
+ }
289
+ catch {
290
+ // insertCustomer is idempotent; ignore duplicate errors
291
+ }
292
+ }
293
+ // ── Namespace getters (public API) ─────────────────────────
294
+ /**
295
+ * Subscription management namespace.
296
+ *
297
+ * All methods take explicit `entityId` — use them directly in your own
298
+ * Convex functions, or let `creem.api({ resolve })` handle auth for you.
299
+ *
300
+ * - `.getCurrent()` — current active subscription with product join (Convex DB)
301
+ * - `.list()` — active subscriptions, excludes ended + expired trials (Convex DB)
302
+ * - `.listAll()` — all subscriptions including ended (Convex DB)
303
+ * - `.update()` — plan switch (`productId`) or seat change (`units`) (Creem API, optimistic)
304
+ * - `.cancel()` — cancel subscription (Creem API, optimistic)
305
+ * - `.pause()` — pause an active subscription (Creem API, optimistic)
306
+ * - `.resume()` — resume a paused or scheduled-cancel subscription (Creem API, optimistic)
307
+ */
308
+ get subscriptions() {
309
+ return {
310
+ getCurrent: (ctx, { entityId }) => this.getCurrentSubscription(ctx, { entityId }),
311
+ list: (ctx, { entityId }) => this.listUserSubscriptions(ctx, { entityId }),
312
+ listAll: (ctx, { entityId }) => this.listAllUserSubscriptions(ctx, { entityId }),
313
+ update: async (ctx, args) => {
314
+ if (args.productId && args.units)
315
+ throw new ConvexError("Provide productId OR units, not both");
316
+ if (!args.productId && !args.units)
317
+ throw new ConvexError("Provide productId or units");
318
+ // Resolve current subscription
319
+ const subscription = args.subscriptionId
320
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
321
+ id: args.subscriptionId,
322
+ })
323
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
324
+ entityId: args.entityId,
325
+ });
326
+ if (!subscription)
327
+ throw new ConvexError("Subscription not found");
328
+ // Write optimistic state
329
+ await ctx.runMutation(this.component.lib.patchSubscription, {
330
+ subscriptionId: subscription.id,
331
+ ...(args.units != null ? { seats: args.units } : {}),
332
+ ...(args.productId ? { productId: args.productId } : {}),
333
+ ...(args.productId && args.units == null
334
+ ? { seats: subscription.seats ?? null }
335
+ : {}),
336
+ });
337
+ // Schedule the Creem API call (runs async, reverts on error)
338
+ await ctx.scheduler.runAfter(0, this.component.lib.executeSubscriptionUpdate, {
339
+ apiKey: this.apiKey,
340
+ serverIdx: this.serverIdx,
341
+ serverURL: this.serverURL,
342
+ subscriptionId: subscription.id,
343
+ productId: args.productId,
344
+ units: args.units,
345
+ updateBehavior: args.updateBehavior,
346
+ previousSeats: subscription.seats ?? undefined,
347
+ previousProductId: subscription.productId,
348
+ });
349
+ },
350
+ cancel: async (ctx, args) => {
351
+ const subscription = args.subscriptionId
352
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
353
+ id: args.subscriptionId,
354
+ })
355
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
356
+ entityId: args.entityId,
357
+ });
358
+ if (!subscription)
359
+ throw new ConvexError("Subscription not found");
360
+ if (subscription.status !== "active" &&
361
+ subscription.status !== "trialing") {
362
+ throw new ConvexError("Subscription is not active");
363
+ }
364
+ // Resolve cancel mode: explicit arg > config default > omit (Creem decides)
365
+ const immediate = args.revokeImmediately ??
366
+ (this.config.cancelMode === "immediate" ? true : undefined);
367
+ const isImmediate = immediate === true;
368
+ // Write optimistic state
369
+ await ctx.runMutation(this.component.lib.patchSubscription, {
370
+ subscriptionId: subscription.id,
371
+ ...(isImmediate
372
+ ? { status: "canceled", cancelAtPeriodEnd: false }
373
+ : { cancelAtPeriodEnd: true }),
374
+ });
375
+ // Resolve cancel mode string for the action
376
+ const cancelMode = isImmediate
377
+ ? "immediate"
378
+ : immediate === false || this.config.cancelMode === "scheduled"
379
+ ? "scheduled"
380
+ : undefined;
381
+ // Schedule the Creem API call
382
+ await ctx.scheduler.runAfter(0, this.component.lib.executeSubscriptionLifecycle, {
383
+ apiKey: this.apiKey,
384
+ serverIdx: this.serverIdx,
385
+ serverURL: this.serverURL,
386
+ subscriptionId: subscription.id,
387
+ operation: "cancel",
388
+ cancelMode,
389
+ previousStatus: subscription.status,
390
+ previousCancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
391
+ });
392
+ },
393
+ pause: async (ctx, args) => {
394
+ const subscription = args.subscriptionId
395
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
396
+ id: args.subscriptionId,
397
+ })
398
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
399
+ entityId: args.entityId,
400
+ });
401
+ if (!subscription)
402
+ throw new ConvexError("Subscription not found");
403
+ if (subscription.status !== "active" &&
404
+ subscription.status !== "trialing") {
405
+ throw new ConvexError("Subscription is not active");
406
+ }
407
+ // Write optimistic state
408
+ await ctx.runMutation(this.component.lib.patchSubscription, {
409
+ subscriptionId: subscription.id,
410
+ status: "paused",
411
+ });
412
+ // Schedule the Creem API call
413
+ await ctx.scheduler.runAfter(0, this.component.lib.executeSubscriptionLifecycle, {
414
+ apiKey: this.apiKey,
415
+ serverIdx: this.serverIdx,
416
+ serverURL: this.serverURL,
417
+ subscriptionId: subscription.id,
418
+ operation: "pause",
419
+ previousStatus: subscription.status,
420
+ });
421
+ },
422
+ resume: async (ctx, args) => {
423
+ const subscription = args.subscriptionId
424
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
425
+ id: args.subscriptionId,
426
+ })
427
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
428
+ entityId: args.entityId,
429
+ });
430
+ if (!subscription)
431
+ throw new ConvexError("Subscription not found");
432
+ if (subscription.status !== "scheduled_cancel" &&
433
+ subscription.status !== "paused") {
434
+ throw new ConvexError("Subscription is not in a resumable state");
435
+ }
436
+ // Write optimistic state
437
+ await ctx.runMutation(this.component.lib.patchSubscription, {
438
+ subscriptionId: subscription.id,
439
+ status: "active",
440
+ cancelAtPeriodEnd: false,
441
+ });
442
+ // Schedule the Creem API call
443
+ await ctx.scheduler.runAfter(0, this.component.lib.executeSubscriptionLifecycle, {
444
+ apiKey: this.apiKey,
445
+ serverIdx: this.serverIdx,
446
+ serverURL: this.serverURL,
447
+ subscriptionId: subscription.id,
448
+ operation: "resume",
449
+ previousStatus: subscription.status,
450
+ previousCancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
451
+ });
452
+ },
453
+ };
454
+ }
455
+ /**
456
+ * Checkout namespace.
457
+ *
458
+ * - `.create()` — create a checkout URL with 3-tier `successUrl` resolution and optional `theme` (Creem API)
459
+ */
460
+ get checkouts() {
461
+ return {
462
+ create: async (ctx, args) => {
463
+ // 3-tier successUrl resolution
464
+ let resolvedSuccessUrl = args.successUrl;
465
+ if (!resolvedSuccessUrl) {
466
+ const product = await ctx.runQuery(this.component.lib.getProduct, {
467
+ id: args.productId,
468
+ });
469
+ resolvedSuccessUrl = product?.defaultSuccessUrl ?? undefined;
470
+ }
471
+ if (!resolvedSuccessUrl) {
472
+ resolvedSuccessUrl = args.fallbackSuccessUrl;
473
+ }
474
+ const checkout = await this.createCheckoutSession(ctx, {
475
+ productId: args.productId,
476
+ entityId: args.entityId,
477
+ userId: args.userId,
478
+ email: args.email,
479
+ ...(resolvedSuccessUrl ? { successUrl: resolvedSuccessUrl } : {}),
480
+ units: args.units,
481
+ metadata: args.metadata,
482
+ });
483
+ let checkoutUrl = checkout.checkoutUrl;
484
+ if (!checkoutUrl)
485
+ throw new ConvexError("Checkout URL missing from Creem response");
486
+ if (args.theme) {
487
+ const separator = checkoutUrl.includes("?") ? "&" : "?";
488
+ checkoutUrl = `${checkoutUrl}${separator}theme=${args.theme}`;
489
+ }
490
+ return { url: checkoutUrl };
491
+ },
492
+ };
493
+ }
494
+ /**
495
+ * Product namespace. All reads come from the local Convex DB (synced via webhooks).
496
+ *
497
+ * - `.list()` — all synced products (public, no `entityId` needed)
498
+ * - `.get()` — single product by Creem product ID
499
+ */
500
+ get products() {
501
+ return {
502
+ list: (ctx, options) => this.listProducts(ctx, options),
503
+ get: (ctx, { productId }) => this.getProduct(ctx, { productId }),
504
+ };
505
+ }
506
+ /**
507
+ * Customer namespace.
508
+ *
509
+ * - `.retrieve()` — customer record by billing entity (Convex DB)
510
+ * - `.portalUrl()` — generate a Creem customer billing portal URL (Creem API)
511
+ */
512
+ get customers() {
513
+ return {
514
+ retrieve: (ctx, { entityId }) => this.getCustomerByEntityId(ctx, entityId),
515
+ portalUrl: (ctx, { entityId }) => this.createCustomerPortalSession(ctx, { entityId }),
516
+ };
517
+ }
518
+ /**
519
+ * Order namespace.
520
+ *
521
+ * - `.list()` — paid one-time orders for a billing entity (Convex DB)
522
+ */
523
+ get orders() {
524
+ return {
525
+ list: (ctx, { entityId }) => this.listUserOrders(ctx, { entityId }),
526
+ };
527
+ }
528
+ // ── Component helpers (public, flat) ──────────────────────
529
+ /**
530
+ * Composite billing model for connected widgets.
531
+ *
532
+ * Aggregates snapshot + products + subscriptions + orders into a single
533
+ * object that `<Subscription.Root>` and `<Product.Root>` widgets consume.
534
+ *
535
+ * Graceful when `entityId` is `null` — returns public product catalog only
536
+ * (useful for unauthenticated pricing pages).
537
+ *
538
+ * @param ctx - Convex query context
539
+ * @param options.entityId - Billing entity ID, or `null` for public-only data
540
+ * @param options.user - User info for the UI (widgets display email, etc.)
541
+ */
542
+ async getBillingModel(ctx, { entityId, user, }) {
543
+ const products = await this.listProducts(ctx);
544
+ if (!entityId) {
545
+ return {
546
+ user: user ?? null,
547
+ billingSnapshot: null,
548
+ allProducts: products,
549
+ ownedProductIds: [],
550
+ subscriptionProductId: null,
551
+ activeSubscriptions: [],
552
+ hasCreemCustomer: false,
553
+ };
554
+ }
555
+ const [billingSnapshot, subscription, activeSubscriptions, customer, orders,] = await Promise.all([
556
+ this.getBillingSnapshot(ctx, { entityId }),
557
+ this.getCurrentSubscription(ctx, { entityId }),
558
+ this.listUserSubscriptions(ctx, { entityId }),
559
+ this.getCustomerByEntityId(ctx, entityId),
560
+ this.listUserOrders(ctx, { entityId }),
561
+ ]);
562
+ const ownedProductIds = [...new Set(orders.map((o) => o.productId))];
563
+ return {
564
+ user: user ?? null,
565
+ billingSnapshot,
566
+ allProducts: products,
567
+ ownedProductIds,
568
+ subscriptionProductId: subscription?.productId ?? null,
569
+ activeSubscriptions: activeSubscriptions.map((s) => ({
570
+ id: s.id,
571
+ productId: s.productId,
572
+ status: s.status,
573
+ cancelAtPeriodEnd: s.cancelAtPeriodEnd,
574
+ currentPeriodEnd: s.currentPeriodEnd,
575
+ currentPeriodStart: s.currentPeriodStart,
576
+ seats: s.seats,
577
+ recurringInterval: s.recurringInterval,
578
+ trialEnd: s.trialEnd ?? null,
579
+ })),
580
+ hasCreemCustomer: customer != null,
581
+ };
582
+ }
583
+ // ── api({ resolve }) convenience ──────────────────────────
584
+ /**
585
+ * Generate ready-to-export Convex function definitions.
586
+ *
587
+ * Each function calls your `resolve` callback to authenticate the user
588
+ * and determine the billing entity, then delegates to the corresponding
589
+ * namespace method. Destructure and re-export in your `convex/billing.ts`.
590
+ *
591
+ * For full control, use the namespace getters directly instead
592
+ * (e.g. `creem.subscriptions.cancel(ctx, { entityId })`).
593
+ *
594
+ * @param options.resolve - Auth callback that returns `{ userId, email, entityId }`
595
+ * @returns Object with `uiModel`, `snapshot`, `checkouts`, `subscriptions`, `products`, `customers`, `orders`
596
+ *
597
+ * @example
598
+ * ```ts
599
+ * const { uiModel, checkouts, subscriptions } = creem.api({ resolve });
600
+ * export { uiModel };
601
+ * export const checkoutsCreate = checkouts.create;
602
+ * ```
603
+ */
604
+ api({ resolve }) {
605
+ return {
606
+ uiModel: queryGeneric({
607
+ args: {},
608
+ returns: v.any(),
609
+ handler: async (ctx) => {
610
+ let resolved = null;
611
+ try {
612
+ resolved = await resolve(ctx);
613
+ }
614
+ catch {
615
+ // No authenticated user — return unauthenticated model
616
+ }
617
+ return await this.getBillingModel(ctx, {
618
+ entityId: resolved?.entityId ?? null,
619
+ user: resolved
620
+ ? { _id: resolved.userId, email: resolved.email }
621
+ : null,
622
+ });
623
+ },
624
+ }),
625
+ snapshot: queryGeneric({
626
+ args: {},
627
+ returns: v.any(),
628
+ handler: async (ctx) => {
629
+ let resolved = null;
630
+ try {
631
+ resolved = await resolve(ctx);
632
+ }
633
+ catch {
634
+ return null;
635
+ }
636
+ if (!resolved)
637
+ return null;
638
+ return await this.getBillingSnapshot(ctx, {
639
+ entityId: resolved.entityId,
640
+ });
641
+ },
642
+ }),
643
+ checkouts: {
644
+ create: actionGeneric({
645
+ args: checkoutCreateArgs,
646
+ returns: v.object({ url: v.string() }),
647
+ handler: async (ctx, args) => {
648
+ const { entityId, userId, email } = await resolve(ctx);
649
+ return await this.checkouts.create(ctx, {
650
+ entityId,
651
+ userId,
652
+ email,
653
+ ...args,
654
+ });
655
+ },
656
+ }),
657
+ },
658
+ subscriptions: {
659
+ update: mutationGeneric({
660
+ args: subscriptionUpdateArgs,
661
+ handler: async (ctx, args) => {
662
+ const { entityId } = await resolve(ctx);
663
+ if (args.productId && args.units)
664
+ throw new ConvexError("Provide productId OR units, not both");
665
+ if (!args.productId && !args.units)
666
+ throw new ConvexError("Provide productId or units");
667
+ // Resolve current subscription
668
+ const subscription = args.subscriptionId
669
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
670
+ id: args.subscriptionId,
671
+ })
672
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
673
+ entityId,
674
+ });
675
+ if (!subscription)
676
+ throw new ConvexError("Subscription not found");
677
+ // Write optimistic state
678
+ // For plan switches, also protect current seats from stale webhook data
679
+ await ctx.runMutation(this.component.lib.patchSubscription, {
680
+ subscriptionId: subscription.id,
681
+ ...(args.units != null ? { seats: args.units } : {}),
682
+ ...(args.productId ? { productId: args.productId } : {}),
683
+ ...(args.productId && args.units == null
684
+ ? { seats: subscription.seats ?? null }
685
+ : {}),
686
+ });
687
+ // Schedule the Creem API call (runs async, reverts on error)
688
+ await ctx.scheduler.runAfter(0, this.component.lib.executeSubscriptionUpdate, {
689
+ apiKey: this.apiKey,
690
+ serverIdx: this.serverIdx,
691
+ serverURL: this.serverURL,
692
+ subscriptionId: subscription.id,
693
+ productId: args.productId,
694
+ units: args.units,
695
+ updateBehavior: args.updateBehavior,
696
+ previousSeats: subscription.seats ?? undefined,
697
+ previousProductId: subscription.productId,
698
+ });
699
+ },
700
+ }),
701
+ cancel: mutationGeneric({
702
+ args: subscriptionCancelArgs,
703
+ handler: async (ctx, args) => {
704
+ const { entityId } = await resolve(ctx);
705
+ const subscription = args.subscriptionId
706
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
707
+ id: args.subscriptionId,
708
+ })
709
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
710
+ entityId,
711
+ });
712
+ if (!subscription)
713
+ throw new ConvexError("Subscription not found");
714
+ if (subscription.status !== "active" &&
715
+ subscription.status !== "trialing") {
716
+ throw new ConvexError("Subscription is not active");
717
+ }
718
+ // Resolve cancel mode: explicit arg > config default > omit (Creem decides)
719
+ const immediate = args.revokeImmediately ??
720
+ (this.config.cancelMode === "immediate" ? true : undefined);
721
+ const isImmediate = immediate === true;
722
+ // Write optimistic state
723
+ await ctx.runMutation(this.component.lib.patchSubscription, {
724
+ subscriptionId: subscription.id,
725
+ ...(isImmediate
726
+ ? { status: "canceled", cancelAtPeriodEnd: false }
727
+ : { cancelAtPeriodEnd: true }),
728
+ });
729
+ // Resolve cancel mode string for the action
730
+ const cancelMode = isImmediate
731
+ ? "immediate"
732
+ : immediate === false || this.config.cancelMode === "scheduled"
733
+ ? "scheduled"
734
+ : undefined;
735
+ // Schedule the Creem API call
736
+ await ctx.scheduler.runAfter(0, this.component.lib.executeSubscriptionLifecycle, {
737
+ apiKey: this.apiKey,
738
+ serverIdx: this.serverIdx,
739
+ serverURL: this.serverURL,
740
+ subscriptionId: subscription.id,
741
+ operation: "cancel",
742
+ cancelMode,
743
+ previousStatus: subscription.status,
744
+ previousCancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
745
+ });
746
+ },
747
+ }),
748
+ resume: mutationGeneric({
749
+ args: subscriptionResumeArgs,
750
+ handler: async (ctx, args) => {
751
+ const { entityId } = await resolve(ctx);
752
+ const subscription = args.subscriptionId
753
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
754
+ id: args.subscriptionId,
755
+ })
756
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
757
+ entityId,
758
+ });
759
+ if (!subscription)
760
+ throw new ConvexError("Subscription not found");
761
+ if (subscription.status !== "scheduled_cancel" &&
762
+ subscription.status !== "paused") {
763
+ throw new ConvexError("Subscription is not in a resumable state");
764
+ }
765
+ // Write optimistic state
766
+ await ctx.runMutation(this.component.lib.patchSubscription, {
767
+ subscriptionId: subscription.id,
768
+ status: "active",
769
+ cancelAtPeriodEnd: false,
770
+ });
771
+ // Schedule the Creem API call
772
+ await ctx.scheduler.runAfter(0, this.component.lib.executeSubscriptionLifecycle, {
773
+ apiKey: this.apiKey,
774
+ serverIdx: this.serverIdx,
775
+ serverURL: this.serverURL,
776
+ subscriptionId: subscription.id,
777
+ operation: "resume",
778
+ previousStatus: subscription.status,
779
+ previousCancelAtPeriodEnd: subscription.cancelAtPeriodEnd,
780
+ });
781
+ },
782
+ }),
783
+ pause: mutationGeneric({
784
+ args: subscriptionPauseArgs,
785
+ handler: async (ctx, args) => {
786
+ const { entityId } = await resolve(ctx);
787
+ const subscription = args.subscriptionId
788
+ ? await ctx.runQuery(this.component.lib.getSubscription, {
789
+ id: args.subscriptionId,
790
+ })
791
+ : await ctx.runQuery(this.component.lib.getCurrentSubscription, {
792
+ entityId,
793
+ });
794
+ if (!subscription)
795
+ throw new ConvexError("Subscription not found");
796
+ if (subscription.status !== "active" &&
797
+ subscription.status !== "trialing") {
798
+ throw new ConvexError("Subscription is not active");
799
+ }
800
+ // Write optimistic state
801
+ await ctx.runMutation(this.component.lib.patchSubscription, {
802
+ subscriptionId: subscription.id,
803
+ status: "paused",
804
+ });
805
+ // Schedule the Creem API call
806
+ await ctx.scheduler.runAfter(0, this.component.lib.executeSubscriptionLifecycle, {
807
+ apiKey: this.apiKey,
808
+ serverIdx: this.serverIdx,
809
+ serverURL: this.serverURL,
810
+ subscriptionId: subscription.id,
811
+ operation: "pause",
812
+ previousStatus: subscription.status,
813
+ });
814
+ },
815
+ }),
816
+ list: queryGeneric({
817
+ args: {},
818
+ returns: v.any(),
819
+ handler: async (ctx) => {
820
+ const { entityId } = await resolve(ctx);
821
+ return await this.subscriptions.list(ctx, { entityId });
822
+ },
823
+ }),
824
+ listAll: queryGeneric({
825
+ args: {},
826
+ returns: v.array(v.object({
827
+ ...schema.tables.subscriptions.validator.fields,
828
+ product: v.union(schema.tables.products.validator, v.null()),
829
+ })),
830
+ handler: async (ctx) => {
831
+ const { entityId } = await resolve(ctx);
832
+ return await this.subscriptions.listAll(ctx, { entityId });
833
+ },
834
+ }),
835
+ },
836
+ products: {
837
+ list: queryGeneric({
838
+ args: {},
839
+ handler: async (ctx) => {
840
+ return await this.products.list(ctx);
841
+ },
842
+ }),
843
+ get: queryGeneric({
844
+ args: { productId: v.string() },
845
+ returns: v.union(schema.tables.products.validator, v.null()),
846
+ handler: async (ctx, args) => {
847
+ return await this.products.get(ctx, { productId: args.productId });
848
+ },
849
+ }),
850
+ },
851
+ customers: {
852
+ retrieve: queryGeneric({
853
+ args: {},
854
+ returns: v.union(schema.tables.customers.validator, v.null()),
855
+ handler: async (ctx) => {
856
+ const { entityId } = await resolve(ctx);
857
+ return await this.customers.retrieve(ctx, { entityId });
858
+ },
859
+ }),
860
+ portalUrl: actionGeneric({
861
+ args: {},
862
+ returns: v.object({ url: v.string() }),
863
+ handler: async (ctx) => {
864
+ const { entityId } = await resolve(ctx);
865
+ return await this.customers.portalUrl(ctx, { entityId });
866
+ },
867
+ }),
868
+ },
869
+ orders: {
870
+ list: queryGeneric({
871
+ args: {},
872
+ returns: v.array(schema.tables.orders.validator),
873
+ handler: async (ctx) => {
874
+ const { entityId } = await resolve(ctx);
875
+ return await this.orders.list(ctx, { entityId });
876
+ },
877
+ }),
878
+ },
879
+ };
880
+ }
881
+ /**
882
+ * Register the Creem webhook HTTP route on your Convex `httpRouter`.
883
+ *
884
+ * Automatically handles `checkout.completed`, `subscription.*`, and `product.*`
885
+ * events — upserts customers, subscriptions, orders, and products in the Convex DB.
886
+ *
887
+ * @param http - Your Convex HTTP router (from `httpRouter()`)
888
+ * @param options.path - Webhook endpoint path (default: `"/creem/events"`)
889
+ * @param options.events - Optional custom handlers that run **after** built-in processing
890
+ *
891
+ * @example
892
+ * ```ts
893
+ * const http = httpRouter();
894
+ * creem.registerRoutes(http, {
895
+ * events: {
896
+ * "checkout.completed": async (ctx, event) => { ... },
897
+ * },
898
+ * });
899
+ * ```
900
+ */
901
+ registerRoutes(http, { path = "/creem/events", events, } = {}) {
902
+ const mergedEvents = { ...events };
903
+ http.route({
904
+ path,
905
+ method: "POST",
906
+ handler: httpActionGeneric(async (ctx, request) => {
907
+ if (!request.body) {
908
+ throw new ConvexError("No body");
909
+ }
910
+ const body = await request.text();
911
+ const headers = {};
912
+ request.headers.forEach((value, key) => {
913
+ headers[key] = value;
914
+ });
915
+ try {
916
+ await this.verifyWebhook(body, headers);
917
+ const event = JSON.parse(body);
918
+ const eventType = getEventType(event);
919
+ const eventData = getEventData(event);
920
+ console.log(`[creem-webhook] eventType=${eventType}`, `body=${JSON.stringify(event)}`);
921
+ if (eventData &&
922
+ typeof eventData === "object" &&
923
+ eventType.startsWith("checkout.")) {
924
+ const raw = eventData;
925
+ const checkout = parseCheckout(raw);
926
+ if (checkout && eventType === "checkout.completed") {
927
+ // Auto-create customer record from checkout metadata
928
+ const customerObj = typeof checkout.customer === "object"
929
+ ? checkout.customer
930
+ : undefined;
931
+ const customerId = getCustomerId(customerObj);
932
+ const entityId = getConvexEntityId(checkout.metadata);
933
+ await this.upsertCustomerFromWebhook(ctx, customerId, entityId, customerObj);
934
+ // Process embedded subscription if present (recurring checkout).
935
+ // checkoutEntityFromJSON already parsed it into a typed SubscriptionEntity,
936
+ // so use it directly — do NOT re-parse through subscriptionEntityFromJSON.
937
+ if (checkout.subscription &&
938
+ typeof checkout.subscription === "object") {
939
+ const embeddedSub = checkout.subscription;
940
+ // Recover metadata: SDK strips it from SubscriptionEntity.
941
+ // Use checkout-level metadata as fallback (same convexUserId).
942
+ const embeddedRaw = (raw.subscription ?? {});
943
+ const rawMeta = (embeddedRaw.metadata ??
944
+ checkout.metadata ??
945
+ {});
946
+ const subscription = convertToDatabaseSubscription(embeddedSub, { rawMetadata: rawMeta });
947
+ await ctx.runMutation(this.component.lib.createSubscription, {
948
+ subscription,
949
+ });
950
+ }
951
+ // Store the order (present for both one-time and subscription checkouts)
952
+ if (checkout.order && typeof checkout.order === "object") {
953
+ const o = checkout.order;
954
+ const order = convertToOrder({
955
+ id: o.id,
956
+ customer: o.customer ?? null,
957
+ product: o.product,
958
+ amount: o.amount,
959
+ currency: o.currency,
960
+ status: o.status,
961
+ type: o.type,
962
+ transaction: o.transaction ?? null,
963
+ subTotal: o.subTotal,
964
+ sub_total: o.sub_total,
965
+ taxAmount: o.taxAmount,
966
+ tax_amount: o.tax_amount,
967
+ discountAmount: o.discountAmount,
968
+ discount_amount: o.discount_amount,
969
+ amountDue: o.amountDue,
970
+ amount_due: o.amount_due,
971
+ amountPaid: o.amountPaid,
972
+ amount_paid: o.amount_paid,
973
+ discount: o.discount ?? null,
974
+ affiliate: o.affiliate ?? null,
975
+ mode: o.mode,
976
+ createdAt: o.createdAt,
977
+ created_at: o.created_at,
978
+ updatedAt: o.updatedAt,
979
+ updated_at: o.updated_at,
980
+ }, {
981
+ checkoutId: checkout.id,
982
+ metadata: checkout.metadata,
983
+ });
984
+ await ctx.runMutation(this.component.lib.createOrder, {
985
+ order,
986
+ });
987
+ }
988
+ }
989
+ }
990
+ if (eventData &&
991
+ typeof eventData === "object" &&
992
+ eventType.startsWith("subscription.")) {
993
+ const raw = eventData;
994
+ const parsed = parseSubscription(raw);
995
+ if (parsed) {
996
+ // Pass raw metadata since SDK's SubscriptionEntity type strips it
997
+ const rawMeta = (raw.metadata ?? {});
998
+ const subscription = convertToDatabaseSubscription(parsed, {
999
+ rawMetadata: rawMeta,
1000
+ });
1001
+ if (eventType === "subscription.created") {
1002
+ await ctx.runMutation(this.component.lib.createSubscription, {
1003
+ subscription,
1004
+ });
1005
+ }
1006
+ else {
1007
+ await ctx.runMutation(this.component.lib.updateSubscription, {
1008
+ subscription,
1009
+ });
1010
+ }
1011
+ // Auto-create customer record from subscription metadata
1012
+ const customerEntity = typeof parsed.customer === "object"
1013
+ ? parsed.customer
1014
+ : undefined;
1015
+ const customerId = getCustomerId(parsed.customer);
1016
+ const entityId = getConvexEntityId(raw.metadata ??
1017
+ parsed.metadata);
1018
+ await this.upsertCustomerFromWebhook(ctx, customerId, entityId, customerEntity);
1019
+ }
1020
+ else {
1021
+ // Fallback: SDK parsing failed (e.g., unknown status)
1022
+ // Still try to extract subscription ID for update events
1023
+ const subId = typeof raw.id === "string" ? raw.id : null;
1024
+ if (subId) {
1025
+ console.warn(`Could not parse subscription for ${eventType}, id: ${subId}`);
1026
+ }
1027
+ }
1028
+ }
1029
+ if (eventData &&
1030
+ typeof eventData === "object" &&
1031
+ eventType.startsWith("product.")) {
1032
+ const raw = eventData;
1033
+ const parsed = parseProduct(raw);
1034
+ if (parsed) {
1035
+ const product = convertToDatabaseProduct(parsed);
1036
+ if (eventType === "product.created") {
1037
+ await ctx.runMutation(this.component.lib.createProduct, {
1038
+ product,
1039
+ });
1040
+ }
1041
+ else {
1042
+ await ctx.runMutation(this.component.lib.updateProduct, {
1043
+ product,
1044
+ });
1045
+ }
1046
+ }
1047
+ else {
1048
+ console.warn(`Could not parse product for ${eventType}`);
1049
+ }
1050
+ }
1051
+ const handler = mergedEvents[eventType];
1052
+ if (handler) {
1053
+ await handler(ctx, event);
1054
+ }
1055
+ return new Response("Accepted", { status: 202 });
1056
+ }
1057
+ catch (error) {
1058
+ if (error instanceof WebhookVerificationError) {
1059
+ console.error(error);
1060
+ return new Response("Forbidden", { status: 403 });
1061
+ }
1062
+ throw error;
1063
+ }
1064
+ }),
1065
+ });
1066
+ }
1067
+ }
1068
+ //# sourceMappingURL=index.js.map