Auto detect currency

This commit is contained in:
Connor Yoh
2025-11-24 13:58:42 +00:00
parent 4138d4b5d7
commit c5a0e59d90
4 changed files with 164 additions and 8 deletions
@@ -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
@@ -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<string>('usd');
const [currency, setCurrency] = useState<string>(() => {
// 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<string>('');
@@ -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}
/>
@@ -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<CheckoutProviderProps> = ({
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<PlanTierGroup | null>(null);
const [minimumSeats, setMinimumSeats] = useState<number>(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<CheckoutOptions>({});
const [hostedCheckoutSuccess, setHostedCheckoutSuccess] = useState<{
isUpgrade: boolean;
@@ -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<string, string> = {
// 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;
}