@blocklet/payment-react
Version:
Reusable react components for payment kit v2
262 lines (261 loc) • 9.47 kB
JavaScript
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") })
] })
] });
}