@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,1554 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { Creem } from "./index.js";
3
+ import type { ComponentApi } from "../component/_generated/component.js";
4
+
5
+ type AnyFn = (...args: any[]) => any;
6
+
7
+ // ── Mock component with symbolic function references ────────────────
8
+
9
+ const REFS = {
10
+ getCustomerByEntityId: Symbol("getCustomerByEntityId"),
11
+ getCurrentSubscription: Symbol("getCurrentSubscription"),
12
+ getSubscription: Symbol("getSubscription"),
13
+ getProduct: Symbol("getProduct"),
14
+ listProducts: Symbol("listProducts"),
15
+ listUserSubscriptions: Symbol("listUserSubscriptions"),
16
+ listAllUserSubscriptions: Symbol("listAllUserSubscriptions"),
17
+ listUserOrders: Symbol("listUserOrders"),
18
+ insertCustomer: Symbol("insertCustomer"),
19
+ patchSubscription: Symbol("patchSubscription"),
20
+ createSubscription: Symbol("createSubscription"),
21
+ updateSubscription: Symbol("updateSubscription"),
22
+ createProduct: Symbol("createProduct"),
23
+ updateProduct: Symbol("updateProduct"),
24
+ createOrder: Symbol("createOrder"),
25
+ syncProducts: Symbol("syncProducts"),
26
+ executeSubscriptionUpdate: Symbol("executeSubscriptionUpdate"),
27
+ executeSubscriptionLifecycle: Symbol("executeSubscriptionLifecycle"),
28
+ } as const;
29
+
30
+ const mockComponent = {
31
+ lib: { ...REFS },
32
+ } as unknown as ComponentApi;
33
+
34
+ // ── Mock ctx factory ────────────────────────────────────────────────
35
+
36
+ function createMockCtx(queryMap: Record<symbol, unknown> = {}) {
37
+ return {
38
+ runQuery: vi.fn(async (ref: symbol, _args?: unknown) => {
39
+ if (ref in queryMap) return queryMap[ref as keyof typeof queryMap];
40
+ // Check if queryMap has a function for dynamic responses
41
+ const entry = queryMap[ref as keyof typeof queryMap];
42
+ return entry ?? null;
43
+ }),
44
+ runMutation: vi.fn(async () => {}),
45
+ runAction: vi.fn(async () => {}),
46
+ scheduler: { runAfter: vi.fn(async () => {}) },
47
+ };
48
+ }
49
+
50
+ // ── Test subscription fixture ───────────────────────────────────────
51
+
52
+ const ACTIVE_SUB = {
53
+ id: "sub_1",
54
+ productId: "prod_1",
55
+ status: "active" as const,
56
+ cancelAtPeriodEnd: false,
57
+ currentPeriodEnd: "2026-03-01T00:00:00Z",
58
+ currentPeriodStart: "2026-02-01T00:00:00Z",
59
+ recurringInterval: "monthly",
60
+ seats: 1,
61
+ trialEnd: null,
62
+ entityId: "user_1",
63
+ };
64
+
65
+ const PRODUCT_1 = {
66
+ id: "prod_1",
67
+ name: "Pro Plan",
68
+ price: 2999,
69
+ currency: "USD",
70
+ billingType: "recurring",
71
+ billingPeriod: "every-month",
72
+ status: "active",
73
+ defaultSuccessUrl: null,
74
+ };
75
+
76
+ // ── Constructor ─────────────────────────────────────────────────────
77
+
78
+ describe("Creem constructor", () => {
79
+ it("uses config values for apiKey and webhookSecret", () => {
80
+ const creem = new Creem(mockComponent, {
81
+ apiKey: "test_key",
82
+ webhookSecret: "test_secret",
83
+ });
84
+ expect(creem.sdk).toBeDefined();
85
+ });
86
+
87
+ it("falls back to env vars when config values not provided", () => {
88
+ const origKey = process.env["CREEM_API_KEY"];
89
+ const origSecret = process.env["CREEM_WEBHOOK_SECRET"];
90
+ process.env["CREEM_API_KEY"] = "env_key";
91
+ process.env["CREEM_WEBHOOK_SECRET"] = "env_secret";
92
+ try {
93
+ const creem = new Creem(mockComponent);
94
+ expect(creem.sdk).toBeDefined();
95
+ } finally {
96
+ if (origKey !== undefined) process.env["CREEM_API_KEY"] = origKey;
97
+ else delete process.env["CREEM_API_KEY"];
98
+ if (origSecret !== undefined)
99
+ process.env["CREEM_WEBHOOK_SECRET"] = origSecret;
100
+ else delete process.env["CREEM_WEBHOOK_SECRET"];
101
+ }
102
+ });
103
+
104
+ it("accepts serverIdx and serverURL config", () => {
105
+ const creem = new Creem(mockComponent, {
106
+ apiKey: "k",
107
+ serverIdx: 1,
108
+ serverURL: "https://custom.creem.io",
109
+ });
110
+ expect(creem.sdk).toBeDefined();
111
+ });
112
+ });
113
+
114
+ // ── Namespace getters: products / customers / orders ────────────────
115
+
116
+ describe("products namespace", () => {
117
+ let creem: Creem;
118
+ beforeEach(() => {
119
+ creem = new Creem(mockComponent, { apiKey: "k", webhookSecret: "s" });
120
+ });
121
+
122
+ it("list delegates to listProducts query", async () => {
123
+ const ctx = createMockCtx({
124
+ [REFS.listProducts]: [PRODUCT_1],
125
+ });
126
+ const result = await creem.products.list(ctx as never);
127
+ expect(ctx.runQuery).toHaveBeenCalledWith(REFS.listProducts, {
128
+ includeArchived: undefined,
129
+ });
130
+ expect(result).toEqual([PRODUCT_1]);
131
+ });
132
+
133
+ it("get delegates to getProduct query", async () => {
134
+ const ctx = createMockCtx({
135
+ [REFS.getProduct]: PRODUCT_1,
136
+ });
137
+ const result = await creem.products.get(ctx as never, {
138
+ productId: "prod_1",
139
+ });
140
+ expect(ctx.runQuery).toHaveBeenCalledWith(REFS.getProduct, {
141
+ id: "prod_1",
142
+ });
143
+ expect(result).toEqual(PRODUCT_1);
144
+ });
145
+ });
146
+
147
+ describe("customers namespace", () => {
148
+ let creem: Creem;
149
+ beforeEach(() => {
150
+ creem = new Creem(mockComponent, { apiKey: "k", webhookSecret: "s" });
151
+ });
152
+
153
+ it("retrieve delegates to getCustomerByEntityId query", async () => {
154
+ const mockCustomer = { id: "cust_1", entityId: "user_1" };
155
+ const ctx = createMockCtx({
156
+ [REFS.getCustomerByEntityId]: mockCustomer,
157
+ });
158
+ const result = await creem.customers.retrieve(ctx as never, {
159
+ entityId: "user_1",
160
+ });
161
+ expect(result).toEqual(mockCustomer);
162
+ });
163
+
164
+ it("portalUrl throws when customer not found", async () => {
165
+ const ctx = createMockCtx({
166
+ [REFS.getCustomerByEntityId]: null,
167
+ });
168
+ await expect(
169
+ creem.customers.portalUrl(ctx as never, { entityId: "user_1" }),
170
+ ).rejects.toThrow("Customer not found");
171
+ });
172
+ });
173
+
174
+ describe("orders namespace", () => {
175
+ let creem: Creem;
176
+ beforeEach(() => {
177
+ creem = new Creem(mockComponent, { apiKey: "k", webhookSecret: "s" });
178
+ });
179
+
180
+ it("list delegates to listUserOrders query", async () => {
181
+ const orders = [{ id: "ord_1", productId: "prod_1" }];
182
+ const ctx = createMockCtx({
183
+ [REFS.listUserOrders]: orders,
184
+ });
185
+ const result = await creem.orders.list(ctx as never, {
186
+ entityId: "user_1",
187
+ });
188
+ expect(result).toEqual(orders);
189
+ });
190
+ });
191
+
192
+ // ── subscriptions namespace ─────────────────────────────────────────
193
+
194
+ describe("subscriptions namespace", () => {
195
+ let creem: Creem;
196
+ beforeEach(() => {
197
+ creem = new Creem(mockComponent, { apiKey: "k", webhookSecret: "s" });
198
+ });
199
+
200
+ describe("getCurrent", () => {
201
+ it("returns subscription with product when found", async () => {
202
+ const ctx = createMockCtx({
203
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
204
+ [REFS.getProduct]: PRODUCT_1,
205
+ });
206
+ const result = await creem.subscriptions.getCurrent(ctx as never, {
207
+ entityId: "user_1",
208
+ });
209
+ expect(result).toEqual({ ...ACTIVE_SUB, product: PRODUCT_1 });
210
+ });
211
+
212
+ it("returns null when no subscription", async () => {
213
+ const ctx = createMockCtx({
214
+ [REFS.getCurrentSubscription]: null,
215
+ });
216
+ const result = await creem.subscriptions.getCurrent(ctx as never, {
217
+ entityId: "user_1",
218
+ });
219
+ expect(result).toBeNull();
220
+ });
221
+
222
+ it("throws when product not found for subscription", async () => {
223
+ const ctx = createMockCtx({
224
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
225
+ [REFS.getProduct]: null,
226
+ });
227
+ await expect(
228
+ creem.subscriptions.getCurrent(ctx as never, { entityId: "user_1" }),
229
+ ).rejects.toThrow("Product not found");
230
+ });
231
+ });
232
+
233
+ describe("update", () => {
234
+ it("throws when both productId and units provided", async () => {
235
+ const ctx = createMockCtx();
236
+ await expect(
237
+ creem.subscriptions.update(ctx as never, {
238
+ entityId: "user_1",
239
+ productId: "prod_2",
240
+ units: 5,
241
+ }),
242
+ ).rejects.toThrow("Provide productId OR units, not both");
243
+ });
244
+
245
+ it("throws when neither productId nor units provided", async () => {
246
+ const ctx = createMockCtx();
247
+ await expect(
248
+ creem.subscriptions.update(ctx as never, { entityId: "user_1" }),
249
+ ).rejects.toThrow("Provide productId or units");
250
+ });
251
+
252
+ it("throws when subscription not found", async () => {
253
+ const ctx = createMockCtx({
254
+ [REFS.getCurrentSubscription]: null,
255
+ });
256
+ await expect(
257
+ creem.subscriptions.update(ctx as never, {
258
+ entityId: "user_1",
259
+ units: 5,
260
+ }),
261
+ ).rejects.toThrow("Subscription not found");
262
+ });
263
+
264
+ it("patches subscription and schedules update for units change", async () => {
265
+ const ctx = createMockCtx({
266
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
267
+ });
268
+ await creem.subscriptions.update(ctx as never, {
269
+ entityId: "user_1",
270
+ units: 10,
271
+ });
272
+ expect(ctx.runMutation).toHaveBeenCalledWith(REFS.patchSubscription, {
273
+ subscriptionId: "sub_1",
274
+ seats: 10,
275
+ });
276
+ expect(ctx.scheduler.runAfter).toHaveBeenCalledWith(
277
+ 0,
278
+ REFS.executeSubscriptionUpdate,
279
+ expect.objectContaining({
280
+ subscriptionId: "sub_1",
281
+ units: 10,
282
+ }),
283
+ );
284
+ });
285
+
286
+ it("patches subscription and schedules update for productId change", async () => {
287
+ const ctx = createMockCtx({
288
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
289
+ });
290
+ await creem.subscriptions.update(ctx as never, {
291
+ entityId: "user_1",
292
+ productId: "prod_new",
293
+ });
294
+ expect(ctx.runMutation).toHaveBeenCalledWith(REFS.patchSubscription, {
295
+ subscriptionId: "sub_1",
296
+ productId: "prod_new",
297
+ seats: 1, // preserves current seats
298
+ });
299
+ expect(ctx.scheduler.runAfter).toHaveBeenCalledWith(
300
+ 0,
301
+ REFS.executeSubscriptionUpdate,
302
+ expect.objectContaining({
303
+ subscriptionId: "sub_1",
304
+ productId: "prod_new",
305
+ previousProductId: "prod_1",
306
+ }),
307
+ );
308
+ });
309
+
310
+ it("resolves by subscriptionId when provided", async () => {
311
+ const ctx = createMockCtx({
312
+ [REFS.getSubscription]: ACTIVE_SUB,
313
+ });
314
+ await creem.subscriptions.update(ctx as never, {
315
+ entityId: "user_1",
316
+ subscriptionId: "sub_1",
317
+ units: 3,
318
+ });
319
+ expect(ctx.runQuery).toHaveBeenCalledWith(REFS.getSubscription, {
320
+ id: "sub_1",
321
+ });
322
+ });
323
+ });
324
+
325
+ describe("cancel", () => {
326
+ it("throws when subscription not found", async () => {
327
+ const ctx = createMockCtx({
328
+ [REFS.getCurrentSubscription]: null,
329
+ });
330
+ await expect(
331
+ creem.subscriptions.cancel(ctx as never, { entityId: "user_1" }),
332
+ ).rejects.toThrow("Subscription not found");
333
+ });
334
+
335
+ it("throws when subscription is not active", async () => {
336
+ const ctx = createMockCtx({
337
+ [REFS.getCurrentSubscription]: { ...ACTIVE_SUB, status: "canceled" },
338
+ });
339
+ await expect(
340
+ creem.subscriptions.cancel(ctx as never, { entityId: "user_1" }),
341
+ ).rejects.toThrow("Subscription is not active");
342
+ });
343
+
344
+ it("scheduled cancel: patches cancelAtPeriodEnd and schedules lifecycle", async () => {
345
+ const ctx = createMockCtx({
346
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
347
+ });
348
+ await creem.subscriptions.cancel(ctx as never, { entityId: "user_1" });
349
+ expect(ctx.runMutation).toHaveBeenCalledWith(REFS.patchSubscription, {
350
+ subscriptionId: "sub_1",
351
+ cancelAtPeriodEnd: true,
352
+ });
353
+ expect(ctx.scheduler.runAfter).toHaveBeenCalledWith(
354
+ 0,
355
+ REFS.executeSubscriptionLifecycle,
356
+ expect.objectContaining({
357
+ operation: "cancel",
358
+ subscriptionId: "sub_1",
359
+ }),
360
+ );
361
+ });
362
+
363
+ it("immediate cancel: patches status to canceled", async () => {
364
+ const ctx = createMockCtx({
365
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
366
+ });
367
+ await creem.subscriptions.cancel(ctx as never, {
368
+ entityId: "user_1",
369
+ revokeImmediately: true,
370
+ });
371
+ expect(ctx.runMutation).toHaveBeenCalledWith(REFS.patchSubscription, {
372
+ subscriptionId: "sub_1",
373
+ status: "canceled",
374
+ cancelAtPeriodEnd: false,
375
+ });
376
+ expect(ctx.scheduler.runAfter).toHaveBeenCalledWith(
377
+ 0,
378
+ REFS.executeSubscriptionLifecycle,
379
+ expect.objectContaining({
380
+ operation: "cancel",
381
+ cancelMode: "immediate",
382
+ }),
383
+ );
384
+ });
385
+
386
+ it("uses config cancelMode as default", async () => {
387
+ const creemImmediate = new Creem(mockComponent, {
388
+ apiKey: "k",
389
+ webhookSecret: "s",
390
+ cancelMode: "immediate",
391
+ });
392
+ const ctx = createMockCtx({
393
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
394
+ });
395
+ await creemImmediate.subscriptions.cancel(ctx as never, {
396
+ entityId: "user_1",
397
+ });
398
+ expect(ctx.runMutation).toHaveBeenCalledWith(REFS.patchSubscription, {
399
+ subscriptionId: "sub_1",
400
+ status: "canceled",
401
+ cancelAtPeriodEnd: false,
402
+ });
403
+ });
404
+
405
+ it("explicit revokeImmediately overrides config cancelMode", async () => {
406
+ const creemImmediate = new Creem(mockComponent, {
407
+ apiKey: "k",
408
+ webhookSecret: "s",
409
+ cancelMode: "immediate",
410
+ });
411
+ const ctx = createMockCtx({
412
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
413
+ });
414
+ await creemImmediate.subscriptions.cancel(ctx as never, {
415
+ entityId: "user_1",
416
+ revokeImmediately: false,
417
+ });
418
+ // revokeImmediately: false overrides config.cancelMode: "immediate"
419
+ expect(ctx.runMutation).toHaveBeenCalledWith(REFS.patchSubscription, {
420
+ subscriptionId: "sub_1",
421
+ cancelAtPeriodEnd: true,
422
+ });
423
+ });
424
+
425
+ it("trialing subscription can be canceled", async () => {
426
+ const ctx = createMockCtx({
427
+ [REFS.getCurrentSubscription]: { ...ACTIVE_SUB, status: "trialing" },
428
+ });
429
+ await creem.subscriptions.cancel(ctx as never, { entityId: "user_1" });
430
+ expect(ctx.runMutation).toHaveBeenCalled();
431
+ });
432
+ });
433
+
434
+ describe("pause", () => {
435
+ it("throws when subscription not found", async () => {
436
+ const ctx = createMockCtx({
437
+ [REFS.getCurrentSubscription]: null,
438
+ });
439
+ await expect(
440
+ creem.subscriptions.pause(ctx as never, { entityId: "user_1" }),
441
+ ).rejects.toThrow("Subscription not found");
442
+ });
443
+
444
+ it("throws when subscription is not active", async () => {
445
+ const ctx = createMockCtx({
446
+ [REFS.getCurrentSubscription]: { ...ACTIVE_SUB, status: "paused" },
447
+ });
448
+ await expect(
449
+ creem.subscriptions.pause(ctx as never, { entityId: "user_1" }),
450
+ ).rejects.toThrow("Subscription is not active");
451
+ });
452
+
453
+ it("patches status to paused and schedules lifecycle", async () => {
454
+ const ctx = createMockCtx({
455
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
456
+ });
457
+ await creem.subscriptions.pause(ctx as never, { entityId: "user_1" });
458
+ expect(ctx.runMutation).toHaveBeenCalledWith(REFS.patchSubscription, {
459
+ subscriptionId: "sub_1",
460
+ status: "paused",
461
+ });
462
+ expect(ctx.scheduler.runAfter).toHaveBeenCalledWith(
463
+ 0,
464
+ REFS.executeSubscriptionLifecycle,
465
+ expect.objectContaining({
466
+ operation: "pause",
467
+ subscriptionId: "sub_1",
468
+ previousStatus: "active",
469
+ }),
470
+ );
471
+ });
472
+ });
473
+
474
+ describe("resume", () => {
475
+ it("throws when subscription not found", async () => {
476
+ const ctx = createMockCtx({
477
+ [REFS.getCurrentSubscription]: null,
478
+ });
479
+ await expect(
480
+ creem.subscriptions.resume(ctx as never, { entityId: "user_1" }),
481
+ ).rejects.toThrow("Subscription not found");
482
+ });
483
+
484
+ it("throws when subscription is not in a resumable state", async () => {
485
+ const ctx = createMockCtx({
486
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
487
+ });
488
+ await expect(
489
+ creem.subscriptions.resume(ctx as never, { entityId: "user_1" }),
490
+ ).rejects.toThrow("Subscription is not in a resumable state");
491
+ });
492
+
493
+ it("resumes scheduled_cancel subscription", async () => {
494
+ const ctx = createMockCtx({
495
+ [REFS.getCurrentSubscription]: {
496
+ ...ACTIVE_SUB,
497
+ status: "scheduled_cancel",
498
+ cancelAtPeriodEnd: true,
499
+ },
500
+ });
501
+ await creem.subscriptions.resume(ctx as never, { entityId: "user_1" });
502
+ expect(ctx.runMutation).toHaveBeenCalledWith(REFS.patchSubscription, {
503
+ subscriptionId: "sub_1",
504
+ status: "active",
505
+ cancelAtPeriodEnd: false,
506
+ });
507
+ expect(ctx.scheduler.runAfter).toHaveBeenCalledWith(
508
+ 0,
509
+ REFS.executeSubscriptionLifecycle,
510
+ expect.objectContaining({
511
+ operation: "resume",
512
+ subscriptionId: "sub_1",
513
+ previousStatus: "scheduled_cancel",
514
+ }),
515
+ );
516
+ });
517
+
518
+ it("resumes paused subscription", async () => {
519
+ const ctx = createMockCtx({
520
+ [REFS.getCurrentSubscription]: { ...ACTIVE_SUB, status: "paused" },
521
+ });
522
+ await creem.subscriptions.resume(ctx as never, { entityId: "user_1" });
523
+ expect(ctx.runMutation).toHaveBeenCalledWith(REFS.patchSubscription, {
524
+ subscriptionId: "sub_1",
525
+ status: "active",
526
+ cancelAtPeriodEnd: false,
527
+ });
528
+ });
529
+ });
530
+ });
531
+
532
+ // ── getBillingModel ─────────────────────────────────────────────────
533
+
534
+ describe("getBillingModel", () => {
535
+ let creem: Creem;
536
+ beforeEach(() => {
537
+ creem = new Creem(mockComponent, { apiKey: "k", webhookSecret: "s" });
538
+ });
539
+
540
+ it("returns unauthenticated model when entityId is null", async () => {
541
+ const ctx = createMockCtx({
542
+ [REFS.listProducts]: [PRODUCT_1],
543
+ });
544
+ const result = await creem.getBillingModel(ctx as never, {
545
+ entityId: null,
546
+ });
547
+ expect(result.billingSnapshot).toBeNull();
548
+ expect(result.allProducts).toEqual([PRODUCT_1]);
549
+ expect(result.ownedProductIds).toEqual([]);
550
+ expect(result.hasCreemCustomer).toBe(false);
551
+ expect(result.user).toBeNull();
552
+ });
553
+
554
+ it("returns unauthenticated model with user object when provided", async () => {
555
+ const ctx = createMockCtx({
556
+ [REFS.listProducts]: [PRODUCT_1],
557
+ });
558
+ const result = await creem.getBillingModel(ctx as never, {
559
+ entityId: null,
560
+ user: { _id: "user_1", email: "a@b.com" },
561
+ });
562
+ expect(result.user).toEqual({ _id: "user_1", email: "a@b.com" });
563
+ });
564
+
565
+ it("returns full model for authenticated user", async () => {
566
+ const ctx = createMockCtx({
567
+ [REFS.listProducts]: [PRODUCT_1],
568
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
569
+ [REFS.getProduct]: PRODUCT_1,
570
+ [REFS.listAllUserSubscriptions]: [ACTIVE_SUB],
571
+ [REFS.listUserSubscriptions]: [ACTIVE_SUB],
572
+ [REFS.getCustomerByEntityId]: { id: "cust_1" },
573
+ [REFS.listUserOrders]: [{ productId: "prod_1" }],
574
+ });
575
+ const result = await creem.getBillingModel(ctx as never, {
576
+ entityId: "user_1",
577
+ user: { _id: "user_1", email: "a@b.com" },
578
+ });
579
+ expect(result.billingSnapshot).toBeDefined();
580
+ expect(result.allProducts).toEqual([PRODUCT_1]);
581
+ expect(result.subscriptionProductId).toBe("prod_1");
582
+ expect(result.ownedProductIds).toEqual(["prod_1"]);
583
+ expect(result.hasCreemCustomer).toBe(true);
584
+ expect(result.activeSubscriptions).toHaveLength(1);
585
+ expect(result.activeSubscriptions[0].id).toBe("sub_1");
586
+ });
587
+
588
+ it("hasCreemCustomer is false when no customer", async () => {
589
+ const ctx = createMockCtx({
590
+ [REFS.listProducts]: [],
591
+ [REFS.getCurrentSubscription]: null,
592
+ [REFS.listAllUserSubscriptions]: [],
593
+ [REFS.listUserSubscriptions]: [],
594
+ [REFS.getCustomerByEntityId]: null,
595
+ [REFS.listUserOrders]: [],
596
+ });
597
+ const result = await creem.getBillingModel(ctx as never, {
598
+ entityId: "user_1",
599
+ });
600
+ expect(result.hasCreemCustomer).toBe(false);
601
+ expect(result.subscriptionProductId).toBeNull();
602
+ });
603
+ });
604
+
605
+ // ── getBillingSnapshot ──────────────────────────────────────────────
606
+
607
+ describe("getBillingSnapshot", () => {
608
+ let creem: Creem;
609
+ beforeEach(() => {
610
+ creem = new Creem(mockComponent, { apiKey: "k", webhookSecret: "s" });
611
+ });
612
+
613
+ it("returns a snapshot with no subscription", async () => {
614
+ const ctx = createMockCtx({
615
+ [REFS.getCurrentSubscription]: null,
616
+ [REFS.listAllUserSubscriptions]: [],
617
+ });
618
+ const result = await creem.getBillingSnapshot(ctx as never, {
619
+ entityId: "user_1",
620
+ });
621
+ expect(result).toBeDefined();
622
+ expect(result.activeCategory).toBeDefined();
623
+ expect(result.resolvedAt).toBeDefined();
624
+ });
625
+
626
+ it("returns a snapshot with active subscription", async () => {
627
+ const ctx = createMockCtx({
628
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
629
+ [REFS.getProduct]: PRODUCT_1,
630
+ [REFS.listAllUserSubscriptions]: [ACTIVE_SUB],
631
+ });
632
+ const result = await creem.getBillingSnapshot(ctx as never, {
633
+ entityId: "user_1",
634
+ });
635
+ expect(result).toBeDefined();
636
+ expect(result.activeCategory).toBeDefined();
637
+ expect(result.resolvedAt).toBeDefined();
638
+ });
639
+ });
640
+
641
+ // ── syncProducts ────────────────────────────────────────────────────
642
+
643
+ describe("syncProducts", () => {
644
+ it("delegates to component syncProducts action", async () => {
645
+ const creem = new Creem(mockComponent, {
646
+ apiKey: "test_key",
647
+ webhookSecret: "s",
648
+ });
649
+ const ctx = createMockCtx();
650
+ await creem.syncProducts(ctx as never);
651
+ expect(ctx.runAction).toHaveBeenCalledWith(REFS.syncProducts, {
652
+ apiKey: "test_key",
653
+ serverIdx: undefined,
654
+ serverURL: undefined,
655
+ });
656
+ });
657
+ });
658
+
659
+ // ── verifyWebhook (tested indirectly via HMAC path) ─────────────────
660
+
661
+ describe("verifyWebhook (HMAC path)", () => {
662
+ it("verifies a valid HMAC signature", async () => {
663
+ const creem = new Creem(mockComponent, {
664
+ apiKey: "k",
665
+ webhookSecret: "test-webhook-secret",
666
+ });
667
+
668
+ const body = '{"eventType":"checkout.completed","object":{}}';
669
+ // Generate a valid HMAC signature
670
+ const key = await crypto.subtle.importKey(
671
+ "raw",
672
+ new TextEncoder().encode("test-webhook-secret"),
673
+ { name: "HMAC", hash: "SHA-256" },
674
+ false,
675
+ ["sign"],
676
+ );
677
+ const digest = await crypto.subtle.sign(
678
+ "HMAC",
679
+ key,
680
+ new TextEncoder().encode(body),
681
+ );
682
+ const signature = Array.from(new Uint8Array(digest))
683
+ .map((b) => b.toString(16).padStart(2, "0"))
684
+ .join("");
685
+
686
+ const headers = { "creem-signature": signature };
687
+
688
+ // Access private verifyWebhook via the registerRoutes handler
689
+ // Instead, we test it through the webhook handler
690
+ const routeCapture: { handler?: AnyFn } = {};
691
+ const mockHttp = {
692
+ route: (config: { handler: { _handler: AnyFn } }) => {
693
+ routeCapture.handler = config.handler._handler;
694
+ },
695
+ };
696
+
697
+ const mockCtx = createMockCtx();
698
+ creem.registerRoutes(mockHttp as never, { path: "/test" });
699
+
700
+ // Call the captured handler with a valid signed request
701
+ const mockRequest = {
702
+ body: true,
703
+ text: async () => body,
704
+ headers: new Map(Object.entries(headers)),
705
+ };
706
+
707
+ const response = (await routeCapture.handler!(
708
+ mockCtx,
709
+ mockRequest,
710
+ )) as Response;
711
+ expect(response.status).toBe(202);
712
+ });
713
+
714
+ it("rejects invalid HMAC signature with 403", async () => {
715
+ const creem = new Creem(mockComponent, {
716
+ apiKey: "k",
717
+ webhookSecret: "test-webhook-secret",
718
+ });
719
+
720
+ const body = '{"eventType":"test.event","object":{}}';
721
+ const headers = { "creem-signature": "invalid_signature" };
722
+
723
+ const routeCapture: { handler?: AnyFn } = {};
724
+ const mockHttp = {
725
+ route: (config: { handler: { _handler: AnyFn } }) => {
726
+ routeCapture.handler = config.handler._handler;
727
+ },
728
+ };
729
+
730
+ const mockCtx = createMockCtx();
731
+ creem.registerRoutes(mockHttp as never, { path: "/test" });
732
+
733
+ const mockRequest = {
734
+ body: true,
735
+ text: async () => body,
736
+ headers: new Map(Object.entries(headers)),
737
+ };
738
+
739
+ const response = (await routeCapture.handler!(
740
+ mockCtx,
741
+ mockRequest,
742
+ )) as Response;
743
+ expect(response.status).toBe(403);
744
+ });
745
+
746
+ it("rejects when no signature headers present", async () => {
747
+ const creem = new Creem(mockComponent, {
748
+ apiKey: "k",
749
+ webhookSecret: "test-webhook-secret",
750
+ });
751
+
752
+ const body = '{"eventType":"test.event","object":{}}';
753
+ const headers = {};
754
+
755
+ const routeCapture: { handler?: AnyFn } = {};
756
+ const mockHttp = {
757
+ route: (config: { handler: { _handler: AnyFn } }) => {
758
+ routeCapture.handler = config.handler._handler;
759
+ },
760
+ };
761
+
762
+ const mockCtx = createMockCtx();
763
+ creem.registerRoutes(mockHttp as never, { path: "/test" });
764
+
765
+ const mockRequest = {
766
+ body: true,
767
+ text: async () => body,
768
+ headers: new Map(Object.entries(headers)),
769
+ };
770
+
771
+ const response = (await routeCapture.handler!(
772
+ mockCtx,
773
+ mockRequest,
774
+ )) as Response;
775
+ expect(response.status).toBe(403);
776
+ });
777
+
778
+ it("rejects when webhook secret is missing", async () => {
779
+ const creem = new Creem(mockComponent, {
780
+ apiKey: "k",
781
+ webhookSecret: "",
782
+ });
783
+
784
+ const body = '{"eventType":"test.event","object":{}}';
785
+ const headers = { "creem-signature": "abc" };
786
+
787
+ const routeCapture: { handler?: AnyFn } = {};
788
+ const mockHttp = {
789
+ route: (config: { handler: { _handler: AnyFn } }) => {
790
+ routeCapture.handler = config.handler._handler;
791
+ },
792
+ };
793
+
794
+ const mockCtx = createMockCtx();
795
+ creem.registerRoutes(mockHttp as never, { path: "/test" });
796
+
797
+ const mockRequest = {
798
+ body: true,
799
+ text: async () => body,
800
+ headers: new Map(Object.entries(headers)),
801
+ };
802
+
803
+ // Missing webhook secret throws ConvexError, not WebhookVerificationError
804
+ // The handler should still return or throw
805
+ await expect(routeCapture.handler!(mockCtx, mockRequest)).rejects.toThrow();
806
+ });
807
+ });
808
+
809
+ // ── registerRoutes webhook handler ──────────────────────────────────
810
+
811
+ describe("registerRoutes", () => {
812
+ function setupWebhookHandler(creem: Creem, events?: Record<string, AnyFn>) {
813
+ const routeCapture: { handler?: AnyFn; path?: string } = {};
814
+ const mockHttp = {
815
+ route: (config: { handler: { _handler: AnyFn }; path: string }) => {
816
+ // httpActionGeneric wraps the handler — extract the raw function
817
+ routeCapture.handler = config.handler._handler;
818
+ routeCapture.path = config.path;
819
+ },
820
+ };
821
+ creem.registerRoutes(mockHttp as never, {
822
+ path: "/creem/events",
823
+ events: events as never,
824
+ });
825
+ return routeCapture;
826
+ }
827
+
828
+ async function signAndSend(
829
+ handler: AnyFn,
830
+ ctx: ReturnType<typeof createMockCtx>,
831
+ body: string,
832
+ secret: string,
833
+ ) {
834
+ const key = await crypto.subtle.importKey(
835
+ "raw",
836
+ new TextEncoder().encode(secret),
837
+ { name: "HMAC", hash: "SHA-256" },
838
+ false,
839
+ ["sign"],
840
+ );
841
+ const digest = await crypto.subtle.sign(
842
+ "HMAC",
843
+ key,
844
+ new TextEncoder().encode(body),
845
+ );
846
+ const signature = Array.from(new Uint8Array(digest))
847
+ .map((b) => b.toString(16).padStart(2, "0"))
848
+ .join("");
849
+
850
+ const mockRequest = {
851
+ body: true,
852
+ text: async () => body,
853
+ headers: new Map([["creem-signature", signature]]),
854
+ };
855
+ return handler(ctx, mockRequest) as Promise<Response>;
856
+ }
857
+
858
+ const SECRET = "webhook-test-secret";
859
+
860
+ it("routes to default path /creem/events", () => {
861
+ const creem = new Creem(mockComponent, {
862
+ apiKey: "k",
863
+ webhookSecret: SECRET,
864
+ });
865
+ const routeCapture: { path?: string } = {};
866
+ const mockHttp = {
867
+ route: (config: { handler: { _handler: AnyFn }; path: string }) => {
868
+ routeCapture.path = config.path;
869
+ },
870
+ };
871
+ creem.registerRoutes(mockHttp as never);
872
+ expect(routeCapture.path).toBe("/creem/events");
873
+ });
874
+
875
+ it("handles checkout.completed with order — creates order", async () => {
876
+ const creem = new Creem(mockComponent, {
877
+ apiKey: "k",
878
+ webhookSecret: SECRET,
879
+ });
880
+ const { handler } = setupWebhookHandler(creem);
881
+ const ctx = createMockCtx();
882
+
883
+ // Use real Creem test webhook payload
884
+ const body = JSON.stringify({
885
+ eventType: "checkout.completed",
886
+ created_at: 1772265126979,
887
+ object: {
888
+ id: "ch_6u5rCq19LEsXpltlDGtc9Y",
889
+ object: "checkout",
890
+ request_id: "6e75be93-e116-4f30-9d83-c6a5a0021f90",
891
+ order: {
892
+ object: "order",
893
+ id: "ord_7ZKxZkpnNcs0d1TxbBPcr0",
894
+ customer: "cust_6aVJrSJi8h9r7cGzWPVBF0",
895
+ product: "prod_35PR89LmiAjsR8JJwO7uM7",
896
+ amount: 2999,
897
+ currency: "USD",
898
+ sub_total: 2999,
899
+ tax_amount: 500,
900
+ amount_due: 2999,
901
+ amount_paid: 2999,
902
+ status: "paid",
903
+ type: "onetime",
904
+ transaction: "tran_78fYacdKnQu3Bw3wOwtG60",
905
+ created_at: "2026-02-28T07:52:06.979Z",
906
+ updated_at: "2026-02-28T07:52:06.979Z",
907
+ mode: "test",
908
+ },
909
+ product: {
910
+ id: "prod_35PR89LmiAjsR8JJwO7uM7",
911
+ object: "product",
912
+ name: "Test Product",
913
+ description: "A test product",
914
+ price: 2999,
915
+ currency: "USD",
916
+ billing_type: "onetime",
917
+ billing_period: "once",
918
+ status: "active",
919
+ tax_mode: "exclusive",
920
+ tax_category: "saas",
921
+ default_success_url: null,
922
+ created_at: "2026-02-28T07:52:06.979Z",
923
+ updated_at: "2026-02-28T07:52:06.979Z",
924
+ mode: "test",
925
+ },
926
+ units: 1,
927
+ success_url: "https://example.com/success",
928
+ customer: {
929
+ id: "cust_6aVJrSJi8h9r7cGzWPVBF0",
930
+ object: "customer",
931
+ email: "test-customer@creem.io",
932
+ name: "Test Customer",
933
+ country: "US",
934
+ created_at: "2026-02-28T07:52:06.979Z",
935
+ updated_at: "2026-02-28T07:52:06.979Z",
936
+ mode: "test",
937
+ },
938
+ status: "completed",
939
+ mode: "test",
940
+ metadata: { convexUserId: "user_1" },
941
+ },
942
+ });
943
+
944
+ const response = await signAndSend(handler!, ctx, body, SECRET);
945
+ expect(response.status).toBe(202);
946
+ // Should have called insertCustomer and createOrder
947
+ expect(ctx.runMutation).toHaveBeenCalled();
948
+ });
949
+
950
+ it("handles subscription.active — updates subscription", async () => {
951
+ const creem = new Creem(mockComponent, {
952
+ apiKey: "k",
953
+ webhookSecret: SECRET,
954
+ });
955
+ const { handler } = setupWebhookHandler(creem);
956
+ const ctx = createMockCtx();
957
+
958
+ // Use real Creem test webhook payload
959
+ const body = JSON.stringify({
960
+ eventType: "subscription.active",
961
+ created_at: 1772265324184,
962
+ object: {
963
+ id: "sub_35c5ooVUuIUtru83zXjrjC",
964
+ object: "subscription",
965
+ product: {
966
+ id: "prod_3prhYLElQzQaZMq7pePQqf",
967
+ object: "product",
968
+ name: "Test Subscription Product",
969
+ description: "A test product for webhook simulation",
970
+ price: 2999,
971
+ currency: "USD",
972
+ billing_type: "recurring",
973
+ billing_period: "once",
974
+ status: "active",
975
+ tax_mode: "exclusive",
976
+ tax_category: "saas",
977
+ default_success_url: null,
978
+ created_at: "2026-02-28T07:55:24.184Z",
979
+ updated_at: "2026-02-28T07:55:24.184Z",
980
+ mode: "test",
981
+ },
982
+ customer: {
983
+ id: "cust_50fklVtISQkWoAydmZ1LJb",
984
+ object: "customer",
985
+ email: "test-customer@creem.io",
986
+ name: "Test Customer",
987
+ country: "US",
988
+ created_at: "2026-02-28T07:55:24.184Z",
989
+ updated_at: "2026-02-28T07:55:24.184Z",
990
+ mode: "test",
991
+ },
992
+ collection_method: "charge_automatically",
993
+ status: "active",
994
+ current_period_start_date: "2026-02-28T07:55:24.184Z",
995
+ current_period_end_date: "2026-03-30T07:55:24.184Z",
996
+ canceled_at: null,
997
+ created_at: "2026-02-28T07:55:24.184Z",
998
+ updated_at: "2026-02-28T07:55:24.184Z",
999
+ mode: "test",
1000
+ metadata: { convexUserId: "user_1" },
1001
+ },
1002
+ });
1003
+
1004
+ const response = await signAndSend(handler!, ctx, body, SECRET);
1005
+ expect(response.status).toBe(202);
1006
+ // Should update (not create, since eventType is subscription.active, not subscription.created)
1007
+ expect(ctx.runMutation).toHaveBeenCalledWith(
1008
+ REFS.updateSubscription,
1009
+ expect.objectContaining({
1010
+ subscription: expect.objectContaining({
1011
+ id: "sub_35c5ooVUuIUtru83zXjrjC",
1012
+ }),
1013
+ }),
1014
+ );
1015
+ });
1016
+
1017
+ it("handles subscription.created — calls createSubscription", async () => {
1018
+ const creem = new Creem(mockComponent, {
1019
+ apiKey: "k",
1020
+ webhookSecret: SECRET,
1021
+ });
1022
+ const { handler } = setupWebhookHandler(creem);
1023
+ const ctx = createMockCtx();
1024
+
1025
+ // Use real-shaped payload with eventType=subscription.created
1026
+ const body = JSON.stringify({
1027
+ eventType: "subscription.created",
1028
+ created_at: 1772265324184,
1029
+ object: {
1030
+ id: "sub_new_123",
1031
+ object: "subscription",
1032
+ product: {
1033
+ id: "prod_3prhYLElQzQaZMq7pePQqf",
1034
+ object: "product",
1035
+ name: "Test Subscription Product",
1036
+ description: "A test product",
1037
+ price: 2999,
1038
+ currency: "USD",
1039
+ billing_type: "recurring",
1040
+ billing_period: "once",
1041
+ status: "active",
1042
+ tax_mode: "exclusive",
1043
+ tax_category: "saas",
1044
+ default_success_url: null,
1045
+ created_at: "2026-02-28T07:55:24.184Z",
1046
+ updated_at: "2026-02-28T07:55:24.184Z",
1047
+ mode: "test",
1048
+ },
1049
+ customer: {
1050
+ id: "cust_50fklVtISQkWoAydmZ1LJb",
1051
+ object: "customer",
1052
+ email: "test-customer@creem.io",
1053
+ name: "Test Customer",
1054
+ country: "US",
1055
+ created_at: "2026-02-28T07:55:24.184Z",
1056
+ updated_at: "2026-02-28T07:55:24.184Z",
1057
+ mode: "test",
1058
+ },
1059
+ collection_method: "charge_automatically",
1060
+ status: "active",
1061
+ current_period_start_date: "2026-02-28T07:55:24.184Z",
1062
+ current_period_end_date: "2026-03-30T07:55:24.184Z",
1063
+ canceled_at: null,
1064
+ created_at: "2026-02-28T07:55:24.184Z",
1065
+ updated_at: "2026-02-28T07:55:24.184Z",
1066
+ mode: "test",
1067
+ metadata: { convexUserId: "user_2" },
1068
+ },
1069
+ });
1070
+
1071
+ const response = await signAndSend(handler!, ctx, body, SECRET);
1072
+ expect(response.status).toBe(202);
1073
+ expect(ctx.runMutation).toHaveBeenCalledWith(
1074
+ REFS.createSubscription,
1075
+ expect.objectContaining({
1076
+ subscription: expect.objectContaining({ id: "sub_new_123" }),
1077
+ }),
1078
+ );
1079
+ });
1080
+
1081
+ it("handles product.created — calls createProduct", async () => {
1082
+ const creem = new Creem(mockComponent, {
1083
+ apiKey: "k",
1084
+ webhookSecret: SECRET,
1085
+ });
1086
+ const { handler } = setupWebhookHandler(creem);
1087
+ const ctx = createMockCtx();
1088
+
1089
+ // product.* events have the product directly in object
1090
+ // But parseProduct may fail on simplified data — the handler logs warning and skips
1091
+ // Use real product shape from the checkout payload
1092
+ const body = JSON.stringify({
1093
+ eventType: "product.created",
1094
+ object: {
1095
+ id: "prod_35PR89LmiAjsR8JJwO7uM7",
1096
+ object: "product",
1097
+ name: "Test Product",
1098
+ description: "A test product for webhook simulation",
1099
+ price: 2999,
1100
+ currency: "USD",
1101
+ billing_type: "onetime",
1102
+ billing_period: "once",
1103
+ status: "active",
1104
+ tax_mode: "exclusive",
1105
+ tax_category: "saas",
1106
+ default_success_url: null,
1107
+ created_at: "2026-02-28T07:52:06.979Z",
1108
+ updated_at: "2026-02-28T07:52:06.979Z",
1109
+ mode: "test",
1110
+ },
1111
+ });
1112
+
1113
+ const response = await signAndSend(handler!, ctx, body, SECRET);
1114
+ expect(response.status).toBe(202);
1115
+ expect(ctx.runMutation).toHaveBeenCalledWith(
1116
+ REFS.createProduct,
1117
+ expect.objectContaining({
1118
+ product: expect.objectContaining({ id: "prod_35PR89LmiAjsR8JJwO7uM7" }),
1119
+ }),
1120
+ );
1121
+ });
1122
+
1123
+ it("handles product.updated — calls updateProduct", async () => {
1124
+ const creem = new Creem(mockComponent, {
1125
+ apiKey: "k",
1126
+ webhookSecret: SECRET,
1127
+ });
1128
+ const { handler } = setupWebhookHandler(creem);
1129
+ const ctx = createMockCtx();
1130
+
1131
+ const body = JSON.stringify({
1132
+ eventType: "product.updated",
1133
+ object: {
1134
+ id: "prod_35PR89LmiAjsR8JJwO7uM7",
1135
+ object: "product",
1136
+ name: "Updated Product",
1137
+ description: "Updated description",
1138
+ price: 1999,
1139
+ currency: "USD",
1140
+ billing_type: "onetime",
1141
+ billing_period: "once",
1142
+ status: "active",
1143
+ tax_mode: "exclusive",
1144
+ tax_category: "saas",
1145
+ default_success_url: null,
1146
+ created_at: "2026-02-28T07:52:06.979Z",
1147
+ updated_at: "2026-02-28T08:00:00.000Z",
1148
+ mode: "test",
1149
+ },
1150
+ });
1151
+
1152
+ const response = await signAndSend(handler!, ctx, body, SECRET);
1153
+ expect(response.status).toBe(202);
1154
+ expect(ctx.runMutation).toHaveBeenCalledWith(
1155
+ REFS.updateProduct,
1156
+ expect.objectContaining({
1157
+ product: expect.objectContaining({ id: "prod_35PR89LmiAjsR8JJwO7uM7" }),
1158
+ }),
1159
+ );
1160
+ });
1161
+
1162
+ it("calls custom event handler when provided", async () => {
1163
+ const creem = new Creem(mockComponent, {
1164
+ apiKey: "k",
1165
+ webhookSecret: SECRET,
1166
+ });
1167
+ const customHandler = vi.fn();
1168
+ const { handler } = setupWebhookHandler(creem, {
1169
+ "refund.created": customHandler,
1170
+ });
1171
+ const ctx = createMockCtx();
1172
+
1173
+ const body = JSON.stringify({
1174
+ eventType: "refund.created",
1175
+ object: { id: "ref_1", object: "refund" },
1176
+ });
1177
+
1178
+ const response = await signAndSend(handler!, ctx, body, SECRET);
1179
+ expect(response.status).toBe(202);
1180
+ expect(customHandler).toHaveBeenCalledWith(
1181
+ ctx,
1182
+ expect.objectContaining({ eventType: "refund.created" }),
1183
+ );
1184
+ });
1185
+
1186
+ it("handles subscription.canceled — updates subscription", async () => {
1187
+ const creem = new Creem(mockComponent, {
1188
+ apiKey: "k",
1189
+ webhookSecret: SECRET,
1190
+ });
1191
+ const { handler } = setupWebhookHandler(creem);
1192
+ const ctx = createMockCtx();
1193
+
1194
+ const body = JSON.stringify({
1195
+ eventType: "subscription.canceled",
1196
+ created_at: 1772265355057,
1197
+ object: {
1198
+ id: "sub_5PHyfaaDkDUmAsKdbH8mZZ",
1199
+ object: "subscription",
1200
+ product: {
1201
+ id: "prod_3Hj6Hd2higtUp5f4gi50vR",
1202
+ object: "product",
1203
+ name: "Test Subscription Product",
1204
+ description: "A test product for webhook simulation",
1205
+ price: 2999,
1206
+ currency: "USD",
1207
+ billing_type: "recurring",
1208
+ billing_period: "once",
1209
+ status: "active",
1210
+ tax_mode: "exclusive",
1211
+ tax_category: "saas",
1212
+ default_success_url: null,
1213
+ created_at: "2026-02-28T07:55:55.057Z",
1214
+ updated_at: "2026-02-28T07:55:55.057Z",
1215
+ mode: "test",
1216
+ },
1217
+ customer: {
1218
+ id: "cust_3TfrBYcWhn9oX22YP8B5Hc",
1219
+ object: "customer",
1220
+ email: "test-customer@creem.io",
1221
+ name: "Test Customer",
1222
+ country: "US",
1223
+ created_at: "2026-02-28T07:55:55.057Z",
1224
+ updated_at: "2026-02-28T07:55:55.057Z",
1225
+ mode: "test",
1226
+ },
1227
+ collection_method: "charge_automatically",
1228
+ status: "canceled",
1229
+ canceled_at: "2026-02-28T07:55:55.057Z",
1230
+ current_period_start_date: "2026-02-28T07:55:55.057Z",
1231
+ current_period_end_date: "2026-03-30T07:55:55.057Z",
1232
+ created_at: "2026-02-28T07:55:55.057Z",
1233
+ updated_at: "2026-02-28T07:55:55.057Z",
1234
+ mode: "test",
1235
+ metadata: { convexUserId: "user_1" },
1236
+ },
1237
+ });
1238
+
1239
+ const response = await signAndSend(handler!, ctx, body, SECRET);
1240
+ expect(response.status).toBe(202);
1241
+ expect(ctx.runMutation).toHaveBeenCalledWith(
1242
+ REFS.updateSubscription,
1243
+ expect.objectContaining({
1244
+ subscription: expect.objectContaining({
1245
+ id: "sub_5PHyfaaDkDUmAsKdbH8mZZ",
1246
+ }),
1247
+ }),
1248
+ );
1249
+ });
1250
+
1251
+ it("rejects request with no body", async () => {
1252
+ const creem = new Creem(mockComponent, {
1253
+ apiKey: "k",
1254
+ webhookSecret: SECRET,
1255
+ });
1256
+ const { handler } = setupWebhookHandler(creem);
1257
+ const ctx = createMockCtx();
1258
+
1259
+ const mockRequest = {
1260
+ body: null,
1261
+ text: async () => "",
1262
+ headers: new Map(),
1263
+ };
1264
+
1265
+ // ConvexError wraps the message in a data property
1266
+ await expect(handler!(ctx, mockRequest)).rejects.toThrow();
1267
+ });
1268
+ });
1269
+
1270
+ // ── api() convenience exports ───────────────────────────────────────
1271
+
1272
+ describe("api() convenience exports", () => {
1273
+ function extractHandler(wrapped: { _handler: AnyFn }) {
1274
+ return wrapped._handler;
1275
+ }
1276
+
1277
+ const resolve = vi.fn();
1278
+ let creem: Creem;
1279
+ let apiExports: ReturnType<Creem["api"]>;
1280
+
1281
+ beforeEach(() => {
1282
+ creem = new Creem(mockComponent, { apiKey: "k", webhookSecret: "s" });
1283
+ resolve.mockReset();
1284
+ apiExports = creem.api({ resolve });
1285
+ });
1286
+
1287
+ describe("uiModel", () => {
1288
+ it("returns unauthenticated model when resolve throws", async () => {
1289
+ resolve.mockRejectedValue(new Error("Not authenticated"));
1290
+ const ctx = createMockCtx({
1291
+ [REFS.listProducts]: [PRODUCT_1],
1292
+ });
1293
+ const handler = extractHandler(apiExports.uiModel as never);
1294
+ const result = await handler(ctx, {});
1295
+ expect(result.billingSnapshot).toBeNull();
1296
+ expect(result.allProducts).toEqual([PRODUCT_1]);
1297
+ });
1298
+
1299
+ it("returns authenticated model when resolve succeeds", async () => {
1300
+ resolve.mockResolvedValue({
1301
+ userId: "user_1",
1302
+ email: "a@b.com",
1303
+ entityId: "user_1",
1304
+ });
1305
+ const ctx = createMockCtx({
1306
+ [REFS.listProducts]: [PRODUCT_1],
1307
+ [REFS.getCurrentSubscription]: null,
1308
+ [REFS.listAllUserSubscriptions]: [],
1309
+ [REFS.listUserSubscriptions]: [],
1310
+ [REFS.getCustomerByEntityId]: null,
1311
+ [REFS.listUserOrders]: [],
1312
+ });
1313
+ const handler = extractHandler(apiExports.uiModel as never);
1314
+ const result = await handler(ctx, {});
1315
+ expect(result.user).toEqual({ _id: "user_1", email: "a@b.com" });
1316
+ expect(result.allProducts).toEqual([PRODUCT_1]);
1317
+ });
1318
+ });
1319
+
1320
+ describe("snapshot", () => {
1321
+ it("returns null when resolve throws", async () => {
1322
+ resolve.mockRejectedValue(new Error("Not authenticated"));
1323
+ const ctx = createMockCtx();
1324
+ const handler = extractHandler(apiExports.snapshot as never);
1325
+ const result = await handler(ctx, {});
1326
+ expect(result).toBeNull();
1327
+ });
1328
+
1329
+ it("returns null when resolve returns null", async () => {
1330
+ resolve.mockResolvedValue(null);
1331
+ const ctx = createMockCtx();
1332
+ const handler = extractHandler(apiExports.snapshot as never);
1333
+ const result = await handler(ctx, {});
1334
+ expect(result).toBeNull();
1335
+ });
1336
+
1337
+ it("returns snapshot when resolve succeeds", async () => {
1338
+ resolve.mockResolvedValue({ entityId: "user_1" });
1339
+ const ctx = createMockCtx({
1340
+ [REFS.getCurrentSubscription]: null,
1341
+ [REFS.listAllUserSubscriptions]: [],
1342
+ });
1343
+ const handler = extractHandler(apiExports.snapshot as never);
1344
+ const result = await handler(ctx, {});
1345
+ expect(result).toBeDefined();
1346
+ expect(result.resolvedAt).toBeDefined();
1347
+ });
1348
+ });
1349
+
1350
+ describe("subscriptions.update", () => {
1351
+ it("delegates to component with resolved entityId", async () => {
1352
+ resolve.mockResolvedValue({ entityId: "user_1" });
1353
+ const ctx = createMockCtx({
1354
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
1355
+ });
1356
+ const handler = extractHandler(apiExports.subscriptions.update as never);
1357
+ await handler(ctx, { units: 5 });
1358
+ expect(ctx.runMutation).toHaveBeenCalledWith(
1359
+ REFS.patchSubscription,
1360
+ expect.objectContaining({ subscriptionId: "sub_1", seats: 5 }),
1361
+ );
1362
+ });
1363
+
1364
+ it("throws when both productId and units provided", async () => {
1365
+ resolve.mockResolvedValue({ entityId: "user_1" });
1366
+ const ctx = createMockCtx();
1367
+ const handler = extractHandler(apiExports.subscriptions.update as never);
1368
+ await expect(
1369
+ handler(ctx, { productId: "prod_2", units: 5 }),
1370
+ ).rejects.toThrow();
1371
+ });
1372
+
1373
+ it("throws when neither productId nor units provided", async () => {
1374
+ resolve.mockResolvedValue({ entityId: "user_1" });
1375
+ const ctx = createMockCtx();
1376
+ const handler = extractHandler(apiExports.subscriptions.update as never);
1377
+ await expect(handler(ctx, {})).rejects.toThrow();
1378
+ });
1379
+ });
1380
+
1381
+ describe("subscriptions.cancel", () => {
1382
+ it("cancels subscription via resolved entityId", async () => {
1383
+ resolve.mockResolvedValue({ entityId: "user_1" });
1384
+ const ctx = createMockCtx({
1385
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
1386
+ });
1387
+ const handler = extractHandler(apiExports.subscriptions.cancel as never);
1388
+ await handler(ctx, {});
1389
+ expect(ctx.runMutation).toHaveBeenCalledWith(
1390
+ REFS.patchSubscription,
1391
+ expect.objectContaining({ subscriptionId: "sub_1" }),
1392
+ );
1393
+ expect(ctx.scheduler.runAfter).toHaveBeenCalledWith(
1394
+ 0,
1395
+ REFS.executeSubscriptionLifecycle,
1396
+ expect.objectContaining({ operation: "cancel" }),
1397
+ );
1398
+ });
1399
+
1400
+ it("throws when subscription not active", async () => {
1401
+ resolve.mockResolvedValue({ entityId: "user_1" });
1402
+ const ctx = createMockCtx({
1403
+ [REFS.getCurrentSubscription]: { ...ACTIVE_SUB, status: "canceled" },
1404
+ });
1405
+ const handler = extractHandler(apiExports.subscriptions.cancel as never);
1406
+ await expect(handler(ctx, {})).rejects.toThrow(
1407
+ "Subscription is not active",
1408
+ );
1409
+ });
1410
+
1411
+ it("supports immediate cancel via revokeImmediately arg", async () => {
1412
+ resolve.mockResolvedValue({ entityId: "user_1" });
1413
+ const ctx = createMockCtx({
1414
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
1415
+ });
1416
+ const handler = extractHandler(apiExports.subscriptions.cancel as never);
1417
+ await handler(ctx, { revokeImmediately: true });
1418
+ expect(ctx.runMutation).toHaveBeenCalledWith(REFS.patchSubscription, {
1419
+ subscriptionId: "sub_1",
1420
+ status: "canceled",
1421
+ cancelAtPeriodEnd: false,
1422
+ });
1423
+ });
1424
+ });
1425
+
1426
+ describe("subscriptions.resume", () => {
1427
+ it("resumes scheduled_cancel subscription", async () => {
1428
+ resolve.mockResolvedValue({ entityId: "user_1" });
1429
+ const ctx = createMockCtx({
1430
+ [REFS.getCurrentSubscription]: {
1431
+ ...ACTIVE_SUB,
1432
+ status: "scheduled_cancel",
1433
+ cancelAtPeriodEnd: true,
1434
+ },
1435
+ });
1436
+ const handler = extractHandler(apiExports.subscriptions.resume as never);
1437
+ await handler(ctx, {});
1438
+ expect(ctx.runMutation).toHaveBeenCalledWith(REFS.patchSubscription, {
1439
+ subscriptionId: "sub_1",
1440
+ status: "active",
1441
+ cancelAtPeriodEnd: false,
1442
+ });
1443
+ });
1444
+
1445
+ it("throws when subscription is not resumable", async () => {
1446
+ resolve.mockResolvedValue({ entityId: "user_1" });
1447
+ const ctx = createMockCtx({
1448
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
1449
+ });
1450
+ const handler = extractHandler(apiExports.subscriptions.resume as never);
1451
+ await expect(handler(ctx, {})).rejects.toThrow(
1452
+ "Subscription is not in a resumable state",
1453
+ );
1454
+ });
1455
+ });
1456
+
1457
+ describe("subscriptions.pause", () => {
1458
+ it("pauses active subscription", async () => {
1459
+ resolve.mockResolvedValue({ entityId: "user_1" });
1460
+ const ctx = createMockCtx({
1461
+ [REFS.getCurrentSubscription]: ACTIVE_SUB,
1462
+ });
1463
+ const handler = extractHandler(apiExports.subscriptions.pause as never);
1464
+ await handler(ctx, {});
1465
+ expect(ctx.runMutation).toHaveBeenCalledWith(REFS.patchSubscription, {
1466
+ subscriptionId: "sub_1",
1467
+ status: "paused",
1468
+ });
1469
+ });
1470
+
1471
+ it("throws when subscription is not active", async () => {
1472
+ resolve.mockResolvedValue({ entityId: "user_1" });
1473
+ const ctx = createMockCtx({
1474
+ [REFS.getCurrentSubscription]: { ...ACTIVE_SUB, status: "paused" },
1475
+ });
1476
+ const handler = extractHandler(apiExports.subscriptions.pause as never);
1477
+ await expect(handler(ctx, {})).rejects.toThrow(
1478
+ "Subscription is not active",
1479
+ );
1480
+ });
1481
+ });
1482
+
1483
+ describe("subscriptions.list", () => {
1484
+ it("delegates to subscriptions.list", async () => {
1485
+ resolve.mockResolvedValue({ entityId: "user_1" });
1486
+ const ctx = createMockCtx({
1487
+ [REFS.listUserSubscriptions]: [ACTIVE_SUB],
1488
+ });
1489
+ const handler = extractHandler(apiExports.subscriptions.list as never);
1490
+ const result = await handler(ctx, {});
1491
+ expect(result).toEqual([ACTIVE_SUB]);
1492
+ });
1493
+ });
1494
+
1495
+ describe("subscriptions.listAll", () => {
1496
+ it("delegates to subscriptions.listAll", async () => {
1497
+ resolve.mockResolvedValue({ entityId: "user_1" });
1498
+ const ctx = createMockCtx({
1499
+ [REFS.listAllUserSubscriptions]: [ACTIVE_SUB],
1500
+ });
1501
+ const handler = extractHandler(apiExports.subscriptions.listAll as never);
1502
+ const result = await handler(ctx, {});
1503
+ expect(result).toEqual([ACTIVE_SUB]);
1504
+ });
1505
+ });
1506
+
1507
+ describe("products.list", () => {
1508
+ it("delegates to products.list", async () => {
1509
+ const ctx = createMockCtx({
1510
+ [REFS.listProducts]: [PRODUCT_1],
1511
+ });
1512
+ const handler = extractHandler(apiExports.products.list as never);
1513
+ const result = await handler(ctx, {});
1514
+ expect(result).toEqual([PRODUCT_1]);
1515
+ });
1516
+ });
1517
+
1518
+ describe("products.get", () => {
1519
+ it("delegates to products.get", async () => {
1520
+ const ctx = createMockCtx({
1521
+ [REFS.getProduct]: PRODUCT_1,
1522
+ });
1523
+ const handler = extractHandler(apiExports.products.get as never);
1524
+ const result = await handler(ctx, { productId: "prod_1" });
1525
+ expect(result).toEqual(PRODUCT_1);
1526
+ });
1527
+ });
1528
+
1529
+ describe("customers.retrieve", () => {
1530
+ it("delegates to customers.retrieve", async () => {
1531
+ resolve.mockResolvedValue({ entityId: "user_1" });
1532
+ const mockCustomer = { id: "cust_1", entityId: "user_1" };
1533
+ const ctx = createMockCtx({
1534
+ [REFS.getCustomerByEntityId]: mockCustomer,
1535
+ });
1536
+ const handler = extractHandler(apiExports.customers.retrieve as never);
1537
+ const result = await handler(ctx, {});
1538
+ expect(result).toEqual(mockCustomer);
1539
+ });
1540
+ });
1541
+
1542
+ describe("orders.list", () => {
1543
+ it("delegates to orders.list", async () => {
1544
+ resolve.mockResolvedValue({ entityId: "user_1" });
1545
+ const orders = [{ id: "ord_1", productId: "prod_1" }];
1546
+ const ctx = createMockCtx({
1547
+ [REFS.listUserOrders]: orders,
1548
+ });
1549
+ const handler = extractHandler(apiExports.orders.list as never);
1550
+ const result = await handler(ctx, {});
1551
+ expect(result).toEqual(orders);
1552
+ });
1553
+ });
1554
+ });