diff --git a/components/escrow/FiatXlmPreview.tsx b/components/escrow/FiatXlmPreview.tsx
index 11d76c8..26b2e58 100644
--- a/components/escrow/FiatXlmPreview.tsx
+++ b/components/escrow/FiatXlmPreview.tsx
@@ -15,15 +15,14 @@
import React, { useEffect } from 'react';
import {
- AlertTriangle,
AlertCircle,
TrendingDown,
TrendingUp,
Loader2,
- CheckCircle2,
} from 'lucide-react';
import { useFiatXlmSlippage } from '@/hooks/useFiatXlmSlippage';
import { formatNgn } from '@/services/fxService';
+import { SlippageWarning } from '@/components/escrow/SlippageWarning';
export interface FiatXlmPreviewProps {
/** The XLM amount being paid */
@@ -184,67 +183,15 @@ export function FiatXlmPreview({
)}
- {/* Warning Section — volatile slippage (>2%) */}
- {slippage.isVolatile && !slippage.requiresAcknowledgment && (
-
-
-
-
FX Rate Volatility Detected
-
- The NGN/XLM rate has shifted{' '}
-
- {Math.abs(slippagePercent).toFixed(2)}%
-
- {' '}since your quote. Please review the updated rate.
-
-
-
- )}
-
- {/* Critical Section — critical slippage (>5%) */}
- {slippage.requiresAcknowledgment && (
-
-
-
-
⚠️ High FX Rate Volatility
-
- The NGN/XLM rate has shifted more than{' '}
-
- {Math.abs(slippagePercent).toFixed(2)}%
-
- . This is a significant change. You must acknowledge this volatility
- before proceeding.
-
-
- {/* Acknowledgment Checkbox */}
-
- slippage.setAcknowledged(e.target.checked)}
- disabled={isSubmitting}
- className="mt-1 h-4 w-4 cursor-pointer rounded border-gray-300 text-red-600 focus:ring-red-500 dark:border-gray-600 dark:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-50"
- aria-label="Acknowledge FX rate volatility"
- />
-
- I acknowledge the rate volatility and accept the current rate
-
-
-
-
- )}
+ {/* FX rate volatility warning — see SlippageWarning for the 2%/5% threshold UI */}
+
{/* Loading State */}
{slippage.isLoadingRate && (
@@ -254,14 +201,6 @@ export function FiatXlmPreview({
)}
- {/* Success State */}
- {slippage.isAcknowledged && slippage.requiresAcknowledgment && (
-
-
- Volatility acknowledged. Ready to proceed.
-
- )}
-
{/* Action Buttons */}
2%): non-blocking advisory banner.
+ * - Critical (>5%): blocking banner with a required acknowledgment checkbox.
+ * - Acknowledged: confirmation that the user accepted the critical variance.
+ *
+ * Kept separate from FiatXlmPreview so any escrow flow that quotes a
+ * fiat/XLM rate can reuse the same warning UI and thresholds.
+ *
+ * Architecture: SlippageWarning (Component) → useFiatXlmSlippage (Hook) →
+ * fiatXlmSlippageService → fxService → Backend
+ */
+
+import React from 'react';
+import { AlertTriangle, CheckCircle2 } from 'lucide-react';
+
+export interface SlippageWarningProps {
+ /** Percentage change of the current rate against the quoted rate */
+ slippagePercent: number;
+ /** True if slippage magnitude exceeds the 2% volatility threshold */
+ isVolatile: boolean;
+ /** True if slippage magnitude exceeds the 5% critical threshold */
+ requiresAcknowledgment: boolean;
+ /** Whether the user has ticked the acknowledgment checkbox */
+ isAcknowledged: boolean;
+ /** Called with the new checked state when the checkbox is toggled */
+ onAcknowledgeChange: (acknowledged: boolean) => void;
+ /** Disables the checkbox while a submission is in flight */
+ isSubmitting?: boolean;
+}
+
+export function SlippageWarning({
+ slippagePercent,
+ isVolatile,
+ requiresAcknowledgment,
+ isAcknowledged,
+ onAcknowledgeChange,
+ isSubmitting = false,
+}: SlippageWarningProps) {
+ if (!isVolatile && !requiresAcknowledgment) {
+ return null;
+ }
+
+ const magnitude = Math.abs(slippagePercent).toFixed(2);
+
+ if (requiresAcknowledgment) {
+ return (
+
+
+
+
High FX Rate Volatility
+
+ The NGN/XLM rate has shifted more than{' '}
+ {magnitude}% . This is a
+ significant change. You must acknowledge this volatility before
+ proceeding.
+
+
+
+ onAcknowledgeChange(e.target.checked)}
+ disabled={isSubmitting}
+ className="mt-1 h-4 w-4 cursor-pointer rounded border-gray-300 text-red-600 focus:ring-red-500 dark:border-gray-600 dark:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-50"
+ aria-label="Acknowledge FX rate volatility"
+ />
+
+ I acknowledge the rate volatility and accept the current rate
+
+
+
+ {isAcknowledged && (
+
+
+ Volatility acknowledged. Ready to proceed.
+
+ )}
+
+
+ );
+ }
+
+ return (
+
+
+
+
FX Rate Volatility Detected
+
+ The NGN/XLM rate has shifted{' '}
+ {magnitude}% since your
+ quote. Please review the updated rate.
+
+
+
+ );
+}
diff --git a/components/escrow/__tests__/SlippageWarning.test.tsx b/components/escrow/__tests__/SlippageWarning.test.tsx
new file mode 100644
index 0000000..69a4edb
--- /dev/null
+++ b/components/escrow/__tests__/SlippageWarning.test.tsx
@@ -0,0 +1,111 @@
+/**
+ * Tests for SlippageWarning
+ *
+ * Covers:
+ * - No render when slippage is within normal range
+ * - Advisory banner for volatile slippage (>2%)
+ * - Blocking banner + checkbox for critical slippage (>5%)
+ * - Acknowledgment callback wiring
+ * - Success state once acknowledged
+ */
+
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import { SlippageWarning } from '@/components/escrow/SlippageWarning';
+
+describe('SlippageWarning', () => {
+ it('renders nothing when slippage is within the normal range', () => {
+ render(
+
+ );
+
+ expect(screen.queryByRole('alert')).not.toBeInTheDocument();
+ });
+
+ it('shows the volatility advisory banner when slippage exceeds 2% but not 5%', () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('alert')).toBeInTheDocument();
+ expect(screen.getByText(/FX Rate Volatility Detected/i)).toBeInTheDocument();
+ expect(screen.getByText(/3\.14%/)).toBeInTheDocument();
+ expect(screen.queryByRole('checkbox')).not.toBeInTheDocument();
+ });
+
+ it('shows the blocking critical banner with a checkbox when slippage exceeds 5%', () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('alert')).toBeInTheDocument();
+ expect(screen.getByText(/High FX Rate Volatility/i)).toBeInTheDocument();
+ const checkbox = screen.getByRole('checkbox') as HTMLInputElement;
+ expect(checkbox).toBeInTheDocument();
+ expect(checkbox.checked).toBe(false);
+ });
+
+ it('invokes onAcknowledgeChange with the new checked state when toggled', () => {
+ const onAcknowledgeChange = jest.fn();
+ render(
+
+ );
+
+ fireEvent.click(screen.getByRole('checkbox'));
+
+ expect(onAcknowledgeChange).toHaveBeenCalledWith(true);
+ });
+
+ it('disables the checkbox while a submission is in flight', () => {
+ render(
+
+ );
+
+ expect(screen.getByRole('checkbox')).toBeDisabled();
+ });
+
+ it('shows the acknowledged confirmation once the checkbox is checked', () => {
+ render(
+
+ );
+
+ expect(screen.getByText(/Volatility acknowledged/i)).toBeInTheDocument();
+ });
+});