Skip to content

feat: link external wallet#2437

Open
lwin-kyaw wants to merge 50 commits into
masterfrom
feat/link-ext-wallet
Open

feat: link external wallet#2437
lwin-kyaw wants to merge 50 commits into
masterfrom
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

High Risk
High risk because it adds new account-linking and account-switching flows that touch core connector interfaces, modal state management, and WalletConnect session handling, which can affect authentication and connection lifecycle across apps.

Overview
Adds external wallet account linking and active-wallet switching to Web3Auth, including new linkAccount, unlinkAccount, and switchAccount APIs, plus React/Vue hooks/composables and REST helpers for Citadel-backed linking/unlinking requests.

Updates the modal UI/state machine to support a dedicated WalletConnect-based account-linking session (QR flow, status/intent tracking, cleanup/reset on close), and adjusts Wagmi providers to resync on Web3Auth connection changes rather than provider identity.

Demo apps are updated to showcase linking, unlinking, switching between connected wallets, and displaying userInfo.connectedAccounts; dependencies and lockfiles are refreshed (notably MetaMask connect packages).

Reviewed by Cursor Bugbot for commit d0c8e95. 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 13, 2026 2:45am

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
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
/** Address of the account */
address: string | null;
/** Auth connection id of the account */
authConnectionId: string | null;
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.

groupedAuthConnectionId?

success: boolean;

/** Refreshed id token for the user */
idToken: string;
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 also get a new access token?

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.

No. We don't store linked account info in the accessToken, only in idToken.

/** Whether the Citadel server accepted the linking request. */
success: boolean;

/** Refreshed id token for the user */
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.

same

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.

No, we don't refresh the accessToken.

*/
export interface UnlinkAccountPayload {
/** Access token to authenticate the request */
idToken: string;
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.

isn't all these access token and not idtoken

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.

idToken, if I'm not mistaken.

I've updated the comment for the interfaces.

let result: UnlinkAccountResult;

try {
result = await post<UnlinkAccountResult>(url, payload, {
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.

deduplicate this file

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.

Deduped here, a36472d

[CONNECTOR_EVENTS.MFA_ENABLED]: (isMFAEnabled: boolean) => void;
[CONNECTOR_EVENTS.CONSENT_REQUIRING]: (data: CONNECTED_EVENT_DATA) => void;
[CONNECTOR_EVENTS.CONSENT_ACCEPTED]: (data: CONNECTED_EVENT_DATA & { loginMode: LoginModeType }) => void;
[CONNECTOR_EVENTS.CONNECTION_UPDATED]: () => void;
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.

where's the data?

[CONNECTOR_EVENTS.CONSENT_REQUIRING]: () => void;
[CONNECTOR_EVENTS.CONSENT_ACCEPTED]: (data: SDK_CONSENT_ACCEPTED_EVENT_DATA) => void;
[CONNECTOR_EVENTS.ERRORED]: (error: Web3AuthError, loginMode: LoginModeType) => void;
[CONNECTOR_EVENTS.CONNECTION_UPDATED]: () => void;
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.

data?

};

const connectionUpdatedListener = () => {
status.value = newWeb3Auth.status;
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.

use the ref

return this.state.idToken || null;
}

get accessToken(): string | null {
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.

lint warning

Comment thread packages/no-modal/src/noModal.ts
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 f3f96c9. Configure here.

Comment thread packages/no-modal/src/noModal.ts
}

// attach all event bridges to the new provider
this._providerEngineProxy = provider;
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.

we shouldn't ideally re-assign this object.
listeners are already set on this by clients
you're moving listeners to a new obj but depending on client framework used, it may not work correctly

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