-
Notifications
You must be signed in to change notification settings - Fork 0
FX UI Implementation
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.
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:
fromCurrencyandtoCurrency
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
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
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
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:
- "Fetch Online" - Triggers API refresh
- "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
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"
app/
├── fx-rates/
│ ├── index.tsx → RateListScreen (main rates view)
│ └── manual.tsx → ManualRateEntryScreen
└── settings.tsx → Settings with FX section
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)
-
useFxRates()- From@modules/fx-rates/hooks/use-fx-rates- Provides:
rateCount,isStale,oldestUpdate,refreshRates,loading,refreshing
- Provides:
-
useRouter(),useNavigation()- Expo Router navigation -
useLocalSearchParams()- For query parameter handling
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");RateListScreen directly uses FxRateRepository for fetching all rates:
import { FxRateRepository } from "@modules/fx-rates/repository";
const allRates = await FxRateRepository.getAllActiveRates();All components strictly follow the CrewSplit design system:
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
All components include proper accessibility attributes:
-
accessibilityRole- Identifies element type (button, checkbox, etc.) -
accessibilityLabel- Readable description -
accessibilityHint- Explains action result -
accessibilityState- Current state (checked, busy, etc.)
- All interactive elements meet 44x44pt minimum (iOS/Android guidelines)
- Proper padding and spacing
-
KeyboardAvoidingViewon forms -
keyboardShouldPersistTaps="handled"for ScrollViews
- WCAG AA compliant color contrast (4.5:1 minimum)
- Clear visual hierarchy
- Color-coded states with text labels (not color alone)
- User opens Settings
- Taps "Manage Exchange Rates"
- Views list of current rates
- Sees staleness warning if rates >7 days old
- Pulls down to refresh OR taps "Refresh Rates"
- Rates update, warning disappears
- User opens Settings → Manage Exchange Rates
- Taps "Add Manual Rate"
- Selects "From Currency" (e.g., USD)
- Selects "To Currency" (e.g., EUR)
- Enters rate (e.g., 0.92)
- Sees preview: "1 USD = 0.9200 EUR"
- Taps "Save Rate"
- Confirmation alert appears
- Returns to rate list
- User views settlement with display currency (e.g., JPY)
- No rate found for trip currency → display currency
-
NoRateAvailableModalappears - 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
-
Option A: Fetch Online
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
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.",
);
}- FlatList for long lists - RateListScreen uses FlatList (not ScrollView)
- Pull-to-refresh - Native RefreshControl component
- Memoization - useFxRates hook caches staleness info
- Minimal re-renders - State updates are scoped appropriately
- Debouncing - Input validation is inline (no debounce needed for single field)
All async operations show loading indicators:
- ManualRateEntryScreen: "Saving..." button text
- RateListScreen: Full-screen ActivityIndicator on initial load
- NoRateAvailableModal: Spinner during fetch
-
Empty state
- Delete all rates from DB
- Open RateListScreen
- Verify empty state shows with helpful message
-
Staleness warning
- Manually set a rate's
fetchedAtto 14 days ago - Open RateListScreen
- Verify warning banner appears
- Verify rate has amber color for age
- Manually set a rate's
-
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
-
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
-
Accessibility
- Enable screen reader (TalkBack/VoiceOver)
- Navigate through RateListScreen
- Verify all interactive elements are announced
- Verify proper focus order
-
Responsive design
- Test on small screen (iPhone SE)
- Test on large screen (iPad)
- Verify touch targets are large enough
- Verify text doesn't overflow
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
Potential improvements for future iterations:
- Batch delete rates - Select multiple rates to archive
- Rate history chart - Visual graph of rate changes over time
- Smart suggestions - Recommend likely currency pairs based on trip history
- Offline indicator - Show when app is offline (prevent fetch attempts)
- Rate notifications - Alert when rates become stale
- Import/export rates - Share rates between devices
- Inverse rate editing - Show both directions (USD→EUR and EUR→USD)
- Rate comparison - Compare manual vs API rates side-by-side
src/modules/fx-rates/screens/ManualRateEntryScreen.tsxsrc/modules/fx-rates/screens/RateListScreen.tsxsrc/modules/fx-rates/screens/index.ts
src/ui/components/StalenessWarningBanner.tsxsrc/ui/components/NoRateAvailableModal.tsx
app/fx-rates/index.tsxapp/fx-rates/manual.tsx
-
app/settings.tsx- Added Exchange Rates section -
src/ui/components/index.ts- Exported new components
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.