@coin-voyage/paykit
Version:
Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.
275 lines • 16.6 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useCallback, useEffect, useRef, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { ResetContainer } from "../../../styles";
import Portal from "../Portal";
import { flattenChildren, isWalletConnectConnector, } from "../../../utils";
import { BackButton, BackgroundOverlay, BoxContainer, CloseButton, Container, ControllerContainer, Disclaimer, DisclaimerBackground, ErrorMessage, InfoButton, InnerContainer, ModalContainer, ModalHeading, PageContainer, PageContents, TextWithHr, } from "./styles";
import useLockBodyScroll from "../../../hooks/useLockBodyScroll";
import { ROUTES } from "../../../types/routes";
import { getChainName, getChainTypeName, PayOrderMode } from "@coin-voyage/shared/common";
import { isMobile } from "@coin-voyage/shared/utils";
import { useTransition } from "react-transition-state";
import { useAccount, useSwitchChain } from "wagmi";
import FocusTrap from "../../../hooks/useFocusTrap";
import useLocales from "../../../hooks/useLocales";
import usePrevious from "../../../hooks/usePrevious";
import { useThemeContext } from "../../../providers/theme/provider";
import usePayContext from "../../contexts/pay";
import FitText from "../FitText";
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;
export 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 = ({ pages, pageId, positionInside, inline, onClose, onBack, onInfo, }) => {
const context = usePayContext();
const themeContext = useThemeContext();
const mobile = isMobile();
const { selectedWallet, connectorChainType, selectedTokenOption, payOrder } = context.paymentState;
const walletName = selectedWallet?.name;
const locales = useLocales({
CONNECTORNAME: walletName,
CHAIN_TYPE: getChainTypeName(connectorChainType),
});
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(context.open);
if (context.open)
setInTransition(undefined);
}, [context.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);
blockTimeout = setTimeout(() => setInTransition(false), 360);
// Calculate new content bounds
updateBounds(node);
}, [context.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 shouldUseQrcode() {
if (!selectedWallet)
return false; // Fail states are shown in the injector flow
const useInjector = !selectedWallet.getWalletConnectDeeplink || selectedWallet.isInstalled;
return !useInjector;
}
function getHeading() {
switch (context.route) {
case ROUTES.ABOUT:
return locales.aboutScreen_heading;
case ROUTES.CONNECT:
if (shouldUseQrcode()) {
return isWalletConnectConnector(selectedWallet?.id)
? locales.scanScreen_heading
: locales.scanScreen_heading_withConnector;
}
return walletName;
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:
return locales.selectMethodScreen_heading;
case ROUTES.SELECT_TOKEN:
return "Select Token";
case ROUTES.PAY_WITH_TOKEN: {
if (!selectedTokenOption)
return undefined;
const chainName = getChainName(selectedTokenOption.chain_id);
return `Pay with ${chainName} (${selectedTokenOption.ticker})`;
}
case ROUTES.CONFIRMATION: {
const isDeposit = payOrder?.mode === PayOrderMode.DEPOSIT;
if (isDeposit) {
return "Confirming Deposit";
}
return "Confirming Payment";
}
}
}
const Content = (_jsx(ResetContainer, { "$useTheme": themeContext.theme, "$useMode": themeContext.mode, "$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",
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, 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 }));
};
export const OrDivider = ({ children, hideHr }) => {
const locales = useLocales();
return (_jsx(TextWithHr, { "$disableHr": hideHr, children: _jsx("span", { children: children ?? locales.or }) }));
};
export default Modal;
//# sourceMappingURL=index.js.map