UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

503 lines (502 loc) 19.1 kB
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 } ); }