Files
GearBox/.planning/phases/33-currency-system/33-05-PLAN.md
Jean-Luc Makiola 7a696f39a5 docs(33): create phase plans for currency system
6 plans across 3 waves covering market-aware pricing, exchange rates,
community price data, and currency-normalized display.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:58:37 +02:00

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
33-currency-system 05 execute 3
01
03
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
true
D-10
D-11
D-12
D-13
D-14
D-15
D-16
truths artifacts key_links
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
path provides exports
src/client/lib/formatters.ts Extended formatPrice with dual display and conversion options
formatPrice
formatDualPrice
path provides exports
src/client/hooks/useCurrency.ts Market-aware currency hook
useCurrency
path provides exports
src/client/hooks/useExchangeRates.ts React Query hook for exchange rates
useExchangeRates
path provides
src/client/routes/settings.tsx Updated settings page with market/currency selector and conversion toggle
from to via pattern
src/client/hooks/useFormatters.ts src/client/lib/formatters.ts formatPrice import formatPrice|formatDualPrice
from to via pattern
src/client/hooks/useExchangeRates.ts /api/exchange-rates React Query fetch exchange-rates
from to via pattern
src/client/hooks/useCurrency.ts src/client/hooks/useSettings.ts useSetting('currency') 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.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):

export function useCurrency(): Currency;

From src/client/hooks/useFormatters.ts (current):

export function useFormatters(): {
  weight: (grams: number | null) => string;
  price: (cents: number | null) => string;
  unit: WeightUnit;
  currency: Currency;
};

From src/client/hooks/useSettings.ts (pattern):

export function useSetting(key: string): { data: string | undefined, ... };
export function useUpdateSetting(): UseMutationResult<...>;

From src/client/lib/api.ts:

export function apiGet<T>(path: string): Promise<T>;
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:

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:

import { useQuery } from "@tanstack/react-query";
import { apiGet } from "../lib/api";

interface ExchangeRates {
  base: string;
  date: string;
  rates: Record<string, number>;
}

export function useExchangeRates() {
  return useQuery({
    queryKey: ["exchange-rates"],
    queryFn: () => apiGet<ExchangeRates>("/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<string, number>,
): 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. <acceptance_criteria> - 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 </acceptance_criteria> 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<string, string> = { 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 <acceptance_criteria>
    • 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 </acceptance_criteria> cd /home/jlmak/Projects/jlmak/GearBox && bun run build Market/currency selector with auto-suggestion, conversion toggle, and updated currency hook deployed

<threat_model>

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"
</threat_model>
- `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

<success_criteria>

  • 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) </success_criteria>
After completion, create `.planning/phases/33-currency-system/33-05-SUMMARY.md`