Skip to content

FX UI Implementation

Masked-Kunsiquat edited this page Dec 24, 2025 · 1 revision

FX Rate UI Implementation Summary

Overview

This document summarizes the UI/UX implementation for the foreign exchange (FX) rate system in CrewSplit. The implementation follows the "zero friction" UX philosophy and integrates seamlessly with the existing design system.

Components Implemented

1. ManualRateEntryScreen (src/modules/fx-rates/screens/ManualRateEntryScreen.tsx)

A polished form for manually entering exchange rates when API rates are unavailable.

Features:

  • Currency pair selection via CurrencyPicker components
  • Rate input with decimal keyboard
  • Real-time validation (positive numbers, realistic values)
  • Preview calculation (shows 1 unit and 100 units conversions)
  • Confirmation for unusually high rates (>1000)
  • Pre-fill support for error recovery flow
  • Saves to database with highest priority (manual = 100)

Navigation:

  • Route: /fx-rates/manual
  • Can be called with query params: fromCurrency and toCurrency

UX Highlights:

  • Info card explains manual rates override automatic rates
  • Helper text shows conversion context: "How many EUR per 1 USD?"
  • Preview card provides immediate feedback
  • Clear error messages for invalid inputs

2. RateListScreen (src/modules/fx-rates/screens/RateListScreen.tsx)

A comprehensive view of all stored exchange rates with metadata and refresh functionality.

Features:

  • FlatList of all active rates (sorted by most recent first)
  • Each rate shows:
    • Currency pair (USD → EUR)
    • Rate value (4 decimal precision)
    • Source (manual, frankfurter, exchangerate-api) with emoji
    • Age with color coding:
      • Green: <1 day old
      • Gray: 1-7 days old
      • Amber/warning: >7 days old
  • Summary card showing:
    • Total number of rates
    • Oldest update timestamp
  • Staleness warning banner when rates >7 days old
  • Pull-to-refresh support
  • "Refresh Rates" button to fetch latest from API
  • "Add Manual Rate" button (in header and footer)
  • Empty state with helpful guidance

Navigation:

  • Route: /fx-rates/
  • Accessible from Settings > Manage Exchange Rates

UX Highlights:

  • Relative time formatting (e.g., "2d ago", "14h ago")
  • Color-coded staleness indicators
  • Swipe down to refresh
  • Clear visual hierarchy with cards

3. StalenessWarningBanner (src/ui/components/StalenessWarningBanner.tsx)

A reusable warning banner for displaying stale exchange rate alerts.

Features:

  • Warning icon and title showing age (e.g., "Exchange rate is 14 days old")
  • Context-aware message with currency pair
  • Optional "Refresh" button
  • Accessible with proper ARIA labels
  • Loading state during refresh

Props:

interface StalenessWarningBannerProps {
  currencyPair?: string; // e.g., "USD → EUR"
  daysOld: number; // Days since last update
  onRefresh?: () => void; // Callback for refresh action
  refreshing?: boolean; // Loading state
}

Usage Example:

<StalenessWarningBanner
  currencyPair="USD → EUR"
  daysOld={14}
  onRefresh={handleRefresh}
  refreshing={isRefreshing}
/>

Styling:

  • Uses theme.colors.warningBg (muted dark amber background)
  • Warning border and text in theme.colors.warning (amber)
  • Prominent CTA button in warning color

4. NoRateAvailableModal (src/ui/components/NoRateAvailableModal.tsx)

A modal dialog for error recovery when exchange rates are missing.

Features:

  • Full-screen modal overlay with backdrop
  • Currency pair display (large, prominent)
  • Clear explanation of the issue
  • Two recovery options:
    1. "Fetch Online" - Triggers API refresh
    2. "Enter Manually" - Opens ManualRateEntryScreen
  • "Cancel" button to dismiss
  • Loading state during fetch (shows spinner)
  • Accessible with proper roles and hints

Props:

interface NoRateAvailableModalProps {
  visible: boolean;
  fromCurrency: string; // e.g., "JPY"
  toCurrency: string; // e.g., "USD"
  onFetchOnline?: () => void;
  onEnterManually?: () => void;
  onDismiss?: () => void;
  fetching?: boolean;
}

Usage Example:

<NoRateAvailableModal
  visible={showModal}
  fromCurrency="JPY"
  toCurrency="USD"
  onFetchOnline={handleFetch}
  onEnterManually={() =>
    router.push("/fx-rates/manual?fromCurrency=JPY&toCurrency=USD")
  }
  onDismiss={handleDismiss}
  fetching={isFetching}
/>

Styling:

  • Semi-transparent dark overlay (75% opacity)
  • Elevated surface card with shadow
  • Large emoji icon (🔄) for visual appeal
  • Primary-colored currency codes

5. Settings Integration (app/settings.tsx)

Added "Exchange Rates" section to the Settings screen.

Features:

  • Summary showing:
    • Number of stored rates
    • Last updated timestamp with staleness indicator
    • Warning emoji (⚠️) if rates are stale
  • "Manage Exchange Rates" button navigates to RateListScreen
  • Uses existing Card and Button components for consistency

