billing modal

This commit is contained in:
marko-kraemer 2025-08-25 16:00:11 -07:00
parent ad48d9ebe8
commit 008c8944bc
8 changed files with 265 additions and 251 deletions

View File

@ -1,7 +1,7 @@
import React from 'react';
import { useRouter } from 'next/navigation';
import React, { useState } from 'react';
import { Brain, Clock, Crown, Sparkles, Zap } from 'lucide-react';
import { UpgradeDialog as UnifiedUpgradeDialog } from '@/components/ui/upgrade-dialog';
import { BillingModal } from '@/components/billing/billing-modal';
interface UpgradeDialogProps {
open: boolean;
@ -10,15 +10,25 @@ interface UpgradeDialogProps {
}
export function UpgradeDialog({ open, onOpenChange, onDismiss }: UpgradeDialogProps) {
const router = useRouter();
const [showBillingModal, setShowBillingModal] = useState(false);
const handleUpgradeClick = () => {
router.push('/settings/billing');
// Close the upgrade dialog and open the billing modal
onOpenChange(false);
setShowBillingModal(true);
localStorage.setItem('suna_upgrade_dialog_displayed', 'true');
};
const handleBillingModalClose = (isOpen: boolean) => {
setShowBillingModal(isOpen);
if (!isOpen) {
// If billing modal is closed, we can consider the upgrade flow complete
onDismiss();
}
};
return (
<>
<UnifiedUpgradeDialog
open={open}
onOpenChange={onOpenChange}
@ -77,5 +87,14 @@ export function UpgradeDialog({ open, onOpenChange, onDismiss }: UpgradeDialogPr
</div>
</div>
</UnifiedUpgradeDialog>
{/* Billing Modal */}
<BillingModal
open={showBillingModal}
onOpenChange={handleBillingModalClose}
returnUrl={typeof window !== 'undefined' ? window?.location?.href || '/' : '/'}
showUsageLimitAlert={true}
/>
</>
);
}

View File

@ -11,6 +11,7 @@ import { createClient } from '@/lib/supabase/client';
import { User, Session } from '@supabase/supabase-js';
import { SupabaseClient } from '@supabase/supabase-js';
import { checkAndInstallSunaAgent } from '@/lib/utils/install-suna-agent';
import { clearUserLocalStorage } from '@/lib/utils/clear-local-storage';
type AuthContextType = {
supabase: SupabaseClient;
@ -57,6 +58,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
}
break;
case 'SIGNED_OUT':
// Clear local storage when user is signed out (handles all logout scenarios)
clearUserLocalStorage();
break;
case 'TOKEN_REFRESHED':
break;
@ -75,6 +78,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
const signOut = async () => {
try {
await supabase.auth.signOut();
// Clear local storage after successful sign out
clearUserLocalStorage();
} catch (error) {
console.error('❌ Error signing out:', error);
}

View File

@ -13,6 +13,7 @@ import {
useUnenrollFactor,
} from '@/hooks/react-query/phone-verification';
import { signOut } from '@/app/auth/actions';
import { clearUserLocalStorage } from '@/lib/utils/clear-local-storage';
import { Button } from '@/components/ui/button';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { LogOut, Loader2 } from 'lucide-react';
@ -194,6 +195,8 @@ export function PhoneVerificationPage({
const signOutMutation = useMutation({
mutationFn: async () => {
// Clear local storage before sign out
clearUserLocalStorage();
await signOut().catch(() => void 0);
window.location.href = '/';
},

View File

@ -14,6 +14,7 @@ import Link from 'next/link';
import { UserIcon } from 'lucide-react';
import { signOut } from '@/app/auth/actions';
import { useRouter } from 'next/navigation';
import { clearUserLocalStorage } from '@/lib/utils/clear-local-storage';
interface ClientUserAccountButtonProps {
userName?: string;
@ -27,6 +28,8 @@ export default function ClientUserAccountButton({
const router = useRouter();
const handleSignOut = async () => {
// Clear local storage before sign out
clearUserLocalStorage();
await signOut();
router.refresh();
};

View File

@ -237,17 +237,6 @@ export function BillingModal({ open, onOpenChange, returnUrl = typeof window !==
<DialogTitle>Upgrade Your Plan</DialogTitle>
</DialogHeader>
{isLoading || authLoading ? (
<div className="space-y-4">
<Skeleton className="h-20 w-full" />
<Skeleton className="h-40 w-full" />
<Skeleton className="h-10 w-full" />
</div>
) : error ? (
<div className="p-4 bg-destructive/10 border border-destructive/20 rounded-lg text-center">
<p className="text-sm text-destructive">Error loading billing status: {error}</p>
</div>
) : (
<>
{/* Usage Limit Alert */}
{showUsageLimitAlert && (
@ -268,7 +257,17 @@ export function BillingModal({ open, onOpenChange, returnUrl = typeof window !==
</div>
)}
{subscriptionData && (
{/* Usage section - show loading state or actual data */}
{isLoading || authLoading ? (
<div className="mb-6">
<div className="rounded-lg border bg-background p-4">
<div className="flex justify-between items-center">
<Skeleton className="h-4 w-40" />
<Skeleton className="h-4 w-24" />
</div>
</div>
</div>
) : subscriptionData && (
<div className="mb-6">
<div className="rounded-lg border bg-background p-4">
<div className="flex justify-between items-center">
@ -284,33 +283,29 @@ export function BillingModal({ open, onOpenChange, returnUrl = typeof window !==
</div>
)}
{/* Credit Balance Display - Only show for users who can purchase credits
{subscriptionData?.can_purchase_credits && (
<div className="mb-6">
<CreditBalanceDisplay
balance={subscriptionData.credit_balance || 0}
canPurchase={subscriptionData.can_purchase_credits}
onPurchaseClick={() => setShowCreditPurchaseModal(true)}
/>
</div>
)} */}
{/* Show pricing section immediately - no loading state */}
<PricingSection returnUrl={returnUrl} showTitleAndTabs={false} />
{/* Minimalistic Subscription Management Section */}
{subscriptionData?.subscription && (
<div className="mt-6 pt-6 border-t border-border">
<div className="space-y-3">
{/* Subscription Status Row */}
{/* Subscription Management Section - only show if there's actual subscription data */}
{error ? (
<div className="mt-6 pt-4 border-t border-border">
<div className="p-4 bg-destructive/10 border border-destructive/20 rounded-lg text-center">
<p className="text-sm text-destructive">Error loading billing status: {error}</p>
</div>
</div>
) : subscriptionData?.subscription && (
<div className="mt-6 pt-4 border-t border-border">
{/* Subscription Status Info Box */}
<div className="bg-muted/30 border border-border rounded-lg p-3 mb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-muted-foreground">
<span className="text-xs font-medium">
{subscriptionData.subscription.cancel_at_period_end || subscriptionData.subscription.cancel_at
? 'Plan Status'
: 'Current Plan'}
</span>
{commitmentInfo?.has_commitment && (
<Badge variant="outline" className="text-xs px-2 py-0">
<Badge variant="outline" className="text-xs px-1.5 py-0">
{commitmentInfo.months_remaining || 0}mo left
</Badge>
)}
@ -319,7 +314,7 @@ export function BillingModal({ open, onOpenChange, returnUrl = typeof window !==
subscriptionData.subscription.cancel_at_period_end || subscriptionData.subscription.cancel_at
? 'destructive'
: 'secondary'
} className="text-xs">
} className="text-xs px-2 py-0.5">
{subscriptionData.subscription.cancel_at_period_end || subscriptionData.subscription.cancel_at
? 'Ending ' + getEffectiveCancellationDate()
: 'Active'}
@ -328,19 +323,20 @@ export function BillingModal({ open, onOpenChange, returnUrl = typeof window !==
{/* Cancellation Alert */}
{(subscriptionData.subscription.cancel_at_period_end || subscriptionData.subscription.cancel_at) && (
<Alert className="py-2 border-destructive/30 bg-destructive/5">
<AlertTriangle className="h-4 w-4 text-destructive" />
<AlertDescription className="text-xs text-destructive">
<div className="mt-2 flex items-start gap-2 p-2 bg-destructive/5 border border-destructive/20 rounded">
<AlertTriangle className="h-3 w-3 text-destructive mt-0.5 flex-shrink-0" />
<p className="text-xs text-destructive">
{subscriptionData.subscription.cancel_at ?
'Your plan is scheduled to end at commitment completion. You can reactivate anytime.' :
'Your plan is scheduled to end at period completion. You can reactivate anytime.'
}
</AlertDescription>
</Alert>
</p>
</div>
)}
</div>
{/* Action Buttons */}
<div className="flex gap-2">
{/* Action Buttons underneath */}
<div className="flex gap-2 justify-center">
{/* Cancel/Reactivate Button */}
{!(subscriptionData.subscription.cancel_at_period_end || subscriptionData.subscription.cancel_at) ? (
<Dialog open={showCancelDialog} onOpenChange={setShowCancelDialog}>
@ -439,21 +435,8 @@ export function BillingModal({ open, onOpenChange, returnUrl = typeof window !==
</Button>
</div>
</div>
</div>
)}
{/* Legacy Manage Button for non-subscription users */}
{subscriptionData && !subscriptionData.subscription && (
<Button
onClick={handleManageSubscription}
disabled={isManaging}
className="max-w-xs mx-auto w-full bg-primary hover:bg-primary/90 shadow-md hover:shadow-lg transition-all mt-4"
>
{isManaging ? 'Loading...' : 'Manage Subscription'}
</Button>
)}
</>
)}
</DialogContent>
{/* Credit Purchase Modal */}

View File

@ -53,6 +53,7 @@ import { createClient } from '@/lib/supabase/client';
import { useTheme } from 'next-themes';
import { isLocalMode } from '@/lib/config';
import { useFeatureFlag } from '@/lib/feature-flags';
import { clearUserLocalStorage } from '@/lib/utils/clear-local-storage';
export function NavUserWithTeams({
user,
@ -148,6 +149,8 @@ export function NavUserWithTeams({
const handleLogout = async () => {
const supabase = createClient();
await supabase.auth.signOut();
// Clear local storage after sign out
clearUserLocalStorage();
router.push('/auth');
};

View File

@ -84,7 +84,7 @@ export const UsagePreview: React.FC<UsagePreviewProps> = ({
<div className="flex-1 min-w-0">
<motion.div className="flex items-center gap-2 mb-1">
<h4 className="text-sm font-medium text-foreground truncate">
Upgrade for more usage & better AI Models
Upgrade for the best AI Models & more usage
</h4>
</motion.div>

View File

@ -5,11 +5,9 @@ export const clearUserLocalStorage = () => {
// Note: Preserve model preference on logout - user choice should persist
// localStorage.removeItem('suna-preferred-model-v3');
localStorage.removeItem('customModels');
localStorage.removeItem('suna-model-selection-v2');
localStorage.removeItem('agent-selection-storage');
localStorage.removeItem('auth-tracking-storage');
localStorage.removeItem('pendingAgentPrompt');
localStorage.removeItem('suna_upgrade_dialog_displayed');
Object.keys(localStorage).forEach(key => {