@blocklet/payment-react
Version:
Reusable react components for payment kit v2
803 lines (802 loc) • 28.6 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
module.exports = AutoTopup;
exports.waitForAutoRechargeComplete = void 0;
var _jsxRuntime = require("react/jsx-runtime");
var _react = require("react");
var _material = require("@mui/material");
var _iconsMaterial = require("@mui/icons-material");
var _reactHookForm = require("react-hook-form");
var _context = require("@arcblock/ux/lib/Locale/context");
var _Toast = _interopRequireDefault(require("@arcblock/ux/lib/Toast"));
var _Dialog = _interopRequireDefault(require("@arcblock/ux/lib/Dialog"));
var _ahooks = require("ahooks");
var _reactRouterDom = require("react-router-dom");
var _ufo = require("ufo");
var _pWaitFor = _interopRequireDefault(require("p-wait-for"));
var _DID = _interopRequireDefault(require("@arcblock/ux/lib/DID"));
var _switchButton = _interopRequireDefault(require("../switch-button"));
var _api = _interopRequireDefault(require("../../libs/api"));
var _util = require("../../libs/util");
var _payment = require("../../contexts/payment");
var _navigation = require("../../libs/navigation");
var _collapse = _interopRequireDefault(require("../collapse"));
var _input = _interopRequireDefault(require("../input"));
var _stripe = _interopRequireDefault(require("../../payment/form/stripe"));
var _productCard = _interopRequireDefault(require("./product-card"));
var _label = _interopRequireDefault(require("../label"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const fetchConfig = async (customerId, currencyId) => {
const {
data
} = await _api.default.get(`/api/auto-recharge-configs/customer/${customerId}`, {
params: {
currency_id: currencyId
}
});
return data;
};
const fetchCurrencyBalance = async (currencyId, payerAddress) => {
const {
data
} = await _api.default.get("/api/customers/payer-token", {
params: {
currencyId,
payerAddress
}
});
return data;
};
const DEFAULT_VALUES = {
enabled: false,
threshold: "100",
quantity: 1,
payment_method_id: "",
recharge_currency_id: "",
price_id: "",
daily_max_amount: 0,
daily_max_attempts: 0
};
const waitForAutoRechargeComplete = async configId => {
let result;
await (0, _pWaitFor.default)(async () => {
const {
data
} = await _api.default.get(`/api/auto-recharge-configs/retrieve/${configId}`);
result = data;
return !!result.payment_settings?.payment_method_options?.[result.paymentMethod.type]?.payer;
}, {
interval: 2e3,
timeout: 3 * 60 * 1e3
});
return result;
};
exports.waitForAutoRechargeComplete = waitForAutoRechargeComplete;
function PaymentMethodDisplay({
config,
onChangePaymentMethod,
paymentMethod,
currency
}) {
const {
t
} = (0, _context.useLocaleContext)();
const [changePaymentMethod, setChangePaymentMethod] = (0, _react.useState)(false);
const navigate = (0, _reactRouterDom.useNavigate)();
const paymentInfo = config?.payment_settings?.payment_method_options?.[paymentMethod.type || ""];
const {
data: balanceInfo,
loading: balanceLoading
} = (0, _ahooks.useRequest)(async () => {
if (paymentMethod.type === "stripe") {
return null;
}
const result = await fetchCurrencyBalance(currency.id, paymentInfo?.payer);
return result;
}, {
refreshDeps: [currency.id, paymentInfo?.payer],
ready: !!currency.id && !!paymentInfo?.payer
});
const handleChangeToggle = () => {
const newChange = !changePaymentMethod;
setChangePaymentMethod(newChange);
onChangePaymentMethod(newChange);
};
if (!paymentInfo) {
return null;
}
const handleRecharge = e => {
const url = (0, _ufo.joinURL)((0, _util.getPrefix)(), `/customer/recharge/${currency.id}?rechargeAddress=${paymentInfo?.payer}`);
const link = (0, _navigation.createLink)(url, true);
(0, _navigation.handleNavigation)(e, link, navigate);
};
const renderPaymentMethodInfo = () => {
if (paymentMethod.type === "stripe") {
return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
direction: "row",
spacing: 1,
sx: {
alignItems: "center"
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.CreditCard, {
fontSize: "small",
color: "primary"
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
variant: "body2",
children: ["**** **** **** ", paymentInfo?.card_last4 || "****"]
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
variant: "body2",
sx: {
color: "text.secondary",
textTransform: "uppercase"
},
children: paymentInfo?.card_brand || "CARD"
}), paymentInfo?.exp_time && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
variant: "body2",
sx: {
color: "text.secondary",
borderLeft: "1px solid",
borderColor: "divider",
pl: 1
},
children: paymentInfo?.exp_time
})]
});
}
return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
spacing: 1,
sx: {
borderRadius: 1,
backgroundColor: theme => theme.palette.mode === "dark" ? "grey.100" : "grey.50",
p: 2
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_DID.default, {
did: paymentInfo?.payer,
responsive: false,
compact: true,
copyable: false
}), (balanceInfo || balanceLoading) && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
direction: "row",
spacing: 1,
sx: {
alignItems: "center"
},
children: [balanceLoading ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.CircularProgress, {
size: 14,
sx: {
mr: 0.5
}
}) : /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
sx: {
display: "flex",
alignItems: "center",
gap: 1
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.AccountBalanceWalletOutlined, {
fontSize: "small",
sx: {
color: "text.lighter"
}
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
variant: "body2",
sx: {
color: "text.primary"
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)("strong", {
children: [(0, _util.formatBNStr)(balanceInfo?.token || "0", currency?.decimal), " "]
}), currency?.symbol || ""]
})]
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Button, {
size: "small",
variant: "text",
onClick: handleRecharge,
sx: {
fontSize: "smaller",
color: "primary.main"
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.AddOutlined, {
fontSize: "small"
}), t("payment.autoTopup.addFunds")]
})]
})]
});
};
return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
sx: {
p: 2,
border: "1px solid",
borderColor: changePaymentMethod ? "primary.main" : "divider",
borderRadius: 1,
bgcolor: changePaymentMethod ? "primary.50" : "background.paper"
},
children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
spacing: 2,
children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
sx: {
flexDirection: {
xs: "column",
sm: "row"
},
alignItems: {
xs: "flex-start",
sm: "center"
},
justifyContent: {
xs: "flex-start",
sm: "space-between"
}
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
variant: "subtitle2",
sx: {
display: "flex",
alignItems: "center",
gap: 1,
whiteSpace: "nowrap"
},
children: t("payment.autoTopup.currentPaymentMethod")
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, {
size: "small",
startIcon: /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.SwapHoriz, {}),
onClick: handleChangeToggle,
variant: "text",
sx: {
color: "primary.main",
whiteSpace: "nowrap",
alignSelf: {
xs: "flex-end",
sm: "center"
}
},
children: changePaymentMethod ? t("payment.autoTopup.keepCurrent") : t("payment.autoTopup.changePaymentMethod")
})]
}), changePaymentMethod ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
variant: "body2",
sx: {
color: "text.secondary"
},
children: t("payment.autoTopup.changePaymentMethodTip")
}) : renderPaymentMethodInfo()]
})
});
}
function AutoTopup({
open,
onClose,
currencyId,
onSuccess = () => {},
onError = () => {},
defaultEnabled = void 0
}) {
const {
t,
locale
} = (0, _context.useLocaleContext)();
const {
session,
connect,
settings
} = (0, _payment.usePaymentContext)();
const [changePaymentMethod, setChangePaymentMethod] = (0, _react.useState)(false);
const [state, setState] = (0, _ahooks.useSetState)({
loading: false,
submitting: false,
authorizationRequired: false,
stripeContext: {
client_secret: "",
intent_type: "",
status: "",
public_key: "",
customer: {}
}
});
const currencies = (0, _util.flattenPaymentMethods)(settings.paymentMethods);
const methods = (0, _reactHookForm.useForm)({
defaultValues: {
enabled: defaultEnabled || DEFAULT_VALUES.enabled,
threshold: DEFAULT_VALUES.threshold,
quantity: DEFAULT_VALUES.quantity,
payment_method_id: DEFAULT_VALUES.payment_method_id,
recharge_currency_id: DEFAULT_VALUES.recharge_currency_id,
price_id: DEFAULT_VALUES.price_id,
daily_limits: {
max_attempts: DEFAULT_VALUES.daily_max_attempts,
max_amount: DEFAULT_VALUES.daily_max_amount
}
}
});
const {
handleSubmit,
setValue,
watch
} = methods;
const enabled = watch("enabled");
const quantity = watch("quantity");
const rechargeCurrencyId = watch("recharge_currency_id");
const handleClose = () => {
setState({
loading: false,
submitting: false,
authorizationRequired: false
});
onClose();
};
const {
data: config
} = (0, _ahooks.useRequest)(() => fetchConfig(session?.user?.did, currencyId), {
refreshDeps: [session?.user?.did, currencyId],
ready: !!session?.user?.did && !!currencyId,
onError: error => {
_Toast.default.error((0, _util.formatError)(error));
},
onSuccess: data => {
setValue("enabled", defaultEnabled || data.enabled);
setValue("threshold", data.threshold);
setValue("quantity", data.quantity);
setValue("payment_method_id", data.payment_method_id);
setValue("recharge_currency_id", data.recharge_currency_id || data.price?.currency_id);
setValue("price_id", data.price_id);
setValue("daily_limits", {
max_amount: data.daily_limits?.max_amount || 0,
max_attempts: data.daily_limits?.max_attempts || 0
});
}
});
const filterCurrencies = (0, _react.useMemo)(() => {
return currencies.filter(c => config?.price?.currency_options?.find(o => o.currency_id === c.id));
}, [currencies, config]);
const handleConnected = async () => {
try {
const result = await waitForAutoRechargeComplete(config?.id);
if (result) {
setState({
submitting: false,
authorizationRequired: false
});
onSuccess?.(config);
handleClose();
_Toast.default.success(t("payment.autoTopup.saveSuccess"));
}
} catch (err) {
_Toast.default.error((0, _util.formatError)(err));
} finally {
setState({
submitting: false,
authorizationRequired: false
});
}
};
const handleDisable = async () => {
try {
const submitData = {
...config,
enabled: false
};
if (!config?.enabled) {
return;
}
const {
data
} = await _api.default.post("/api/auto-recharge-configs/submit", submitData);
onSuccess?.(data);
_Toast.default.success(t("payment.autoTopup.disableSuccess"));
} catch (error) {
_Toast.default.error((0, _util.formatError)(error));
onError?.(error);
}
};
const handleEnableChange = async checked => {
setValue("enabled", checked);
if (!checked) {
await handleDisable();
}
};
const handleAuthorizationRequired = authData => {
setState({
authorizationRequired: true
});
if (authData.stripeContext) {
setState({
stripeContext: {
client_secret: authData.stripeContext.client_secret,
intent_type: authData.stripeContext.intent_type,
status: authData.stripeContext.status,
public_key: authData.paymentMethod.settings.stripe.publishable_key,
customer: authData.customer
}
});
} else if (authData.delegation) {
handleDidConnect();
}
};
const handleDidConnect = () => {
try {
setState({
submitting: true
});
connect.open({
containerEl: void 0,
saveConnect: false,
locale,
action: "auto-recharge-auth",
prefix: (0, _ufo.joinURL)((0, _util.getPrefix)(), "/api/did"),
extraParams: {
autoRechargeConfigId: config?.id
},
messages: {
scan: t("payment.autoTopup.authTip"),
title: t("payment.autoTopup.authTitle"),
confirm: t("common.connect.confirm")
},
onSuccess: async () => {
connect.close();
await handleConnected();
},
onClose: () => {
connect.close();
setState({
submitting: false,
authorizationRequired: false
});
},
onError: err => {
setState({
submitting: false,
authorizationRequired: false
});
_Toast.default.error((0, _util.formatError)(err));
}
});
} catch (error) {
setState({
submitting: false,
authorizationRequired: false
});
_Toast.default.error((0, _util.formatError)(error));
}
};
const handleFormSubmit = async formData => {
setState({
submitting: true
});
try {
const submitData = {
customer_id: session?.user?.did,
enabled: formData.enabled,
threshold: formData.threshold,
currency_id: currencyId,
recharge_currency_id: formData.recharge_currency_id,
price_id: formData.price_id,
quantity: formData.quantity,
daily_limits: {
max_attempts: formData.daily_limits.max_attempts || 0,
max_amount: formData.daily_limits.max_amount || "0"
},
change_payment_method: changePaymentMethod
};
const {
data
} = await _api.default.post("/api/auto-recharge-configs/submit", submitData);
if (data.balanceResult && !data.balanceResult.sufficient) {
await handleAuthorizationRequired({
...data.balanceResult,
paymentMethod: data.paymentMethod,
customer: data.customer
});
return;
}
setState({
submitting: false,
authorizationRequired: false
});
onSuccess?.(data);
handleClose();
_Toast.default.success(t("payment.autoTopup.saveSuccess"));
} catch (error) {
setState({
submitting: false,
authorizationRequired: false
});
_Toast.default.error((0, _util.formatError)(error));
onError?.(error);
}
};
const onSubmit = formData => {
handleFormSubmit(formData);
};
const rechargeCurrency = filterCurrencies.find(c => c.id === rechargeCurrencyId);
const selectedMethod = settings.paymentMethods.find(method => {
return method.payment_currencies.find(c => c.id === rechargeCurrencyId);
});
const showStripeForm = state.authorizationRequired && selectedMethod?.type === "stripe";
const onStripeConfirm = async () => {
await handleConnected();
};
const onStripeCancel = () => {
setState({
submitting: false,
authorizationRequired: false
});
};
return /* @__PURE__ */(0, _jsxRuntime.jsx)(_Dialog.default, {
open,
onClose: handleClose,
maxWidth: "sm",
fullWidth: true,
className: "base-dialog",
title: t("payment.autoTopup.title"),
actions: enabled ? /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
direction: "row",
spacing: 2,
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, {
variant: "outlined",
onClick: handleClose,
disabled: state.submitting,
children: t("common.cancel")
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Button, {
variant: "contained",
onClick: () => handleSubmit(onSubmit)(),
disabled: state.loading || state.authorizationRequired || state.submitting,
children: [state.submitting && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.CircularProgress, {
size: 20,
sx: {
mr: 1
}
}), t("payment.autoTopup.saveConfiguration")]
})]
}) : null,
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_reactHookForm.FormProvider, {
...methods,
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
component: "form",
onSubmit: handleSubmit(onSubmit),
children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
sx: {
gap: 2
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Alert, {
severity: "info",
children: t("payment.autoTopup.tip")
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
spacing: 2,
direction: "row",
sx: {
alignItems: "center"
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
variant: "subtitle1",
children: t("payment.autoTopup.enableLabel")
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.FormControlLabel, {
control: /* @__PURE__ */(0, _jsxRuntime.jsx)(_switchButton.default, {
checked: enabled,
onChange: e => handleEnableChange(e.target.checked)
}),
label: ""
})]
}), enabled && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
sx: {
pt: 1
},
children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
spacing: 2.5,
children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
sx: {
gap: 2,
flexDirection: {
xs: "column",
sm: "row"
},
alignItems: {
xs: "flex-start",
sm: "center"
},
justifyContent: {
xs: "flex-start",
sm: "space-between"
},
".MuiTextField-root": {
width: {
xs: "100%",
sm: "auto"
}
}
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_label.default, {
boxSx: {
width: "fit-content",
whiteSpace: "nowrap",
marginBottom: 0
},
children: t("payment.autoTopup.triggerThreshold")
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_reactHookForm.Controller, {
name: "threshold",
control: methods.control,
rules: {
required: t("payment.checkout.required"),
min: {
value: 0,
message: t("payment.autoTopup.thresholdMinError")
}
},
render: ({
field
}) => /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.TextField, {
...field,
type: "number",
placeholder: t("payment.autoTopup.thresholdPlaceholder"),
sx: {
input: {
minWidth: 80
}
},
slotProps: {
input: {
endAdornment: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.InputAdornment, {
position: "end",
children: config?.currency?.name
})
},
htmlInput: {
min: 0,
step: 0.01
}
}
})
})]
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
sx: {
gap: 2,
flexDirection: {
xs: "column",
sm: "row"
},
alignItems: {
xs: "flex-start",
sm: "center"
},
justifyContent: {
xs: "flex-start",
sm: "space-between"
}
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_label.default, {
boxSx: {
width: "fit-content",
whiteSpace: "nowrap",
marginBottom: 0
},
children: t("payment.autoTopup.purchaseBelow")
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_reactHookForm.Controller, {
name: "recharge_currency_id",
control: methods.control,
render: ({
field
}) => /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Select, {
...field,
value: field.value,
onChange: e => field.onChange(e.target.value),
size: "small",
children: filterCurrencies.map(x => /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.MenuItem, {
value: x?.id,
children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
direction: "row",
sx: {
alignItems: "center",
gap: 1
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Avatar, {
src: x?.logo,
sx: {
width: 20,
height: 20
},
alt: x?.symbol
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
sx: {
color: "text.secondary"
},
children: [x?.symbol, " (", x?.method?.name, ")"]
})]
})
}, x?.id))
})
})]
}), config?.price?.product && /* @__PURE__ */(0, _jsxRuntime.jsx)(_productCard.default, {
product: config.price.product,
price: config.price,
creditCurrency: config.currency,
currency: filterCurrencies.find(c => c.id === rechargeCurrencyId) || filterCurrencies[0],
quantity,
onQuantityChange: newQuantity => setValue("quantity", newQuantity),
maxQuantity: 9999,
minQuantity: 1
}), config && rechargeCurrency && /* @__PURE__ */(0, _jsxRuntime.jsx)(PaymentMethodDisplay, {
config,
onChangePaymentMethod: setChangePaymentMethod,
currency: rechargeCurrency,
paymentMethod: selectedMethod
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_collapse.default, {
trigger: t("payment.autoTopup.advanced"),
children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
sx: {
gap: 2,
pl: 2,
pr: 1
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_input.default, {
name: "daily_limits.max_amount",
label: t("payment.autoTopup.dailyLimits.maxAmount"),
type: "number",
placeholder: t("payment.autoTopup.dailyLimits.maxAmountPlaceholder"),
tooltip: t("payment.autoTopup.dailyLimits.maxAmountDescription"),
inputProps: {
min: 0,
step: 0.01
},
slotProps: {
input: {
endAdornment: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.InputAdornment, {
position: "end",
children: filterCurrencies.find(c => c.id === rechargeCurrencyId)?.symbol || ""
})
}
},
sx: {
maxWidth: {
xs: "100%",
sm: "220px"
}
},
layout: "horizontal"
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_input.default, {
name: "daily_limits.max_attempts",
label: t("payment.autoTopup.dailyLimits.maxAttempts"),
type: "number",
placeholder: t("payment.autoTopup.dailyLimits.maxAttemptsPlaceholder"),
tooltip: t("payment.autoTopup.dailyLimits.maxAttemptsDescription"),
inputProps: {
min: 0,
step: 1
},
sx: {
maxWidth: {
xs: "100%",
sm: "220px"
}
},
layout: "horizontal"
})]
})
})]
})
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, {
spacing: 2,
children: showStripeForm && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
sx: {
mt: 2
},
children: state.stripeContext && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
sx: {
mt: 2
},
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_stripe.default, {
clientSecret: state.stripeContext.client_secret,
intentType: state.stripeContext.intent_type,
publicKey: state.stripeContext.public_key,
customer: state.stripeContext.customer,
mode: "setup",
onConfirm: onStripeConfirm,
onCancel: onStripeCancel
})
})
})
})]
})]
})
})
})
});
}