diff --git a/src/pages/GiftResult.tsx b/src/pages/GiftResult.tsx
new file mode 100644
index 0000000..a1361b8
--- /dev/null
+++ b/src/pages/GiftResult.tsx
@@ -0,0 +1,380 @@
+import { useCallback, useRef, useState } from 'react';
+import { useSearchParams, useNavigate } from 'react-router';
+import { useQuery } from '@tanstack/react-query';
+import { useTranslation } from 'react-i18next';
+import { motion } from 'framer-motion';
+import { giftApi } from '../api/gift';
+import { Spinner } from '@/components/ui/Spinner';
+import { AnimatedCheckmark } from '@/components/ui/AnimatedCheckmark';
+import { AnimatedCrossmark } from '@/components/ui/AnimatedCrossmark';
+
+const MAX_POLL_MS = 10 * 60 * 1000; // 10 minutes
+
+// ============================================================
+// Sub-components
+// ============================================================
+
+function PendingState() {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+ {t('gift.processing', 'Processing your gift...')}
+
+
+ {t('gift.processingDesc', 'Please wait while we process your payment')}
+
+
+
+ );
+}
+
+function DeliveredState({
+ recipientContact,
+ tariffName,
+ periodDays,
+ giftMessage,
+}: {
+ recipientContact: string | null;
+ tariffName: string | null;
+ periodDays: number | null;
+ giftMessage: string | null;
+}) {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+
{t('gift.sent', 'Gift sent!')}
+ {tariffName && periodDays !== null && (
+
+ {tariffName} — {periodDays} {t('gift.days', 'days')}
+
+ )}
+ {recipientContact && (
+
+ {t('gift.sentTo', {
+ contact: recipientContact,
+ defaultValue: `Sent to ${recipientContact}`,
+ })}
+
+ )}
+ {giftMessage && (
+
“{giftMessage}”
+ )}
+
+
+ navigate('/')}
+ className="flex w-full items-center justify-center rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
+ >
+ {t('gift.backToDashboard', 'Back to dashboard')}
+
+
+ );
+}
+
+function PendingActivationState({
+ recipientContact,
+ tariffName,
+ periodDays,
+}: {
+ recipientContact: string | null;
+ tariffName: string | null;
+ periodDays: number | null;
+}) {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+
+ return (
+
+ {/* Info icon */}
+
+
+
+
+ {t('gift.pendingActivation', 'Gift pending activation')}
+
+ {tariffName && periodDays !== null && (
+
+ {tariffName} — {periodDays} {t('gift.days', 'days')}
+
+ )}
+ {recipientContact && (
+
+ {t('gift.sentTo', {
+ contact: recipientContact,
+ defaultValue: `Sent to ${recipientContact}`,
+ })}
+
+ )}
+
+ {t(
+ 'gift.pendingActivationDesc',
+ 'The recipient currently has an active subscription. Your gift will be activated once their current subscription expires.',
+ )}
+
+
+
+ navigate('/')}
+ className="flex w-full items-center justify-center rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
+ >
+ {t('gift.backToDashboard', 'Back to dashboard')}
+
+
+ );
+}
+
+function FailedState() {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
+
+ {t('gift.failed', 'Something went wrong')}
+
+
+ {t('gift.failedDesc', 'Your gift could not be processed. Please try again.')}
+
+
+
+ navigate('/gift')}
+ className="flex w-full items-center justify-center rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
+ >
+ {t('gift.tryAgain', 'Try again')}
+
+
+ );
+}
+
+function PollTimedOutState({ onRetry }: { onRetry: () => void }) {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+ {t('gift.pollTimedOut', 'Taking longer than expected')}
+
+
+ {t(
+ 'gift.pollTimedOutDesc',
+ 'Payment processing is taking longer than usual. You can try checking again.',
+ )}
+
+
+
+ {t('common.retry', 'Retry')}
+
+
+ );
+}
+
+function NoTokenState() {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+
+ return (
+
+
+
+
{t('gift.invalidLink', 'Invalid link')}
+
+ {t('gift.invalidLinkDesc', 'This gift link is invalid or has expired.')}
+
+
+ navigate('/gift')}
+ className="rounded-xl bg-accent-500 px-6 py-3 text-sm font-medium text-white transition-colors hover:bg-accent-400"
+ >
+ {t('gift.giftNewSubscription', 'Gift a subscription')}
+
+
+ );
+}
+
+// ============================================================
+// Main Component
+// ============================================================
+
+export default function GiftResult() {
+ const [searchParams] = useSearchParams();
+ const token = searchParams.get('token');
+ const mode = searchParams.get('mode');
+
+ const pollStart = useRef(Date.now());
+ const [pollTimedOut, setPollTimedOut] = useState(false);
+
+ const isBalanceMode = mode === 'balance';
+
+ const {
+ data: status,
+ isError,
+ refetch,
+ } = useQuery({
+ queryKey: ['gift-status', token],
+ queryFn: () => giftApi.getPurchaseStatus(token!),
+ enabled: !!token && !pollTimedOut,
+ refetchInterval: (query) => {
+ // Balance mode: fetch once, no polling
+ if (isBalanceMode) return false;
+
+ const s = query.state.data?.status;
+ if (s === 'delivered' || s === 'failed' || s === 'pending_activation') return false;
+
+ // Check poll timeout
+ if (Date.now() - pollStart.current > MAX_POLL_MS) {
+ setPollTimedOut(true);
+ return false;
+ }
+
+ return 3000;
+ },
+ retry: 2,
+ });
+
+ const handleRetryPoll = useCallback(() => {
+ pollStart.current = Date.now();
+ setPollTimedOut(false);
+ refetch();
+ }, [refetch]);
+
+ // No token
+ if (!token) {
+ return (
+
+ );
+ }
+
+ const isDelivered = status?.status === 'delivered';
+ const isPendingActivation = status?.status === 'pending_activation';
+ const isFailed = status?.status === 'failed';
+
+ return (
+
+
+ {isError ? (
+
+ ) : isDelivered ? (
+
+ ) : isPendingActivation ? (
+
+ ) : isFailed ? (
+
+ ) : pollTimedOut ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
diff --git a/src/pages/GiftSubscription.tsx b/src/pages/GiftSubscription.tsx
new file mode 100644
index 0000000..da459c0
--- /dev/null
+++ b/src/pages/GiftSubscription.tsx
@@ -0,0 +1,983 @@
+import { useState, useMemo, useEffect } from 'react';
+import { useNavigate, Link } from 'react-router';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { useTranslation } from 'react-i18next';
+import { motion, AnimatePresence } from 'framer-motion';
+import { giftApi } from '../api/gift';
+import type {
+ GiftConfig,
+ GiftTariff,
+ GiftTariffPeriod,
+ GiftPaymentMethod,
+ GiftPurchaseRequest,
+} from '../api/gift';
+
+import { cn } from '../lib/utils';
+import { getApiErrorMessage } from '../utils/api-error';
+import { formatPrice } from '../utils/format';
+
+// ============================================================
+// Helpers
+// ============================================================
+
+function detectContactType(value: string): 'email' | 'telegram' {
+ return value.startsWith('@') ? 'telegram' : 'email';
+}
+
+function isValidContact(value: string): boolean {
+ const trimmed = value.trim();
+ if (!trimmed) return false;
+ if (trimmed.startsWith('@')) return trimmed.length >= 4;
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed);
+}
+
+function formatPeriodLabel(
+ days: number,
+ t: (key: string, options?: Record) => string,
+): string {
+ const key = `landing.periodLabels.d${days}`;
+ const result = t(key);
+ if (result !== key) return result;
+
+ const months = Math.floor(days / 30);
+ const remainder = days % 30;
+ if (months > 0 && remainder === 0) {
+ return t('landing.periodLabels.nMonths', { count: months });
+ }
+ return t('landing.periodLabels.nDays', { count: days });
+}
+
+// ============================================================
+// Sub-components
+// ============================================================
+
+function LoadingSkeleton() {
+ return (
+
+ );
+}
+
+function ErrorState({ message }: { message: string }) {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
{t('gift.error', 'Error')}
+
{message}
+
+
+ );
+}
+
+function DisabledState() {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+
+ useEffect(() => {
+ const timer = setTimeout(() => navigate('/'), 3000);
+ return () => clearTimeout(timer);
+ }, [navigate]);
+
+ return (
+
+
+
+
+ {t('gift.disabled', 'Gift subscriptions are currently unavailable')}
+
+
+ {t('gift.disabledRedirect', 'Redirecting to dashboard...')}
+
+
+
+ );
+}
+
+function PeriodTabs({
+ periods,
+ selectedDays,
+ onSelect,
+}: {
+ periods: GiftTariffPeriod[];
+ selectedDays: number;
+ onSelect: (days: number) => void;
+}) {
+ const { t } = useTranslation();
+
+ return (
+
+ {periods.map((period) => (
+ onSelect(period.days)}
+ className={cn(
+ 'whitespace-nowrap rounded-full px-4 py-2 text-sm font-medium transition-all duration-200',
+ selectedDays === period.days
+ ? 'bg-accent-500 text-white shadow-lg shadow-accent-500/25'
+ : 'bg-dark-800/50 text-dark-300 hover:bg-dark-700/50 hover:text-dark-100',
+ )}
+ >
+ {formatPeriodLabel(period.days, t)}
+
+ ))}
+
+ );
+}
+
+function TariffCard({
+ tariff,
+ isSelected,
+ selectedPeriod,
+ onSelect,
+}: {
+ tariff: GiftTariff;
+ isSelected: boolean;
+ selectedPeriod: GiftTariffPeriod | undefined;
+ onSelect: () => void;
+}) {
+ const { t } = useTranslation();
+
+ return (
+
+ {/* Header */}
+
+
+
{tariff.name}
+ {tariff.description && (
+
{tariff.description}
+ )}
+
+
+ {isSelected && (
+
+
+
+ )}
+
+
+
+ {/* Info row */}
+
+
+
+
+
+ {tariff.traffic_limit_gb === 0 ? '\u221E' : tariff.traffic_limit_gb}{' '}
+ {t('landing.gb', 'GB')}
+
+
+
+
+
+ {tariff.device_limit} {t('landing.devices', 'devices')}
+
+
+
+ {/* Price */}
+ {selectedPeriod && (
+
+
+
+ {formatPrice(selectedPeriod.price_kopeks)}
+
+ {selectedPeriod.original_price_kopeks != null &&
+ selectedPeriod.original_price_kopeks > selectedPeriod.price_kopeks && (
+ <>
+
+ {formatPrice(selectedPeriod.original_price_kopeks)}
+
+ {selectedPeriod.discount_percent != null && (
+
+ -{selectedPeriod.discount_percent}%
+
+ )}
+ >
+ )}
+
+
+ )}
+
+ );
+}
+
+function PaymentModeToggle({
+ mode,
+ onToggle,
+ balanceLabel,
+}: {
+ mode: 'balance' | 'gateway';
+ onToggle: (mode: 'balance' | 'gateway') => void;
+ balanceLabel: string;
+}) {
+ const { t } = useTranslation();
+
+ return (
+
+ onToggle('balance')}
+ aria-pressed={mode === 'balance'}
+ className={cn(
+ 'flex-1 rounded-lg px-4 py-2.5 text-sm font-medium transition-all duration-200',
+ mode === 'balance'
+ ? 'bg-dark-700 text-dark-50 shadow-sm'
+ : 'text-dark-400 hover:text-dark-200',
+ )}
+ >
+ {balanceLabel}
+
+ onToggle('gateway')}
+ aria-pressed={mode === 'gateway'}
+ className={cn(
+ 'flex-1 rounded-lg px-4 py-2.5 text-sm font-medium transition-all duration-200',
+ mode === 'gateway'
+ ? 'bg-dark-700 text-dark-50 shadow-sm'
+ : 'text-dark-400 hover:text-dark-200',
+ )}
+ >
+ {t('gift.viaGateway', 'Via payment gateway')}
+
+
+ );
+}
+
+function PaymentMethodCard({
+ method,
+ isSelected,
+ selectedSubOption,
+ onSelect,
+ onSelectSubOption,
+}: {
+ method: GiftPaymentMethod;
+ isSelected: boolean;
+ selectedSubOption: string | null;
+ onSelect: () => void;
+ onSelectSubOption: (subOptionId: string) => void;
+}) {
+ const hasSubOptions = method.sub_options && method.sub_options.length > 1;
+
+ return (
+
+
+ {/* Icon */}
+ {method.icon_url && (
+
+
+
+ )}
+
+ {/* Text */}
+
+
{method.display_name}
+ {method.description && (
+
{method.description}
+ )}
+
+
+ {/* Radio */}
+
+
+
+ {/* Sub-options */}
+ {isSelected && hasSubOptions && (
+
+
+ {method.sub_options!.map((opt) => (
+ onSelectSubOption(opt.id)}
+ className={cn(
+ 'rounded-xl px-4 py-2 text-sm font-medium transition-all duration-200',
+ selectedSubOption === opt.id
+ ? 'bg-accent-500 text-white shadow-sm shadow-accent-500/25'
+ : 'bg-dark-800/50 text-dark-300 hover:bg-dark-700/50 hover:text-dark-100',
+ )}
+ >
+ {opt.name}
+
+ ))}
+
+
+ )}
+
+ );
+}
+
+function RecipientSection({ value, onChange }: { value: string; onChange: (v: string) => void }) {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ {t('gift.recipient', 'Recipient')}
+
+
onChange(e.target.value)}
+ placeholder={t('gift.recipientPlaceholder', 'email@example.com or @telegram')}
+ className="w-full rounded-xl border border-dark-700/50 bg-dark-800/50 px-4 py-3 text-sm text-dark-50 placeholder-dark-500 outline-none transition-colors focus:border-accent-500/50 focus:ring-1 focus:ring-accent-500/25"
+ />
+
+ {t(
+ 'gift.recipientHint',
+ 'Enter the email or Telegram username of the person you want to gift',
+ )}
+
+
+
+ );
+}
+
+function GiftMessageSection({ value, onChange }: { value: string; onChange: (v: string) => void }) {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+ {t('gift.giftMessage', 'Personal message')}
+
+
+
+
+ );
+}
+
+function GiftSummaryCard({
+ config,
+ selectedTariff,
+ selectedPeriod,
+ currentPrice,
+ paymentMode,
+ isSubmitting,
+ canSubmit,
+ submitError,
+ insufficientBalance,
+ onSubmit,
+}: {
+ config: GiftConfig;
+ selectedTariff: GiftTariff | undefined;
+ selectedPeriod: GiftTariffPeriod | undefined;
+ currentPrice: number;
+ paymentMode: 'balance' | 'gateway';
+ isSubmitting: boolean;
+ canSubmit: boolean;
+ submitError: string | null;
+ insufficientBalance: boolean;
+ onSubmit: () => void;
+}) {
+ const { t } = useTranslation();
+
+ return (
+
+ {/* Summary */}
+
+ {selectedTariff && (
+
+
+ {t('gift.selectedTariff', 'Tariff')}
+
+
{selectedTariff.name}
+
+ )}
+ {selectedPeriod && (
+
+
+ {t('gift.period', 'Period')}
+
+
+ {formatPeriodLabel(selectedPeriod.days, t)}
+
+
+ )}
+
+
+ {t('gift.total', 'Total')}
+
+
+ {formatPrice(currentPrice)}
+ {selectedPeriod?.original_price_kopeks != null &&
+ selectedPeriod.original_price_kopeks > selectedPeriod.price_kopeks && (
+ <>
+
+ {formatPrice(selectedPeriod.original_price_kopeks)}
+
+ {selectedPeriod.discount_percent != null && (
+
+ -{selectedPeriod.discount_percent}%
+
+ )}
+ >
+ )}
+
+
+
+ {/* Balance info for balance mode */}
+ {paymentMode === 'balance' && (
+
+
+ {t('gift.yourBalance', 'Your balance')}
+
+
+ {formatPrice(config.balance_kopeks)}
+
+
+ )}
+
+
+ {/* Insufficient balance warning */}
+
+ {insufficientBalance && (
+
+
+ {t('gift.insufficientBalance', 'Insufficient balance.')}{' '}
+
+ {t('gift.topUp', 'Top up')}
+
+
+
+ )}
+
+
+ {/* Error */}
+
+ {submitError && (
+
+ {submitError}
+
+ )}
+
+
+ {/* Gift button */}
+
+ {isSubmitting ? (
+
+ ) : (
+ <>
+ {t('gift.giftButton', 'Gift')} {formatPrice(currentPrice)}
+ >
+ )}
+
+
+ );
+}
+
+// ============================================================
+// Main Component
+// ============================================================
+
+export default function GiftSubscription() {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+
+ // Fetch config
+ const {
+ data: config,
+ isLoading,
+ error,
+ } = useQuery({
+ queryKey: ['gift-config'],
+ queryFn: giftApi.getConfig,
+ staleTime: 30_000,
+ });
+
+ // Selection state
+ const [selectedTariffId, setSelectedTariffId] = useState(null);
+ const [selectedPeriodDays, setSelectedPeriodDays] = useState(null);
+ const [recipientValue, setRecipientValue] = useState('');
+ const [giftMessage, setGiftMessage] = useState('');
+ const [paymentMode, setPaymentMode] = useState<'balance' | 'gateway'>('balance');
+ const [selectedMethod, setSelectedMethod] = useState(null);
+ const [selectedSubOption, setSelectedSubOption] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [submitError, setSubmitError] = useState(null);
+
+ // Collect ALL unique periods across ALL tariffs
+ const allPeriods = useMemo(() => {
+ if (!config) return [];
+ const periodMap = new Map();
+ for (const tariff of config.tariffs) {
+ for (const period of tariff.periods) {
+ if (!periodMap.has(period.days)) {
+ periodMap.set(period.days, period);
+ }
+ }
+ }
+ return Array.from(periodMap.values()).sort((a, b) => a.days - b.days);
+ }, [config]);
+
+ // Filter tariffs to only those that have the selected period
+ const visibleTariffs = useMemo(() => {
+ if (!config || !selectedPeriodDays) return config?.tariffs ?? [];
+ return config.tariffs.filter((tariff) =>
+ tariff.periods.some((p) => p.days === selectedPeriodDays),
+ );
+ }, [config, selectedPeriodDays]);
+
+ // Auto-select first tariff, period, method on config load
+ useEffect(() => {
+ if (!config) return;
+
+ if (allPeriods.length > 0 && selectedPeriodDays === null) {
+ setSelectedPeriodDays(allPeriods[0].days);
+ }
+
+ if (visibleTariffs.length > 0 && selectedTariffId === null) {
+ setSelectedTariffId(visibleTariffs[0].id);
+ }
+
+ if (config.payment_methods.length > 0 && selectedMethod === null) {
+ const firstMethod = config.payment_methods[0];
+ setSelectedMethod(firstMethod.method_id);
+ if (firstMethod.sub_options && firstMethod.sub_options.length >= 1) {
+ setSelectedSubOption(firstMethod.sub_options[0].id);
+ } else {
+ setSelectedSubOption(null);
+ }
+ }
+ }, [config, allPeriods, visibleTariffs, selectedTariffId, selectedPeriodDays, selectedMethod]);
+
+ // When period changes, auto-select first visible tariff if current is hidden
+ useEffect(() => {
+ if (!visibleTariffs.length) return;
+ const currentVisible = visibleTariffs.find((tariff) => tariff.id === selectedTariffId);
+ if (!currentVisible) {
+ setSelectedTariffId(visibleTariffs[0].id);
+ }
+ }, [visibleTariffs, selectedTariffId]);
+
+ // Derived data
+ const selectedTariff = useMemo(
+ () => config?.tariffs.find((tr) => tr.id === selectedTariffId),
+ [config?.tariffs, selectedTariffId],
+ );
+
+ const selectedPeriod = useMemo(
+ () => selectedTariff?.periods.find((p) => p.days === selectedPeriodDays),
+ [selectedTariff, selectedPeriodDays],
+ );
+
+ const currentPrice = selectedPeriod?.price_kopeks ?? 0;
+
+ const insufficientBalance =
+ paymentMode === 'balance' && config != null && config.balance_kopeks < currentPrice;
+
+ // Validation
+ const canSubmit = useMemo(() => {
+ if (!selectedTariffId || !selectedPeriodDays) return false;
+ if (!isValidContact(recipientValue)) return false;
+ if (paymentMode === 'gateway' && !selectedMethod) return false;
+ if (insufficientBalance) return false;
+ return true;
+ }, [
+ selectedTariffId,
+ selectedPeriodDays,
+ recipientValue,
+ paymentMode,
+ selectedMethod,
+ insufficientBalance,
+ ]);
+
+ // Purchase mutation
+ const purchaseMutation = useMutation({
+ mutationFn: (data: GiftPurchaseRequest) => giftApi.createPurchase(data),
+ onSuccess: (result) => {
+ if (result.payment_url) {
+ window.location.href = result.payment_url;
+ } else {
+ // Balance mode - show success
+ queryClient.invalidateQueries({ queryKey: ['balance'] });
+ queryClient.invalidateQueries({ queryKey: ['gift-config'] });
+ navigate('/gift/result?token=' + result.purchase_token + '&mode=balance');
+ }
+ },
+ onError: (err) => {
+ const msg = getApiErrorMessage(
+ err,
+ t('gift.purchaseError', 'Something went wrong. Please try again.'),
+ );
+ setSubmitError(msg);
+ setIsSubmitting(false);
+ },
+ });
+
+ // Submit handler
+ const handleSubmit = () => {
+ if (!canSubmit || isSubmitting) return;
+
+ setIsSubmitting(true);
+ setSubmitError(null);
+
+ let paymentMethod: string | undefined;
+ if (paymentMode === 'gateway' && selectedMethod) {
+ paymentMethod = selectedMethod;
+ if (selectedSubOption) {
+ paymentMethod = `${paymentMethod}_${selectedSubOption}`;
+ }
+ }
+
+ const data: GiftPurchaseRequest = {
+ tariff_id: selectedTariffId!,
+ period_days: selectedPeriodDays!,
+ recipient_type: detectContactType(recipientValue),
+ recipient_value: recipientValue.trim(),
+ gift_message: giftMessage.trim() || undefined,
+ payment_mode: paymentMode,
+ payment_method: paymentMethod,
+ };
+
+ purchaseMutation.mutate(data);
+ };
+
+ // Balance label with current amount
+ const balanceLabel = useMemo(() => {
+ if (!config) return t('gift.fromBalance', 'From balance');
+ return `${t('gift.fromBalance', 'From balance')} (${formatPrice(config.balance_kopeks)})`;
+ }, [config, t]);
+
+ // Loading state
+ if (isLoading) {
+ return ;
+ }
+
+ // Error state
+ if (error || !config) {
+ const errMsg = getApiErrorMessage(error, t('gift.notFound', 'Gift configuration not found'));
+ return ;
+ }
+
+ // Disabled state
+ if (!config.is_enabled) {
+ return ;
+ }
+
+ const showTariffCards = visibleTariffs.length > 1;
+
+ return (
+
+
+ {/* Header */}
+
+
+ {t('gift.title', 'Gift Subscription')}
+
+
+ {t('gift.subtitle', 'Give the gift of secure internet to someone special')}
+
+
+
+ {/* Two-column layout */}
+
+ {/* Left column */}
+
+ {/* Period tabs */}
+ {allPeriods.length > 0 && (
+
+
+ {t('gift.choosePeriod', 'Choose period')}
+
+
+
+ )}
+
+ {/* Tariff cards */}
+ {showTariffCards && (
+
+
+ {t('gift.chooseTariff', 'Choose tariff')}
+
+
+ {visibleTariffs.map((tariff) => {
+ const period = tariff.periods.find((p) => p.days === selectedPeriodDays);
+ return (
+ setSelectedTariffId(tariff.id)}
+ />
+ );
+ })}
+
+
+ )}
+
+ {/* Recipient */}
+
+
+ {t('gift.recipientSection', 'Recipient')}
+
+ {
+ setRecipientValue(v);
+ setSubmitError(null);
+ }}
+ />
+
+
+ {/* Gift message */}
+
+
+ {t('gift.messageSection', 'Personal message')}
+
+
+
+
+ {/* Payment mode toggle */}
+
+
+ {t('gift.paymentModeSection', 'Payment method')}
+
+
+
+
+ {/* Payment method cards (gateway mode only) */}
+
+ {paymentMode === 'gateway' && config.payment_methods.length > 0 && (
+
+
+ {config.payment_methods.map((method) => (
+
{
+ setSelectedMethod(method.method_id);
+ if (method.sub_options && method.sub_options.length >= 1) {
+ setSelectedSubOption(method.sub_options[0].id);
+ } else {
+ setSelectedSubOption(null);
+ }
+ }}
+ onSelectSubOption={setSelectedSubOption}
+ />
+ ))}
+
+
+ )}
+
+
+
+ {/* Right column (sticky sidebar / bottom on mobile) */}
+
+
+
+
+
+
+ );
+}