@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
JavaScript
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