{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')}
{/* Connection Data - Top Priority */}
{subscription.subscription_url && (
{t('subscription.connectionInfo')}
{/* Get Config Button */}
{/* Subscription URL */}
{subscription.subscription_url}
)}
{t('subscription.daysLeft')}
{subscription.days_left > 0 ? (
`${subscription.days_left} ${t('subscription.days')}`
) : subscription.hours_left > 0 ? (
`${subscription.hours_left}${t('subscription.hours')} ${subscription.minutes_left}${t('subscription.minutes')}`
) : subscription.minutes_left > 0 ? (
`${subscription.minutes_left}${t('subscription.minutes')}`
) : (
{t('subscription.expired')}
)}
{t('subscription.expiresAt')}
{new Date(subscription.end_date).toLocaleDateString()}
{t('subscription.traffic')}
{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.servers')}
{subscription.servers.map((server) => (
{server.country_code && (
{getFlagEmoji(server.country_code)}
)}
{server.name}
))}
)}
{/* Traffic Usage */}
{subscription.traffic_limit_gb > 0 && (
{t('subscription.trafficUsed')}
{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.days_remaining === 0 ? (
Истекает сегодня
) : purchase.days_remaining === 1 ? (
Остался 1 день
) : (
Осталось {purchase.days_remaining} {purchase.days_remaining >= 5 ? 'дней' : purchase.days_remaining === 1 ? 'день' : 'дня'}
)}
{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')}
{subscription.autopay_days_before} {t('subscription.daysBeforeExpiry')}
)}
) : (
{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')}
{/* Pause mutation error */}
{pauseMutation.isError && (
{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()} ({subscription.days_left} {
subscription.days_left === 1
? t('subscription.pause.days_one')
: subscription.days_left < 5
? t('subscription.pause.days_few')
: t('subscription.pause.days_many')
})
) : 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 && (
Дополнительные опции
{/* Buy Devices */}
{!showDeviceTopup ? (
) : (
Докупить устройства
{devicePriceData?.available === false ? (
{devicePriceData.reason || 'Докупка устройств недоступна'}
) : (
{devicePriceData && (
{devicePriceData.price_per_device_label}/устройство (пропорционально {devicePriceData.days_left} дням)
{devicePriceData.total_price_label}
)}
{devicePurchaseMutation.isError && (
{getErrorMessage(devicePurchaseMutation.error)}
)}
)}
)}
{/* Buy Traffic */}
{subscription.traffic_limit_gb > 0 && (
{!showTrafficTopup ? (
) : (
Докупить трафик
⚠️ Докупленный трафик добавляется к текущему лимиту и не переносится на следующий период
{!trafficPackages || trafficPackages.length === 0 ? (
Докупка трафика недоступна для вашего тарифа
) : (
{trafficPackages.map((pkg) => (
))}
{selectedTrafficPackage && (
)}
{trafficPurchaseMutation.isError && (
{getErrorMessage(trafficPurchaseMutation.error)}
)}
)}
)}
)}
)}
{/* My Devices Section */}
{subscription && (
{t('subscription.myDevices')}
{devicesData && devicesData.devices.length > 0 && (
)}
{devicesLoading ? (
) : devicesData && devicesData.devices.length > 0 ? (
{devicesData.total} / {devicesData.device_limit} {t('subscription.devices')}
{devicesData.devices.map((device) => (
{device.device_model || device.platform}
{device.platform}
))}
) : (
{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')}
{/* Legacy subscription notice - if user has subscription without tariff */}
{subscription && !subscription.is_trial && !subscription.tariff_id && (
📦 Выберите тариф для продления
Ваша текущая подписка была создана до введения тарифов.
Для продления необходимо выбрать один из доступных тарифов.
⚠️ Ваша текущая подписка продолжит действовать до окончания срока.
)}
{/* Switch Tariff Preview Modal */}
{switchTariffId && (
{t('subscription.switchTariff.title')}
{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 && (
Оплата за день
{formatPrice(dailyPrice)}
Списывается ежедневно с баланса
)}
{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 && (
{t('subscription.switchTariff.notEnoughBalance')}: {switchPreview.missing_amount_label}
)}
>
)
})()}
)}
{!showTariffPurchase ? (
<>
{/* Promo group discount banner */}
{tariffs.some(t => t.promo_group_name) && (
Ваша группа: {tariffs.find(t => t.promo_group_name)?.promo_group_name}
Персональные скидки применены к ценам
)}
{/* 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
const canSwitch = subscription && subscription.tariff_id && !isCurrentTariff && !subscription.is_trial
// Если есть подписка БЕЗ tariff_id (классическая) - разрешить выбрать тариф
const isLegacySubscription = subscription && !subscription.is_trial && !subscription.tariff_id
return (
{tariff.name}
{tariff.description && (
{tariff.description}
)}
{isCurrentTariff && (
{t('subscription.currentTariff')}
)}
{tariff.traffic_limit_label}
{tariff.device_limit} {t('subscription.devices')}
{tariff.servers_count} {t('subscription.servers')}
{/* 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!)}
)}
/ день
{/* 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 Гибкая оплата
})()}
{/* Action Buttons */}
{isCurrentTariff ? (
/* Current tariff - extend button (hide for daily tariffs) */
subscription?.is_daily ? (
{t('subscription.currentTariff')}
) : (
)
) : isLegacySubscription ? (
/* Legacy subscription without tariff - allow selecting tariff for renewal */
) : canSwitch ? (
/* Other tariffs with existing tariff - switch button */
) : (
/* No subscription or trial - purchase button */
)}
)
})}
>
) : selectedTariff && (
/* Tariff Purchase Form */
{selectedTariff.name}
{/* Tariff Info */}
{t('subscription.traffic')}:
{selectedTariff.traffic_limit_label}
{t('subscription.devices')}:
{selectedTariff.device_limit}
{t('subscription.servers')}:
{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 && (
{t('subscription.insufficientBalance', {
missing: ((dailyPrice - purchaseOptions.balance_kopeks) / 100).toFixed(2)
})}
)}
{tariffPurchaseMutation.isError && (
{getErrorMessage(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 (
)
})}
)}
{/* Custom days option */}
{selectedTariff.custom_days_enabled && (selectedTariff.price_per_day_kopeks ?? 0) > 0 && (
Произвольное кол-во дней
{useCustomDays && (
)}
)}
{/* Custom traffic option */}
{selectedTariff.custom_traffic_enabled && (selectedTariff.traffic_price_per_gb_kopeks ?? 0) > 0 && (
Трафик
Выбрать объём трафика
{!useCustomTraffic && (
По умолчанию: {selectedTariff.traffic_limit_label}
)}
{useCustomTraffic && (
{customTrafficGb} ГБ × {formatPrice(selectedTariff.traffic_price_per_gb_kopeks ?? 0)}/ГБ
+{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
const hasEnoughBalance = purchaseOptions && totalPrice <= purchaseOptions.balance_kopeks
return (
<>
{/* Price breakdown */}
{useCustomDays ? (
Период: {customDays} дней
{formatPrice(promoPeriod.price)}
{promoPeriod.original && (
{formatPrice(promoPeriod.original)}
)}
) : selectedTariffPeriod && (
Период: {selectedTariffPeriod.label}
{formatPrice(promoPeriod.price)}
{(hasExistingPeriodDiscount || promoPeriod.original) && (
{formatPrice(hasExistingPeriodDiscount ? selectedTariffPeriod.original_price_kopeks! : promoPeriod.original!)}
)}
)}
{useCustomTraffic && selectedTariff.custom_traffic_enabled && (
Трафик: {customTrafficGb} ГБ
+{formatPrice(trafficPrice)}
)}
{/* Promo discount info */}
{promoPeriod.percent && (
{t('promo.discountApplied')} -{promoPeriod.percent}%
)}
{t('subscription.total')}
{formatPrice(totalPrice)}
{originalTotal && (
{formatPrice(originalTotal)}
)}
{purchaseOptions && !hasEnoughBalance && (
{t('subscription.insufficientBalance', {
missing: ((totalPrice - purchaseOptions.balance_kopeks) / 100).toFixed(2)
})}
)}
>
)
})()}
{tariffPurchaseMutation.isError && (
{getErrorMessage(tariffPurchaseMutation.error)}
)}
)}
>
)}
)}
)}
{/* Purchase/Extend Section - Classic Mode */}
{classicOptions && classicOptions.periods.length > 0 && (
{subscription && !subscription.is_trial ? t('subscription.extend') : t('subscription.getSubscription')}
{!showPurchaseForm && (
)}
{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 (
)
})}
)}
{/* 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 (
)
})}
)}
{/* 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 (
)
})}
)}
{/* Step: Device Selection */}
{currentStep === 'devices' && selectedPeriod && (
{selectedDevices}
{t('subscription.devices')}
{selectedPeriod.devices.min} {t('subscription.devicesFree')}
{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.status_message && (
{preview.status_message}
)}
) : null}
)}
{/* Navigation Buttons */}
{!isFirstStep && (
)}
{isFirstStep && (
)}
{!isLastStep ? (
) : (
)}
{purchaseMutation.isError && (
{getErrorMessage(purchaseMutation.error)}
)}
)}
)}
{/* Connection Modal */}
{showConnectionModal && (
setShowConnectionModal(false)} />
)}
)
}