Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
32 changes: 22 additions & 10 deletions src/components/features/subscription/LimitReachedModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useTranslation } from 'react-i18next';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { AlertTriangle, Zap, Clock } from 'lucide-react';
import { useSubscription } from '@/hooks/use-subscription';

interface LimitReachedModalProps {
open: boolean;
Expand All @@ -14,7 +15,10 @@ interface LimitReachedModalProps {

export function LimitReachedModal({ open, onOpenChange, variant, limit, current, onUpgrade }: LimitReachedModalProps) {
const { t } = useTranslation();
const { isPro } = useSubscription();
const isTrialExpired = variant === 'trial_expired';
// A Pro student who used today's included requests already pays. Selling Pro again is the wrong next step.
const paidDailyCap = isPro && !isTrialExpired;

return (
<Dialog open={open} onOpenChange={onOpenChange}>
Expand All @@ -38,16 +42,24 @@ export function LimitReachedModal({ open, onOpenChange, variant, limit, current,
</DialogHeader>

<div className="flex flex-col gap-2 mt-4">
<Button onClick={onUpgrade} size="lg" className="w-full">
<Zap className="h-4 w-4 mr-2" />
{t('limitModal.upgrade')}
</Button>
<Button variant="ghost" className="w-full" onClick={() => onOpenChange(false)}>
{t('limitModal.notNow', 'Not now')}
</Button>
<p className="text-xs text-muted-foreground text-center mt-1">
{t('limitModal.upgradeNote')}
</p>
{paidDailyCap ? (
<Button onClick={() => onOpenChange(false)} size="lg" className="w-full">
{t('limitModal.gotIt')}
</Button>
) : (
<>
<Button onClick={onUpgrade} size="lg" className="w-full">
<Zap className="h-4 w-4 mr-2" />
{t('limitModal.upgrade')}
</Button>
<Button variant="ghost" className="w-full" onClick={() => onOpenChange(false)}>
{t('limitModal.notNow', 'Not now')}
</Button>
<p className="text-xs text-muted-foreground text-center mt-1">
{t('limitModal.upgradeNote')}
</p>
</>
)}
</div>
</DialogContent>
</Dialog>
Expand Down
3 changes: 2 additions & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1332,7 +1332,8 @@
"limitReachedDesc": "You've used {{current}} of {{limit}} included AI uses today. Access refreshes at midnight UTC.",
"upgrade": "Upgrade to Pro - $5/mo",
"upgradeNote": "More AI prep access + high reasoning",
"notNow": "Not now"
"notNow": "Not now",
"gotIt": "OK"
},
"modelModal": {
"title": "Pro Reasoning",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -1332,7 +1332,8 @@
"limitReachedDesc": "Has usado {{current}} de {{limit}} usos de IA incluidos hoy. El acceso se renueva a medianoche UTC.",
"upgrade": "Actualizar a Pro - $5/mes",
"upgradeNote": "Más acceso a preparación con IA + razonamiento alto",
"notNow": "Ahora no"
"notNow": "Ahora no",
"gotIt": "Entendido"
},
"modelModal": {
"title": "Razonamiento Pro",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/hu.json
Original file line number Diff line number Diff line change
Expand Up @@ -1332,7 +1332,8 @@
"limitReachedDesc": "Ma {{current}} / {{limit}} benne foglalt MI használatot használtál fel. A hozzáférés éjfélkor (UTC) frissül.",
"upgrade": "Frissítés Pro-ra - $5/hó",
"upgradeNote": "Több MI felkészülési hozzáférés + magas szintű érvelés",
"notNow": "Most nem"
"notNow": "Most nem",
"gotIt": "Rendben"
},
"modelModal": {
"title": "Pro érvelés",
Expand Down
52 changes: 52 additions & 0 deletions src/test/components/LimitReachedModal.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { LimitReachedModal } from '@/components/features/subscription/LimitReachedModal';

const subscription = vi.hoisted(() => ({ isPro: false }));

vi.mock('@/hooks/use-subscription', () => ({
useSubscription: () => ({ isPro: subscription.isPro, loading: false, tier: subscription.isPro ? 'pro' : 'trial' }),
}));

function renderModal(variant: 'limit_reached' | 'trial_expired' = 'limit_reached') {
const onUpgrade = vi.fn();
const onOpenChange = vi.fn();
render(
<LimitReachedModal
open
variant={variant}
limit={50}
current={50}
onUpgrade={onUpgrade}
onOpenChange={onOpenChange}
/>,
);
return { onUpgrade, onOpenChange };
}

describe('LimitReachedModal', () => {
beforeEach(() => {
subscription.isPro = false;
});

it('offers Pro when a free plan hits the daily cap', () => {
renderModal();
expect(screen.getByRole('button', { name: 'Upgrade to Pro - $5/mo' })).toBeInTheDocument();
});

it('does not sell Pro again when a paying student hits the daily cap', async () => {
subscription.isPro = true;
const { onUpgrade, onOpenChange } = renderModal();
expect(screen.queryByRole('button', { name: 'Upgrade to Pro - $5/mo' })).not.toBeInTheDocument();
expect(screen.queryByText('More AI prep access + high reasoning')).not.toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'OK' }));
expect(onOpenChange).toHaveBeenCalledWith(false);
expect(onUpgrade).not.toHaveBeenCalled();
});

it('still offers Pro when the trial has ended', () => {
subscription.isPro = false;
renderModal('trial_expired');
expect(screen.getByRole('button', { name: 'Upgrade to Pro - $5/mo' })).toBeInTheDocument();
});
});
Loading