mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-09 12:27:43 +00:00
* init * wip * wip * wip * wip * wip * wip * wip * wip * wip * feat(stripe): enable subscription support and update pricing plans * feat(stripe): add Vitest configuration and initial tests for Stripe integration * feat(stripe): implement setCookieToHeader function and update tests for customer creation and subscription handling * feat(stripe): add seats support for subscriptions and update related endpoints * feat(stripe): update schema to include unique referenceId, stripeSubscriptionId, and periodEnd fields * wip docs * docs * docs: imporves * fix(stripe): update webhook handlers to use correct subscription identification * refactor(stripe): simplify customer management by storing Stripe customer ID directly on user * chore(stripe): update package configuration and build setup - Migrated from tsup to unbuild for build configuration - Updated package.json with improved export and dependency management - Added build configuration for better module support - Removed tsup configuration file * chore(stripe): update pnpm lockfile dependencies - Moved `better-auth` from devDependencies to dependencies - Added `zod` as a direct dependency - Reorganized package dependencies in the lockfile * feat(stripe): enhance subscription management and error handling - Added toast error handling for subscription upgrades in the dashboard - Updated Stripe price IDs for different plans - Improved Stripe plugin documentation with beta warning and team subscription details - Implemented intermediate redirect for checkout success to handle race conditions - Added support for fetching and updating subscription status after checkout - Fixed Next.js cookie handling and build configuration * chore: update snapshot
191 lines
5.5 KiB
TypeScript
191 lines
5.5 KiB
TypeScript
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import { Label } from "@/components/ui/label";
|
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
import { client } from "@/lib/auth-client";
|
|
import { cn } from "@/lib/utils";
|
|
import { ArrowUpFromLine, CreditCard, RefreshCcw } from "lucide-react";
|
|
import { useId, useState } from "react";
|
|
import { toast } from "sonner";
|
|
|
|
function Component(props: {
|
|
currentPlan?: string;
|
|
isTrial?: boolean;
|
|
}) {
|
|
const [selectedPlan, setSelectedPlan] = useState("starter");
|
|
const id = useId();
|
|
return (
|
|
<Dialog>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
variant={!props.currentPlan ? "default" : "outline"}
|
|
size="sm"
|
|
className={cn(
|
|
"gap-2",
|
|
!props.currentPlan &&
|
|
" bg-gradient-to-br from-purple-100 to-stone-300",
|
|
)}
|
|
>
|
|
{props.currentPlan ? (
|
|
<RefreshCcw className="opacity-80" size={14} strokeWidth={2} />
|
|
) : (
|
|
<ArrowUpFromLine className="opacity-80" size={14} strokeWidth={2} />
|
|
)}
|
|
{props.currentPlan ? "Change Plan" : "Upgrade Plan"}
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<div className="mb-2 flex flex-col gap-2">
|
|
<div
|
|
className="flex size-11 shrink-0 items-center justify-center rounded-full border border-border"
|
|
aria-hidden="true"
|
|
>
|
|
{props.currentPlan ? (
|
|
<RefreshCcw className="opacity-80" size={16} strokeWidth={2} />
|
|
) : (
|
|
<CreditCard className="opacity-80" size={16} strokeWidth={2} />
|
|
)}
|
|
</div>
|
|
<DialogHeader>
|
|
<DialogTitle className="text-left">
|
|
{!props.currentPlan ? "Upgrade" : "Change"} your plan
|
|
</DialogTitle>
|
|
<DialogDescription className="text-left">
|
|
Pick one of the following plans.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
</div>
|
|
|
|
<form className="space-y-5">
|
|
<RadioGroup
|
|
className="gap-2"
|
|
defaultValue="2"
|
|
value={selectedPlan}
|
|
onValueChange={(value) => setSelectedPlan(value)}
|
|
>
|
|
<div className="relative flex w-full items-center gap-2 rounded-lg border border-input px-4 py-3 shadow-sm shadow-black/5 has-[[data-state=checked]]:border-ring has-[[data-state=checked]]:bg-accent">
|
|
<RadioGroupItem
|
|
value="starter"
|
|
id={`${id}-1`}
|
|
aria-describedby={`${id}-1-description`}
|
|
className="order-1 after:absolute after:inset-0"
|
|
/>
|
|
<div className="grid grow gap-1">
|
|
<Label htmlFor={`${id}-1`}>Starter</Label>
|
|
<p
|
|
id={`${id}-1-description`}
|
|
className="text-xs text-muted-foreground"
|
|
>
|
|
$50/month
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="relative flex w-full items-center gap-2 rounded-lg border border-input px-4 py-3 shadow-sm shadow-black/5 has-[[data-state=checked]]:border-ring has-[[data-state=checked]]:bg-accent">
|
|
<RadioGroupItem
|
|
value="professional"
|
|
id={`${id}-2`}
|
|
aria-describedby={`${id}-2-description`}
|
|
className="order-1 after:absolute after:inset-0"
|
|
/>
|
|
<div className="grid grow gap-1">
|
|
<Label htmlFor={`${id}-2`}>Professional</Label>
|
|
<p
|
|
id={`${id}-2-description`}
|
|
className="text-xs text-muted-foreground"
|
|
>
|
|
$99/month
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div className="relative flex w-full items-center gap-2 rounded-lg border border-input px-4 py-3 shadow-sm shadow-black/5 has-[[data-state=checked]]:border-ring has-[[data-state=checked]]:bg-accent">
|
|
<RadioGroupItem
|
|
value="enterprise"
|
|
id={`${id}-3`}
|
|
aria-describedby={`${id}-3-description`}
|
|
className="order-1 after:absolute after:inset-0"
|
|
/>
|
|
<div className="grid grow gap-1">
|
|
<Label htmlFor={`${id}-3`}>Enterprise</Label>
|
|
<p
|
|
id={`${id}-3-description`}
|
|
className="text-xs text-muted-foreground"
|
|
>
|
|
Contact our sales team
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</RadioGroup>
|
|
|
|
<div className="space-y-3">
|
|
<p className="text-xs text-white/70 text-center">
|
|
note: all upgrades takes effect immediately and you'll be charged
|
|
the new amount on your next billing cycle.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid gap-2">
|
|
<Button
|
|
type="button"
|
|
className="w-full"
|
|
disabled={
|
|
selectedPlan === props.currentPlan?.toLowerCase() &&
|
|
!props.isTrial
|
|
}
|
|
onClick={async () => {
|
|
if (selectedPlan === "enterprise") {
|
|
return;
|
|
}
|
|
await client.subscription.upgrade(
|
|
{
|
|
plan: selectedPlan,
|
|
},
|
|
{
|
|
onError: (ctx) => {
|
|
toast.error(ctx.error.message);
|
|
},
|
|
},
|
|
);
|
|
}}
|
|
>
|
|
{selectedPlan === props.currentPlan?.toLowerCase()
|
|
? props.isTrial
|
|
? "Upgrade"
|
|
: "Current Plan"
|
|
: selectedPlan === "starter"
|
|
? !props.currentPlan
|
|
? "Upgrade"
|
|
: "Downgrade"
|
|
: selectedPlan === "professional"
|
|
? "Upgrade"
|
|
: "Contact us"}
|
|
</Button>
|
|
{props.currentPlan && (
|
|
<Button
|
|
type="button"
|
|
variant="destructive"
|
|
className="w-full"
|
|
onClick={async () => {
|
|
await client.subscription.cancel({
|
|
returnUrl: "/dashboard",
|
|
});
|
|
}}
|
|
>
|
|
Cancel Plan
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|
|
|
|
export { Component };
|