diff --git a/.changeset/hungry-meals-float.md b/.changeset/hungry-meals-float.md new file mode 100644 index 00000000..0dfea18a --- /dev/null +++ b/.changeset/hungry-meals-float.md @@ -0,0 +1,5 @@ +--- +"@better-auth/stripe": patch +--- + +Fix duplicate trials when switching plans \ No newline at end of file diff --git a/demo/nextjs/app/dashboard/change-plan.tsx b/demo/nextjs/app/dashboard/change-plan.tsx index 376e955f..bc8ade1b 100644 --- a/demo/nextjs/app/dashboard/change-plan.tsx +++ b/demo/nextjs/app/dashboard/change-plan.tsx @@ -19,7 +19,7 @@ function Component(props: { currentPlan?: string; isTrial?: boolean; }) { - const [selectedPlan, setSelectedPlan] = useState("starter"); + const [selectedPlan, setSelectedPlan] = useState("plus"); const id = useId(); return ( @@ -72,35 +72,35 @@ function Component(props: { >
- +

- $50/month + $20/month

- +

- $99/month + $200/month

@@ -158,11 +158,11 @@ function Component(props: { ? props.isTrial ? "Upgrade" : "Current Plan" - : selectedPlan === "starter" + : selectedPlan === "plus" ? !props.currentPlan ? "Upgrade" : "Downgrade" - : selectedPlan === "professional" + : selectedPlan === "pro" ? "Upgrade" : "Contact us"} diff --git a/demo/nextjs/app/dashboard/user-card.tsx b/demo/nextjs/app/dashboard/user-card.tsx index 18a05918..6219e12b 100644 --- a/demo/nextjs/app/dashboard/user-card.tsx +++ b/demo/nextjs/app/dashboard/user-card.tsx @@ -142,11 +142,11 @@ export default function UserCard(props: {
diff --git a/demo/nextjs/app/pricing/page.tsx b/demo/nextjs/app/pricing/page.tsx index 0d06e346..0bd0ab8a 100644 --- a/demo/nextjs/app/pricing/page.tsx +++ b/demo/nextjs/app/pricing/page.tsx @@ -2,9 +2,9 @@ import { Pricing } from "@/components/blocks/pricing"; const demoPlans = [ { - name: "STARTER", - price: "50", - yearlyPrice: "40", + name: "Plus", + price: "20", + yearlyPrice: "16", period: "per month", features: [ "Up to 10 projects", @@ -18,9 +18,9 @@ const demoPlans = [ isPopular: false, }, { - name: "PROFESSIONAL", - price: "99", - yearlyPrice: "79", + name: "Pro", + price: "50", + yearlyPrice: "40", period: "per month", features: [ "Unlimited projects", @@ -34,24 +34,6 @@ const demoPlans = [ href: "/sign-up", isPopular: true, }, - { - name: "ENTERPRISE", - price: "299", - yearlyPrice: "239", - period: "per month", - features: [ - "Everything in Professional", - "Custom solutions", - "Dedicated account manager", - "1-hour support response time", - "SSO Authentication", - "Advanced security", - ], - description: "For large organizations with specific needs", - buttonText: "Contact Sales", - href: "/contact", - isPopular: false, - }, ]; export default function Page() { diff --git a/demo/nextjs/components/tier-labels.tsx b/demo/nextjs/components/tier-labels.tsx index 7d7e41cd..558088b6 100644 --- a/demo/nextjs/components/tier-labels.tsx +++ b/demo/nextjs/components/tier-labels.tsx @@ -8,9 +8,8 @@ const tierVariants = cva( variants: { variant: { free: "bg-gray-500 text-white ring-gray-400 hover:bg-gray-600", - starter: "bg-lime-700/40 text-white ring-lime-200/40 hover:bg-lime-600", - professional: "bg-purple-800/80 ring-purple-400 hover:bg-purple-700", - enterprise: "bg-amber-500 text-black ring-amber-400 hover:bg-amber-600", + plus: "bg-lime-700/40 text-white ring-lime-200/40 hover:bg-lime-600", + pro: "bg-purple-800/80 ring-purple-400 hover:bg-purple-700", }, }, defaultVariants: { @@ -22,7 +21,7 @@ const tierVariants = cva( export interface SubscriptionTierLabelProps extends React.HTMLAttributes, VariantProps { - tier?: "free" | "starter" | "professional" | "enterprise"; + tier?: "free" | "plus" | "pro"; } export const SubscriptionTierLabel: React.FC = ({ diff --git a/demo/nextjs/lib/auth.ts b/demo/nextjs/lib/auth.ts index a034e757..d9240b72 100644 --- a/demo/nextjs/lib/auth.ts +++ b/demo/nextjs/lib/auth.ts @@ -39,15 +39,14 @@ if (!dialect) { throw new Error("No dialect found"); } -const PROFESSION_PRICE_ID = { - default: "price_1QxWZ5LUjnrYIrml5Dnwnl0X", - annual: "price_1QxWZTLUjnrYIrmlyJYpwyhz", +const PRO_PRICE_ID = { + default: "price_1RoxnRHmTADgihIt4y8c0lVE", + annual: "price_1RoxnoHmTADgihItzFvVP8KT", }; -const STARTER_PRICE_ID = { - default: "price_1QxWWtLUjnrYIrmleljPKszG", - annual: "price_1QxWYqLUjnrYIrmlonqPThVF", +const PLUS_PRICE_ID = { + default: "price_1RoxnJHmTADgihIthZTLmrPn", + annual: "price_1Roxo5HmTADgihItEbJu5llL", }; - export const auth = betterAuth({ appName: "Better Auth Demo", database: { @@ -174,22 +173,23 @@ export const auth = betterAuth({ stripeWebhookSecret: process.env.STRIPE_WEBHOOK_SECRET!, subscription: { enabled: true, + allowReTrialsForDifferentPlans: true, plans: [ { - name: "Starter", - priceId: STARTER_PRICE_ID.default, - annualDiscountPriceId: STARTER_PRICE_ID.annual, + name: "Plus", + priceId: PLUS_PRICE_ID.default, + annualDiscountPriceId: PLUS_PRICE_ID.annual, freeTrial: { days: 7, }, }, { - name: "Professional", - priceId: PROFESSION_PRICE_ID.default, - annualDiscountPriceId: PROFESSION_PRICE_ID.annual, - }, - { - name: "Enterprise", + name: "Pro", + priceId: PRO_PRICE_ID.default, + annualDiscountPriceId: PRO_PRICE_ID.annual, + freeTrial: { + days: 7, + }, }, ], }, diff --git a/packages/stripe/src/index.ts b/packages/stripe/src/index.ts index 639464e6..a1eb307b 100644 --- a/packages/stripe/src/index.ts +++ b/packages/stripe/src/index.ts @@ -319,23 +319,20 @@ export const stripe = (options: O) => { } } - const activeSubscription = customerId - ? await client.subscriptions - .list({ - customer: customerId, - status: "active", - }) - .then((res) => - res.data.find( - (subscription) => - subscription.id === - subscriptionToUpdate?.stripeSubscriptionId || - ctx.body.subscriptionId, - ), - ) - .catch((e) => null) - : null; - + const activeSubscriptions = await client.subscriptions + .list({ + customer: customerId, + }) + .then((res) => + res.data.filter( + (sub) => sub.status === "active" || sub.status === "trialing", + ), + ); + const activeSubscription = activeSubscriptions.find((sub) => + subscriptionToUpdate?.stripeSubscriptionId + ? sub.id === subscriptionToUpdate?.stripeSubscriptionId + : true, + ); const subscriptions = subscriptionToUpdate ? [subscriptionToUpdate] : await ctx.context.adapter.findMany({ @@ -370,6 +367,12 @@ export const stripe = (options: O) => { return_url: getUrl(ctx, ctx.body.returnUrl || "/"), flow_data: { type: "subscription_update_confirm", + after_completion: { + type: "redirect", + redirect: { + return_url: getUrl(ctx, ctx.body.returnUrl || "/"), + }, + }, subscription_update_confirm: { subscription: activeSubscription.id, items: [ @@ -426,11 +429,13 @@ export const stripe = (options: O) => { ctx, ); - const freeTrial = plan.freeTrial - ? { - trial_period_days: plan.freeTrial.days, - } - : undefined; + const alreadyHasTrial = subscription.status === "trialing"; + const freeTrial = + !alreadyHasTrial && plan.freeTrial + ? { + trial_period_days: plan.freeTrial.days, + } + : undefined; let priceIdToUse: string | undefined = undefined; if (ctx.body.annual) { diff --git a/packages/stripe/src/types.ts b/packages/stripe/src/types.ts index cf2da45f..775b4b24 100644 --- a/packages/stripe/src/types.ts +++ b/packages/stripe/src/types.ts @@ -318,6 +318,11 @@ export interface StripeOptions { enabled: boolean; }; }; + /** + * A callback to run after a stripe event is received + * @param event - Stripe Event + * @returns + */ onEvent?: (event: Stripe.Event) => Promise; /** * Schema for the stripe plugin