@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
JavaScript
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;
};