import { useState, useMemo, useEffect } from 'react'; import { Box, Stack, Typography, TextField, ToggleButton, ToggleButtonGroup, Button } from '@mui/material'; import { useLocaleContext } from '@arcblock/ux/lib/Locale/context'; export type SlippageConfigValue = { mode: 'percent' | 'rate'; percent: number; min_acceptable_rate?: string; base_currency?: string; updated_at_ms?: number; }; export interface SlippageConfigProps { value: number; onChange: (value: number) => void; config?: SlippageConfigValue; onConfigChange?: (value: SlippageConfigValue) => void; exchangeRate?: string | null; baseCurrency?: string; disabled?: boolean; sx?: any; onCancel?: () => void; onSave?: () => void; } const PRESET_SLIPPAGE = [0.5, 1, 3, 5, 10]; export default function SlippageConfig({ value, onChange, config = undefined, onConfigChange = undefined, exchangeRate = null, baseCurrency = 'USD', disabled = false, sx = {}, onCancel = undefined, onSave = undefined, }: SlippageConfigProps) { const { t } = useLocaleContext(); const [inputValue, setInputValue] = useState(''); const [inputMode, setInputMode] = useState<'percent' | 'rate'>(config?.mode || 'percent'); const [error, setError] = useState(null); const [isEditing, setIsEditing] = useState(false); // Track if user is actively editing input const percentValue = config?.percent ?? value; // Round exchange rate to 2 decimal places for simpler display and calculation const roundedRate = useMemo(() => { if (!exchangeRate) return null; const rateNum = Number(exchangeRate); if (Number.isNaN(rateNum) || rateNum <= 0) return null; return Math.round(rateNum * 100) / 100; }, [exchangeRate]); // Compute min rate from percent using rounded rate const computeMinRateFromPercent = (percent: number) => { if (!roundedRate) return ''; const slippageMultiplier = 1 + percent / 100; return (roundedRate / slippageMultiplier).toFixed(2); }; // Sync mode from config useEffect(() => { if (config?.mode && config.mode !== inputMode) { setInputMode(config.mode); } }, [config?.mode, inputMode]); // Sync input value based on mode - skip when user is actively editing useEffect(() => { // Skip sync when user is typing to prevent overwriting their input if (isEditing) { return; } if (inputMode === 'percent') { setInputValue(percentValue.toFixed(2)); return; } if (config?.min_acceptable_rate) { setInputValue(String(config.min_acceptable_rate)); return; } const minRate = computeMinRateFromPercent(percentValue); setInputValue(minRate); // eslint-disable-next-line react-hooks/exhaustive-deps }, [percentValue, inputMode, exchangeRate, config?.min_acceptable_rate, isEditing]); const handlePresetClick = (preset: number) => { if (disabled) return; setInputValue(preset.toFixed(2)); setInputMode('percent'); setError(null); onChange(preset); // Always pass min_acceptable_rate so backend saves the exact value user sees const minRate = computeMinRateFromPercent(preset); onConfigChange?.({ mode: 'percent', percent: preset, ...(minRate ? { min_acceptable_rate: minRate } : {}), }); }; const handleInputChange = (newValue: string) => { setInputValue(newValue); setError(null); const numValue = Number(newValue); if (!newValue || Number.isNaN(numValue)) { setError(t('payment.checkout.quote.slippage.invalid')); return; } if (numValue <= 0) { setError(t('payment.checkout.quote.slippage.invalidPositive')); return; } if (inputMode === 'percent') { onChange(numValue); // Always pass min_acceptable_rate so backend saves the exact value user sees const minRate = computeMinRateFromPercent(numValue); onConfigChange?.({ mode: 'percent', percent: numValue, ...(minRate ? { min_acceptable_rate: minRate } : {}), }); } else { // Rate mode: compute equivalent percent from input rate using rounded rate if (!roundedRate) { setError(t('payment.checkout.quote.slippage.rateRequired')); return; } // No range restriction - accept any positive rate const percent = ((roundedRate - numValue) / numValue) * 100; onChange(Math.max(0, percent)); onConfigChange?.({ mode: 'rate', percent: Math.max(0, percent), min_acceptable_rate: newValue }); } }; const handleModeChange = (_: any, newMode: 'percent' | 'rate' | null) => { if (disabled || !newMode) return; setInputMode(newMode); setError(null); if (newMode === 'rate') { if (!roundedRate) { setError(t('payment.checkout.quote.slippage.rateRequired')); return; } const minRate = config?.min_acceptable_rate || computeMinRateFromPercent(percentValue); setInputValue(minRate); onConfigChange?.({ mode: 'rate', percent: percentValue, min_acceptable_rate: minRate, }); } else { setInputValue(percentValue.toFixed(2)); // Always pass min_acceptable_rate so backend saves the exact value user sees const minRate = computeMinRateFromPercent(percentValue); onConfigChange?.({ mode: 'percent', percent: percentValue, ...(minRate ? { min_acceptable_rate: minRate } : {}), }); } }; const minAcceptableRate = useMemo(() => { if (!roundedRate) return null; const slippageMultiplier = 1 + percentValue / 100; return (roundedRate / slippageMultiplier).toFixed(2); }, [roundedRate, percentValue]); const currentRateLabel = useMemo(() => { if (!roundedRate) { return '—'; } return roundedRate.toFixed(2); }, [roundedRate]); const handleCancel = () => { onCancel?.(); }; const handleSave = () => { onSave?.(); }; return ( {/* Description */} {t('payment.checkout.quote.slippageLimit.description')} {/* Mode toggle */} {t('payment.checkout.quote.slippageLimit.configTogglePercent')} {t('payment.checkout.quote.slippageLimit.configToggleRate')} {/* Percent mode */} {inputMode === 'percent' && ( {PRESET_SLIPPAGE.map((preset) => ( handlePresetClick(preset)} size="small" disabled={disabled} sx={{ flex: 1, py: 1, borderRadius: 1, border: '1px solid', borderColor: 'divider', '&.Mui-selected': { bgcolor: 'primary.main', color: 'primary.contrastText', borderColor: 'primary.main', '&:hover': { bgcolor: 'primary.dark', }, }, }}> {preset}% ))} {/* Custom input */} handleInputChange(e.target.value)} onFocus={() => setIsEditing(true)} onBlur={() => setIsEditing(false)} error={!!error} helperText={error} disabled={disabled} label={t('common.custom')} InputProps={{ endAdornment: ( % ), }} placeholder="0.50" /> )} {/* Rate mode */} {inputMode === 'rate' && ( handleInputChange(e.target.value)} onFocus={() => setIsEditing(true)} onBlur={() => setIsEditing(false)} error={!!error} helperText={error} disabled={disabled || !roundedRate} label={t('payment.checkout.quote.slippage.rateInputLabel', { currency: baseCurrency })} InputProps={{ endAdornment: ( {baseCurrency} ), }} placeholder={roundedRate?.toFixed(2) || '0.00'} /> )} {/* Rate info - only show when exchange rate is available */} {roundedRate && ( {t('payment.checkout.quote.slippageLimit.derivedCurrentRate')} {currentRateLabel} {baseCurrency} {t('payment.checkout.quote.slippageLimit.derivedMinRate')} {inputMode === 'rate' ? inputValue : minAcceptableRate || '—'} {baseCurrency} )} {/* Actions */} ); }