mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
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).
141 lines
3.9 KiB
TypeScript
141 lines
3.9 KiB
TypeScript
import { useCallback, useEffect, useMemo } from 'react';
|
||
import { useQuery } from '@tanstack/react-query';
|
||
import { useTranslation } from 'react-i18next';
|
||
import { currencyApi, type ExchangeRates } from '../api/currency';
|
||
import { setExchangeRates as setGlobalExchangeRates } from '../utils/format';
|
||
|
||
// Map language to currency
|
||
const LANGUAGE_CURRENCY_MAP: Record<string, keyof ExchangeRates | 'RUB'> = {
|
||
ru: 'RUB',
|
||
en: 'USD',
|
||
zh: 'CNY',
|
||
fa: 'IRR',
|
||
};
|
||
|
||
// Default rates for fallback
|
||
const DEFAULT_RATES: ExchangeRates = {
|
||
USD: 100,
|
||
CNY: 14,
|
||
IRR: 0.0024,
|
||
};
|
||
|
||
export function useCurrency() {
|
||
const { i18n, t } = useTranslation();
|
||
|
||
// Fetch exchange rates
|
||
const { data: exchangeRates = DEFAULT_RATES } = useQuery({
|
||
queryKey: ['exchange-rates'],
|
||
queryFn: currencyApi.getExchangeRates,
|
||
staleTime: 60 * 60 * 1000, // 1 hour
|
||
refetchOnWindowFocus: false,
|
||
retry: 2,
|
||
});
|
||
|
||
// Делимся курсом с синхронным formatPrice из utils/format.ts —
|
||
// у него нет своего источника rates, а на лендинге его дёргают subcomponents.
|
||
useEffect(() => {
|
||
setGlobalExchangeRates(exchangeRates);
|
||
}, [exchangeRates]);
|
||
|
||
// Get current language and currency
|
||
const currentLanguage = i18n.language;
|
||
const targetCurrency = LANGUAGE_CURRENCY_MAP[currentLanguage] || 'USD';
|
||
|
||
// Check if current language is Russian (no conversion needed)
|
||
const isRussian = currentLanguage === 'ru';
|
||
|
||
// Get currency symbol from translations
|
||
const currencySymbol = t('common.currency');
|
||
|
||
// Format amount with currency conversion
|
||
const formatAmount = useCallback(
|
||
(rubAmount: number, decimals: number = 2): string => {
|
||
if (isRussian) {
|
||
return rubAmount.toFixed(decimals);
|
||
}
|
||
|
||
// Convert to target currency
|
||
const convertedAmount = currencyApi.convertFromRub(
|
||
rubAmount,
|
||
targetCurrency as keyof ExchangeRates,
|
||
exchangeRates,
|
||
);
|
||
|
||
// For IRR (Iranian Toman), use no decimals as amounts are large
|
||
if (targetCurrency === 'IRR') {
|
||
return Math.round(convertedAmount).toLocaleString('fa-IR');
|
||
}
|
||
|
||
return convertedAmount.toFixed(decimals);
|
||
},
|
||
[isRussian, targetCurrency, exchangeRates],
|
||
);
|
||
|
||
// Format amount with currency symbol
|
||
const formatWithCurrency = useCallback(
|
||
(rubAmount: number, decimals: number = 2): string => {
|
||
return `${formatAmount(rubAmount, decimals)} ${currencySymbol}`;
|
||
},
|
||
[formatAmount, currencySymbol],
|
||
);
|
||
|
||
// Format amount with + sign (for earnings/bonuses)
|
||
const formatPositive = useCallback(
|
||
(rubAmount: number, decimals: number = 2): string => {
|
||
return `+${formatAmount(rubAmount, decimals)} ${currencySymbol}`;
|
||
},
|
||
[formatAmount, currencySymbol],
|
||
);
|
||
|
||
// Get raw converted amount (for calculations)
|
||
const convertAmount = useCallback(
|
||
(rubAmount: number): number => {
|
||
if (isRussian) {
|
||
return rubAmount;
|
||
}
|
||
return currencyApi.convertFromRub(
|
||
rubAmount,
|
||
targetCurrency as keyof ExchangeRates,
|
||
exchangeRates,
|
||
);
|
||
},
|
||
[isRussian, targetCurrency, exchangeRates],
|
||
);
|
||
|
||
// Convert from user's currency back to rubles
|
||
const convertToRub = useCallback(
|
||
(amount: number): number => {
|
||
if (isRussian) {
|
||
return amount;
|
||
}
|
||
return currencyApi.convertToRub(amount, targetCurrency as keyof ExchangeRates, exchangeRates);
|
||
},
|
||
[isRussian, targetCurrency, exchangeRates],
|
||
);
|
||
|
||
return useMemo(
|
||
() => ({
|
||
exchangeRates,
|
||
targetCurrency,
|
||
isRussian,
|
||
currencySymbol,
|
||
formatAmount,
|
||
formatWithCurrency,
|
||
formatPositive,
|
||
convertAmount,
|
||
convertToRub,
|
||
}),
|
||
[
|
||
exchangeRates,
|
||
targetCurrency,
|
||
isRussian,
|
||
currencySymbol,
|
||
formatAmount,
|
||
formatWithCurrency,
|
||
formatPositive,
|
||
convertAmount,
|
||
convertToRub,
|
||
],
|
||
);
|
||
}
|