{t('subscription.title')}
{/* Current Subscription */}
{subscription ? (
{t('subscription.currentPlan')}
{subscription.tariff_name && (
{subscription.tariff_name}
)}
{subscription.is_trial
? t('subscription.trialStatus')
: subscription.is_active
? t('subscription.active')
: t('subscription.expired')}
{/* Trial Period Info Banner */}
{subscription.is_trial && subscription.is_active && (
{t('subscription.trialInfo.title')}
{t('subscription.trialInfo.description')}
{subscription.days_left > 0
? t('subscription.days', { count: subscription.days_left })
: `${subscription.hours_left}${t('subscription.hours')} ${subscription.minutes_left}${t('subscription.minutes')}`}
{t('subscription.trialInfo.remaining')}
{subscription.traffic_limit_gb || '∞'} GB
{t('subscription.traffic')}
{subscription.device_limit}
{t('subscription.devices')}
)}
{/* Connection Data - Top Priority */}
{subscription.subscription_url && (
{t('subscription.connectionInfo')}
{/* Get Config Button */}
setShowConnectionModal(true)}
className="btn-primary mb-3 flex w-full items-center justify-center gap-2 py-3"
>
{t('subscription.getConfig')}
{/* Subscription URL - hidden when hide_subscription_link is true */}
{!subscription.hide_subscription_link && (
{subscription.subscription_url}
{copied ? : }
)}
)}
{t('subscription.daysLeft')}
{subscription.days_left > 0 ? (
t('subscription.days', { count: subscription.days_left })
) : subscription.hours_left > 0 ? (
`${t('subscription.hours', { count: subscription.hours_left })} ${t('subscription.minutes', { count: subscription.minutes_left })}`
) : subscription.minutes_left > 0 ? (
t('subscription.minutes', { count: subscription.minutes_left })
) : (
{t('subscription.expired')}
)}
{t('subscription.expiresAt')}
{new Date(subscription.end_date).toLocaleDateString()}
{t('subscription.traffic')}
refreshTrafficMutation.mutate()}
disabled={refreshTrafficMutation.isPending || trafficRefreshCooldown > 0}
className="rounded-full p-1 text-dark-400 transition-colors hover:bg-dark-700/50 hover:text-accent-400 disabled:cursor-not-allowed disabled:opacity-50"
title={
trafficRefreshCooldown > 0 ? `${trafficRefreshCooldown}s` : t('common.refresh')
}
>
{(trafficData?.traffic_used_gb ?? subscription.traffic_used_gb).toFixed(1)} /{' '}
{subscription.traffic_limit_gb || '∞'} GB
{t('subscription.devices')}
{subscription.device_limit}
{/* Servers */}
{subscription.servers && subscription.servers.length > 0 && (
{t('subscription.serversLabel')}
{subscription.servers.map((server) => (
{server.country_code && (
{getFlagEmoji(server.country_code)}
)}
{server.name}
))}
)}
{/* Traffic Usage */}
{subscription.traffic_limit_gb > 0 && (
{t('subscription.trafficUsed')}
{(trafficData?.traffic_used_percent ?? subscription.traffic_used_percent).toFixed(
1,
)}
%
)}
{/* Purchased Traffic Packages */}
{subscription.traffic_purchases && subscription.traffic_purchases.length > 0 && (
{t('subscription.purchasedTraffic')}
{subscription.traffic_purchases.map((purchase) => (
{purchase.traffic_gb} {t('common.units.gb')}
{purchase.days_remaining === 0 ? (
{t('subscription.expired')}
) : (
{t('subscription.days', { count: purchase.days_remaining })}
)}
{t('subscription.trafficResetAt')}:{' '}
{new Date(purchase.expires_at).toLocaleDateString(undefined, {
day: '2-digit',
month: '2-digit',
year: 'numeric',
})}
{new Date(purchase.created_at).toLocaleDateString()}
{new Date(purchase.expires_at).toLocaleDateString()}
))}
)}
{/* Autopay Toggle - hide for daily tariffs */}
{!subscription.is_trial && !subscription.is_daily && (
{t('subscription.autoRenewal')}
{t('subscription.daysBeforeExpiry', { count: subscription.autopay_days_before })}
autopayMutation.mutate(!subscription.autopay_enabled)}
disabled={autopayMutation.isPending}
className={`relative h-6 w-12 rounded-full transition-colors ${
subscription.autopay_enabled ? 'bg-accent-500' : 'bg-dark-700'
}`}
>
)}
) : (
{t('subscription.noSubscription')}
)}
{/* Daily Subscription Pause */}
{subscription && subscription.is_daily && !subscription.is_trial && (
{t('subscription.pause.title')}
{subscription.is_daily_paused
? t('subscription.pause.paused')
: t('subscription.pause.active')}
pauseMutation.mutate()}
disabled={pauseMutation.isPending}
className={`rounded-lg px-4 py-2 font-medium transition-colors ${
subscription.is_daily_paused
? 'bg-success-500/20 text-success-400 hover:bg-success-500/30'
: 'bg-warning-500/20 text-warning-400 hover:bg-warning-500/30'
}`}
>
{pauseMutation.isPending ? (
) : subscription.is_daily_paused ? (
t('subscription.pause.resumeBtn')
) : (
t('subscription.pause.pauseBtn')
)}
{/* Pause mutation error */}
{pauseMutation.isError &&
(() => {
const balanceError = getInsufficientBalanceError(pauseMutation.error);
if (balanceError) {
const missingAmount = balanceError.required - balanceError.balance;
return (
);
}
return (
{getErrorMessage(pauseMutation.error)}
);
})()}
{/* Paused info or Next charge progress bar */}
{subscription.is_daily_paused ? (
⏸️
{t('subscription.pause.pausedInfo')}
{t('subscription.pause.pausedDescription')}{' '}
{new Date(subscription.end_date).toLocaleDateString()} (
{t('subscription.pause.days', { count: subscription.days_left })})
) : (
subscription.next_daily_charge_at &&
(() => {
const now = new Date();
// Backend returns UTC datetime, ensure it's parsed as UTC
const nextChargeStr = subscription.next_daily_charge_at.endsWith('Z')
? subscription.next_daily_charge_at
: subscription.next_daily_charge_at + 'Z';
const nextCharge = new Date(nextChargeStr);
const totalMs = 24 * 60 * 60 * 1000; // 24 hours in ms
const remainingMs = Math.max(0, nextCharge.getTime() - now.getTime());
const elapsedMs = totalMs - remainingMs;
const progress = Math.min(100, (elapsedMs / totalMs) * 100);
const hours = Math.floor(remainingMs / (1000 * 60 * 60));
const minutes = Math.floor((remainingMs % (1000 * 60 * 60)) / (1000 * 60));
return (
{t('subscription.pause.nextCharge')}
{hours > 0
? `${hours}${t('subscription.pause.hours')} ${minutes}${t('subscription.pause.minutes')}`
: `${minutes}${t('subscription.pause.minutes')}`}
{subscription.daily_price_kopeks && (
{t('subscription.pause.willBeCharged')}:{' '}
{formatPrice(subscription.daily_price_kopeks)}
)}
);
})()
)}
)}
{/* Additional Options (Buy Devices) */}
{subscription && subscription.is_active && !subscription.is_trial && (
{t('subscription.additionalOptions.title')}
{/* Buy Devices */}
{!showDeviceTopup ? (
setShowDeviceTopup(true)}
className="w-full rounded-xl border border-dark-700/50 bg-dark-800/30 p-4 text-left transition-colors hover:border-dark-600"
>
{t('subscription.additionalOptions.buyDevices')}
{t('subscription.additionalOptions.currentDeviceLimit', {
count: subscription.device_limit,
})}
) : (
{t('subscription.buyDevices')}
setShowDeviceTopup(false)}
className="text-sm text-dark-400 hover:text-dark-200"
>
✕
{/* Check if completely unavailable (no subscription, price not set, etc.) */}
{devicePriceData?.available === false && !devicePriceData?.max_device_limit ? (
{devicePriceData.reason || t('subscription.additionalOptions.devicesUnavailable')}
) : (
{/* Device selector - show even at max limit */}
setDevicesToAdd(Math.max(1, devicesToAdd - 1))}
disabled={devicesToAdd <= 1}
className="btn-secondary flex h-12 w-12 items-center justify-center !p-0 text-2xl"
>
-
{devicesToAdd}
{t('subscription.additionalOptions.devicesUnit')}
setDevicesToAdd(devicesToAdd + 1)}
disabled={
devicePriceData?.max_device_limit
? (devicePriceData.current_device_limit || 0) + devicesToAdd >=
devicePriceData.max_device_limit
: false
}
className="btn-secondary flex h-12 w-12 items-center justify-center !p-0 text-2xl"
>
+
{/* Show limit info when at or near max */}
{devicePriceData?.max_device_limit && (
{t('subscription.additionalOptions.currentDeviceLimit', {
count: devicePriceData.current_device_limit || subscription.device_limit,
})}{' '}
/{' '}
{t('subscription.additionalOptions.maxDevices', {
count: devicePriceData.max_device_limit,
})}
)}
{/* Show reason if can't add requested amount */}
{devicePriceData?.available === false && devicePriceData?.reason && (
{devicePriceData.reason}
)}
{/* Price info - only when available */}
{devicePriceData?.available && devicePriceData.price_per_device_label && (
{devicePriceData.price_per_device_label}/
{t('subscription.perDevice').replace('/ ', '')} (
{t('subscription.days', { count: devicePriceData.days_left })})
{devicePriceData.total_price_label}
)}
{devicePriceData?.available &&
purchaseOptions &&
devicePriceData.total_price_kopeks &&
devicePriceData.total_price_kopeks > purchaseOptions.balance_kopeks && (
{
await subscriptionApi.saveDevicesCart(devicesToAdd);
}}
/>
)}
devicePurchaseMutation.mutate()}
disabled={
devicePurchaseMutation.isPending ||
!devicePriceData?.available ||
!!(
devicePriceData?.total_price_kopeks &&
purchaseOptions &&
devicePriceData.total_price_kopeks > purchaseOptions.balance_kopeks
)
}
className="btn-primary w-full py-3"
>
{devicePurchaseMutation.isPending ? (
) : (
t('subscription.additionalOptions.buy')
)}
{devicePurchaseMutation.isError && (
{getErrorMessage(devicePurchaseMutation.error)}
)}
)}
)}
{/* Reduce Devices */}
{!showDeviceReduction ? (
setShowDeviceReduction(true)}
className="w-full rounded-xl border border-dark-700/50 bg-dark-800/30 p-4 text-left transition-colors hover:border-dark-600"
>
{t('subscription.additionalOptions.reduceDevices')}
{t('subscription.additionalOptions.reduceDevicesDescription')}
) : (
{t('subscription.additionalOptions.reduceDevicesTitle')}
setShowDeviceReduction(false)}
className="text-sm text-dark-400 hover:text-dark-200"
>
✕
{deviceReductionInfo?.available === false ? (
{deviceReductionInfo.reason ||
t('subscription.additionalOptions.reduceUnavailable')}
) : deviceReductionInfo ? (
{/* Device limit selector */}
setTargetDeviceLimit(
Math.max(
Math.max(
deviceReductionInfo.min_device_limit,
deviceReductionInfo.connected_devices_count,
),
targetDeviceLimit - 1,
),
)
}
disabled={
targetDeviceLimit <=
Math.max(
deviceReductionInfo.min_device_limit,
deviceReductionInfo.connected_devices_count,
)
}
className="btn-secondary flex h-12 w-12 items-center justify-center !p-0 text-2xl"
>
-
{targetDeviceLimit}
{t('subscription.additionalOptions.devicesUnit')}
setTargetDeviceLimit(
Math.min(
deviceReductionInfo.current_device_limit - 1,
targetDeviceLimit + 1,
),
)
}
disabled={targetDeviceLimit >= deviceReductionInfo.current_device_limit - 1}
className="btn-secondary flex h-12 w-12 items-center justify-center !p-0 text-2xl"
>
+
{/* Info */}
{t('subscription.additionalOptions.currentDeviceLimit', {
count: deviceReductionInfo.current_device_limit,
})}
{t('subscription.additionalOptions.minDeviceLimit', {
count: deviceReductionInfo.min_device_limit,
})}
{t('subscription.additionalOptions.connectedDevices', {
count: deviceReductionInfo.connected_devices_count,
})}
{/* Warning if connected devices block reduction */}
{deviceReductionInfo.connected_devices_count >
deviceReductionInfo.min_device_limit && (
{t('subscription.additionalOptions.disconnectDevicesFirst', {
count: deviceReductionInfo.connected_devices_count,
})}
)}
{/* New limit preview */}
{t('subscription.additionalOptions.newDeviceLimit', {
count: targetDeviceLimit,
})}
deviceReductionMutation.mutate()}
disabled={
deviceReductionMutation.isPending ||
targetDeviceLimit >= deviceReductionInfo.current_device_limit ||
targetDeviceLimit < deviceReductionInfo.min_device_limit ||
targetDeviceLimit < deviceReductionInfo.connected_devices_count
}
className="btn-primary w-full py-3"
>
{deviceReductionMutation.isPending ? (
{t('subscription.additionalOptions.reducing')}
) : (
t('subscription.additionalOptions.reduce')
)}
{deviceReductionMutation.isError && (
{getErrorMessage(deviceReductionMutation.error)}
)}
) : (
)}
)}
{/* Buy Traffic */}
{subscription.traffic_limit_gb > 0 && (
{!showTrafficTopup ? (
setShowTrafficTopup(true)}
className="w-full rounded-xl border border-dark-700/50 bg-dark-800/30 p-4 text-left transition-colors hover:border-dark-600"
>
{t('subscription.additionalOptions.buyTraffic')}
{t('subscription.additionalOptions.currentTrafficLimit', {
limit: subscription.traffic_limit_gb,
used: subscription.traffic_used_gb.toFixed(1),
})}
) : (
{t('subscription.additionalOptions.buyTrafficTitle')}
{
setShowTrafficTopup(false);
setSelectedTrafficPackage(null);
}}
className="text-sm text-dark-400 hover:text-dark-200"
>
✕
⚠️ {t('subscription.additionalOptions.trafficWarning')}
{!trafficPackages || trafficPackages.length === 0 ? (
{t('subscription.additionalOptions.trafficUnavailable')}
) : (
{trafficPackages.map((pkg) => (
setSelectedTrafficPackage(pkg.gb)}
className={`rounded-xl border p-4 text-center transition-all ${
selectedTrafficPackage === pkg.gb
? 'border-accent-500 bg-accent-500/10'
: 'border-dark-700/50 bg-dark-800/30 hover:border-dark-600'
}`}
>
{pkg.is_unlimited
? '♾️ ' + t('subscription.additionalOptions.unlimited')
: `${pkg.gb} ${t('common.units.gb')}`}
{formatPrice(pkg.price_kopeks)}
))}
{selectedTrafficPackage &&
(() => {
const selectedPkg = trafficPackages.find(
(p) => p.gb === selectedTrafficPackage,
);
const hasEnoughBalance =
!selectedPkg ||
!purchaseOptions ||
selectedPkg.price_kopeks <= purchaseOptions.balance_kopeks;
const missingAmount =
selectedPkg && purchaseOptions
? selectedPkg.price_kopeks - purchaseOptions.balance_kopeks
: 0;
return (
<>
{!hasEnoughBalance && missingAmount > 0 && (
{
await subscriptionApi.saveTrafficCart(selectedTrafficPackage);
}}
/>
)}
trafficPurchaseMutation.mutate(selectedTrafficPackage)
}
disabled={trafficPurchaseMutation.isPending || !hasEnoughBalance}
className="btn-primary w-full py-3"
>
{trafficPurchaseMutation.isPending ? (
) : (
t('subscription.additionalOptions.buyTrafficGb', {
gb: selectedTrafficPackage,
})
)}
>
);
})()}
{trafficPurchaseMutation.isError && (
{getErrorMessage(trafficPurchaseMutation.error)}
)}
)}
)}
)}
{/* Server Management - only in classic mode */}
{!isTariffsMode && (
{!showServerManagement ? (
setShowServerManagement(true)}
className="w-full rounded-xl border border-dark-700/50 bg-dark-800/30 p-4 text-left transition-colors hover:border-dark-600"
>
{t('subscription.additionalOptions.manageServers')}
{t('subscription.servers', { count: subscription.servers?.length || 0 })}
) : (
{t('subscription.additionalOptions.manageServersTitle')}
{
setShowServerManagement(false);
setSelectedServersToUpdate([]);
}}
className="text-sm text-dark-400 hover:text-dark-200"
>
✕
{countriesLoading ? (
) : countriesData && countriesData.countries.length > 0 ? (
{t('subscription.serverManagement.statusLegend')}
{countriesData.discount_percent > 0 && (
🎁{' '}
{t('subscription.serverManagement.discountBanner', {
percent: countriesData.discount_percent,
})}
)}
{countriesData.countries.map((country) => {
const isCurrentlyConnected = country.is_connected;
const isSelected = selectedServersToUpdate.includes(country.uuid);
const willBeAdded = !isCurrentlyConnected && isSelected;
const willBeRemoved = isCurrentlyConnected && !isSelected;
return (
{
if (isSelected) {
setSelectedServersToUpdate((prev) =>
prev.filter((u) => u !== country.uuid),
);
} else {
setSelectedServersToUpdate((prev) => [...prev, country.uuid]);
}
}}
disabled={!country.is_available && !isCurrentlyConnected}
className={`flex w-full items-center justify-between rounded-xl border p-3 text-left transition-all ${
isSelected
? willBeAdded
? 'border-success-500 bg-success-500/10'
: 'border-accent-500 bg-accent-500/10'
: willBeRemoved
? 'border-error-500/50 bg-error-500/5'
: 'border-dark-700/50 bg-dark-800/30 hover:border-dark-600'
} ${!country.is_available && !isCurrentlyConnected ? 'cursor-not-allowed opacity-50' : ''}`}
>
{willBeAdded
? '➕'
: willBeRemoved
? '➖'
: isSelected
? '✅'
: '⚪'}
{country.name}
{country.has_discount && !isCurrentlyConnected && (
-{country.discount_percent}%
)}
{willBeAdded && (
+{formatPrice(country.price_kopeks)}{' '}
{t('subscription.serverManagement.forDays', {
days: countriesData.days_left,
})}
{country.has_discount && (
{formatPrice(
Math.round(
(country.base_price_kopeks *
countriesData.days_left) /
30,
),
)}
)}
)}
{!willBeAdded && !isCurrentlyConnected && (
{formatPrice(country.price_per_month_kopeks)}
{t('subscription.serverManagement.perMonth')}
{country.has_discount && (
{formatPrice(country.base_price_kopeks)}
)}
)}
{!country.is_available && !isCurrentlyConnected && (
{t('subscription.serverManagement.unavailable')}
)}
{country.country_code && (
{getFlagEmoji(country.country_code)}
)}
);
})}
{(() => {
const currentConnected = countriesData.countries
.filter((c) => c.is_connected)
.map((c) => c.uuid);
const added = selectedServersToUpdate.filter(
(u) => !currentConnected.includes(u),
);
const removed = currentConnected.filter(
(u) => !selectedServersToUpdate.includes(u),
);
const hasChanges = added.length > 0 || removed.length > 0;
// Calculate cost for added servers
const addedServers = countriesData.countries.filter((c) =>
added.includes(c.uuid),
);
const totalCost = addedServers.reduce((sum, s) => sum + s.price_kopeks, 0);
const hasEnoughBalance =
!purchaseOptions || totalCost <= purchaseOptions.balance_kopeks;
const missingAmount = purchaseOptions
? totalCost - purchaseOptions.balance_kopeks
: 0;
return hasChanges ? (
{added.length > 0 && (
{t('subscription.serverManagement.toAdd')}
{' '}
{addedServers.map((s) => s.name).join(', ')}
)}
{removed.length > 0 && (
{t('subscription.serverManagement.toDisconnect')}
{' '}
{countriesData.countries
.filter((c) => removed.includes(c.uuid))
.map((s) => s.name)
.join(', ')}
)}
{totalCost > 0 && (
{t('subscription.serverManagement.paymentProrated')}
{formatPrice(totalCost)}
)}
{totalCost > 0 && !hasEnoughBalance && missingAmount > 0 && (
)}
updateCountriesMutation.mutate(selectedServersToUpdate)
}
disabled={
updateCountriesMutation.isPending ||
selectedServersToUpdate.length === 0 ||
(totalCost > 0 && !hasEnoughBalance)
}
className="btn-primary w-full py-3"
>
{updateCountriesMutation.isPending ? (
) : (
t('subscription.serverManagement.applyChanges')
)}
) : (
{t('subscription.serverManagement.selectServersHint')}
);
})()}
{updateCountriesMutation.isError && (
{getErrorMessage(updateCountriesMutation.error)}
)}
) : (
{t('subscription.serverManagement.noServersAvailable')}
)}
)}
)}
)}
{/* My Devices Section */}
{subscription && (
{t('subscription.myDevices')}
{devicesData && devicesData.devices.length > 0 && (
{
if (confirm(t('subscription.confirmDeleteAllDevices'))) {
deleteAllDevicesMutation.mutate();
}
}}
disabled={deleteAllDevicesMutation.isPending}
className="text-sm text-error-400 hover:text-error-300"
>
{t('subscription.deleteAllDevices')}
)}
{devicesLoading ? (
) : devicesData && devicesData.devices.length > 0 ? (
{devicesData.total} /{' '}
{t('subscription.devices', { count: devicesData.device_limit })}
{devicesData.devices.map((device) => (
{device.device_model || device.platform}
{device.platform}
{
if (confirm(t('subscription.confirmDeleteDevice'))) {
deleteDeviceMutation.mutate(device.hwid);
}
}}
disabled={deleteDeviceMutation.isPending}
className="p-2 text-dark-400 transition-colors hover:text-error-400"
title={t('subscription.deleteDevice')}
>
))}
) : (
{t('subscription.noDevices')}
)}
)}
{/* Tariffs Section - Combined Purchase/Extend/Switch like MiniApp */}
{isTariffsMode && tariffs.length > 0 && (
{subscription?.is_daily && !subscription?.is_trial
? t('subscription.switchTariff.title')
: subscription && !subscription.is_trial
? t('subscription.extend')
: t('subscription.getSubscription')}
{/* Trial upgrade prompt */}
{subscription?.is_trial && (
{t('subscription.trialUpgrade.title')}
{t('subscription.trialUpgrade.description')}
)}
{/* Expired subscription notice - prompt to purchase new tariff */}
{isTariffsMode &&
purchaseOptions &&
'subscription_is_expired' in purchaseOptions &&
purchaseOptions.subscription_is_expired && (
{t('subscription.expiredBanner.title')}
{t('subscription.expiredBanner.selectTariff')}
)}
{/* Legacy subscription notice - if user has subscription without tariff */}
{subscription && !subscription.is_trial && !subscription.tariff_id && (
{t('subscription.legacy.selectTariffTitle')}
{t('subscription.legacy.selectTariffDescription')}
{t('subscription.legacy.currentSubContinues')}
)}
{/* Switch Tariff Preview Modal */}
{switchTariffId && (
{t('subscription.switchTariff.title')}
setSwitchTariffId(null)}
className="text-sm text-dark-400 hover:text-dark-200"
>
✕
{switchPreviewLoading ? (
) : (
switchPreview &&
(() => {
// Find the target tariff to get daily price info
const targetTariff = tariffs.find((t) => t.id === switchTariffId);
const dailyPrice =
targetTariff?.daily_price_kopeks ?? targetTariff?.price_per_day_kopeks ?? 0;
const isDailyTariff = dailyPrice > 0;
return (
<>
{t('subscription.switchTariff.currentTariff')}
{switchPreview.current_tariff_name || '-'}
{t('subscription.switchTariff.newTariff')}
{switchPreview.new_tariff_name}
{t('subscription.switchTariff.remainingDays')}
{switchPreview.remaining_days}
{/* Daily tariff info */}
{isDailyTariff && (
{t('subscription.switchTariff.dailyPayment')}
{formatPrice(dailyPrice)}
{t('subscription.switchTariff.dailyChargeDescription')}
)}
{t('subscription.switchTariff.upgradeCost')}
{switchPreview.upgrade_cost_kopeks > 0
? switchPreview.upgrade_cost_label
: t('subscription.switchTariff.free')}
{!switchPreview.has_enough_balance &&
switchPreview.upgrade_cost_kopeks > 0 && (
)}
switchTariffMutation.mutate(switchTariffId)}
disabled={switchTariffMutation.isPending || !switchPreview.can_switch}
className="btn-primary w-full py-2.5"
>
{switchTariffMutation.isPending ? (
) : (
t('subscription.switchTariff.switch')
)}
{/* Show error (except subscription_expired which redirects to purchase) */}
{switchTariffMutation.isError &&
(() => {
const detail =
switchTariffMutation.error instanceof AxiosError
? switchTariffMutation.error.response?.data?.detail
: null;
// Skip displaying if it's subscription_expired (handled by redirect)
if (
typeof detail === 'object' &&
detail?.error_code === 'subscription_expired'
) {
return null;
}
return (
{getErrorMessage(switchTariffMutation.error)}
);
})()}
>
);
})()
)}
)}
{!showTariffPurchase ? (
<>
{/* Promo group discount banner */}
{tariffs.some((t) => t.promo_group_name) && (
{t('subscription.promoGroup.yourGroup', {
name: tariffs.find((t) => t.promo_group_name)?.promo_group_name,
})}
{t('subscription.promoGroup.personalDiscountsApplied')}
)}
{/* Tariff List - current tariff first */}
{[...tariffs]
// Hide Trial tariffs for users who already have trial subscription
.filter((tariff) => {
if (subscription?.is_trial && tariff.name.toLowerCase().includes('trial')) {
return false;
}
return true;
})
.sort((a, b) => {
const aIsCurrent = a.is_current || a.id === subscription?.tariff_id;
const bIsCurrent = b.is_current || b.id === subscription?.tariff_id;
if (aIsCurrent && !bIsCurrent) return -1;
if (!aIsCurrent && bIsCurrent) return 1;
return 0;
})
.map((tariff) => {
const isCurrentTariff =
tariff.is_current || tariff.id === subscription?.tariff_id;
// Check if subscription is expired from purchaseOptions
const isSubscriptionExpired =
isTariffsMode &&
purchaseOptions &&
'subscription_is_expired' in purchaseOptions &&
purchaseOptions.subscription_is_expired === true;
// canSwitch only if subscription is active (not expired, not trial)
const canSwitch =
subscription &&
subscription.tariff_id &&
!isCurrentTariff &&
!subscription.is_trial &&
!isSubscriptionExpired &&
subscription.is_active;
// Если есть подписка БЕЗ tariff_id (классическая) - разрешить выбрать тариф
const isLegacySubscription =
subscription && !subscription.is_trial && !subscription.tariff_id;
return (
{tariff.name}
{tariff.description && (
{tariff.description}
)}
{isCurrentTariff && (
{t('subscription.currentTariff')}
)}
{/* Traffic */}
{tariff.traffic_limit_label}
{/* Devices */}
{t('subscription.devices', { count: tariff.device_limit })}
{/* Servers */}
{t('subscription.servers', { count: tariff.servers_count })}
{/* Price info - daily or period-based */}
{(() => {
// Daily tariff price (is_daily + daily_price_kopeks)
const dailyPrice =
tariff.daily_price_kopeks ?? tariff.price_per_day_kopeks ?? 0;
const originalDailyPrice = tariff.original_daily_price_kopeks || 0;
const hasExistingDailyDiscount = originalDailyPrice > dailyPrice;
if (dailyPrice > 0) {
// Apply promo discount if no existing discount
const promoDaily = applyPromoDiscount(
dailyPrice,
hasExistingDailyDiscount,
);
return (
{formatPrice(promoDaily.price)}
{/* Show original price from promo group or promo offer */}
{(hasExistingDailyDiscount || promoDaily.original) && (
{formatPrice(
hasExistingDailyDiscount
? originalDailyPrice
: promoDaily.original!,
)}
)}
{t('subscription.tariff.perDay')}
{/* Show discount badge */}
{tariff.daily_discount_percent &&
tariff.daily_discount_percent > 0 ? (
-{tariff.daily_discount_percent}%
) : (
promoDaily.percent && (
-{promoDaily.percent}%
)
)}
);
}
// Period-based price
if (tariff.periods.length > 0) {
const firstPeriod = tariff.periods[0];
const hasExistingDiscount = !!(
firstPeriod?.original_price_kopeks &&
firstPeriod.original_price_kopeks > firstPeriod.price_kopeks
);
// Apply promo discount if no existing discount
const promoPeriod = applyPromoDiscount(
firstPeriod?.price_kopeks || 0,
hasExistingDiscount,
);
return (
{t('subscription.from')}
{formatPrice(promoPeriod.price)}
{/* Show original price from promo group or promo offer */}
{(hasExistingDiscount || promoPeriod.original) && (
{formatPrice(
hasExistingDiscount
? firstPeriod.original_price_kopeks!
: promoPeriod.original!,
)}
)}
{/* Show discount badge */}
{hasExistingDiscount && firstPeriod.discount_percent ? (
-{firstPeriod.discount_percent}%
) : (
promoPeriod.percent && (
-{promoPeriod.percent}%
)
)}
);
}
// Fallback
return (
{t('subscription.tariff.flexiblePayment')}
);
})()}
{/* Action Buttons */}
{isCurrentTariff ? (
/* Current tariff - extend button (hide for daily tariffs) */
subscription?.is_daily ? (
{t('subscription.currentTariff')}
) : (
{
setSelectedTariff(tariff);
setSelectedTariffPeriod(tariff.periods[0] || null);
setShowTariffPurchase(true);
}}
className="btn-primary flex-1 py-2 text-sm"
>
{t('subscription.extend')}
)
) : isLegacySubscription ? (
/* Legacy subscription without tariff - allow selecting tariff for renewal */
{
setSelectedTariff(tariff);
setSelectedTariffPeriod(tariff.periods[0] || null);
setShowTariffPurchase(true);
}}
className="btn-primary flex-1 py-2 text-sm"
>
{t('subscription.tariff.selectForRenewal')}
) : canSwitch ? (
/* Other tariffs with existing tariff - switch button */
setSwitchTariffId(tariff.id)}
className="btn-secondary flex-1 py-2 text-sm"
>
{t('subscription.switchTariff.switch')}
) : (
/* No subscription or trial - purchase button */
{
setSelectedTariff(tariff);
setSelectedTariffPeriod(tariff.periods[0] || null);
setShowTariffPurchase(true);
}}
className="btn-primary flex-1 py-2 text-sm"
>
{t('subscription.purchase')}
)}
);
})}
>
) : (
selectedTariff && (
/* Tariff Purchase Form */
{selectedTariff.name}
{
setShowTariffPurchase(false);
setSelectedTariff(null);
setSelectedTariffPeriod(null);
}}
className="text-dark-400 hover:text-dark-200"
>
← {t('common.back')}
{/* Tariff Info */}
{t('subscription.traffic')}:
{selectedTariff.traffic_limit_label}
{t('subscription.devices')}:
{selectedTariff.device_limit}
{selectedTariff.extra_devices_count > 0 && (
(+{selectedTariff.extra_devices_count})
)}
{t('subscription.serversLabel')}:
{selectedTariff.servers_count}
{selectedTariff.servers.length > 0 && (
{selectedTariff.servers.map((server) => (
{server.name}
))}
{selectedTariff.servers_count > selectedTariff.servers.length && (
+{selectedTariff.servers_count - selectedTariff.servers.length}
)}
)}
{/* Daily Tariff Purchase */}
{selectedTariff.is_daily ||
(selectedTariff.daily_price_kopeks && selectedTariff.daily_price_kopeks > 0) ? (
{t('subscription.dailyPurchase.costPerDay')}
{formatPrice(selectedTariff.daily_price_kopeks || 0)}
•
{t('subscription.dailyPurchase.chargedDaily')}
•
{t('subscription.dailyPurchase.canPause')}
•
{t('subscription.dailyPurchase.pausedOnLowBalance')}
{/* Purchase button for daily tariff */}
{(() => {
const dailyPrice = selectedTariff.daily_price_kopeks || 0;
const hasEnoughBalance =
purchaseOptions && dailyPrice <= purchaseOptions.balance_kopeks;
return (
{purchaseOptions && !hasEnoughBalance && (
)}
tariffPurchaseMutation.mutate()}
disabled={tariffPurchaseMutation.isPending}
className="btn-primary w-full py-3"
>
{tariffPurchaseMutation.isPending ? (
{t('common.loading')}
) : (
t('subscription.dailyPurchase.activate', {
price: formatPrice(dailyPrice),
})
)}
{tariffPurchaseMutation.isError &&
!getInsufficientBalanceError(tariffPurchaseMutation.error) && (
{getErrorMessage(tariffPurchaseMutation.error)}
)}
{tariffPurchaseMutation.isError &&
getInsufficientBalanceError(tariffPurchaseMutation.error) && (
)}
);
})()}
) : (
<>
{/* Period Selection for non-daily tariffs */}
{t('subscription.selectPeriod')}
{/* Fixed periods */}
{selectedTariff.periods.length > 0 && !useCustomDays && (
{selectedTariff.periods.map((period) => {
const hasExistingDiscount = !!(
period.original_price_kopeks &&
period.original_price_kopeks > period.price_kopeks
);
const promoPeriod = applyPromoDiscount(
period.price_kopeks,
hasExistingDiscount,
);
const displayDiscount = hasExistingDiscount
? period.discount_percent
: promoPeriod.percent;
const displayOriginal = hasExistingDiscount
? period.original_price_kopeks
: promoPeriod.original;
const displayPrice = promoPeriod.price;
const displayPerMonth = hasExistingDiscount
? period.price_per_month_kopeks
: Math.round(promoPeriod.price / (period.days / 30));
return (
{
setSelectedTariffPeriod(period);
setUseCustomDays(false);
}}
className={`relative rounded-xl border p-4 text-left transition-all ${
selectedTariffPeriod?.days === period.days && !useCustomDays
? 'border-accent-500 bg-accent-500/10'
: 'border-dark-700/50 bg-dark-800/30 hover:border-dark-600'
}`}
>
{displayDiscount && displayDiscount > 0 && (
-{displayDiscount}%
)}
{period.label}
{formatPrice(displayPrice)}
{displayOriginal && displayOriginal > displayPrice && (
{formatPrice(displayOriginal)}
)}
{formatPrice(displayPerMonth)}/{t('subscription.month')}
);
})}
)}
{/* Custom days option */}
{selectedTariff.custom_days_enabled &&
(selectedTariff.price_per_day_kopeks ?? 0) > 0 && (
{t('subscription.customDays.title')}
setUseCustomDays(!useCustomDays)}
className={`relative h-6 w-10 rounded-full transition-colors ${
useCustomDays ? 'bg-accent-500' : 'bg-dark-600'
}`}
>
{useCustomDays && (
)}
)}
{/* Custom traffic option */}
{selectedTariff.custom_traffic_enabled &&
(selectedTariff.traffic_price_per_gb_kopeks ?? 0) > 0 && (
{t('subscription.customTraffic.label')}
{t('subscription.customTraffic.selectVolume')}
setUseCustomTraffic(!useCustomTraffic)}
className={`relative h-6 w-10 rounded-full transition-colors ${
useCustomTraffic ? 'bg-accent-500' : 'bg-dark-600'
}`}
>
{!useCustomTraffic && (
{t('subscription.customTraffic.default', {
label: selectedTariff.traffic_limit_label,
})}
)}
{useCustomTraffic && (
{customTrafficGb} {t('common.units.gb')} ×{' '}
{formatPrice(selectedTariff.traffic_price_per_gb_kopeks ?? 0)}/
{t('common.units.gb')}
+
{formatPrice(
customTrafficGb *
(selectedTariff.traffic_price_per_gb_kopeks ?? 0),
)}
)}
)}
{/* Summary & Purchase */}
{(selectedTariffPeriod || useCustomDays) && (
{(() => {
// Calculate prices with promo discount
const basePeriodPrice = useCustomDays
? customDays * (selectedTariff.price_per_day_kopeks ?? 0)
: selectedTariffPeriod?.price_kopeks || 0;
const hasExistingPeriodDiscount =
!useCustomDays && selectedTariffPeriod?.original_price_kopeks
? selectedTariffPeriod.original_price_kopeks >
selectedTariffPeriod.price_kopeks
: false;
const promoPeriod = applyPromoDiscount(
basePeriodPrice,
hasExistingPeriodDiscount,
);
const trafficPrice =
useCustomTraffic && selectedTariff.custom_traffic_enabled
? customTrafficGb * (selectedTariff.traffic_price_per_gb_kopeks ?? 0)
: 0;
const totalPrice = promoPeriod.price + trafficPrice;
const originalTotal = promoPeriod.original
? promoPeriod.original + trafficPrice
: null;
return (
<>
{/* Price breakdown */}
{useCustomDays ? (
{t('subscription.stepPeriod')}:{' '}
{t('subscription.days', { count: customDays })}
{formatPrice(promoPeriod.price)}
{promoPeriod.original && (
{formatPrice(promoPeriod.original)}
)}
) : (
selectedTariffPeriod && (
<>
{/* Если есть доп. устройства - показываем разбивку */}
{(selectedTariffPeriod.extra_devices_count ?? 0) > 0 &&
selectedTariffPeriod.base_tariff_price_kopeks ? (
<>
{t('subscription.baseTariff')}:{' '}
{selectedTariffPeriod.label}
{formatPrice(
selectedTariffPeriod.base_tariff_price_kopeks,
)}
{t('subscription.extraDevices')} (
{selectedTariffPeriod.extra_devices_count})
+
{formatPrice(
selectedTariffPeriod.extra_devices_cost_kopeks ?? 0,
)}
>
) : (
{t('subscription.summary.period', {
label: selectedTariffPeriod.label,
})}
{formatPrice(promoPeriod.price)}
{(hasExistingPeriodDiscount ||
promoPeriod.original) && (
{formatPrice(
hasExistingPeriodDiscount
? selectedTariffPeriod.original_price_kopeks!
: promoPeriod.original!,
)}
)}
)}
>
)
)}
{useCustomTraffic && selectedTariff.custom_traffic_enabled && (
{t('subscription.summary.traffic', { gb: customTrafficGb })}
+{formatPrice(trafficPrice)}
)}
{/* Promo discount info */}
{promoPeriod.percent && (
{t('promo.discountApplied')} -{promoPeriod.percent}%
)}
{t('subscription.total')}
{formatPrice(totalPrice)}
{originalTotal && (
{formatPrice(originalTotal)}
)}
tariffPurchaseMutation.mutate()}
disabled={tariffPurchaseMutation.isPending}
className="btn-primary w-full py-3"
>
{tariffPurchaseMutation.isPending ? (
{t('common.loading')}
) : (
t('subscription.purchase')
)}
>
);
})()}
{tariffPurchaseMutation.isError &&
!getInsufficientBalanceError(tariffPurchaseMutation.error) && (
{getErrorMessage(tariffPurchaseMutation.error)}
)}
{tariffPurchaseMutation.isError &&
getInsufficientBalanceError(tariffPurchaseMutation.error) && (
)}
)}
>
)}
)
)}
)}
{/* Purchase/Extend Section - Classic Mode */}
{classicOptions && classicOptions.periods.length > 0 && (
{subscription && !subscription.is_trial
? t('subscription.extend')
: t('subscription.getSubscription')}
{!showPurchaseForm && (
setShowPurchaseForm(true)} className="btn-primary">
{subscription && !subscription.is_trial
? t('subscription.extend')
: t('subscription.getSubscription')}
)}
{showPurchaseForm && (
{/* Step Indicator */}
{t('subscription.step', { current: currentStepIndex + 1, total: steps.length })}
{steps.map((step, idx) => (
))}
{getStepLabel(currentStep)}
{/* Step: Period Selection */}
{currentStep === 'period' && classicOptions && (
{classicOptions.periods.map((period) => {
const hasExistingDiscount = !!(
period.discount_percent && period.discount_percent > 0
);
const promoPeriod = applyPromoDiscount(
period.price_kopeks,
hasExistingDiscount,
);
const displayDiscount = hasExistingDiscount
? period.discount_percent
: promoPeriod.percent;
const displayOriginal = hasExistingDiscount
? period.original_price_kopeks
: promoPeriod.original;
return (
{
setSelectedPeriod(period);
if (period.traffic.current !== undefined) {
setSelectedTraffic(period.traffic.current);
}
if (period.servers.selected) {
setSelectedServers(period.servers.selected);
}
if (period.devices.current) {
setSelectedDevices(period.devices.current);
}
}}
className={`bento-card-hover relative p-4 text-left transition-all ${
selectedPeriod?.id === period.id
? 'bento-card-glow border-accent-500'
: ''
}`}
>
{displayDiscount && displayDiscount > 0 && (
-{displayDiscount}%
)}
{period.label}
{formatPrice(promoPeriod.price)}
{displayOriginal && displayOriginal > promoPeriod.price && (
{formatPrice(displayOriginal)}
)}
);
})}
)}
{/* Step: Traffic Selection */}
{currentStep === 'traffic' && selectedPeriod?.traffic.options && (
{selectedPeriod.traffic.options.map((option) => {
const hasExistingDiscount = !!(
option.discount_percent && option.discount_percent > 0
);
const promoTraffic = applyPromoDiscount(
option.price_kopeks,
hasExistingDiscount,
);
return (
setSelectedTraffic(option.value)}
disabled={!option.is_available}
className={`bento-card-hover relative p-4 text-center transition-all ${
selectedTraffic === option.value
? 'bento-card-glow border-accent-500'
: ''
} ${!option.is_available ? 'cursor-not-allowed opacity-50' : ''}`}
>
{promoTraffic.percent && (
-{promoTraffic.percent}%
)}
{option.label}
{formatPrice(promoTraffic.price)}
{promoTraffic.original && (
{formatPrice(promoTraffic.original)}
)}
);
})}
)}
{/* Step: Server Selection */}
{currentStep === 'servers' && selectedPeriod?.servers.options && (
{selectedPeriod.servers.options
// Hide Trial server for users who already have trial subscription
.filter((server) => {
if (subscription?.is_trial && server.name.toLowerCase().includes('trial')) {
return false;
}
return true;
})
.map((server) => {
const hasExistingDiscount = !!(
server.discount_percent && server.discount_percent > 0
);
const promoServer = applyPromoDiscount(
server.price_kopeks,
hasExistingDiscount,
);
return (
toggleServer(server.uuid)}
disabled={!server.is_available}
className={`relative rounded-xl border p-4 text-left transition-all ${
selectedServers.includes(server.uuid)
? 'border-accent-500 bg-accent-500/10'
: server.is_available
? 'border-dark-700/50 bg-dark-800/30 hover:border-dark-600'
: 'cursor-not-allowed border-dark-800/30 bg-dark-900/30 opacity-50'
}`}
>
{promoServer.percent && (
-{promoServer.percent}%
)}
{selectedServers.includes(server.uuid) && }
{server.name}
{formatPrice(promoServer.price)}
{t('subscription.perMonth')}
{promoServer.original && (
{formatPrice(promoServer.original)}
)}
);
})}
)}
{/* Step: Device Selection */}
{currentStep === 'devices' && selectedPeriod && (
setSelectedDevices(
Math.max(selectedPeriod.devices.min, selectedDevices - 1),
)
}
disabled={selectedDevices <= selectedPeriod.devices.min}
className="btn-secondary flex h-14 w-14 items-center justify-center !p-0 text-2xl"
>
-
{selectedDevices}
{t('subscription.devices')}
setSelectedDevices(
Math.min(selectedPeriod.devices.max, selectedDevices + 1),
)
}
disabled={selectedDevices >= selectedPeriod.devices.max}
className="btn-secondary flex h-14 w-14 items-center justify-center !p-0 text-2xl"
>
+
{t('subscription.devicesFree', { count: selectedPeriod.devices.min })}
{selectedPeriod.devices.max > selectedPeriod.devices.min && (
{formatPrice(selectedPeriod.devices.price_per_device_kopeks)}{' '}
{t('subscription.perExtraDevice')}
)}
)}
{/* Step: Confirm */}
{currentStep === 'confirm' && (
{previewLoading ? (
) : preview ? (
{/* Active promo discount banner */}
{activeDiscount?.is_active &&
activeDiscount.discount_percent &&
!preview.original_price_kopeks && (
{t('promo.discountApplied')} -{activeDiscount.discount_percent}%
)}
{preview.breakdown.map((item, idx) => (
{item.label}
{item.value}
))}
{(() => {
// Apply promo discount if not already applied by server
const hasServerDiscount = !!preview.original_price_kopeks;
const promoTotal = applyPromoDiscount(
preview.total_price_kopeks,
hasServerDiscount,
);
const displayTotal = promoTotal.price;
const displayOriginal = hasServerDiscount
? preview.original_price_kopeks
: promoTotal.original;
return (
{t('subscription.total')}
{formatPrice(displayTotal)}
{displayOriginal && (
{formatPrice(displayOriginal)}
)}
);
})()}
{preview.discount_label && (
{preview.discount_label}
)}
{!preview.can_purchase &&
(preview.missing_amount_kopeks > 0 ? (
) : preview.status_message ? (
{preview.status_message}
) : null)}
) : null}
)}
{/* Navigation Buttons */}
{!isFirstStep && (
{t('common.back')}
)}
{isFirstStep && (
{t('common.cancel')}
)}
{!isLastStep ? (
{t('common.next')}
) : (
purchaseMutation.mutate()}
disabled={
purchaseMutation.isPending || previewLoading || !preview?.can_purchase
}
className="btn-primary flex-1"
>
{purchaseMutation.isPending ? (
{t('common.loading')}
) : (
t('subscription.purchase')
)}
)}
{purchaseMutation.isError && (
{getErrorMessage(purchaseMutation.error)}
)}
)}
)}
{/* Connection Modal */}
{showConnectionModal &&
setShowConnectionModal(false)} />}
);
}