UNPKG

@coin-voyage/paykit

Version:

Seamless crypto payments. Onboard users from any chain, any coin into your app with one click.

252 lines (251 loc) 10.6 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useInWagmiContext } from "@coin-voyage/crypto/evm/provider/provider"; import { useAccount, useConnectCallback } from "@coin-voyage/crypto/hooks"; import { PayOrderMode, PayOrderStatus } from "@coin-voyage/shared/types"; import { Buffer } from "buffer"; import { useCallback, useEffect, useRef, useState } from "react"; import { ThemeProvider as StyledThemeProvider } from "styled-components"; import { PayContext } from "../components/contexts/pay/index"; import { PayModal } from "../components/pay-modal/index"; import { useThemeFont } from "../hooks/useGoogleFont"; import { usePaymentState } from "../hooks/usePaymentState"; import defaultTheme from "../styles/defaultTheme"; import { PaymentMethod } from "../types/enums"; import { ROUTES } from "../types/routes"; import { ApiProvider } from "./api-provider"; const defaultOptions = { language: "en-US", hideTooltips: false, hideQuestionMarkCTA: false, hideNoWalletCTA: false, walletConnectCTA: "link", hideRecentBadge: false, embedGoogleFonts: false, disclaimer: null, bufferPolyfill: true, overlayBlur: undefined, optimisticConfirmation: true, }; export const PayKitProvider = ({ apiKey, environment = "production", ...props }) => { const inWagmiContext = useInWagmiContext(); if (!inWagmiContext) { throw new Error("PayKitProvider must be used within a WagmiContext"); } return (_jsx(ApiProvider, { apiKey: apiKey, environment: environment, children: _jsx(PayKitProviderInternal, { ...props }) })); }; function PayKitProviderInternal({ theme = "auto", mode = "auto", customTheme, options, onConnect, onConnectValidation, onDisconnect, debugMode = false, children, }) { const [allowedWallets, setAllowedWallets] = useState(onConnectValidation ? [] : null); useConnectCallback({ onConnect, onDisconnect, onConnectValidation, setAllowedWallets, }); const opts = Object.assign({}, defaultOptions, options); if (typeof window !== "undefined" && opts.bufferPolyfill) { // Buffer Polyfill, needed for bundlers that don't provide Node polyfills (e.g CRA, Vite, etc.) window.Buffer = window.Buffer ?? Buffer; } const [ckTheme, setTheme] = useState(theme); const [ckMode, setMode] = useState(mode); const [ckCustomTheme, setCustomTheme] = useState(customTheme ?? {}); const [ckLang, setLang] = useState("en-US"); const [open, setOpenState] = useState(false); const [paymentCompleted, setPaymentCompleted] = useState(false); const [route, setRoute] = useState(ROUTES.SELECT_METHOD); const [modalOptions, setModalOptions] = useState(); const [errorMessage, setErrorMessage] = useState(""); const [resize, onResize] = useState(0); const onOpenRef = useRef(undefined); const onCloseRef = useRef(undefined); const setOnOpen = useCallback((fn) => { onOpenRef.current = fn; }, []); const setOnClose = useCallback((fn) => { onCloseRef.current = fn; }, []); // Ref to access the latest paymentState without causing stale closures const paymentStateRef = useRef(null); // Include Google Font that is needed for themes useThemeFont(opts.embedGoogleFonts ? theme : null); const setOpen = useCallback((open) => { setOpenState(open); // Reset payment state on close if resetOnSuccess is true if (!open && paymentCompleted && modalOptions?.resetOnSuccess) { setPaymentCompleted(false); // Use ref to get the latest paymentState without stale closure issues paymentStateRef.current?.clearUserSelection(); } // Run the onOpen and onClose callbacks if (open) onOpenRef.current?.(); else onCloseRef.current?.(); }, [modalOptions?.resetOnSuccess, paymentCompleted]); // Callback when a payOrder is successfully completed (regardless of whether // the final call succeeded or bounced) const onSuccess = useCallback(() => { if (modalOptions?.closeOnSuccess) { setTimeout(() => setOpen(false), 1000); } setPaymentCompleted(true); }, [modalOptions?.closeOnSuccess, setOpen, setPaymentCompleted]); // Other Configuration useEffect(() => setTheme(theme), [theme]); useEffect(() => setLang(opts.language || "en-US"), [opts.language]); useEffect(() => setErrorMessage(null), [route, open]); const { account } = useAccount(); const log = useCallback((...args) => { if (debugMode) console.log(...args); }, [debugMode]); // PaymentState is a second, inner context object containing a PayOrder // plus all associated status and callbacks. In order for useContext() and // downstream hooks like usePayStatus() to work correctly, we must set // set refresh context when payment status changes; done via setPayOrder. const [payOrder, setPayOrder] = useState(); const paymentState = usePaymentState({ payOrder, setPayOrder, setRoute, log, }); const { refreshOrder, setPayId, setConnectorChainType } = paymentState; // Keep paymentStateRef in sync to avoid stale closures paymentStateRef.current = paymentState; useEffect(() => { if (typeof window === "undefined") return; const params = new URLSearchParams(window.location.search); const initState = params.get("initState"); if (!initState) return; const removeInitStateParam = () => { params.delete("initState"); const newUrl = window.location.pathname + (params.toString() ? `?${params.toString()}` : "") + window.location.hash; window.history.replaceState({}, document.title, newUrl); }; try { const decoded = atob(initState); const parsed = JSON.parse(decoded); if (parsed.open) { const { payOrderId, chainType, route } = parsed; const timeoutId = setTimeout(() => { // Wait a bit for everything to initialize // Then open the modal and set the payOrderId setOpen(true); setPayId(payOrderId); setConnectorChainType(chainType); if (route) setRoute(route); }, 500); return () => clearTimeout(timeoutId); } } catch (e) { console.error("Failed to parse initState", e); } finally { removeInitStateParam(); } }, [setOpen, setPayId, setConnectorChainType, setRoute]); // When a payment has started, poll for updates. Do this regardless // of whether the modal is still being displayed for usePayStatus(). useEffect(() => { if (!payOrder?.status) return; let intervalMs = 0; if (payOrder.status === PayOrderStatus.AWAITING_PAYMENT) { intervalMs = 5000; } else if ([PayOrderStatus.AWAITING_CONFIRMATION, PayOrderStatus.OPTIMISTIC_CONFIRMED].includes(payOrder.status)) { intervalMs = 2500; } else if (payOrder?.status === PayOrderStatus.EXECUTING_ORDER) { // Poll faster for deposits, sales completion is not/less relevant for users intervalMs = payOrder.mode === PayOrderMode.DEPOSIT ? 1000 : 2500; } else { return; } const timeout = setTimeout(refreshOrder, intervalMs); return () => { clearTimeout(timeout); }; }, [payOrder, refreshOrder]); const showModal = useCallback(async (modalOptions) => { setModalOptions(modalOptions); setOpen(true); const isConnected = account.isConnected; if (isConnected) { paymentState.setConnectorChainType(account.chainType); } const payOrder = paymentState.payOrder; const paymentMethod = paymentState.paymentMethod; if (payOrder?.status !== PayOrderStatus.PENDING && payOrder?.status !== PayOrderStatus.AWAITING_PAYMENT && payOrder?.status !== PayOrderStatus.EXPIRED) { setRoute(ROUTES.CONFIRMATION); } else if (paymentMethod === PaymentMethod.Wallet && payOrder?.status === PayOrderStatus.AWAITING_PAYMENT && payOrder.payment?.src) { paymentState.setSelectedCurrencyOption(payOrder.payment.src); setRoute(ROUTES.PAY_WITH_TOKEN); } else if (paymentMethod === PaymentMethod.DepositAddress && payOrder?.status === PayOrderStatus.AWAITING_PAYMENT && payOrder.payment?.src) { paymentState.setPayToAddressChainId(payOrder.payment.src.chain_id); paymentState.setPayToAddressCurrency(payOrder.payment.src); setRoute(ROUTES.PAY_TO_ADDRESS); } else { setRoute(isConnected ? ROUTES.SELECT_TOKEN : ROUTES.SELECT_METHOD); } }, [account.isConnected, account.chainType, paymentState, setOpen]); useEffect(() => { if (payOrder?.status !== PayOrderStatus.PENDING && payOrder?.status !== PayOrderStatus.AWAITING_PAYMENT && payOrder?.status !== PayOrderStatus.EXPIRED) { setRoute(ROUTES.CONFIRMATION); } }, [payOrder?.status]); const triggerResize = useCallback(() => onResize((prev) => prev + 1), []); const value = { theme: ckTheme, setTheme, mode: ckMode, setMode, customTheme: ckCustomTheme, setCustomTheme, lang: ckLang, setLang, setOnOpen, setOnClose, open, setOpen, onSuccess, route, setRoute, // Other configuration options: opts, errorMessage, debugMode, log, displayError: (message, code) => { setErrorMessage(message); console.log("---------COINVOYAGE DEBUG---------"); if (code) console.table(code); console.log("---------/COINVOYAGE DEBUG---------"); }, resize, triggerResize, // Pay context showModal, paymentState, allowedWallets, }; return (_jsx(PayContext.Provider, { value: value, children: _jsxs(StyledThemeProvider, { theme: defaultTheme, children: [children, _jsx(PayModal, {})] }) })); }