fix: platform-aware delete confirmation + trial CTA to purchase

Delete subscription:
- Telegram mini app: native destructive popup (red button)
- Web (mobile + desktop): Sheet bottom panel with full-width buttons
- Both: proper loading state on delete button

Trial CTA: leads to /subscription/purchase (trial cannot be renewed)
This commit is contained in:
c0mrade
2026-03-24 18:30:59 +03:00
parent 23edd6a211
commit debee7729f
2 changed files with 68 additions and 33 deletions

View File

@@ -38,9 +38,12 @@ export default function PurchaseCTAButton({
? t('subscription.cta.renewHint', 'Продление подписки') ? t('subscription.cta.renewHint', 'Продление подписки')
: t('subscription.cta.activeHint'); : t('subscription.cta.activeHint');
// In multi-tariff mode: renew goes to per-subscription renew page // Trial → purchase page (buy a real tariff, trial can't be renewed)
const linkTo = // Multi-tariff active → per-subscription renew page
isMultiTariff && subscription?.id // Otherwise → purchase page
const linkTo = isTrial
? '/subscription/purchase'
: isMultiTariff && subscription?.id
? `/subscriptions/${subscription.id}/renew` ? `/subscriptions/${subscription.id}/renew`
: '/subscription/purchase'; : '/subscription/purchase';

View File

@@ -5,13 +5,15 @@ import { useNavigate, useParams } from 'react-router';
import { subscriptionApi } from '../api/subscription'; import { subscriptionApi } from '../api/subscription';
import { WebBackButton } from '../components/WebBackButton'; import { WebBackButton } from '../components/WebBackButton';
import { import {
Dialog, Sheet,
DialogContent, SheetContent,
DialogDescription, SheetDescription,
DialogFooter, SheetFooter,
DialogHeader, SheetHeader,
DialogTitle, SheetTitle,
} from '../components/primitives/Dialog'; } from '../components/primitives/Sheet';
import { useDestructiveConfirm } from '../platform/hooks/useNativeDialog';
import { usePlatform } from '../platform';
import TrafficProgressBar from '../components/dashboard/TrafficProgressBar'; import TrafficProgressBar from '../components/dashboard/TrafficProgressBar';
import { HoverBorderGradient } from '../components/ui/hover-border-gradient'; import { HoverBorderGradient } from '../components/ui/hover-border-gradient';
import { useTrafficZone } from '../hooks/useTrafficZone'; import { useTrafficZone } from '../hooks/useTrafficZone';
@@ -183,8 +185,10 @@ export default function Subscription() {
const g = getGlassColors(isDark); const g = getGlassColors(isDark);
const haptic = useHaptic(); const haptic = useHaptic();
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteSheet, setShowDeleteSheet] = useState(false);
const [deleteLoading, setDeleteLoading] = useState(false); const [deleteLoading, setDeleteLoading] = useState(false);
const { platform } = usePlatform();
const destructiveConfirm = useDestructiveConfirm();
// Helper to format price from kopeks // Helper to format price from kopeks
const formatPrice = (kopeks: number) => `${formatAmount(kopeks / 100)} ${currencySymbol}`; const formatPrice = (kopeks: number) => `${formatAmount(kopeks / 100)} ${currencySymbol}`;
@@ -1278,8 +1282,33 @@ export default function Subscription() {
{isMultiTariff && subscription && !subscription.is_active && !subscription.is_trial && ( {isMultiTariff && subscription && !subscription.is_active && !subscription.is_trial && (
<> <>
<button <button
onClick={() => setShowDeleteConfirm(true)} onClick={async () => {
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-red-400/20 bg-red-400/5 p-3.5 text-sm font-medium text-red-400 transition-colors hover:bg-red-400/10" if (platform === 'telegram') {
// Telegram: native destructive popup
const confirmed = await destructiveConfirm(
t(
'subscription.deleteWarning',
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны.',
),
t('subscription.confirmDelete', 'Да, удалить'),
t('subscription.deleteTitle', 'Удалить подписку?'),
);
if (!confirmed) return;
setDeleteLoading(true);
try {
await subscriptionApi.deleteSubscription(subscription.id);
queryClient.invalidateQueries({ queryKey: ['subscriptions-list'] });
navigate('/subscriptions', { replace: true });
} catch {
setDeleteLoading(false);
}
} else {
// Web: show Sheet
setShowDeleteSheet(true);
}
}}
disabled={deleteLoading}
className="flex w-full items-center justify-center gap-2 rounded-2xl border border-red-400/20 bg-red-400/5 p-3.5 text-sm font-medium text-red-400 transition-colors hover:bg-red-400/10 disabled:opacity-50"
> >
<svg <svg
className="h-4 w-4" className="h-4 w-4"
@@ -1294,27 +1323,24 @@ export default function Subscription() {
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/> />
</svg> </svg>
{t('subscription.delete', 'Удалить подписку')} {deleteLoading
? t('common.processing', 'Удаление...')
: t('subscription.delete', 'Удалить подписку')}
</button> </button>
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}> {/* Web: Sheet confirmation */}
<DialogContent className="max-w-sm"> <Sheet open={showDeleteSheet} onOpenChange={setShowDeleteSheet}>
<DialogHeader> <SheetContent>
<DialogTitle>{t('subscription.deleteTitle', 'Удалить подписку?')}</DialogTitle> <SheetHeader>
<DialogDescription> <SheetTitle>{t('subscription.deleteTitle', 'Удалить подписку?')}</SheetTitle>
<SheetDescription>
{t( {t(
'subscription.deleteWarning', 'subscription.deleteWarning',
'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны. Это действие нельзя отменить.', 'Подписка будет удалена безвозвратно. Все данные, устройства и настройки будут потеряны. Это действие нельзя отменить.',
)} )}
</DialogDescription> </SheetDescription>
</DialogHeader> </SheetHeader>
<DialogFooter className="gap-2 sm:gap-0"> <SheetFooter>
<button
onClick={() => setShowDeleteConfirm(false)}
className="rounded-xl border border-dark-700 bg-dark-800 px-4 py-2.5 text-sm font-medium text-dark-300 transition-colors hover:bg-dark-700"
>
{t('common.cancel', 'Отмена')}
</button>
<button <button
onClick={async () => { onClick={async () => {
setDeleteLoading(true); setDeleteLoading(true);
@@ -1324,19 +1350,25 @@ export default function Subscription() {
navigate('/subscriptions', { replace: true }); navigate('/subscriptions', { replace: true });
} catch { } catch {
setDeleteLoading(false); setDeleteLoading(false);
setShowDeleteConfirm(false); setShowDeleteSheet(false);
} }
}} }}
disabled={deleteLoading} disabled={deleteLoading}
className="rounded-xl bg-red-500 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-red-600 disabled:opacity-50" className="w-full rounded-xl bg-red-500 py-3 text-sm font-semibold text-white transition-colors hover:bg-red-600 disabled:opacity-50"
> >
{deleteLoading {deleteLoading
? t('common.processing', 'Удаление...') ? t('common.processing', 'Удаление...')
: t('subscription.confirmDelete', 'Да, удалить')} : t('subscription.confirmDelete', 'Да, удалить')}
</button> </button>
</DialogFooter> <button
</DialogContent> onClick={() => setShowDeleteSheet(false)}
</Dialog> className="w-full rounded-xl border border-dark-700 bg-dark-800 py-3 text-sm font-medium text-dark-300 transition-colors hover:bg-dark-700"
>
{t('common.cancel', 'Отмена')}
</button>
</SheetFooter>
</SheetContent>
</Sheet>
</> </>
)} )}