/* eslint-disable react/no-unused-prop-types */ /* eslint-disable react/require-default-props */ /* eslint-disable @typescript-eslint/indent */ import Dialog from '@arcblock/ux/lib/Dialog'; import { useLocaleContext } from '@arcblock/ux/lib/Locale/context'; import type { DonationSettings, PaymentBeneficiary, PaymentDetails, TCheckoutSessionExpanded, TPaymentCurrency, TPaymentLink, TPaymentMethod, TSetting, } from '@blocklet/payment-types'; import { Avatar, AvatarGroup, Box, Button, CircularProgress, IconButton, Popover, Stack, Typography, Tooltip, type ButtonProps as MUIButtonProps, } 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'; import { formatAmount, formatBNStr, getCustomerAvatar, getTxLink, getUserProfileLink, lazyLoad, openDonationSettings, } from '../libs/util'; import type { CheckoutProps, PaymentThemeOptions } from '../types'; import CheckoutForm from './form'; import { PaymentThemeProvider } from '../theme'; import { usePaymentContext } from '../contexts/payment'; import Livemode from '../components/livemode'; import { useMobile } from '../hooks/mobile'; import { useDonateContext } from '../contexts/donate'; export type DonateHistory = { supporters: TCheckoutSessionExpanded[]; currency: TPaymentCurrency; method: TPaymentMethod; total?: number; totalAmount: string; }; export type CheckoutDonateSettings = { target: string; title: string; description: string; reference: string; beneficiaries: PaymentBeneficiary[]; amount?: { presets?: string[]; preset?: string; minimum?: string; maximum?: string; custom?: boolean; }; appearance?: { button?: { text?: any; icon?: any; size?: string; color?: string; variant?: string; }; history?: { variant?: string; }; }; }; export interface ButtonType extends Omit { text?: string | React.ReactNode; icon: React.ReactNode; } export type DonateProps = Pick & { settings: CheckoutDonateSettings; livemode?: boolean; timeout?: number; mode?: 'inline' | 'default' | 'custom'; inlineOptions?: { button?: ButtonType; }; theme?: 'default' | 'inherit' | PaymentThemeOptions; children?: ( openDialog: () => void, donateTotalAmount: string, supporters: DonateHistory, loading?: boolean, donateSettings?: DonationSettings ) => React.ReactNode; }; const donationCache: { [key: string]: Promise } = {}; const createOrUpdateDonation = (settings: DonationSettings, livemode: boolean = true): Promise => { const donationKey = `${settings.target}-${livemode}`; if (!donationCache[donationKey]) { donationCache[donationKey] = api .post(`/api/donations?livemode=${livemode}`, omit(settings, ['appearance'])) .then((res: any) => res?.data) .finally(() => { setTimeout(() => { delete donationCache[donationKey]; }, 3000); }); } return donationCache[donationKey]; }; const supporterCache: { [key: string]: Promise } = {}; const fetchSupporters = (target: string, livemode: boolean = true): Promise => { if (!supporterCache[target]) { supporterCache[target] = api .get('/api/donations', { params: { target, livemode } }) .then((res: any) => res?.data) .finally(() => { setTimeout(() => { delete supporterCache[target]; }, 3000); }); } 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 }: DonateHistory) { const { locale } = useLocaleContext(); return ( {supporters.map((x) => ( 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 as PaymentDetails); if (link && text) { window.open(link, '_blank'); } }}> { e.stopPropagation(); if (x.customer?.metadata?.anonymous) { return; } if (x.customer?.did) { window.open(getUserProfileLink(x.customer?.did, locale), '_blank'); } }} /> { if (x.customer?.metadata?.anonymous) { return; } e.stopPropagation(); if (x.customer?.did) { window.open(getUserProfileLink(x.customer?.did, locale), '_blank'); } }}> {x.customer?.name} {formatBNStr(x.amount_total, currency.decimal)} {currency.symbol} ))} ); } // eslint-disable-next-line @typescript-eslint/no-unused-vars function SupporterAvatar({ supporters = [], totalAmount = '0', currency, method, showDonateDetails = false, }: DonateHistory & { showDonateDetails?: boolean }) { const [open, setOpen] = useState(false); const customers = uniqBy(supporters, 'customer_id'); const customersNum = customers.length; if (customersNum === 0) return null; return ( {customers.slice(0, 5).map((supporter) => ( ))} showDonateDetails && setOpen(true)}> {`${customersNum} supporter${customersNum > 1 ? 's' : ''} (${formatAmount(totalAmount || '0', currency?.decimal)} ${currency.symbol})`} 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' : ''}`}> ); } // eslint-disable-next-line @typescript-eslint/no-unused-vars function SupporterTable({ supporters = [], totalAmount = '0', currency, method }: DonateHistory) { const customers = uniqBy(supporters, 'customer_id'); const customersNum = customers.length; if (customersNum === 0) return null; return ( ); } // eslint-disable-next-line @typescript-eslint/no-unused-vars function SupporterSimple({ supporters = [], totalAmount = '0', currency, method }: DonateHistory) { const { t } = useLocaleContext(); const customers = uniqBy(supporters, 'customer_id'); const customersNum = customers.length; return ( {customersNum === 0 ? ( {t('payment.checkout.donation.empty')} ) : ( t('payment.checkout.donation.summary', { total: customersNum, symbol: currency.symbol, totalAmount: formatAmount(totalAmount || '0', currency.decimal), }) )} {customers.map((x) => ( ))} ); } const defaultDonateAmount = { presets: ['1', '5', '10'], preset: '1', minimum: '0.01', maximum: '100', custom: true, }; function useDonation(settings: CheckoutDonateSettings, livemode: boolean, mode = 'default') { const [state, setState] = useSetState({ open: false, supporterLoaded: false, exist: false, }); const donateContext = useDonateContext(); const { isMobile } = useMobile(); const { settings: donateConfig = {} as TSetting } = 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); // eslint-disable-next-line consistent-return return () => observer.unobserve(element); // eslint-disable-next-line react-hooks/exhaustive-deps }, [mode]); useEffect(() => { if (donation.data && state.supporterLoaded === false) { setState({ supporterLoaded: true }); supporters.runAsync().catch(console.error); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [donation.data]); return { containerRef, donation, supporters, state, setState, donateSettings, supportUpdateSettings: !!donateContext.settings, }; } function CheckoutDonateInner({ settings, livemode = true, timeout, onPaid, onError, mode, inlineOptions = {}, theme, children, }: DonateProps) { // eslint-disable-line const { containerRef, state, setState, donation, supporters, donateSettings, supportUpdateSettings } = useDonation( settings, livemode, mode ); const customers = uniqBy((supporters?.data as DonateHistory)?.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: any[]) => { if (onPaid) { // @ts-ignore onPaid(...args); } supporters.runAsync().catch(console.error); setTimeout(() => { setState({ open: false }); }, timeout); }; if (donation.error) { return null; } const handlePopoverOpen = (event: any) => { 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 = ( <> {supporters.loading && (
)}
); const defaultRender = ( {supporters.data && donateSettings.appearance.history.variant === 'avatar' && ( )} {supporters.data && donateSettings.appearance.history.variant === 'table' && ( )} ); const renderInnerView = () => { if (mode === 'inline') { return inlineRender; } if (mode === 'custom') { return children && typeof children === 'function' ? ( <> {children( startDonate, `${formatAmount( (supporters.data as DonateHistory)?.totalAmount || '0', (supporters.data as DonateHistory)?.currency?.decimal )} ${(supporters.data as DonateHistory)?.currency?.symbol || ''}`, (supporters.data as DonateHistory) || {}, !!supporters.loading, donateSettings )} ) : ( Please provide a valid render function{' '}
{'(openDonate, donateTotalAmount, supporters, loading, donateSettings) => ReactNode'}
); } return defaultRender; }; const isAdmin = ['owner', 'admin'].includes(session?.user?.role); return (
{renderInnerView()} {donation.data && ( {donateSettings.title} {supportUpdateSettings && isAdmin && ( { e.stopPropagation(); openDonationSettings(true); }}> )} {!donation.data.livemode && } } maxWidth="md" toolbar={ isMobile ? null : ( {customers.map((x: any) => ( ))} {customers?.length > 0 && ( {t('payment.checkout.donation.gaveTips', { count: customers?.length })} )} ) } showCloseButton={false} disableEscapeKeyDown 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: any, reason: string) => setState({ open: reason === 'backdropClick' })}> { connect.close(); setState({ open: false }); }}> {t('common.cancel')} ), onCancel: () => { connect.close(); setState({ open: false }); }, }} /> )}
); } export default function CheckoutDonate(rawProps: DonateProps) { const props = Object.assign( { theme: 'default', livemode: undefined, inlineOptions: { button: { text: 'Tip', }, }, timeout: 5000, mode: 'default', }, rawProps ); const { livemode } = usePaymentContext(); const content = ( // eslint-disable-next-line react/prop-types ); // eslint-disable-next-line react/prop-types if (props.theme === 'inherit') { return content; } // eslint-disable-next-line react/prop-types if (props.theme && typeof props.theme === 'object') { // eslint-disable-next-line react/prop-types return {content}; } return {content}; }