UNPKG

@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
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%; } `;