@blocklet/payment-react
Version:
Reusable react components for payment kit v2
468 lines (467 loc) • 15.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
module.exports = ProductDonation;
var _jsxRuntime = require("react/jsx-runtime");
var _context = require("@arcblock/ux/lib/Locale/context");
var _material = require("@mui/material");
var _AutoAwesome = _interopRequireDefault(require("@mui/icons-material/AutoAwesome"));
var _ahooks = require("ahooks");
var _react = require("react");
var _util = require("../libs/util");
var _payment = require("../contexts/payment");
var _scroll = require("../hooks/scroll");
var _keyboard = require("../hooks/keyboard");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const DONATION_PRESET_KEY_BASE = "payment-donation-preset";
const DONATION_CUSTOM_AMOUNT_KEY_BASE = "payment-donation-custom-amount";
const formatAmount = amount => {
const str = String(amount);
if (!str || str === "0") return str;
const num = parseFloat(str);
if (Number.isNaN(num)) return str;
return num.toString();
};
function ProductDonation({
item,
settings,
onChange,
currency
}) {
const {
t,
locale
} = (0, _context.useLocaleContext)();
const {
setPayable,
session
} = (0, _payment.usePaymentContext)();
(0, _scroll.usePreventWheel)();
const presets = (settings?.amount?.presets || []).map(formatAmount);
const getUserStorageKey = base => {
const userDid = session?.user?.did;
return userDid ? `${base}:${userDid}` : base;
};
const getSavedCustomAmount = () => {
try {
const saved = localStorage.getItem(getUserStorageKey(DONATION_CUSTOM_AMOUNT_KEY_BASE)) || "";
return saved ? formatAmount(saved) : "";
} catch (e) {
console.warn("Failed to access localStorage", e);
return "";
}
};
const getDefaultPreset = () => {
try {
const savedPreset = localStorage.getItem(getUserStorageKey(DONATION_PRESET_KEY_BASE));
if (savedPreset) {
if (presets.includes(formatAmount(savedPreset))) {
return formatAmount(savedPreset);
}
if (savedPreset === "custom" && supportCustom) {
return "custom";
}
}
} catch (e) {
console.warn("Failed to access localStorage", e);
}
if (presets.length > 0) {
const middleIndex = Math.floor(presets.length / 2);
return presets[middleIndex];
}
return "0";
};
const supportPreset = presets.length > 0;
const supportCustom = !!settings?.amount?.custom;
const defaultPreset = getDefaultPreset();
const defaultCustomAmount = defaultPreset === "custom" ? getSavedCustomAmount() : "";
const [state, setState] = (0, _ahooks.useSetState)({
selected: defaultPreset === "custom" ? "" : defaultPreset,
input: defaultCustomAmount,
custom: !supportPreset || defaultPreset === "custom",
error: "",
animating: false
});
const customInputRef = (0, _react.useRef)(null);
const containerRef = (0, _react.useRef)(null);
const handleSelect = amount => {
setPayable(true);
setState({
selected: formatAmount(amount),
custom: false,
error: ""
});
onChange({
priceId: item.price_id,
amount: formatAmount(amount)
});
localStorage.setItem(getUserStorageKey(DONATION_PRESET_KEY_BASE), formatAmount(amount));
};
const handleCustomSelect = () => {
setState({
custom: true,
selected: "",
animating: true
});
const hasPresets = presets.length > 0;
let sortedPresets = [];
if (hasPresets) {
sortedPresets = [...presets].map(p => parseFloat(p)).sort((a, b) => a - b);
}
const minPreset = hasPresets ? sortedPresets[sortedPresets.length - 1] : 1;
let maxPreset = hasPresets ? sortedPresets[sortedPresets.length - 1] * 5 : 100;
const systemMax = settings.amount.maximum ? parseFloat(settings.amount.maximum) : Infinity;
maxPreset = Math.min(maxPreset, systemMax);
const detectPrecision = () => {
let maxPrecision = 2;
if (!hasPresets) return 0;
const allIntegers = presets.every(preset => {
const num = parseFloat(preset);
return num === Math.floor(num);
});
if (allIntegers) return 0;
presets.forEach(preset => {
const decimalPart = preset.toString().split(".")[1];
if (decimalPart) {
maxPrecision = Math.max(maxPrecision, decimalPart.length);
}
});
return maxPrecision;
};
const precision = detectPrecision();
let randomAmount;
if (precision === 0) {
randomAmount = (Math.round(Math.random() * (maxPreset - minPreset) + minPreset) || 1).toString();
} else {
randomAmount = (Math.random() * (maxPreset - minPreset) + minPreset).toFixed(precision);
}
const startValue = state.input ? parseFloat(state.input) : 0;
const targetValue = parseFloat(randomAmount);
const difference = targetValue - startValue;
const startTime = Date.now();
const duration = 800;
const updateCounter = () => {
const currentTime = Date.now();
const elapsed = currentTime - startTime;
if (elapsed < duration) {
const progress = elapsed / duration;
const intermediateValue = startValue + difference * progress;
const currentValue = precision === 0 ? Math.floor(intermediateValue).toString() : intermediateValue.toFixed(precision);
setState({
input: currentValue
});
requestAnimationFrame(updateCounter);
} else {
setState({
input: randomAmount,
animating: false,
error: ""
});
onChange({
priceId: item.price_id,
amount: formatAmount(randomAmount)
});
setPayable(true);
localStorage.setItem(getUserStorageKey(DONATION_CUSTOM_AMOUNT_KEY_BASE), formatAmount(randomAmount));
setTimeout(() => {
customInputRef.current?.focus();
}, 200);
}
};
requestAnimationFrame(updateCounter);
localStorage.setItem(getUserStorageKey(DONATION_PRESET_KEY_BASE), "custom");
};
const handleTabSelect = selectedItem => {
if (selectedItem === "custom") {
handleCustomSelect();
} else {
handleSelect(selectedItem);
}
};
const {
handleKeyDown
} = (0, _keyboard.useTabNavigation)(presets, handleTabSelect, {
includeCustom: supportCustom,
currentValue: state.custom ? void 0 : state.selected,
isCustomSelected: state.custom,
enabled: true,
selector: ".tab-navigable-card button",
containerRef
});
(0, _react.useEffect)(() => {
const currentPreset = getDefaultPreset();
const isCustom = currentPreset === "custom";
setState({
selected: isCustom ? "" : currentPreset,
custom: !supportPreset || currentPreset === "custom",
input: defaultCustomAmount,
error: ""
});
if (!isCustom) {
onChange({
priceId: item.price_id,
amount: currentPreset
});
setPayable(true);
} else if (defaultCustomAmount) {
onChange({
priceId: item.price_id,
amount: defaultCustomAmount
});
setPayable(true);
} else {
setPayable(false);
}
}, [settings.amount.preset, settings.amount.presets, supportPreset]);
(0, _react.useEffect)(() => {
if (containerRef.current) {
containerRef.current.focus();
}
if (state.custom) {
setTimeout(() => {
customInputRef.current?.focus();
}, 0);
}
}, [state.custom]);
const handleInput = event => {
const {
value
} = event.target;
const min = parseFloat(settings.amount.minimum || "0");
const max = settings.amount.maximum ? parseFloat(settings.amount.maximum) : Infinity;
const precision = currency?.maximum_precision || 6;
if ((0, _util.formatAmountPrecisionLimit)(value, locale)) {
setState({
input: value,
error: (0, _util.formatAmountPrecisionLimit)(value, locale, precision)
});
setPayable(false);
return;
}
if (value < min || value > max) {
setState({
input: value,
error: t("payment.checkout.donation.between", {
min,
max
})
});
setPayable(false);
return;
}
setPayable(true);
setState({
error: "",
input: value
});
onChange({
priceId: item.price_id,
amount: formatAmount(value)
});
localStorage.setItem(getUserStorageKey(DONATION_CUSTOM_AMOUNT_KEY_BASE), formatAmount(value));
};
return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
ref: containerRef,
onKeyDown: handleKeyDown,
tabIndex: 0,
sx: {
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: 1.5,
outline: "none"
},
children: [supportPreset && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Grid, {
container: true,
spacing: 2,
children: [presets.map(amount => /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Grid, {
size: {
xs: 6,
sm: 3
},
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Card, {
variant: "outlined",
className: "tab-navigable-card",
sx: {
minWidth: 115,
textAlign: "center",
transition: "all 0.3s",
cursor: "pointer",
"&:hover": {
transform: "translateY(-4px)",
boxShadow: 3
},
".MuiCardActionArea-focusHighlight": {
backgroundColor: "transparent"
},
height: "42px",
...(formatAmount(state.selected) === formatAmount(amount) && !state.custom ? {
borderColor: "primary.main",
borderWidth: 1
} : {})
},
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.CardActionArea, {
onClick: () => handleSelect(amount),
tabIndex: 0,
"aria-selected": formatAmount(state.selected) === formatAmount(amount) && !state.custom,
children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
direction: "row",
spacing: 0.5,
sx: {
alignItems: "center",
justifyContent: "center",
py: 1.5,
px: 1.5
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Avatar, {
src: currency?.logo,
sx: {
width: 16,
height: 16,
mr: 0.5
},
alt: currency?.symbol
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
component: "strong",
variant: "h3",
sx: {
lineHeight: 1,
fontVariantNumeric: "tabular-nums",
fontWeight: 400
},
children: amount
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
sx: {
lineHeight: 1,
fontSize: 14,
color: "text.secondary"
},
children: currency?.symbol
})]
})
})
})
}, amount)), supportCustom && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Grid, {
size: {
xs: 6,
sm: 3
},
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Card, {
variant: "outlined",
className: "tab-navigable-card",
sx: {
textAlign: "center",
transition: "all 0.3s",
cursor: "pointer",
"&:hover": {
transform: "translateY(-4px)",
boxShadow: 3
},
".MuiCardActionArea-focusHighlight": {
backgroundColor: "transparent"
},
height: "42px",
...(state.custom ? {
borderColor: "primary.main",
borderWidth: 1
} : {})
},
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.CardActionArea, {
onClick: handleCustomSelect,
tabIndex: 0,
"aria-selected": state.custom,
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, {
direction: "row",
spacing: 0.5,
sx: {
alignItems: "center",
justifyContent: "center",
py: 1.5,
px: 1.5
},
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
variant: "h3",
sx: {
lineHeight: 1,
fontWeight: 400
},
children: t("common.custom")
})
})
})
})
}, "custom")]
}), state.custom && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.TextField, {
type: "number",
value: state.input,
onChange: handleInput,
margin: "none",
fullWidth: true,
error: !!state.error,
helperText: state.error,
inputRef: customInputRef,
sx: {
mt: defaultPreset !== "0" ? 0 : 1,
"& .MuiInputBase-root": {
transition: "all 0.3s ease"
},
"& input[type=number]": {
MozAppearance: "textfield"
},
"& input[type=number]::-webkit-outer-spin-button": {
WebkitAppearance: "none",
margin: 0
},
"& input[type=number]::-webkit-inner-spin-button": {
WebkitAppearance: "none",
margin: 0
},
"& input": {
transition: "all 0.25s ease"
}
},
slotProps: {
input: {
endAdornment: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
direction: "row",
spacing: 0.5,
sx: {
alignItems: "center",
ml: 1
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.IconButton, {
size: "small",
onClick: handleCustomSelect,
disabled: state.animating,
sx: {
mr: 0.5,
opacity: state.animating ? 0.5 : 1,
transition: "all 0.2s ease",
"&:hover": {
transform: "scale(1.2)",
transition: "transform 0.3s ease"
}
},
"aria-label": t("common.random"),
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_AutoAwesome.default, {
fontSize: "small"
})
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Avatar, {
src: currency?.logo,
sx: {
width: 16,
height: 16
},
alt: currency?.symbol
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
children: currency?.symbol
})]
}),
autoComplete: "off"
}
},
autoComplete: "off"
})]
});
}