diff --git a/components/escrow/FiatXlmPreview.tsx b/components/escrow/FiatXlmPreview.tsx
index 26b2e58..30923a3 100644
--- a/components/escrow/FiatXlmPreview.tsx
+++ b/components/escrow/FiatXlmPreview.tsx
@@ -9,8 +9,13 @@
*
* The component blocks submission if critical slippage is detected but not acknowledged.
*
+ * Also renders live, localized NGN/USD equivalents below the XLM total,
+ * which update instantly whenever the caller's xlmAmount changes.
+ *
* Architecture: FiatXlmPreview (Component) → useFiatXlmSlippage (Hook) →
* fiatXlmSlippageService → fxService → Backend
+ * → useLocalizedFiatPreview (Hook) → localizedFxService →
+ * currencyRateService → Backend
*/
import React, { useEffect } from 'react';
@@ -21,6 +26,7 @@ import {
Loader2,
} from 'lucide-react';
import { useFiatXlmSlippage } from '@/hooks/useFiatXlmSlippage';
+import { useLocalizedFiatPreview } from '@/hooks/useLocalizedFiatPreview';
import { formatNgn } from '@/services/fxService';
import { SlippageWarning } from '@/components/escrow/SlippageWarning';
@@ -53,6 +59,7 @@ export function FiatXlmPreview({
cancelLabel = 'Cancel',
}: FiatXlmPreviewProps) {
const slippage = useFiatXlmSlippage();
+ const localizedFiat = useLocalizedFiatPreview(xlmAmount);
// Initialize quote tracking when component mounts or rate changes
useEffect(() => {
@@ -117,6 +124,20 @@ export function FiatXlmPreview({
+ {/* Localized fiat equivalents — updates instantly as xlmAmount changes */}
+ {localizedFiat.equivalents.some((eq) => eq.formatted) && (
+
+ {localizedFiat.equivalents.map((eq) =>
+ eq.formatted ? (
+
+ ≈ {eq.formatted}{' '}
+ ({eq.fiatCode})
+
+ ) : null,
+ )}
+
+ )}
+
{/* Quoted NGN */}
diff --git a/components/fleet/DriverReputation.tsx b/components/fleet/DriverReputation.tsx
index 8066041..41bf13c 100644
--- a/components/fleet/DriverReputation.tsx
+++ b/components/fleet/DriverReputation.tsx
@@ -97,10 +97,11 @@ export function DriverReputation({
) : onChainScore && onChainScore > 0 ? (
+ Verified On-Chain
{onChainScore.toLocaleString()}
) : null}
diff --git a/components/layout/Breadcrumbs.tsx b/components/layout/Breadcrumbs.tsx
index f2b2528..606a933 100644
--- a/components/layout/Breadcrumbs.tsx
+++ b/components/layout/Breadcrumbs.tsx
@@ -1,54 +1,9 @@
'use client';
-import React from 'react';
-import Link from 'next/link';
-import { ChevronRight, Home } from 'lucide-react';
-import { useBreadcrumbs } from '@/hooks/useBreadcrumbs';
-import { clsx, type ClassValue } from 'clsx';
-import { twMerge } from 'tailwind-merge';
+// The dynamic breadcrumb implementation now lives in components/shared/BreadcrumbNav.
+// Re-exported here under its original name/path for backward compatibility.
+import { BreadcrumbNav } from '@/components/shared/BreadcrumbNav';
-function cn(...inputs: ClassValue[]) {
- return twMerge(clsx(inputs));
-}
-
-export const Breadcrumbs: React.FC = () => {
- const breadcrumbs = useBreadcrumbs();
-
- if (breadcrumbs.length <= 1) return null;
-
- return (
-
- );
-};
+export const Breadcrumbs = BreadcrumbNav;
export default Breadcrumbs;
diff --git a/components/shared/BreadcrumbNav.tsx b/components/shared/BreadcrumbNav.tsx
index 6195dcf..ded9333 100644
--- a/components/shared/BreadcrumbNav.tsx
+++ b/components/shared/BreadcrumbNav.tsx
@@ -1,11 +1,65 @@
'use client';
+/**
+ * BreadcrumbNav — Dynamic breadcrumb navigation.
+ *
+ * Intercepts the current router pathname and renders a human-readable
+ * trail, mapping raw UUID/numeric slugs (e.g. /escrow/123) to friendly
+ * labels (e.g. "Escrow > View Contract") instead of showing the raw URL.
+ *
+ * Architecture: BreadcrumbNav (Component) -> useBreadcrumbs (Hook) ->
+ * breadcrumbService -> Backend
+ */
+
import React from 'react';
-import Breadcrumbs from '@/components/layout/Breadcrumbs';
+import Link from 'next/link';
+import { ChevronRight, Home } from 'lucide-react';
+import { clsx, type ClassValue } from 'clsx';
+import { twMerge } from 'tailwind-merge';
+import { useBreadcrumbs } from '@/hooks/useBreadcrumbs';
+
+function cn(...inputs: ClassValue[]) {
+ return twMerge(clsx(inputs));
+}
-// Simple shared wrapper to expose a breadcrumb component from shared folder
export const BreadcrumbNav: React.FC = () => {
- return ;
+ const breadcrumbs = useBreadcrumbs();
+
+ if (breadcrumbs.length <= 1) return null;
+
+ return (
+
+ );
};
export default BreadcrumbNav;
diff --git a/hooks/useDriverReputation.ts b/hooks/useDriverReputation.ts
index c17c92e..e70e2d1 100644
--- a/hooks/useDriverReputation.ts
+++ b/hooks/useDriverReputation.ts
@@ -1,14 +1,64 @@
'use client';
-// This is a placeholder. In a real implementation, this would be a proper hook
-// following the Component -> Hook -> Service pattern.
-// It would use React Query and a service to call the Soroban RPC.
-export const useDriverReputation = (driverId: string) => {
- // For this demonstration, we return a static score.
- console.log(`Fetching on-chain reputation for driver: ${driverId}`);
- return {
- onChainScore: 1250,
- isLoading: false,
- error: null,
- };
-};
\ No newline at end of file
+import { useEffect, useState } from 'react';
+import axios from 'axios';
+import { reputationService } from '@/services/reputationService';
+
+export interface UseDriverReputationResult {
+ onChainScore: number | null;
+ isLoading: boolean;
+ error: string | null;
+}
+
+/**
+ * useDriverReputation — fetches a driver's on-chain reputation token score.
+ *
+ * Components consume this hook; they never call reputationService directly.
+ * Aborts in-flight requests on unmount or driverId change to avoid setting
+ * state on an unmounted component.
+ */
+export function useDriverReputation(driverId: string): UseDriverReputationResult {
+ const [onChainScore, setOnChainScore] = useState(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ if (!driverId) {
+ setOnChainScore(null);
+ setIsLoading(false);
+ return;
+ }
+
+ const controller = new AbortController();
+ let cancelled = false;
+
+ setIsLoading(true);
+
+ reputationService
+ .getDriverReputation(driverId, controller.signal)
+ .then((data) => {
+ if (cancelled) return;
+ setOnChainScore(data.onChainScore);
+ setError(null);
+ })
+ .catch((err: unknown) => {
+ if (cancelled) return;
+ if (axios.isCancel(err)) return;
+ const message =
+ err instanceof Error && err.message
+ ? err.message
+ : 'Failed to load on-chain reputation';
+ setError(message);
+ })
+ .finally(() => {
+ if (!cancelled) setIsLoading(false);
+ });
+
+ return () => {
+ cancelled = true;
+ controller.abort();
+ };
+ }, [driverId]);
+
+ return { onChainScore, isLoading, error };
+}
diff --git a/hooks/useLocalizedFiatPreview.ts b/hooks/useLocalizedFiatPreview.ts
new file mode 100644
index 0000000..0fa8d47
--- /dev/null
+++ b/hooks/useLocalizedFiatPreview.ts
@@ -0,0 +1,82 @@
+/**
+ * useLocalizedFiatPreview — Hook layer for live NGN/USD equivalents of an XLM amount.
+ *
+ * Fetches (and caches) the current NGN and USD rates per XLM, then computes
+ * the localized fiat equivalents for the given amount on every render so the
+ * display updates instantly as the caller's XLM amount changes.
+ *
+ * Architecture: Component -> useLocalizedFiatPreview (Hook) ->
+ * localizedFxService -> currencyRateService -> Backend
+ */
+
+'use client';
+
+import { useQuery } from '@tanstack/react-query';
+import {
+ localizedFxService,
+ formatLocalizedAmount,
+ SUPPORTED_FIAT_CODES,
+ type SupportedFiatCode,
+} from '@/services/localizedFxService';
+
+const REFETCH_INTERVAL_MS = 60_000; // matches the service cache TTL
+
+export interface LocalizedFiatEquivalent {
+ fiatCode: SupportedFiatCode;
+ /** Formatted currency string, e.g. "₦25,000.00". Empty string when unavailable. */
+ formatted: string;
+ isLoading: boolean;
+ isError: boolean;
+}
+
+export interface UseLocalizedFiatPreviewResult {
+ equivalents: LocalizedFiatEquivalent[];
+ isLoading: boolean;
+}
+
+/**
+ * @param xlmAmount - The XLM amount to convert. Non-positive values resolve
+ * to unavailable equivalents rather than fetching.
+ */
+export function useLocalizedFiatPreview(xlmAmount: number): UseLocalizedFiatPreviewResult {
+ const isValidAmount = Number.isFinite(xlmAmount) && xlmAmount > 0;
+
+ const ngnQuery = useQuery({
+ queryKey: ['localized-fx-rate', 'NGN'],
+ queryFn: () => localizedFxService.getRate('NGN'),
+ enabled: isValidAmount,
+ refetchInterval: REFETCH_INTERVAL_MS,
+ staleTime: REFETCH_INTERVAL_MS,
+ retry: false,
+ });
+
+ const usdQuery = useQuery({
+ queryKey: ['localized-fx-rate', 'USD'],
+ queryFn: () => localizedFxService.getRate('USD'),
+ enabled: isValidAmount,
+ refetchInterval: REFETCH_INTERVAL_MS,
+ staleTime: REFETCH_INTERVAL_MS,
+ retry: false,
+ });
+
+ const queriesByFiat = { NGN: ngnQuery, USD: usdQuery } as const;
+
+ const equivalents: LocalizedFiatEquivalent[] = SUPPORTED_FIAT_CODES.map((fiatCode) => {
+ const query = queriesByFiat[fiatCode];
+ const rate = query.data?.ratePerXlm;
+ const amount =
+ isValidAmount && typeof rate === 'number' && rate > 0 ? xlmAmount * rate : null;
+
+ return {
+ fiatCode,
+ formatted: amount !== null ? formatLocalizedAmount(amount, fiatCode) : '',
+ isLoading: query.isLoading,
+ isError: query.isError,
+ };
+ });
+
+ return {
+ equivalents,
+ isLoading: ngnQuery.isLoading || usdQuery.isLoading,
+ };
+}
diff --git a/services/localizedFxService.ts b/services/localizedFxService.ts
new file mode 100644
index 0000000..c2eea1c
--- /dev/null
+++ b/services/localizedFxService.ts
@@ -0,0 +1,88 @@
+/**
+ * localizedFxService — Service layer for multi-currency (NGN + USD) live FX rates.
+ *
+ * Wraps currencyRateService with a small in-memory TTL cache keyed per fiat
+ * code, and exposes an Intl.NumberFormat-based formatter so the hook/component
+ * layer never instantiates formatters directly.
+ *
+ * Architecture: Component -> useLocalizedFiatPreview (Hook) ->
+ * localizedFxService -> currencyRateService -> Backend
+ */
+
+import { currencyRateService } from '@/services/currencyRateService';
+
+export type SupportedFiatCode = 'NGN' | 'USD';
+
+export const SUPPORTED_FIAT_CODES: SupportedFiatCode[] = ['NGN', 'USD'];
+
+export interface LocalizedRate {
+ fiatCode: SupportedFiatCode;
+ ratePerXlm: number;
+ updatedAt: string;
+}
+
+const CACHE_TTL_MS = 60_000; // 60 seconds — balance between freshness and API load
+
+interface CacheEntry {
+ data: LocalizedRate;
+ expiresAt: number;
+}
+
+const cache = new Map();
+
+const FORMATTERS: Record = {
+ NGN: new Intl.NumberFormat('en-NG', {
+ style: 'currency',
+ currency: 'NGN',
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }),
+ USD: new Intl.NumberFormat('en-US', {
+ style: 'currency',
+ currency: 'USD',
+ minimumFractionDigits: 2,
+ maximumFractionDigits: 2,
+ }),
+};
+
+/**
+ * Formats an amount as a localized currency string.
+ * e.g. formatLocalizedAmount(1234.5, 'NGN') -> "₦1,234.50"
+ */
+export function formatLocalizedAmount(amount: number, fiatCode: SupportedFiatCode): string {
+ return FORMATTERS[fiatCode].format(amount);
+}
+
+export const localizedFxService = {
+ /**
+ * Returns the current XLM rate for the given fiat currency.
+ * Hits the in-memory cache for up to `CACHE_TTL_MS` before re-fetching.
+ */
+ async getRate(fiatCode: SupportedFiatCode): Promise {
+ const now = Date.now();
+
+ const cached = cache.get(fiatCode);
+ if (cached && now < cached.expiresAt) {
+ return cached.data;
+ }
+
+ const response = await currencyRateService.getXlmRate(fiatCode);
+
+ const entry: LocalizedRate = {
+ fiatCode,
+ ratePerXlm: response.xlmRate,
+ updatedAt: response.updatedAt,
+ };
+
+ cache.set(fiatCode, { data: entry, expiresAt: now + CACHE_TTL_MS });
+
+ return entry;
+ },
+
+ /**
+ * Clears the in-memory cache — useful for testing or forced refresh.
+ */
+ clearCache(): void {
+ cache.clear();
+ },
+};
diff --git a/services/reputationService.ts b/services/reputationService.ts
new file mode 100644
index 0000000..2b539c6
--- /dev/null
+++ b/services/reputationService.ts
@@ -0,0 +1,31 @@
+import axios from 'axios';
+
+const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL ?? '';
+
+export interface DriverReputationData {
+ driverId: string;
+ onChainScore: number;
+ updatedAt: string;
+}
+
+/**
+ * reputationService — fetches a driver's tokenized on-chain reputation.
+ *
+ * The backend resolves this by querying the Soroban RPC network for the
+ * driver's reputation token state; the frontend only ever talks to the
+ * backend API, never the RPC endpoint directly.
+ *
+ * Hooks call this; components never call this directly.
+ */
+export const reputationService = {
+ async getDriverReputation(
+ driverId: string,
+ signal?: AbortSignal,
+ ): Promise {
+ const { data } = await axios.get(
+ `${API_BASE_URL}/fleet/drivers/${driverId}/reputation`,
+ { signal },
+ );
+ return data;
+ },
+};