diff --git a/frontend/src/proprietary/components/shared/UpgradeBanner.tsx b/frontend/src/proprietary/components/shared/UpgradeBanner.tsx index 3f09104ff9..bbb6a729b4 100644 --- a/frontend/src/proprietary/components/shared/UpgradeBanner.tsx +++ b/frontend/src/proprietary/components/shared/UpgradeBanner.tsx @@ -75,7 +75,7 @@ const UpgradeBanner: React.FC = () => { // Handle upgrade button click const handleUpgrade = () => { openCheckout('server', { - currency: 'usd', + // Currency auto-detected from locale in CheckoutContext minimumSeats: 1, onSuccess: () => { // Banner will auto-hide on next render when license is detected diff --git a/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx b/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx index 061aeedb33..9be46d1d83 100644 --- a/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx +++ b/frontend/src/proprietary/components/shared/config/configSections/AdminPlanSection.tsx @@ -10,12 +10,16 @@ import StaticPlanSection from '@app/components/shared/config/configSections/plan import { alert } from '@app/components/toast'; import LocalIcon from '@app/components/shared/LocalIcon'; import { isSupabaseConfigured } from '@app/services/supabaseClient'; +import { getPreferredCurrency, setCachedCurrency } from '@app/utils/currencyDetection'; const AdminPlanSection: React.FC = () => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { openCheckout } = useCheckout(); const { licenseInfo, refetchLicense } = useLicense(); - const [currency, setCurrency] = useState('usd'); + const [currency, setCurrency] = useState(() => { + // Initialize with auto-detected currency on first render + return getPreferredCurrency(i18n.language); + }); const [useStaticVersion, setUseStaticVersion] = useState(false); const [showLicenseKey, setShowLicenseKey] = useState(false); const [licenseKeyInput, setLicenseKeyInput] = useState(''); @@ -102,6 +106,12 @@ const AdminPlanSection: React.FC = () => { } }, [licenseInfo, t]); + const handleCurrencyChange = useCallback((newCurrency: string) => { + setCurrency(newCurrency); + // Persist user's manual selection to localStorage + setCachedCurrency(newCurrency); + }, []); + const handleUpgradeClick = useCallback( (planGroup: PlanTierGroup) => { // Only allow upgrades for server and enterprise tiers @@ -171,7 +181,7 @@ const AdminPlanSection: React.FC = () => { onUpgradeClick={handleUpgradeClick} onManageClick={handleManageClick} currency={currency} - onCurrencyChange={setCurrency} + onCurrencyChange={handleCurrencyChange} currencyOptions={currencyOptions} /> diff --git a/frontend/src/proprietary/contexts/CheckoutContext.tsx b/frontend/src/proprietary/contexts/CheckoutContext.tsx index e135b24a38..13f753199e 100644 --- a/frontend/src/proprietary/contexts/CheckoutContext.tsx +++ b/frontend/src/proprietary/contexts/CheckoutContext.tsx @@ -8,10 +8,11 @@ import { alert } from '@app/components/toast'; import { pollLicenseKeyWithBackoff, activateLicenseKey, resyncExistingLicense } from '@app/utils/licenseCheckoutUtils'; import { useLicense } from '@app/contexts/LicenseContext'; import { isSupabaseConfigured } from '@app/services/supabaseClient'; +import { getPreferredCurrency } from '@app/utils/currencyDetection'; export interface CheckoutOptions { minimumSeats?: number; // Override calculated seats for enterprise - currency?: string; // Optional currency override (defaults to 'gbp') + currency?: string; // Optional currency override (auto-detected from locale) onSuccess?: (sessionId: string) => void; // Callback after successful payment onError?: (error: string) => void; // Callback on error } @@ -35,15 +36,18 @@ interface CheckoutProviderProps { export const CheckoutProvider: React.FC = ({ children, - defaultCurrency = 'gbp' + defaultCurrency }) => { - const { t } = useTranslation(); + const { t, i18n } = useTranslation(); const { refetchLicense } = useLicense(); const [isOpen, setIsOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); const [selectedPlanGroup, setSelectedPlanGroup] = useState(null); const [minimumSeats, setMinimumSeats] = useState(1); - const [currentCurrency, setCurrentCurrency] = useState(defaultCurrency); + const [currentCurrency, setCurrentCurrency] = useState(() => { + // Use provided default or auto-detect from locale + return defaultCurrency || getPreferredCurrency(i18n.language); + }); const [currentOptions, setCurrentOptions] = useState({}); const [hostedCheckoutSuccess, setHostedCheckoutSuccess] = useState<{ isUpgrade: boolean; diff --git a/frontend/src/proprietary/utils/currencyDetection.ts b/frontend/src/proprietary/utils/currencyDetection.ts new file mode 100644 index 0000000000..408002a931 --- /dev/null +++ b/frontend/src/proprietary/utils/currencyDetection.ts @@ -0,0 +1,142 @@ +/** + * Currency detection utility + * Auto-detects user's preferred currency from browser locale + */ + +const STORAGE_KEY = 'preferredCurrency'; + +/** + * Map of locale codes to currency codes + * Covers all major locales and their corresponding currencies + */ +const LOCALE_TO_CURRENCY_MAP: Record = { + // English variants + 'en-US': 'usd', + 'en-CA': 'usd', + 'en-AU': 'usd', + 'en-NZ': 'usd', + 'en-GB': 'gbp', + 'en-IE': 'eur', + + // European locales - Euro + 'de-DE': 'eur', + 'de-AT': 'eur', + 'de-CH': 'eur', + 'fr-FR': 'eur', + 'fr-BE': 'eur', + 'fr-CH': 'eur', + 'it-IT': 'eur', + 'es-ES': 'eur', + 'pt-PT': 'eur', + 'nl-NL': 'eur', + 'nl-BE': 'eur', + 'pl-PL': 'eur', + 'ro-RO': 'eur', + 'el-GR': 'eur', + 'fi-FI': 'eur', + 'sv-SE': 'eur', + 'da-DK': 'eur', + 'no-NO': 'eur', + + // Chinese variants + 'zh-CN': 'cny', + 'zh-TW': 'cny', + 'zh-HK': 'cny', + 'zh-SG': 'cny', + + // Indian locales + 'hi-IN': 'inr', + 'en-IN': 'inr', + 'bn-IN': 'inr', + 'te-IN': 'inr', + 'ta-IN': 'inr', + 'mr-IN': 'inr', + + // Brazilian Portuguese + 'pt-BR': 'brl', + + // Indonesian + 'id-ID': 'idr', + 'jv-ID': 'idr', + + // Other major locales defaulting to USD + 'ja-JP': 'usd', + 'ko-KR': 'usd', + 'ru-RU': 'usd', + 'ar-SA': 'usd', + 'th-TH': 'usd', + 'vi-VN': 'usd', + 'tr-TR': 'usd', +}; + +/** + * Detect currency from browser locale + * @param locale - Browser locale string (e.g., 'en-US', 'de-DE') + * @returns Currency code ('usd', 'gbp', 'eur', etc.) + */ +export function detectCurrencyFromLocale(locale: string): string { + // Try exact match first + if (LOCALE_TO_CURRENCY_MAP[locale]) { + return LOCALE_TO_CURRENCY_MAP[locale]; + } + + // Try matching just the language code (e.g., 'en' from 'en-US') + const languageCode = locale.split('-')[0]; + const matchingLocale = Object.keys(LOCALE_TO_CURRENCY_MAP).find( + key => key.startsWith(languageCode) + ); + + if (matchingLocale) { + return LOCALE_TO_CURRENCY_MAP[matchingLocale]; + } + + // Default fallback to USD + return 'usd'; +} + +/** + * Get cached currency preference from localStorage + * @returns Cached currency code or null if not set + */ +export function getCachedCurrency(): string | null { + try { + return localStorage.getItem(STORAGE_KEY); + } catch (error) { + console.warn('Failed to read currency from localStorage:', error); + return null; + } +} + +/** + * Save currency preference to localStorage + * @param currency - Currency code to cache + */ +export function setCachedCurrency(currency: string): void { + try { + localStorage.setItem(STORAGE_KEY, currency); + } catch (error) { + console.warn('Failed to save currency to localStorage:', error); + } +} + +/** + * Get preferred currency with auto-detection fallback + * Priority: localStorage > locale detection > default (USD) + * @param currentLocale - Current browser/i18n locale + * @returns Currency code + */ +export function getPreferredCurrency(currentLocale: string): string { + // 1. Check localStorage (user has previously selected) + const cached = getCachedCurrency(); + if (cached) { + return cached; + } + + // 2. Auto-detect from locale + const detected = detectCurrencyFromLocale(currentLocale); + + // 3. Cache the detection for future visits + setCachedCurrency(detected); + + return detected; +}