--- phase: 33-currency-system plan: 05 type: execute wave: 3 depends_on: [01, 03] files_modified: - src/client/lib/formatters.ts - src/client/hooks/useCurrency.ts - src/client/hooks/useFormatters.ts - src/client/hooks/useExchangeRates.ts - src/client/routes/settings.tsx autonomous: true requirements: [D-10, D-11, D-12, D-13, D-14, D-15, D-16] must_haves: truths: - "Currency picker in settings now implies market selection" - "Settings page has a 'Show Converted Prices' toggle" - "formatPrice supports dual display format: source price + converted in parentheses" - "Converted prices always show ~ prefix to indicate approximation" - "useCurrency returns currency, market, and showConversions flag" - "Auto-suggestion appears on first visit based on browser locale" artifacts: - path: "src/client/lib/formatters.ts" provides: "Extended formatPrice with dual display and conversion options" exports: ["formatPrice", "formatDualPrice"] - path: "src/client/hooks/useCurrency.ts" provides: "Market-aware currency hook" exports: ["useCurrency"] - path: "src/client/hooks/useExchangeRates.ts" provides: "React Query hook for exchange rates" exports: ["useExchangeRates"] - path: "src/client/routes/settings.tsx" provides: "Updated settings page with market/currency selector and conversion toggle" key_links: - from: "src/client/hooks/useFormatters.ts" to: "src/client/lib/formatters.ts" via: "formatPrice import" pattern: "formatPrice|formatDualPrice" - from: "src/client/hooks/useExchangeRates.ts" to: "/api/exchange-rates" via: "React Query fetch" pattern: "exchange-rates" - from: "src/client/hooks/useCurrency.ts" to: "src/client/hooks/useSettings.ts" via: "useSetting('currency')" pattern: "useSetting" --- Evolve the client-side price formatting, currency hook, and settings UI to support market-aware pricing with dual display. Purpose: User-facing currency system — market/currency selector, auto-suggestion, conversion toggle, and dual price display format. Output: Updated formatters, enhanced currency hook, new exchange rates hook, redesigned settings currency section. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/33-currency-system/33-CONTEXT.md @.planning/phases/33-currency-system/33-UI-SPEC.md From src/client/lib/formatters.ts (current): ```typescript export type Currency = "USD" | "EUR" | "GBP" | "JPY" | "CAD" | "AUD"; export function formatPrice(cents: number | null | undefined, currency: Currency = "USD"): string; ``` From src/client/hooks/useCurrency.ts (current): ```typescript export function useCurrency(): Currency; ``` From src/client/hooks/useFormatters.ts (current): ```typescript export function useFormatters(): { weight: (grams: number | null) => string; price: (cents: number | null) => string; unit: WeightUnit; currency: Currency; }; ``` From src/client/hooks/useSettings.ts (pattern): ```typescript export function useSetting(key: string): { data: string | undefined, ... }; export function useUpdateSetting(): UseMutationResult<...>; ``` From src/client/lib/api.ts: ```typescript export function apiGet(path: string): Promise; ``` Task 1: Extend formatPrice with dual display and create exchange rates hook src/client/lib/formatters.ts, src/client/hooks/useExchangeRates.ts src/client/lib/formatters.ts, src/client/hooks/useFormatters.ts, src/client/lib/api.ts Update `src/client/lib/formatters.ts`: Per D-14: Add `formatDualPrice` function: ```typescript export interface DualPriceOptions { sourceCents: number; sourceCurrency: Currency; targetCurrency: Currency; convertedCents: number; } export function formatDualPrice(options: DualPriceOptions): { source: string; converted: string } { const source = formatPrice(options.sourceCents, options.sourceCurrency); const converted = `~${formatPrice(options.convertedCents, options.targetCurrency)}`; return { source, converted }; } ``` Per D-11: The `~` prefix on converted prices indicates approximation. The `converted` string is always prefixed with `~`. Keep existing `formatPrice` unchanged for backward compatibility — all existing callers continue to work. `formatDualPrice` is additive. Create `src/client/hooks/useExchangeRates.ts`: ```typescript import { useQuery } from "@tanstack/react-query"; import { apiGet } from "../lib/api"; interface ExchangeRates { base: string; date: string; rates: Record; } export function useExchangeRates() { return useQuery({ queryKey: ["exchange-rates"], queryFn: () => apiGet("/api/exchange-rates"), staleTime: 1000 * 60 * 60, // 1 hour client-side stale time gcTime: 1000 * 60 * 60 * 24, // 24 hour garbage collection refetchOnWindowFocus: false, }); } export function convertClientPrice( cents: number, from: string, to: string, rates: Record, ): number { if (from === to) return cents; const fromRate = rates[from] ?? 1; const toRate = rates[to] ?? 1; return Math.round((cents / fromRate) * toRate); } ``` This provides both a React Query hook for components and a pure conversion function that mirrors the server-side logic. - src/client/lib/formatters.ts exports formatDualPrice alongside existing formatPrice - formatDualPrice returns { source: "€2,000.00", converted: "~$2,160.00" } format - Existing formatPrice function unchanged (backward compatible) - src/client/hooks/useExchangeRates.ts exports useExchangeRates and convertClientPrice - useExchangeRates fetches from /api/exchange-rates with 1h stale time - convertClientPrice(1000, "EUR", "EUR", rates) returns 1000 cd /home/jlmak/Projects/jlmak/GearBox && grep -c "formatDualPrice\|useExchangeRates\|convertClientPrice" src/client/lib/formatters.ts src/client/hooks/useExchangeRates.ts Dual price display format and exchange rate hook available for all components Task 2: Evolve useCurrency hook and update settings page with market selector + conversion toggle src/client/hooks/useCurrency.ts, src/client/hooks/useFormatters.ts, src/client/routes/settings.tsx src/client/hooks/useCurrency.ts, src/client/hooks/useFormatters.ts, src/client/routes/settings.tsx, src/client/hooks/useSettings.ts Per D-12: Update `src/client/hooks/useCurrency.ts`: ```typescript import type { Currency } from "../lib/formatters"; import { useSetting } from "./useSettings"; const VALID_CURRENCIES: Currency[] = ["USD", "EUR", "GBP", "JPY", "CAD", "AUD"]; const CURRENCY_MARKET_MAP: Record = { EUR: "EU", USD: "US", GBP: "UK", JPY: "JP", CAD: "CA", AUD: "AU", }; export interface CurrencyContext { currency: Currency; market: string; showConversions: boolean; } export function useCurrency(): CurrencyContext { const { data: currencyData } = useSetting("currency"); const { data: showConversionsData } = useSetting("showConversions"); const currency: Currency = (currencyData && VALID_CURRENCIES.includes(currencyData as Currency)) ? (currencyData as Currency) : "USD"; return { currency, market: CURRENCY_MARKET_MAP[currency] ?? currency, showConversions: showConversionsData === "true", }; } ``` IMPORTANT: The return type changes from `Currency` to `CurrencyContext`. Update `src/client/hooks/useFormatters.ts` to destructure correctly: ```typescript export function useFormatters() { const unit = useWeightUnit(); const { currency } = useCurrency(); // Destructure currency from CurrencyContext return { weight: (grams: number | null) => formatWeight(grams, unit), price: (cents: number | null) => formatPrice(cents, currency), unit, currency, }; } ``` Also update ALL other files that call `useCurrency()` and expect a plain `Currency` string — search with `grep -rn "useCurrency()" src/client/` and update each to destructure `{ currency }` or `{ currency, market, showConversions }` as needed. The settings.tsx file is the primary consumer beyond useFormatters. Per D-13, D-15, D-16: Update `src/client/routes/settings.tsx`: 1. Change the "Currency" section heading to "Market & Currency" per UI-SPEC 2. Change description to "Sets your market region and currency for price display" 3. Keep the same pill toggle pattern for currency selection (same bg-gray-100 rounded-full container) 4. Add a new "Show Converted Prices" toggle below the currency picker, separated by `border-t border-gray-100`: - Heading: "Show Converted Prices" (text-sm font-medium text-gray-900) - Description: "Display approximate conversions when local price is not available" (text-xs text-gray-500) - Toggle: A simple button/switch that saves `showConversions` setting as "true"/"false" using updateSetting.mutate({ key: "showConversions", value: "true"/"false" }) - Toggle styles: `w-10 h-5 rounded-full` container, `bg-gray-200` when off, `bg-blue-500` when on, inner circle `w-4 h-4 rounded-full bg-white shadow-sm` translated right when on 5. Per D-13: Add auto-suggestion banner above the settings card (only shown when no currency setting exists): - Detect suggested currency from `navigator.language`: parse locale (e.g., "de-DE" → EUR, "en-US" → USD, "en-GB" → GBP, "ja-JP" → JPY, "fr-CA" → CAD, "en-AU" → AUD) - Banner: `bg-blue-50 border border-blue-100 rounded-xl px-4 py-3 mb-4 flex items-center justify-between` - Text: LucideIcon "globe" (16px, text-blue-500) + "Based on your location, we suggest {CURRENCY} ({SYMBOL})" (text-sm text-blue-700) - CTA: "Use {SYMBOL}" button (text-sm font-medium text-blue-700 hover:text-blue-800 underline) that saves the currency setting and hides the banner - Use useState for banner visibility, default to showing when `useSetting("currency").data` is undefined - src/client/hooks/useCurrency.ts exports CurrencyContext interface - useCurrency() returns { currency, market, showConversions } object - src/client/hooks/useFormatters.ts destructures { currency } from useCurrency() - settings.tsx heading reads "Market & Currency" - settings.tsx has "Show Converted Prices" toggle that persists to settings - settings.tsx has auto-suggestion banner using navigator.language when no currency set - All existing components that call useCurrency() still compile (no type errors from return type change) - `bun run build` succeeds cd /home/jlmak/Projects/jlmak/GearBox && bun run build Market/currency selector with auto-suggestion, conversion toggle, and updated currency hook deployed ## Trust Boundaries | Boundary | Description | |----------|-------------| | browser→app | navigator.language used for auto-suggestion — untrusted but low risk (suggestion only) | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-33-13 | Spoofing | settings.tsx auto-suggestion | accept | navigator.language is a suggestion only — user explicitly confirms by clicking "Use". No security impact if spoofed. | | T-33-14 | Tampering | useCurrency hook | mitigate | Currency value validated against VALID_CURRENCIES allowlist — invalid values fall back to "USD" | - `bun run build` succeeds (no TypeScript errors) - Settings page shows Market & Currency with pill toggle - Settings page shows Show Converted Prices toggle - Auto-suggestion banner appears when no currency setting exists - useCurrency() returns CurrencyContext object in all consumers - Market/currency selector in settings - Conversion toggle in settings - Auto-suggestion based on locale - Dual price format available in formatter - Exchange rates hook ready for components - All existing price displays still work (backward compatible) After completion, create `.planning/phases/33-currency-system/33-05-SUMMARY.md`