@coin-voyage/paykit
Version:
Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.
177 lines (176 loc) • 12.1 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { AnimatePresence } from "framer-motion";
import { useCallback, useEffect, useRef, useState } from "react";
import { ConnectingContainer, Container, Content, RetryButton, RetryIconContainer } from "./styles";
import Alert from "../../ui/Alert";
import Button from "../../ui/Button";
import { ModalBody, ModalContent, ModalContentContainer, ModalH1, ModalHeading, PageContent, } from "../../ui/Modal/styles";
import Tooltip from "../../ui/Tooltip";
import SquircleSpinner from "../../spinners/SquircleSpinner";
import { useUniversalConnect } from "@coin-voyage/crypto/hooks";
import { getConnectorId } from "@coin-voyage/crypto/lib/utils/connector";
import { detectBrowser } from "@coin-voyage/shared/common";
import { AlertIcon, RetryIconCircle, TickIcon } from "../../../assets/icons";
import useLocales from "../../../hooks/useLocales";
import { ROUTES } from "../../../types/routes";
import { isInjectedConnector, isWalletConnectConnector } from "../../../utils";
import usePayContext from "../../contexts/pay";
import CircleSpinner from "../../spinners/CircleSpinner";
import { AnimationContainer } from "../../ui/AnimationContainer/styles";
import BrowserIcon from "../../ui/BrowserIcon";
export const states = {
CONNECTED: "connected",
CONNECTING: "connecting",
EXPIRING: "expiring",
FAILED: "failed",
REJECTED: "rejected",
NOTCONNECTED: "notconnected",
UNAVAILABLE: "unavailable",
};
const contentVariants = {
initial: {
willChange: "transform,opacity",
position: "relative",
opacity: 0,
scale: 0.95,
},
animate: {
position: "relative",
opacity: 1,
scale: 1,
transition: {
ease: [0.16, 1, 0.3, 1],
duration: 0.4,
delay: 0.05,
position: { delay: 0 },
},
},
exit: {
position: "absolute",
opacity: 0,
scale: 0.95,
transition: {
ease: [0.16, 1, 0.3, 1],
duration: 0.3,
},
},
};
export default function ConnectWithInjector({ forceState }) {
const { setRoute, log } = usePayContext();
const { connect } = useUniversalConnect({
onMutate: (data) => {
if (data?.connector) {
setStatus(states.CONNECTING);
}
else {
setStatus(states.UNAVAILABLE);
}
},
onError(error) {
if (error) {
setShowTryAgainTooltip(true);
setTimeout(() => setShowTryAgainTooltip(false), 3500);
log("Error connecting with injector", { error });
if (error.code) {
// https://github.com/MetaMask/eth-rpc-errors/blob/main/src/error-constants.ts
switch (error.code) {
case -32002:
setStatus(states.NOTCONNECTED);
break;
case 4001:
setStatus(states.REJECTED);
break;
default:
setStatus(states.FAILED);
break;
}
}
else {
// Sometimes the error doesn't respond with a code
if (error.message && typeof error.message === "string") {
if (error.message.includes("User rejected")) {
setStatus(states.REJECTED);
return;
}
setStatus(states.FAILED);
}
}
setTimeout(triggerResize, 100);
}
},
onSuccess(_, variables) {
if (variables?.connector) {
setStatus(states.CONNECTED);
setRoute(ROUTES.SELECT_TOKEN);
}
},
});
const { triggerResize, paymentState } = usePayContext();
const wallet = paymentState.selectedWallet;
const walletInfo = {
name: wallet?.name,
shortName: wallet?.shortName ?? wallet?.name,
icon: wallet?.iconConnector ?? wallet?.icon,
iconShape: wallet?.iconShape ?? "circle",
iconShouldShrink: wallet?.iconShouldShrink,
};
const [showTryAgainTooltip, setShowTryAgainTooltip] = useState(false);
const expiryDefault = 9; // Starting at 10 causes layout shifting, better to start at 9
const [_expiryTimer, _setExpiryTimer] = useState(expiryDefault);
const browser = detectBrowser();
const extensionUrl = wallet?.downloadUrls?.[browser];
const suggestedExtension = wallet?.downloadUrls
? {
name: Object.keys(wallet?.downloadUrls)[0],
label: Object.keys(wallet?.downloadUrls)[0]?.charAt(0).toUpperCase() +
Object.keys(wallet?.downloadUrls)[0]?.slice(1), // Capitalise first letter, but this might be better suited as a lookup table
url: wallet?.downloadUrls[Object.keys(wallet?.downloadUrls)[0]],
}
: undefined;
const [status, setStatus] = useState(forceState ? forceState : !wallet?.isInstalled ? states.UNAVAILABLE : states.CONNECTING);
const locales = useLocales({
CONNECTORNAME: walletInfo.name,
CONNECTORSHORTNAME: walletInfo.shortName ?? walletInfo.name,
SUGGESTEDEXTENSIONBROWSER: suggestedExtension?.label ?? "your browser",
});
const connectRef = useRef(connect);
useEffect(() => {
connectRef.current = connect;
}, [connect]);
const runConnect = useCallback(async () => {
if (wallet?.isInstalled && wallet?.connectors) {
await connectRef.current({ walletConnector: wallet.connectors[0] });
}
else {
setStatus(states.UNAVAILABLE);
}
}, [wallet, setStatus]);
useEffect(() => {
if (status !== states.CONNECTING)
return;
// UX: Give user time to see the UI before opening the extension
const timeoutId = setTimeout(runConnect, 600);
return () => clearTimeout(timeoutId);
}, [status, runConnect]);
if (!wallet) {
return (_jsx(PageContent, { children: _jsxs(Container, { children: [_jsx(ModalHeading, { children: "Invalid State" }), _jsx(ModalContent, { children: _jsx(Alert, { children: "No connectors match the id given. This state should never happen." }) })] }) }));
}
const connectorId = getConnectorId(wallet.connectors[0].connector);
const isWalletConnect = isWalletConnectConnector(connectorId);
const isInjected = isInjectedConnector(connectorId);
// TODO: Make this more generic
if (isWalletConnect) {
return (_jsx(PageContent, { children: _jsxs(Container, { children: [_jsx(ModalHeading, { children: "Invalid State" }), _jsx(ModalContent, { children: _jsx(Alert, { children: "WalletConnect does not have an injection flow. This state should never happen." }) })] }) }));
}
return (_jsx(PageContent, { children: _jsxs(Container, { children: [_jsx(ConnectingContainer, { children: _jsxs(AnimationContainer, { "$shake": status === states.FAILED || status === states.REJECTED, "$circle": walletInfo.iconShape === "circle", children: [_jsx(AnimatePresence, { children: (status === states.FAILED || status === states.REJECTED) && (_jsx(RetryButton, { "aria-label": "Retry", initial: { opacity: 0, scale: 0.8 }, animate: { opacity: 1, scale: 1 }, exit: { opacity: 0, scale: 0.8 }, whileTap: { scale: 0.9 }, transition: { duration: 0.1 }, onClick: runConnect, children: _jsx(RetryIconContainer, { children: _jsx(Tooltip, { open: showTryAgainTooltip && (status === states.FAILED || status === states.REJECTED), message: locales.tryAgainQuestion, xOffset: -6, children: _jsx(RetryIconCircle, {}) }) }) })) }), walletInfo.iconShape === "circle" ? (_jsx(CircleSpinner, { logo: status === states.UNAVAILABLE ? (_jsx("div", { style: {
transform: "scale(1.14)",
position: "relative",
width: "100%",
}, children: walletInfo.icon })) : (_jsx(_Fragment, { children: walletInfo.icon })), smallLogo: walletInfo.iconShouldShrink, loading: status === states.CONNECTING, unavailable: status === states.UNAVAILABLE })) : (_jsx(SquircleSpinner, { logo: status === states.UNAVAILABLE ? (_jsx("div", { style: {
transform: "scale(1.14)",
position: "relative",
width: "100%",
}, children: walletInfo.icon })) : (_jsx(_Fragment, { children: walletInfo.icon })), loading: status === states.CONNECTING }))] }) }), _jsx(ModalContentContainer, { children: _jsxs(AnimatePresence, { initial: false, children: [status === states.FAILED && (_jsx(Content, { initial: "initial", animate: "animate", exit: "exit", variants: contentVariants, children: _jsxs(ModalContent, { children: [_jsxs(ModalH1, { "$error": true, children: [_jsx(AlertIcon, {}), locales.injectionScreen_failed_h1] }), _jsx(ModalBody, { children: locales.injectionScreen_failed_p })] }) }, states.FAILED)), status === states.REJECTED && (_jsx(Content, { initial: "initial", animate: "animate", exit: "exit", variants: contentVariants, children: _jsxs(ModalContent, { style: { paddingBottom: 28 }, children: [_jsx(ModalH1, { children: locales.injectionScreen_rejected_h1 }), _jsx(ModalBody, { children: locales.injectionScreen_rejected_p })] }) }, states.REJECTED)), (status === states.CONNECTING || status === states.EXPIRING) && (_jsx(Content, { initial: "initial", animate: "animate", exit: "exit", variants: contentVariants, children: _jsxs(ModalContent, { style: { paddingBottom: 28 }, children: [_jsx(ModalH1, { children: isInjected
? locales.injectionScreen_connecting_injected_h1
: locales.injectionScreen_connecting_h1 }), _jsx(ModalBody, { children: isInjected ? locales.injectionScreen_connecting_injected_p : locales.injectionScreen_connecting_p })] }) }, states.CONNECTING)), status === states.CONNECTED && (_jsx(Content, { initial: "initial", animate: "animate", exit: "exit", variants: contentVariants, children: _jsxs(ModalContent, { children: [_jsxs(ModalH1, { "$valid": true, children: [_jsx(TickIcon, {}), " ", locales.injectionScreen_connected_h1] }), _jsx(ModalBody, { children: locales.injectionScreen_connected_p })] }) }, states.CONNECTED)), status === states.NOTCONNECTED && (_jsx(Content, { initial: "initial", animate: "animate", exit: "exit", variants: contentVariants, children: _jsxs(ModalContent, { children: [_jsx(ModalH1, { children: locales.injectionScreen_notconnected_h1 }), _jsx(ModalBody, { children: locales.injectionScreen_notconnected_p })] }) }, states.NOTCONNECTED)), status === states.UNAVAILABLE && (_jsx(Content, { initial: "initial", animate: "animate", exit: "exit", variants: contentVariants, children: !extensionUrl ? (_jsxs(_Fragment, { children: [_jsxs(ModalContent, { style: { paddingBottom: 12 }, children: [_jsx(ModalH1, { children: locales.injectionScreen_unavailable_h1 }), _jsx(ModalBody, { children: locales.injectionScreen_unavailable_p })] }), !wallet.isInstalled && suggestedExtension && (_jsxs(Button, { href: suggestedExtension?.url, icon: _jsx(BrowserIcon, { browser: suggestedExtension?.name }), children: ["Install on ", suggestedExtension?.label] }))] })) : (_jsxs(_Fragment, { children: [_jsxs(ModalContent, { style: { paddingBottom: 18 }, children: [_jsx(ModalH1, { children: locales.injectionScreen_install_h1 }), _jsx(ModalBody, { children: locales.injectionScreen_install_p })] }), !wallet.isInstalled && extensionUrl && (_jsx(Button, { href: extensionUrl, icon: _jsx(BrowserIcon, {}), children: locales.installTheExtension }))] })) }, states.UNAVAILABLE))] }) })] }) }));
}