Helper Function:

  • formatRelativeTime(timestamp) - Converts ISO timestamps to human-readable format
    • "Just now", "5m ago", "2h ago", "3d ago", "2w ago", "6mo ago"

Routing Structure

Expo Router Routes

app/
├── fx-rates/
│   ├── index.tsx         → RateListScreen (main rates view)
│   └── manual.tsx        → ManualRateEntryScreen
└── settings.tsx          → Settings with FX section

Navigation Flow

Settings Screen
    ↓ (Tap "Manage Exchange Rates")
Rate List Screen (/fx-rates/)
    ↓ (Tap "Add Manual Rate")
Manual Rate Entry Screen (/fx-rates/manual)
    ↓ (Save)
    ← (Back to Rate List)

OR

Error State (Missing Rate)
    ↓ (Show NoRateAvailableModal)
    ↓ (Tap "Enter Manually")
Manual Rate Entry Screen (/fx-rates/manual?fromCurrency=JPY&toCurrency=USD)
    ↓ (Save)
    ← (Back to previous screen)

Integration Points

1. Existing Hooks Used

  • useFxRates() - From @modules/fx-rates/hooks/use-fx-rates

    • Provides: rateCount, isStale, oldestUpdate, refreshRates, loading, refreshing
  • useRouter(), useNavigation() - Expo Router navigation

  • useLocalSearchParams() - For query parameter handling

2. Provider Integration

All screens use cachedFxRateProvider singleton:

import { cachedFxRateProvider } from "@modules/fx-rates/provider";

// Set manual rate
await cachedFxRateProvider.setManualRate(fromCurrency, toCurrency, rate);

// Check if rate exists
const hasRate = cachedFxRateProvider.hasRate("USD", "EUR");

3. Repository Access

RateListScreen directly uses FxRateRepository for fetching all rates:

import { FxRateRepository } from "@modules/fx-rates/repository";

const allRates = await FxRateRepository.getAllActiveRates();

Design System Compliance

All components strictly follow the CrewSplit design system:

Theme Tokens Used

