// Copyright: © 2026 TWWIM UG. All rights reserved. (www.twwim.com) /** * Read-only display + edit flow for the company's billing currency. * * UNLOCKED state (no paid payment yet): * • Dropdown lets the owner switch EUR↔USD. * • Submit posts PUT /companies/:id/currency — server rejects with 409 if * the lock was just set by a concurrent payment. * * LOCKED state (currency_locked_at != null): * • Read-only display + lock badge + hint "cancel to change". * * Mounted on both /dashboard/profile (PERSONAL accounts) and * /dashboard/company (BUSINESS accounts) — a PERSONAL account's "company" * row is a shadow over the user themselves, so the same section serves * both UX paths without duplication. */ import { useState } from 'react'; import { CircleDollarSign, Lock, Check, Loader2 } from 'lucide-react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from '@/i18n/TranslationProvider'; import { companiesApi } from '@/infrastructure/http/api/company'; import { queryKeys } from '@/lib/query-keys'; import { Currency, SUPPORTED_CURRENCIES } from '@archer/domain'; import { useAuthenticatedUser } from '@/features/auth/hooks/useAuthenticatedUser'; import { forceTokenRefresh } from '@/infrastructure/http/auth-refresh'; /** Humanized labels keyed off the domain Currency enum — single source of truth. */ const CURRENCY_LABEL: Record = { [Currency.EUR]: '€ Euro', [Currency.USD]: '$ US Dollar', }; export function BillingCurrencySection() { const { t } = useTranslation(); const queryClient = useQueryClient(); const authed = useAuthenticatedUser(); const companyId = authed?.companyId; const { data: company, isLoading } = useQuery({ queryKey: queryKeys.companies.detail(companyId ?? ''), queryFn: () => companiesApi.getCompany(companyId!), enabled: !!companyId, }); const mutation = useMutation({ mutationFn: (currency: Currency) => companiesApi.updateCurrency(companyId!, currency), onSuccess: async () => { queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); queryClient.invalidateQueries({ queryKey: queryKeys.profile.all }); // Currency drives plan-selection modal pricing + price formatter. Round- // trip the JWT so the AuthenticatedUser snapshot rebuilds from server. try { await forceTokenRefresh(); } catch { /* refresh failure: snapshot keeps prior currency until next /auth/me */ } }, }); const currentCurrency = company?.preferredCurrency ?? null; const locked = company?.isCurrencyLocked() ?? false; const [draft, setDraft] = useState(''); if (!companyId) return null; const handleSave = () => { if (draft && draft !== currentCurrency) { mutation.mutate(draft); } }; return (

{t('profile.billingCurrency') || 'Billing currency'}

{locked && ( {t('profile.locked') || 'Locked'} )}
{isLoading ? (

) : locked ? ( <>

{currentCurrency ? (CURRENCY_LABEL[currentCurrency as Currency] ?? currentCurrency) : '—'}

{t('profile.currencyLockedBody') || 'Your billing currency is locked for the active subscription. Cancel the subscription to change it.'}

) : ( <>

{t('profile.currencyUnlockedBody') || 'Your billing currency. Locks automatically after your first paid payment.'}

{mutation.isError && (

{(mutation.error as Error).message || t('profile.currencyChangeFailed') || 'Could not change currency.'}

)} {mutation.isSuccess && (

{t('profile.currencyChanged') || 'Currency updated.'}

)} )}
); }