UNPKG

@coin-voyage/paykit

Version:

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

276 lines (275 loc) 10.5 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useInWagmiContext } from "@coin-voyage/crypto/evm"; import { useAccount, useConnectCallback } from "@coin-voyage/crypto/hooks"; import { getDepositAddress, getPaymentStep } from "@coin-voyage/shared/payment"; import { PaymentMethod, PayOrderMode, PayOrderStatus } from "@coin-voyage/shared/types"; import { Buffer } from "buffer"; import { useCallback, useEffect, useMemo, 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 { ROUTE } 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 { account } = useAccount(); const { isConnected, chainType } = account; const [allowedWallets, setAllowedWallets] = useState(onConnectValidation ? [] : null); useConnectCallback({ onConnect, onDisconnect, onConnectValidation, setAllowedWallets, }); const opts = useMemo(() => ({ ...defaultOptions, ...options }), [options]); useEffect(() => { if (typeof window !== "undefined" && opts.bufferPolyfill) { window.Buffer = window.Buffer ?? Buffer; } }, [opts.bufferPolyfill]); useThemeFont(opts.embedGoogleFonts ? theme : null); const [ckTheme, setTheme] = useState(theme); const [ckMode, setMode] = useState(mode); const [ckCustomTheme, setCustomTheme] = useState(customTheme ?? {}); const [ckLang, setLang] = useState(opts.language || "en-US"); const [open, setOpenState] = useState(false); const [paymentCompleted, setPaymentCompleted] = useState(false); const [route, setRoute] = useState(ROUTE.SELECT_METHOD); const [modalOptions, setModalOptions] = useState(); const [errorMessage, setErrorMessage] = useState(null); const [resize, onResize] = useState(0); const onOpenRef = useRef(undefined); const onCloseRef = useRef(undefined); const openRef = useRef(false); useEffect(() => { openRef.current = open; }, [open]); const setOnOpen = useCallback((fn) => { onOpenRef.current = fn; }, []); const setOnClose = useCallback((fn) => { onCloseRef.current = fn; }, []); const log = useCallback((...args) => { if (debugMode) { console.log(...args); } }, [debugMode]); const [payOrder, setPayOrder] = useState(); const paymentState = usePaymentState({ payOrder, setPayOrder, setRoute, log, }); const paymentStateRef = useRef(paymentState); useEffect(() => { paymentStateRef.current = paymentState; }, [paymentState]); const { refreshOrder, setPayId, setConnectorChainType } = paymentState; useEffect(() => { setTheme(theme); }, [theme]); useEffect(() => { setMode(mode); }, [mode]); useEffect(() => { setCustomTheme(customTheme ?? {}); }, [customTheme]); useEffect(() => { setLang(opts.language || "en-US"); }, [opts.language]); useEffect(() => { setErrorMessage(null); }, [route, open]); const resetAfterClose = useCallback(() => { if (!paymentCompleted || !modalOptions?.resetOnSuccess) return; setPaymentCompleted(false); paymentStateRef.current.resetPaymentState(); }, [modalOptions?.resetOnSuccess, paymentCompleted]); const setOpen = useCallback((nextOpen) => { if (openRef.current === nextOpen) { return; } openRef.current = nextOpen; setOpenState(nextOpen); if (nextOpen) { onOpenRef.current?.(); return; } onCloseRef.current?.(); }, []); const onSuccess = useCallback(() => { setPaymentCompleted(true); if (modalOptions?.closeOnSuccess) { setTimeout(() => setOpen(false), 1000); } }, [modalOptions?.closeOnSuccess, setOpen]); // Keep paymentStateRef in sync to avoid stale closures useEffect(() => { paymentStateRef.current = paymentState; }, [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) return; const { payOrderId, chainType, route } = parsed; const timeoutId = setTimeout(() => { 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]); useEffect(() => { const intervalMs = getPollingIntervalMs(payOrder?.status, payOrder?.mode); if (!intervalMs) return; const timeoutId = setTimeout(refreshOrder, intervalMs); return () => clearTimeout(timeoutId); }, [payOrder?.mode, payOrder?.status, refreshOrder]); useEffect(() => { if (isFinalPayOrderStatus(payOrder?.status)) { setRoute(ROUTE.CONFIRMATION); } }, [payOrder?.status]); const showModal = useCallback((modalOptions) => { setModalOptions(modalOptions); setOpen(true); const { payOrder, paymentMethod } = paymentState; const status = payOrder?.status; const payment = payOrder?.payment; if (isConnected) { paymentState.setConnectorChainType(chainType); } if (isFinalPayOrderStatus(status)) { setRoute(ROUTE.CONFIRMATION); return; } if (status === PayOrderStatus.AWAITING_PAYMENT && payment) { if (paymentMethod === PaymentMethod.CARD) { setRoute(ROUTE.CARD_PAYMENT); return; } const step = getPaymentStep(payment); const isTransactionStep = step?.kind === "transaction"; if (paymentMethod === PaymentMethod.WALLET || isTransactionStep) { paymentState.setSelectedCurrencyOption(payment.src); setRoute(ROUTE.WALLET_PAYMENT); return; } const depositAddress = getDepositAddress(payment); if (paymentMethod === PaymentMethod.DEPOSIT_ADDRESS && depositAddress) { paymentState.setPayToAddressChainId(payment.src.chain_id); paymentState.setPayToAddressCurrency(payment.src); setRoute(ROUTE.PAY_TO_ADDRESS); return; } } setRoute(isConnected ? ROUTE.WALLET_TOKEN_SELECT : ROUTE.SELECT_METHOD); }, [isConnected, chainType, paymentState, setOpen]); const triggerResize = useCallback(() => onResize((prev) => prev + 1), []); const displayError = useCallback((message, code) => { setErrorMessage(message); if (!debugMode) return; console.log("---------COINVOYAGE DEBUG---------"); if (code) { console.table(code); } console.log("---------/COINVOYAGE DEBUG---------"); }, [debugMode]); 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, resize, triggerResize, // Pay context showModal, paymentState, allowedWallets, }; return (_jsx(PayContext.Provider, { value: value, children: _jsxs(StyledThemeProvider, { theme: defaultTheme, children: [children, _jsx(PayModal, { onExited: resetAfterClose })] }) })); } // === Helper functions === const NON_FINAL_PAY_ORDER_STATUSES = [PayOrderStatus.PENDING, PayOrderStatus.AWAITING_PAYMENT, PayOrderStatus.EXPIRED]; const isFinalPayOrderStatus = (status) => !!status && !NON_FINAL_PAY_ORDER_STATUSES.includes(status); const getPollingIntervalMs = (status, mode) => { if (!status) return null; if (status === PayOrderStatus.AWAITING_PAYMENT) { return 5000; } if ([PayOrderStatus.AWAITING_CONFIRMATION, PayOrderStatus.OPTIMISTIC_CONFIRMED].includes(status)) { return 2500; } if (status === PayOrderStatus.EXECUTING_ORDER) { return mode === PayOrderMode.DEPOSIT ? 1000 : 2500; } return null; };