Colors:

  • theme.colors.background - Main screen background (#0a0a0a)
  • theme.colors.surface - Card backgrounds (#1a1a1a)
  • theme.colors.surfaceElevated - Elevated cards (#2a2a2a)
  • theme.colors.primary - Primary actions (#4a9eff)
  • theme.colors.warning - Stale rate warnings (#fbbf24)
  • theme.colors.warningBg - Warning banner background (#3d2e1f)
  • theme.colors.text - Primary text (#ffffff)
  • theme.colors.textSecondary - Secondary text (#a0a0a0)
  • theme.colors.textMuted - Muted text (#666666)
  • theme.colors.border - Borders (#333333)

Spacing:

  • theme.spacing.xs (4px) - Tight gaps
  • theme.spacing.sm (8px) - Small gaps
  • theme.spacing.md (16px) - Standard gaps
  • theme.spacing.lg (24px) - Large gaps
  • theme.spacing.xl (32px) - Extra large gaps

Typography:

  • theme.typography.xs (11px) - Helper text
  • theme.typography.sm (13px) - Labels, metadata
  • theme.typography.base (15px) - Body text
  • theme.typography.lg (17px) - Emphasized text
  • theme.typography.xl (19px) - Titles
  • theme.typography.xxl (24px) - Large displays
  • Font weights: medium, semibold, bold

Other:

  • theme.borderRadius.sm/md/lg - Rounded corners
  • theme.touchTarget.minHeight (44px) - Accessibility compliance
  • theme.shadows.md/lg - Elevation shadows

Accessibility Features

All components include proper accessibility attributes:

Screen Reader Support

  • accessibilityRole - Identifies element type (button, checkbox, etc.)
  • accessibilityLabel - Readable description
  • accessibilityHint - Explains action result
  • accessibilityState - Current state (checked, busy, etc.)

Touch Targets

  • All interactive elements meet 44x44pt minimum (iOS/Android guidelines)
  • Proper padding and spacing

Keyboard Support

  • KeyboardAvoidingView on forms
  • keyboardShouldPersistTaps="handled" for ScrollViews

Visual Accessibility

  • WCAG AA compliant color contrast (4.5:1 minimum)
  • Clear visual hierarchy
  • Color-coded states with text labels (not color alone)

User Flows

Flow 1: View and Refresh Rates

  1. User opens Settings
  2. Taps "Manage Exchange Rates"
  3. Views list of current rates
  4. Sees staleness warning if rates >7 days old
  5. Pulls down to refresh OR taps "Refresh Rates"
  6. Rates update, warning disappears

Flow 2: Add Manual Rate

  1. User opens Settings → Manage Exchange Rates
  2. Taps "Add Manual Rate"
  3. Selects "From Currency" (e.g., USD)
  4. Selects "To Currency" (e.g., EUR)
  5. Enters rate (e.g., 0.92)
  6. Sees preview: "1 USD = 0.9200 EUR"
  7. Taps "Save Rate"
  8. Confirmation alert appears
  9. Returns to rate list

Flow 3: Error Recovery (Missing Rate)

  1. User views settlement with display currency (e.g., JPY)
  2. No rate found for trip currency → display currency
  3. NoRateAvailableModal appears
  4. User chooses:
    • Option A: Fetch Online
      • Spinner shows
      • API fetches latest rates
      • Modal dismisses
      • Settlement recalculates
    • Option B: Enter Manually
      • Modal dismisses
      • ManualRateEntryScreen opens
      • Currency pair pre-filled (JPY → USD)
      • User enters rate
      • Saves and returns
      • Settlement recalculates

Error Handling

Validation Errors

ManualRateEntryScreen:

  • Rate <= 0: "Rate must be a positive number"
  • Same currency: "Source and target currencies cannot be the same"
  • Rate > 1000: Confirmation dialog ("The exchange rate 1500 seems unusually high. Are you sure?")
  • Network error: "Failed to save exchange rate. Please try again."

RateListScreen:

  • Load failure: Alert with "Failed to load exchange rates. Please try again."
  • Refresh failure: Alert with error message or generic network message

Network Errors

All async operations include try/catch with user-friendly error messages:

try {
  await refreshRates();
} catch (error) {
  Alert.alert(
    "Refresh Failed",
    error instanceof Error
      ? error.message
      : "Could not refresh rates. Check your internet connection.",
  );
}

Performance Considerations

Optimizations

  1. FlatList for long lists - RateListScreen uses FlatList (not ScrollView)
  2. Pull-to-refresh - Native RefreshControl component
  3. Memoization - useFxRates hook caches staleness info
  4. Minimal re-renders - State updates are scoped appropriately
  5. Debouncing - Input validation is inline (no debounce needed for single field)

Loading States

All async operations show loading indicators:

  • ManualRateEntryScreen: "Saving..." button text
  • RateListScreen: Full-screen ActivityIndicator on initial load
  • NoRateAvailableModal: Spinner during fetch

Testing Recommendations

Manual Testing Scenarios

  1. Empty state

    • Delete all rates from DB
    • Open RateListScreen
    • Verify empty state shows with helpful message
  2. Staleness warning

    • Manually set a rate's fetchedAt to 14 days ago
    • Open RateListScreen
    • Verify warning banner appears
    • Verify rate has amber color for age
  3. Manual rate priority

    • Add manual rate for USD → EUR (e.g., 0.90)
    • Refresh rates (fetches API rate, e.g., 0.92)
    • Verify manual rate (0.90) is still used in conversions
  4. Error recovery flow

    • Create expense in currency with no rate
    • View settlement with display currency
    • Verify NoRateAvailableModal appears
    • Test both "Fetch Online" and "Enter Manually" paths
  5. Accessibility

    • Enable screen reader (TalkBack/VoiceOver)
    • Navigate through RateListScreen
    • Verify all interactive elements are announced
    • Verify proper focus order
  6. Responsive design

    • Test on small screen (iPhone SE)
    • Test on large screen (iPad)
    • Verify touch targets are large enough
    • Verify text doesn't overflow

Automated Testing

Suggested test coverage:

  • ManualRateEntryScreen.test.tsx - Form validation, save flow
  • RateListScreen.test.tsx - List rendering, refresh, navigation
  • StalenessWarningBanner.test.tsx - Rendering with different props
  • NoRateAvailableModal.test.tsx - Modal interactions

Future Enhancements

Potential improvements for future iterations:

  1. Batch delete rates - Select multiple rates to archive
  2. Rate history chart - Visual graph of rate changes over time
  3. Smart suggestions - Recommend likely currency pairs based on trip history
  4. Offline indicator - Show when app is offline (prevent fetch attempts)
  5. Rate notifications - Alert when rates become stale
  6. Import/export rates - Share rates between devices
  7. Inverse rate editing - Show both directions (USD→EUR and EUR→USD)
  8. Rate comparison - Compare manual vs API rates side-by-side

Files Created

Screens

  • src/modules/fx-rates/screens/ManualRateEntryScreen.tsx
  • src/modules/fx-rates/screens/RateListScreen.tsx
  • src/modules/fx-rates/screens/index.ts

Components

  • src/ui/components/StalenessWarningBanner.tsx
  • src/ui/components/NoRateAvailableModal.tsx

Routes

  • app/fx-rates/index.tsx
  • app/fx-rates/manual.tsx

Modified Files

  • app/settings.tsx - Added Exchange Rates section
  • src/ui/components/index.ts - Exported new components

Summary

This implementation provides a complete, user-friendly interface for managing exchange rates in CrewSplit. The UI follows the "zero friction" philosophy with:

  • Minimal input - Smart defaults, autofill, clear previews
  • Clear feedback - Loading states, validation messages, confirmations
  • Error recovery - Modal guidance when rates are missing
  • Visual polish - Consistent design system, color-coded staleness
  • Accessibility - Screen reader support, proper touch targets

All components integrate seamlessly with the existing architecture (hooks, providers, repositories) and follow React Native + Expo best practices.

Clone this wiki locally