mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-30 10:27:49 +00:00
refactor(purchase): extract SwitchTariffSheet from god page
Move the tariff-switch preview modal (137 lines of inline JSX + its previewTariffSwitch query + switchTariff mutation + the auto-scroll-into-view useEffect + the ref) into a self-contained sheet at src/components/subscription/sheets/SwitchTariffSheet.tsx. Parent keeps the trigger (switchTariffId selection state lives on the TariffPickerGrid card click) but no longer holds the query, the mutation, the ref, or the side-effect. The subscription_expired backend signal — which used to call three setters at parent level (setSelectedTariff, setSelectedTariffPeriod, setShowTariffPurchase) — now flows through a single onExpiredFallback(tariff) callback. SubscriptionPurchase.tsx: 2010 → 1841 lines (-169).
This commit is contained in:
239
src/components/subscription/sheets/SwitchTariffSheet.tsx
Normal file
239
src/components/subscription/sheets/SwitchTariffSheet.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { subscriptionApi } from '../../../api/subscription';
|
||||||
|
import { getErrorMessage } from '../../../utils/subscriptionHelpers';
|
||||||
|
import { useCurrency } from '../../../hooks/useCurrency';
|
||||||
|
import InsufficientBalancePrompt from '../../InsufficientBalancePrompt';
|
||||||
|
import type { Tariff } from '../../../types';
|
||||||
|
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
// SwitchTariffSheet
|
||||||
|
//
|
||||||
|
// Inline preview + confirm of a tariff switch on an existing
|
||||||
|
// subscription. Self-owns:
|
||||||
|
// - the previewTariffSwitch query
|
||||||
|
// - the switchTariff mutation
|
||||||
|
// - the auto-scroll-into-view effect
|
||||||
|
//
|
||||||
|
// The parent keeps the `switchTariffId` selection state because the
|
||||||
|
// trigger lives on a tariff card inside TariffPickerGrid. When the
|
||||||
|
// backend reports `subscription_expired` (the user's subscription
|
||||||
|
// lapsed while the modal was open), the sheet hands off to the
|
||||||
|
// parent via `onExpiredFallback(tariff)`, which opens the regular
|
||||||
|
// TariffPurchaseForm instead of attempting another switch.
|
||||||
|
// ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface SwitchTariffSheetProps {
|
||||||
|
open: boolean;
|
||||||
|
tariffId: number | null;
|
||||||
|
subscriptionId: number | undefined;
|
||||||
|
tariffs: Tariff[];
|
||||||
|
onClose: () => void;
|
||||||
|
onExpiredFallback: (tariff: Tariff) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SwitchTariffSheet({
|
||||||
|
open,
|
||||||
|
tariffId,
|
||||||
|
subscriptionId,
|
||||||
|
tariffs,
|
||||||
|
onClose,
|
||||||
|
onExpiredFallback,
|
||||||
|
}: SwitchTariffSheetProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { formatAmount, currencySymbol } = useCurrency();
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const formatPrice = (kopeks: number) =>
|
||||||
|
kopeks === 0
|
||||||
|
? t('subscription.free', 'Бесплатно')
|
||||||
|
: `${formatAmount(kopeks / 100)} ${currencySymbol}`;
|
||||||
|
|
||||||
|
const { data: switchPreview, isLoading: switchPreviewLoading } = useQuery({
|
||||||
|
queryKey: ['tariff-switch-preview', tariffId],
|
||||||
|
queryFn: () => subscriptionApi.previewTariffSwitch(tariffId!, subscriptionId),
|
||||||
|
enabled: !!tariffId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const switchMutation = useMutation({
|
||||||
|
mutationFn: (id: number) => subscriptionApi.switchTariff(id, subscriptionId),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['subscription', subscriptionId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['purchase-options', subscriptionId] });
|
||||||
|
onClose();
|
||||||
|
navigate('/subscriptions', { replace: true });
|
||||||
|
},
|
||||||
|
onError: (error: unknown) => {
|
||||||
|
// Backend signal: the subscription lapsed mid-flight. Hand the
|
||||||
|
// selected tariff back to the parent so it can open the regular
|
||||||
|
// purchase form instead.
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
const detail = error.response?.data?.detail;
|
||||||
|
if (
|
||||||
|
typeof detail === 'object' &&
|
||||||
|
detail?.error_code === 'subscription_expired' &&
|
||||||
|
detail?.use_purchase_flow === true
|
||||||
|
) {
|
||||||
|
const targetTariff = tariffs.find((tariff) => tariff.id === tariffId);
|
||||||
|
if (targetTariff) {
|
||||||
|
onClose();
|
||||||
|
onExpiredFallback(targetTariff);
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['purchase-options', subscriptionId] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Smoothly scroll the panel into view when opened.
|
||||||
|
useEffect(() => {
|
||||||
|
if (open && ref.current) {
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
ref.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
if (!open || !tariffId) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className="mb-6 space-y-4 rounded-xl bg-dark-800/50 p-5">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="font-medium text-dark-100">{t('subscription.switchTariff.title')}</h3>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-sm text-dark-400 hover:text-dark-200"
|
||||||
|
aria-label={t('common.close', 'Close')}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{switchPreviewLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-4">
|
||||||
|
<div className="h-6 w-6 animate-spin rounded-full border-2 border-accent-500 border-t-transparent" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
switchPreview &&
|
||||||
|
(() => {
|
||||||
|
const targetTariff = tariffs.find((tariff) => tariff.id === tariffId);
|
||||||
|
const dailyPrice =
|
||||||
|
targetTariff?.daily_price_kopeks ?? targetTariff?.price_per_day_kopeks ?? 0;
|
||||||
|
const isDailyTariff = dailyPrice > 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="space-y-2 text-sm">
|
||||||
|
<div className="flex justify-between text-dark-300">
|
||||||
|
<span>{t('subscription.switchTariff.currentTariff')}</span>
|
||||||
|
<span className="font-medium text-dark-100">
|
||||||
|
{switchPreview.current_tariff_name || '-'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-dark-300">
|
||||||
|
<span>{t('subscription.switchTariff.newTariff')}</span>
|
||||||
|
<span className="font-medium text-accent-400">
|
||||||
|
{switchPreview.new_tariff_name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-dark-300">
|
||||||
|
<span>{t('subscription.switchTariff.remainingDays')}</span>
|
||||||
|
<span>{switchPreview.remaining_days}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isDailyTariff && (
|
||||||
|
<div className="rounded-lg border border-accent-500/30 bg-accent-500/10 p-3 text-center">
|
||||||
|
<div className="text-sm text-dark-300">
|
||||||
|
{t('subscription.switchTariff.dailyPayment')}
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-bold text-accent-400">{formatPrice(dailyPrice)}</div>
|
||||||
|
<div className="mt-1 text-xs text-dark-400">
|
||||||
|
{t('subscription.switchTariff.dailyChargeDescription')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between border-t border-dark-700/50 pt-3">
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-dark-100">
|
||||||
|
{t('subscription.switchTariff.upgradeCost')}
|
||||||
|
</span>
|
||||||
|
{switchPreview.discount_percent && switchPreview.discount_percent > 0 && (
|
||||||
|
<span className="ml-2 inline-block rounded-full bg-success-500/20 px-2 py-0.5 text-xs font-medium text-success-400">
|
||||||
|
-{switchPreview.discount_percent}%
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
{switchPreview.discount_percent &&
|
||||||
|
switchPreview.discount_percent > 0 &&
|
||||||
|
switchPreview.base_upgrade_cost_kopeks &&
|
||||||
|
switchPreview.base_upgrade_cost_kopeks > 0 && (
|
||||||
|
<span className="mr-2 text-sm text-dark-500 line-through">
|
||||||
|
{formatPrice(switchPreview.base_upgrade_cost_kopeks)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`text-lg font-bold ${switchPreview.upgrade_cost_kopeks === 0 ? 'text-success-400' : 'text-accent-400'}`}
|
||||||
|
>
|
||||||
|
{switchPreview.upgrade_cost_kopeks > 0
|
||||||
|
? switchPreview.upgrade_cost_label
|
||||||
|
: t('subscription.switchTariff.free')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!switchPreview.has_enough_balance && switchPreview.upgrade_cost_kopeks > 0 && (
|
||||||
|
<InsufficientBalancePrompt
|
||||||
|
missingAmountKopeks={switchPreview.missing_amount_kopeks}
|
||||||
|
compact
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => switchMutation.mutate(tariffId)}
|
||||||
|
disabled={switchMutation.isPending || !switchPreview.can_switch}
|
||||||
|
className="btn-primary w-full py-2.5"
|
||||||
|
>
|
||||||
|
{switchMutation.isPending ? (
|
||||||
|
<span className="flex items-center justify-center gap-2">
|
||||||
|
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
t('subscription.switchTariff.switch')
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{switchMutation.isError &&
|
||||||
|
(() => {
|
||||||
|
// Suppress the toast when the subscription_expired
|
||||||
|
// fallback already triggered: the parent is now
|
||||||
|
// showing the regular purchase form, surfacing the
|
||||||
|
// raw axios message here would be misleading.
|
||||||
|
const detail =
|
||||||
|
switchMutation.error instanceof AxiosError
|
||||||
|
? switchMutation.error.response?.data?.detail
|
||||||
|
: null;
|
||||||
|
if (typeof detail === 'object' && detail?.error_code === 'subscription_expired') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="mt-3 text-center text-sm text-error-400">
|
||||||
|
{getErrorMessage(switchMutation.error)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ import { useState, useEffect, useMemo, useRef, useCallback } from 'react';
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate, useSearchParams } from 'react-router';
|
import { useNavigate, useSearchParams } from 'react-router';
|
||||||
import { AxiosError } from 'axios';
|
|
||||||
import { subscriptionApi } from '../api/subscription';
|
import { subscriptionApi } from '../api/subscription';
|
||||||
import { usePromoDiscount } from '../hooks/usePromoDiscount';
|
import { usePromoDiscount } from '../hooks/usePromoDiscount';
|
||||||
import { WebBackButton } from '../components/WebBackButton';
|
import { WebBackButton } from '../components/WebBackButton';
|
||||||
@@ -20,6 +19,7 @@ import { useCurrency } from '../hooks/useCurrency';
|
|||||||
import { useCloseOnSuccessNotification } from '../store/successNotification';
|
import { useCloseOnSuccessNotification } from '../store/successNotification';
|
||||||
import { CheckIcon } from '../components/icons';
|
import { CheckIcon } from '../components/icons';
|
||||||
import Twemoji from 'react-twemoji';
|
import Twemoji from 'react-twemoji';
|
||||||
|
import { SwitchTariffSheet } from '../components/subscription/sheets/SwitchTariffSheet';
|
||||||
import {
|
import {
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
getInsufficientBalanceError,
|
getInsufficientBalanceError,
|
||||||
@@ -102,8 +102,7 @@ export default function SubscriptionPurchase() {
|
|||||||
const [useCustomDays, setUseCustomDays] = useState(false);
|
const [useCustomDays, setUseCustomDays] = useState(false);
|
||||||
const [useCustomTraffic, setUseCustomTraffic] = useState(false);
|
const [useCustomTraffic, setUseCustomTraffic] = useState(false);
|
||||||
|
|
||||||
// Refs for auto-scroll
|
// Refs for auto-scroll (switch-modal ref moved into <SwitchTariffSheet>)
|
||||||
const switchModalRef = useRef<HTMLDivElement>(null);
|
|
||||||
const tariffPurchaseRef = useRef<HTMLDivElement>(null);
|
const tariffPurchaseRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Tariff switch
|
// Tariff switch
|
||||||
@@ -206,44 +205,7 @@ export default function SubscriptionPurchase() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Switch preview query
|
// (switch preview query + switchTariffMutation moved into <SwitchTariffSheet>)
|
||||||
const { data: switchPreview, isLoading: switchPreviewLoading } = useQuery({
|
|
||||||
queryKey: ['tariff-switch-preview', switchTariffId],
|
|
||||||
queryFn: () => subscriptionApi.previewTariffSwitch(switchTariffId!, subscriptionId),
|
|
||||||
enabled: !!switchTariffId,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tariff switch mutation
|
|
||||||
const switchTariffMutation = useMutation({
|
|
||||||
mutationFn: (tariffId: number) => subscriptionApi.switchTariff(tariffId, subscriptionId),
|
|
||||||
onSuccess: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['subscription', subscriptionId] });
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['purchase-options', subscriptionId] });
|
|
||||||
setSwitchTariffId(null);
|
|
||||||
|
|
||||||
navigate('/subscriptions', { replace: true });
|
|
||||||
},
|
|
||||||
onError: (error: unknown) => {
|
|
||||||
if (error instanceof AxiosError) {
|
|
||||||
const detail = error.response?.data?.detail;
|
|
||||||
if (
|
|
||||||
typeof detail === 'object' &&
|
|
||||||
detail?.error_code === 'subscription_expired' &&
|
|
||||||
detail?.use_purchase_flow === true
|
|
||||||
) {
|
|
||||||
const targetTariff = tariffs.find((tariff) => tariff.id === switchTariffId);
|
|
||||||
if (targetTariff) {
|
|
||||||
setSwitchTariffId(null);
|
|
||||||
|
|
||||||
setSelectedTariff(targetTariff);
|
|
||||||
setSelectedTariffPeriod(targetTariff.periods[0] || null);
|
|
||||||
setShowTariffPurchase(true);
|
|
||||||
queryClient.invalidateQueries({ queryKey: ['purchase-options', subscriptionId] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Tariff purchase mutation
|
// Tariff purchase mutation
|
||||||
const tariffPurchaseMutation = useMutation({
|
const tariffPurchaseMutation = useMutation({
|
||||||
@@ -281,16 +243,7 @@ export default function SubscriptionPurchase() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto-scroll effects
|
// Auto-scroll effects (switch-tariff scroll moved into the sheet)
|
||||||
useEffect(() => {
|
|
||||||
if (switchTariffId && switchModalRef.current) {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
switchModalRef.current?.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
}, 100);
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, [switchTariffId]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (showTariffPurchase && tariffPurchaseRef.current) {
|
if (showTariffPurchase && tariffPurchaseRef.current) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@@ -523,143 +476,18 @@ export default function SubscriptionPurchase() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Switch Tariff Preview Modal */}
|
{/* Switch Tariff Preview Modal */}
|
||||||
{switchTariffId && (
|
<SwitchTariffSheet
|
||||||
<div ref={switchModalRef} className="mb-6 space-y-4 rounded-xl bg-dark-800/50 p-5">
|
open={switchTariffId !== null}
|
||||||
<div className="flex items-center justify-between">
|
tariffId={switchTariffId}
|
||||||
<h3 className="font-medium text-dark-100">
|
subscriptionId={subscriptionId}
|
||||||
{t('subscription.switchTariff.title')}
|
tariffs={tariffs}
|
||||||
</h3>
|
onClose={() => setSwitchTariffId(null)}
|
||||||
<button
|
onExpiredFallback={(tariff) => {
|
||||||
onClick={() => setSwitchTariffId(null)}
|
setSelectedTariff(tariff);
|
||||||
className="text-sm text-dark-400 hover:text-dark-200"
|
setSelectedTariffPeriod(tariff.periods[0] || null);
|
||||||
>
|
setShowTariffPurchase(true);
|
||||||
✕
|
}}
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{switchPreviewLoading ? (
|
|
||||||
<div className="flex items-center justify-center py-4">
|
|
||||||
<div className="h-6 w-6 animate-spin rounded-full border-2 border-accent-500 border-t-transparent" />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
switchPreview &&
|
|
||||||
(() => {
|
|
||||||
const targetTariff = tariffs.find((tariff) => tariff.id === switchTariffId);
|
|
||||||
const dailyPrice =
|
|
||||||
targetTariff?.daily_price_kopeks ?? targetTariff?.price_per_day_kopeks ?? 0;
|
|
||||||
const isDailyTariff = dailyPrice > 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="space-y-2 text-sm">
|
|
||||||
<div className="flex justify-between text-dark-300">
|
|
||||||
<span>{t('subscription.switchTariff.currentTariff')}</span>
|
|
||||||
<span className="font-medium text-dark-100">
|
|
||||||
{switchPreview.current_tariff_name || '-'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-dark-300">
|
|
||||||
<span>{t('subscription.switchTariff.newTariff')}</span>
|
|
||||||
<span className="font-medium text-accent-400">
|
|
||||||
{switchPreview.new_tariff_name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between text-dark-300">
|
|
||||||
<span>{t('subscription.switchTariff.remainingDays')}</span>
|
|
||||||
<span>{switchPreview.remaining_days}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{isDailyTariff && (
|
|
||||||
<div className="rounded-lg border border-accent-500/30 bg-accent-500/10 p-3 text-center">
|
|
||||||
<div className="text-sm text-dark-300">
|
|
||||||
{t('subscription.switchTariff.dailyPayment')}
|
|
||||||
</div>
|
|
||||||
<div className="text-lg font-bold text-accent-400">
|
|
||||||
{formatPrice(dailyPrice)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-1 text-xs text-dark-400">
|
|
||||||
{t('subscription.switchTariff.dailyChargeDescription')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between border-t border-dark-700/50 pt-3">
|
|
||||||
<div>
|
|
||||||
<span className="font-medium text-dark-100">
|
|
||||||
{t('subscription.switchTariff.upgradeCost')}
|
|
||||||
</span>
|
|
||||||
{switchPreview.discount_percent && switchPreview.discount_percent > 0 && (
|
|
||||||
<span className="ml-2 inline-block rounded-full bg-success-500/20 px-2 py-0.5 text-xs font-medium text-success-400">
|
|
||||||
-{switchPreview.discount_percent}%
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
{switchPreview.discount_percent &&
|
|
||||||
switchPreview.discount_percent > 0 &&
|
|
||||||
switchPreview.base_upgrade_cost_kopeks &&
|
|
||||||
switchPreview.base_upgrade_cost_kopeks > 0 && (
|
|
||||||
<span className="mr-2 text-sm text-dark-500 line-through">
|
|
||||||
{formatPrice(switchPreview.base_upgrade_cost_kopeks)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className={`text-lg font-bold ${switchPreview.upgrade_cost_kopeks === 0 ? 'text-success-400' : 'text-accent-400'}`}
|
|
||||||
>
|
|
||||||
{switchPreview.upgrade_cost_kopeks > 0
|
|
||||||
? switchPreview.upgrade_cost_label
|
|
||||||
: t('subscription.switchTariff.free')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!switchPreview.has_enough_balance &&
|
|
||||||
switchPreview.upgrade_cost_kopeks > 0 && (
|
|
||||||
<InsufficientBalancePrompt
|
|
||||||
missingAmountKopeks={switchPreview.missing_amount_kopeks}
|
|
||||||
compact
|
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => switchTariffMutation.mutate(switchTariffId)}
|
|
||||||
disabled={switchTariffMutation.isPending || !switchPreview.can_switch}
|
|
||||||
className="btn-primary w-full py-2.5"
|
|
||||||
>
|
|
||||||
{switchTariffMutation.isPending ? (
|
|
||||||
<span className="flex items-center justify-center gap-2">
|
|
||||||
<span className="h-4 w-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
t('subscription.switchTariff.switch')
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{switchTariffMutation.isError &&
|
|
||||||
(() => {
|
|
||||||
const detail =
|
|
||||||
switchTariffMutation.error instanceof AxiosError
|
|
||||||
? switchTariffMutation.error.response?.data?.detail
|
|
||||||
: null;
|
|
||||||
if (
|
|
||||||
typeof detail === 'object' &&
|
|
||||||
detail?.error_code === 'subscription_expired'
|
|
||||||
) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div className="mt-3 text-center text-sm text-error-400">
|
|
||||||
{getErrorMessage(switchTariffMutation.error)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})()
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!showTariffPurchase ? (
|
{!showTariffPurchase ? (
|
||||||
<>
|
<>
|
||||||
|
|||||||
Reference in New Issue
Block a user