@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,212 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
isSupportedRecurringCycle,
|
|
4
|
+
normalizeRecurringCycle,
|
|
5
|
+
normalizePlanCategory,
|
|
6
|
+
normalizeBillingType,
|
|
7
|
+
normalizePlanCatalog,
|
|
8
|
+
findPlanById,
|
|
9
|
+
findPlanByProductId,
|
|
10
|
+
} from "./catalog.js";
|
|
11
|
+
|
|
12
|
+
describe("isSupportedRecurringCycle", () => {
|
|
13
|
+
it("returns true for every-month", () => {
|
|
14
|
+
expect(isSupportedRecurringCycle("every-month")).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("returns true for every-year", () => {
|
|
18
|
+
expect(isSupportedRecurringCycle("every-year")).toBe(true);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns false for unknown cycle", () => {
|
|
22
|
+
expect(isSupportedRecurringCycle("weekly")).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("normalizeRecurringCycle", () => {
|
|
27
|
+
it("returns undefined for null", () => {
|
|
28
|
+
expect(normalizeRecurringCycle(null)).toBeUndefined();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("returns undefined for undefined", () => {
|
|
32
|
+
expect(normalizeRecurringCycle(undefined)).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns the value for supported cycles", () => {
|
|
36
|
+
expect(normalizeRecurringCycle("every-month")).toBe("every-month");
|
|
37
|
+
expect(normalizeRecurringCycle("every-three-months")).toBe(
|
|
38
|
+
"every-three-months",
|
|
39
|
+
);
|
|
40
|
+
expect(normalizeRecurringCycle("every-six-months")).toBe(
|
|
41
|
+
"every-six-months",
|
|
42
|
+
);
|
|
43
|
+
expect(normalizeRecurringCycle("every-year")).toBe("every-year");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns custom for unsupported cycles", () => {
|
|
47
|
+
expect(normalizeRecurringCycle("weekly")).toBe("custom");
|
|
48
|
+
expect(normalizeRecurringCycle("biweekly")).toBe("custom");
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("normalizePlanCategory", () => {
|
|
53
|
+
it("returns custom for null", () => {
|
|
54
|
+
expect(normalizePlanCategory(null)).toBe("custom");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns custom for undefined", () => {
|
|
58
|
+
expect(normalizePlanCategory(undefined)).toBe("custom");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("returns the value for known categories", () => {
|
|
62
|
+
expect(normalizePlanCategory("free")).toBe("free");
|
|
63
|
+
expect(normalizePlanCategory("trial")).toBe("trial");
|
|
64
|
+
expect(normalizePlanCategory("paid")).toBe("paid");
|
|
65
|
+
expect(normalizePlanCategory("enterprise")).toBe("enterprise");
|
|
66
|
+
expect(normalizePlanCategory("custom")).toBe("custom");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("returns custom for unknown values", () => {
|
|
70
|
+
expect(normalizePlanCategory("premium")).toBe("custom");
|
|
71
|
+
expect(normalizePlanCategory("vip")).toBe("custom");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("normalizeBillingType", () => {
|
|
76
|
+
it("returns custom for null", () => {
|
|
77
|
+
expect(normalizeBillingType(null)).toBe("custom");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns custom for undefined", () => {
|
|
81
|
+
expect(normalizeBillingType(undefined)).toBe("custom");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("returns the value for known types", () => {
|
|
85
|
+
expect(normalizeBillingType("recurring")).toBe("recurring");
|
|
86
|
+
expect(normalizeBillingType("onetime")).toBe("onetime");
|
|
87
|
+
expect(normalizeBillingType("custom")).toBe("custom");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns custom for unknown values", () => {
|
|
91
|
+
expect(normalizeBillingType("metered")).toBe("custom");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("normalizePlanCatalog", () => {
|
|
96
|
+
it("returns undefined for null", () => {
|
|
97
|
+
expect(normalizePlanCatalog(null)).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("returns undefined for undefined", () => {
|
|
101
|
+
expect(normalizePlanCatalog(undefined)).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("normalizes plan entries", () => {
|
|
105
|
+
const catalog = normalizePlanCatalog({
|
|
106
|
+
version: "1",
|
|
107
|
+
plans: [
|
|
108
|
+
{
|
|
109
|
+
planId: "pro",
|
|
110
|
+
category: "paid",
|
|
111
|
+
billingType: "recurring",
|
|
112
|
+
billingCycles: ["every-month", "every-year"],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
planId: "custom",
|
|
116
|
+
category: "unknown_category" as any,
|
|
117
|
+
billingType: "unknown_type" as any,
|
|
118
|
+
billingCycles: ["every-month", "biweekly" as any],
|
|
119
|
+
},
|
|
120
|
+
],
|
|
121
|
+
});
|
|
122
|
+
expect(catalog).toBeDefined();
|
|
123
|
+
expect(catalog!.plans[0].category).toBe("paid");
|
|
124
|
+
expect(catalog!.plans[0].billingType).toBe("recurring");
|
|
125
|
+
expect(catalog!.plans[0].billingCycles).toEqual([
|
|
126
|
+
"every-month",
|
|
127
|
+
"every-year",
|
|
128
|
+
]);
|
|
129
|
+
// Unknown values normalize to "custom"
|
|
130
|
+
expect(catalog!.plans[1].category).toBe("custom");
|
|
131
|
+
expect(catalog!.plans[1].billingType).toBe("custom");
|
|
132
|
+
expect(catalog!.plans[1].billingCycles).toEqual(["every-month", "custom"]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it("filters out null cycles from normalization", () => {
|
|
136
|
+
const catalog = normalizePlanCatalog({
|
|
137
|
+
version: "1",
|
|
138
|
+
plans: [
|
|
139
|
+
{
|
|
140
|
+
planId: "test",
|
|
141
|
+
category: "paid",
|
|
142
|
+
billingCycles: [null as any, "every-month"],
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
// null normalizes to undefined which gets filtered out
|
|
147
|
+
expect(catalog!.plans[0].billingCycles).toEqual(["every-month"]);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe("findPlanById", () => {
|
|
152
|
+
const catalog = {
|
|
153
|
+
version: "1",
|
|
154
|
+
plans: [
|
|
155
|
+
{ planId: "free", category: "free" as const },
|
|
156
|
+
{ planId: "pro", category: "paid" as const },
|
|
157
|
+
],
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
it("finds a plan by id", () => {
|
|
161
|
+
expect(findPlanById(catalog, "pro")).toEqual({
|
|
162
|
+
planId: "pro",
|
|
163
|
+
category: "paid",
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("returns undefined for unknown id", () => {
|
|
168
|
+
expect(findPlanById(catalog, "enterprise")).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("returns undefined for undefined catalog", () => {
|
|
172
|
+
expect(findPlanById(undefined, "pro")).toBeUndefined();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("findPlanByProductId", () => {
|
|
177
|
+
const catalog = {
|
|
178
|
+
version: "1",
|
|
179
|
+
plans: [
|
|
180
|
+
{
|
|
181
|
+
planId: "pro",
|
|
182
|
+
category: "paid" as const,
|
|
183
|
+
creemProductIds: {
|
|
184
|
+
"every-month": "prod_1",
|
|
185
|
+
"every-year": "prod_2",
|
|
186
|
+
} as Record<string, string>,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
planId: "basic",
|
|
190
|
+
category: "paid" as const,
|
|
191
|
+
creemProductIds: { "every-month": "prod_3" } as Record<string, string>,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
it("finds plan by product id", () => {
|
|
197
|
+
expect(findPlanByProductId(catalog, "prod_2")?.planId).toBe("pro");
|
|
198
|
+
expect(findPlanByProductId(catalog, "prod_3")?.planId).toBe("basic");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("returns undefined for unknown product id", () => {
|
|
202
|
+
expect(findPlanByProductId(catalog, "prod_unknown")).toBeUndefined();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("returns undefined for undefined catalog", () => {
|
|
206
|
+
expect(findPlanByProductId(undefined, "prod_1")).toBeUndefined();
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("returns undefined for undefined productId", () => {
|
|
210
|
+
expect(findPlanByProductId(catalog, undefined)).toBeUndefined();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BillingType,
|
|
3
|
+
PlanCatalog,
|
|
4
|
+
PlanCatalogEntry,
|
|
5
|
+
PlanCategory,
|
|
6
|
+
RecurringCycle,
|
|
7
|
+
SupportedRecurringCycle,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
|
|
10
|
+
/** All billing cycles supported by the Creem API, in display order. */
|
|
11
|
+
export const SUPPORTED_RECURRING_CYCLES: SupportedRecurringCycle[] = [
|
|
12
|
+
"every-month",
|
|
13
|
+
"every-three-months",
|
|
14
|
+
"every-six-months",
|
|
15
|
+
"every-year",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const PLAN_CATEGORIES: PlanCategory[] = [
|
|
19
|
+
"free",
|
|
20
|
+
"trial",
|
|
21
|
+
"paid",
|
|
22
|
+
"enterprise",
|
|
23
|
+
"custom",
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const BILLING_TYPES: BillingType[] = ["recurring", "onetime", "custom"];
|
|
27
|
+
|
|
28
|
+
const PLAN_CATEGORY_SET = new Set(PLAN_CATEGORIES);
|
|
29
|
+
const BILLING_TYPE_SET = new Set(BILLING_TYPES);
|
|
30
|
+
const RECURRING_CYCLE_SET = new Set(SUPPORTED_RECURRING_CYCLES);
|
|
31
|
+
|
|
32
|
+
/** Type guard: check if a string is a supported Creem billing cycle. */
|
|
33
|
+
export const isSupportedRecurringCycle = (
|
|
34
|
+
value: string,
|
|
35
|
+
): value is SupportedRecurringCycle =>
|
|
36
|
+
RECURRING_CYCLE_SET.has(value as SupportedRecurringCycle);
|
|
37
|
+
|
|
38
|
+
/** Normalize a billing cycle string to a `RecurringCycle`. Returns `"custom"` for unrecognized values, `undefined` for nullish. */
|
|
39
|
+
export const normalizeRecurringCycle = (
|
|
40
|
+
value: string | null | undefined,
|
|
41
|
+
): RecurringCycle | undefined => {
|
|
42
|
+
if (!value) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (isSupportedRecurringCycle(value)) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
return "custom";
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** Normalize a plan category string. Returns `"custom"` for unrecognized or nullish values. */
|
|
52
|
+
export const normalizePlanCategory = (
|
|
53
|
+
value: string | null | undefined,
|
|
54
|
+
): PlanCategory => {
|
|
55
|
+
if (!value) {
|
|
56
|
+
return "custom";
|
|
57
|
+
}
|
|
58
|
+
if (PLAN_CATEGORY_SET.has(value as PlanCategory)) {
|
|
59
|
+
return value as PlanCategory;
|
|
60
|
+
}
|
|
61
|
+
return "custom";
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/** Normalize a billing type string. Returns `"custom"` for unrecognized or nullish values. */
|
|
65
|
+
export const normalizeBillingType = (
|
|
66
|
+
value: string | null | undefined,
|
|
67
|
+
): BillingType => {
|
|
68
|
+
if (!value) {
|
|
69
|
+
return "custom";
|
|
70
|
+
}
|
|
71
|
+
if (BILLING_TYPE_SET.has(value as BillingType)) {
|
|
72
|
+
return value as BillingType;
|
|
73
|
+
}
|
|
74
|
+
return "custom";
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/** Normalize all entries in a plan catalog (categories, billing types, cycles). Returns `undefined` for nullish input. */
|
|
78
|
+
export const normalizePlanCatalog = (
|
|
79
|
+
catalog: PlanCatalog | null | undefined,
|
|
80
|
+
): PlanCatalog | undefined => {
|
|
81
|
+
if (!catalog) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
...catalog,
|
|
86
|
+
plans: catalog.plans.map((plan) => ({
|
|
87
|
+
...plan,
|
|
88
|
+
category: normalizePlanCategory(plan.category),
|
|
89
|
+
billingType: normalizeBillingType(plan.billingType),
|
|
90
|
+
billingCycles: (plan.billingCycles ?? [])
|
|
91
|
+
.map((cycle) => normalizeRecurringCycle(cycle))
|
|
92
|
+
.flatMap((cycle) => (cycle ? [cycle] : [])),
|
|
93
|
+
})),
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/** Find a plan in the catalog by its `planId`. */
|
|
98
|
+
export const findPlanById = (
|
|
99
|
+
catalog: PlanCatalog | undefined,
|
|
100
|
+
planId: string,
|
|
101
|
+
): PlanCatalogEntry | undefined => {
|
|
102
|
+
if (!catalog) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return catalog.plans.find((plan) => plan.planId === planId);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Find a plan in the catalog that contains the given Creem product ID in its `creemProductIds`. */
|
|
109
|
+
export const findPlanByProductId = (
|
|
110
|
+
catalog: PlanCatalog | undefined,
|
|
111
|
+
productId: string | undefined,
|
|
112
|
+
): PlanCatalogEntry | undefined => {
|
|
113
|
+
if (!catalog || !productId) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
return catalog.plans.find((plan) =>
|
|
117
|
+
Object.values(plan.creemProductIds ?? {}).includes(productId),
|
|
118
|
+
);
|
|
119
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./types.js";
|
|
2
|
+
export * from "./catalog.js";
|
|
3
|
+
export * from "./resolver.js";
|
|
4
|
+
export * from "./selectors.js";
|
|
5
|
+
export * from "./payments.js";
|
|
6
|
+
export * from "./markdown.js";
|
|
7
|
+
export { pendingCheckout } from "./pendingCheckout.js";
|
|
8
|
+
export * from "./subscriptionUpdate.js";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { renderMarkdown, renderMarkdownInline } from "./markdown.js";
|
|
3
|
+
|
|
4
|
+
describe("renderMarkdown", () => {
|
|
5
|
+
it("renders basic markdown to HTML", () => {
|
|
6
|
+
const result = renderMarkdown("**bold** text");
|
|
7
|
+
expect(result).toContain("<strong>bold</strong>");
|
|
8
|
+
expect(result).toContain("text");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("returns empty string for null input", () => {
|
|
12
|
+
expect(renderMarkdown(null)).toBe("");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("returns empty string for undefined input", () => {
|
|
16
|
+
expect(renderMarkdown(undefined)).toBe("");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns empty string for empty string input", () => {
|
|
20
|
+
expect(renderMarkdown("")).toBe("");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("renders GFM line breaks", () => {
|
|
24
|
+
const result = renderMarkdown("line1\nline2");
|
|
25
|
+
expect(result).toContain("<br");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("renderMarkdownInline", () => {
|
|
30
|
+
it("renders inline markdown without wrapping p tags", () => {
|
|
31
|
+
const result = renderMarkdownInline("**bold** text");
|
|
32
|
+
expect(result).toContain("<strong>bold</strong>");
|
|
33
|
+
expect(result).not.toContain("<p>");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns empty string for null input", () => {
|
|
37
|
+
expect(renderMarkdownInline(null)).toBe("");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns empty string for undefined input", () => {
|
|
41
|
+
expect(renderMarkdownInline(undefined)).toBe("");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { Marked } from "marked";
|
|
2
|
+
|
|
3
|
+
const marked = new Marked({
|
|
4
|
+
async: false,
|
|
5
|
+
gfm: true,
|
|
6
|
+
breaks: true,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Render a markdown string to HTML.
|
|
11
|
+
* Intended for short product/plan descriptions synced from Creem.
|
|
12
|
+
* Returns an empty string for nullish input.
|
|
13
|
+
*/
|
|
14
|
+
export function renderMarkdown(md: string | undefined | null): string {
|
|
15
|
+
if (!md) return "";
|
|
16
|
+
return marked.parse(md) as string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Render a markdown string to inline HTML (no wrapping `<p>` tags).
|
|
21
|
+
* Useful for single-line titles or short labels.
|
|
22
|
+
*/
|
|
23
|
+
export function renderMarkdownInline(md: string | undefined | null): string {
|
|
24
|
+
if (!md) return "";
|
|
25
|
+
return marked.parseInline(md) as string;
|
|
26
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
parseCheckoutSuccessParams,
|
|
4
|
+
hasCheckoutSuccessParams,
|
|
5
|
+
} from "./payments.js";
|
|
6
|
+
|
|
7
|
+
describe("parseCheckoutSuccessParams", () => {
|
|
8
|
+
it("parses a full query string with leading ?", () => {
|
|
9
|
+
const result = parseCheckoutSuccessParams(
|
|
10
|
+
"?checkout_id=chk_1&order_id=ord_2&customer_id=cus_3&product_id=prod_4&request_id=req_5&signature=sig_6",
|
|
11
|
+
);
|
|
12
|
+
expect(result).toEqual({
|
|
13
|
+
checkoutId: "chk_1",
|
|
14
|
+
orderId: "ord_2",
|
|
15
|
+
customerId: "cus_3",
|
|
16
|
+
productId: "prod_4",
|
|
17
|
+
requestId: "req_5",
|
|
18
|
+
signature: "sig_6",
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("parses a query string without leading ?", () => {
|
|
23
|
+
const result = parseCheckoutSuccessParams(
|
|
24
|
+
"checkout_id=chk_1&order_id=ord_2",
|
|
25
|
+
);
|
|
26
|
+
expect(result.checkoutId).toBe("chk_1");
|
|
27
|
+
expect(result.orderId).toBe("ord_2");
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns undefined for missing params", () => {
|
|
31
|
+
const result = parseCheckoutSuccessParams("");
|
|
32
|
+
expect(result.checkoutId).toBeUndefined();
|
|
33
|
+
expect(result.orderId).toBeUndefined();
|
|
34
|
+
expect(result.customerId).toBeUndefined();
|
|
35
|
+
expect(result.productId).toBeUndefined();
|
|
36
|
+
expect(result.requestId).toBeUndefined();
|
|
37
|
+
expect(result.signature).toBeUndefined();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("accepts a URLSearchParams instance", () => {
|
|
41
|
+
const params = new URLSearchParams({
|
|
42
|
+
checkout_id: "chk_url",
|
|
43
|
+
order_id: "ord_url",
|
|
44
|
+
});
|
|
45
|
+
const result = parseCheckoutSuccessParams(params);
|
|
46
|
+
expect(result.checkoutId).toBe("chk_url");
|
|
47
|
+
expect(result.orderId).toBe("ord_url");
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe("hasCheckoutSuccessParams", () => {
|
|
52
|
+
it("returns true when both checkoutId and orderId are present", () => {
|
|
53
|
+
expect(
|
|
54
|
+
hasCheckoutSuccessParams({ checkoutId: "chk_1", orderId: "ord_1" }),
|
|
55
|
+
).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("returns false when checkoutId is missing", () => {
|
|
59
|
+
expect(hasCheckoutSuccessParams({ orderId: "ord_1" })).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("returns false when orderId is missing", () => {
|
|
63
|
+
expect(hasCheckoutSuccessParams({ checkoutId: "chk_1" })).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns false when both are missing", () => {
|
|
67
|
+
expect(hasCheckoutSuccessParams({})).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { CheckoutSuccessParams } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const getSearchParams = (search: string | URLSearchParams): URLSearchParams => {
|
|
4
|
+
if (search instanceof URLSearchParams) {
|
|
5
|
+
return search;
|
|
6
|
+
}
|
|
7
|
+
const raw = search.startsWith("?") ? search.slice(1) : search;
|
|
8
|
+
return new URLSearchParams(raw);
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Parse Creem checkout success query parameters from a URL search string.
|
|
13
|
+
* Call on your success page to extract checkout/order/customer/product IDs.
|
|
14
|
+
*
|
|
15
|
+
* @param search - URL search string (e.g. `window.location.search`) or `URLSearchParams` instance
|
|
16
|
+
*/
|
|
17
|
+
export const parseCheckoutSuccessParams = (
|
|
18
|
+
search: string | URLSearchParams,
|
|
19
|
+
): CheckoutSuccessParams => {
|
|
20
|
+
const params = getSearchParams(search);
|
|
21
|
+
return {
|
|
22
|
+
checkoutId: params.get("checkout_id") ?? undefined,
|
|
23
|
+
orderId: params.get("order_id") ?? undefined,
|
|
24
|
+
customerId: params.get("customer_id") ?? undefined,
|
|
25
|
+
productId: params.get("product_id") ?? undefined,
|
|
26
|
+
requestId: params.get("request_id") ?? undefined,
|
|
27
|
+
signature: params.get("signature") ?? undefined,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Check if the parsed params contain the minimum required fields for a valid checkout success (checkoutId + orderId). */
|
|
32
|
+
export const hasCheckoutSuccessParams = (params: CheckoutSuccessParams) =>
|
|
33
|
+
Boolean(params.checkoutId && params.orderId);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
3
|
+
import { pendingCheckout } from "./pendingCheckout.js";
|
|
4
|
+
|
|
5
|
+
describe("pendingCheckout", () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
sessionStorage.clear();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("save stores intent in sessionStorage", () => {
|
|
11
|
+
pendingCheckout.save({ productId: "prod_123" });
|
|
12
|
+
const raw = sessionStorage.getItem("creem:pending-checkout");
|
|
13
|
+
expect(raw).not.toBeNull();
|
|
14
|
+
expect(JSON.parse(raw!)).toEqual({ productId: "prod_123" });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("save stores intent with units", () => {
|
|
18
|
+
pendingCheckout.save({ productId: "prod_abc", units: 5 });
|
|
19
|
+
const raw = sessionStorage.getItem("creem:pending-checkout");
|
|
20
|
+
expect(JSON.parse(raw!)).toEqual({ productId: "prod_abc", units: 5 });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("load returns the saved intent and clears storage", () => {
|
|
24
|
+
pendingCheckout.save({ productId: "prod_xyz" });
|
|
25
|
+
const intent = pendingCheckout.load();
|
|
26
|
+
expect(intent).toEqual({ productId: "prod_xyz" });
|
|
27
|
+
// Should be cleared after load
|
|
28
|
+
expect(sessionStorage.getItem("creem:pending-checkout")).toBeNull();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("load returns null when nothing is saved", () => {
|
|
32
|
+
expect(pendingCheckout.load()).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("clear removes the saved intent", () => {
|
|
36
|
+
pendingCheckout.save({ productId: "prod_del" });
|
|
37
|
+
pendingCheckout.clear();
|
|
38
|
+
expect(sessionStorage.getItem("creem:pending-checkout")).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("clear is safe when nothing is saved", () => {
|
|
42
|
+
expect(() => pendingCheckout.clear()).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { CheckoutIntent } from "./types.js";
|
|
2
|
+
|
|
3
|
+
const STORAGE_KEY = "creem:pending-checkout";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tiny sessionStorage-based helper for the "save intent → sign in → resume" pattern.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* 1. In `onBeforeCheckout`, call `pendingCheckout.save(intent)` then open sign-in
|
|
10
|
+
* 2. After auth completes, call `pendingCheckout.load()` — returns the intent and auto-clears
|
|
11
|
+
* 3. Resume checkout with the loaded intent (e.g. call `checkouts.create` manually)
|
|
12
|
+
*/
|
|
13
|
+
export const pendingCheckout = {
|
|
14
|
+
save(intent: CheckoutIntent): void {
|
|
15
|
+
try {
|
|
16
|
+
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(intent));
|
|
17
|
+
} catch {
|
|
18
|
+
// sessionStorage unavailable (SSR, private browsing quota) — silently ignore
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
load(): CheckoutIntent | null {
|
|
23
|
+
try {
|
|
24
|
+
const raw = sessionStorage.getItem(STORAGE_KEY);
|
|
25
|
+
if (!raw) return null;
|
|
26
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
27
|
+
return JSON.parse(raw) as CheckoutIntent;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
clear(): void {
|
|
34
|
+
try {
|
|
35
|
+
sessionStorage.removeItem(STORAGE_KEY);
|
|
36
|
+
} catch {
|
|
37
|
+
// silently ignore
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
};
|