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) */