@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,726 @@
1
+ import { Creem } from "creem";
2
+
3
+ import { ConvexError, v } from "convex/values";
4
+ import { action, mutation, query } from "./_generated/server.js";
5
+ import schema from "./schema.js";
6
+ import { asyncMap } from "convex-helpers";
7
+ import { api } from "./_generated/api.js";
8
+ import { convertToDatabaseProduct } from "./util.js";
9
+
10
+ export const getCustomerByEntityId = query({
11
+ args: {
12
+ entityId: v.string(),
13
+ },
14
+ returns: v.union(schema.tables.customers.validator, v.null()),
15
+ handler: async (ctx, args) => {
16
+ const customer = await ctx.db
17
+ .query("customers")
18
+ .withIndex("entityId", (q) => q.eq("entityId", args.entityId))
19
+ .unique();
20
+ return omitSystemFields(customer);
21
+ },
22
+ });
23
+
24
+ export const insertCustomer = mutation({
25
+ args: schema.tables.customers.validator,
26
+ returns: v.id("customers"),
27
+ handler: async (ctx, args) => {
28
+ const existingCustomer = await ctx.db
29
+ .query("customers")
30
+ .withIndex("entityId", (q) => q.eq("entityId", args.entityId))
31
+ .unique();
32
+ if (existingCustomer) {
33
+ // Enrich existing customer record with any new fields
34
+ const patch: Record<string, unknown> = {};
35
+ if (args.email && !existingCustomer.email) patch.email = args.email;
36
+ if (args.name && !existingCustomer.name) patch.name = args.name;
37
+ if (args.country && !existingCustomer.country)
38
+ patch.country = args.country;
39
+ if (args.mode) patch.mode = args.mode;
40
+ if (args.updatedAt) patch.updatedAt = args.updatedAt;
41
+ if (Object.keys(patch).length > 0) {
42
+ await ctx.db.patch(existingCustomer._id, patch);
43
+ }
44
+ return existingCustomer._id;
45
+ }
46
+ return ctx.db.insert("customers", args);
47
+ },
48
+ });
49
+
50
+ export const getSubscription = query({
51
+ args: {
52
+ id: v.string(),
53
+ },
54
+ returns: v.union(schema.tables.subscriptions.validator, v.null()),
55
+ handler: async (ctx, args) => {
56
+ const subscription = await ctx.db
57
+ .query("subscriptions")
58
+ .withIndex("id", (q) => q.eq("id", args.id))
59
+ .unique();
60
+ return omitSystemFields(subscription);
61
+ },
62
+ });
63
+
64
+ export const getProduct = query({
65
+ args: {
66
+ id: v.string(),
67
+ },
68
+ returns: v.union(schema.tables.products.validator, v.null()),
69
+ handler: async (ctx, args) => {
70
+ const product = await ctx.db
71
+ .query("products")
72
+ .withIndex("id", (q) => q.eq("id", args.id))
73
+ .unique();
74
+ return omitSystemFields(product);
75
+ },
76
+ });
77
+
78
+ /** For apps that have 0 or 1 active subscription per user. Excludes expired trials. */
79
+ export const getCurrentSubscription = query({
80
+ args: {
81
+ entityId: v.string(),
82
+ },
83
+ returns: v.union(
84
+ v.object({
85
+ ...schema.tables.subscriptions.validator.fields,
86
+ product: schema.tables.products.validator,
87
+ }),
88
+ v.null(),
89
+ ),
90
+ handler: async (ctx, args) => {
91
+ const customer = await ctx.db
92
+ .query("customers")
93
+ .withIndex("entityId", (q) => q.eq("entityId", args.entityId))
94
+ .unique();
95
+ if (!customer) {
96
+ return null;
97
+ }
98
+ const subscription = await ctx.db
99
+ .query("subscriptions")
100
+ .withIndex("customerId_endedAt", (q) =>
101
+ q.eq("customerId", customer.id).eq("endedAt", null),
102
+ )
103
+ .first();
104
+ if (!subscription) {
105
+ return null;
106
+ }
107
+ if (
108
+ subscription.status === "trialing" &&
109
+ subscription.trialEnd &&
110
+ subscription.trialEnd <= new Date().toISOString()
111
+ ) {
112
+ return null;
113
+ }
114
+ const product = await ctx.db
115
+ .query("products")
116
+ .withIndex("id", (q) => q.eq("id", subscription.productId))
117
+ .unique();
118
+ if (!product) {
119
+ throw new ConvexError(`Product not found: ${subscription.productId}`);
120
+ }
121
+ return {
122
+ ...omitSystemFields(subscription),
123
+ product: omitSystemFields(product),
124
+ };
125
+ },
126
+ });
127
+
128
+ /** List active subscriptions for a user, excluding ended and expired trials. */
129
+ export const listUserSubscriptions = query({
130
+ args: {
131
+ entityId: v.string(),
132
+ },
133
+ returns: v.array(
134
+ v.object({
135
+ ...schema.tables.subscriptions.validator.fields,
136
+ product: v.union(schema.tables.products.validator, v.null()),
137
+ }),
138
+ ),
139
+ handler: async (ctx, args) => {
140
+ const customer = await ctx.db
141
+ .query("customers")
142
+ .withIndex("entityId", (q) => q.eq("entityId", args.entityId))
143
+ .unique();
144
+ if (!customer) {
145
+ return [];
146
+ }
147
+ const now = new Date().toISOString();
148
+ const subscriptions = await asyncMap(
149
+ ctx.db
150
+ .query("subscriptions")
151
+ .withIndex("customerId", (q) => q.eq("customerId", customer.id))
152
+ .collect(),
153
+ async (subscription) => {
154
+ if (
155
+ (subscription.endedAt && subscription.endedAt <= now) ||
156
+ (subscription.status === "trialing" &&
157
+ subscription.trialEnd &&
158
+ subscription.trialEnd <= now)
159
+ ) {
160
+ return;
161
+ }
162
+ const product = subscription.productId
163
+ ? (await ctx.db
164
+ .query("products")
165
+ .withIndex("id", (q) => q.eq("id", subscription.productId))
166
+ .unique()) || null
167
+ : null;
168
+ return {
169
+ ...omitSystemFields(subscription),
170
+ product: omitSystemFields(product),
171
+ };
172
+ },
173
+ );
174
+ return subscriptions.flatMap((subscription) =>
175
+ subscription ? [subscription] : [],
176
+ );
177
+ },
178
+ });
179
+
180
+ /** Returns all subscriptions for a user, including ended and expired trials. */
181
+ export const listAllUserSubscriptions = query({
182
+ args: {
183
+ entityId: v.string(),
184
+ },
185
+ returns: v.array(
186
+ v.object({
187
+ ...schema.tables.subscriptions.validator.fields,
188
+ product: v.union(schema.tables.products.validator, v.null()),
189
+ }),
190
+ ),
191
+ handler: async (ctx, args) => {
192
+ const customer = await ctx.db
193
+ .query("customers")
194
+ .withIndex("entityId", (q) => q.eq("entityId", args.entityId))
195
+ .unique();
196
+ if (!customer) {
197
+ return [];
198
+ }
199
+ const subscriptions = await asyncMap(
200
+ ctx.db
201
+ .query("subscriptions")
202
+ .withIndex("customerId", (q) => q.eq("customerId", customer.id))
203
+ .collect(),
204
+ async (subscription) => {
205
+ const product = subscription.productId
206
+ ? (await ctx.db
207
+ .query("products")
208
+ .withIndex("id", (q) => q.eq("id", subscription.productId))
209
+ .unique()) || null
210
+ : null;
211
+ return {
212
+ ...omitSystemFields(subscription),
213
+ product: omitSystemFields(product),
214
+ };
215
+ },
216
+ );
217
+ return subscriptions;
218
+ },
219
+ });
220
+
221
+ export const listProducts = query({
222
+ args: {
223
+ includeArchived: v.optional(v.boolean()),
224
+ },
225
+ returns: v.array(schema.tables.products.validator),
226
+ handler: async (ctx, args) => {
227
+ const q = ctx.db.query("products");
228
+ const products = args.includeArchived
229
+ ? await q.collect()
230
+ : await q.withIndex("status", (q) => q.eq("status", "active")).collect();
231
+ return products.map((product) => omitSystemFields(product));
232
+ },
233
+ });
234
+
235
+ export const createSubscription = mutation({
236
+ args: {
237
+ subscription: schema.tables.subscriptions.validator,
238
+ },
239
+ handler: async (ctx, args) => {
240
+ const existingSubscription = await ctx.db
241
+ .query("subscriptions")
242
+ .withIndex("id", (q) => q.eq("id", args.subscription.id))
243
+ .unique();
244
+ if (!existingSubscription) {
245
+ await ctx.db.insert("subscriptions", args.subscription);
246
+ return;
247
+ }
248
+ // Timestamp guard: skip if existing record is newer
249
+ const incomingModifiedAt = args.subscription.modifiedAt ?? "";
250
+ const existingModifiedAt = existingSubscription.modifiedAt ?? "";
251
+ if (existingModifiedAt > incomingModifiedAt) {
252
+ return; // stale webhook, skip
253
+ }
254
+ await ctx.db.patch(existingSubscription._id, args.subscription);
255
+ },
256
+ });
257
+
258
+ export const updateSubscription = mutation({
259
+ args: {
260
+ subscription: schema.tables.subscriptions.validator,
261
+ },
262
+ handler: async (ctx, args) => {
263
+ const existingSubscription = await ctx.db
264
+ .query("subscriptions")
265
+ .withIndex("id", (q) => q.eq("id", args.subscription.id))
266
+ .unique();
267
+ if (!existingSubscription) {
268
+ // Subscription doesn't exist yet — insert instead of throwing
269
+ await ctx.db.insert("subscriptions", args.subscription);
270
+ return;
271
+ }
272
+ // Timestamp guard: skip if existing record is newer
273
+ const incomingModifiedAt = args.subscription.modifiedAt ?? "";
274
+ const existingModifiedAt = existingSubscription.modifiedAt ?? "";
275
+ if (existingModifiedAt > incomingModifiedAt) {
276
+ return; // stale webhook, skip
277
+ }
278
+
279
+ // Optimistic-update guard: if a recent patchSubscription set optimistic
280
+ // fields, don't let intermediate webhook events revert those values.
281
+ const existingMeta = (existingSubscription.metadata ?? {}) as Record<
282
+ string,
283
+ unknown
284
+ >;
285
+ const pendingAt = existingMeta._optimisticPendingAt as number | undefined;
286
+ const optimisticFields = existingMeta._optimisticFields as
287
+ | string[]
288
+ | undefined;
289
+ const isOptimisticPending =
290
+ pendingAt != null && Date.now() - pendingAt < 30_000;
291
+
292
+ const subscriptionToWrite = { ...args.subscription };
293
+
294
+ if (isOptimisticPending && optimisticFields?.length) {
295
+ console.debug(
296
+ `[creem] optimistic guard active for sub=${args.subscription.id}`,
297
+ {
298
+ guardFields: optimisticFields,
299
+ guardAge: `${Math.round((Date.now() - (pendingAt ?? 0)) / 1000)}s`,
300
+ incoming: {
301
+ productId: args.subscription.productId,
302
+ seats: args.subscription.seats,
303
+ },
304
+ db: {
305
+ productId: existingSubscription.productId,
306
+ seats: existingSubscription.seats,
307
+ },
308
+ },
309
+ );
310
+ let allConfirmed = true;
311
+
312
+ if (optimisticFields.includes("seats")) {
313
+ if (args.subscription.seats !== existingSubscription.seats) {
314
+ subscriptionToWrite.seats = existingSubscription.seats;
315
+ allConfirmed = false;
316
+ console.log(
317
+ `[creem] guard: preserving optimistic seats=${existingSubscription.seats} (webhook sent ${args.subscription.seats})`,
318
+ );
319
+ }
320
+ }
321
+ if (optimisticFields.includes("productId")) {
322
+ if (args.subscription.productId !== existingSubscription.productId) {
323
+ subscriptionToWrite.productId = existingSubscription.productId;
324
+ allConfirmed = false;
325
+ console.log(
326
+ `[creem] guard: preserving optimistic productId=${existingSubscription.productId} (webhook sent ${args.subscription.productId})`,
327
+ );
328
+ }
329
+ }
330
+
331
+ // Only clear the guard when ALL tracked fields match in a single webhook.
332
+ // Partial matches are not trusted — Creem sends intermediate states where
333
+ // some fields update temporarily before reverting (e.g. subscription.product
334
+ // changes on upgrade but items[0].product_id stays stale).
335
+ const incomingMeta = (args.subscription.metadata ?? {}) as Record<
336
+ string,
337
+ unknown
338
+ >;
339
+ if (allConfirmed) {
340
+ console.log(
341
+ `[creem] guard: all optimistic fields confirmed for sub=${args.subscription.id} — clearing`,
342
+ );
343
+ const {
344
+ _optimisticPendingAt: _,
345
+ _optimisticFields: __,
346
+ ...cleanMeta
347
+ } = { ...existingMeta, ...incomingMeta };
348
+ subscriptionToWrite.metadata = cleanMeta;
349
+ } else {
350
+ subscriptionToWrite.metadata = {
351
+ ...existingMeta,
352
+ ...incomingMeta,
353
+ _optimisticPendingAt: pendingAt,
354
+ _optimisticFields: optimisticFields,
355
+ };
356
+ }
357
+ }
358
+
359
+ await ctx.db.patch(existingSubscription._id, subscriptionToWrite);
360
+ },
361
+ });
362
+
363
+ export const createProduct = mutation({
364
+ args: {
365
+ product: schema.tables.products.validator,
366
+ },
367
+ handler: async (ctx, args) => {
368
+ const existingProduct = await ctx.db
369
+ .query("products")
370
+ .withIndex("id", (q) => q.eq("id", args.product.id))
371
+ .unique();
372
+ if (!existingProduct) {
373
+ await ctx.db.insert("products", args.product);
374
+ return;
375
+ }
376
+ // Timestamp guard: skip if existing record is newer
377
+ const incomingModifiedAt = args.product.modifiedAt ?? "";
378
+ const existingModifiedAt = existingProduct.modifiedAt ?? "";
379
+ if (existingModifiedAt > incomingModifiedAt) {
380
+ return; // stale webhook, skip
381
+ }
382
+ await ctx.db.patch(existingProduct._id, args.product);
383
+ },
384
+ });
385
+
386
+ export const updateProduct = mutation({
387
+ args: {
388
+ product: schema.tables.products.validator,
389
+ },
390
+ handler: async (ctx, args) => {
391
+ const existingProduct = await ctx.db
392
+ .query("products")
393
+ .withIndex("id", (q) => q.eq("id", args.product.id))
394
+ .unique();
395
+ if (!existingProduct) {
396
+ // Product doesn't exist yet — insert instead of throwing
397
+ await ctx.db.insert("products", args.product);
398
+ return;
399
+ }
400
+ // Timestamp guard: skip if existing record is newer
401
+ const incomingModifiedAt = args.product.modifiedAt ?? "";
402
+ const existingModifiedAt = existingProduct.modifiedAt ?? "";
403
+ if (existingModifiedAt > incomingModifiedAt) {
404
+ return; // stale webhook, skip
405
+ }
406
+ await ctx.db.patch(existingProduct._id, args.product);
407
+ },
408
+ });
409
+
410
+ export const createOrder = mutation({
411
+ args: {
412
+ order: schema.tables.orders.validator,
413
+ },
414
+ handler: async (ctx, args) => {
415
+ const existing = await ctx.db
416
+ .query("orders")
417
+ .withIndex("id", (q) => q.eq("id", args.order.id))
418
+ .unique();
419
+ if (!existing) {
420
+ await ctx.db.insert("orders", args.order);
421
+ return;
422
+ }
423
+ // Update if incoming is newer
424
+ if ((args.order.updatedAt ?? "") >= (existing.updatedAt ?? "")) {
425
+ await ctx.db.patch(existing._id, args.order);
426
+ }
427
+ },
428
+ });
429
+
430
+ /** List paid one-time orders for a user. */
431
+ export const listUserOrders = query({
432
+ args: {
433
+ entityId: v.string(),
434
+ },
435
+ returns: v.array(schema.tables.orders.validator),
436
+ handler: async (ctx, args) => {
437
+ const customer = await ctx.db
438
+ .query("customers")
439
+ .withIndex("entityId", (q) => q.eq("entityId", args.entityId))
440
+ .unique();
441
+ if (!customer) {
442
+ return [];
443
+ }
444
+ const orders = await ctx.db
445
+ .query("orders")
446
+ .withIndex("customerId", (q) => q.eq("customerId", customer.id))
447
+ .collect();
448
+ return orders
449
+ .filter((o) => o.status === "paid" && o.type === "onetime")
450
+ .map(omitSystemFields);
451
+ },
452
+ });
453
+
454
+ export const listCustomerSubscriptions = query({
455
+ args: {
456
+ customerId: v.string(),
457
+ },
458
+ returns: v.array(schema.tables.subscriptions.validator),
459
+ handler: async (ctx, args) => {
460
+ const subscriptions = await ctx.db
461
+ .query("subscriptions")
462
+ .withIndex("customerId", (q) => q.eq("customerId", args.customerId))
463
+ .collect();
464
+ return subscriptions.map(omitSystemFields);
465
+ },
466
+ });
467
+
468
+ export const syncProducts = action({
469
+ args: {
470
+ apiKey: v.string(),
471
+ serverIdx: v.optional(v.number()),
472
+ serverURL: v.optional(v.string()),
473
+ },
474
+ handler: async (ctx, args) => {
475
+ const creem = new Creem({
476
+ apiKey: args.apiKey,
477
+ ...(args.serverIdx !== undefined ? { serverIdx: args.serverIdx } : {}),
478
+ ...(args.serverURL ? { serverURL: args.serverURL } : {}),
479
+ });
480
+ let pageNumber = 1;
481
+ let isDone = false;
482
+ do {
483
+ const products = await creem.products.search(pageNumber, 100);
484
+ pageNumber += 1;
485
+ isDone =
486
+ products.pagination.currentPage >= products.pagination.totalPages;
487
+ await ctx.runMutation(api.lib.updateProducts, {
488
+ products: products.items.map(convertToDatabaseProduct),
489
+ });
490
+ } while (!isDone);
491
+ },
492
+ });
493
+
494
+ export const updateProducts = mutation({
495
+ args: {
496
+ products: v.array(schema.tables.products.validator),
497
+ },
498
+ handler: async (ctx, args) => {
499
+ await asyncMap(args.products, async (product) => {
500
+ const existingProduct = await ctx.db
501
+ .query("products")
502
+ .withIndex("id", (q) => q.eq("id", product.id))
503
+ .unique();
504
+ if (existingProduct) {
505
+ await ctx.db.patch(existingProduct._id, product);
506
+ return;
507
+ }
508
+ await ctx.db.insert("products", product);
509
+ });
510
+ },
511
+ });
512
+
513
+ /** Lightweight patch for optimistic UI updates (seats, productId, status).
514
+ * Tracks which fields were optimistically changed via `_optimisticPendingAt`
515
+ * and `_optimisticFields` in the subscription's metadata so that incoming
516
+ * webhooks with stale intermediate values don't overwrite the optimistic state. */
517
+ export const patchSubscription = mutation({
518
+ args: {
519
+ subscriptionId: v.string(),
520
+ seats: v.optional(v.union(v.number(), v.null())),
521
+ productId: v.optional(v.string()),
522
+ status: v.optional(v.string()),
523
+ cancelAtPeriodEnd: v.optional(v.boolean()),
524
+ clearOptimistic: v.optional(v.boolean()),
525
+ },
526
+ handler: async (ctx, args) => {
527
+ const sub = await ctx.db
528
+ .query("subscriptions")
529
+ .withIndex("id", (q) => q.eq("id", args.subscriptionId))
530
+ .unique();
531
+ if (!sub)
532
+ throw new ConvexError(`Subscription not found: ${args.subscriptionId}`);
533
+ const patch: Record<string, unknown> = {};
534
+ const optimisticFields: string[] = [];
535
+ if (args.seats !== undefined) {
536
+ patch.seats = args.seats;
537
+ optimisticFields.push("seats");
538
+ }
539
+ if (args.productId !== undefined) {
540
+ patch.productId = args.productId;
541
+ optimisticFields.push("productId");
542
+ }
543
+ if (args.status !== undefined) patch.status = args.status;
544
+ if (args.cancelAtPeriodEnd !== undefined)
545
+ patch.cancelAtPeriodEnd = args.cancelAtPeriodEnd;
546
+
547
+ // Track optimistic fields so updateSubscription can guard against stale webhooks.
548
+ // Merge with any existing optimistic fields (cumulative across consecutive patches).
549
+ const existingMeta = (sub.metadata ?? {}) as Record<string, unknown>;
550
+ if (args.clearOptimistic) {
551
+ const {
552
+ _optimisticPendingAt: _,
553
+ _optimisticFields: __,
554
+ ...cleanMeta
555
+ } = existingMeta;
556
+ patch.metadata = cleanMeta;
557
+ } else if (optimisticFields.length > 0) {
558
+ const existingOptimistic =
559
+ (existingMeta._optimisticFields as string[] | undefined) ?? [];
560
+ const mergedOptimistic = [
561
+ ...new Set([...existingOptimistic, ...optimisticFields]),
562
+ ];
563
+ patch.metadata = {
564
+ ...existingMeta,
565
+ _optimisticPendingAt: Date.now(),
566
+ _optimisticFields: mergedOptimistic,
567
+ };
568
+ }
569
+
570
+ if (Object.keys(patch).length > 0) {
571
+ if (optimisticFields.length > 0 || args.clearOptimistic) {
572
+ console.log(`[creem] optimistic patch sub=${args.subscriptionId}`, {
573
+ fields: optimisticFields,
574
+ ...(args.seats !== undefined ? { seats: args.seats } : {}),
575
+ ...(args.productId !== undefined
576
+ ? { productId: args.productId }
577
+ : {}),
578
+ ...(args.clearOptimistic ? { clear: true } : {}),
579
+ });
580
+ }
581
+ await ctx.db.patch(sub._id, patch);
582
+ }
583
+ },
584
+ });
585
+
586
+ /** Action that calls Creem API and reverts on error. Scheduled by mutations.
587
+ * Public (not internal) so it's accessible via ComponentApi for scheduling from app-level mutations.
588
+ * Secured by requiring apiKey argument (same pattern as syncProducts). */
589
+ export const executeSubscriptionUpdate = action({
590
+ args: {
591
+ apiKey: v.string(),
592
+ serverIdx: v.optional(v.number()),
593
+ serverURL: v.optional(v.string()),
594
+ subscriptionId: v.string(),
595
+ productId: v.optional(v.string()),
596
+ units: v.optional(v.number()),
597
+ updateBehavior: v.optional(v.string()),
598
+ previousSeats: v.optional(v.union(v.number(), v.null())),
599
+ previousProductId: v.optional(v.string()),
600
+ },
601
+ handler: async (ctx, args) => {
602
+ const sdk = new Creem({
603
+ apiKey: args.apiKey,
604
+ ...(args.serverIdx !== undefined ? { serverIdx: args.serverIdx } : {}),
605
+ ...(args.serverURL ? { serverURL: args.serverURL } : {}),
606
+ });
607
+ try {
608
+ if (args.productId) {
609
+ // Plan/interval switch
610
+ await sdk.subscriptions.upgrade(args.subscriptionId, {
611
+ productId: args.productId,
612
+ ...(args.updateBehavior
613
+ ? {
614
+ updateBehavior: args.updateBehavior as
615
+ | "proration-charge-immediately"
616
+ | "proration-charge"
617
+ | "proration-none",
618
+ }
619
+ : {}),
620
+ });
621
+ } else if (args.units !== undefined) {
622
+ // Seat update — need live item IDs from Creem
623
+ const live = await sdk.subscriptions.get(args.subscriptionId);
624
+ const item = live.items?.[0];
625
+ if (!item) throw new ConvexError("Subscription has no items");
626
+ await sdk.subscriptions.update(args.subscriptionId, {
627
+ items: [
628
+ {
629
+ id: item.id,
630
+ productId: item.productId,
631
+ priceId: item.priceId,
632
+ units: args.units,
633
+ },
634
+ ],
635
+ ...(args.updateBehavior
636
+ ? {
637
+ updateBehavior: args.updateBehavior as
638
+ | "proration-charge-immediately"
639
+ | "proration-charge"
640
+ | "proration-none",
641
+ }
642
+ : {}),
643
+ });
644
+ }
645
+ } catch (error) {
646
+ console.error(`[creem] subscription update failed:`, error);
647
+ // Revert optimistic state and clear the optimistic guard so webhooks write normally
648
+ await ctx.runMutation(api.lib.patchSubscription, {
649
+ subscriptionId: args.subscriptionId,
650
+ ...(args.previousSeats !== undefined
651
+ ? { seats: args.previousSeats }
652
+ : {}),
653
+ ...(args.previousProductId
654
+ ? { productId: args.previousProductId }
655
+ : {}),
656
+ clearOptimistic: true,
657
+ });
658
+ }
659
+ },
660
+ });
661
+
662
+ /** Action that calls Creem API for cancel/resume/pause and reverts on error.
663
+ * Scheduled by the corresponding mutations in api(). */
664
+ export const executeSubscriptionLifecycle = action({
665
+ args: {
666
+ apiKey: v.string(),
667
+ serverIdx: v.optional(v.number()),
668
+ serverURL: v.optional(v.string()),
669
+ subscriptionId: v.string(),
670
+ operation: v.union(
671
+ v.literal("cancel"),
672
+ v.literal("resume"),
673
+ v.literal("pause"),
674
+ ),
675
+ cancelMode: v.optional(v.string()),
676
+ // For reverting on error:
677
+ previousStatus: v.optional(v.string()),
678
+ previousCancelAtPeriodEnd: v.optional(v.boolean()),
679
+ },
680
+ handler: async (ctx, args) => {
681
+ const sdk = new Creem({
682
+ apiKey: args.apiKey,
683
+ ...(args.serverIdx !== undefined ? { serverIdx: args.serverIdx } : {}),
684
+ ...(args.serverURL ? { serverURL: args.serverURL } : {}),
685
+ });
686
+ try {
687
+ if (args.operation === "cancel") {
688
+ const cancelParams =
689
+ args.cancelMode === "immediate"
690
+ ? { mode: "immediate" as const }
691
+ : args.cancelMode === "scheduled"
692
+ ? { mode: "scheduled" as const, onExecute: "cancel" as const }
693
+ : {};
694
+ await sdk.subscriptions.cancel(args.subscriptionId, cancelParams);
695
+ } else if (args.operation === "resume") {
696
+ await sdk.subscriptions.resume(args.subscriptionId);
697
+ } else if (args.operation === "pause") {
698
+ await sdk.subscriptions.pause(args.subscriptionId);
699
+ }
700
+ } catch (error) {
701
+ console.error(`[creem] subscription ${args.operation} failed:`, error);
702
+ // Revert optimistic state
703
+ await ctx.runMutation(api.lib.patchSubscription, {
704
+ subscriptionId: args.subscriptionId,
705
+ ...(args.previousStatus !== undefined
706
+ ? { status: args.previousStatus }
707
+ : {}),
708
+ ...(args.previousCancelAtPeriodEnd !== undefined
709
+ ? { cancelAtPeriodEnd: args.previousCancelAtPeriodEnd }
710
+ : {}),
711
+ });
712
+ }
713
+ },
714
+ });
715
+
716
+ export const omitSystemFields = <
717
+ T extends { _id: string; _creationTime: number } | null | undefined,
718
+ >(
719
+ doc: T,
720
+ ) => {
721
+ if (!doc) {
722
+ return doc;
723
+ }
724
+ const { _id, _creationTime, ...rest } = doc;
725
+ return rest;
726
+ };