@coin-voyage/paykit
Version:
Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.
155 lines (151 loc) • 7.96 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useAccount } from "@coin-voyage/crypto/hooks";
import { assert, getChainTypeByChainId } from "@coin-voyage/shared/common";
import { ChainType, PayOrderMode, PayOrderStatus } from "@coin-voyage/shared/types";
import { AnimatePresence, motion } from "framer-motion";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useChainId, useSwitchChain } from "wagmi";
import { chainToLogo } from "../../../assets/chains";
import { AlertIcon, RetryIconCircle } from "../../../assets/icons";
import useLocales from "../../../hooks/useLocales";
import styled from "../../../styles/styled";
import { ROUTES } from "../../../types/routes";
import usePayContext from "../../contexts/pay";
import { RetryButton, RetryIconContainer } from "../../pay-modal/ConnectWithInjector/styles";
import CircleSpinner from "../../spinners/CircleSpinner";
import { AnimationContainer } from "../../ui/AnimationContainer/styles";
import { ModalBody, ModalContent, ModalH1, PageContent } from "../../ui/Modal/styles";
import Tooltip from "../../ui/Tooltip";
var PayState;
(function (PayState) {
PayState["RequestingPayment"] = "Requesting Payment";
PayState["SwitchingChain"] = "Switching Chain";
PayState["RequestCancelled"] = "Payment Cancelled";
PayState["RequestSuccessful"] = "Payment Successful";
})(PayState || (PayState = {}));
export default function PayWithToken() {
const { triggerResize, paymentState, setRoute, log, allowedWallets } = usePayContext();
const { selectedCurrencyOption, payFromWallet, payOrder } = paymentState;
const { account } = useAccount();
const [payState, setPayState] = useState(PayState.RequestingPayment);
const locales = useLocales();
const walletChainId = useChainId();
const { switchChainAsync } = useSwitchChain();
assert(payOrder !== undefined, "Pay order must be defined");
const isExpired = payOrder.status === PayOrderStatus.EXPIRED;
const isDeposit = payOrder.mode === PayOrderMode.DEPOSIT;
const chainType = getChainTypeByChainId(paymentState.selectedCurrencyOption?.chain_id);
const trySwitchingChain = useCallback(async (option, forceSwitch = false) => {
if (walletChainId === option.chain_id && !forceSwitch)
return true;
try {
const switched = await switchChainAsync({ chainId: option.chain_id });
return switched?.id === option.chain_id;
}
catch (e) {
console.error("Failed to switch chain", e);
return false;
}
}, [walletChainId, switchChainAsync]);
const ensureCorrectChain = useCallback(async (token) => {
if (chainType !== ChainType.EVM)
return true;
setPayState(PayState.SwitchingChain);
return trySwitchingChain(token);
}, [chainType, trySwitchingChain]);
const executePayment = useCallback(async (token) => {
const txHash = await payFromWallet(token);
if (!txHash)
throw new Error("Payment rejected");
}, [payFromWallet]);
const isRestricted = useMemo(() => {
if (allowedWallets === null)
return false;
return !allowedWallets.some((w) => w.address === account.address && w.chainType === account.chainType && w.allowed);
}, [allowedWallets, account]);
const handleTransfer = useCallback(async (token) => {
if (isRestricted)
return;
if (!(await ensureCorrectChain(token))) {
setPayState(PayState.RequestCancelled);
return;
}
setPayState(PayState.RequestingPayment);
try {
await executePayment(token);
setPayState(PayState.RequestSuccessful);
setTimeout(() => setRoute(ROUTES.CONFIRMATION), 200);
}
catch (e) {
if (e?.name === "ConnectorChainMismatchError") {
log("Chain mismatch detected, retrying");
if (await trySwitchingChain(token, true)) {
await executePayment(token);
return;
}
}
setPayState(PayState.RequestCancelled);
log("Failed to pay with token", e);
}
}, [isRestricted, ensureCorrectChain, executePayment, trySwitchingChain, setPayState, setRoute, log]);
useEffect(() => {
if (!selectedCurrencyOption)
return;
const timeoutId = setTimeout(() => {
handleTransfer(selectedCurrencyOption);
}, 100);
return () => clearTimeout(timeoutId);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedCurrencyOption]);
useEffect(() => {
triggerResize();
}, [payState, triggerResize]);
const onRetry = () => {
if (isExpired && isDeposit) {
paymentState.copyDepositPayOrder();
setRoute(ROUTES.SELECT_TOKEN);
}
else if (selectedCurrencyOption) {
handleTransfer(selectedCurrencyOption);
}
};
return (_jsxs(PageContent, { children: [_jsx(LoadingContainer, { children: _jsxs(AnimationContainer, { "$shake": payState === PayState.RequestCancelled, "$circle": true, children: [_jsx(AnimatePresence, { children: payState === PayState.RequestCancelled || (isExpired && isDeposit) ? (_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: onRetry, children: _jsx(RetryIconContainer, { children: _jsx(Tooltip, { open: payState === PayState.RequestCancelled, message: locales.tryAgainQuestion, xOffset: -6, children: _jsx(RetryIconCircle, {}) }) }) })) : (_jsx(ChainLogoContainer, { children: selectedCurrencyOption && chainToLogo[selectedCurrencyOption.chain_id] }, "ChainLogoContainer")) }), _jsx(AnimatePresence, { children: _jsx(CircleSpinner, { logo: _jsx("img", { src: selectedCurrencyOption?.image_uri, alt: selectedCurrencyOption?.ticker }, selectedCurrencyOption?.image_uri), loading: payState === PayState.RequestingPayment && !isExpired, unavailable: false }, "CircleSpinner") })] }) }), !isExpired ? (_jsxs(ModalContent, { children: [payState === PayState.RequestCancelled && (_jsxs(_Fragment, { children: [_jsxs(ModalH1, { "$warning": true, children: [_jsx(AlertIcon, {}), locales.injectionScreen_rejected_h1] }), _jsx(ModalBody, { children: locales.injectionScreen_rejected_p })] })), payState === PayState.SwitchingChain && _jsx(ModalH1, { children: locales.switchNetworkScreen_heading }), payState === PayState.RequestingPayment && (_jsxs(_Fragment, { children: [_jsx(ModalH1, { children: locales.requesting_payment_h1 }), _jsx(ModalBody, { children: locales.requesting_payment_p })] })), payState === PayState.RequestSuccessful && _jsx(ModalH1, { children: locales.injectionScreen_connected_h1 })] })) : (_jsxs(ModalContent, { children: [_jsxs(ModalH1, { "$warning": true, children: [_jsx(AlertIcon, {}), locales.payWithTokenScreen_expired_h1] }), _jsx(ModalBody, { children: locales.payWithTokenScreen_expired_p })] }))] }));
}
const LoadingContainer = styled(motion.div) `
display: flex;
align-items: center;
justify-content: center;
margin: 10px auto 16px;
height: 120px;
`;
const ChainLogoContainer = styled(motion.div) `
z-index: 10;
position: absolute;
right: 2px;
bottom: 2px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 16px;
overflow: hidden;
color: var(--ck-body-background);
transition: color 200ms ease;
&:before {
z-index: 5;
content: "";
position: absolute;
inset: 0;
opacity: 0;
transition: opacity 200ms ease;
background: var(--ck-body-color);
}
svg {
display: block;
position: relative;
width: 100%;
height: 100%;
}
`;