fix(format): convert price by exchange rate on landings and gift page

formatPrice from utils/format.ts only swapped the currency symbol
(220 ₽ -> ¥220) without converting the underlying amount, because it
had no source of exchange rates and the landing/gift pages never called
the useCurrency hook that knew how to fetch them.

- Add a module-level rates cache in utils/format with setExchangeRates
  setter. formatPrice now converts kopeks/100 from RUB to the target
  currency via currencyApi.convertFromRub when rates are cached.
- useCurrency pushes its loaded rates into that cache so any subsequent
  formatPrice call benefits, including subcomponents that cannot easily
  receive a prop.
- Call useCurrency in QuickPurchase, GiftSubscription, and
  AdminLandingEditor — the only entry points whose subcomponents still
  use the synchronous formatPrice (everything else already routes
  through useCurrency.formatAmount).

For RU locale behavior is unchanged. For EN/ZH/FA the amount is now
divided by the per-currency rate and formatted via Intl.NumberFormat
with 2 fraction digits (0 for IRR since amounts are large).
This commit is contained in:
Fringg
2026-05-13 04:56:15 +03:00
parent 1fdafbd0a1
commit 6ffd0ae824
5 changed files with 54 additions and 9 deletions

View File

@@ -13,6 +13,7 @@ import {
} from '../api/landings';
import { tariffsApi, TariffListItem, PeriodPrice } from '../api/tariffs';
import { formatPrice } from '../utils/format';
import { useCurrency } from '../hooks/useCurrency';
import { adminPaymentMethodsApi } from '../api/adminPaymentMethods';
import { Toggle, LocaleTabs, LocalizedInput } from '../components/admin';
import { BackgroundConfigEditor } from '../components/admin/BackgroundConfigEditor';
@@ -97,6 +98,9 @@ export default function AdminLandingEditor() {
const { capabilities } = usePlatform();
const isEdit = !!id;
// Прогреваем кэш курсов валют для formatPrice (preview лендинга в не-RU локали).
useCurrency();
// Section visibility
const [openSections, setOpenSections] = useState<Record<string, boolean>>({
general: true,

View File

@@ -19,6 +19,7 @@ import { cn } from '../lib/utils';
import { copyToClipboard } from '../utils/clipboard';
import { getApiErrorMessage } from '../utils/api-error';
import { formatPrice } from '../utils/format';
import { useCurrency } from '../hooks/useCurrency';
import { usePlatform, useHaptic } from '@/platform';
function GiftIcon({ className }: { className?: string }) {
@@ -1286,6 +1287,9 @@ export default function GiftSubscription() {
const { t } = useTranslation();
const [searchParams] = useSearchParams();
// Прогреваем кэш курсов валют для formatPrice (см. QuickPurchase).
useCurrency();
// URL params: ?tab=activate&code=TOKEN for auto-activation
const urlTab = searchParams.get('tab') as TabId | null;
const rawCode = searchParams.get('code');

View File

@@ -19,6 +19,7 @@ import LanguageSwitcher from '../components/LanguageSwitcher';
import { cn } from '../lib/utils';
import { getApiErrorMessage } from '../utils/api-error';
import { formatPrice } from '../utils/format';
import { useCurrency } from '../hooks/useCurrency';
function detectContactType(value: string): 'email' | 'telegram' {
return value.startsWith('@') ? 'telegram' : 'email';
@@ -795,6 +796,10 @@ export default function QuickPurchase() {
const { t, i18n } = useTranslation();
const queryClient = useQueryClient();
// Подгружаем курсы валют, чтобы formatPrice конвертировал суммы для не-RU локалей
// (хук кладёт rates в глобальный кэш utils/format.ts).
useCurrency();
// Fetch config
const {
data: config,