UNPKG

@daimo/pay

Version:

Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.

275 lines (272 loc) 11.8 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import { debugJson, DaimoPayOrderMode, DaimoPayOrderStatusSource } from '@daimo/pay-common'; import { Buffer } from 'buffer'; import React, { useMemo, useState, useRef, useCallback, useEffect, createElement } from 'react'; import { ThemeProvider } from 'styled-components'; import { WagmiContext } from 'wagmi'; import { DaimoPayModal } from '../components/DaimoPayModal/index.js'; import { ROUTES } from '../constants/routes.js'; import { REQUIRED_CHAINS } from '../defaultConfig.js'; import { useChains } from '../hooks/useChains.js'; import { useConnectCallback } from '../hooks/useConnectCallback.js'; import { useDaimoPay } from '../hooks/useDaimoPay.js'; import { usePaymentState } from '../hooks/usePaymentState.js'; import { PaymentProvider, PaymentContext } from './PaymentProvider.js'; import defaultTheme from '../styles/defaultTheme.js'; import { createTrpcClient } from '../utils/trpc.js'; import { setInWalletPaymentUrlFromApiUrl } from '../wallets/walletConfigs.js'; import { PayContext } from './PayContext.js'; import { SolanaContextProvider } from './SolanaContextProvider.js'; import { Web3ContextProvider } from './Web3ContextProvider.js'; const DaimoPayUIProvider = ({ children, theme = "auto", mode = "auto", customTheme, options, onConnect, onDisconnect, debugMode = false, payApiUrl, log, }) => { if (!React.useContext(PaymentContext)) { throw Error("DaimoPayProvider must be within a PaymentProvider"); } if (!React.useContext(WagmiContext)) { throw Error("DaimoPayProvider must be within a WagmiProvider"); } // Only allow for mounting DaimoPayProvider once, so we avoid weird global // state collisions. if (React.useContext(PayContext)) { throw new Error("Multiple, nested usages of DaimoPayProvider detected. Please use only one."); } useConnectCallback({ onConnect, onDisconnect, }); const chains = useChains(); for (const requiredChain of REQUIRED_CHAINS) { if (!chains.some((c) => c.id === requiredChain.id)) { throw new Error(`Daimo Pay requires chains ${REQUIRED_CHAINS.map((c) => c.name).join(", ")}. Use \`getDefaultConfig\` to automatically configure required chains.`); } } // Default config options const defaultOptions = { language: "en-US", hideBalance: false, hideTooltips: false, hideQuestionMarkCTA: false, hideNoWalletCTA: false, hideRecentBadge: false, avoidLayoutShift: true, embedGoogleFonts: false, truncateLongENSAddress: true, reducedMotion: false, disclaimer: null, bufferPolyfill: true, customAvatar: undefined, initialChainId: undefined, enforceSupportedChains: false, ethereumOnboardingUrl: undefined, walletOnboardingUrl: undefined, overlayBlur: undefined, disableMobileInjector: false, }; const opts = Object.assign({}, defaultOptions, options); if (typeof window !== "undefined") { // Buffer Polyfill, needed for bundlers that don't provide Node polyfills (e.g CRA, Vite, etc.) if (opts.bufferPolyfill) window.Buffer = window.Buffer ?? Buffer; // Some bundlers may need `global` and `process.env` polyfills as well // Not implemented here to avoid unexpected behaviors, but leaving example here for future reference /* * window.global = window.global ?? window; * window.process = window.process ?? { env: {} }; */ } const pay = useDaimoPay(); const [ckTheme, setTheme] = useState(theme); const [ckMode, setMode] = useState(mode); const [ckCustomTheme, setCustomTheme] = useState(customTheme ?? {}); const [ckLang, setLang] = useState("en-US"); const [disableMobileInjector, setDisableMobileInjector] = useState(opts.disableMobileInjector ?? false); const onOpenRef = useRef(); const onCloseRef = useRef(); const setOnOpen = useCallback((fn) => { onOpenRef.current = fn; }, []); const setOnClose = useCallback((fn) => { onCloseRef.current = fn; }, []); const [open, setOpenState] = useState(false); const [lockPayParams, setLockPayParams] = useState(false); const [paymentCompleted, setPaymentCompleted] = useState(false); const [route, setRouteState] = useState(ROUTES.SELECT_METHOD); const [modalOptions, setModalOptions] = useState(); // Daimo Pay context const [pendingConnectorId, setPendingConnectorId] = useState(undefined); // Track sessions. Each generates separate intent IDs unless using externalId. const [sessionId] = useState(() => crypto.randomUUID().replaceAll("-", "")); const [solanaConnector, setSolanaConnector] = useState(); // Other configuration const [errorMessage, setErrorMessage] = useState(""); const [confirmationMessage, setConfirmationMessage] = useState(undefined); const [redirectReturnUrl, setRedirectReturnUrl] = useState(undefined); // Connect to the Daimo Pay TRPC API const trpc = useMemo(() => { return createTrpcClient(payApiUrl, sessionId); }, [payApiUrl, sessionId]); const [resize, onResize] = useState(0); useEffect(() => { setInWalletPaymentUrlFromApiUrl(payApiUrl); }, [payApiUrl]); const setOpen = useCallback((open, meta) => { setOpenState(open); // Lock pay params starting from the first time the modal is opened to // prevent the daimo pay order from changing from under the user if (open) { setLockPayParams(true); } // Reset payment state on close if resetOnSuccess is true if (!open && paymentCompleted && modalOptions?.resetOnSuccess) { setPaymentCompleted(false); setLockPayParams(false); paymentState.resetOrder(); } // Log the open/close event trpc.nav.mutate({ action: open ? "navOpenPay" : "navClosePay", orderId: pay.order?.id?.toString(), data: meta ?? {}, }); // Run the onOpen and onClose callbacks if (open) onOpenRef.current?.(); else onCloseRef.current?.(); }, // We don't have good caching on paymentState, so don't include it as a dep // eslint-disable-next-line react-hooks/exhaustive-deps [trpc, pay.order?.id, modalOptions?.resetOnSuccess, paymentCompleted]); // Callback when a payment is successfully completed (regardless of whether // the final call succeeded or bounced) const onSuccess = useCallback(() => { if (modalOptions?.closeOnSuccess) { setTimeout(() => setOpen(false, { event: "wait-success" }), 1000); } setPaymentCompleted(true); }, [modalOptions?.closeOnSuccess, setOpen, setPaymentCompleted]); const setRoute = useCallback((route, data) => { const action = route.replace("daimoPay", ""); log(`[SET ROUTE] ${action} ${pay.order?.id} ${debugJson(data ?? {})}`); trpc.nav.mutate({ action, orderId: pay.order?.id?.toString(), data: data ?? {}, }); setRouteState(route); }, [trpc, pay.order?.id, log]); // Other Configuration useEffect(() => setTheme(theme), [theme]); useEffect(() => setLang(opts.language || "en-US"), [opts.language]); useEffect(() => setDisableMobileInjector(opts.disableMobileInjector ?? false), [opts.disableMobileInjector]); useEffect(() => setErrorMessage(null), [route, open]); const paymentState = usePaymentState({ trpc, lockPayParams, setRoute, log, redirectReturnUrl, }); const showPayment = async (modalOptions) => { const id = pay.order?.id; log(`[PAY] showing modal ${debugJson({ id, modalOptions, paymentFsmState: pay.paymentState })}`); setModalOptions(modalOptions); setOpen(true); if (modalOptions.connectedWalletOnly) { paymentState.setTokenMode("all"); } if (pay.paymentState === "error") { setRoute(ROUTES.ERROR); } else if (pay.paymentState === "payment_started" || pay.paymentState === "payment_completed" || pay.paymentState === "payment_bounced") { setRoute(ROUTES.CONFIRMATION); } else if (modalOptions.connectedWalletOnly) { setRoute(ROUTES.SELECT_TOKEN); } else { setRoute(ROUTES.SELECT_METHOD); } }; // Watch when the order gets paid and navigate to confirmation // ...if underpaid, go to the deposit addr screen, let the user finish paying. const isUnderpaid = pay.order?.mode === DaimoPayOrderMode.HYDRATED && pay.order.sourceStatus === DaimoPayOrderStatusSource.WAITING_PAYMENT && pay.order.sourceTokenAmount != null; useEffect(() => { if (pay.paymentState === "payment_started" || pay.paymentState === "payment_completed" || pay.paymentState === "payment_bounced") { setRoute(ROUTES.CONFIRMATION, { event: "payment-started" }); } else if (isUnderpaid) { paymentState.setSelectedDepositAddressOption(undefined); setRoute(ROUTES.WAITING_DEPOSIT_ADDRESS); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [pay.paymentState, setRoute, isUnderpaid]); const value = { theme: ckTheme, setTheme, mode: ckMode, setMode, customTheme, setCustomTheme, lang: ckLang, setLang, disableMobileInjector, setDisableMobileInjector, setOnOpen, setOnClose, open, setOpen, route, setRoute, // Daimo Pay context pendingConnectorId, setPendingConnectorId, sessionId, solanaConnector, setSolanaConnector, onConnect, // Other configuration options: opts, errorMessage, onSuccess, confirmationMessage, setConfirmationMessage, redirectReturnUrl, setRedirectReturnUrl, debugMode, log, displayError: (message, code) => { setErrorMessage(message); console.log("---------DAIMOPAY DEBUG---------"); console.log(message); if (code) console.table(code); console.log("---------/DAIMOPAY DEBUG---------"); }, resize, triggerResize: () => onResize((prev) => prev + 1), // Above: generic ConnectKit context // Below: Daimo Pay context showPayment, paymentState, trpc, }; return createElement(PayContext.Provider, { value }, jsx(Web3ContextProvider, { children: jsxs(ThemeProvider, { theme: defaultTheme, children: [children, jsx(DaimoPayModal, { lang: ckLang, theme: ckTheme, mode: mode, customTheme: ckCustomTheme, disableMobileInjector: disableMobileInjector })] }) })); }; /** * Provides context for DaimoPayButton and hooks. Place in app root or layout. */ const DaimoPayProvider = (props) => { const payApiUrl = props.payApiUrl ?? "https://pay-api.daimo.xyz/"; const log = useMemo(() => props.debugMode ? (...args) => console.log(...args) : () => { }, [props.debugMode]); return (jsx(PaymentProvider, { payApiUrl: payApiUrl, log: log, children: jsx(SolanaContextProvider, { solanaRpcUrl: props.solanaRpcUrl, children: jsx(DaimoPayUIProvider, { ...props, payApiUrl: payApiUrl, log: log }) }) })); }; export { DaimoPayProvider }; //# sourceMappingURL=DaimoPayProvider.js.map