@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
JavaScript
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, {})] }) }));
}