Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/web/client/src/components/ui/avatar-dropdown/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getInitials } from '@onlook/utility';
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import { useState } from 'react';
import { UsageSection } from './plans';
import { SettingsTabValue } from '../settings-modal/helpers';

export const CurrentUserAvatar = ({ className }: { className?: string }) => {
const stateManager = useStateManager();
Expand All @@ -40,11 +41,13 @@ export const CurrentUserAvatar = ({ className }: { className?: string }) => {
};

const handleOpenSubscription = () => {
stateManager.isSubscriptionModalOpen = true;
stateManager.settingsTab = SettingsTabValue.SUBSCRIPTION;
stateManager.isSettingsModalOpen = true;
setOpen(false);
};

const handleOpenSettings = () => {
stateManager.settingsTab = SettingsTabValue.PREFERENCES;
stateManager.isSettingsModalOpen = true;
setOpen(false);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export enum SettingsTabValue {
DOMAIN = 'domain',
PROJECT = 'project',
PREFERENCES = 'preferences',
SUBSCRIPTION = 'subscription',
VERSIONS = 'versions',
ADVANCED = 'advanced',
SITE = 'site',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AnimatePresence, motion } from 'framer-motion';
import { observer } from 'mobx-react-lite';
import { SettingsTabValue, type SettingTab } from './helpers';
import { PreferencesTab } from './preferences-tab';
import { SubscriptionTab } from './subscription-tab';

export const NonProjectSettingsModal = observer(() => {
const stateManager = useStateManager();
Expand All @@ -18,6 +19,11 @@ export const NonProjectSettingsModal = observer(() => {
icon: <Icons.Person className="mr-2 h-4 w-4" />,
component: <PreferencesTab />,
},
{
label: SettingsTabValue.SUBSCRIPTION,
icon: <Icons.CreditCard className="mr-2 h-4 w-4" />,
component: <SubscriptionTab />,
},
]

return (
Expand Down
182 changes: 182 additions & 0 deletions apps/web/client/src/components/ui/settings-modal/subscription-tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
'use client';

import { useStateManager } from '@/components/store/state';
import { api } from '@/trpc/react';
import { Button } from '@onlook/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@onlook/ui/dropdown-menu';
import { Icons } from '@onlook/ui/icons';
import { Separator } from '@onlook/ui/separator';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@onlook/ui/dialog';
import { observer } from 'mobx-react-lite';
import { useState } from 'react';
import { useSubscription } from '../pricing-modal/use-subscription';
import { ProductType, ScheduledSubscriptionAction } from '@onlook/stripe';

export const SubscriptionTab = observer(() => {
const stateManager = useStateManager();
const { data: user } = api.user.get.useQuery();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The 'user' data from the API query is fetched but never used. Consider removing it if not needed.

Suggested change
const { data: user } = api.user.get.useQuery();

const { subscription, isPro } = useSubscription();
const [showCancelModal, setShowCancelModal] = useState(false);
const [isManageDropdownOpen, setIsManageDropdownOpen] = useState(false);
const [isLoadingPortal, setIsLoadingPortal] = useState(false);

const manageSubscriptionMutation = api.subscription.manageSubscription.useMutation({
onSuccess: (session) => {
if (session?.url) {
window.open(session.url, '_blank');
}
},
onError: (error) => {
console.error('Failed to create portal session:', error);
},
onSettled: () => {
setIsLoadingPortal(false);
}
});

const handleUpgradePlan = () => {
stateManager.isSubscriptionModalOpen = true;
stateManager.isSettingsModalOpen = false;
setIsManageDropdownOpen(false);
};

const handleCancelSubscription = () => {
setShowCancelModal(true);
setIsManageDropdownOpen(false);
};

const handleConfirmCancel = () => {
// Cancellation logic will be implemented later
setShowCancelModal(false);
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subscription cancellation implementation is incomplete. The handleConfirmCancel function only closes the modal without actually canceling the subscription. This creates a misleading user experience where users might believe they've canceled their subscription when they haven't. Consider implementing the actual cancellation logic before shipping this feature, or clearly indicating to users that this functionality is coming soon if it must be shipped in this state.

Suggested change
const handleConfirmCancel = () => {
// Cancellation logic will be implemented later
setShowCancelModal(false);
};
const handleConfirmCancel = async () => {
try {
// Call the subscription cancellation API
await cancelUserSubscription();
// Show success message to user
toast.success("Your subscription has been successfully canceled");
// Close the modal
setShowCancelModal(false);
} catch (error) {
// Handle errors
console.error("Failed to cancel subscription:", error);
toast.error("Failed to cancel your subscription. Please try again or contact support.");
}
};

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.


const handleManageBilling = async () => {
if (isPro && subscription) {
setIsLoadingPortal(true);
await manageSubscriptionMutation.mutateAsync();
}
};

return (
<div className="flex flex-col p-8">
{/* Subscription Section */}
<div className="space-y-6">
<div>
<h2 className="text-title3 mb-2">Subscription</h2>
<p className="text-muted-foreground text-small">
Manage your subscription plan and billing
</p>
</div>

<div className="space-y-4">
<div className="flex items-center justify-between py-4">
<div className="space-y-1">
<p className="text-regularPlus font-medium">Current Plan</p>
<p className="text-small text-muted-foreground">
{isPro ? (
subscription?.scheduledChange?.scheduledAction === ScheduledSubscriptionAction.CANCELLATION ? (
<>Pro plan (cancelling on {subscription.scheduledChange.scheduledChangeAt.toLocaleDateString()})</>
Comment on lines +85 to +86
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential null pointer exception when accessing subscription.scheduledChange.scheduledChangeAt. If scheduledChange exists but scheduledChangeAt is null/undefined, calling toLocaleDateString() will throw a runtime error. Add null checking: subscription.scheduledChange?.scheduledChangeAt?.toLocaleDateString()

Suggested change
subscription?.scheduledChange?.scheduledAction === ScheduledSubscriptionAction.CANCELLATION ? (
<>Pro plan (cancelling on {subscription.scheduledChange.scheduledChangeAt.toLocaleDateString()})</>
subscription?.scheduledChange?.scheduledAction === ScheduledSubscriptionAction.CANCELLATION ? (
<>Pro plan (cancelling on {subscription.scheduledChange?.scheduledChangeAt?.toLocaleDateString()})</>

Spotted by Diamond

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

) : (
<>Pro plan - {subscription?.price?.monthlyMessageLimit || 'Unlimited'} messages per month</>
)
) : (
'You are currently on the Free plan'
)}
</p>
</div>
<DropdownMenu open={isManageDropdownOpen} onOpenChange={setIsManageDropdownOpen}>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm">
Manage
<Icons.ChevronDown className="ml-1 h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
{!isPro && (
<DropdownMenuItem
onClick={handleUpgradePlan}
className="cursor-pointer"
>
<Icons.Sparkles className="mr-2 h-4 w-4" />
Upgrade plan
</DropdownMenuItem>
)}
{isPro && subscription?.scheduledChange?.scheduledAction !== ScheduledSubscriptionAction.CANCELLATION && (
<DropdownMenuItem
onClick={handleUpgradePlan}
className="cursor-pointer"
>
<Icons.Sparkles className="mr-2 h-4 w-4" />
Change plan
</DropdownMenuItem>
)}
{isPro && (
<DropdownMenuItem
onClick={handleCancelSubscription}
className="cursor-pointer text-red-200 hover:text-red-100 group"
>
<Icons.CrossS className="mr-2 h-4 w-4 text-red-200 group-hover:text-red-100" />
{subscription?.scheduledChange?.scheduledAction === ScheduledSubscriptionAction.CANCELLATION ? 'Reactivate subscription' : 'Cancel subscription'}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
</div>

<Separator />

{/* Payment Section */}
<div className="flex items-center justify-between py-4">
<div className="space-y-1">
<p className="text-regularPlus font-medium">Payment</p>
<p className="text-small text-muted-foreground">
Manage your payment methods and billing details
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleManageBilling}
disabled={isLoadingPortal || !isPro}
>
{isLoadingPortal ? 'Opening...' : 'Manage'}
</Button>
</div>
</div>
</div>

{/* Cancel Subscription Modal */}
<Dialog open={showCancelModal} onOpenChange={setShowCancelModal}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Cancel Subscription</DialogTitle>
<DialogDescription className="pt-2">
Are you sure you want to cancel your subscription? You'll lose access to all premium features at the end of your current billing period.
</DialogDescription>
</DialogHeader>
<DialogFooter className="flex-col sm:flex-row gap-3 sm:gap-2">
<Button
variant="outline"
onClick={() => setShowCancelModal(false)}
className="order-2 sm:order-1"
>
Keep Subscription
</Button>
<Button
variant="outline"
onClick={handleConfirmCancel}
className="order-1 sm:order-2 text-red-200 hover:text-red-100 hover:bg-red-500/10 border-red-200 hover:border-red-100"
>
Cancel Subscription
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import DomainTab from './domain';
import { SettingsTabValue, type SettingTab } from './helpers';
import { PreferencesTab } from './preferences-tab';
import { SubscriptionTab } from './subscription-tab';
import { ProjectTab } from './project';
import { SiteTab } from './site';
import { VersionsTab } from './versions';
Expand Down Expand Up @@ -68,6 +69,11 @@ export const SettingsModalWithProjects = observer(() => {
icon: <Icons.Person className="mr-2 h-4 w-4" />,
component: <PreferencesTab />,
},
{
label: SettingsTabValue.SUBSCRIPTION,
icon: <Icons.CreditCard className="mr-2 h-4 w-4" />,
component: <SubscriptionTab />,
},
];

const projectTabs: SettingTab[] = [
Expand Down