From 9d1b1ce6c3aaf2c1080d15e78de3b8a42bf615fe Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 19 Jan 2026 00:35:44 +0300 Subject: [PATCH 1/3] Add files via upload --- src/components/InsufficientBalancePrompt.tsx | 200 +++++++++++++++++++ src/components/TopUpModal.tsx | 22 +- 2 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 src/components/InsufficientBalancePrompt.tsx diff --git a/src/components/InsufficientBalancePrompt.tsx b/src/components/InsufficientBalancePrompt.tsx new file mode 100644 index 0000000..243f508 --- /dev/null +++ b/src/components/InsufficientBalancePrompt.tsx @@ -0,0 +1,200 @@ +import { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useQuery } from '@tanstack/react-query' +import { balanceApi } from '../api/balance' +import { useCurrency } from '../hooks/useCurrency' +import TopUpModal from './TopUpModal' +import type { PaymentMethod } from '../types' + +interface InsufficientBalancePromptProps { + /** Amount missing in kopeks */ + missingAmountKopeks: number + /** Optional custom message */ + message?: string + /** Compact mode for inline use */ + compact?: boolean + /** Additional className */ + className?: string +} + +export default function InsufficientBalancePrompt({ + missingAmountKopeks, + message, + compact = false, + className = '', +}: InsufficientBalancePromptProps) { + const { t } = useTranslation() + const { formatAmount, currencySymbol } = useCurrency() + const [showMethodSelect, setShowMethodSelect] = useState(false) + const [selectedMethod, setSelectedMethod] = useState(null) + + const { data: paymentMethods } = useQuery({ + queryKey: ['payment-methods'], + queryFn: balanceApi.getPaymentMethods, + enabled: showMethodSelect, + }) + + const missingRubles = missingAmountKopeks / 100 + const displayAmount = formatAmount(missingRubles) + + const handleMethodSelect = (method: PaymentMethod) => { + setSelectedMethod(method) + setShowMethodSelect(false) + } + + if (compact) { + return ( + <> +
+
+ + + + + {message || t('balance.insufficientFunds')}: {displayAmount} {currencySymbol} + +
+ +
+ + {showMethodSelect && ( + setShowMethodSelect(false)} + /> + )} + + {selectedMethod && ( + setSelectedMethod(null)} + /> + )} + + ) + } + + return ( + <> +
+
+
+ + + +
+
+
+ {t('balance.insufficientFunds')} +
+
+ {message || t('balance.topUpToComplete')} +
+
+
+ {t('balance.missing')}: {displayAmount} {currencySymbol} +
+
+
+
+ +
+ + {showMethodSelect && ( + setShowMethodSelect(false)} + /> + )} + + {selectedMethod && ( + setSelectedMethod(null)} + /> + )} + + ) +} + +interface PaymentMethodModalProps { + paymentMethods: PaymentMethod[] | undefined + onSelect: (method: PaymentMethod) => void + onClose: () => void +} + +function PaymentMethodModal({ paymentMethods, onSelect, onClose }: PaymentMethodModalProps) { + const { t } = useTranslation() + const { formatAmount, currencySymbol } = useCurrency() + + return ( +
+
+
+

{t('balance.selectPaymentMethod')}

+ +
+ + {!paymentMethods ? ( +
+
+
+ ) : paymentMethods.length === 0 ? ( +
+ {t('balance.noPaymentMethods')} +
+ ) : ( +
+ {paymentMethods.map((method) => { + const methodKey = method.id.toLowerCase().replace(/-/g, '_') + const translatedName = t(`balance.paymentMethods.${methodKey}.name`, { defaultValue: '' }) + const translatedDesc = t(`balance.paymentMethods.${methodKey}.description`, { defaultValue: '' }) + + return ( + + ) + })} +
+ )} +
+
+ ) +} diff --git a/src/components/TopUpModal.tsx b/src/components/TopUpModal.tsx index c6d0107..772c949 100644 --- a/src/components/TopUpModal.tsx +++ b/src/components/TopUpModal.tsx @@ -35,12 +35,28 @@ const openPaymentLink = (url: string, reservedWindow?: Window | null) => { window.location.href = url } -interface TopUpModalProps { method: PaymentMethod; onClose: () => void } +interface TopUpModalProps { + method: PaymentMethod + onClose: () => void + /** Pre-filled amount in rubles (will be converted to user's currency) */ + initialAmountRubles?: number +} -export default function TopUpModal({ method, onClose }: TopUpModalProps) { +export default function TopUpModal({ method, onClose, initialAmountRubles }: TopUpModalProps) { const { t } = useTranslation() const { formatAmount, currencySymbol, convertAmount, convertToRub, targetCurrency } = useCurrency() - const [amount, setAmount] = useState('') + + // Calculate initial amount in user's currency + const getInitialAmount = (): string => { + if (!initialAmountRubles || initialAmountRubles <= 0) return '' + const converted = convertAmount(initialAmountRubles) + if (targetCurrency === 'IRR' || targetCurrency === 'RUB') { + return Math.ceil(converted).toString() + } + return converted.toFixed(2) + } + + const [amount, setAmount] = useState(getInitialAmount) const [error, setError] = useState(null) const [selectedOption, setSelectedOption] = useState( method.options && method.options.length > 0 ? method.options[0].id : null From a7f0d705ff54cf35d2024f57d5057ebff8382ede Mon Sep 17 00:00:00 2001 From: Egor Date: Mon, 19 Jan 2026 00:36:19 +0300 Subject: [PATCH 2/3] Add files via upload --- src/pages/Subscription.tsx | 82 ++++++++++++++++++++++++-------------- src/pages/Wheel.tsx | 22 +++++----- 2 files changed, 65 insertions(+), 39 deletions(-) diff --git a/src/pages/Subscription.tsx b/src/pages/Subscription.tsx index 5762afb..a827d44 100644 --- a/src/pages/Subscription.tsx +++ b/src/pages/Subscription.tsx @@ -7,6 +7,7 @@ import { subscriptionApi } from '../api/subscription' import { promoApi } from '../api/promo' import type { PurchaseSelection, PeriodOption, Tariff, TariffPeriod, ClassicPurchaseOptions } from '../types' import ConnectionModal from '../components/ConnectionModal' +import InsufficientBalancePrompt from '../components/InsufficientBalancePrompt' import { useCurrency } from '../hooks/useCurrency' // Helper to extract error message from axios/api errors @@ -772,9 +773,16 @@ export default function Subscription() {
)} + {devicePriceData && purchaseOptions && devicePriceData.total_price_kopeks && devicePriceData.total_price_kopeks > purchaseOptions.balance_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 && ( + + )} + + + ) + })()} {trafficPurchaseMutation.isError && (
@@ -1050,9 +1073,10 @@ export default function Subscription() {
{!switchPreview.has_enough_balance && switchPreview.upgrade_cost_kopeks > 0 && ( -
- {t('subscription.switchTariff.notEnoughBalance')}: {switchPreview.missing_amount_label} -
+ )}