import { Box } from '@biom3/react'; import EthereumProvider from '@walletconnect/ethereum-provider'; import { ChainId, CheckoutErrorType, EIP6963ProviderDetail, WrappedBrowserProvider, WalletProviderName, WalletProviderRdns, ThemeOverrides, } from '@imtbl/checkout-sdk'; import { useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react'; import { motion } from 'framer-motion'; import { useTranslation } from 'react-i18next'; import { UnableToConnectDrawer } from '../../../components/UnableToConnectDrawer/UnableToConnectDrawer'; import { ChangedYourMindDrawer } from '../../../components/ChangedYourMindDrawer/ChangedYourMindDrawer'; import { ConnectWidgetViews } from '../../../context/view-context/ConnectViewContextTypes'; import { ConnectActions, ConnectContext } from '../context/ConnectContext'; import { WalletItem } from './WalletItem'; import { ViewActions, ViewContext, } from '../../../context/view-context/ViewContext'; import { useWalletConnect } from '../../../lib/hooks/useWalletConnect'; import { useInjectedProviders } from '../../../lib/hooks/useInjectedProviders'; import { walletListStyle } from './WalletListStyles'; import { getProviderSlugFromRdns, isPassportProvider, } from '../../../lib/provider'; import { addProviderListenersForWidgetRoot } from '../../../lib'; import { listItemVariants, listVariants, } from '../../../lib/animation/listAnimation'; import { WalletDrawer } from '../../../components/WalletDrawer/WalletDrawer'; import { WalletChangeEvent } from '../../../components/WalletDrawer/WalletDrawerEvents'; import { WalletConnectItem } from './WalletConnectItem'; import { BrowserWalletItem } from './BrowserWalletItem'; import { NonPassportWarningDrawer } from './NonPassportWarningDrawer'; import { removeSpace } from '../../../lib/utils'; import { parseChainId } from '../../../lib/chains'; export interface WalletListProps { targetWalletRdns?: string; targetChainId: ChainId; allowedChains: ChainId[]; allowlistWalletRdns?: string[]; blocklistWalletRdns: string[]; isCheckNetworkEnabled: boolean; themeOverrides: ThemeOverrides; } // eslint-disable-next-line max-len const getAllowedProviders = (providers: EIP6963ProviderDetail[], allowlist: string[] | undefined, blocklist: string[]) => (allowlist ? providers.filter((provider) => !blocklist.includes(provider.info.rdns) && allowlist.includes(provider.info.rdns)) : providers.filter((provider) => !blocklist.includes(provider.info.rdns))); export function WalletList(props: WalletListProps) { const { t } = useTranslation(); const { targetWalletRdns, targetChainId, allowedChains, isCheckNetworkEnabled, themeOverrides, allowlistWalletRdns, blocklistWalletRdns, } = props; const { connectDispatch, connectState: { checkout }, } = useContext(ConnectContext); const { viewDispatch } = useContext(ViewContext); const { providers } = useInjectedProviders({ checkout }); const [showWalletDrawer, setShowWalletDrawer] = useState(false); const { isWalletConnectEnabled, openWalletConnectModal } = useWalletConnect(); const walletConnectItemRef = useRef(null); const [showChangedYourMindDrawer, setShowChangedYourMindDrawer] = useState(false); const [showUnableToConnectDrawer, setShowUnableToConnectDrawer] = useState(false); const [showNonPassportWarning, setShowNonPassportWarning] = useState(false); const [chosenProviderDetail, setChosenProviderDetail] = useState(); const allowedProviders = useMemo( () => getAllowedProviders(providers, allowlistWalletRdns, blocklistWalletRdns), [providers, allowlistWalletRdns, blocklistWalletRdns], ); const filteredProviders = useMemo( () => allowedProviders .filter( (provider) => !(provider.info.rdns === WalletProviderRdns.PASSPORT), ), [allowedProviders], ); // Don't allow Passport if targetChainId is L1 const passportProviderDetail = useMemo( () => targetChainId !== checkout!.config.l1ChainId && allowedProviders .find((provider) => provider.info.rdns === WalletProviderRdns.PASSPORT), [allowedProviders, checkout, targetChainId], ); const selectBrowserProvider = useCallback( (browserProvider: WrappedBrowserProvider, providerName: string) => { connectDispatch({ payload: { type: ConnectActions.SET_PROVIDER, provider: browserProvider, }, }); connectDispatch({ payload: { type: ConnectActions.SET_WALLET_PROVIDER_NAME, walletProviderName: providerName as WalletProviderName, }, }); }, [], ); const handleConnectViewUpdate = async (provider: WrappedBrowserProvider) => { const isPassport = isPassportProvider(provider); const chainId = await provider.send!('eth_chainId', []); const parsedChainId = parseChainId(chainId); if ( parsedChainId !== targetChainId && !(parsedChainId && allowedChains?.includes(parsedChainId)) ) { // TODO: What do we do with Passport here as it can't connect to L1 if (isPassport) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: ConnectWidgetViews.SUCCESS }, }, }); return; } if (isCheckNetworkEnabled) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: ConnectWidgetViews.SWITCH_NETWORK }, }, }); return; } } viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: ConnectWidgetViews.SUCCESS }, }, }); }; const selectProviderDetail = useCallback( async (providerDetail: EIP6963ProviderDetail) => { if (!checkout) return; try { const isMetaMask = providerDetail.info.rdns === WalletProviderRdns.METAMASK; const browserProvider = new WrappedBrowserProvider( providerDetail.provider, ); try { // TODO: Find a nice way to detect if the wallet supports switching accounts via requestPermissions const changeAccount = isMetaMask; const connectResult = await checkout.connect({ provider: browserProvider, requestWalletPermissions: changeAccount, }); // Set up EIP-1193 provider event listeners for widget root instances addProviderListenersForWidgetRoot(connectResult.provider); selectBrowserProvider( browserProvider, getProviderSlugFromRdns(providerDetail.info.rdns), ); await handleConnectViewUpdate(browserProvider); } catch (err: CheckoutErrorType | any) { if (err.type === CheckoutErrorType.USER_REJECTED_REQUEST_ERROR) { // eslint-disable-next-line no-console console.error('Connect rejected', err); setShowChangedYourMindDrawer(true); } else { // eslint-disable-next-line no-console console.error('Connect error', err); setShowUnableToConnectDrawer(true); } } } catch (err: any) { // eslint-disable-next-line no-console console.error('Connect unknown error', err); setShowUnableToConnectDrawer(true); } }, [checkout], ); const connectCallback = async (ethereumProvider: EthereumProvider) => { if (ethereumProvider.connected && ethereumProvider.session) { // Pass 'any' so ethers doesn't pin the provider to the initial WC chain. // Without 'any', the subsequent getNetwork() throws NETWORK_ERROR ("network changed: ..."). const browserProvider = new WrappedBrowserProvider(ethereumProvider, 'any'); selectBrowserProvider(browserProvider, 'walletconnect'); const { chainId } = await ((await browserProvider.getSigner()).provider.getNetwork()); if (ethereumProvider.chainId !== targetChainId) { // @ts-ignore allow protected method `switchEthereumChain` to be called await ethereumProvider.switchEthereumChain(targetChainId); } if (chainId as unknown as ChainId !== targetChainId) { viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: ConnectWidgetViews.SWITCH_NETWORK }, }, }); } viewDispatch({ payload: { type: ViewActions.UPDATE_VIEW, view: { type: ConnectWidgetViews.SUCCESS }, }, }); } }; const handleWalletConnectConnection = async () => { await openWalletConnectModal({ connectCallback, restoreSession: false, }); }; const handleWalletChange = async (event: WalletChangeEvent) => { const { providerDetail } = event; setChosenProviderDetail(providerDetail); await selectProviderDetail(providerDetail); setShowWalletDrawer(false); }; const handleWalletItemClick = useCallback( async (providerDetail: EIP6963ProviderDetail) => { const isPassport = providerDetail.info.rdns === WalletProviderRdns.PASSPORT; const hasSeenNonPassportWarning = localStorage.getItem( 'hasSeenNonPassportWarning', ); if (!isPassport && !hasSeenNonPassportWarning) { setChosenProviderDetail(providerDetail); setShowNonPassportWarning(true); return; } setShowChangedYourMindDrawer(false); setShowUnableToConnectDrawer(false); setChosenProviderDetail(providerDetail); await selectProviderDetail(providerDetail); }, [checkout], ); const onChosenProviderDetailChange = useCallback(() => { if (!chosenProviderDetail) return; handleWalletItemClick(chosenProviderDetail!); }, [chosenProviderDetail]); const onBrowserWalletsClick = useCallback(() => { setShowWalletDrawer(true); }, []); useEffect(() => { // Auto-trigger wallet connection via rdns if (targetWalletRdns && targetWalletRdns?.length > 0) { if ( targetWalletRdns === WalletProviderRdns.PASSPORT && passportProviderDetail ) { handleWalletItemClick(passportProviderDetail); } else if ( targetWalletRdns === WalletProviderRdns.WALLETCONNECT && walletConnectItemRef.current ) { (walletConnectItemRef.current as any).connect(); } else { const targetProviderDetail = filteredProviders.find( (providerDetail) => providerDetail.info.rdns === targetWalletRdns, ); if (targetProviderDetail) { handleWalletItemClick(targetProviderDetail); } } } }, [filteredProviders, targetWalletRdns]); const handleNonPassportWarningDrawerButtonClick = async () => { if (chosenProviderDetail) { localStorage.setItem('hasSeenNonPassportWarning', 'true'); setShowNonPassportWarning(false); await selectProviderDetail(chosenProviderDetail); } }; return ( } sx={walletListStyle} > {passportProviderDetail && ( handleWalletItemClick(passportProviderDetail)} providerInfo={passportProviderDetail.info} rc={( )} /> )} {filteredProviders.length === 1 && ( handleWalletItemClick(filteredProviders[0])} providerInfo={filteredProviders[0].info} rc={( )} /> )} {filteredProviders.length > 1 && ( )} {isWalletConnectEnabled && ( 0 ? 1 : 0) } key="walletconnect" style={{ width: '100%' }} > )} { setShowWalletDrawer(show); }} onWalletChange={handleWalletChange} drawerBackground={themeOverrides.drawerBackground} /> setShowChangedYourMindDrawer(false)} onTryAgain={onChosenProviderDetailChange} /> setShowUnableToConnectDrawer(false)} onTryAgain={() => setShowUnableToConnectDrawer(false)} /> setShowNonPassportWarning(false)} handleCtaButtonClick={handleNonPassportWarningDrawerButtonClick} /> ); }