@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
package/README.md
ADDED
|
@@ -0,0 +1,1176 @@
|
|
|
1
|
+
# Convex Creem Component
|
|
2
|
+
|
|
3
|
+
Add subscriptions, one-time purchases, and billing to your Convex app with
|
|
4
|
+
[Creem](https://www.creem.io).
|
|
5
|
+
|
|
6
|
+
**Check out the [Svelte example](example-svelte) and
|
|
7
|
+
[React example](example-react) for complete integrations.**
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Quick Start — Backend](#quick-start--backend)
|
|
12
|
+
- [1. Install](#1-install)
|
|
13
|
+
- [2. Register component](#2-register-component)
|
|
14
|
+
- [3. Set environment variables](#3-set-environment-variables)
|
|
15
|
+
- [4. Configure billing](#4-configure-billing)
|
|
16
|
+
- [5. Register webhooks](#5-register-webhooks)
|
|
17
|
+
- [6. Sync products](#6-sync-products)
|
|
18
|
+
- [Quick Start — Frontend (UI Widgets)](#quick-start--frontend-ui-widgets)
|
|
19
|
+
- [7. Install Tailwind CSS](#7-Install-UI-primitives)
|
|
20
|
+
- [7. Install Tailwind CSS](#8-install-tailwind-css)
|
|
21
|
+
- [8. Import styles](#9-import-styles)
|
|
22
|
+
- [Entity Model](#entity-model)
|
|
23
|
+
- [Scenarios](#scenarios)
|
|
24
|
+
- [Wire the billing API](#wire-the-billing-api)
|
|
25
|
+
- [1. Subscriptions](#1-subscriptions)
|
|
26
|
+
- [2. Products](#2-products)
|
|
27
|
+
- [3. Billing Portal](#3-billing-portal)
|
|
28
|
+
- [4. Feature Gating](#4-feature-gating)
|
|
29
|
+
- [5. Checkout Success](#5-checkout-success)
|
|
30
|
+
- [Advanced](#advanced)
|
|
31
|
+
- [Webhook event middleware](#webhook-event-middleware)
|
|
32
|
+
- [Security & Access Control](#security--access-control)
|
|
33
|
+
- [Custom billing UI model](#custom-billing-ui-model)
|
|
34
|
+
- [Server endpoint overrides](#server-endpoint-overrides)
|
|
35
|
+
- [API Reference](#api-reference)
|
|
36
|
+
- [Resource namespaces](#resource-namespaces--creemnamespace)
|
|
37
|
+
- [`creem.api({ resolve })` — convenience exports](#creemapi-resolve---convenience-exports)
|
|
38
|
+
- [Infrastructure](#infrastructure)
|
|
39
|
+
- [Direct API access — `creem.sdk.*`](#direct-api-access--creemsdk)
|
|
40
|
+
- [Component Reference](#component-reference)
|
|
41
|
+
- [Widgets](#widgets)
|
|
42
|
+
- [Presentational components](#presentational-components)
|
|
43
|
+
- [Troubleshooting](#troubleshooting)
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Quick Start — Backend
|
|
48
|
+
|
|
49
|
+
Complete these steps to use the billing API from your Convex functions. No
|
|
50
|
+
frontend framework required.
|
|
51
|
+
|
|
52
|
+
### 1. Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install @mmailaender/convex-creem
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2. Register component
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// convex/convex.config.ts
|
|
62
|
+
import { defineApp } from "convex/server";
|
|
63
|
+
import creem from "@mmailaender/convex-creem/convex.config";
|
|
64
|
+
|
|
65
|
+
const app = defineApp();
|
|
66
|
+
app.use(creem);
|
|
67
|
+
|
|
68
|
+
export default app;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 3. Set environment variables
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx convex env set CREEM_API_KEY <your_creem_api_key>
|
|
75
|
+
npx convex env set CREEM_WEBHOOK_SECRET <your_creem_webhook_signing_secret>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 4. Configure billing
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
// convex/billing.ts
|
|
82
|
+
import { Creem, type ApiResolver } from "@mmailaender/convex-creem";
|
|
83
|
+
import { api, components } from "./_generated/api";
|
|
84
|
+
import { query, internalAction } from "./_generated/server";
|
|
85
|
+
|
|
86
|
+
export const creem = new Creem(components.creem);
|
|
87
|
+
|
|
88
|
+
// Auth resolver — replace with your own auth logic
|
|
89
|
+
const resolve: ApiResolver = async (ctx) => {
|
|
90
|
+
const user = await ctx.runQuery(api.users.currentUser);
|
|
91
|
+
return {
|
|
92
|
+
userId: user._id,
|
|
93
|
+
email: user.email,
|
|
94
|
+
entityId: user._id, // For org billing: user.activeOrgId ?? user._id
|
|
95
|
+
};
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Generate Convex function exports — each calls resolve(), then delegates
|
|
99
|
+
const {
|
|
100
|
+
uiModel,
|
|
101
|
+
snapshot,
|
|
102
|
+
checkouts,
|
|
103
|
+
subscriptions,
|
|
104
|
+
products,
|
|
105
|
+
customers,
|
|
106
|
+
orders,
|
|
107
|
+
} = creem.api({ resolve });
|
|
108
|
+
|
|
109
|
+
export { uiModel, snapshot };
|
|
110
|
+
export const checkoutsCreate = checkouts.create;
|
|
111
|
+
export const subscriptionsUpdate = subscriptions.update;
|
|
112
|
+
export const subscriptionsCancel = subscriptions.cancel;
|
|
113
|
+
export const subscriptionsResume = subscriptions.resume;
|
|
114
|
+
export const subscriptionsPause = subscriptions.pause;
|
|
115
|
+
export const subscriptionsList = subscriptions.list;
|
|
116
|
+
export const subscriptionsListAll = subscriptions.listAll;
|
|
117
|
+
export const productsList = products.list;
|
|
118
|
+
export const productsGet = products.get;
|
|
119
|
+
export const customersRetrieve = customers.retrieve;
|
|
120
|
+
export const customersPortalUrl = customers.portalUrl;
|
|
121
|
+
export const ordersList = orders.list;
|
|
122
|
+
|
|
123
|
+
// Sync products from Creem (CLI / dashboard only)
|
|
124
|
+
export const syncBillingProducts = internalAction({
|
|
125
|
+
args: {},
|
|
126
|
+
handler: async (ctx) => {
|
|
127
|
+
await creem.syncProducts(ctx);
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 5. Register webhooks
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
// convex/http.ts
|
|
136
|
+
import { httpRouter } from "convex/server";
|
|
137
|
+
import { creem } from "./billing";
|
|
138
|
+
|
|
139
|
+
const http = httpRouter();
|
|
140
|
+
|
|
141
|
+
creem.registerRoutes(http);
|
|
142
|
+
|
|
143
|
+
export default http;
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Use your **Convex site URL** + `/creem/events` as the webhook endpoint in your
|
|
147
|
+
Creem dashboard. The component automatically handles `checkout.completed`,
|
|
148
|
+
`subscription.*`, and `product.*` events.
|
|
149
|
+
|
|
150
|
+
> For custom event handlers (e.g. sending emails on checkout), see
|
|
151
|
+
> [Webhook event middleware](#webhook-event-middleware).
|
|
152
|
+
|
|
153
|
+
### 6. Sync products
|
|
154
|
+
|
|
155
|
+
After configuring webhooks, pull your Creem products into the Convex database:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
npx convex run billing:syncBillingProducts
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
> This is an `internalAction` — it can only be triggered from the CLI or the
|
|
162
|
+
> Convex dashboard.
|
|
163
|
+
|
|
164
|
+
**You're done with the backend.** You can now call `api.billing.*` from your
|
|
165
|
+
frontend or other Convex functions. If you only need the API (no UI widgets),
|
|
166
|
+
skip ahead to the [API Reference](#api-reference).
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Quick Start — Frontend (UI Widgets)
|
|
171
|
+
|
|
172
|
+
The component ships pre-built Svelte and React widgets that handle checkout,
|
|
173
|
+
plan switching, cancellation, seat management, and billing state — all connected
|
|
174
|
+
to Convex. Complete these three extra steps to use them.
|
|
175
|
+
|
|
176
|
+
### 7. Install UI primitives
|
|
177
|
+
|
|
178
|
+
The widgets are built on [Ark UI](https://ark-ui.com) headless primitives.
|
|
179
|
+
Install the adapter for your framework:
|
|
180
|
+
|
|
181
|
+
**React**
|
|
182
|
+
|
|
183
|
+
```sh
|
|
184
|
+
npm install @ark-ui/react
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
**Svelte**
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
npm install @ark-ui/svelte
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 8. Install Tailwind CSS
|
|
194
|
+
|
|
195
|
+
The widgets use [Tailwind CSS v4](https://tailwindcss.com/docs/installation). If
|
|
196
|
+
your project doesn't have Tailwind yet, install it following the
|
|
197
|
+
[official guide](https://tailwindcss.com/docs/installation).
|
|
198
|
+
|
|
199
|
+
### 9. Import styles
|
|
200
|
+
|
|
201
|
+
Add the component's design system import to your CSS entry point, **after** the
|
|
202
|
+
Tailwind import:
|
|
203
|
+
|
|
204
|
+
```css
|
|
205
|
+
@import "tailwindcss";
|
|
206
|
+
@import "@mmailaender/convex-creem/styles";
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
This registers the component's design tokens, base styles, and `@source`
|
|
210
|
+
directives so Tailwind scans the library's component files automatically.
|
|
211
|
+
|
|
212
|
+
**You're ready to use the UI widgets.** Continue with the scenarios below.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Entity Model
|
|
217
|
+
|
|
218
|
+
By default, billing is scoped to the **authenticated user** — the `entityId`
|
|
219
|
+
returned from your resolver is used as the billing entity. All
|
|
220
|
+
`creem.api({ resolve })` functions, checkout metadata, and webhook resolution
|
|
221
|
+
automatically use this entity.
|
|
222
|
+
|
|
223
|
+
For **organization or team billing**, return the org ID as `entityId`:
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const resolve: ApiResolver = async (ctx) => {
|
|
227
|
+
const user = await ctx.runQuery(api.users.currentUser);
|
|
228
|
+
const org = await ctx.runQuery(api.orgs.getActiveOrg);
|
|
229
|
+
return {
|
|
230
|
+
userId: user._id,
|
|
231
|
+
email: user.email,
|
|
232
|
+
entityId: org?._id ?? user._id, // org billing or personal billing
|
|
233
|
+
};
|
|
234
|
+
};
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
All billing operations scope to the `entityId`. Webhooks resolve
|
|
238
|
+
`convexBillingEntityId` from checkout metadata (falls back to `convexUserId`).
|
|
239
|
+
No other code changes needed.
|
|
240
|
+
|
|
241
|
+
For access control details, see
|
|
242
|
+
[Security & Access Control](#security--access-control).
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Scenarios
|
|
247
|
+
|
|
248
|
+
Both Svelte and React widgets share **identical props and APIs** — only the
|
|
249
|
+
import path and framework boilerplate differ.
|
|
250
|
+
|
|
251
|
+
> **Convention:** Where the markup is identical in both frameworks, examples are
|
|
252
|
+
> shown once. The only recurring difference is `class=` (Svelte) vs `className=`
|
|
253
|
+
> (React) when passing CSS classes. Where Svelte and React syntax diverges (e.g.
|
|
254
|
+
> children rendering), both versions are shown.
|
|
255
|
+
|
|
256
|
+
### Wire the billing API
|
|
257
|
+
|
|
258
|
+
Every widget needs a `ConnectedBillingApi` object. Create it once in your layout
|
|
259
|
+
or page component:
|
|
260
|
+
|
|
261
|
+
**Svelte**
|
|
262
|
+
|
|
263
|
+
```svelte
|
|
264
|
+
<script lang="ts">
|
|
265
|
+
import { setupConvex } from "convex-svelte";
|
|
266
|
+
import {
|
|
267
|
+
Subscription, Product, BillingPortal,
|
|
268
|
+
type ConnectedBillingApi,
|
|
269
|
+
} from "@mmailaender/convex-creem/svelte";
|
|
270
|
+
import { api } from "../convex/_generated/api.js";
|
|
271
|
+
|
|
272
|
+
setupConvex(import.meta.env.VITE_CONVEX_URL);
|
|
273
|
+
|
|
274
|
+
const billingApi: ConnectedBillingApi = {
|
|
275
|
+
uiModel: api.billing.uiModel,
|
|
276
|
+
checkouts: { create: api.billing.checkoutsCreate },
|
|
277
|
+
subscriptions: {
|
|
278
|
+
update: api.billing.subscriptionsUpdate,
|
|
279
|
+
cancel: api.billing.subscriptionsCancel,
|
|
280
|
+
resume: api.billing.subscriptionsResume,
|
|
281
|
+
},
|
|
282
|
+
customers: { portalUrl: api.billing.customersPortalUrl },
|
|
283
|
+
};
|
|
284
|
+
</script>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**React**
|
|
288
|
+
|
|
289
|
+
```tsx
|
|
290
|
+
import {
|
|
291
|
+
Subscription,
|
|
292
|
+
Product,
|
|
293
|
+
BillingPortal,
|
|
294
|
+
type ConnectedBillingApi,
|
|
295
|
+
} from "@mmailaender/convex-creem/react";
|
|
296
|
+
import { api } from "../convex/_generated/api";
|
|
297
|
+
|
|
298
|
+
const billingApi: ConnectedBillingApi = {
|
|
299
|
+
uiModel: api.billing.uiModel,
|
|
300
|
+
checkouts: { create: api.billing.checkoutsCreate },
|
|
301
|
+
subscriptions: {
|
|
302
|
+
update: api.billing.subscriptionsUpdate,
|
|
303
|
+
cancel: api.billing.subscriptionsCancel,
|
|
304
|
+
resume: api.billing.subscriptionsResume,
|
|
305
|
+
},
|
|
306
|
+
customers: { portalUrl: api.billing.customersPortalUrl },
|
|
307
|
+
};
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
> The `ConnectedBillingApi` object is the same shape in both frameworks. Only
|
|
311
|
+
> the Convex client setup differs: `setupConvex()` in Svelte vs
|
|
312
|
+
> `<ConvexProvider>` in React (see
|
|
313
|
+
> [convex-svelte](https://github.com/get-convex/convex-svelte) and
|
|
314
|
+
> [convex/react](https://docs.convex.dev/client/react) docs).
|
|
315
|
+
|
|
316
|
+
### 1. Subscriptions
|
|
317
|
+
|
|
318
|
+
#### 1.1 Standard subscription plans
|
|
319
|
+
|
|
320
|
+
A typical pricing page with Free / Basic / Premium / Enterprise tiers. The
|
|
321
|
+
billing toggle auto-derives from the cycles present in registered plans.
|
|
322
|
+
|
|
323
|
+
```svelte
|
|
324
|
+
<Subscription.Root api={billingApi}>
|
|
325
|
+
<Subscription.Item type="free" title="Free" description="Up to 3 users" />
|
|
326
|
+
<Subscription.Item
|
|
327
|
+
planId="basic"
|
|
328
|
+
type="single"
|
|
329
|
+
productIds={{
|
|
330
|
+
"every-month": "prod_basic_monthly",
|
|
331
|
+
"every-year": "prod_basic_yearly",
|
|
332
|
+
}}
|
|
333
|
+
/>
|
|
334
|
+
<Subscription.Item
|
|
335
|
+
planId="premium"
|
|
336
|
+
type="single"
|
|
337
|
+
recommended
|
|
338
|
+
productIds={{
|
|
339
|
+
"every-month": "prod_premium_monthly",
|
|
340
|
+
"every-year": "prod_premium_yearly",
|
|
341
|
+
}}
|
|
342
|
+
/>
|
|
343
|
+
<Subscription.Item
|
|
344
|
+
type="enterprise"
|
|
345
|
+
title="Enterprise"
|
|
346
|
+
contactUrl="https://example.com/sales"
|
|
347
|
+
/>
|
|
348
|
+
</Subscription.Root>
|
|
349
|
+
<BillingPortal api={billingApi} />
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
**What you get:**
|
|
353
|
+
|
|
354
|
+
- Pricing cards with auto-resolved titles, descriptions (rendered as Markdown),
|
|
355
|
+
and prices from Creem product data
|
|
356
|
+
- Billing cycle toggle (monthly/yearly) — hidden when all plans share a single
|
|
357
|
+
cycle
|
|
358
|
+
- "Current plan" badge on the active subscription
|
|
359
|
+
- Plan switching with confirmation dialog
|
|
360
|
+
- Trial countdown badge
|
|
361
|
+
- Cancel / resume subscription (with confirmation dialog)
|
|
362
|
+
- Scheduled cancellation banner with "Undo" button
|
|
363
|
+
|
|
364
|
+
#### 1.2 Seat-based subscriptions
|
|
365
|
+
|
|
366
|
+
Two workflows for seat-based pricing:
|
|
367
|
+
|
|
368
|
+
**User-selectable seats** — the customer picks a quantity before checkout:
|
|
369
|
+
|
|
370
|
+
```svelte
|
|
371
|
+
<Subscription.Root api={billingApi} showSeatPicker>
|
|
372
|
+
<Subscription.Item
|
|
373
|
+
type="seat-based"
|
|
374
|
+
productIds={{ "every-month": "prod_team_monthly" }}
|
|
375
|
+
/>
|
|
376
|
+
<Subscription.Item
|
|
377
|
+
type="seat-based"
|
|
378
|
+
productIds={{ "every-month": "prod_business_monthly" }}
|
|
379
|
+
/>
|
|
380
|
+
</Subscription.Root>
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
**Auto-derived seats** — pass a fixed count (e.g. org member count) to checkout:
|
|
384
|
+
|
|
385
|
+
```svelte
|
|
386
|
+
<Subscription.Root api={billingApi} units={orgMemberCount}>
|
|
387
|
+
<Subscription.Item
|
|
388
|
+
type="seat-based"
|
|
389
|
+
productIds={{ "every-month": "prod_team_monthly" }}
|
|
390
|
+
/>
|
|
391
|
+
</Subscription.Root>
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
When `subscriptions.update` is provided in the API, active seat-based plans show
|
|
395
|
+
a "Change seats" control.
|
|
396
|
+
|
|
397
|
+
> **Tip:** For auto-derived seats, keep the subscription in sync with your data.
|
|
398
|
+
> When your member count changes, call `subscriptions.update` with the new
|
|
399
|
+
> `units` so the billing reflects the current seat count.
|
|
400
|
+
|
|
401
|
+
### 2. Products
|
|
402
|
+
|
|
403
|
+
#### 2.1 Single one-time product
|
|
404
|
+
|
|
405
|
+
A standalone product purchased once. Shows "Owned" after purchase:
|
|
406
|
+
|
|
407
|
+
```svelte
|
|
408
|
+
<Product.Root api={billingApi}>
|
|
409
|
+
<Product.Item type="one-time" productId="prod_license" />
|
|
410
|
+
</Product.Root>
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
#### 2.2 Repeating product (consumable)
|
|
414
|
+
|
|
415
|
+
Can be purchased multiple times — no "Owned" badge:
|
|
416
|
+
|
|
417
|
+
```svelte
|
|
418
|
+
<Product.Root api={billingApi}>
|
|
419
|
+
<Product.Item type="recurring" productId="prod_credits" title="100 Credits" />
|
|
420
|
+
</Product.Root>
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
#### 2.3 Mutually exclusive product group
|
|
424
|
+
|
|
425
|
+
Use the `transition` prop to define upgrade paths between products. When the
|
|
426
|
+
user owns a lower-tier product, only valid upgrade paths are shown:
|
|
427
|
+
|
|
428
|
+
```svelte
|
|
429
|
+
<Product.Root
|
|
430
|
+
api={billingApi}
|
|
431
|
+
transition={[
|
|
432
|
+
{
|
|
433
|
+
from: "prod_basic_license",
|
|
434
|
+
to: "prod_premium_license",
|
|
435
|
+
kind: "via_product",
|
|
436
|
+
viaProductId: "prod_basic_to_premium_upgrade",
|
|
437
|
+
},
|
|
438
|
+
]}
|
|
439
|
+
>
|
|
440
|
+
<Product.Item type="one-time" productId="prod_basic_license" />
|
|
441
|
+
<Product.Item type="one-time" productId="prod_premium_license" />
|
|
442
|
+
</Product.Root>
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Transition kinds:**
|
|
446
|
+
|
|
447
|
+
- **`via_product`** — checkout uses a dedicated upgrade product (delta pricing)
|
|
448
|
+
- **`direct`** — checkout uses the target product directly
|
|
449
|
+
|
|
450
|
+
### 3. Billing Portal
|
|
451
|
+
|
|
452
|
+
`<BillingPortal>` opens the Creem customer billing portal. It auto-hides when
|
|
453
|
+
the billing entity has no Creem customer record.
|
|
454
|
+
|
|
455
|
+
Pass `permissions` to control who can access the portal (e.g. only admins):
|
|
456
|
+
|
|
457
|
+
```svelte
|
|
458
|
+
<BillingPortal api={billingApi} permissions={{ canAccessPortal: isAdmin }} />
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
```svelte
|
|
462
|
+
<!-- After a subscription group -->
|
|
463
|
+
<BillingPortal api={billingApi} />
|
|
464
|
+
|
|
465
|
+
<!-- Standalone with custom label -->
|
|
466
|
+
<BillingPortal api={billingApi}>Manage billing & invoices</BillingPortal>
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### 4. Feature Gating
|
|
470
|
+
|
|
471
|
+
Use `BillingGate` to conditionally render UI based on available billing actions:
|
|
472
|
+
|
|
473
|
+
**Svelte**
|
|
474
|
+
|
|
475
|
+
```svelte
|
|
476
|
+
<BillingGate snapshot={billingSnapshot} requiredActions="portal">
|
|
477
|
+
{#snippet children()}
|
|
478
|
+
<p>You have portal access.</p>
|
|
479
|
+
{/snippet}
|
|
480
|
+
{#snippet fallback()}
|
|
481
|
+
<p>Upgrade to access the billing portal.</p>
|
|
482
|
+
{/snippet}
|
|
483
|
+
</BillingGate>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
**React**
|
|
487
|
+
|
|
488
|
+
```tsx
|
|
489
|
+
<BillingGate
|
|
490
|
+
snapshot={billingSnapshot}
|
|
491
|
+
requiredActions="portal"
|
|
492
|
+
fallback={<p>Upgrade to access the billing portal.</p>}
|
|
493
|
+
>
|
|
494
|
+
<p>You have portal access.</p>
|
|
495
|
+
</BillingGate>
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
Available actions: `checkout`, `portal`, `cancel`, `reactivate`,
|
|
499
|
+
`switch_interval`, `update_seats`, `contact_sales`.
|
|
500
|
+
|
|
501
|
+
### 5. Checkout Success
|
|
502
|
+
|
|
503
|
+
Show a confirmation banner when the user returns from checkout. The component
|
|
504
|
+
parses Creem's query parameters automatically:
|
|
505
|
+
|
|
506
|
+
```svelte
|
|
507
|
+
<CheckoutSuccessSummary />
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
---
|
|
511
|
+
|
|
512
|
+
## Advanced
|
|
513
|
+
|
|
514
|
+
### Webhook event middleware
|
|
515
|
+
|
|
516
|
+
`registerRoutes` accepts an optional `events` object to run app-specific logic
|
|
517
|
+
alongside the component's automatic handling:
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
creem.registerRoutes(http, {
|
|
521
|
+
path: "/creem/events", // default
|
|
522
|
+
events: {
|
|
523
|
+
"checkout.completed": async (ctx, event) => {
|
|
524
|
+
// ctx is a Convex mutation context
|
|
525
|
+
// event has { type, data } from Creem
|
|
526
|
+
// Example: send confirmation email, grant entitlements, log analytics
|
|
527
|
+
},
|
|
528
|
+
"subscription.updated": async (ctx, event) => {
|
|
529
|
+
const data = event.data as { customerCancellationReason?: string };
|
|
530
|
+
if (data?.customerCancellationReason) {
|
|
531
|
+
console.log("Cancellation reason:", data.customerCancellationReason);
|
|
532
|
+
}
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
});
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
Your handlers run **after** the component's built-in processing
|
|
539
|
+
(customer/subscription/order upserts). The `ctx` is a Convex mutation context —
|
|
540
|
+
you can read/write to your own tables.
|
|
541
|
+
|
|
542
|
+
**Supported events:** `checkout.completed`, `subscription.active`,
|
|
543
|
+
`subscription.updated`, `subscription.canceled`, `subscription.paused`,
|
|
544
|
+
`subscription.resumed`, `product.created`, `product.updated`.
|
|
545
|
+
|
|
546
|
+
### Security & Access Control
|
|
547
|
+
|
|
548
|
+
**Auth is the app's responsibility.** The component is a sync engine — it reads
|
|
549
|
+
from Convex DB and writes to the Creem API. Every class method takes explicit
|
|
550
|
+
args; there is no hidden auth layer.
|
|
551
|
+
|
|
552
|
+
**Choose your approach:**
|
|
553
|
+
|
|
554
|
+
- **Quick start** — use [`creem.api({ resolve })`](#4-configure-billing) to
|
|
555
|
+
generate ready-to-export Convex functions. Each one calls your `resolve`
|
|
556
|
+
callback to authenticate and determine the `entityId`. The billing entity is
|
|
557
|
+
derived from the authenticated session, never from client input. See
|
|
558
|
+
[Step 4: Configure billing](#4-configure-billing) and the
|
|
559
|
+
[`creem.api({ resolve })` reference](#creemapi-resolve---convenience-exports).
|
|
560
|
+
|
|
561
|
+
- **Full control** — use the
|
|
562
|
+
[resource namespaces](#resource-namespaces--creemnamespace)
|
|
563
|
+
(`creem.subscriptions.*`, `creem.checkouts.*`, etc.) directly in your own
|
|
564
|
+
Convex functions. You handle auth, entity resolution, and permission checks
|
|
565
|
+
yourself.
|
|
566
|
+
|
|
567
|
+
The library exports **shared arg validators** that match exactly what the
|
|
568
|
+
connected widgets send. Use them to keep your custom functions in sync:
|
|
569
|
+
|
|
570
|
+
| Export | Used by |
|
|
571
|
+
| ------------------------ | ------------------------------------------------ |
|
|
572
|
+
| `checkoutCreateArgs` | `<Subscription.Root>`, `<Product.Root>` |
|
|
573
|
+
| `subscriptionUpdateArgs` | `<Subscription.Root>` (plan switch, seat update) |
|
|
574
|
+
| `subscriptionCancelArgs` | `<Subscription.Root>` (cancel button) |
|
|
575
|
+
| `subscriptionResumeArgs` | `<Subscription.Root>` (resume button) |
|
|
576
|
+
| `subscriptionPauseArgs` | `<Subscription.Root>` (pause button) |
|
|
577
|
+
|
|
578
|
+
Example:
|
|
579
|
+
|
|
580
|
+
```ts
|
|
581
|
+
// convex/billing.ts
|
|
582
|
+
import {
|
|
583
|
+
Creem,
|
|
584
|
+
checkoutCreateArgs,
|
|
585
|
+
subscriptionCancelArgs,
|
|
586
|
+
} from "@mmailaender/convex-creem";
|
|
587
|
+
import { ConvexError } from "convex/values";
|
|
588
|
+
import { action, mutation } from "./_generated/server";
|
|
589
|
+
import { api, components } from "./_generated/api";
|
|
590
|
+
|
|
591
|
+
const creem = new Creem(components.creem);
|
|
592
|
+
|
|
593
|
+
async function resolveAuth(ctx) {
|
|
594
|
+
const session = await ctx.runQuery(api.auth.getSession);
|
|
595
|
+
if (!session) throw new ConvexError("Not authenticated");
|
|
596
|
+
const org = await ctx.runQuery(api.orgs.getActiveOrg);
|
|
597
|
+
return {
|
|
598
|
+
userId: session.userId,
|
|
599
|
+
email: session.user.email,
|
|
600
|
+
entityId: org?._id ?? session.userId,
|
|
601
|
+
role: session.user.role,
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Admin-only: create checkout
|
|
606
|
+
export const checkoutsCreate = action({
|
|
607
|
+
args: checkoutCreateArgs,
|
|
608
|
+
handler: async (ctx, args) => {
|
|
609
|
+
const auth = await resolveAuth(ctx);
|
|
610
|
+
if (auth.role !== "admin") throw new ConvexError("Forbidden");
|
|
611
|
+
return await creem.checkouts.create(ctx, {
|
|
612
|
+
entityId: auth.entityId,
|
|
613
|
+
userId: auth.userId,
|
|
614
|
+
email: auth.email,
|
|
615
|
+
...args,
|
|
616
|
+
});
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
// Admin-only: cancel subscription
|
|
621
|
+
export const subscriptionsCancel = mutation({
|
|
622
|
+
args: subscriptionCancelArgs,
|
|
623
|
+
handler: async (ctx, args) => {
|
|
624
|
+
const auth = await resolveAuth(ctx);
|
|
625
|
+
if (auth.role !== "admin") throw new ConvexError("Forbidden");
|
|
626
|
+
await creem.subscriptions.cancel(ctx, { entityId: auth.entityId, ...args });
|
|
627
|
+
},
|
|
628
|
+
});
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
**UI-side permissions** — `BillingPermissions` controls which buttons are
|
|
632
|
+
enabled in the widgets. This is cosmetic gating only; enforce real permissions
|
|
633
|
+
server-side.
|
|
634
|
+
|
|
635
|
+
```ts
|
|
636
|
+
type BillingPermissions = {
|
|
637
|
+
canCheckout?: boolean;
|
|
638
|
+
canChangeSubscription?: boolean;
|
|
639
|
+
canCancelSubscription?: boolean;
|
|
640
|
+
canResumeSubscription?: boolean;
|
|
641
|
+
canUpdateSeats?: boolean;
|
|
642
|
+
canAccessPortal?: boolean;
|
|
643
|
+
};
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
```svelte
|
|
647
|
+
<script lang="ts">
|
|
648
|
+
const isAdmin = $derived(currentUser?.role === "admin" || currentUser?.role === "owner");
|
|
649
|
+
const permissions = $derived({
|
|
650
|
+
canCheckout: isAdmin,
|
|
651
|
+
canChangeSubscription: isAdmin,
|
|
652
|
+
canCancelSubscription: isAdmin,
|
|
653
|
+
canResumeSubscription: isAdmin,
|
|
654
|
+
canUpdateSeats: isAdmin,
|
|
655
|
+
});
|
|
656
|
+
</script>
|
|
657
|
+
|
|
658
|
+
<Subscription.Root api={billingApi} {permissions}>
|
|
659
|
+
...
|
|
660
|
+
</Subscription.Root>
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
When a permission is `false`, the button renders as disabled (greyed out). When
|
|
664
|
+
omitted or `undefined`, all actions default to enabled.
|
|
665
|
+
|
|
666
|
+
### Pre-checkout gate — `onBeforeCheckout`
|
|
667
|
+
|
|
668
|
+
Both `<Subscription.Root>` and `<Product.Root>` accept an `onBeforeCheckout`
|
|
669
|
+
callback that fires **before** the widget calls `checkouts.create`. Return
|
|
670
|
+
`true` to proceed, `false` to abort silently.
|
|
671
|
+
|
|
672
|
+
This is a generic hook — use it for authentication gates, terms acceptance,
|
|
673
|
+
confirmation dialogs, analytics, or any logic that must run before checkout.
|
|
674
|
+
|
|
675
|
+
```svelte
|
|
676
|
+
<Subscription.Root
|
|
677
|
+
api={billingApi}
|
|
678
|
+
onBeforeCheckout={(intent) => {
|
|
679
|
+
if (!currentUser) {
|
|
680
|
+
pendingCheckout.save(intent);
|
|
681
|
+
openSignInDialog();
|
|
682
|
+
return false;
|
|
683
|
+
}
|
|
684
|
+
return true;
|
|
685
|
+
}}
|
|
686
|
+
>
|
|
687
|
+
...
|
|
688
|
+
</Subscription.Root>
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
**`CheckoutIntent`** — the object passed to the callback:
|
|
692
|
+
|
|
693
|
+
```ts
|
|
694
|
+
type CheckoutIntent = {
|
|
695
|
+
productId: string;
|
|
696
|
+
units?: number;
|
|
697
|
+
};
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
#### Auto-resume after sign-in
|
|
701
|
+
|
|
702
|
+
The widget automatically resumes a pending checkout when the user becomes
|
|
703
|
+
authenticated. The full flow:
|
|
704
|
+
|
|
705
|
+
1. Unauthenticated user clicks "Subscribe" → `onBeforeCheckout` fires
|
|
706
|
+
2. Your callback saves the intent via `pendingCheckout.save(intent)`, opens your
|
|
707
|
+
sign-in dialog/redirect, and returns `false`
|
|
708
|
+
3. After sign-in, the Convex query re-fires → `model.user` becomes non-null
|
|
709
|
+
4. The widget detects the pending checkout and auto-triggers `checkouts.create`
|
|
710
|
+
|
|
711
|
+
This works for both **modal auth** (Clerk, Auth0 popup) and **redirect auth**
|
|
712
|
+
(OAuth) — no manual resume code needed.
|
|
713
|
+
|
|
714
|
+
**Safety:** The widget skips auto-resume if the user already has an active
|
|
715
|
+
subscription (`<Subscription.Root>`) or already owns the product
|
|
716
|
+
(`<Product.Root>`), preventing duplicate purchases after sign-in.
|
|
717
|
+
|
|
718
|
+
`pendingCheckout` is a tiny sessionStorage-based helper exported from the
|
|
719
|
+
library:
|
|
720
|
+
|
|
721
|
+
```ts
|
|
722
|
+
import { pendingCheckout } from "@mmailaender/convex-creem/svelte";
|
|
723
|
+
|
|
724
|
+
pendingCheckout.save(intent); // store before sign-in
|
|
725
|
+
pendingCheckout.load(); // read + auto-clear (used internally by widgets)
|
|
726
|
+
pendingCheckout.clear(); // manual clear if needed
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
### Custom billing UI model
|
|
730
|
+
|
|
731
|
+
`uiModel` (from `creem.api({ resolve })`) returns everything the connected
|
|
732
|
+
widgets need. If you need app-specific fields, write your own query using
|
|
733
|
+
`creem.getBillingModel()`:
|
|
734
|
+
|
|
735
|
+
```ts
|
|
736
|
+
import { query } from "./_generated/server";
|
|
737
|
+
|
|
738
|
+
export const getCustomBillingModel = query({
|
|
739
|
+
args: {},
|
|
740
|
+
handler: async (ctx) => {
|
|
741
|
+
const user = await currentUser(ctx);
|
|
742
|
+
const billingData = await creem.getBillingModel(ctx, {
|
|
743
|
+
entityId: user?._id ?? null,
|
|
744
|
+
user: user ? { _id: user._id, email: user.email } : null,
|
|
745
|
+
});
|
|
746
|
+
return {
|
|
747
|
+
...billingData,
|
|
748
|
+
teamSize: user?.teamSize,
|
|
749
|
+
featureFlags: user?.featureFlags,
|
|
750
|
+
};
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### Server endpoint overrides
|
|
756
|
+
|
|
757
|
+
Only needed for non-default API endpoints (e.g. test/staging):
|
|
758
|
+
|
|
759
|
+
```bash
|
|
760
|
+
npx convex env set CREEM_SERVER_IDX <index>
|
|
761
|
+
# or
|
|
762
|
+
npx convex env set CREEM_SERVER_URL <url>
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
Leave both unset to use the default Creem production endpoint.
|
|
766
|
+
|
|
767
|
+
---
|
|
768
|
+
|
|
769
|
+
## API Reference
|
|
770
|
+
|
|
771
|
+
### Resource namespaces — `creem.<namespace>.*`
|
|
772
|
+
|
|
773
|
+
All methods take explicit arguments. Use them directly in your own Convex
|
|
774
|
+
functions, or let `creem.api({ resolve })` generate ready-to-export wrappers.
|
|
775
|
+
|
|
776
|
+
**`creem.subscriptions.*`**
|
|
777
|
+
|
|
778
|
+
| Method | Data source | Description |
|
|
779
|
+
| ---------------------------------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
780
|
+
| `.getCurrent(ctx, { entityId })` | Convex DB | Current active subscription with product join |
|
|
781
|
+
| `.list(ctx, { entityId })` | Convex DB | Active subscriptions (excludes ended + expired trials) |
|
|
782
|
+
| `.listAll(ctx, { entityId })` | Convex DB | All subscriptions including ended |
|
|
783
|
+
| `.update(ctx, { entityId, subscriptionId?, productId?, units?, updateBehavior? })` | Creem API | Unified plan switch (`productId`) or seat update (`units`). Pass `subscriptionId` when the entity has multiple active subscriptions. Optional proration override. |
|
|
784
|
+
| `.cancel(ctx, { entityId, revokeImmediately? })` | Creem API | Cancel subscription |
|
|
785
|
+
| `.pause(ctx, { entityId })` | Creem API | Pause an active subscription |
|
|
786
|
+
| `.resume(ctx, { entityId })` | Creem API | Resume a paused or scheduled-cancel subscription |
|
|
787
|
+
|
|
788
|
+
**`creem.checkouts.*`**
|
|
789
|
+
|
|
790
|
+
| Method | Data source | Description |
|
|
791
|
+
| ------------------------------------------------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------- |
|
|
792
|
+
| `.create(ctx, { entityId, userId, email, productId, successUrl?, fallbackSuccessUrl?, units?, metadata?, theme? })` | Creem API | Create checkout URL with 3-tier `successUrl` resolution and optional `theme` |
|
|
793
|
+
|
|
794
|
+
**`creem.products.*`**
|
|
795
|
+
|
|
796
|
+
| Method | Data source | Description |
|
|
797
|
+
| -------------------------- | ----------- | --------------------------------------------------- |
|
|
798
|
+
| `.list(ctx, options?)` | Convex DB | All synced products (public — no `entityId` needed) |
|
|
799
|
+
| `.get(ctx, { productId })` | Convex DB | Single product by ID (public) |
|
|
800
|
+
|
|
801
|
+
**`creem.customers.*`**
|
|
802
|
+
|
|
803
|
+
| Method | Data source | Description |
|
|
804
|
+
| ------------------------------- | ----------- | --------------------------- |
|
|
805
|
+
| `.retrieve(ctx, { entityId })` | Convex DB | Customer record by entity |
|
|
806
|
+
| `.portalUrl(ctx, { entityId })` | Creem API | Customer billing portal URL |
|
|
807
|
+
|
|
808
|
+
**`creem.orders.*`**
|
|
809
|
+
|
|
810
|
+
| Method | Data source | Description |
|
|
811
|
+
| -------------------------- | ----------- | -------------------- |
|
|
812
|
+
| `.list(ctx, { entityId })` | Convex DB | Paid one-time orders |
|
|
813
|
+
|
|
814
|
+
**Composite helpers (top-level methods)**
|
|
815
|
+
|
|
816
|
+
| Method | Description |
|
|
817
|
+
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
818
|
+
| `creem.getBillingModel(ctx, { entityId, user? })` | Aggregates snapshot + products + subscriptions + orders into a single object for widgets. Graceful when `entityId` is null (returns public catalog only). |
|
|
819
|
+
| `creem.getBillingSnapshot(ctx, { entityId })` | Resolved billing state (plan, status, available actions). Uses `resolvePlan` override if configured, otherwise built-in resolver. |
|
|
820
|
+
|
|
821
|
+
### `creem.api({ resolve })` — convenience exports
|
|
822
|
+
|
|
823
|
+
Generates ready-to-export Convex function definitions. Each function calls your
|
|
824
|
+
`resolve` callback, then delegates to the corresponding namespace method.
|
|
825
|
+
|
|
826
|
+
| Export | Wraps | Type | Description |
|
|
827
|
+
| ----------------------- | ----------------------- | ------ | ---------------------------------------------------------------------------------- |
|
|
828
|
+
| `uiModel` | `getBillingModel` | query | Calls `resolve()`, then `getBillingModel`. Graceful when unauthenticated. |
|
|
829
|
+
| `snapshot` | `getBillingSnapshot` | query | Calls `resolve()`, then `getBillingSnapshot`. Returns `null` when unauthenticated. |
|
|
830
|
+
| `checkouts.create` | `checkouts.create` | action | Auto-resolves auth |
|
|
831
|
+
| `subscriptions.update` | `subscriptions.update` | action | Auto-resolves auth |
|
|
832
|
+
| `subscriptions.cancel` | `subscriptions.cancel` | action | Auto-resolves auth |
|
|
833
|
+
| `subscriptions.resume` | `subscriptions.resume` | action | Auto-resolves auth |
|
|
834
|
+
| `subscriptions.pause` | `subscriptions.pause` | action | Auto-resolves auth |
|
|
835
|
+
| `subscriptions.list` | `subscriptions.list` | query | Auto-resolves auth |
|
|
836
|
+
| `subscriptions.listAll` | `subscriptions.listAll` | query | Auto-resolves auth |
|
|
837
|
+
| `products.list` | `products.list` | query | Public, no auth needed |
|
|
838
|
+
| `products.get` | `products.get` | query | Public, no auth needed |
|
|
839
|
+
| `customers.retrieve` | `customers.retrieve` | query | Auto-resolves auth |
|
|
840
|
+
| `customers.portalUrl` | `customers.portalUrl` | action | Auto-resolves auth |
|
|
841
|
+
| `orders.list` | `orders.list` | query | Auto-resolves auth |
|
|
842
|
+
|
|
843
|
+
### Infrastructure
|
|
844
|
+
|
|
845
|
+
| Method | Description |
|
|
846
|
+
| ------------------------------------------------ | ------------------------------------------- |
|
|
847
|
+
| `creem.syncProducts(ctx)` | Pull products from Creem API into Convex DB |
|
|
848
|
+
| `creem.registerRoutes(http, { path?, events? })` | Register webhook HTTP routes |
|
|
849
|
+
|
|
850
|
+
### Direct API access — `creem.sdk.*`
|
|
851
|
+
|
|
852
|
+
The resource namespaces above cover all **billing features that stay in sync**
|
|
853
|
+
with Convex via webhooks. Some Creem API resources have no webhook support, so
|
|
854
|
+
the component cannot mirror them in Convex DB. For these, use `creem.sdk.*`
|
|
855
|
+
directly inside your own Convex actions — it's the same Creem SDK client,
|
|
856
|
+
already configured with your API key:
|
|
857
|
+
|
|
858
|
+
| Resource | Synced to Convex? | Access |
|
|
859
|
+
| ---------------- | -------------------- | -------------------------- |
|
|
860
|
+
| Subscriptions | Yes (webhook) | `creem.subscriptions.*` |
|
|
861
|
+
| Checkouts | Yes (webhook) | `creem.checkouts.*` |
|
|
862
|
+
| Products | Yes (webhook + sync) | `creem.products.*` |
|
|
863
|
+
| Customers | Yes (webhook) | `creem.customers.*` |
|
|
864
|
+
| Orders | Yes (webhook) | `creem.orders.*` |
|
|
865
|
+
| **Licenses** | No webhook | `creem.sdk.licenses.*` |
|
|
866
|
+
| **Discounts** | No webhook | `creem.sdk.discounts.*` |
|
|
867
|
+
| **Transactions** | No webhook | `creem.sdk.transactions.*` |
|
|
868
|
+
|
|
869
|
+
```ts
|
|
870
|
+
import { action } from "./_generated/server";
|
|
871
|
+
import { v } from "convex/values";
|
|
872
|
+
|
|
873
|
+
// Example: create a discount (not synced — Creem has no webhook for discounts)
|
|
874
|
+
export const createDiscount = action({
|
|
875
|
+
args: { code: v.string(), percentage: v.number() },
|
|
876
|
+
handler: async (ctx, args) => {
|
|
877
|
+
return await creem.sdk.discounts.create({
|
|
878
|
+
name: args.code,
|
|
879
|
+
code: args.code,
|
|
880
|
+
type: "percentage",
|
|
881
|
+
percentage: args.percentage,
|
|
882
|
+
duration: "forever",
|
|
883
|
+
appliesTo: [],
|
|
884
|
+
});
|
|
885
|
+
},
|
|
886
|
+
});
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
---
|
|
890
|
+
|
|
891
|
+
## Component Reference
|
|
892
|
+
|
|
893
|
+
All components share **identical props** across Svelte and React.
|
|
894
|
+
|
|
895
|
+
- **Import:** `@mmailaender/convex-creem/svelte` or
|
|
896
|
+
`@mmailaender/convex-creem/react`
|
|
897
|
+
- **CSS class prop:** `class` in Svelte, `className` in React
|
|
898
|
+
- **Children:** Svelte `Snippet` / React `ReactNode`
|
|
899
|
+
- **Svelte** components use Svelte 5 runes and snippet rendering
|
|
900
|
+
(`{@render ...}`)
|
|
901
|
+
|
|
902
|
+
See the [Svelte example](example-svelte) and [React example](example-react) for
|
|
903
|
+
complete integrations.
|
|
904
|
+
|
|
905
|
+
### Widgets
|
|
906
|
+
|
|
907
|
+
These query Convex directly and manage billing state end-to-end.
|
|
908
|
+
|
|
909
|
+
#### `<Subscription.Root>`
|
|
910
|
+
|
|
911
|
+
Container for subscription plan cards. Handles billing cycle toggle, checkout,
|
|
912
|
+
plan switching, cancellation, and seat management.
|
|
913
|
+
|
|
914
|
+
| Prop | Type | Default | Description |
|
|
915
|
+
| ------------------- | --------------------------------------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
916
|
+
| `api` | `ConnectedBillingApi` | — | **Required.** Backend function references |
|
|
917
|
+
| `permissions` | `BillingPermissions` | all enabled | Disable actions based on user role |
|
|
918
|
+
| `class`/`className` | `string` | `""` | Wrapper CSS class |
|
|
919
|
+
| `successUrl` | `string` | product's `defaultSuccessUrl` → current page | Override redirect after checkout. When omitted, uses the product's `defaultSuccessUrl` from Creem; if that is also unset, falls back to the current page. |
|
|
920
|
+
| `units` | `number` | — | Auto-derived seat count for seat-based plans |
|
|
921
|
+
| `showSeatPicker` | `boolean` | `false` | Show quantity picker on seat-based cards |
|
|
922
|
+
| `twoColumnLayout` | `boolean` | `false` | Use two-column card layout |
|
|
923
|
+
| `updateBehavior` | `UpdateBehavior` | `"proration-charge-immediately"` | How plan switches and seat updates are billed. See below. |
|
|
924
|
+
| `onBeforeCheckout` | `(intent: CheckoutIntent) => Promise<boolean> \| boolean` | — | Gate checkout (auth, terms, etc.). Return `false` to abort. |
|
|
925
|
+
| `children` | `Snippet` / `ReactNode` | — | `<Subscription.Item>` children |
|
|
926
|
+
|
|
927
|
+
**`UpdateBehavior`** controls how the Creem API handles plan switches and seat
|
|
928
|
+
changes:
|
|
929
|
+
|
|
930
|
+
- `"proration-charge-immediately"` — prorate and charge the difference now
|
|
931
|
+
(default)
|
|
932
|
+
- `"proration-charge"` — prorate, charge on next invoice
|
|
933
|
+
- `"proration-none"` — no proration, change takes effect on next billing cycle
|
|
934
|
+
|
|
935
|
+
#### `<Subscription.Item>`
|
|
936
|
+
|
|
937
|
+
Registers a plan inside `<Subscription.Root>`. Renders nothing on its own — the
|
|
938
|
+
root component renders the pricing cards.
|
|
939
|
+
|
|
940
|
+
| Prop | Type | Default | Description |
|
|
941
|
+
| ------------- | ---------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------- |
|
|
942
|
+
| `type` | `"free" \| "single" \| "seat-based" \| "enterprise"` | — | **Required.** Plan type |
|
|
943
|
+
| `planId` | `string` | first product ID or `type` | Unique plan identifier |
|
|
944
|
+
| `title` | `string` | from Creem product data | Plan display title |
|
|
945
|
+
| `description` | `string` | from Creem product data | Plan subtitle (rendered as Markdown) |
|
|
946
|
+
| `contactUrl` | `string` | — | "Contact sales" link. **Required when `type="enterprise"`**. |
|
|
947
|
+
| `recommended` | `boolean` | `false` | Highlight as recommended plan |
|
|
948
|
+
| `productIds` | `Partial<Record<RecurringCycle, string>>` | — | Creem product IDs keyed by billing cycle. **Required when `type="single"` or `type="seat-based"`**. |
|
|
949
|
+
|
|
950
|
+
**Supported billing cycles:** `every-month`, `every-three-months`,
|
|
951
|
+
`every-six-months`, `every-year`.
|
|
952
|
+
|
|
953
|
+
`Subscription` and `Subscription.Item` are aliases — use whichever reads better
|
|
954
|
+
in your markup.
|
|
955
|
+
|
|
956
|
+
#### `<Product.Root>`
|
|
957
|
+
|
|
958
|
+
Container for one-time or repeating product cards. Handles ownership tracking,
|
|
959
|
+
upgrade transitions, and checkout.
|
|
960
|
+
|
|
961
|
+
| Prop | Type | Default | Description |
|
|
962
|
+
| ------------------- | --------------------------------------------------------- | -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
963
|
+
| `api` | `ConnectedBillingApi` | — | **Required.** Backend function references |
|
|
964
|
+
| `permissions` | `BillingPermissions` | all enabled | Disable actions based on user role |
|
|
965
|
+
| `transition` | `Transition[]` | `[]` | Upgrade path rules between products |
|
|
966
|
+
| `class`/`className` | `string` | `""` | Wrapper CSS class |
|
|
967
|
+
| `layout` | `"default" \| "single"` | `"default"` | Card layout mode |
|
|
968
|
+
| `styleVariant` | `"legacy" \| "pricing"` | `"legacy"` | Visual style variant |
|
|
969
|
+
| `showImages` | `boolean` | `false` | Show product images on cards |
|
|
970
|
+
| `pricingCtaVariant` | `"filled" \| "faded"` | `"faded"` | Call-to-action button style |
|
|
971
|
+
| `successUrl` | `string` | product's `defaultSuccessUrl` → current page | Override redirect after checkout. When omitted, uses the product's `defaultSuccessUrl` from Creem; if that is also unset, falls back to the current page. |
|
|
972
|
+
| `onBeforeCheckout` | `(intent: CheckoutIntent) => Promise<boolean> \| boolean` | — | Gate checkout (auth, terms, etc.). Return `false` to abort. |
|
|
973
|
+
| `children` | `Snippet` / `ReactNode` | — | `<Product.Item>` children |
|
|
974
|
+
|
|
975
|
+
**Transition types:**
|
|
976
|
+
|
|
977
|
+
```ts
|
|
978
|
+
type Transition =
|
|
979
|
+
| { from: string; to: string; kind: "direct" }
|
|
980
|
+
| { from: string; to: string; kind: "via_product"; viaProductId: string };
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
#### `<Product.Item>`
|
|
984
|
+
|
|
985
|
+
Registers a product inside `<Product.Root>`.
|
|
986
|
+
|
|
987
|
+
| Prop | Type | Default | Description |
|
|
988
|
+
| ------------- | --------------------------- | ----------------------- | --------------------------------------------------------- |
|
|
989
|
+
| `productId` | `string` | — | **Required.** Creem product ID |
|
|
990
|
+
| `type` | `"one-time" \| "recurring"` | — | **Required.** One-time shows "Owned" badge after purchase |
|
|
991
|
+
| `title` | `string` | from Creem product data | Card display title |
|
|
992
|
+
| `description` | `string` | from Creem product data | Card subtitle (rendered as Markdown) |
|
|
993
|
+
|
|
994
|
+
`Product` and `Product.Item` are aliases.
|
|
995
|
+
|
|
996
|
+
#### `<BillingPortal>`
|
|
997
|
+
|
|
998
|
+
Button that opens the Creem customer billing portal. Auto-hides when the billing
|
|
999
|
+
entity has no Creem customer record, or when `canAccessPortal` is `false`.
|
|
1000
|
+
|
|
1001
|
+
| Prop | Type | Default | Description |
|
|
1002
|
+
| ------------------- | ----------------------- | ------------------ | --------------------------------------------------------- |
|
|
1003
|
+
| `api` | `ConnectedBillingApi` | — | **Required.** Backend function references |
|
|
1004
|
+
| `permissions` | `BillingPermissions` | all enabled | Control portal access (e.g. `{ canAccessPortal: false }`) |
|
|
1005
|
+
| `class`/`className` | `string` | `""` | Button CSS class |
|
|
1006
|
+
| `children` | `Snippet` / `ReactNode` | `"Manage billing"` | Custom button label |
|
|
1007
|
+
|
|
1008
|
+
### Presentational components
|
|
1009
|
+
|
|
1010
|
+
Lower-level building blocks for custom layouts. These do **not** call Convex
|
|
1011
|
+
directly — pass data and callbacks as props.
|
|
1012
|
+
|
|
1013
|
+
#### `<PricingSection>`
|
|
1014
|
+
|
|
1015
|
+
Renders a grid of pricing cards with an optional billing cycle toggle.
|
|
1016
|
+
|
|
1017
|
+
| Prop | Type | Description |
|
|
1018
|
+
| ----------------------- | ------------------------- | ------------------------------------- |
|
|
1019
|
+
| `plans` | `UIPlanEntry[]` | Plan definitions |
|
|
1020
|
+
| `snapshot` | `BillingSnapshot \| null` | Current billing state |
|
|
1021
|
+
| `selectedCycle` | `RecurringCycle` | Active billing cycle |
|
|
1022
|
+
| `products` | `ConnectedProduct[]` | Product data for price resolution |
|
|
1023
|
+
| `subscriptionProductId` | `string \| null` | Currently subscribed product |
|
|
1024
|
+
| `subscriptionStatus` | `string \| null` | Subscription status |
|
|
1025
|
+
| `units` | `number` | Seat count |
|
|
1026
|
+
| `showSeatPicker` | `boolean` | Show quantity picker |
|
|
1027
|
+
| `subscribedSeats` | `number \| null` | Current seat count |
|
|
1028
|
+
| `isGroupSubscribed` | `boolean` | Whether group has active subscription |
|
|
1029
|
+
| `disableCheckout` | `boolean` | Disable checkout buttons |
|
|
1030
|
+
| `disableSwitch` | `boolean` | Disable plan switch buttons |
|
|
1031
|
+
| `disableSeats` | `boolean` | Disable seat controls |
|
|
1032
|
+
| `onCycleChange` | `(cycle) => void` | Billing cycle change handler |
|
|
1033
|
+
| `onCheckout` | `(payload) => void` | Checkout handler |
|
|
1034
|
+
| `onSwitchPlan` | `(payload) => void` | Plan switch handler |
|
|
1035
|
+
| `onUpdateSeats` | `(payload) => void` | Seat update handler |
|
|
1036
|
+
|
|
1037
|
+
#### `<PricingCard>`
|
|
1038
|
+
|
|
1039
|
+
A single plan card with price, description, and action button. Same props as
|
|
1040
|
+
`<PricingSection>` for a single plan (see source for full list).
|
|
1041
|
+
|
|
1042
|
+
#### `<BillingToggle>`
|
|
1043
|
+
|
|
1044
|
+
Billing cycle segment control (e.g. Monthly / Yearly).
|
|
1045
|
+
|
|
1046
|
+
| Prop | Type | Description |
|
|
1047
|
+
| --------------- | ------------------ | ---------------- |
|
|
1048
|
+
| `cycles` | `RecurringCycle[]` | Available cycles |
|
|
1049
|
+
| `value` | `RecurringCycle` | Selected cycle |
|
|
1050
|
+
| `onValueChange` | `(cycle) => void` | Change handler |
|
|
1051
|
+
| `className` | `string` | CSS class |
|
|
1052
|
+
|
|
1053
|
+
#### `<CheckoutButton>`
|
|
1054
|
+
|
|
1055
|
+
Styled checkout button. Supports both `onCheckout` callback and `href` link
|
|
1056
|
+
modes.
|
|
1057
|
+
|
|
1058
|
+
| Prop | Type | Description |
|
|
1059
|
+
| ------------ | ----------------------- | ---------------------------------- |
|
|
1060
|
+
| `productId` | `string` | Product ID |
|
|
1061
|
+
| `href` | `string` | Link mode: direct URL |
|
|
1062
|
+
| `disabled` | `boolean` | Disable button |
|
|
1063
|
+
| `className` | `string` | CSS class |
|
|
1064
|
+
| `onCheckout` | `(payload) => void` | Callback mode: `{ productId }` |
|
|
1065
|
+
| `children` | `Snippet` / `ReactNode` | Button label (default: "Checkout") |
|
|
1066
|
+
|
|
1067
|
+
#### `<OneTimeCheckoutButton>`
|
|
1068
|
+
|
|
1069
|
+
Same as `<CheckoutButton>` with default label "Buy now".
|
|
1070
|
+
|
|
1071
|
+
#### `<CustomerPortalButton>`
|
|
1072
|
+
|
|
1073
|
+
Styled button for opening the customer billing portal.
|
|
1074
|
+
|
|
1075
|
+
| Prop | Type | Description |
|
|
1076
|
+
| -------------- | ----------------------- | ---------------------------------------- |
|
|
1077
|
+
| `href` | `string` | Link mode: direct URL |
|
|
1078
|
+
| `disabled` | `boolean` | Disable button |
|
|
1079
|
+
| `className` | `string` | CSS class |
|
|
1080
|
+
| `onOpenPortal` | `() => void` | Callback mode |
|
|
1081
|
+
| `children` | `Snippet` / `ReactNode` | Button label (default: "Manage billing") |
|
|
1082
|
+
|
|
1083
|
+
#### `<BillingGate>`
|
|
1084
|
+
|
|
1085
|
+
Conditionally renders children based on available billing actions.
|
|
1086
|
+
|
|
1087
|
+
| Prop | Type | Description |
|
|
1088
|
+
| ----------------- | -------------------------------------- | --------------------------------------- |
|
|
1089
|
+
| `snapshot` | `BillingSnapshot \| null` | Current billing state |
|
|
1090
|
+
| `requiredActions` | `AvailableAction \| AvailableAction[]` | Actions that must be available |
|
|
1091
|
+
| `children` | `Snippet` / `ReactNode` | Rendered when all actions are available |
|
|
1092
|
+
| `fallback` | `Snippet` / `ReactNode` | Rendered otherwise |
|
|
1093
|
+
|
|
1094
|
+
#### `<CheckoutSuccessSummary>`
|
|
1095
|
+
|
|
1096
|
+
Displays a success banner after checkout. Parses Creem query params
|
|
1097
|
+
automatically.
|
|
1098
|
+
|
|
1099
|
+
| Prop | Type | Description |
|
|
1100
|
+
| ----------- | ----------------------- | ------------------------------------------------------------ |
|
|
1101
|
+
| `params` | `CheckoutSuccessParams` | Manual params (overrides URL parsing) |
|
|
1102
|
+
| `search` | `string` | Query string to parse (defaults to `window.location.search`) |
|
|
1103
|
+
| `className` | `string` | CSS class |
|
|
1104
|
+
|
|
1105
|
+
React also exports a `useCheckoutSuccessParams()` hook that returns the parsed
|
|
1106
|
+
params directly.
|
|
1107
|
+
|
|
1108
|
+
#### `<TrialLimitBanner>`
|
|
1109
|
+
|
|
1110
|
+
Shows a trial expiration notice.
|
|
1111
|
+
|
|
1112
|
+
| Prop | Type | Description |
|
|
1113
|
+
| ------------- | ------------------------- | ----------------------- |
|
|
1114
|
+
| `snapshot` | `BillingSnapshot \| null` | Current billing state |
|
|
1115
|
+
| `trialEndsAt` | `string \| null` | Override trial end date |
|
|
1116
|
+
| `className` | `string` | CSS class |
|
|
1117
|
+
|
|
1118
|
+
#### `<ScheduledChangeBanner>`
|
|
1119
|
+
|
|
1120
|
+
Shows a cancellation-scheduled notice with optional "Undo" button.
|
|
1121
|
+
|
|
1122
|
+
| Prop | Type | Description |
|
|
1123
|
+
| ----------- | ------------------------- | ------------------------------------------------- |
|
|
1124
|
+
| `snapshot` | `BillingSnapshot \| null` | Current billing state |
|
|
1125
|
+
| `isLoading` | `boolean` | Loading state for resume button |
|
|
1126
|
+
| `onResume` | `() => void` | Resume handler (shows "Undo cancellation" button) |
|
|
1127
|
+
| `className` | `string` | CSS class |
|
|
1128
|
+
|
|
1129
|
+
#### `<PaymentWarningBanner>`
|
|
1130
|
+
|
|
1131
|
+
Shows a warning for pending, refunded, or partially refunded payments.
|
|
1132
|
+
|
|
1133
|
+
| Prop | Type | Description |
|
|
1134
|
+
| ----------- | ------------------------- | --------------------- |
|
|
1135
|
+
| `snapshot` | `BillingSnapshot \| null` | Current billing state |
|
|
1136
|
+
| `payment` | `PaymentSnapshot \| null` | Override payment data |
|
|
1137
|
+
| `className` | `string` | CSS class |
|
|
1138
|
+
|
|
1139
|
+
#### `<OneTimePaymentStatusBadge>`
|
|
1140
|
+
|
|
1141
|
+
Inline status badge for one-time payments.
|
|
1142
|
+
|
|
1143
|
+
| Prop | Type | Description |
|
|
1144
|
+
| ----------- | ----------------------------------------------------------- | -------------- |
|
|
1145
|
+
| `status` | `"pending" \| "paid" \| "refunded" \| "partially_refunded"` | Payment status |
|
|
1146
|
+
| `className` | `string` | CSS class |
|
|
1147
|
+
|
|
1148
|
+
---
|
|
1149
|
+
|
|
1150
|
+
## Troubleshooting
|
|
1151
|
+
|
|
1152
|
+
**Webhooks not receiving events** Verify your Creem dashboard webhook URL
|
|
1153
|
+
matches `<CONVEX_SITE_URL>/creem/events`. Check that `CREEM_WEBHOOK_SECRET`
|
|
1154
|
+
matches the signing secret in Creem. Check the Convex dashboard logs for
|
|
1155
|
+
verification errors.
|
|
1156
|
+
|
|
1157
|
+
**Products not syncing** Run `npx convex run billing:syncBillingProducts` after
|
|
1158
|
+
setting up webhooks. Ensure `CREEM_API_KEY` is set and the key has read access
|
|
1159
|
+
to products.
|
|
1160
|
+
|
|
1161
|
+
**Widgets rendering unstyled** Ensure both Tailwind CSS v4 and
|
|
1162
|
+
`@import "@mmailaender/convex-creem/styles"` are in your CSS entry point. The
|
|
1163
|
+
styles import must come after the Tailwind import.
|
|
1164
|
+
|
|
1165
|
+
**Checkout URL missing from response** The Creem API returned no checkout URL.
|
|
1166
|
+
Verify the product ID exists and is active in your Creem dashboard. Check the
|
|
1167
|
+
Convex dashboard logs for the full error.
|
|
1168
|
+
|
|
1169
|
+
**Entity/org billing not scoping correctly** Ensure `billingEntityId` is
|
|
1170
|
+
returned from `getUserInfo`. If omitted, `userId` is used as the billing entity.
|
|
1171
|
+
Verify that checkout metadata includes `convexBillingEntityId` by checking
|
|
1172
|
+
webhook logs.
|
|
1173
|
+
|
|
1174
|
+
**"Customer not found" when opening billing portal** The customer record is
|
|
1175
|
+
created on first checkout. If the user hasn't completed a checkout yet, there's
|
|
1176
|
+
no customer to link to the portal.
|