Skip to content

feat: link external wallet#2437

Open
lwin-kyaw wants to merge 46 commits intomasterfrom
feat/link-ext-wallet
Open

feat: link external wallet#2437
lwin-kyaw wants to merge 46 commits intomasterfrom
feat/link-ext-wallet

Conversation

@lwin-kyaw
Copy link
Copy Markdown
Contributor

@lwin-kyaw lwin-kyaw commented Apr 2, 2026

Jira Link

https://consensyssoftware.atlassian.net/browse/EMBED-85?atlOrigin=eyJpIjoiM2UxZDY3YTZlOGFkNGU2Nzg0YjFjYjliOTc5N2I1MjAiLCJwIjoiaiJ9

Description

This PR adds external wallet account linking to Web3Auth.

Users authenticated through the AUTH connector can now link and unlink external wallets, retrieve connected account metadata from user info, and access framework-level React/Vue helpers for the flow. The change also updates the demo apps to showcase the new linking UX.

What Changed

  • Added linkAccount(params) and unlinkAccount(address) to Web3AuthNoModal.
  • Introduced account-linking types and REST helpers for Citadel-backed /v1/link/wallet and /v1/unlink/wallet requests.
  • Added dedicated AccountLinkingError error codes and analytics events for linking/unlinking start, success, and failure.
  • Extended connector interfaces with generateChallengeAndSign() so external wallets can produce proof-of-ownership signatures for linking.
  • Refactored EVM, Solana, and WalletConnect v2 connectors to support reusable challenge/sign flows.
  • Implemented isolated connector creation during linking so external wallet proof generation does not mutate the main SDK session state.
  • Updated AuthConnector.getUserInfo() to include connectedAccounts fetched from Citadel /v1/user.
  • Added and exported useLinkAccount hooks/composables for:
    • @web3auth/modal/react
    • @web3auth/modal/vue
    • @web3auth/no-modal/react
    • @web3auth/no-modal/vue
  • Updated the Vue and Wagmi React demo apps to:
    • display connected wallets from user info
    • link external wallets
    • unlink linked wallets
    • show success/error state for the flow

User-Facing Features

  • Authenticated users can link an external wallet to their Web3Auth account.
  • Linked wallets can be unlinked later by address.
  • User info now includes connected account metadata, including primary account state and linked wallet details.
  • React and Vue consumers get a simplified useLinkAccount API with loading, error, and linked account state.

Notes

  • Account linking/unlinking is only supported when the active session is connected with the AUTH connector.
  • Automatic wallet linking currently supports METAMASK and WALLET_CONNECT_V2.
  • Linking uses signed wallet challenges and refreshed identity tokens returned from Citadel.

How has this been tested?

  • Authenticate with the AUTH connector.
  • Call getUserInfo() and verify connectedAccounts is returned.
  • Link a MetaMask wallet and confirm the linked account appears in the response.
  • Link a WalletConnect wallet and confirm the linked account appears in the response.
  • Unlink a previously linked wallet and confirm it is removed from the returned linked accounts.
  • Verify React/Vue demo flows show loading, success, and error states correctly.

Screenshots (if appropriate)

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)

Checklist

  • My code follows the code style of this project. (run lint)
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

Note

Medium Risk
Adds new account-linking/switching flows (including WalletConnect modal-session handling) and changes wagmi provider resync behavior, which can impact connection state and wallet UX across frameworks. Demo and lockfile updates are lower risk but broaden surface area for regression in login/modal flows.

Overview
Adds external wallet account linking/switching support. Web3Auth in packages/modal now exposes linkAccount and switchAccount, with special handling for WalletConnect v2 that opens the modal, tracks an account-linking session, and cleans up connectors/listeners on success, failure, or user-close.

Introduces account-linking UI state + screen. The login modal state gains an accountLinking state machine (intent, status, walletConnectUri, errors), and Root/Widget now render a dedicated AccountLinking QR flow and skip the normal WalletConnect “prewarm” during linking.

Framework integrations updated. React/Vue exports add useLinkAccount/useSwitchAccount, and the React/Vue wagmi providers now resync off the Web3Auth connection object (not the stable provider proxy) to correctly refresh wagmi state on account switches.

Demos refreshed. The Vue demo adds an AccountLinkingSection (list/switch/unlink/link wallets) and uses getUserInfo() for fresh connected account data; the wagmi React demo adds a simple “Link Wallet” UI. Miscellaneous formatting/typings tweaks and package-lock.json churn accompany the feature.

Reviewed by Cursor Bugbot for commit 8573cb0. Bugbot is set up for automated code reviews on this repo. Configure here.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
web3auth-web Ready Ready Preview, Comment May 5, 2026 8:42am

Request Review

@lwin-kyaw lwin-kyaw changed the base branch from master to feat/External-wallet-session-management April 2, 2026 12:50
Base automatically changed from feat/External-wallet-session-management to master April 6, 2026 08:21
@lwin-kyaw lwin-kyaw marked this pull request as ready for review April 7, 2026 08:39
@lwin-kyaw lwin-kyaw requested review from a team as code owners April 7, 2026 08:39
Comment thread demo/vue-app-new/src/components/AppDashboard.vue Outdated
Comment thread .npmrc Outdated
Comment thread demo/vue-app-new/src/components/AppDashboard.vue Outdated
return !showPasswordLessInput && !areSocialLoginsVisible;
}, [areSocialLoginsVisible, showPasswordLessInput]);

const isWalletConnectAccountLinkingVisible = useMemo(() => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move all linking stuff to a new component pls

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3257baf. Configure here.

Comment thread demo/vue-app-new/src/MainView.vue Outdated
@@ -1,4 +1,4 @@
// import "./global";
import "./global";
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need this global shim ?

const { showCallsStatus, isPending: isShowCallsStatusPending, error: showCallsStatusError } = useShowCallsStatus();

// Account Linking
const { linkAccount, loading: isLinkAccountLoading, error: linkAccountError } = useLinkAccount();
Copy link
Copy Markdown
Member

@tuna1207 tuna1207 May 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think the useLinkAccount hook should handle and return linkAccountResult state so consumer don't have to handle it themselves everytime

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hook return the function, linkAccount which internally handle the account linking logic though.

useEffect(() => {
// TODO: maybe move this inside root
if (!modalState.modalVisibility) return;
if (modalState.accountLinking.active) return;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should add some comment here explaining why we don't do this effect if account linking active

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added comments here, chore: addressed comments

Comment thread packages/modal/src/ui/loginModal.tsx Outdated
activeAccount: this.activeAccount,
currentChainId: this.currentChainId,
});
if (!switchResult) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we log something here if the switch doesn't do anything

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's only one case that authConnector.switchAccount can return empty value, i.e when the target account is already active.

We should not need any log for that, this is not an error. Clients should handle it to prevent switching to same account again.

if (connector.status === CONNECTOR_STATUS.NOT_READY && this.cachedConnector !== connectorName) {
if (
connector.status === CONNECTOR_STATUS.NOT_READY &&
this.cachedConnector !== connectorName &&
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need to check both ? what could be the case 1 of cached and current connection match

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On load, we init all the connectors (including external wallet connectors) again.
There's a case that user has linked the external wallet (the connector status is Connected) but it's not an active account (not cachedConnector).

Comment thread packages/modal/src/ui/loginModal.tsx Outdated
status: ACCOUNT_LINKING_STATUS.INITIALIZING,
};
});
this.setState({
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateAccountLinkingState already setState i don't think we need this anymore

tuna1207
tuna1207 previously approved these changes May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants