@blocklet/payment-react
Version:
Reusable react components for payment kit v2
503 lines (502 loc) • 19.1 kB
JavaScript
import { jsx, jsxs } from "react/jsx-runtime";
import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
import Toast from "@arcblock/ux/lib/Toast";
import { CheckOutlined } from "@mui/icons-material";
import {
Avatar,
Box,
Chip,
List,
ListItem,
ListItemIcon,
ListItemText,
MenuItem,
Select,
Stack,
ToggleButton,
ToggleButtonGroup,
Typography
} from "@mui/material";
import { styled } from "@mui/system";
import { useSetState } from "ahooks";
import { useEffect, useMemo, useState } from "react";
import { BN } from "@ocap/util";
import isEmpty from "lodash/isEmpty";
import { usePaymentContext } from "../contexts/payment.js";
import {
formatError,
formatPriceAmount,
formatRecurring,
getPriceCurrencyOptions,
getPriceUintAmountByCurrency,
isMobileSafari
} from "../libs/util.js";
import { useMobile } from "../hooks/mobile.js";
import TruncatedText from "./truncated-text.js";
import LoadingButton from "./loading-button.js";
const sortOrder = {
year: 1,
month: 2,
day: 3,
hour: 4
};
const groupItemsByRecurring = (items, currency) => {
const grouped = {};
const recurring = {};
(items || []).forEach((x) => {
const key = [x.price.recurring?.interval, x.price.recurring?.interval_count].join("-");
if (x.price.currency_options?.find((c) => c.currency_id === currency.id)) {
recurring[key] = x.price.recurring;
}
if (!grouped[key]) {
grouped[key] = [];
}
grouped[key].push(x);
});
return { recurring, grouped };
};
export default function PricingTable({
table,
alignItems = "center",
interval = "",
mode = "checkout",
onSelect,
hideCurrency = false
}) {
const { t, locale } = useLocaleContext();
const { isMobile } = useMobile();
const {
settings: { paymentMethods = [] },
livemode,
setLivemode,
refresh
} = usePaymentContext();
const isMobileSafariEnv = isMobileSafari();
useEffect(() => {
if (table) {
if (livemode !== table.livemode) {
setLivemode(table.livemode);
}
}
}, [table, livemode, setLivemode, refresh]);
const [currency, setCurrency] = useState(table.currency || {});
const { recurring, grouped } = useMemo(() => groupItemsByRecurring(table.items, currency), [table.items, currency]);
const recurringKeysList = useMemo(() => {
if (isEmpty(recurring)) {
return [];
}
return Object.keys(recurring).sort((a, b) => {
const [aType, aValue] = a.split("-");
const [bType, bValue] = b.split("-");
if (sortOrder[aType] !== sortOrder[bType]) {
return sortOrder[aType] - sortOrder[bType];
}
if (aValue && bValue) {
return bValue - aValue;
}
return b - a;
});
}, [recurring]);
const [state, setState] = useSetState({ interval });
const currencyMap = useMemo(() => {
if (!paymentMethods || paymentMethods.length === 0) {
return {};
}
const ans = {};
paymentMethods.forEach((paymentMethod) => {
const { payment_currencies: paymentCurrencies = [] } = paymentMethod;
if (paymentCurrencies && paymentCurrencies.length > 0) {
paymentCurrencies.forEach((x) => {
ans[x.id] = {
...x,
method: paymentMethod.name
};
});
}
});
return ans;
}, [paymentMethods]);
const currencyList = useMemo(() => {
const visited = {};
if (!state.interval) {
return [];
}
(grouped[state.interval] || []).forEach((x) => {
getPriceCurrencyOptions(x.price).forEach((c) => {
visited[c?.currency_id] = true;
});
});
return Object.keys(visited).map((x) => currencyMap[x]).filter((v) => v);
}, [currencyMap, grouped, state.interval]);
const productList = useMemo(() => {
return (grouped[state.interval] || []).filter((x) => {
const price = getPriceUintAmountByCurrency(x.price, currency);
if (new BN(price).isZero() || !price) {
return false;
}
return true;
});
}, [grouped, state.interval, currency]);
useEffect(() => {
if (table) {
if (!state.interval || !grouped[state.interval]) {
const keys = Object.keys(recurring);
if (keys[0]) {
setState({ interval: keys[0] });
}
}
}
}, [table]);
const Root = styled(Box)`
.btn-row {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
width: 100%;
gap: 20px;
}
.price-table-wrap {
scrollbar-width: none;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none;
}
}
@media (max-width: ${({ theme }) => theme.breakpoints.values.sm}px) {
// .price-table-item {
// width: 90% !important;
// }
// .btn-row {
// padding: 0 20px;
// }
}
@media (min-width: ${({ theme }) => theme.breakpoints.values.md}px) {
.price-table-wrap:has(> div:nth-of-type(1)) {
max-width: 360px !important;
}
.price-table-wrap:has(> div:nth-of-type(2)) {
max-width: 780px !important;
}
.price-table-wrap:has(> div:nth-of-type(3)) {
max-width: 1200px !important;
}
}
`;
return /* @__PURE__ */ jsx(
Root,
{
sx: {
flex: 1,
overflow: {
xs: isMobileSafariEnv ? "visible" : "hidden",
md: "hidden"
},
display: "flex",
flexDirection: "column"
},
children: /* @__PURE__ */ jsxs(
Stack,
{
direction: "column",
alignItems: alignItems === "center" ? "center" : "flex-start",
sx: {
gap: {
xs: 3,
sm: mode === "select" ? 3 : 5
},
height: "100%",
overflow: {
xs: "auto",
md: "hidden"
}
},
children: [
/* @__PURE__ */ jsxs(Stack, { className: "btn-row", flexDirection: "row", children: [
recurringKeysList.length > 0 && /* @__PURE__ */ jsx(Box, { children: isMobile && recurringKeysList.length > 1 ? /* @__PURE__ */ jsx(
Select,
{
value: state.interval,
onChange: (e) => setState({ interval: e.target.value }),
size: "small",
sx: { m: 1 },
children: recurringKeysList.map((x) => /* @__PURE__ */ jsx(MenuItem, { value: x, children: /* @__PURE__ */ jsx(Typography, { color: x === state.interval ? "text.primary" : "text.secondary", children: formatRecurring(recurring[x], true, "", locale) }) }, x))
}
) : /* @__PURE__ */ jsx(
ToggleButtonGroup,
{
size: "small",
value: state.interval,
sx: {
padding: "4px",
borderRadius: "36px",
height: "40px",
boxSizing: "border-box",
backgroundColor: "grey.100",
border: 0
},
onChange: (_, value) => {
if (value !== null) {
setState({ interval: value });
}
},
exclusive: true,
children: recurringKeysList.map((x) => /* @__PURE__ */ jsx(
ToggleButton,
{
size: "small",
value: x,
sx: {
textTransform: "capitalize",
padding: "5px 12px",
fontSize: "13px",
backgroundColor: ({ palette }) => x === state.interval ? `${palette.background.default} !important` : `${palette.grey[100]} !important`,
border: "0px",
"&.Mui-selected": {
borderRadius: "9999px !important",
border: "1px solid",
borderColor: "divider"
}
},
children: formatRecurring(recurring[x], true, "", locale)
},
x
))
}
) }),
currencyList.length > 0 && !hideCurrency && /* @__PURE__ */ jsx(
Select,
{
value: currency?.id,
onChange: (e) => setCurrency(currencyList.find((v) => v?.id === e.target.value)),
size: "small",
sx: { m: 1, minWidth: 180 },
children: currencyList.map((x) => /* @__PURE__ */ jsx(MenuItem, { value: x?.id, children: /* @__PURE__ */ jsxs(Stack, { direction: "row", alignItems: "center", gap: 1, children: [
/* @__PURE__ */ jsx(Avatar, { src: x?.logo, sx: { width: 20, height: 20 }, alt: x?.symbol }),
/* @__PURE__ */ jsxs(Typography, { fontSize: "12px", color: "text.secondary", children: [
x?.symbol,
"\uFF08",
x?.method,
"\uFF09"
] })
] }) }, x?.id))
}
)
] }),
/* @__PURE__ */ jsx(
Stack,
{
flexWrap: "wrap",
direction: "row",
gap: "20px",
justifyContent: alignItems === "center" ? "center" : "flex-start",
sx: {
flex: "0 1 auto",
pb: 2.5
},
className: "price-table-wrap",
children: productList?.map((x) => {
let action = x.subscription_data?.trial_period_days ? t("payment.checkout.try") : t("payment.checkout.subscription");
if (mode === "select") {
action = x.is_selected ? t("payment.checkout.selected") : t("payment.checkout.select");
}
const [amount, unit] = formatPriceAmount(x.price, currency, x.product.unit_label).split("/");
return /* @__PURE__ */ jsxs(
Stack,
{
padding: 4,
spacing: 2,
direction: "column",
alignItems: "flex-start",
className: "price-table-item",
justifyContent: "flex-start",
sx: {
cursor: "pointer",
borderWidth: "1px",
borderStyle: "solid",
borderColor: mode === "select" && x.is_selected ? "primary.main" : "divider",
borderRadius: 2,
transition: "border-color 0.3s ease 0s, box-shadow 0.3s ease 0s",
boxShadow: 2,
"&:hover": {
borderColor: mode === "select" && x.is_selected ? "primary.main" : "divider",
boxShadow: 2
},
width: {
xs: "100%",
md: "360px"
},
maxWidth: "360px",
minWidth: "300px",
padding: "20px",
position: "relative"
},
children: [
/* @__PURE__ */ jsx(Box, { textAlign: "center", children: /* @__PURE__ */ jsxs(
Stack,
{
direction: "column",
justifyContent: "center",
alignItems: "flex-start",
spacing: 1,
sx: { gap: "12px" },
children: [
/* @__PURE__ */ jsxs(Box, { sx: { display: "flex", alignItems: "center", justifyContent: "space-between" }, children: [
/* @__PURE__ */ jsx(
Typography,
{
color: "text.secondary",
fontWeight: 600,
sx: {
fontSize: "18px !important",
fontWeight: "600"
},
children: /* @__PURE__ */ jsx(TruncatedText, { text: x.product.name, maxLength: 26, useWidth: true })
}
),
x.is_highlight && /* @__PURE__ */ jsx(
Chip,
{
label: x.highlight_text,
color: "primary",
size: "small",
sx: {
position: "absolute",
top: "20px",
right: "20px"
}
}
)
] }),
/* @__PURE__ */ jsxs(
Typography,
{
component: "div",
sx: {
my: 0,
fontWeight: "700",
fontSize: "32px",
letterSpacing: "-0.03rem",
fontVariantNumeric: "tabular-nums",
display: "flex",
alignItems: "baseline",
gap: "4px",
flexWrap: "wrap",
lineHeight: "normal"
},
children: [
amount,
unit ? /* @__PURE__ */ jsxs(
Typography,
{
sx: { fontSize: "16px", fontWeight: "400", color: "text.secondary", textAlign: "left" },
children: [
"/ ",
unit
]
}
) : ""
]
}
),
/* @__PURE__ */ jsx(
Typography,
{
color: "text.secondary",
sx: {
marginTop: "0px !important",
fontWeight: "400",
fontSize: "16px",
textAlign: "left"
},
children: x.product.description
}
)
]
}
) }),
x.product.features.length > 0 && /* @__PURE__ */ jsx(Box, { sx: { width: "100%" }, children: /* @__PURE__ */ jsxs(List, { dense: true, sx: { display: "flex", flexDirection: "column", gap: "16px", padding: "0px" }, children: [
/* @__PURE__ */ jsx(
Box,
{
sx: {
width: "100%",
position: "relative",
borderTop: "1px solid",
borderColor: "divider",
boxSizing: "border-box",
height: "1px"
}
}
),
x.product.features.map((f) => /* @__PURE__ */ jsxs(ListItem, { disableGutters: true, disablePadding: true, sx: { fontSize: "16px !important" }, children: [
/* @__PURE__ */ jsx(ListItemIcon, { sx: { minWidth: 25, color: "text.secondary", fontSize: "64px" }, children: /* @__PURE__ */ jsx(
CheckOutlined,
{
color: "success",
fontSize: "small",
sx: {
fontSize: "18px",
color: "success.main"
}
}
) }),
/* @__PURE__ */ jsx(
ListItemText,
{
sx: {
".MuiListItemText-primary": {
fontSize: "16px",
color: "text.primary",
fontWeight: "500"
}
},
primary: f.name
}
)
] }, f.name))
] }) }),
/* @__PURE__ */ jsx(Subscribe, { x, action, onSelect, currencyId: currency?.id })
]
},
x?.price_id
);
})
}
)
]
}
)
}
);
}
function Subscribe({ x, action, onSelect, currencyId }) {
const [state, setState] = useState({ loading: "", loaded: false });
const handleSelect = async (priceId) => {
try {
setState({ loading: priceId, loaded: true });
await onSelect(priceId, currencyId);
} catch (err) {
console.error(err);
Toast.error(formatError(err));
}
};
return /* @__PURE__ */ jsx(
LoadingButton,
{
fullWidth: true,
size: "medium",
variant: "contained",
color: "primary",
sx: {
fontSize: "16px",
padding: "10px 20px",
lineHeight: "28px"
},
loading: state.loading === x.price_id && !state.loaded,
disabled: x.is_disabled,
onClick: () => handleSelect(x.price_id),
children: action
}
);
}