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