import HeadingSmall from '@/components/heading-small';
import { ProBadge } from '@/components/pro-badge';
import {
PlanCard,
UpgradeDialog,
} from '@/components/subscription/upgrade-dialog';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import AppLayout from '@/layouts/app-layout';
import SettingsLayout from '@/layouts/settings/layout';
import {
destroy as revokeConsent,
store as storeConsent,
} from '@/routes/ai/consent';
import { billing } from '@/routes/settings';
import { portal, refund as refundRoute } from '@/routes/settings/billing';
import { checkout } from '@/routes/subscribe';
import { type BreadcrumbItem, type SharedData } from '@/types';
import { Plan } from '@/types/pricing';
import { formatCurrency } from '@/utils/currency';
import { __ } from '@/utils/i18n';
import { Head, router, usePage } from '@inertiajs/react';
import axios from 'axios';
import {
CheckIcon,
CreditCardIcon,
InfinityIcon,
InfoIcon,
LandmarkIcon,
ShieldCheckIcon,
SparklesIcon,
ZapIcon,
} from 'lucide-react';
import { useState } from 'react';
import { toast } from 'sonner';
const breadcrumbs: BreadcrumbItem[] = [
{
title: 'Manage Plan',
href: billing().url,
},
];
const benefits = [
{
icon: LandmarkIcon,
title: __('Connected Bank Accounts'),
description: __(
'Automatically sync transactions directly from your bank. No manual imports needed.',
),
},
{
icon: InfinityIcon,
title: __('Unlimited Everything'),
description: __(
'No limits on bank accounts, transactions, or categories.',
),
},
{
icon: ShieldCheckIcon,
title: __('Privacy First'),
description: __(
'Your data is never shared with third parties. You are always the owner.',
),
},
{
icon: SparklesIcon,
title: __('Smart Automation'),
description: __(
'Automation rules to categorize transactions automatically.',
),
},
{
icon: CreditCardIcon,
title: __('Priority Support'),
description: __(
'Get help when you need it with priority email support.',
),
},
];
function BenefitsGrid() {
return (
{benefits.map((benefit) => (
{benefit.title}
{benefit.description}
))}
);
}
function UpgradeSection({
planEntries,
defaultPlan,
currency,
locale,
}: {
planEntries: [string, Plan][];
defaultPlan: string;
currency: string;
locale: string;
}) {
const [selectedPlan, setSelectedPlan] = useState(defaultPlan);
const selectedPlanData = planEntries.find(
([key]) => key === selectedPlan,
)?.[1];
return (
{__('Choose your billing cycle')}
{planEntries.map(([key, plan]) => (
setSelectedPlan(key)}
currency={currency}
locale={locale}
/>
))}
{selectedPlanData && selectedPlanData.features.length > 0 && (
{selectedPlanData.features
.slice(0, 4)
.map((feature) => (
{__(feature)}
))}
)}
{__('Upgrade to Standard Plan')}
);
}
interface RefundInfo {
canSelfRefund: boolean;
deadline: string | null;
}
function RefundCard({
deadline,
locale,
}: {
deadline: string | null;
locale: string;
}) {
const [confirming, setConfirming] = useState(false);
const [processing, setProcessing] = useState(false);
const deadlineLabel = deadline
? new Date(deadline).toLocaleDateString(locale, {
day: 'numeric',
month: 'long',
})
: null;
const submit = () => {
setProcessing(true);
router.post(
refundRoute.url(),
{},
{ onFinish: () => setProcessing(false) },
);
};
return (
{__('Money-back guarantee')}
{deadlineLabel
? __(
'Changed your mind? Request a full refund until :date. This cancels your subscription and disconnects your bank accounts — the data you already imported is kept.',
{ date: deadlineLabel },
)
: __(
'Changed your mind? Request a full refund. This cancels your subscription and disconnects your bank accounts — the data you already imported is kept.',
)}
{confirming ? (
{__('Confirm refund')}
setConfirming(false)}
>
{__('Keep my plan')}
) : (
setConfirming(true)}
>
{__('Request a refund')}
)}
);
}
function SubscribedSection({
isDemoAccount,
defaultPlan,
currency,
locale,
refund,
}: {
isDemoAccount: boolean;
defaultPlan: Plan | undefined;
currency: string;
locale: string;
refund?: RefundInfo;
}) {
return (
{isDemoAccount && (
{__(
'Billing management is not available on the demo account.',
)}
)}
{__('Pro Plan Active')}
{defaultPlan && (
{`\u2014 ${formatCurrency(defaultPlan.price * 100, currency, locale)}/${defaultPlan.billing_period}`}
)}
{__(
'Manage your subscription, update payment methods, or view invoices through the Stripe billing portal.',
)}
{!isDemoAccount && (
{__('Manage Subscription')}
)}
{refund?.canSelfRefund && (
)}
);
}
function AiConsentSection({
initialConsent,
hasProPlan,
onUpgradeNeeded,
}: {
initialConsent: boolean;
hasProPlan: boolean;
onUpgradeNeeded: () => void;
}) {
const [consented, setConsented] = useState(initialConsent);
const [saving, setSaving] = useState(false);
const handleToggle = async (checked: boolean) => {
// Free users can't enable AI directly — it needs a paid plan. Prompt
// them to subscribe instead of recording consent (which would silently
// lock them behind the paywall on the next navigation).
if (checked && !hasProPlan) {
onUpgradeNeeded();
return;
}
setSaving(true);
try {
if (checked) {
await axios.post(storeConsent.url());
} else {
await axios.delete(revokeConsent.url());
}
setConsented(checked);
toast.success(
checked
? __('AI categorization enabled')
: __('AI categorization disabled'),
);
} catch {
toast.error(__('Something went wrong.'));
} finally {
setSaving(false);
}
};
return (
handleToggle(checked === true)
}
className="mt-0.5"
/>
{__('Allow AI categorization')}
{__(
'With your permission, we send merchant names from your transactions to our AI provider so it can suggest categories. You can revoke this at any time.',
)}
);
}
export default function Billing() {
const { auth, pricing, locale, hasAiConsent, refund } = usePage<
SharedData & { hasAiConsent: boolean; refund?: RefundInfo }
>().props;
const isDemoAccount = auth?.isDemoAccount ?? false;
const hasProPlan = auth?.hasProPlan ?? false;
const planEntries = Object.entries(pricing.plans);
const defaultPlan = pricing.plans[pricing.defaultPlan];
const [showAiUpgrade, setShowAiUpgrade] = useState(false);
return (
{hasProPlan ? (
) : (
)}
setShowAiUpgrade(true)}
/>
{!hasProPlan && (
)}
);
}