UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

468 lines (467 loc) 15.2 kB
"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" })] }); }