diff --git a/src/components/subscription/purchase/TariffPickerGrid.tsx b/src/components/subscription/purchase/TariffPickerGrid.tsx
new file mode 100644
index 0000000..0b63dc3
--- /dev/null
+++ b/src/components/subscription/purchase/TariffPickerGrid.tsx
@@ -0,0 +1,347 @@
+import { useTranslation } from 'react-i18next';
+import { useNavigate } from 'react-router';
+import { useTheme } from '../../../hooks/useTheme';
+import { useCurrency } from '../../../hooks/useCurrency';
+import { usePromoDiscount } from '../../../hooks/usePromoDiscount';
+import { getGlassColors } from '../../../utils/glassTheme';
+import type { Tariff, Subscription, PurchaseOptions } from '../../../types';
+
+// ──────────────────────────────────────────────────────────────────
+// TariffPickerGrid
+//
+// The tariff selection surface inside SubscriptionPurchase. Renders:
+// - an optional promo-group banner when any tariff carries a
+// promo_group_name
+// - the "all tariffs purchased" empty state (multi-tariff mode)
+// - the grid itself (1 col mobile, 2 cols sm+) with promo prices,
+// per-tariff CTAs differentiated by user state (extend / switch /
+// purchase / legacy renewal)
+//
+// Owns nothing — pure presentation that calls back into the parent
+// for selection (`onSelectTariff`) and switch (`onSwitchTariff`).
+// ──────────────────────────────────────────────────────────────────
+
+export interface TariffPickerGridProps {
+ tariffs: Tariff[];
+ subscription: Subscription | null;
+ purchaseOptions: PurchaseOptions | undefined;
+ isTariffsMode: boolean;
+ isMultiTariff: boolean;
+ onSelectTariff: (tariff: Tariff) => void;
+ onSwitchTariff: (tariffId: number) => void;
+}
+
+export function TariffPickerGrid({
+ tariffs,
+ subscription,
+ purchaseOptions,
+ isTariffsMode,
+ isMultiTariff,
+ onSelectTariff,
+ onSwitchTariff,
+}: TariffPickerGridProps) {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const { isDark } = useTheme();
+ const g = getGlassColors(isDark);
+ const { formatAmount, currencySymbol } = useCurrency();
+ const { applyPromoDiscount } = usePromoDiscount();
+
+ const formatPrice = (kopeks: number) =>
+ kopeks === 0
+ ? t('subscription.free', 'Бесплатно')
+ : `${formatAmount(kopeks / 100)} ${currencySymbol}`;
+
+ return (
+ <>
+ {/* Promo group discount banner */}
+ {tariffs.some((tariff) => tariff.promo_group_name) && (
+
+
+
+
+ {t('subscription.promoGroup.title', {
+ name: tariffs.find((tariff) => tariff.promo_group_name)?.promo_group_name,
+ })}
+
+
+ {t('subscription.promoGroup.personalDiscountsApplied')}
+
+
+
+ )}
+
+ {/* Tariff Grid */}
+ {isMultiTariff &&
+ purchaseOptions &&
+ 'all_tariffs_purchased' in purchaseOptions &&
+ purchaseOptions.all_tariffs_purchased && (
+
+
✅
+
+ {t('subscription.allTariffsPurchased', 'Все тарифы подключены')}
+
+
+ {t(
+ 'subscription.allTariffsPurchasedDesc',
+ 'Вы уже приобрели все доступные тарифы. Продлить подписку можно на странице тарифа.',
+ )}
+
+
+
+ )}
+
+ {[...tariffs]
+ .filter((tariff) => {
+ // In multi-tariff mode: hide already purchased tariffs
+ if (isMultiTariff && tariff.is_purchased) return false;
+ 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 isSubscriptionExpired =
+ isTariffsMode &&
+ purchaseOptions &&
+ 'subscription_is_expired' in purchaseOptions &&
+ purchaseOptions.subscription_is_expired === true;
+ const canSwitch =
+ !isMultiTariff &&
+ subscription &&
+ subscription.tariff_id &&
+ !isCurrentTariff &&
+ !subscription.is_trial &&
+ !isSubscriptionExpired &&
+ (subscription.is_active || subscription.is_limited);
+ 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 === 0
+ ? '∞'
+ : t('subscription.devices', { count: tariff.device_limit })}
+
+
+ {tariff.traffic_reset_mode && tariff.traffic_reset_mode !== 'NO_RESET' && (
+
+
+
+ {t(`subscription.trafficReset.${tariff.traffic_reset_mode}`)}
+
+
+ )}
+
+ {/* Price info */}
+
+ {(() => {
+ const dailyPrice =
+ tariff.daily_price_kopeks ?? tariff.price_per_day_kopeks ?? 0;
+ const originalDailyPrice = tariff.original_daily_price_kopeks || 0;
+ if (dailyPrice > 0 || originalDailyPrice > 0) {
+ const promoDaily = applyPromoDiscount(
+ dailyPrice,
+ originalDailyPrice > dailyPrice ? originalDailyPrice : undefined,
+ );
+ return (
+
+
+ {formatPrice(promoDaily.price)}
+
+ {promoDaily.original && promoDaily.original > promoDaily.price && (
+
+ {formatPrice(promoDaily.original)}
+
+ )}
+ {t('subscription.tariff.perDay')}
+ {promoDaily.percent && promoDaily.percent > 0 && (
+
+ -{promoDaily.percent}%
+
+ )}
+
+ );
+ }
+ if (tariff.periods.length > 0) {
+ const firstPeriod = tariff.periods[0];
+ const promoPeriod = applyPromoDiscount(
+ firstPeriod?.price_kopeks || 0,
+ firstPeriod?.original_price_kopeks,
+ );
+ return (
+
+ {t('subscription.from')}
+
+ {formatPrice(promoPeriod.price)}
+
+ {promoPeriod.original && promoPeriod.original > promoPeriod.price && (
+
+ {formatPrice(promoPeriod.original)}
+
+ )}
+ {promoPeriod.percent && promoPeriod.percent > 0 && (
+
+ -{promoPeriod.percent}%
+
+ )}
+
+ );
+ }
+ return (
+
+ {t('subscription.tariff.flexiblePayment')}
+
+ );
+ })()}
+
+
+ {/* Action Buttons */}
+
+ {isCurrentTariff ? (
+ subscription?.is_daily ? (
+
+ {t('subscription.currentTariff')}
+
+ ) : (
+
+ )
+ ) : isLegacySubscription ? (
+
+ ) : canSwitch ? (
+
+ ) : (
+
+ )}
+
+
+ );
+ })}
+
+ >
+ );
+}
diff --git a/src/pages/SubscriptionPurchase.tsx b/src/pages/SubscriptionPurchase.tsx
index f3c108d..ba17404 100644
--- a/src/pages/SubscriptionPurchase.tsx
+++ b/src/pages/SubscriptionPurchase.tsx
@@ -15,6 +15,7 @@ import { CheckIcon } from '../components/icons';
import Twemoji from 'react-twemoji';
import { SwitchTariffSheet } from '../components/subscription/sheets/SwitchTariffSheet';
import { TariffPurchaseForm } from '../components/subscription/purchase/TariffPurchaseForm';
+import { TariffPickerGrid } from '../components/subscription/purchase/TariffPickerGrid';
import { getErrorMessage, type PurchaseStep } from '../utils/subscriptionHelpers';
export default function SubscriptionPurchase() {
@@ -436,313 +437,18 @@ export default function SubscriptionPurchase() {
/>
{!showTariffPurchase ? (
- <>
- {/* Promo group discount banner */}
- {tariffs.some((tariff) => tariff.promo_group_name) && (
-
-
-
-
- {t('subscription.promoGroup.yourGroup', {
- name: tariffs.find((tariff) => tariff.promo_group_name)?.promo_group_name,
- })}
-
-
- {t('subscription.promoGroup.personalDiscountsApplied')}
-
-
-
- )}
-
- {/* Tariff Grid */}
- {isMultiTariff &&
- purchaseOptions &&
- 'all_tariffs_purchased' in purchaseOptions &&
- purchaseOptions.all_tariffs_purchased && (
-
-
✅
-
- {t('subscription.allTariffsPurchased', 'Все тарифы подключены')}
-
-
- {t(
- 'subscription.allTariffsPurchasedDesc',
- 'Вы уже приобрели все доступные тарифы. Продлить подписку можно на странице тарифа.',
- )}
-
-
-
- )}
-
- {[...tariffs]
- .filter((tariff) => {
- // In multi-tariff mode: hide already purchased tariffs
- if (isMultiTariff && tariff.is_purchased) return false;
- 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 isSubscriptionExpired =
- isTariffsMode &&
- purchaseOptions &&
- 'subscription_is_expired' in purchaseOptions &&
- purchaseOptions.subscription_is_expired === true;
- const canSwitch =
- !isMultiTariff &&
- subscription &&
- subscription.tariff_id &&
- !isCurrentTariff &&
- !subscription.is_trial &&
- !isSubscriptionExpired &&
- (subscription.is_active || subscription.is_limited);
- 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 === 0
- ? '∞'
- : t('subscription.devices', { count: tariff.device_limit })}
-
-
- {tariff.traffic_reset_mode &&
- tariff.traffic_reset_mode !== 'NO_RESET' && (
-
-
-
- {t(`subscription.trafficReset.${tariff.traffic_reset_mode}`)}
-
-
- )}
-
- {/* Price info */}
-
- {(() => {
- const dailyPrice =
- tariff.daily_price_kopeks ?? tariff.price_per_day_kopeks ?? 0;
- const originalDailyPrice = tariff.original_daily_price_kopeks || 0;
- if (dailyPrice > 0 || originalDailyPrice > 0) {
- const promoDaily = applyPromoDiscount(
- dailyPrice,
- originalDailyPrice > dailyPrice ? originalDailyPrice : undefined,
- );
- return (
-
-
- {formatPrice(promoDaily.price)}
-
- {promoDaily.original &&
- promoDaily.original > promoDaily.price && (
-
- {formatPrice(promoDaily.original)}
-
- )}
- {t('subscription.tariff.perDay')}
- {promoDaily.percent && promoDaily.percent > 0 && (
-
- -{promoDaily.percent}%
-
- )}
-
- );
- }
- if (tariff.periods.length > 0) {
- const firstPeriod = tariff.periods[0];
- const promoPeriod = applyPromoDiscount(
- firstPeriod?.price_kopeks || 0,
- firstPeriod?.original_price_kopeks,
- );
- return (
-
- {t('subscription.from')}
-
- {formatPrice(promoPeriod.price)}
-
- {promoPeriod.original &&
- promoPeriod.original > promoPeriod.price && (
-
- {formatPrice(promoPeriod.original)}
-
- )}
- {promoPeriod.percent && promoPeriod.percent > 0 && (
-
- -{promoPeriod.percent}%
-
- )}
-
- );
- }
- return (
-
- {t('subscription.tariff.flexiblePayment')}
-
- );
- })()}
-
-
- {/* Action Buttons */}
-
- {isCurrentTariff ? (
- subscription?.is_daily ? (
-
- {t('subscription.currentTariff')}
-
- ) : (
-
- )
- ) : isLegacySubscription ? (
-
- ) : canSwitch ? (
-
- ) : (
-
- )}
-
-
- );
- })}
-
- >
+ {
+ setSelectedTariff(tariff);
+ setShowTariffPurchase(true);
+ }}
+ onSwitchTariff={(tariffId) => setSwitchTariffId(tariffId)}
+ />
) : (
selectedTariff && (
/* Tariff Purchase Form (extracted into its own component) */