Skip to content

fixes metamask autoconnect in mobile #2241

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jul 23, 2025
31 changes: 2 additions & 29 deletions packages/modal/src/modalManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -603,9 +603,8 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal {
}): Promise<void> => {
try {
const connector = this.getConnector(params.connector as WALLET_CONNECTOR_TYPE, params.loginParams?.chainNamespace);
// auto-connect WalletConnect and non-injected MetaMask in background to generate QR code URI without interfering with user's selected connection
const shouldStartConnectionInBackground =
connector.name === WALLET_CONNECTORS.WALLET_CONNECT_V2 || (connector.name === WALLET_CONNECTORS.METAMASK && !connector.isInjected);
// auto-connect WalletConnect in background to generate QR code URI without interfering with user's selected connection
const shouldStartConnectionInBackground = connector.name === WALLET_CONNECTORS.WALLET_CONNECT_V2;
if (shouldStartConnectionInBackground) {
const initialChain = this.getInitialChainIdForConnector(connector);
await connector.connect({ chainId: initialChain.chainId });
Expand Down Expand Up @@ -646,32 +645,6 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal {
wcConnector.status = CONNECTOR_STATUS.READY;
}
}

// handle MM session refresh if MM is not injected
const metamaskConnector = this.getConnector(WALLET_CONNECTORS.METAMASK);
if (metamaskConnector && !metamaskConnector.isInjected) {
const status = metamaskConnector?.status;
log.debug("trying refreshing MM session", visibility, status);
if (visibility && (status === CONNECTOR_STATUS.READY || status === CONNECTOR_STATUS.CONNECTING)) {
log.debug("refreshing MM session");

// refreshing session for MM whenever modal is opened.
try {
const initialChain = this.getInitialChainIdForConnector(metamaskConnector);
metamaskConnector.connect({ chainId: initialChain.chainId });
} catch (error) {
log.error(`Error while connecting to MM`, error);
}
}
if (
!visibility &&
this.status === CONNECTOR_STATUS.CONNECTED &&
(status === CONNECTOR_STATUS.READY || status === CONNECTOR_STATUS.CONNECTING)
) {
log.debug("this stops MM connector from trying to reconnect once proposal expires");
metamaskConnector.status = CONNECTOR_STATUS.READY;
}
}
};

private getChainNamespaces = (): ChainNamespaceType[] => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function ButtonWallet(props: ButtonWalletProps) {
const isDark = useContext(ThemedContext);

const isLink = useMemo(
() => deviceDetails.platform !== "desktop" && button.href && button.hasWalletConnect && !button.hasInjectedWallet,
() => deviceDetails.platform !== "desktop" && button.href && button.hasWalletConnect && !button.isInstalled,
[deviceDetails, button]
);

Expand Down
83 changes: 51 additions & 32 deletions packages/modal/src/ui/components/ConnectWallet/ConnectWallet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ function ConnectWallet(props: ConnectWalletProps) {
return walletDiscoverySupported ? defaultButtons.length : installedWalletButtons.length;
}, [walletDiscoverySupported, defaultButtons, installedWalletButtons, isShowAllWallets, totalExternalWalletsCount]);

/**
* Wallet click logic
* - For installed wallets
* - For MetaMask non-injected on desktop, show QR code for connection
* - Ask user to select a chain namespace if it has multiple namespaces
* - Otherwise, use their connectors to connect
* - For wallet-discovery wallets (not installed)
* - On desktop, show QR code for connection if wallet connect v2 is supported, otherwise show install links
* - On mobile, open deeplink with wallet connect uri (won't go into this function as it'll open the deeplink)
*/
const handleWalletClick = (button: ExternalButton) => {
analytics?.track(ANALYTICS_EVENTS.EXTERNAL_WALLET_SELECTED, {
connector: button.isInstalled ? button.name : button.hasWalletConnect ? WALLET_CONNECTORS.WALLET_CONNECT_V2 : "",
Expand All @@ -165,41 +175,50 @@ function ConnectWallet(props: ConnectWalletProps) {
total_external_wallets: allUniqueButtons.length,
});

// show chain namespace selector if the button is an injected connector with multiple chain namespaces
const isChainNamespaceSelectorRequired = button.hasInjectedWallet && button.chainNamespaces?.length > 1;
if (isChainNamespaceSelectorRequired) {
setBodyState({
...bodyState,
multiChainSelector: {
show: true,
wallet: button,
},
});
return;
}
// for installed wallets
if (button.isInstalled) {
// for MetaMask non-injected on desktop, show QR code for connection
if (button.name === WALLET_CONNECTORS.METAMASK && !button.hasInjectedWallet && deviceDetails.platform === "desktop") {
handleExternalWalletClick({ connector: button.name });
setSelectedButton(button);
setSelectedWallet(true);
setCurrentPage(CONNECT_WALLET_PAGES.SELECTED_WALLET);
handleWalletDetailsHeight();
return;
}

// connect with connector if injected and single chain namespace or custom connector (except MetaMask)
const isInjectedConnectorAndSingleChainNamespace = button.hasInjectedWallet && button.chainNamespaces?.length === 1;
const isCustomConnector = !button.hasInjectedWallet && button.isInstalled;
if (isInjectedConnectorAndSingleChainNamespace || (isCustomConnector && button.name !== WALLET_CONNECTORS.METAMASK)) {
return handleExternalWalletClick({ connector: button.name });
}
// show chain namespace selector if the button has multiple chain namespaces
if (button.chainNamespaces?.length > 1) {
setBodyState({
...bodyState,
multiChainSelector: {
show: true,
wallet: button,
},
});
return;
}

// show QR code for wallet connect v2 and MM (non-injected)
if (button.hasWalletConnect) {
setSelectedButton(button);
setSelectedWallet(true);
setCurrentPage(CONNECT_WALLET_PAGES.SELECTED_WALLET);
handleWalletDetailsHeight();
// otherwise, use their connectors to connect
handleExternalWalletClick({ connector: button.name });
return;
} else {
// show install links
setBodyState({
...bodyState,
installLinks: {
show: true,
wallet: button,
},
});
// show QR code if wallet connect v2 is supported
if (button.hasWalletConnect) {
setSelectedButton(button);
setSelectedWallet(true);
setCurrentPage(CONNECT_WALLET_PAGES.SELECTED_WALLET);
handleWalletDetailsHeight();
} else {
// otherwise, show install links
setBodyState({
...bodyState,
installLinks: {
show: true,
wallet: button,
},
});
}
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const ConnectWalletChainNamespaceSelect = (props: ConnectWalletChainNamespaceSel
const { isDark, wallet, handleExternalWalletClick } = props;
const [t] = useTranslation(undefined, { i18n });

const chainNamespaces = wallet.chainNamespaces!.map((chainNamespace) => {
const chainNamespaces = wallet.chainNamespaces!.sort().map((chainNamespace) => {
const imageId = chainNamespace === "eip155" ? "evm" : chainNamespace;
const displayName = chainNamespace === "eip155" ? "EVM" : chainNamespace;
return {
Expand Down
13 changes: 11 additions & 2 deletions packages/modal/src/ui/components/Login/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ function Login(props: LoginProps) {
showInstalledExternalWallets,
logoAlignment = "center",
buttonRadius = "pill",
deviceDetails,
} = props;

const [t] = useTranslation(undefined, { i18n });
Expand Down Expand Up @@ -342,6 +343,12 @@ function Login(props: LoginProps) {
}
};

/**
* Installed wallet click logic:
* - For MetaMask: If not injected and on desktop, display QR code for connection.
* - If wallet supports multiple chain namespaces, prompt user to select a chain.
* - Otherwise, connect directly using the wallet connector.
*/
const handleInstalledWalletClick = (wallet: ExternalButton) => {
analytics?.track(ANALYTICS_EVENTS.EXTERNAL_WALLET_SELECTED, {
connector: wallet.name,
Expand All @@ -354,8 +361,10 @@ function Login(props: LoginProps) {
has_wallet_registry_item: !!wallet.walletRegistryItem,
total_external_wallets: totalExternalWallets,
});
// for non-injected Metamask, show QR code to connect
if (wallet.name === WALLET_CONNECTORS.METAMASK && !wallet.hasInjectedWallet) {
// for non-injected Metamask on desktop, show QR code to connect
if (wallet.name === WALLET_CONNECTORS.METAMASK && !wallet.hasInjectedWallet && deviceDetails.platform === "desktop") {
handleExternalWalletClick({ connector: wallet.name });
// We should show QR code only if the wallet is not installed.
setBodyState({
...bodyState,
metamaskQrCode: {
Expand Down
4 changes: 4 additions & 0 deletions packages/modal/src/ui/components/Login/Login.type.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { BUILD_ENV_TYPE, WEB3AUTH_NETWORK_TYPE } from "@web3auth/auth";

import type {
browser,
ButtonRadiusType,
ExternalButton,
ExternalWalletEventType,
LogoAlignmentType,
os,
platform,
SocialLoginEventType,
SocialLoginsConfig,
} from "../../interfaces";
Expand All @@ -31,6 +34,7 @@ export interface LoginProps {
totalExternalWallets: number;
logoAlignment?: LogoAlignmentType;
buttonRadius?: ButtonRadiusType;
deviceDetails: { platform: platform; browser: browser; os: os };
handleExternalWalletBtnClick?: (flag: boolean) => void;
handleSocialLoginClick: (params: SocialLoginEventType) => void;
handleExternalWalletClick: (params: ExternalWalletEventType) => void;
Expand Down
27 changes: 5 additions & 22 deletions packages/modal/src/ui/components/Root/Root.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { WALLET_CONNECTORS, type WalletRegistryItem } from "@web3auth/no-modal";
import Bowser from "bowser";
import { JSX, useCallback, useContext, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";

import { CONNECT_WALLET_PAGES, DEFAULT_METAMASK_WALLET_REGISTRY_ITEM, PAGES } from "../../constants";
import { BodyState, RootContext } from "../../context/RootContext";
import { ThemedContext } from "../../context/ThemeContext";
import { browser, ExternalButton, mobileOs, MODAL_STATUS, os, platform, TOAST_TYPE, ToastType } from "../../interfaces";
import { ExternalButton, mobileOs, MODAL_STATUS, TOAST_TYPE, ToastType } from "../../interfaces";
import i18n from "../../localeImport";
import { cn, getBrowserExtensionUrl, getBrowserName, getIcons, getMobileInstallLink, getOsName } from "../../utils";
import BottomSheet from "../BottomSheet";
Expand Down Expand Up @@ -42,6 +41,7 @@ function Root(props: RootProps) {
isSmsPasswordLessLoginVisible,
preHandleExternalWalletClick,
uiConfig,
deviceDetails,
} = props;

const {
Expand Down Expand Up @@ -99,16 +99,6 @@ function Root(props: RootProps) {
};

// Wallet Details
const deviceDetails = useMemo<{ platform: platform; browser: browser; os: mobileOs }>(() => {
if (typeof window === "undefined") return { platform: "mobile", browser: "chrome", os: "ios" };
const browserData = Bowser.getParser(window.navigator.userAgent);
return {
platform: browserData.getPlatformType() as platform,
browser: browserData.getBrowserName().toLowerCase() as browser,
os: browserData.getOSName() as mobileOs,
};
}, []);

const mobileInstallLinks = useMemo<JSX.Element[]>(() => {
if (deviceDetails.platform === "desktop") return [];
const installConfig = bodyState.installLinks?.wallet?.walletRegistryItem?.app || {};
Expand Down Expand Up @@ -424,12 +414,8 @@ function Root(props: RootProps) {
);

const isShowLoader = useMemo(() => {
// don't show loader if metamask is connecting and there is a connect uri
if (modalState.detailedLoaderConnector === WALLET_CONNECTORS.METAMASK && modalState.metamaskConnectUri) {
return false;
}
return modalState.status !== MODAL_STATUS.INITIALIZED;
}, [modalState.detailedLoaderConnector, modalState.metamaskConnectUri, modalState.status]);
}, [modalState.status]);

return (
<RootContext.Provider value={contextValue}>
Expand Down Expand Up @@ -494,6 +480,7 @@ function Root(props: RootProps) {
totalExternalWallets={totalExternalWallets}
logoAlignment={logoAlignment}
buttonRadius={buttonRadiusType}
deviceDetails={deviceDetails}
handleSocialLoginClick={handleSocialLoginClick}
handleExternalWalletBtnClick={onExternalWalletBtnClick}
handleSocialLoginHeight={handleSocialLoginHeight}
Expand All @@ -511,11 +498,7 @@ function Root(props: RootProps) {
allExternalButtons={allButtons}
connectorVisibilityMap={connectorVisibilityMap}
customConnectorButtons={customConnectorButtons}
deviceDetails={{
platform: deviceDetails.platform,
browser: deviceDetails.browser,
os: deviceDetails.os as os,
}}
deviceDetails={deviceDetails}
chainNamespace={chainNamespaces}
buttonRadius={buttonRadiusType}
handleWalletDetailsHeight={handleWalletDetailsHeight}
Expand Down
3 changes: 2 additions & 1 deletion packages/modal/src/ui/components/Root/Root.type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ChainNamespaceType, WalletRegistry } from "@web3auth/no-modal";

import { ModalState, SocialLoginEventType, SocialLoginsConfig, UIConfig } from "../../interfaces";
import { browser, ModalState, os, platform, SocialLoginEventType, SocialLoginsConfig, UIConfig } from "../../interfaces";

export interface RootProps {
appLogo?: string;
Expand All @@ -18,6 +18,7 @@ export interface RootProps {
isEmailPasswordLessLoginVisible: boolean;
isSmsPasswordLessLoginVisible: boolean;
uiConfig: UIConfig;
deviceDetails: { platform: platform; browser: browser; os: os };
handleSocialLoginClick: (params: SocialLoginEventType) => void;
handleExternalWalletBtnClick?: (flag: boolean) => void;
preHandleExternalWalletClick: (params: { connector: string; chainNamespace?: ChainNamespaceType }) => void;
Expand Down
10 changes: 3 additions & 7 deletions packages/modal/src/ui/components/Widget/Widget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ function Widget(props: WidgetProps) {
chainNamespaces,
walletRegistry,
uiConfig,
deviceDetails,
} = props;

const { widgetType } = uiConfig;
Expand Down Expand Up @@ -183,13 +184,6 @@ function Widget(props: WidgetProps) {
if (wcAvailable && !modalState.walletConnectUri && typeof handleExternalWalletClick === "function") {
handleExternalWalletClick({ connector: WALLET_CONNECTORS.WALLET_CONNECT_V2 });
}

// auto connect to MetaMask if not injected to generate QR code URI for mobile connection
const mmAvailable =
modalState.externalWalletsConfig[WALLET_CONNECTORS.METAMASK] && !modalState.externalWalletsConfig[WALLET_CONNECTORS.METAMASK]?.isInjected;
if (mmAvailable && !modalState.metamaskConnectUri && typeof handleExternalWalletClick === "function") {
handleExternalWalletClick({ connector: WALLET_CONNECTORS.METAMASK });
}
}
}, [modalState, handleExternalWalletClick]);

Expand Down Expand Up @@ -226,6 +220,7 @@ function Widget(props: WidgetProps) {
isEmailPasswordLessLoginVisible={isEmailPasswordLessLoginVisible}
isSmsPasswordLessLoginVisible={isSmsPasswordLessLoginVisible}
uiConfig={uiConfig}
deviceDetails={deviceDetails}
/>
)}
</Modal>
Expand Down Expand Up @@ -257,6 +252,7 @@ function Widget(props: WidgetProps) {
isEmailPasswordLessLoginVisible={isEmailPasswordLessLoginVisible}
isSmsPasswordLessLoginVisible={isSmsPasswordLessLoginVisible}
uiConfig={uiConfig}
deviceDetails={deviceDetails}
/>
)}
</Embed>
Expand Down
5 changes: 3 additions & 2 deletions packages/modal/src/ui/components/Widget/Widget.type.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import { SafeEventEmitter } from "@web3auth/auth";
import { ChainNamespaceType, WalletRegistry } from "@web3auth/no-modal";

import { ExternalWalletEventType, SocialLoginEventType, StateEmitterEvents, UIConfig } from "../../interfaces";
import { browser, ExternalWalletEventType, os, platform, SocialLoginEventType, StateEmitterEvents, UIConfig } from "../../interfaces";

export interface WidgetProps {
stateListener: SafeEventEmitter<StateEmitterEvents>;
appLogo?: string;
appName?: string;
chainNamespaces: ChainNamespaceType[];
walletRegistry?: WalletRegistry;
uiConfig: UIConfig;
deviceDetails: { platform: platform; browser: browser; os: os };
handleSocialLoginClick: (params: SocialLoginEventType) => void;
handleExternalWalletClick: (params: ExternalWalletEventType) => void;
handleShowExternalWallets: (externalWalletsInitialized: boolean) => void;
closeModal: () => void;
uiConfig: UIConfig;
}
Loading