From 387df147d94e9c200322f5b66daa0e72faa64d09 Mon Sep 17 00:00:00 2001 From: Lionell Briones Date: Thu, 30 Apr 2026 10:32:34 +0800 Subject: [PATCH 1/7] show linking and authorizing state to modal ui --- packages/modal/src/modalManager.ts | 58 ++++++++++++++++--- packages/modal/src/ui/loginModal.tsx | 44 ++++++++++++++ .../auth-connector/authConnector.ts | 5 ++ 3 files changed, 98 insertions(+), 9 deletions(-) diff --git a/packages/modal/src/modalManager.ts b/packages/modal/src/modalManager.ts index 7b9bc86b8..729f9a7ea 100644 --- a/packages/modal/src/modalManager.ts +++ b/packages/modal/src/modalManager.ts @@ -282,10 +282,12 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal { switchResult.kind === "external" && !isExistingConnectorConnected ? (await this.getProjectAndWalletConfig())?.projectConfig : undefined; if (switchResult.kind !== "external" || isExistingConnectorConnected) { - await super.processSwitchAccountResult(authConnector, switchResult, { - walletConnector: isExistingConnectorConnected && existingConnector ? existingConnector : undefined, - projectConfig, - }); + await this.runNonWalletConnectAccountAction(switchResult.targetAccount.connector, () => + super.processSwitchAccountResult(authConnector, switchResult, { + walletConnector: isExistingConnectorConnected && existingConnector ? existingConnector : undefined, + projectConfig, + }) + ); await authConnector.trackSwitchAccountCompleted(switchResult.targetAccount); return; } @@ -297,10 +299,12 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal { ); if (connectorToSwitchTo.name !== WALLET_CONNECTORS.WALLET_CONNECT_V2) { - await super.processSwitchAccountResult(authConnector, switchResult, { - walletConnector: connectorToSwitchTo, - projectConfig, - }); + await this.runNonWalletConnectAccountAction(switchResult.targetAccount.connector, () => + super.processSwitchAccountResult(authConnector, switchResult, { + walletConnector: connectorToSwitchTo, + projectConfig, + }) + ); await authConnector.trackSwitchAccountCompleted(switchResult.targetAccount); return; } @@ -317,7 +321,11 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal { const connectorToLink = await this.prepareAccountLinkingConnector(params.connectorName, chainId); if (connectorToLink.name !== WALLET_CONNECTORS.WALLET_CONNECT_V2) { - return super.linkAccountWithConnector(params.connectorName, chainId, connectorToLink); + return this.runNonWalletConnectAccountAction( + params.connectorName, + () => super.linkAccountWithConnector(params.connectorName, chainId, connectorToLink), + { connector: connectorToLink } + ); } return this.runWalletConnectV2AccountAction({ @@ -1019,6 +1027,38 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal { } } + private async runNonWalletConnectAccountAction( + connectorName: WALLET_CONNECTOR_TYPE | string, + fn: () => Promise, + options: { connector?: IConnector } = {} + ): Promise { + if (!this.loginModal) throw WalletInitializationError.notReady("Login modal is not initialized"); + const displayName = CONNECTOR_NAMES[connectorName as WALLET_CONNECTOR_TYPE] || connectorName; + this.loginModal.startConnectingLoader({ connector: connectorName, connectorName: displayName }); + + // Forward the AUTHORIZING event from the linking wallet connector to the modal so we can + // swap the loader from "connecting" to "authorizing" while the user reviews the signature + // request inside their wallet. + const handleAuthorizing = () => this.loginModal?.markLoaderAuthorizing(); + if (options.connector) { + options.connector.on(CONNECTOR_EVENTS.AUTHORIZING, handleAuthorizing); + } + + try { + const result = await fn(); + this.loginModal.endConnectingLoader({ success: true }); + return result; + } catch (error) { + const message = (error as Error)?.message; + this.loginModal.endConnectingLoader({ success: false, errorMessage: message }); + throw error; + } finally { + if (options.connector) { + options.connector.removeListener(CONNECTOR_EVENTS.AUTHORIZING, handleAuthorizing); + } + } + } + private subscribeToAccountLinkingConnectorEvents(connector: IConnector): () => void { const handleConnecting = () => { this.updateAccountLinkingModalSession({ diff --git a/packages/modal/src/ui/loginModal.tsx b/packages/modal/src/ui/loginModal.tsx index c92899ca3..20f5f774d 100644 --- a/packages/modal/src/ui/loginModal.tsx +++ b/packages/modal/src/ui/loginModal.tsx @@ -358,6 +358,50 @@ export class LoginModal { return this.accountLinkingState.active; }; + startConnectingLoader = (params: { connector: WALLET_CONNECTOR_TYPE | string; connectorName: string }): void => { + this.setState({ + status: MODAL_STATUS.CONNECTING, + modalVisibility: true, + detailedLoaderConnector: params.connector, + detailedLoaderConnectorName: params.connectorName, + }); + if (this.callbacks.onModalVisibility) { + this.callbacks.onModalVisibility(true); + } + }; + + markLoaderAuthorizing = (): void => { + this.setState({ status: MODAL_STATUS.AUTHORIZING }); + }; + + endConnectingLoader = (params: { success: boolean; errorMessage?: string }): void => { + if (params.success) { + this.setState({ + status: MODAL_STATUS.CONNECTED, + postLoadingMessage: "modal.post-loading.connected", + }); + // Loader auto-closes after 1s on CONNECTED for non CONNECT_AND_SIGN modes. + // For CONNECT_AND_SIGN, the Loader does not auto-close on CONNECTED, so close explicitly. + if (this.uiConfig.initialAuthenticationMode === CONNECTOR_INITIAL_AUTHENTICATION_MODE.CONNECT_AND_SIGN) { + const delay = this.uiConfig.hideSuccessScreen ? 0 : 1000; + setTimeout(() => this.closeModal(), delay); + } + return; + } + + if (this.uiConfig.displayErrorsOnModal) { + this.setState({ + modalVisibility: true, + status: MODAL_STATUS.ERRORED, + postLoadingMessage: params.errorMessage || "modal.post-loading.something-wrong", + }); + } else { + this.setState({ + modalVisibility: false, + }); + } + }; + open = () => { this.setState({ modalVisibility: true, diff --git a/packages/no-modal/src/connectors/auth-connector/authConnector.ts b/packages/no-modal/src/connectors/auth-connector/authConnector.ts index 13b9aaee6..aad2a0c6b 100644 --- a/packages/no-modal/src/connectors/auth-connector/authConnector.ts +++ b/packages/no-modal/src/connectors/auth-connector/authConnector.ts @@ -728,6 +728,11 @@ class AuthConnector extends BaseConnector implements IAuthConne signatureType: "eip191" | "sip99"; network: "ethereum" | "solana"; }> { + // Notify listeners that the linking wallet is about to be asked for a signature so the UI + // (e.g. modal) can switch from a "connecting" loader to an "authorizing" prompt while the + // user reviews the signature request inside their wallet. Emitted on the isolated wallet + // connector (not the auth connector) so it doesn't mutate the global SDK status. + connector.emit(CONNECTOR_EVENTS.AUTHORIZING, { connector: connector.name as WALLET_CONNECTOR_TYPE }); const { challenge, signature, chainNamespace } = await connector.generateChallengeAndSign(); const address = await this.getLinkingWalletAddress(connector, chainNamespace); From ce2ca215f01f90c44af7283d875262e988599396 Mon Sep 17 00:00:00 2001 From: Lionell Briones Date: Thu, 30 Apr 2026 13:59:52 +0800 Subject: [PATCH 2/7] account linking wallet picker --- .../src/components/AccountLinkingSection.vue | 10 ++ packages/modal/src/modalManager.ts | 91 +++++++++++++++---- .../modal/src/ui/containers/Root/Root.tsx | 3 +- packages/modal/src/ui/interfaces.ts | 7 ++ packages/modal/src/ui/loginModal.tsx | 24 +++++ .../src/base/account-linking/interfaces.ts | 8 +- packages/no-modal/src/base/core/IWeb3Auth.ts | 2 +- packages/no-modal/src/noModal.ts | 5 +- .../src/react/hooks/useLinkAccount.ts | 4 +- .../src/vue/composables/useLinkAccount.ts | 4 +- 10 files changed, 132 insertions(+), 26 deletions(-) diff --git a/demo/vue-app-new/src/components/AccountLinkingSection.vue b/demo/vue-app-new/src/components/AccountLinkingSection.vue index 4c3b36b94..c82fc2981 100644 --- a/demo/vue-app-new/src/components/AccountLinkingSection.vue +++ b/demo/vue-app-new/src/components/AccountLinkingSection.vue @@ -46,6 +46,15 @@ const onLinkAccount = async () => { } }; +const onLinkAccountWithPicker = async () => { + lastUnlinkedAddress.value = null; + const result = await linkAccount(); + if (result) { + await props.refreshUserInfo(); + props.printToConsole("Link Wallet (picker) Result", result); + } +}; + const onUnlinkAccount = async (address: string) => { lastUnlinkedAddress.value = null; pendingUnlinkAddress.value = address; @@ -177,5 +186,6 @@ const formatAddress = (address?: string | null): string => { + diff --git a/packages/modal/src/modalManager.ts b/packages/modal/src/modalManager.ts index 729f9a7ea..00ca19860 100644 --- a/packages/modal/src/modalManager.ts +++ b/packages/modal/src/modalManager.ts @@ -89,6 +89,10 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal { private removeAccountLinkingResetOnCloseListener: (() => void) | null = null; + private accountLinkingPickerResolver: ((connectorName: WALLET_CONNECTOR_TYPE | string) => void) | null = null; + + private removeAccountLinkingPickerCloseListener: (() => void) | null = null; + constructor(options: Web3AuthOptions, initialState?: Partial) { super(options, initialState); this.options = { ...options }; @@ -316,26 +320,18 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal { } } - public async linkAccount(params: LinkAccountParams): Promise { - const chainId = this.resolveLinkAccountChainId(params.chainId); - const connectorToLink = await this.prepareAccountLinkingConnector(params.connectorName, chainId); + public async linkAccount(params?: LinkAccountParams): Promise { + // Pre-flight: ensure user is connected via AUTH so we fail fast before opening the modal. + this.getMainAuthConnector(); - if (connectorToLink.name !== WALLET_CONNECTORS.WALLET_CONNECT_V2) { - return this.runNonWalletConnectAccountAction( - params.connectorName, - () => super.linkAccountWithConnector(params.connectorName, chainId, connectorToLink), - { connector: connectorToLink } - ); + const chainId = this.resolveLinkAccountChainId(params?.chainId); + + if (!params?.connectorName) { + const pickedConnector = await this.pickWalletForAccountLinking(chainId); + return this.linkAccountWithChosenConnector(pickedConnector, chainId); } - return this.runWalletConnectV2AccountAction({ - connector: connectorToLink, - connectParams: { chainId, isAccountLinking: true }, - onConnected: async (connector) => ({ - result: await super.linkAccountWithConnector(params.connectorName, chainId, connector), - }), - intent: ACCOUNT_LINKING_INTENT.LINK, - }); + return this.linkAccountWithChosenConnector(params.connectorName, chainId); } protected startAccountLinkingModalSession(params: { @@ -827,6 +823,13 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal { connector: WALLET_CONNECTOR_TYPE | string; loginParams: { chainNamespace: ChainNamespaceType }; }): Promise => { + // If the modal is in account-linking picker mode, hand off the selected connector + // to the linking flow instead of running the regular login `connectTo`. + if (this.accountLinkingPickerResolver) { + const resolver = this.accountLinkingPickerResolver; + resolver(params.connector); + return; + } try { const connector = this.getConnector(params.connector as WALLET_CONNECTOR_TYPE, params.loginParams?.chainNamespace); // auto-connect WalletConnect in background to generate QR code URI without interfering with user's selected connection @@ -1027,6 +1030,60 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal { } } + private async linkAccountWithChosenConnector(connectorName: WALLET_CONNECTOR_TYPE | string, chainId: string): Promise { + const connectorToLink = await this.prepareAccountLinkingConnector(connectorName, chainId); + + if (connectorToLink.name !== WALLET_CONNECTORS.WALLET_CONNECT_V2) { + return this.runNonWalletConnectAccountAction(connectorName, () => super.linkAccountWithConnector(connectorName, chainId, connectorToLink), { + connector: connectorToLink, + }); + } + + return this.runWalletConnectV2AccountAction({ + connector: connectorToLink, + connectParams: { chainId, isAccountLinking: true }, + onConnected: async (connector) => ({ + result: await super.linkAccountWithConnector(connectorName, chainId, connector), + }), + intent: ACCOUNT_LINKING_INTENT.LINK, + }); + } + + private pickWalletForAccountLinking(chainId: string): Promise { + if (!this.loginModal) throw WalletInitializationError.notReady("Login modal is not initialized"); + if (this.accountLinkingPickerResolver) { + throw AccountLinkingError.requestFailed("Another account linking picker is already in progress."); + } + + this.loginModal.startAccountLinkingPicker({ chainId }); + + return new Promise((resolve, reject) => { + const cleanup = () => { + this.accountLinkingPickerResolver = null; + if (this.removeAccountLinkingPickerCloseListener) { + this.removeAccountLinkingPickerCloseListener(); + this.removeAccountLinkingPickerCloseListener = null; + } + this.loginModal.endAccountLinkingPicker(); + }; + + const handleVisibility = (visible: boolean) => { + if (visible) return; + cleanup(); + reject(WalletLoginError.popupClosed("User closed the modal")); + }; + + this.accountLinkingPickerResolver = (connector) => { + cleanup(); + resolve(connector); + }; + this.on(LOGIN_MODAL_EVENTS.MODAL_VISIBILITY, handleVisibility); + this.removeAccountLinkingPickerCloseListener = () => { + this.removeListener(LOGIN_MODAL_EVENTS.MODAL_VISIBILITY, handleVisibility); + }; + }); + } + private async runNonWalletConnectAccountAction( connectorName: WALLET_CONNECTOR_TYPE | string, fn: () => Promise, diff --git a/packages/modal/src/ui/containers/Root/Root.tsx b/packages/modal/src/ui/containers/Root/Root.tsx index c0ddf6c3f..09998fdf4 100644 --- a/packages/modal/src/ui/containers/Root/Root.tsx +++ b/packages/modal/src/ui/containers/Root/Root.tsx @@ -333,6 +333,7 @@ function RootContent(props: RootProps) { )} {/* Login Screen */} {!isWalletConnectAccountLinkingVisible && + !modalState.accountLinking.pickerActive && modalState.currentPage === PAGES.LOGIN_OPTIONS && shouldShowLoginPage && modalState.status === MODAL_STATUS.INITIALIZED && ( @@ -345,7 +346,7 @@ function RootContent(props: RootProps) { {/* Connect Wallet Screen */} {!isWalletConnectAccountLinkingVisible && modalState.currentPage === PAGES.WALLET_LIST && - (!shouldShowLoginPage || isExternalWalletModeOnly) && + (!shouldShowLoginPage || isExternalWalletModeOnly || modalState.accountLinking.pickerActive) && modalState.status === MODAL_STATUS.INITIALIZED && ( { + this.accountLinkingState = { + ...DEFAULT_ACCOUNT_LINKING_STATE, + active: true, + pickerActive: true, + chainId: params.chainId, + intent: ACCOUNT_LINKING_INTENT.LINK, + }; + this.setState({ + accountLinking: this.accountLinkingState, + currentPage: PAGES.WALLET_LIST, + modalVisibility: true, + status: MODAL_STATUS.INITIALIZED, + }); + if (this.callbacks.onModalVisibility) { + this.callbacks.onModalVisibility(true); + } + }; + + endAccountLinkingPicker = (): void => { + this.accountLinkingState = { ...this.accountLinkingState, pickerActive: false }; + this.setState({ accountLinking: this.accountLinkingState }); + }; + startConnectingLoader = (params: { connector: WALLET_CONNECTOR_TYPE | string; connectorName: string }): void => { this.setState({ status: MODAL_STATUS.CONNECTING, diff --git a/packages/no-modal/src/base/account-linking/interfaces.ts b/packages/no-modal/src/base/account-linking/interfaces.ts index 8cd581b80..e1d67894c 100644 --- a/packages/no-modal/src/base/account-linking/interfaces.ts +++ b/packages/no-modal/src/base/account-linking/interfaces.ts @@ -3,9 +3,13 @@ import { WALLET_CONNECTOR_TYPE } from "../wallet"; export interface LinkAccountParams { /** * Name of the external wallet connector to link. - * Example: WALLET_CONNECTORS.METAMASK, WALLET_CONNECTORS.WALLET_CONNECT_V2 + * Example: WALLET_CONNECTORS.METAMASK, WALLET_CONNECTORS.WALLET_CONNECT_V2. + * + * Optional in the modal SDK (`@web3auth/modal`): when omitted, the modal opens + * a wallet picker dedicated to account linking and uses the wallet selected by + * the user. Required in the no-modal SDK (`@web3auth/no-modal`). */ - connectorName: WALLET_CONNECTOR_TYPE | string; + connectorName?: WALLET_CONNECTOR_TYPE | string; /** * Chain ID to use when generating the wallet identity proof. diff --git a/packages/no-modal/src/base/core/IWeb3Auth.ts b/packages/no-modal/src/base/core/IWeb3Auth.ts index 91735691f..15fe38869 100644 --- a/packages/no-modal/src/base/core/IWeb3Auth.ts +++ b/packages/no-modal/src/base/core/IWeb3Auth.ts @@ -258,7 +258,7 @@ export interface IWeb3Auth extends IWeb3AuthCore { * @param params - Linking parameters including the target connector name. * @returns A result object confirming the link, including the linked address. */ - linkAccount(params: LinkAccountParams): Promise; + linkAccount(params?: LinkAccountParams): Promise; /** * Unlink an external wallet from the currently authenticated user account diff --git a/packages/no-modal/src/noModal.ts b/packages/no-modal/src/noModal.ts index 133b4a3ff..7ae677472 100644 --- a/packages/no-modal/src/noModal.ts +++ b/packages/no-modal/src/noModal.ts @@ -618,7 +618,10 @@ export class Web3AuthNoModal extends SafeEventEmitter imp } } - public async linkAccount(params: LinkAccountParams): Promise { + public async linkAccount(params?: LinkAccountParams): Promise { + if (!params?.connectorName) { + throw WalletInitializationError.invalidParams("connectorName is required when calling linkAccount on the no-modal SDK"); + } const chainId = this.resolveLinkAccountChainId(params.chainId); const isolatedConnector = await this.createLinkingWalletConnector(params.connectorName, chainId); return this.linkAccountWithConnector(params.connectorName, chainId, isolatedConnector); diff --git a/packages/no-modal/src/react/hooks/useLinkAccount.ts b/packages/no-modal/src/react/hooks/useLinkAccount.ts index 2be386825..0faa71917 100644 --- a/packages/no-modal/src/react/hooks/useLinkAccount.ts +++ b/packages/no-modal/src/react/hooks/useLinkAccount.ts @@ -14,7 +14,7 @@ export interface IUseLinkAccount { loading: boolean; error: Web3AuthError | null; linkedAccounts: LinkedAccountInfo[]; - linkAccount(params: LinkAccountParams): Promise; + linkAccount(params?: LinkAccountParams): Promise; unlinkAccount(address: string): Promise; } @@ -26,7 +26,7 @@ export const useLinkAccount = (): IUseLinkAccount => { const [linkedAccounts, setLinkedAccounts] = useState([]); const linkAccount = useCallback( - async (params: LinkAccountParams): Promise => { + async (params?: LinkAccountParams): Promise => { if (!web3Auth) throw WalletInitializationError.notReady(); setLoading(true); setError(null); diff --git a/packages/no-modal/src/vue/composables/useLinkAccount.ts b/packages/no-modal/src/vue/composables/useLinkAccount.ts index 55080c10d..c1d7b14ec 100644 --- a/packages/no-modal/src/vue/composables/useLinkAccount.ts +++ b/packages/no-modal/src/vue/composables/useLinkAccount.ts @@ -15,7 +15,7 @@ export interface IUseLinkAccount { loading: Ref; error: Ref; linkedAccounts: Ref; - linkAccount(params: LinkAccountParams): Promise; + linkAccount(params?: LinkAccountParams): Promise; unlinkAccount(address: string): Promise; } @@ -25,7 +25,7 @@ export const useLinkAccount = (): IUseLinkAccount => { const error = ref(null); const linkedAccounts = ref([]); - const linkAccount = async (params: LinkAccountParams): Promise => { + const linkAccount = async (params?: LinkAccountParams): Promise => { if (!web3Auth.value) throw WalletInitializationError.notReady(); try { error.value = null; From e7f4a58398dbfa06f8e56fad67d56ad20d745257 Mon Sep 17 00:00:00 2001 From: Lionell Briones Date: Thu, 30 Apr 2026 14:48:21 +0800 Subject: [PATCH 3/7] link wallet connnect flow from wallet list --- .../src/ui/containers/ConnectWallet/ConnectWallet.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/modal/src/ui/containers/ConnectWallet/ConnectWallet.tsx b/packages/modal/src/ui/containers/ConnectWallet/ConnectWallet.tsx index fbfcd064f..dd2435dda 100644 --- a/packages/modal/src/ui/containers/ConnectWallet/ConnectWallet.tsx +++ b/packages/modal/src/ui/containers/ConnectWallet/ConnectWallet.tsx @@ -246,6 +246,14 @@ function ConnectWallet(props: ConnectWalletProps) { } else { // show QR code if wallet connect v2 is supported if (button.hasWalletConnect) { + // In account-linking picker mode, hand off to the WC v2 account-linking flow in + // modalManager (via the picker resolver in onExternalWalletLogin) instead of showing + // the regular inline QR — the regular QR uses a connector owned by the login flow, + // which is not what we want for linking. + if (modalState.accountLinking.pickerActive) { + handleExternalWalletClick({ connector: button.name }); + return; + } setSelectedButton(button); setSelectedWallet(true); setCurrentPage(CONNECT_WALLET_PAGES.SELECTED_WALLET); From 755723196678eb0c6778270c02c6f328454e2068 Mon Sep 17 00:00:00 2001 From: Lionell Briones Date: Thu, 30 Apr 2026 18:45:40 +0800 Subject: [PATCH 4/7] auto adjust modal height for loader screens --- packages/modal/src/ui/components/Loader/Loader.tsx | 4 ++-- packages/modal/src/ui/containers/Root/Root.tsx | 11 +---------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/packages/modal/src/ui/components/Loader/Loader.tsx b/packages/modal/src/ui/components/Loader/Loader.tsx index ed3baf5df..49d3ae5ae 100644 --- a/packages/modal/src/ui/components/Loader/Loader.tsx +++ b/packages/modal/src/ui/components/Loader/Loader.tsx @@ -23,7 +23,7 @@ function ConnectingStatus(props: ConnectingStatusType) { ); return ( -
+
{providerIcon} @@ -69,7 +69,7 @@ function ConnectedStatus(props: ConnectedStatusType) { function ErroredStatus(props: ErroredStatusType) { const { message } = props; return ( -
+
-
+
{/* Content */} {isShowLoader ? ( Date: Mon, 4 May 2026 11:54:08 +0800 Subject: [PATCH 5/7] minor ui fixes --- .../src/components/AccountLinkingSection.vue | 2 +- packages/modal/src/ui/components/Loader/Loader.tsx | 6 +++--- .../ui/containers/AccountLinking/AccountLinking.tsx | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/demo/vue-app-new/src/components/AccountLinkingSection.vue b/demo/vue-app-new/src/components/AccountLinkingSection.vue index d649ddf96..12f8ef50d 100644 --- a/demo/vue-app-new/src/components/AccountLinkingSection.vue +++ b/demo/vue-app-new/src/components/AccountLinkingSection.vue @@ -244,7 +244,7 @@ const getWalletCardClasses = (account: ConnectedAccountInfo): string => {

No connected wallets found in user info yet.

- +
Link Wallet

Choose a wallet connector to start the account-linking flow. diff --git a/packages/modal/src/ui/components/Loader/Loader.tsx b/packages/modal/src/ui/components/Loader/Loader.tsx index 8eb8a3393..3f37d8da4 100644 --- a/packages/modal/src/ui/components/Loader/Loader.tsx +++ b/packages/modal/src/ui/components/Loader/Loader.tsx @@ -23,7 +23,7 @@ function ConnectingStatus(props: ConnectingStatusType) { ); return ( -

+
{providerIcon} @@ -47,7 +47,7 @@ function ConnectingStatus(props: ConnectingStatusType) { function ConnectedStatus(props: ConnectedStatusType) { const { message } = props; return ( -
+
+
-
-

{accountLinkingDisplayName}

+
+
+

{accountLinkingDisplayName}

{modalState.accountLinking.status === ACCOUNT_LINKING_STATUS.ERRORED ? ( -
-

{accountLinkingMessage}

+
+

{accountLinkingMessage}

) : ( )} {accountLinkingMessage && modalState.accountLinking.status !== ACCOUNT_LINKING_STATUS.ERRORED && ( -

{accountLinkingMessage}

+

{accountLinkingMessage}

)}
); From a87e55dd156011fb2758659c7259a8a249586656 Mon Sep 17 00:00:00 2001 From: Lionell Briones Date: Mon, 4 May 2026 11:54:28 +0800 Subject: [PATCH 6/7] update selector copywriting for wallet link --- .../modal/src/ui/containers/ConnectWallet/ConnectWallet.tsx | 1 + .../ConnectWalletHeader/ConnectWalletHeader.tsx | 6 ++++-- .../ConnectWalletHeader/ConnectWalletHeader.type.ts | 1 + packages/modal/src/ui/i18n/amharic.json | 1 + packages/modal/src/ui/i18n/dutch.json | 1 + packages/modal/src/ui/i18n/english.json | 1 + packages/modal/src/ui/i18n/french.json | 1 + packages/modal/src/ui/i18n/german.json | 1 + packages/modal/src/ui/i18n/japanese.json | 1 + packages/modal/src/ui/i18n/korean.json | 1 + packages/modal/src/ui/i18n/mandarin.json | 1 + packages/modal/src/ui/i18n/portuguese.json | 1 + packages/modal/src/ui/i18n/spanish.json | 1 + packages/modal/src/ui/i18n/turkish.json | 1 + 14 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/modal/src/ui/containers/ConnectWallet/ConnectWallet.tsx b/packages/modal/src/ui/containers/ConnectWallet/ConnectWallet.tsx index ba59de4be..1f3b40df8 100644 --- a/packages/modal/src/ui/containers/ConnectWallet/ConnectWallet.tsx +++ b/packages/modal/src/ui/containers/ConnectWallet/ConnectWallet.tsx @@ -300,6 +300,7 @@ function ConnectWallet(props: ConnectWalletProps) { onBackClick={handleBack} currentPage={currentPage} selectedButton={selectedButton} + isLinking={modalState.accountLinking.pickerActive} /> {/* Body */} {selectedWallet ? ( diff --git a/packages/modal/src/ui/containers/ConnectWallet/ConnectWalletHeader/ConnectWalletHeader.tsx b/packages/modal/src/ui/containers/ConnectWallet/ConnectWalletHeader/ConnectWalletHeader.tsx index e45ed17e3..570b837d5 100644 --- a/packages/modal/src/ui/containers/ConnectWallet/ConnectWalletHeader/ConnectWalletHeader.tsx +++ b/packages/modal/src/ui/containers/ConnectWallet/ConnectWalletHeader/ConnectWalletHeader.tsx @@ -5,7 +5,7 @@ import i18n from "../../../localeImport"; import { ConnectWalletHeaderProps } from "./ConnectWalletHeader.type"; function ConnectWalletHeader(props: ConnectWalletHeaderProps) { - const { hideBackButton, disableBackButton, onBackClick, currentPage, selectedButton } = props; + const { hideBackButton, disableBackButton, onBackClick, currentPage, selectedButton, isLinking } = props; const [t] = useTranslation(undefined, { i18n }); const handleBack = () => { @@ -44,7 +44,9 @@ function ConnectWalletHeader(props: ConnectWalletHeaderProps) { {currentPage === CONNECT_WALLET_PAGES.SELECTED_WALLET ? selectedButton?.displayName : currentPage === CONNECT_WALLET_PAGES.CONNECT_WALLET - ? t("modal.connectYourWallet") + ? isLinking + ? t("modal.linkYourWallet") + : t("modal.connectYourWallet") : currentPage}

diff --git a/packages/modal/src/ui/containers/ConnectWallet/ConnectWalletHeader/ConnectWalletHeader.type.ts b/packages/modal/src/ui/containers/ConnectWallet/ConnectWalletHeader/ConnectWalletHeader.type.ts index af71b20c6..58aa1d214 100644 --- a/packages/modal/src/ui/containers/ConnectWallet/ConnectWalletHeader/ConnectWalletHeader.type.ts +++ b/packages/modal/src/ui/containers/ConnectWallet/ConnectWalletHeader/ConnectWalletHeader.type.ts @@ -6,4 +6,5 @@ export interface ConnectWalletHeaderProps { currentPage: string; selectedButton: ExternalButton; hideBackButton?: boolean; + isLinking?: boolean; } diff --git a/packages/modal/src/ui/i18n/amharic.json b/packages/modal/src/ui/i18n/amharic.json index 67df6d64d..f0ac4ba84 100644 --- a/packages/modal/src/ui/i18n/amharic.json +++ b/packages/modal/src/ui/i18n/amharic.json @@ -6,6 +6,7 @@ "allChains": "ሁሉም ቼኖች", "connect-wallet.more-wallets": "ተጨማሪ ዋሌቶች", "connectYourWallet": "ዋሌትዎን ያገናኙ", + "linkYourWallet": "ዋሌትዎን ያያይዙ", "errors-invalid-email": "የተሳሳተ ኢሜይል", "errors-invalid-number": "የተሳሳተ የስልክ ቁጥር", "errors-invalid-number-email": "የተሳሳተ ኢሜይል ወይም የስልክ ቁጥር", diff --git a/packages/modal/src/ui/i18n/dutch.json b/packages/modal/src/ui/i18n/dutch.json index 85ee885ad..93fa8905f 100644 --- a/packages/modal/src/ui/i18n/dutch.json +++ b/packages/modal/src/ui/i18n/dutch.json @@ -6,6 +6,7 @@ "allChains": "Alle ketens", "connect-wallet.more-wallets": "Meer portemonnees", "connectYourWallet": "Uw portemonnee verbinden", + "linkYourWallet": "Uw portemonnee koppelen", "errors-invalid-email": "Ongeldig e-mailadres", "errors-invalid-number": "Ongeldig telefoonnummer", "errors-invalid-number-email": "Ongeldig e-mailadres of telefoonnummer", diff --git a/packages/modal/src/ui/i18n/english.json b/packages/modal/src/ui/i18n/english.json index e0ae3ae8b..d56eb5337 100644 --- a/packages/modal/src/ui/i18n/english.json +++ b/packages/modal/src/ui/i18n/english.json @@ -6,6 +6,7 @@ "allChains": "All Chains", "connect-wallet.more-wallets": "More Wallets", "connectYourWallet": "Connect your wallet", + "linkYourWallet": "Link your wallet", "errors-invalid-email": "Invalid Email", "errors-invalid-number": "Invalid Phone Number", "errors-invalid-number-email": "Invalid Email or Phone Number", diff --git a/packages/modal/src/ui/i18n/french.json b/packages/modal/src/ui/i18n/french.json index e8c31efb4..03a050f48 100644 --- a/packages/modal/src/ui/i18n/french.json +++ b/packages/modal/src/ui/i18n/french.json @@ -6,6 +6,7 @@ "allChains": "Toutes les chaînes", "connect-wallet.more-wallets": "Plus de portefeuilles", "connectYourWallet": "Connectez votre portefeuille", + "linkYourWallet": "Liez votre portefeuille", "errors-invalid-email": "E-mail invalide", "errors-invalid-number": "Numéro de téléphone invalide", "errors-invalid-number-email": "Adresse e-mail ou numéro de téléphone invalide", diff --git a/packages/modal/src/ui/i18n/german.json b/packages/modal/src/ui/i18n/german.json index 69ca5399a..af5901490 100644 --- a/packages/modal/src/ui/i18n/german.json +++ b/packages/modal/src/ui/i18n/german.json @@ -6,6 +6,7 @@ "allChains": "Alle Kettens", "connect-wallet.more-wallets": "Mehr Wallets", "connectYourWallet": "Ihre Wallet verbinden", + "linkYourWallet": "Ihre Wallet verknüpfen", "errors-invalid-email": "Ungültige E-Mail", "errors-invalid-number": "Ungültige Telefonnummer", "errors-invalid-number-email": "Ungültige E-Mail-Adresse oder Telefonnummer", diff --git a/packages/modal/src/ui/i18n/japanese.json b/packages/modal/src/ui/i18n/japanese.json index f05fc4988..48824ee42 100644 --- a/packages/modal/src/ui/i18n/japanese.json +++ b/packages/modal/src/ui/i18n/japanese.json @@ -6,6 +6,7 @@ "allChains": "すべてのチェーン", "connect-wallet.more-wallets": "ウォレットをもっと見る", "connectYourWallet": "ウォレットを接続", + "linkYourWallet": "ウォレットをリンク", "errors-invalid-email": "無効な電子メール", "errors-invalid-number": "無効な電話番号", "errors-invalid-number-email": "無効なメールアドレスまたは電話番号", diff --git a/packages/modal/src/ui/i18n/korean.json b/packages/modal/src/ui/i18n/korean.json index cc628496b..b91b2d3a0 100644 --- a/packages/modal/src/ui/i18n/korean.json +++ b/packages/modal/src/ui/i18n/korean.json @@ -6,6 +6,7 @@ "allChains": "모든 체인", "connect-wallet.more-wallets": "더 많은 지갑", "connectYourWallet": "지갑 연결", + "linkYourWallet": "지갑 연동", "errors-invalid-email": "유효하지 않은 이메일", "errors-invalid-number": "유효하지 않은 전화번호", "errors-invalid-number-email": "유효하지 않은 이메일 또는 전화번호", diff --git a/packages/modal/src/ui/i18n/mandarin.json b/packages/modal/src/ui/i18n/mandarin.json index e3ead5cdf..5ab8e636f 100644 --- a/packages/modal/src/ui/i18n/mandarin.json +++ b/packages/modal/src/ui/i18n/mandarin.json @@ -6,6 +6,7 @@ "allChains": "所有链", "connect-wallet.more-wallets": "查看更多钱包", "connectYourWallet": "连接您的钱包", + "linkYourWallet": "关联您的钱包", "errors-invalid-email": "无效的电子邮件", "errors-invalid-number": "电话号码无效", "errors-invalid-number-email": "无效的电子邮件或电话号码", diff --git a/packages/modal/src/ui/i18n/portuguese.json b/packages/modal/src/ui/i18n/portuguese.json index 96ee1eae6..aa2f0a3c6 100644 --- a/packages/modal/src/ui/i18n/portuguese.json +++ b/packages/modal/src/ui/i18n/portuguese.json @@ -6,6 +6,7 @@ "allChains": "Todas as cadeias", "connect-wallet.more-wallets": "Mais carteiras", "connectYourWallet": "Conecte seu portefeuille", + "linkYourWallet": "Vincule seu portefeuille", "errors-invalid-email": "E-mail inválido", "errors-invalid-number": "Número de telefone inválido", "errors-invalid-number-email": "Email ou número de telefone inválido", diff --git a/packages/modal/src/ui/i18n/spanish.json b/packages/modal/src/ui/i18n/spanish.json index fcf1ca2df..162fde9fb 100644 --- a/packages/modal/src/ui/i18n/spanish.json +++ b/packages/modal/src/ui/i18n/spanish.json @@ -6,6 +6,7 @@ "allChains": "Todas las cadenas", "connect-wallet.more-wallets": "Más carteras", "connectYourWallet": "Conecte su portefeuille", + "linkYourWallet": "Vincule su portefeuille", "errors-invalid-email": "Correo electrónico no válido", "errors-invalid-number": "Número de teléfono no válido", "errors-invalid-number-email": "Correo electrónico o número de teléfono no válido", diff --git a/packages/modal/src/ui/i18n/turkish.json b/packages/modal/src/ui/i18n/turkish.json index a9f562a0b..ec19ba452 100644 --- a/packages/modal/src/ui/i18n/turkish.json +++ b/packages/modal/src/ui/i18n/turkish.json @@ -6,6 +6,7 @@ "allChains": "Tüm zincirler", "connect-wallet.more-wallets": "Daha fazla cüzdan", "connectYourWallet": "Cüzdanınızı bağlayın", + "linkYourWallet": "Cüzdanınızı ilişkilendirin", "errors-invalid-email": "Geçersiz E-posta", "errors-invalid-number": "Geçersiz Telefon Numarası", "errors-invalid-number-email": "Geçersiz E-posta veya Telefon Numarası", From 0b5aeb3ce38ec38f2cf2afb05380571ec7df66f9 Mon Sep 17 00:00:00 2001 From: Lionell Briones Date: Wed, 6 May 2026 09:16:38 +0800 Subject: [PATCH 7/7] display error and success message --- packages/modal/src/modalManager.ts | 6 +++++- packages/modal/src/ui/containers/Root/Root.tsx | 10 ++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/modal/src/modalManager.ts b/packages/modal/src/modalManager.ts index 00ca19860..66d06eb12 100644 --- a/packages/modal/src/modalManager.ts +++ b/packages/modal/src/modalManager.ts @@ -1106,7 +1106,11 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal { this.loginModal.endConnectingLoader({ success: true }); return result; } catch (error) { - const message = (error as Error)?.message; + let message = (error as Error)?.message; + if (error instanceof AccountLinkingError) { + const isUnlink = error.code >= 5406 && error.code <= 5408; + message = isUnlink ? `[${error.code}] Account unlinking failed` : `[${error.code}] Account linking failed`; + } this.loginModal.endConnectingLoader({ success: false, errorMessage: message }); throw error; } finally { diff --git a/packages/modal/src/ui/containers/Root/Root.tsx b/packages/modal/src/ui/containers/Root/Root.tsx index 3dfec54e2..d6bf756f7 100644 --- a/packages/modal/src/ui/containers/Root/Root.tsx +++ b/packages/modal/src/ui/containers/Root/Root.tsx @@ -1,5 +1,6 @@ import { WALLET_CONNECTORS, type WalletRegistryItem } from "@web3auth/no-modal"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import Footer from "../../components/Footer/Footer"; import Loader from "../../components/Loader"; @@ -9,6 +10,7 @@ import { useModalState } from "../../context/ModalStateContext"; import { RootProvider } from "../../context/RootContext"; import { useWidget } from "../../context/WidgetContext"; import { type ExternalButton, MODAL_STATUS } from "../../interfaces"; +import i18n from "../../localeImport"; import AccountLinking from "../AccountLinking"; import ConnectWallet from "../ConnectWallet"; import Login from "../Login"; @@ -18,6 +20,7 @@ import RootBodySheets from "./RootBodySheets/RootBodySheets"; function RootContent(props: RootProps) { const { onCloseLoader } = props; + const [t] = useTranslation(undefined, { i18n }); const { modalState, shouldShowLoginPage, showPasswordLessInput, areSocialLoginsVisible } = useModalState(); const { deviceDetails, uiConfig, isConnectAndSignAuthenticationMode, handleMobileVerifyConnect, handleAcceptConsent, handleDeclineConsent } = useWidget(); @@ -193,6 +196,12 @@ function RootContent(props: RootProps) { return !isWalletConnectAccountLinkingVisible && modalState.status !== MODAL_STATUS.INITIALIZED; }, [isWalletConnectAccountLinkingVisible, modalState.status]); + const loaderMessage = useMemo(() => { + const message = modalState.postLoadingMessage; + if (!message) return undefined; + return i18n.exists(message) ? t(message) : message; + }, [modalState.postLoadingMessage, t]); + return (