UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

262 lines (261 loc) 9.47 kB
import { jsx, jsxs } from "react/jsx-runtime"; 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"; const PRESET_SLIPPAGE = [0.5, 1, 3, 5, 10]; export default function SlippageConfig({ value, onChange, config = void 0, onConfigChange = void 0, exchangeRate = null, baseCurrency = "USD", disabled = false, sx = {}, onCancel = void 0, onSave = void 0 }) { const { t } = useLocaleContext(); const [inputValue, setInputValue] = useState(""); const [inputMode, setInputMode] = useState(config?.mode || "percent"); const [error, setError] = useState(null); const [isEditing, setIsEditing] = useState(false); const percentValue = config?.percent ?? value; 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]); const computeMinRateFromPercent = (percent) => { if (!roundedRate) return ""; const slippageMultiplier = 1 + percent / 100; return (roundedRate / slippageMultiplier).toFixed(2); }; useEffect(() => { if (config?.mode && config.mode !== inputMode) { setInputMode(config.mode); } }, [config?.mode, inputMode]); useEffect(() => { 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); }, [percentValue, inputMode, exchangeRate, config?.min_acceptable_rate, isEditing]); const handlePresetClick = (preset) => { if (disabled) return; setInputValue(preset.toFixed(2)); setInputMode("percent"); setError(null); onChange(preset); const minRate = computeMinRateFromPercent(preset); onConfigChange?.({ mode: "percent", percent: preset, ...minRate ? { min_acceptable_rate: minRate } : {} }); }; const handleInputChange = (newValue) => { 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); const minRate = computeMinRateFromPercent(numValue); onConfigChange?.({ mode: "percent", percent: numValue, ...minRate ? { min_acceptable_rate: minRate } : {} }); } else { if (!roundedRate) { setError(t("payment.checkout.quote.slippage.rateRequired")); return; } 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 = (_, newMode) => { 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)); 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 "\u2014"; } return roundedRate.toFixed(2); }, [roundedRate]); const handleCancel = () => { onCancel?.(); }; const handleSave = () => { onSave?.(); }; return /* @__PURE__ */ jsxs(Stack, { spacing: 2.5, sx, children: [ /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", children: t("payment.checkout.quote.slippageLimit.description") }), /* @__PURE__ */ jsxs( ToggleButtonGroup, { value: inputMode, exclusive: true, onChange: handleModeChange, size: "small", fullWidth: true, disabled, children: [ /* @__PURE__ */ jsx(ToggleButton, { value: "percent", children: t("payment.checkout.quote.slippageLimit.configTogglePercent") }), /* @__PURE__ */ jsx(ToggleButton, { value: "rate", disabled: !roundedRate, children: t("payment.checkout.quote.slippageLimit.configToggleRate") }) ] } ), inputMode === "percent" && /* @__PURE__ */ jsxs(Stack, { spacing: 1.5, children: [ /* @__PURE__ */ jsx(Stack, { direction: "row", spacing: 1, children: PRESET_SLIPPAGE.map((preset) => /* @__PURE__ */ jsx( ToggleButton, { value: preset, selected: Math.abs(percentValue - preset) < 0.01, onClick: () => handlePresetClick(preset), size: "small", 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" } } }, children: /* @__PURE__ */ jsxs(Typography, { variant: "body2", children: [ preset, "%" ] }) }, preset )) }), /* @__PURE__ */ jsx( TextField, { size: "small", fullWidth: true, value: inputValue, onChange: (e) => handleInputChange(e.target.value), onFocus: () => setIsEditing(true), onBlur: () => setIsEditing(false), error: !!error, helperText: error, disabled, label: t("common.custom"), InputProps: { endAdornment: /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { color: "text.secondary", mr: 1 }, children: "%" }) }, placeholder: "0.50" } ) ] }), inputMode === "rate" && /* @__PURE__ */ jsx(Stack, { spacing: 1.5, children: /* @__PURE__ */ jsx( TextField, { size: "small", fullWidth: true, value: inputValue, onChange: (e) => 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: /* @__PURE__ */ jsx(Typography, { variant: "body2", sx: { color: "text.secondary", mr: 1 }, children: baseCurrency }) }, placeholder: roundedRate?.toFixed(2) || "0.00" } ) }), roundedRate && /* @__PURE__ */ jsx( Box, { sx: { borderRadius: 1, p: 1.5, bgcolor: "action.hover" }, children: /* @__PURE__ */ jsxs(Stack, { spacing: 0.5, children: [ /* @__PURE__ */ jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", children: [ /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", children: t("payment.checkout.quote.slippageLimit.derivedCurrentRate") }), /* @__PURE__ */ jsxs(Typography, { variant: "body2", fontWeight: 500, children: [ currentRateLabel, " ", baseCurrency ] }) ] }), /* @__PURE__ */ jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", children: [ /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "text.secondary", children: t("payment.checkout.quote.slippageLimit.derivedMinRate") }), /* @__PURE__ */ jsxs(Typography, { variant: "body2", fontWeight: 500, color: "primary.main", children: [ inputMode === "rate" ? inputValue : minAcceptableRate || "\u2014", " ", baseCurrency ] }) ] }) ] }) } ), /* @__PURE__ */ jsxs(Stack, { direction: "row", spacing: 1, justifyContent: "flex-end", children: [ /* @__PURE__ */ jsx(Button, { onClick: handleCancel, disabled, color: "inherit", children: t("common.cancel") }), /* @__PURE__ */ jsx(Button, { variant: "contained", onClick: handleSave, disabled: disabled || !!error, children: t("common.save") }) ] }) ] }); }