@daimo/pay
Version:
Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.
524 lines (521 loc) • 20.1 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, shouldShowExternalQRCodeOnDesktop } 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 }) => /* @__PURE__ */ 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: /* @__PURE__ */ 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 }) => /* @__PURE__ */ jsx(
motion.svg,
{
width: 14,
height: 14,
viewBox: "0 0 14 14",
fill: "none",
xmlns: "http://www.w3.org/2000/svg",
...props,
children: /* @__PURE__ */ jsx(
"path",
{
d: "M1 13L13 1M1 1L13 13",
stroke: "currentColor",
strokeWidth: "2",
strokeLinecap: "round"
}
)
}
);
const BackIcon = ({ ...props }) => /* @__PURE__ */ jsx(
motion.svg,
{
width: 9,
height: 16,
viewBox: "0 0 9 16",
fill: "none",
xmlns: "http://www.w3.org/2000/svg",
...props,
children: /* @__PURE__ */ 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);
if (!positionInside) useLockBodyScroll(mounted);
useEffect(() => {
setOpen(open);
if (open) setInTransition(void 0);
}, [open]);
const [dimensions, setDimensions] = useState({
width: void 0,
height: void 0
});
const [inTransition, setInTransition] = useState(
void 0
);
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;
setInTransition(inTransition === void 0 ? false : true);
clearTimeout(blockTimeout);
blockTimeout = setTimeout(() => setInTransition(false), 360);
updateBounds(node);
},
[open, inTransition]
);
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: void 0,
height: void 0
});
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
};
const shouldShowWalletQRCodeOnDesktop = context.pendingConnectorId === WALLET_ID_MOBILE_WALLETS || context.pendingConnectorId === "world";
function getHeading() {
const payWithString = flattenChildren(locales.payWith).join("");
switch (context.route) {
case ROUTES.ABOUT:
return locales.aboutScreen_heading;
case ROUTES.CONNECT:
if (shouldShowWalletQRCodeOnDesktop) {
return locales.scanWithPhone;
} else {
return walletInfo?.name;
}
case ROUTES.SELECT_EXCHANGE:
return locales.selectExchange;
case ROUTES.SOLANA_CONNECTOR:
return context.solanaConnector ?? locales.solanaWallet;
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 void 0;
return `${payWithString} ${selectedSolanaTokenOption.required.token.symbol}`;
case ROUTES.WAITING_EXTERNAL:
if (selectedExternalOption && shouldShowExternalQRCodeOnDesktop(selectedExternalOption.id) && !mobile) {
return locales.scanWithPhone;
}
return selectedExternalOption?.cta;
case ROUTES.SELECT_DEPOSIT_ADDRESS_CHAIN:
return locales.selectChain;
case ROUTES.WAITING_DEPOSIT_ADDRESS:
if (!selectedDepositAddressOption) return void 0;
return `${payWithString} ${selectedDepositAddressOption.id}`;
case ROUTES.SELECT_ZKP2P:
return locales.selectApp;
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 locales.selectAmount;
case ROUTES.PAY_WITH_TOKEN:
if (selectedTokenOption == null) return void 0;
const chainName = getChainName(
selectedTokenOption.balance.token.chainId
);
return `${payWithString} ${chainName} ${selectedTokenOption.balance.token.symbol}`;
case ROUTES.CONFIRMATION:
return locales.paymentSuccessful;
case ROUTES.ERROR:
return locales.error;
case ROUTES.SELECT_WALLET_CHAIN:
return locales.selectChain;
}
}
const Content = /* @__PURE__ */ jsx(
ResetContainer,
{
$useTheme: demo?.theme ?? themeContext.theme,
$useMode: demo?.mode ?? themeContext.mode,
$customTheme: demo?.customTheme ?? themeContext.customTheme,
children: /* @__PURE__ */ jsxs(
ModalContainer,
{
role: "dialog",
style: {
pointerEvents: rendered ? "auto" : "none",
position: positionInside ? "absolute" : void 0
},
children: [
!inline && /* @__PURE__ */ jsx(
BackgroundOverlay,
{
$active: rendered,
onClick: onClose,
$blur: context.options?.overlayBlur
}
),
/* @__PURE__ */ jsxs(
Container,
{
style: dimensionsCSS,
initial: false,
children: [
/* @__PURE__ */ 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"
}
}
),
/* @__PURE__ */ jsxs(BoxContainer, { className: `${rendered && "active"}`, children: [
/* @__PURE__ */ jsx(AnimatePresence, { initial: false, children: context.options?.disclaimer && context.route === ROUTES.CONNECTORS && /* @__PURE__ */ 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]
},
children: /* @__PURE__ */ jsx(Disclaimer, { children: /* @__PURE__ */ jsx("div", { children: context.options?.disclaimer }) })
}
) }),
/* @__PURE__ */ jsx(AnimatePresence, { initial: false, children: context.errorMessage && /* @__PURE__ */ jsxs(
ErrorMessage,
{
initial: { y: "10%", x: "-50%" },
animate: { y: "-100%" },
exit: { y: "100%" },
transition: { duration: 0.2, ease: "easeInOut" },
children: [
/* @__PURE__ */ jsx("span", { children: context.errorMessage }),
/* @__PURE__ */ jsx(
"div",
{
onClick: () => context.displayError(null),
style: {
position: "absolute",
right: 24,
top: 24,
cursor: "pointer"
},
children: /* @__PURE__ */ jsx(CloseIcon, {})
}
)
]
}
) }),
/* @__PURE__ */ jsxs(ControllerContainer, { children: [
onClose && /* @__PURE__ */ jsx(
CloseButton,
{
"aria-label": flattenChildren(locales.close).toString(),
onClick: onClose,
children: /* @__PURE__ */ jsx(CloseIcon, {})
}
),
/* @__PURE__ */ jsx(
"div",
{
style: {
position: "absolute",
top: 23,
left: 20,
width: 32,
height: 32
},
children: /* @__PURE__ */ jsx(AnimatePresence, { children: onBack ? /* @__PURE__ */ 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: /* @__PURE__ */ jsx(BackIcon, {})
},
"backButton"
) : onInfo && !context.options?.hideQuestionMarkCTA && /* @__PURE__ */ 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: /* @__PURE__ */ jsx(InfoIcon, {})
},
"infoButton"
) })
}
)
] }),
/* @__PURE__ */ jsx(ModalHeading, { children: /* @__PURE__ */ jsx(AnimatePresence, { children: /* @__PURE__ */ 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: /* @__PURE__ */ jsx(FitText, { children: getHeading() })
},
`${context.route}`
) }) }),
/* @__PURE__ */ jsx(InnerContainer, { children: Object.keys(pages).map((key) => /* @__PURE__ */ 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: /* @__PURE__ */ jsx(
PageContents,
{
ref: contentRef,
style: {
pointerEvents: key === pageId && rendered ? "auto" : "none"
},
children: pages[key]
},
`inner-${key}`
)
},
key
)) })
] })
]
}
)
]
}
)
}
);
return /* @__PURE__ */ jsx(Fragment, { children: mounted && /* @__PURE__ */ jsx(Fragment, { children: positionInside ? Content : /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ 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 /* @__PURE__ */ jsx(
PageContainer,
{
className: `${rendered ? enterAnim : exitAnim}`,
style: {
animationDuration: initial ? "0ms" : void 0,
animationDelay: initial ? "0ms" : void 0
},
children
}
);
};
const OrDivider = ({ children }) => {
const locales = useLocales();
return /* @__PURE__ */ jsx(TextWithHr, { children: /* @__PURE__ */ jsx("span", { children: children ?? locales.or }) });
};
export { OrDivider, contentVariants, Modal as default };
//# sourceMappingURL=index.js.map