@blocklet/payment-react
Version:
Reusable react components for payment kit v2
826 lines (825 loc) • 26.7 kB
JavaScript
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import Dialog from "@arcblock/ux/lib/Dialog";
import { useLocaleContext } from "@arcblock/ux/lib/Locale/context";
import {
Avatar,
AvatarGroup,
Box,
Button,
CircularProgress,
IconButton,
Popover,
Stack,
Typography,
Tooltip
} from "@mui/material";
import { useRequest, useSetState } from "ahooks";
import omit from "lodash/omit";
import uniqBy from "lodash/uniqBy";
import { useEffect, useRef, useState } from "react";
import { Settings } from "@mui/icons-material";
import api from "../libs/api.js";
import {
formatAmount,
formatBNStr,
getCustomerAvatar,
getTxLink,
getUserProfileLink,
lazyLoad,
openDonationSettings
} from "../libs/util.js";
import CheckoutForm from "./form.js";
import { PaymentThemeProvider } from "../theme/index.js";
import { usePaymentContext } from "../contexts/payment.js";
import Livemode from "../components/livemode.js";
import { useMobile } from "../hooks/mobile.js";
import { useDonateContext } from "../contexts/donate.js";
const donationCache = {};
const createOrUpdateDonation = (settings, livemode = true) => {
const donationKey = `${settings.target}-${livemode}`;
if (!donationCache[donationKey]) {
donationCache[donationKey] = api.post(`/api/donations?livemode=${livemode}`, omit(settings, ["appearance"])).then((res) => res?.data).finally(() => {
setTimeout(() => {
delete donationCache[donationKey];
}, 3e3);
});
}
return donationCache[donationKey];
};
const supporterCache = {};
const fetchSupporters = (target, livemode = true) => {
if (!supporterCache[target]) {
supporterCache[target] = api.get("/api/donations", { params: { target, livemode } }).then((res) => res?.data).finally(() => {
setTimeout(() => {
delete supporterCache[target];
}, 3e3);
});
}
return supporterCache[target];
};
const emojiFont = {
fontFamily: 'Avenir, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"'
};
export function DonateDetails({ supporters = [], currency, method }) {
const { locale } = useLocaleContext();
return /* @__PURE__ */ jsx(
Stack,
{
className: "cko-donate-details",
sx: {
width: "100%",
minWidth: "256px",
maxWidth: "calc(100vw - 32px)",
maxHeight: "300px",
overflowX: "hidden",
overflowY: "auto",
margin: "0 auto"
},
children: supporters.map((x) => /* @__PURE__ */ jsxs(
Box,
{
sx: {
padding: "6px",
"&:hover": {
backgroundColor: (theme) => theme.palette.divider,
transition: "background-color 200ms linear",
cursor: "pointer"
},
borderBottom: "1px solid",
borderColor: "divider",
display: "flex",
justifyContent: "space-between",
alignItems: "center"
},
onClick: () => {
const { link, text } = getTxLink(method, x.payment_details);
if (link && text) {
window.open(link, "_blank");
}
},
children: [
/* @__PURE__ */ jsxs(
Stack,
{
direction: "row",
spacing: 0.5,
sx: {
alignItems: "center",
flex: 3,
overflow: "hidden"
},
children: [
/* @__PURE__ */ jsx(
Avatar,
{
src: getCustomerAvatar(x.customer?.did, x?.updated_at ? new Date(x.updated_at).toISOString() : "", 20),
alt: x.customer?.metadata?.anonymous ? "" : x.customer?.name,
variant: "circular",
sx: { width: 20, height: 20 },
onClick: (e) => {
e.stopPropagation();
if (x.customer?.metadata?.anonymous) {
return;
}
if (x.customer?.did) {
window.open(getUserProfileLink(x.customer?.did, locale), "_blank");
}
}
},
x.id
),
/* @__PURE__ */ jsx(
Typography,
{
sx: {
ml: "8px !important",
fontSize: "0.875rem",
fontWeight: "500",
overflow: "hidden",
whiteSpace: "nowrap",
textOverflow: "ellipsis"
},
onClick: (e) => {
if (x.customer?.metadata?.anonymous) {
return;
}
e.stopPropagation();
if (x.customer?.did) {
window.open(getUserProfileLink(x.customer?.did, locale), "_blank");
}
},
children: x.customer?.name
}
)
]
}
),
/* @__PURE__ */ jsxs(
Stack,
{
direction: "row",
spacing: 0.5,
sx: {
alignItems: "center",
justifyContent: "flex-end",
flex: 1
},
children: [
/* @__PURE__ */ jsx(
Avatar,
{
src: currency?.logo,
alt: currency?.symbol,
variant: "circular",
sx: { width: 16, height: 16 }
},
x.id
),
/* @__PURE__ */ jsx(Typography, { sx: { color: "text.secondary" }, children: formatBNStr(x.amount_total, currency.decimal) }),
/* @__PURE__ */ jsx(Typography, { sx: { color: "text.secondary" }, children: currency.symbol })
]
}
)
]
},
x.id
))
}
);
}
function SupporterAvatar({
supporters = [],
totalAmount = "0",
currency,
method,
showDonateDetails = false
}) {
const [open, setOpen] = useState(false);
const customers = uniqBy(supporters, "customer_id");
const customersNum = customers.length;
if (customersNum === 0) return null;
return /* @__PURE__ */ jsxs(
Stack,
{
sx: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center"
},
children: [
/* @__PURE__ */ jsx(
AvatarGroup,
{
max: 5,
sx: {
"& .MuiAvatar-root": {
backgroundColor: "background.paper",
"&.MuiAvatar-colorDefault": {
backgroundColor: "#bdbdbd"
}
}
},
children: customers.slice(0, 5).map((supporter) => /* @__PURE__ */ jsx(
Avatar,
{
src: getCustomerAvatar(
supporter.customer?.did,
supporter?.updated_at ? new Date(supporter.updated_at).toISOString() : "",
24
),
alt: supporter.customer?.metadata?.anonymous ? "" : supporter.customer?.name,
sx: {
width: "24px",
height: "24px"
}
},
supporter.customer?.id
))
}
),
/* @__PURE__ */ jsx(
Box,
{
sx: {
fontSize: "14px",
color: "text.secondary",
pl: 1.5,
pr: 1,
ml: -1,
borderRadius: "8px",
backgroundColor: "grey.100",
height: "24px",
...showDonateDetails ? {
cursor: "pointer",
"&:hover": {
backgroundColor: "grey.200"
}
} : {}
},
onClick: () => showDonateDetails && setOpen(true),
children: `${customersNum} supporter${customersNum > 1 ? "s" : ""} (${formatAmount(totalAmount || "0", currency?.decimal)} ${currency.symbol})`
}
),
/* @__PURE__ */ jsx(
Dialog,
{
open,
onClose: () => setOpen(false),
sx: {
".MuiDialogContent-root": {
width: {
xs: "100%",
md: "450px"
},
padding: "8px"
},
".cko-donate-details": {
maxHeight: {
xs: "100%",
md: "300px"
}
}
},
title: `${customersNum} supporter${customersNum > 1 ? "s" : ""}`,
children: /* @__PURE__ */ jsx(DonateDetails, { supporters, currency, method, totalAmount })
}
)
]
}
);
}
function SupporterTable({ supporters = [], totalAmount = "0", currency, method }) {
const customers = uniqBy(supporters, "customer_id");
const customersNum = customers.length;
if (customersNum === 0) return null;
return /* @__PURE__ */ jsxs(
Box,
{
sx: {
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: { xs: 0.5, sm: 1 }
},
children: [
/* @__PURE__ */ jsx(SupporterAvatar, { supporters, totalAmount, currency, method }),
/* @__PURE__ */ jsx(DonateDetails, { supporters, totalAmount, currency, method })
]
}
);
}
function SupporterSimple({ supporters = [], totalAmount = "0", currency, method }) {
const { t } = useLocaleContext();
const customers = uniqBy(supporters, "customer_id");
const customersNum = customers.length;
return /* @__PURE__ */ jsxs(
Box,
{
sx: {
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: {
xs: 0.5,
sm: 1
}
},
children: [
/* @__PURE__ */ jsx(
Typography,
{
component: "p",
sx: {
color: "text.secondary"
},
children: customersNum === 0 ? /* @__PURE__ */ jsx("span", { style: emojiFont, children: t("payment.checkout.donation.empty") }) : t("payment.checkout.donation.summary", {
total: customersNum,
symbol: currency.symbol,
totalAmount: formatAmount(totalAmount || "0", currency.decimal)
})
}
),
/* @__PURE__ */ jsx(
AvatarGroup,
{
total: customersNum,
max: 10,
spacing: 4,
sx: {
"& .MuiAvatar-root": {
width: 24,
height: 24,
fontSize: "0.6rem"
}
},
children: customers.map((x) => /* @__PURE__ */ jsx(
Avatar,
{
title: x.customer?.name,
alt: x.customer?.metadata?.anonymous ? "" : x.customer?.name,
src: getCustomerAvatar(x.customer?.did, x?.updated_at ? new Date(x.updated_at).toISOString() : "", 48),
variant: "circular"
},
x.id
))
}
)
]
}
);
}
const defaultDonateAmount = {
presets: ["1", "5", "10"],
preset: "1",
minimum: "0.01",
maximum: "100",
custom: true
};
function useDonation(settings, livemode, mode = "default") {
const [state, setState] = useSetState({
open: false,
supporterLoaded: false,
exist: false
});
const donateContext = useDonateContext();
const { isMobile } = useMobile();
const { settings: donateConfig = {} } = donateContext || {};
const donateSettings = {
...settings,
amount: settings.amount || donateConfig?.settings?.amount || defaultDonateAmount,
appearance: {
button: {
...settings?.appearance?.button || {},
text: settings?.appearance?.button?.text || donateConfig?.settings?.btnText || "Donate",
icon: settings?.appearance?.button?.icon || donateConfig?.settings?.icon || null
},
history: {
variant: settings?.appearance?.history?.variant || donateConfig?.settings?.historyType || "avatar"
}
}
};
const hasRequestedRef = useRef(false);
const containerRef = useRef(null);
const donation = useRequest(() => createOrUpdateDonation(donateSettings, livemode), {
manual: true,
loadingDelay: 300
});
const supporters = useRequest(
() => donation.data ? fetchSupporters(donation.data.id, livemode) : Promise.resolve({}),
{
manual: true,
loadingDelay: 300
}
);
const rootMargin = isMobile ? "50px" : `${Math.min(window.innerHeight / 2, 300)}px`;
useEffect(() => {
if (mode === "inline") return;
const element = containerRef.current;
if (!element) return;
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !hasRequestedRef.current) {
hasRequestedRef.current = true;
lazyLoad(() => {
donation.run();
supporters.run();
});
}
},
{ threshold: 0, rootMargin }
);
observer.observe(element);
return () => observer.unobserve(element);
}, [mode]);
useEffect(() => {
if (donation.data && state.supporterLoaded === false) {
setState({ supporterLoaded: true });
supporters.runAsync().catch(console.error);
}
}, [donation.data]);
return {
containerRef,
donation,
supporters,
state,
setState,
donateSettings,
supportUpdateSettings: !!donateContext.settings
};
}
function CheckoutDonateInner({
settings,
livemode = true,
timeout,
onPaid,
onError,
mode,
inlineOptions = {},
theme,
children
}) {
const { containerRef, state, setState, donation, supporters, donateSettings, supportUpdateSettings } = useDonation(
settings,
livemode,
mode
);
const customers = uniqBy(supporters?.data?.supporters || [], "customer_did");
const { t } = useLocaleContext();
const [anchorEl, setAnchorEl] = useState(null);
const [popoverOpen, setPopoverOpen] = useState(false);
const { isMobile } = useMobile();
const { connect, session } = usePaymentContext();
const handlePaid = (...args) => {
if (onPaid) {
onPaid(...args);
}
supporters.runAsync().catch(console.error);
setTimeout(() => {
setState({ open: false });
}, timeout);
};
if (donation.error) {
return null;
}
const handlePopoverOpen = (event) => {
donation.run();
supporters.run();
setAnchorEl(event.currentTarget);
setPopoverOpen(true);
};
const handlePopoverClose = () => {
setPopoverOpen(false);
};
const startDonate = () => {
setState({ open: true });
};
const inlineText = inlineOptions?.button?.text || donateSettings.appearance.button.text;
const inlineRender = /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsx(
Button,
{
size: donateSettings.appearance?.button?.size || "medium",
color: donateSettings.appearance?.button?.color || "primary",
variant: donateSettings.appearance?.button?.variant || "contained",
...donateSettings.appearance?.button,
onClick: handlePopoverOpen,
children: /* @__PURE__ */ jsxs(
Stack,
{
direction: "row",
spacing: 0.5,
sx: {
alignItems: "center"
},
children: [
donateSettings.appearance.button.icon,
typeof donateSettings.appearance.button.text === "string" ? /* @__PURE__ */ jsx(Typography, { sx: { whiteSpace: "nowrap" }, children: donateSettings.appearance.button.text }) : donateSettings.appearance.button.text
]
}
)
}
),
/* @__PURE__ */ jsx(
Popover,
{
id: "mouse-over-popper",
open: popoverOpen,
anchorEl,
onClose: handlePopoverClose,
anchorOrigin: {
vertical: "top",
horizontal: "center"
},
transformOrigin: {
vertical: "bottom",
horizontal: "center"
},
children: /* @__PURE__ */ jsxs(
Box,
{
sx: {
minWidth: 320,
padding: "20px"
},
children: [
supporters.loading && /* @__PURE__ */ jsx(
"div",
{
style: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "background.paper",
opacity: 0.8
},
children: /* @__PURE__ */ jsx(CircularProgress, {})
}
),
/* @__PURE__ */ jsxs(
Box,
{
sx: {
display: "flex",
alignItems: "center",
flexDirection: "column",
gap: 2
},
children: [
/* @__PURE__ */ jsx(Button, { ...inlineOptions.button, onClick: () => startDonate(), children: /* @__PURE__ */ jsxs(
Stack,
{
direction: "row",
spacing: 0.5,
sx: {
alignItems: "center"
},
children: [
inlineOptions?.button?.icon,
typeof inlineText === "string" ? /* @__PURE__ */ jsx(Typography, { sx: { whiteSpace: "nowrap" }, children: inlineText }) : inlineText
]
}
) }),
/* @__PURE__ */ jsx(SupporterSimple, { ...supporters.data })
]
}
)
]
}
)
}
)
] });
const defaultRender = /* @__PURE__ */ jsxs(
Box,
{
sx: {
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: { xs: 1, sm: 2 },
width: "100%",
minWidth: 300,
maxWidth: 720
},
children: [
/* @__PURE__ */ jsx(
Button,
{
size: donateSettings.appearance?.button?.size || "medium",
color: donateSettings.appearance?.button?.color || "primary",
variant: donateSettings.appearance?.button?.variant || "outlined",
sx: {
...!donateSettings.appearance?.button?.variant ? {
color: "primary.main",
borderColor: "divider"
} : {},
// @ts-ignore
...donateSettings.appearance?.button?.sx || {}
},
...donateSettings.appearance?.button,
onClick: () => startDonate(),
children: /* @__PURE__ */ jsxs(
Stack,
{
direction: "row",
spacing: 0.5,
sx: {
alignItems: "center"
},
children: [
donateSettings.appearance.button.icon && /* @__PURE__ */ jsx(Typography, { sx: { mr: 0.5, display: "inline-flex", alignItems: "center" }, children: donateSettings.appearance.button.icon }),
typeof donateSettings.appearance.button.text === "string" ? /* @__PURE__ */ jsx(Typography, { children: donateSettings.appearance.button.text }) : donateSettings.appearance.button.text
]
}
)
}
),
supporters.data && donateSettings.appearance.history.variant === "avatar" && /* @__PURE__ */ jsx(SupporterAvatar, { ...supporters.data, showDonateDetails: true }),
supporters.data && donateSettings.appearance.history.variant === "table" && /* @__PURE__ */ jsx(SupporterTable, { ...supporters.data })
]
}
);
const renderInnerView = () => {
if (mode === "inline") {
return inlineRender;
}
if (mode === "custom") {
return children && typeof children === "function" ? /* @__PURE__ */ jsx(Fragment, { children: children(
startDonate,
`${formatAmount(
supporters.data?.totalAmount || "0",
supporters.data?.currency?.decimal
)} ${supporters.data?.currency?.symbol || ""}`,
supporters.data || {},
!!supporters.loading,
donateSettings
) }) : /* @__PURE__ */ jsxs(Typography, { children: [
"Please provide a valid render function",
" ",
/* @__PURE__ */ jsx("pre", { children: "(openDonate, donateTotalAmount, supporters, loading, donateSettings) => ReactNode" })
] });
}
return defaultRender;
};
const isAdmin = ["owner", "admin"].includes(session?.user?.role);
return /* @__PURE__ */ jsxs("div", { ref: containerRef, children: [
renderInnerView(),
donation.data && /* @__PURE__ */ jsx(
Dialog,
{
open: state.open,
title: /* @__PURE__ */ jsxs(
Box,
{
sx: {
display: "flex",
alignItems: "center",
gap: 0.5
},
children: [
/* @__PURE__ */ jsx(Typography, { variant: "h3", sx: { maxWidth: 320, textOverflow: "ellipsis", overflow: "hidden" }, children: donateSettings.title }),
supportUpdateSettings && isAdmin && /* @__PURE__ */ jsx(Tooltip, { title: t("payment.checkout.donation.configTip"), placement: "bottom", children: /* @__PURE__ */ jsx(
IconButton,
{
size: "small",
onClick: (e) => {
e.stopPropagation();
openDonationSettings(true);
},
children: /* @__PURE__ */ jsx(Settings, { fontSize: "small", sx: { ml: -0.5 } })
}
) }),
!donation.data.livemode && /* @__PURE__ */ jsx(Livemode, { sx: { width: "fit-content", ml: 0.5 } })
]
}
),
maxWidth: "md",
toolbar: isMobile ? null : /* @__PURE__ */ jsxs(
Box,
{
sx: {
display: "flex",
alignItems: "center",
gap: 1,
color: "text.secondary"
},
children: [
/* @__PURE__ */ jsx(
AvatarGroup,
{
total: customers?.length,
max: 5,
spacing: 4,
sx: {
"& .MuiAvatar-root": {
width: 18,
height: 18,
fontSize: "0.6rem"
}
},
children: customers.map((x) => /* @__PURE__ */ jsx(
Avatar,
{
title: x.customer?.name,
src: getCustomerAvatar(
x.customer?.did,
x?.updated_at ? new Date(x.updated_at).toISOString() : "",
24
),
variant: "circular"
},
x.id
))
}
),
customers?.length > 0 && /* @__PURE__ */ jsx(Typography, { variant: "body2", children: t("payment.checkout.donation.gaveTips", { count: customers?.length }) })
]
}
),
showCloseButton: false,
disableEscapeKeyDown: true,
sx: {
".MuiDialogContent-root": {
padding: "16px 24px",
borderTop: "1px solid",
borderColor: "divider",
width: "100%"
},
".ux-dialog_header": {
gap: 5
}
},
PaperProps: {
style: { minHeight: "auto", width: 680 }
},
onClose: (e, reason) => setState({ open: reason === "backdropClick" }),
children: /* @__PURE__ */ jsx(Box, { sx: { height: "100%", width: "100%" }, children: /* @__PURE__ */ jsx(
CheckoutForm,
{
id: donation.data?.id,
onPaid: handlePaid,
onError,
action: donateSettings.appearance?.button?.text,
mode: "inline",
theme,
formType: "donation",
extraParams: {
livemode
},
formRender: {
cancel: /* @__PURE__ */ jsx(
Button,
{
variant: "outlined",
size: "large",
onClick: () => {
connect.close();
setState({ open: false });
},
children: t("common.cancel")
}
),
onCancel: () => {
connect.close();
setState({ open: false });
}
}
}
) })
}
)
] });
}
export default function CheckoutDonate(rawProps) {
const props = Object.assign(
{
theme: "default",
livemode: void 0,
inlineOptions: {
button: {
text: "Tip"
}
},
timeout: 5e3,
mode: "default"
},
rawProps
);
const { livemode } = usePaymentContext();
const content = (
// eslint-disable-next-line react/prop-types
/* @__PURE__ */ jsx(CheckoutDonateInner, { ...props, livemode: props.livemode === void 0 ? livemode : props.livemode })
);
if (props.theme === "inherit") {
return content;
}
if (props.theme && typeof props.theme === "object") {
return /* @__PURE__ */ jsx(PaymentThemeProvider, { theme: props.theme, children: content });
}
return /* @__PURE__ */ jsx(PaymentThemeProvider, { children: content });
}