@daimo/pay
Version:
Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.
303 lines (300 loc) • 17.9 kB
JavaScript
import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
import { useEffect, useState, useCallback, useRef } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { ResetContainer } from '../../../styles/index.js';
import Portal from '../Portal/index.js';
import { isMobile, flattenChildren } from '../../../utils/index.js';
import { ModalContainer, BackgroundOverlay, Container, BoxContainer, DisclaimerBackground, Disclaimer, ErrorMessage, ControllerContainer, CloseButton, BackButton, InfoButton, ModalHeading, InnerContainer, PageContents, PageContainer, TextWithHr } from './styles.js';
import useLockBodyScroll from '../../../hooks/useLockBodyScroll.js';
import { usePayContext } from '../../../hooks/usePayContext.js';
import { getChainName } from '@daimo/pay-common';
import { useTransition } from 'react-transition-state';
import { useAccount, useSwitchChain } from 'wagmi';
import { ROUTES } from '../../../constants/routes.js';
import { useDaimoPay } from '../../../hooks/useDaimoPay.js';
import FocusTrap from '../../../hooks/useFocusTrap.js';
import useLocales from '../../../hooks/useLocales.js';
import usePrevious from '../../../hooks/usePrevious.js';
import { useWallet, WALLET_ID_MOBILE_WALLETS } from '../../../wallets/useWallets.js';
import { useThemeContext } from '../../DaimoPayThemeProvider/DaimoPayThemeProvider.js';
import FitText from '../FitText/index.js';
const InfoIcon = ({ ...props }) => (jsx("svg", { "aria-hidden": "true", width: "22", height: "22", viewBox: "0 0 22 22", fill: "none", xmlns: "http://www.w3.org/2000/svg", ...props, children: jsx("path", { fillRule: "evenodd", clipRule: "evenodd", d: "M20 11C20 15.9706 15.9706 20 11 20C6.02944 20 2 15.9706 2 11C2 6.02944 6.02944 2 11 2C15.9706 2 20 6.02944 20 11ZM22 11C22 17.0751 17.0751 22 11 22C4.92487 22 0 17.0751 0 11C0 4.92487 4.92487 0 11 0C17.0751 0 22 4.92487 22 11ZM11.6445 12.7051C11.6445 13.1348 11.3223 13.4678 10.7744 13.4678C10.2266 13.4678 9.92578 13.1885 9.92578 12.6191V12.4795C9.92578 11.4268 10.4951 10.8574 11.2686 10.3203C12.2031 9.67578 12.665 9.32129 12.665 8.59082C12.665 7.76367 12.0205 7.21582 11.043 7.21582C10.3232 7.21582 9.80762 7.57031 9.45312 8.16113C9.38282 8.24242 9.32286 8.32101 9.2667 8.39461C9.04826 8.68087 8.88747 8.8916 8.40039 8.8916C8.0459 8.8916 7.66992 8.62305 7.66992 8.15039C7.66992 7.96777 7.70215 7.7959 7.75586 7.61328C8.05664 6.625 9.27051 5.75488 11.1182 5.75488C12.9336 5.75488 14.5234 6.71094 14.5234 8.50488C14.5234 9.7832 13.7822 10.417 12.7402 11.1045C11.999 11.5986 11.6445 11.9746 11.6445 12.5762V12.7051ZM11.9131 15.5625C11.9131 16.1855 11.376 16.6797 10.7529 16.6797C10.1299 16.6797 9.59277 16.1748 9.59277 15.5625C9.59277 14.9395 10.1191 14.4453 10.7529 14.4453C11.3867 14.4453 11.9131 14.9287 11.9131 15.5625Z", fill: "currentColor" }) }));
const CloseIcon = ({ ...props }) => (jsx(motion.svg, { width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", xmlns: "http://www.w3.org/2000/svg", ...props, children: jsx("path", { d: "M1 13L13 1M1 1L13 13", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round" }) }));
const BackIcon = ({ ...props }) => (jsx(motion.svg, { width: 9, height: 16, viewBox: "0 0 9 16", fill: "none", xmlns: "http://www.w3.org/2000/svg", ...props, children: jsx("path", { d: "M8 1L1 8L8 15", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round" }) }));
const contentTransitionDuration = 0.22;
const contentVariants = {
initial: {
//willChange: 'transform,opacity',
zIndex: 2,
opacity: 0,
},
animate: {
opacity: 1,
scale: 1,
transition: {
duration: contentTransitionDuration * 0.75,
delay: contentTransitionDuration * 0.25,
ease: [0.26, 0.08, 0.25, 1],
},
},
exit: {
zIndex: 1,
opacity: 0,
pointerEvents: "none",
position: "absolute",
left: ["50%", "50%"],
x: ["-50%", "-50%"],
transition: {
duration: contentTransitionDuration,
ease: [0.26, 0.08, 0.25, 1],
},
},
};
const Modal = ({ open, pages, pageId, positionInside, inline, demo, onClose, onBack, onInfo, }) => {
const context = usePayContext();
const themeContext = useThemeContext();
const mobile = isMobile();
const { selectedExternalOption, selectedTokenOption, selectedSolanaTokenOption, selectedDepositAddressOption, } = context.paymentState;
const { order } = useDaimoPay();
const { connector } = useAccount();
const wallet = useWallet(connector?.id ?? "");
const walletInfo = {
name: wallet?.name,
shortName: wallet?.shortName ?? wallet?.name,
icon: wallet?.iconConnector ?? wallet?.icon,
iconShape: wallet?.iconShape ?? "circle",
iconShouldShrink: wallet?.iconShouldShrink,
};
const locales = useLocales({
CONNECTORNAME: walletInfo?.name,
});
const [state, setOpen] = useTransition({
timeout: 160,
preEnter: true,
mountOnEnter: true,
unmountOnExit: true,
});
const mounted = !(state === "exited" || state === "unmounted");
const rendered = state === "preEnter" || state !== "exiting";
const currentDepth = context.route === ROUTES.CONNECTORS
? 0
: context.route === ROUTES.DOWNLOAD
? 2
: 1;
const prevDepth = usePrevious(currentDepth, currentDepth);
// eslint-disable-next-line react-hooks/rules-of-hooks
if (!positionInside)
useLockBodyScroll(mounted);
useEffect(() => {
setOpen(open);
if (open)
setInTransition(undefined);
}, [open]);
const [dimensions, setDimensions] = useState({
width: undefined,
height: undefined,
});
const [inTransition, setInTransition] = useState(undefined);
// Calculate new content bounds
const updateBounds = (node) => {
const bounds = {
width: node?.offsetWidth,
height: node?.offsetHeight,
};
setDimensions({
width: `${bounds?.width}px`,
height: `${bounds?.height}px`,
});
};
let blockTimeout;
const contentRef = useCallback((node) => {
if (!node)
return;
ref.current = node;
// Avoid transition mixups
setInTransition(inTransition === undefined ? false : true);
clearTimeout(blockTimeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
blockTimeout = setTimeout(() => setInTransition(false), 360);
// Calculate new content bounds
updateBounds(node);
}, [open, inTransition]);
// Update layout on chain/network switch to avoid clipping
const { chain } = useAccount();
const { switchChain } = useSwitchChain();
const ref = useRef(null);
useEffect(() => {
if (ref.current)
updateBounds(ref.current);
}, [chain, switchChain, mobile, context.options, context.resize]);
useEffect(() => {
if (!mounted) {
setDimensions({
width: undefined,
height: undefined,
});
return;
}
const listener = (e) => {
if (e.key === "Escape" && onClose)
onClose();
};
document.addEventListener("keydown", listener);
return () => {
document.removeEventListener("keydown", listener);
};
}, [mounted, onClose]);
const dimensionsCSS = {
"--height": dimensions.height,
"--width": dimensions.width,
};
function getHeading() {
switch (context.route) {
case ROUTES.ABOUT:
return locales.aboutScreen_heading;
case ROUTES.CONNECT:
if (context.pendingConnectorId === WALLET_ID_MOBILE_WALLETS) {
return "Scan with Phone";
}
else {
return walletInfo?.name;
}
case ROUTES.SOLANA_CONNECTOR:
return context.solanaConnector ?? "Solana Wallet";
case ROUTES.CONNECTORS:
return locales.connectorsScreen_heading;
case ROUTES.MOBILECONNECTORS:
return locales.mobileConnectorsScreen_heading;
case ROUTES.DOWNLOAD:
return locales.downloadAppScreen_heading;
case ROUTES.ONBOARDING:
return locales.onboardingScreen_heading;
case ROUTES.SWITCHNETWORKS:
return locales.switchNetworkScreen_heading;
case ROUTES.SELECT_METHOD:
case ROUTES.SELECT_TOKEN:
return order?.metadata.intent;
case ROUTES.SOLANA_PAY_WITH_TOKEN:
if (!selectedSolanaTokenOption)
return undefined;
return `Pay with ${selectedSolanaTokenOption.required.token.symbol}`;
case ROUTES.WAITING_EXTERNAL:
return selectedExternalOption?.cta;
case ROUTES.SELECT_DEPOSIT_ADDRESS_CHAIN:
return "Select Chain";
case ROUTES.WAITING_DEPOSIT_ADDRESS:
if (!selectedDepositAddressOption)
return undefined;
return `Pay with ${selectedDepositAddressOption.id}`;
case ROUTES.SELECT_ZKP2P:
return "Select App";
case ROUTES.SELECT_AMOUNT:
case ROUTES.SELECT_EXTERNAL_AMOUNT:
case ROUTES.SELECT_DEPOSIT_ADDRESS_AMOUNT:
case ROUTES.SOLANA_SELECT_AMOUNT:
case ROUTES.SELECT_WALLET_AMOUNT:
return "Select Amount";
case ROUTES.PAY_WITH_TOKEN:
if (selectedTokenOption == null)
return undefined;
const chainName = getChainName(selectedTokenOption.balance.token.chainId);
return `Pay with ${chainName} ${selectedTokenOption.balance.token.symbol}`;
case ROUTES.CONFIRMATION:
return "Payment Successful";
case ROUTES.ERROR:
return "Error";
case ROUTES.SELECT_WALLET_CHAIN:
return "Select Chain";
}
}
const Content = (jsx(ResetContainer, { "$useTheme": demo?.theme ?? themeContext.theme, "$useMode": demo?.mode ?? themeContext.mode, "$customTheme": demo?.customTheme ?? themeContext.customTheme, children: jsxs(ModalContainer, { role: "dialog", style: {
pointerEvents: rendered ? "auto" : "none",
position: positionInside ? "absolute" : undefined,
}, children: [!inline && (jsx(BackgroundOverlay, { "$active": rendered, onClick: onClose, "$blur": context.options?.overlayBlur })), jsxs(Container, { style: dimensionsCSS, initial: false, children: [jsx("div", { style: {
pointerEvents: inTransition ? "all" : "none", // Block interaction while transitioning
position: "absolute",
top: 0,
bottom: 0,
left: "50%",
transform: "translateX(-50%)",
width: "var(--width)",
zIndex: 9,
transition: "width 200ms ease",
} }), jsxs(BoxContainer, { className: `${rendered && "active"}`, children: [jsx(AnimatePresence, { initial: false, children: context.options?.disclaimer &&
context.route === ROUTES.CONNECTORS && (jsx(DisclaimerBackground, { initial: {
opacity: 0,
}, animate: {
opacity: 1,
}, exit: { opacity: 0 }, transition: {
delay: 0,
duration: 0.2,
ease: [0.25, 0.1, 0.25, 1.0],
}, children: jsx(Disclaimer, { children: jsx("div", { children: context.options?.disclaimer }) }) })) }), jsx(AnimatePresence, { initial: false, children: context.errorMessage && (jsxs(ErrorMessage, { initial: { y: "10%", x: "-50%" }, animate: { y: "-100%" }, exit: { y: "100%" }, transition: { duration: 0.2, ease: "easeInOut" }, children: [jsx("span", { children: context.errorMessage }), jsx("div", { onClick: () => context.displayError(null), style: {
position: "absolute",
right: 24,
top: 24,
cursor: "pointer",
}, children: jsx(CloseIcon, {}) })] })) }), jsxs(ControllerContainer, { children: [onClose && (jsx(CloseButton, { "aria-label": flattenChildren(locales.close).toString(), onClick: onClose, children: jsx(CloseIcon, {}) })), jsx("div", { style: {
position: "absolute",
top: 23,
left: 20,
width: 32,
height: 32,
}, children: jsx(AnimatePresence, { children: onBack ? (jsx(BackButton, { disabled: inTransition, "aria-label": flattenChildren(locales.back).toString(), onClick: onBack, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: {
duration: mobile ? 0 : 0.1,
delay: mobile ? 0.01 : 0,
}, children: jsx(BackIcon, {}) }, "backButton")) : (onInfo &&
!context.options?.hideQuestionMarkCTA && (jsx(InfoButton, { disabled: inTransition, "aria-label": flattenChildren(locales.moreInformation).toString(), onClick: onInfo, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: {
duration: mobile ? 0 : 0.1,
delay: mobile ? 0.01 : 0,
}, children: jsx(InfoIcon, {}) }, "infoButton"))) }) })] }), jsx(ModalHeading, { children: jsx(AnimatePresence, { children: jsx(motion.div, { style: {
position: "absolute",
top: 0,
bottom: 0,
left: 52,
right: 52,
display: "flex",
//alignItems: 'center',
justifyContent: "center",
}, initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, transition: {
duration: mobile ? 0 : 0.17,
delay: mobile ? 0.01 : 0,
}, children: jsx(FitText, { children: getHeading() }) }, `${context.route}`) }) }), jsx(InnerContainer, { children: Object.keys(pages).map((key) => (jsx(Page, { open: key === pageId, initial: !positionInside && state !== "entered", enterAnim: key === pageId
? currentDepth > prevDepth
? "active-scale-up"
: "active"
: "", exitAnim: key !== pageId
? currentDepth < prevDepth
? "exit-scale-down"
: "exit"
: "", children: jsx(PageContents, { ref: contentRef, style: {
pointerEvents: key === pageId && rendered ? "auto" : "none",
}, children: pages[key] }, `inner-${key}`) }, key))) })] })] })] }) }));
return (jsx(Fragment, { children: mounted && (jsx(Fragment, { children: positionInside ? (Content) : (jsx(Fragment, { children: jsx(Portal, { children: jsx(FocusTrap, { children: Content }) }) })) })) }));
};
const Page = ({ children, open, initial, prevDepth, currentDepth, enterAnim, exitAnim, }) => {
const [state, setOpen] = useTransition({
timeout: 400,
preEnter: true,
initialEntered: open,
mountOnEnter: true,
unmountOnExit: true,
});
const mounted = !(state === "exited" || state === "unmounted");
const rendered = state === "preEnter" || state !== "exiting";
useEffect(() => {
setOpen(open);
}, [open]);
if (!mounted)
return null;
return (jsx(PageContainer, { className: `${rendered ? enterAnim : exitAnim}`, style: {
animationDuration: initial ? "0ms" : undefined,
animationDelay: initial ? "0ms" : undefined,
}, children: children }));
};
const OrDivider = ({ children }) => {
const locales = useLocales();
return (jsx(TextWithHr, { children: jsx("span", { children: children ?? locales.or }) }));
};
export { OrDivider, contentVariants, Modal as default };
//# sourceMappingURL=index.js.map