UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

826 lines (825 loc) 26.7 kB
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 }); }