From f7db8dfa107b5d9203ae6f2ce77595c116072382 Mon Sep 17 00:00:00 2001 From: dnnyorji Date: Sat, 1 Aug 2026 21:44:22 +0100 Subject: [PATCH 1/3] enhance(fleet): fetch driver on-chain reputation from backend API Replace the hardcoded useDriverReputation placeholder with a real Component -> Hook -> Service pipeline (reputationService) that fetches each driver's tokenized Soroban reputation score from the backend, and add an explicit "Verified On-Chain" label to the reputation badge. --- components/fleet/DriverReputation.tsx | 3 +- hooks/useDriverReputation.ts | 74 ++++++++++++++++++++++----- services/reputationService.ts | 31 +++++++++++ 3 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 services/reputationService.ts 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/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/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; + }, +}; From d5d9d1e2fdd837776e3b5285b586b60d57a16a0f Mon Sep 17 00:00:00 2001 From: dnnyorji Date: Sat, 1 Aug 2026 21:45:24 +0100 Subject: [PATCH 2/3] refactor(shared): consolidate dynamic breadcrumb nav into BreadcrumbNav Move the breadcrumb rendering into components/shared/BreadcrumbNav.tsx so the Component (BreadcrumbNav) -> Hook (useBreadcrumbs) -> Service (breadcrumbService) chain lives in one place; components/layout/Breadcrumbs now re-exports it for backward compatibility. Behavior is unchanged: raw UUID/numeric slugs still resolve to human-readable labels via breadcrumbService. --- components/layout/Breadcrumbs.tsx | 53 ++----------------------- components/shared/BreadcrumbNav.tsx | 60 +++++++++++++++++++++++++++-- 2 files changed, 61 insertions(+), 52 deletions(-) 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; From fe858cc2ee5b07440d41833f7d65fb163248e031 Mon Sep 17 00:00:00 2001 From: dnnyorji Date: Sat, 1 Aug 2026 21:47:52 +0100 Subject: [PATCH 3/3] enhance(escrow): add live NGN/USD equivalents to FiatXlmPreview Add localizedFxService (cached, per-currency XLM rates) and useLocalizedFiatPreview to compute localized fiat equivalents, then render them below the XLM total in FiatXlmPreview using Intl.NumberFormat. The display recalculates instantly whenever the caller's xlmAmount prop changes. --- components/escrow/FiatXlmPreview.tsx | 21 +++++++ hooks/useLocalizedFiatPreview.ts | 82 ++++++++++++++++++++++++++ services/localizedFxService.ts | 88 ++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 hooks/useLocalizedFiatPreview.ts create mode 100644 services/localizedFxService.ts 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/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(); + }, +};