Created
February 21, 2025 19:29
-
-
Save mmikhan/7836ddf3e5c9c612da0f2042f9dd0162 to your computer and use it in GitHub Desktop.
Stripe plan upgrade downgrade dialog with Hook form implementation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use client"; | |
import { Button } from "@/components/ui/button"; | |
import { | |
Dialog, | |
DialogContent, | |
DialogDescription, | |
DialogTitle, | |
DialogTrigger, | |
} from "@/components/ui/dialog"; | |
import { Form } from "@/components/ui/form"; | |
import { Label } from "@/components/ui/label"; | |
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; | |
import { ToastAction } from "@/components/ui/toast"; | |
import { toast } from "@/hooks/use-toast"; | |
import { api } from "@/trpc/react"; | |
import { TRPCClientError } from "@trpc/client"; | |
import { Loader2, Package } from "lucide-react"; | |
import { useRouter } from "next/navigation"; | |
import { useState } from "react"; | |
import { useForm } from "react-hook-form"; | |
interface FormValues { | |
productId: string; | |
} | |
export default function PlanUpgradeDowngradeDialog() { | |
const form = useForm<FormValues>(); | |
const utils = api.useUtils(); | |
const router = useRouter(); | |
const [open, setOpen] = useState(false); | |
const { data: products, isLoading: isLoadingProducts } = | |
api.products.active.useQuery(); | |
const { data: currentBilling, isLoading: isLoadingBilling } = | |
api.payments.getCurrentBilling.useQuery(); | |
const { mutate: changePlan, isPending: isChangingPlan } = | |
api.payments.changePlan.useMutation({ | |
onSuccess: async () => { | |
setOpen(false); | |
await utils.payments.getCurrentBilling.invalidate(); | |
toast({ | |
title: "Success", | |
description: "Your plan has been updated successfully.", | |
}); | |
}, | |
onError: (error) => { | |
if (error instanceof TRPCClientError) { | |
toast({ | |
title: "Error", | |
description: error.message, | |
variant: "destructive", | |
action: <ToastAction altText="Try again">Try again</ToastAction>, | |
}); | |
} | |
}, | |
}); | |
const { mutate: checkout, isPending: isCheckingOut } = | |
api.payments.createCheckout.useMutation({ | |
onSuccess: async ({ url, type }) => { | |
if (type === "free") { | |
await utils.payments.getCurrentBilling.invalidate(); | |
setOpen(false); | |
return; | |
} | |
router.push(url); | |
}, | |
onError: (error) => { | |
if (error instanceof TRPCClientError) { | |
toast({ | |
title: "Error", | |
description: error.message, | |
variant: "destructive", | |
action: <ToastAction altText="Try again">Try again</ToastAction>, | |
}); | |
} | |
}, | |
}); | |
const onSubmit = (formData: FormValues) => { | |
if ( | |
!currentBilling || | |
currentBilling.product?.isFree || | |
!currentBilling.providerId || | |
products?.find((p) => p.id === formData.productId)?.isFree | |
) { | |
checkout({ productId: formData.productId }); | |
return; | |
} | |
changePlan({ | |
newProductId: formData.productId, | |
subscriptionId: currentBilling.providerId, | |
}); | |
}; | |
return ( | |
<Dialog open={open} onOpenChange={setOpen}> | |
<DialogTrigger asChild> | |
<Button | |
variant="outline" | |
size="sm" | |
disabled={isLoadingProducts || isLoadingBilling} | |
> | |
{isLoadingProducts || isLoadingBilling ? ( | |
<Loader2 className="h-4 w-4 animate-spin" /> | |
) : !currentBilling?.providerId ? ( | |
"Upgrade Plan" | |
) : ( | |
"Change Plan" | |
)} | |
</Button> | |
</DialogTrigger> | |
<DialogContent> | |
<DialogTitle>Change Your Plan</DialogTitle> | |
<DialogDescription> | |
Select a new plan to upgrade or downgrade your current subscription. | |
</DialogDescription> | |
<Form {...form}> | |
<form | |
onSubmit={form.handleSubmit(onSubmit)} | |
className="space-y-4 py-4" | |
> | |
{!products?.length ? ( | |
<div className="flex flex-col items-center justify-center rounded-md border border-dashed p-8 text-center"> | |
<Package className="h-10 w-10 text-muted-foreground" /> | |
<h3 className="mt-4 font-semibold">No Plans Available</h3> | |
<p className="mt-2 text-sm text-muted-foreground"> | |
There are currently no plans available for subscription. | |
</p> | |
</div> | |
) : ( | |
<RadioGroup | |
defaultValue={currentBilling?.productId} | |
className="space-y-4" | |
> | |
{products.map((product) => ( | |
<div | |
key={product.id} | |
className={`rounded-lg border p-4 transition-colors ${ | |
form.getValues().productId === product.id | |
? "border-primary bg-primary/5" | |
: "hover:border-primary/50" | |
} ${ | |
currentBilling?.productId === product.id | |
? "border-primary/50 bg-muted" | |
: "" | |
}`} | |
> | |
<div className="flex items-center space-x-4"> | |
<RadioGroupItem | |
value={product.id} | |
id={product.id} | |
{...form.register("productId")} | |
/> | |
<Label | |
htmlFor={product.id} | |
className="flex flex-1 cursor-pointer items-center justify-between" | |
> | |
<div> | |
<h3 className="font-medium">{product.name}</h3> | |
<p className="text-sm text-muted-foreground"> | |
{product.description} | |
</p> | |
</div> | |
<div className="text-right"> | |
<p className="font-medium"> | |
${Number(product.price).toFixed(2)} | |
</p> | |
<p className="text-sm text-muted-foreground"> | |
per {product.type} | |
</p> | |
</div> | |
</Label> | |
</div> | |
{currentBilling?.productId === product.id && ( | |
<p className="mt-2 text-sm text-primary">Current Plan</p> | |
)} | |
</div> | |
))} | |
</RadioGroup> | |
)} | |
<div className="flex justify-end space-x-2"> | |
<Button | |
type="button" | |
size={"sm"} | |
variant="ghost" | |
onClick={() => setOpen(false)} | |
> | |
Cancel | |
</Button> | |
<Button | |
type="submit" | |
size={"sm"} | |
variant={"outline"} | |
disabled={ | |
isChangingPlan || | |
isCheckingOut || | |
!form.getValues().productId || | |
form.getValues().productId === currentBilling?.productId || | |
((products?.find((p) => p.id === form.getValues().productId) | |
?.isFree ?? | |
false) && | |
currentBilling?.status === "active") | |
} | |
> | |
{isChangingPlan || isCheckingOut ? ( | |
<> | |
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> | |
{!currentBilling?.providerId | |
? "Upgrading..." | |
: "Changing..."} | |
</> | |
) : !currentBilling?.providerId ? ( | |
"Upgrade" | |
) : ( | |
"Confirm Change" | |
)} | |
</Button> | |
</div> | |
</form> | |
</Form> | |
</DialogContent> | |
</Dialog> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment