-
Notifications
You must be signed in to change notification settings - Fork 0
FX UI Integration Guide
Phase 6 Completion: Error Recovery Modals and Staleness Warnings
This document describes the complete UI integration of exchange rate error handling and staleness warnings in the CrewSplit application.
The FX rate UI integration provides non-blocking, user-friendly error recovery when exchange rates are missing or stale. Users can:
- See staleness warnings when rates are >7 days old
- Fetch rates online with a single tap
- Enter rates manually as a fallback
- Dismiss warnings without blocking core functionality
Location: src/ui/components/NoRateAvailableModal.tsx
Purpose: Shown when a required exchange rate is completely missing from the cache.
Props:
-
visible: boolean- Whether modal is displayed -
fromCurrency: string- Source currency code -
toCurrency: string- Target currency code -
onFetchOnline?: () => void- Handler for "Fetch Online" button -
onEnterManually?: () => void- Handler for "Enter Manually" button -
onDismiss?: () => void- Handler for modal dismissal -
fetching?: boolean- Show loading state during fetch
Usage Example:
<NoRateAvailableModal
visible={rateModalVisible}
fromCurrency="USD"
toCurrency="EUR"
onFetchOnline={handleFetchOnline}
onEnterManually={handleEnterManually}
onDismiss={() => setRateModalVisible(false)}
fetching={fxRefreshing}
/>Location: src/ui/components/StalenessWarningBanner.tsx
Purpose: Non-intrusive banner shown at top of screen when rates are >7 days old.
Props:
-
currencyPair?: string- Display string like "USD → EUR" -
daysOld: number- Age of oldest rate -
onRefresh?: () => void- Handler for refresh button -
refreshing?: boolean- Show loading state during refresh
Usage Example:
{
isStale && daysOld && (
<StalenessWarningBanner
currencyPair="USD → EUR"
daysOld={14}
onRefresh={handleRefreshStaleRates}
refreshing={fxRefreshing}
/>
);
}Location: src/modules/settlement/screens/SettlementSummaryScreen.tsx
Integration Points:
-
Imports:
import { NoRateAvailableModal, StalenessWarningBanner, } from "@ui/components"; import { useFxSync } from "@modules/fx-rates/hooks/use-fx-sync";
-
Hooks:
const { settlement, conversionError, // NEW - error when rate missing refetch: refetchSettlement, } = useSettlementWithDisplay(tripId, displayCurrency); const { isStale, daysOld, refreshing: fxRefreshing, refreshNow: refreshFxRates, } = useFxSync({ autoRefresh: false });
-
State:
const [rateModalVisible, setRateModalVisible] = useState(false);
-
Handlers:
const handleFetchOnline = async () => { await refreshFxRates(); setRateModalVisible(false); await refetchSettlement(); }; const handleEnterManually = () => { setRateModalVisible(false); router.push(`/fx-rates/manual?from=${fromCurrency}&to=${toCurrency}`); }; const handleRefreshStaleRates = async () => { await refreshFxRates(); await refetchSettlement(); };
-
UI Placement:
- Staleness Banner: At top of ScrollView, before expense breakdown
- No Rate Modal: After TripExportModal at end of component
Location: src/modules/expenses/screens/ExpenseDetailsScreen.tsx
Integration Points:
-
Imports: Same as SettlementSummaryScreen
-
Conversion Logic:
const displayAmounts = useMemo(() => { try { setConversionError(null); const fxRate = cachedFxRateProvider.getRate( expense.currency, displayCurrency, ); // ... conversion logic } catch (error) { setConversionError({ fromCurrency: expense.currency, toCurrency: displayCurrency, }); return null; } }, [expense, displayCurrency]);
-
State:
const [rateModalVisible, setRateModalVisible] = useState(false); const [conversionError, setConversionError] = useState<{ fromCurrency: string; toCurrency: string; } | null>(null);
-
UI Placement:
- Staleness Banner: At top of ScrollView, before expense info
- No Rate Modal: After TripExportModal
- User opens settlement/expense screen
- Display currency conversion works (rates < 7 days old)
- No warnings or modals shown
- Amounts displayed in both trip and display currencies
- User opens settlement/expense screen
- Conversion fails (rate not in cache)
- NoRateAvailableModal appears immediately
- User has 3 options:
- Fetch Online: Downloads latest rates, modal closes on success
- Enter Manually: Navigates to manual entry screen with pre-filled currency pair
- Cancel: Closes modal, amounts shown in trip currency only
- User opens settlement/expense screen
- Conversion succeeds but rates are >7 days old
- StalenessWarningBanner appears at top
- User can:
- Tap Refresh: Updates rates in background, banner updates/disappears
- Ignore: Continue viewing with stale rates
- Pull to refresh: Also triggers rate refresh via pull-to-refresh gesture
- Conversion errors never crash the app
- Missing rates show modal but allow dismissal
- Stale rates are warnings, not errors
- Users can always view trip currency amounts
- Critical: NoRateAvailableModal (centered, darkened backdrop)
- Warning: StalenessWarningBanner (top of screen, yellow/warning color)
- Informational: Display currency amounts (secondary, muted text)
- Fetching rates: Modal shows loading spinner with "Fetching rates..." text
- Refreshing: Banner button shows "Refreshing..." and disables interaction
- Pull-to-refresh: Standard pull gesture triggers both data and rate refresh
All components follow WCAG AA standards:
-
accessibilityLabel: Describes each button action -
accessibilityHint: Explains outcome of interaction -
accessibilityRole: "button" for all interactive elements - Dismissible via backdrop tap or Cancel button
-
accessibilityLabel: "Refresh exchange rates" -
accessibilityHint: "Updates exchange rates from online sources" -
accessibilityRole: "button" for refresh action -
accessibilityState:{ busy: refreshing }during refresh
-
Automatic: Disabled in screens (
autoRefresh: false) to avoid duplicate fetches - Manual: User-initiated via banner or modal
- Background: Enabled in app root for startup refresh
-
cachedFxRateProviderautomatically refreshes after new rates persisted - Settlement/expense screens refetch data after rate updates
- No redundant database queries during refresh
- Network check before attempting online fetch
- Graceful fallback to manual entry if offline
- No blocking operations during startup
describe("NoRateAvailableModal", () => {
it("shows currency pair correctly", () => {});
it("calls onFetchOnline when button pressed", () => {});
it("navigates to manual entry on button press", () => {});
it("shows loading state when fetching", () => {});
});
describe("StalenessWarningBanner", () => {
it("displays correct days old", () => {});
it("calls onRefresh when tapped", () => {});
it("shows refreshing state", () => {});
});-
Missing Rate:
- Set display currency to one without cached rate
- Open settlement screen
- Verify modal appears
- Tap "Fetch Online"
- Verify modal closes and amounts appear
-
Stale Rate:
- Manually set rate timestamp to 10 days ago
- Open settlement screen
- Verify banner appears with "10 days old"
- Tap refresh
- Verify banner updates/disappears
-
Error Recovery:
- Disconnect from internet
- Trigger "Fetch Online"
- Verify error handling (modal stays open or shows error)
- Complete user journey from missing rate to successful display
- Test navigation to manual entry screen with pre-filled values
- Verify pull-to-refresh triggers rate update
- Test accessibility features with screen reader
- Toast Notifications: Success/failure messages after refresh
- Rate Age Indicator: Show age inline with amounts (e.g., "EUR 50.00 (7d old)")
- Batch Refresh: Update multiple currency pairs at once
- Rate History: Show historical rates for auditing
- Offline Queue: Queue rate fetches when offline, retry when online
- Check
conversionErroris being set in catch block - Verify
useEffectdependency array includesconversionError - Ensure
NoRateAvailableModalis rendered at component root level
- Verify
isStaleis true (checkuseFxSyncreturn value) - Ensure
daysOldis not null - Check conditional render logic includes
showDisplayCurrency
- Check network connectivity
- Verify API endpoint is accessible
- Check database permissions for rate updates
- Ensure
cachedFxRateProvider.refreshCache()is called
- Verify
refetchSettlement()/refetchExpense()is called after rate refresh - Check cache invalidation in
cachedFxRateProvider - Ensure conversion logic uses refreshed rates
Phase 6 completes the FX rate UI integration with:
- Non-blocking error recovery via modals and banners
- User-friendly workflows for fetching or entering rates
- Accessible components following WCAG standards
- Performance-optimized with proper cache management
- Thoroughly documented for future maintenance
All components follow the "zero friction" UX philosophy: minimal user input, clear visual feedback, and graceful error handling.