-
Notifications
You must be signed in to change notification settings - Fork 191
feat(ui): add shared UpgradeToProDialog component #321
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,118 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Button } from "@/lib/components/button"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Dialog, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DialogContent, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DialogDescription, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DialogFooter, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DialogHeader, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DialogTitle, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DialogTrigger, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from "@/lib/components/dialog"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { useToast } from "@/lib/components/use-toast"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { $api } from "@/lib/fetch-client"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| interface UpgradeToProDialogProps { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| children: React.ReactNode; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSuccess?: () => void; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function UpgradeToProDialog({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| children, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSuccess, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }: UpgradeToProDialogProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [open, setOpen] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Dialog open={open} onOpenChange={setOpen}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <DialogTrigger asChild>{children}</DialogTrigger> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <DialogContent> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <UpgradeDialogContent | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSuccess={() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setOpen(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onSuccess?.(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </DialogContent> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Dialog> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function UpgradeDialogContent({ onSuccess }: { onSuccess: () => void }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { toast } = useToast(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [loading, setLoading] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const createSubscriptionMutation = $api.useMutation( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "post", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "/subscriptions/create-pro-subscription", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleUpgrade = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(true); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { checkoutUrl } = await createSubscriptionMutation.mutateAsync({}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Redirect to Stripe Checkout | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| window.location.href = checkoutUrl; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| toast({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| title: "Upgrade Failed", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| description: `Failed to create checkout session. Please try again. Error: ${error}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant: "destructive", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+60
to
+65
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improve error handling for better user experience The error object is directly interpolated into the error message string, which could result in Apply this diff to properly handle different error types: } catch (error) {
toast({
title: "Upgrade Failed",
- description: `Failed to create checkout session. Please try again. Error: ${error}`,
+ description: error instanceof Error
+ ? `Failed to create checkout session: ${error.message}`
+ : "Failed to create checkout session. Please try again.",
variant: "destructive",
});
setLoading(false);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+51
to
+67
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Prevent potential race conditions in mutation handling While the button is disabled during loading, there's no explicit check to prevent multiple concurrent mutation calls if the button is rapidly clicked or if the component state updates unexpectedly. Add a guard to prevent concurrent mutations: const handleUpgrade = async () => {
+ if (loading || createSubscriptionMutation.isPending) {
+ return;
+ }
+
setLoading(true);
try {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <DialogHeader> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <DialogTitle>Upgrade to Pro</DialogTitle> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <DialogDescription> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Unlock provider keys and get full access to all features for | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| $50/month. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </DialogDescription> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </DialogHeader> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="py-4"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="border rounded-lg p-4 space-y-3"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <h4 className="font-medium">What you'll get:</h4> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <ul className="space-y-2 text-sm"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <li className="flex items-center gap-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-2 h-2 rounded-full bg-green-500" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Use your own OpenAI, Anthropic, and other provider API keys | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <li className="flex items-center gap-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-2 h-2 rounded-full bg-green-500" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Hybrid mode: fallback to credits when needed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <li className="flex items-center gap-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-2 h-2 rounded-full bg-green-500" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| No surcharges or fees for API keys or credits usage | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <li className="flex items-center gap-2"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="w-2 h-2 rounded-full bg-green-500" /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| All existing features (credits, analytics, etc.) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </li> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </ul> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <DialogFooter> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex flex-col gap-2 items-end"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onClick={handleUpgrade} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| disabled={loading || createSubscriptionMutation.isPending} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {loading || createSubscriptionMutation.isPending | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ? "Redirecting to checkout..." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| : "Upgrade for $50/month now"} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="text-sm text-muted-foreground"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <p>You'll be redirected to Stripe Checkout to complete payment.</p> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </DialogFooter> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider calling onSuccess after successful subscription creation
The
onSuccesscallback is only triggered when the dialog is closed manually, not after a successful subscription creation. Since the component redirects to Stripe immediately after a successful mutation, theonSuccesscallback won't be executed in the typical success flow. This could lead to stale data in the parent component.Consider storing the
onSuccesscallback and executing it after a successful mutation, possibly with a delay before redirect:function UpgradeDialogContent({ onSuccess }: { onSuccess: () => void }) { const { toast } = useToast(); const [loading, setLoading] = useState(false); const createSubscriptionMutation = $api.useMutation( "post", "/subscriptions/create-pro-subscription", ); const handleUpgrade = async () => { setLoading(true); try { const { checkoutUrl } = await createSubscriptionMutation.mutateAsync({}); + // Call onSuccess before redirecting + onSuccess(); + // Redirect to Stripe Checkout window.location.href = checkoutUrl; } catch (error) {🤖 Prompt for AI Agents