import { useEffect, useState } from 'react'; import { useRequest } from 'ahooks'; import { joinURL } from 'ufo'; import { Box } from '@mui/material'; import { CheckoutProvider, useSessionContext, useSubmitFeature } from '@blocklet/payment-react-headless'; import api from '../libs/api'; import { getPrefix, mergeExtraParams } from '../libs/util'; import { PaymentThemeProvider } from '../theme'; import { useMobile } from '../hooks/mobile'; import type { CheckoutV2Props } from './types'; import CheckoutLayout from './layouts/checkout-layout'; import ScenarioRouter from './panels/left/scenario-router'; import PaymentPanel from './panels/right/payment-panel'; import CheckoutDialogs from './components/dialogs/checkout-dialogs'; import LoadingView from './views/loading-view'; import ErrorView from './views/error-view'; import SuccessView from './views/success-view'; // Deduplicated plink_ → cs_ resolution const plinkPromises: Record> = {}; function startFromPaymentLink( id: string, params?: Record ): Promise<{ checkoutSession: { id: string } }> { if (!plinkPromises[id]) { plinkPromises[id] = api .post(`/api/checkout-sessions/start/${id}?${mergeExtraParams(params)}`) .then((res: any) => res?.data) .finally(() => { setTimeout(() => { delete plinkPromises[id]; }, 3000); }); } return plinkPromises[id]; } // Inner router: must be rendered inside CheckoutProvider function CheckoutRouter({ onPaid = undefined, onError = undefined, mode = 'inline', }: { onPaid?: (result: any) => void; onError?: (err: Error) => void; mode?: string; }) { const { isLoading, error, errorCode, session } = useSessionContext(); const submit = useSubmitFeature(); const { isMobile } = useMobile(); useEffect(() => { if (error && onError) { onError(new Error(error)); } }, [error, onError]); useEffect(() => { if (submit.status === 'completed' && submit.result && onPaid) { onPaid(submit.result); } }, [submit.status, submit.result, onPaid]); if (isLoading) { return ; } if (error) { return ; } const isCompleted = submit.status === 'completed'; const mobileCompleted = isCompleted && isMobile; return ( <> ) } right={isCompleted ? : } mode={mode} /> {!isCompleted && } ); } export default function CheckoutV2({ id, onPaid, onError, theme = 'default', mode = 'inline', extraParams = {}, }: CheckoutV2Props) { if (!id.startsWith('plink_') && !id.startsWith('cs_')) { throw new Error('Either a checkoutSession or a paymentLink id is required.'); } const isPaymentLink = id.startsWith('plink_'); const [resolvedSessionId, setResolvedSessionId] = useState(isPaymentLink ? null : id); // Resolve plink_ to cs_ useRequest( async () => { if (!isPaymentLink) return null; const data = await startFromPaymentLink(id, extraParams); const csId = data?.checkoutSession?.id; if (csId) { setResolvedSessionId(csId); if (mode === 'standalone') { window.history.replaceState( null, '', joinURL(getPrefix(), `/checkout/pay/${csId}?${mergeExtraParams(extraParams)}`) ); } } return data; }, { ready: isPaymentLink } ); if (!resolvedSessionId) { return ; } const content = ( ); if (theme === 'inherit') { return content; } if (theme && typeof theme === 'object') { return {content}; } return {content}; }