|
1 | 1 | // src/pages/SettingsPage.tsx |
2 | | -import { useState } from 'react'; |
| 2 | +import { useEffect, useRef, useState } from 'react'; |
3 | 3 | import { useQuery } from '@tanstack/react-query'; |
4 | | -import { Link, useNavigate } from 'react-router-dom'; |
| 4 | +import { Link, useNavigate, useSearchParams } from 'react-router-dom'; |
5 | 5 | import { useTranslation } from 'react-i18next'; |
6 | 6 | import { Settings, Lock, Trash2, Crown, CreditCard, Zap, Shield, Download, UserX } from 'lucide-react'; |
7 | 7 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; |
@@ -33,6 +33,11 @@ import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, D |
33 | 33 | import { Input } from '@/components/ui/input'; |
34 | 34 | import { PageHeader } from '@/components/layout/PageHeader'; |
35 | 35 | import { APP_LANGUAGES, isAppLanguage } from '@/lib/languages'; |
| 36 | +import { |
| 37 | + CHECKOUT_CONFIRM_INTERVAL_MS, |
| 38 | + nextCheckoutConfirmation, |
| 39 | + stripCheckoutParams, |
| 40 | +} from '@/lib/billing/checkout-return'; |
36 | 41 |
|
37 | 42 |
|
38 | 43 | // Tinted badges keep their label readable (white on amber or green was about 2:1). |
@@ -62,6 +67,45 @@ export default function SettingsPage() { |
62 | 67 | const [deletingData, setDeletingData] = useState(false); |
63 | 68 | const [signingOut, setSigningOut] = useState(false); |
64 | 69 | const navigate = useNavigate(); |
| 70 | + const [searchParams, setSearchParams] = useSearchParams(); |
| 71 | + const checkout = searchParams.get('checkout'); |
| 72 | + const refreshSubscription = useRef(subscription.refresh); |
| 73 | + refreshSubscription.current = subscription.refresh; |
| 74 | + const checkoutStarted = useRef<number | null>(null); |
| 75 | + const checkoutSettled = useRef(false); |
| 76 | + const [checkoutPoll, setCheckoutPoll] = useState(0); |
| 77 | + const confirmingCheckout = checkout === 'success' |
| 78 | + && subscription.tier !== 'pro' |
| 79 | + && subscription.tier !== 'legacy_free'; |
| 80 | + |
| 81 | + useEffect(() => { |
| 82 | + const elapsed = checkoutStarted.current === null ? 0 : Date.now() - checkoutStarted.current; |
| 83 | + const action = nextCheckoutConfirmation({ |
| 84 | + checkout, |
| 85 | + tier: subscription.tier, |
| 86 | + elapsedMs: elapsed, |
| 87 | + loading: subscription.loading, |
| 88 | + }); |
| 89 | + if (action === 'ignore' || action === 'wait' || checkoutSettled.current) return; |
| 90 | + |
| 91 | + if (action === 'canceled' || action === 'confirmed' || action === 'slow') { |
| 92 | + checkoutSettled.current = true; |
| 93 | + const notice = action === 'canceled' |
| 94 | + ? { title: t('settings.checkoutCanceled'), description: t('settings.checkoutCanceledDesc') } |
| 95 | + : action === 'confirmed' |
| 96 | + ? { title: t('settings.checkoutConfirmed'), description: t('settings.checkoutConfirmedDesc') } |
| 97 | + : { title: t('settings.checkoutConfirmSlowTitle'), description: t('settings.checkoutConfirmSlow') }; |
| 98 | + toast(notice); |
| 99 | + setSearchParams(stripCheckoutParams(searchParams), { replace: true }); |
| 100 | + return; |
| 101 | + } |
| 102 | + |
| 103 | + if (checkoutStarted.current === null) checkoutStarted.current = Date.now(); |
| 104 | + const timer = window.setTimeout(() => { |
| 105 | + void refreshSubscription.current().finally(() => setCheckoutPoll((count) => count + 1)); |
| 106 | + }, CHECKOUT_CONFIRM_INTERVAL_MS); |
| 107 | + return () => window.clearTimeout(timer); |
| 108 | + }, [checkout, checkoutPoll, searchParams, setSearchParams, subscription.loading, subscription.tier, t, toast]); |
65 | 109 | // Must match deletionConfirmation in supabase/functions/account/index.ts. |
66 | 110 | const expectedAccountConfirmation = user?.email || 'DELETE'; |
67 | 111 |
|
@@ -199,8 +243,10 @@ export default function SettingsPage() { |
199 | 243 | </div> |
200 | 244 | </CardHeader> |
201 | 245 | <CardContent className="space-y-4"> |
202 | | - {subscription.loading ? ( |
203 | | - <p role="status" className="text-sm text-muted-foreground">{t('settings.loading')}</p> |
| 246 | + {subscription.loading || confirmingCheckout ? ( |
| 247 | + <p role="status" className="text-sm text-muted-foreground"> |
| 248 | + {confirmingCheckout ? t('settings.confirmingSubscription') : t('settings.loading')} |
| 249 | + </p> |
204 | 250 | ) : ( |
205 | 251 | <> |
206 | 252 | <div className="flex items-center justify-between"> |
|
0 commit comments