Skip to content

Commit 817cc82

Browse files
Merge pull request #2091 from Web3Auth/feat/sdk-v10-wallet-discovery
Feat/sdk v10 - Multi chain namespaces for Wallet discovery
2 parents b42d5af + 3b3d1e6 commit 817cc82

24 files changed

+444
-421
lines changed

demo/vue-app-new/package-lock.json

Lines changed: 250 additions & 362 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

demo/vue-app-new/package.json

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
"dependencies": {
1313
"@solana/web3.js": "^1.98.0",
1414
"@toruslabs/constants": "^14.2.0",
15-
"@toruslabs/base-controllers": "^7.2.1",
16-
"@toruslabs/ethereum-controllers": "^7.2.1",
17-
"@toruslabs/solana-controllers": "^7.3.1",
15+
"@toruslabs/base-controllers": "^7.4.0",
16+
"@toruslabs/ethereum-controllers": "^7.4.0",
17+
"@toruslabs/solana-controllers": "^7.4.0",
1818
"@toruslabs/vue-components": "^7.9.0",
1919
"@toruslabs/vue-icons": "^7.6.2",
2020
"@web3auth/auth": "^9.6.4",
@@ -24,27 +24,27 @@
2424
"@web3auth/ws-embed": "^4.0.14",
2525
"bs58": "^6.0.0",
2626
"ethers": "^6.13.5",
27-
"petite-vue-i18n": "^11.0.1",
27+
"petite-vue-i18n": "^11.1.2",
2828
"tweetnacl": "^1.0.3",
2929
"vue": "^3.5.13"
3030
},
3131
"devDependencies": {
3232
"@toruslabs/eslint-config-vue": "^3.3.4",
3333
"@types/bs58": "^4.0.4",
34-
"@vitejs/plugin-vue": "^5.2.1",
35-
"autoprefixer": "^10.4.20",
34+
"@vitejs/plugin-vue": "^5.2.3",
35+
"autoprefixer": "^10.4.21",
3636
"eslint": "^8.54.0",
3737
"globals": "^15.14.0",
38-
"postcss": "^8.5.1",
39-
"prettier": "^3.4.2",
38+
"postcss": "^8.5.3",
39+
"prettier": "^3.5.3",
4040
"stream-browserify": "^3.0.0",
4141
"tailwindcss": "^3.4.13",
42-
"typescript": "^5.7.3",
43-
"vite": "^6.0.11",
44-
"vue-tsc": "^2.2.0"
42+
"typescript": "^5.8.2",
43+
"vite": "^6.2.2",
44+
"vue-tsc": "^2.2.8"
4545
},
4646
"optionalDependencies": {
47-
"@esbuild/linux-x64": "0.25.0"
47+
"@esbuild/linux-x64": "0.25.1"
4848
},
4949
"lint-staged": {
5050
"!(*d).{js,ts}": [

demo/vue-app-new/src/MainView.vue

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,16 @@ const options = computed((): Web3AuthOptions => {
7070
for (const chainId of formData.chains) {
7171
const chain = getChainConfig(namespace, chainId, clientIds[formData.network]);
7272
if (!chain) continue;
73-
if (namespace === CHAIN_NAMESPACES.SOLANA && chainId === "0x65") {
74-
chain.rpcTarget = import.meta.env.VITE_APP_SOLANA_MAINNET_RPC || chain.rpcTarget;
73+
// we need to validate chain id as legacy Solana chainIds 0x1, 0x2, 0x3 are not valid anymore
74+
if (chain.chainId !== chainId) continue;
75+
if (namespace === CHAIN_NAMESPACES.SOLANA) {
76+
if (chainId === "0x65") {
77+
chain.rpcTarget = import.meta.env.VITE_APP_SOLANA_MAINNET_RPC || chain.rpcTarget;
78+
} else if (chainId === "0x66") {
79+
chain.rpcTarget = import.meta.env.VITE_APP_SOLANA_TESTNET_RPC || chain.rpcTarget;
80+
} else if (chainId === "0x67") {
81+
chain.rpcTarget = import.meta.env.VITE_APP_SOLANA_DEVNET_RPC || chain.rpcTarget;
82+
}
7583
}
7684
chains.push(chain);
7785
}

locales/importLocales.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ Promise.all(promises)
3434
});
3535

3636
// Create json files
37-
const folder = "./packages/ui/src/i18n/";
37+
const folder = "./packages/modal/src/ui/i18n/";
3838
const folderPath = path.resolve(folder);
3939
if (!fs.existsSync(folderPath)) {
4040
fs.mkdirSync(folderPath);

packages/modal/src/modalManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export class Web3Auth extends Web3AuthNoModal implements IWeb3AuthModal {
7373
this.loginModal = new LoginModal({
7474
...this.options.uiConfig,
7575
connectorListener: this,
76-
chainNamespace: this.currentChain.chainNamespace,
76+
chainNamespaces: [...new Set(this.coreOptions.chains?.map((x) => x.chainNamespace) || [])],
7777
walletRegistry,
7878
});
7979
this.subscribeToLoginModalEvents();
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { useTranslation } from "react-i18next";
2+
3+
import { ExternalButton, ExternalWalletEventType } from "../../interfaces";
4+
import i18n from "../../localeImport";
5+
import Button from "../Button";
6+
import Image from "../Image";
7+
import ExternalWalletHeader from "./ExternalWalletHeader";
8+
9+
interface ExternalWalletChainNamespaceProps {
10+
button: ExternalButton;
11+
handleExternalWalletClick: (params: ExternalWalletEventType) => void;
12+
goBack: () => void;
13+
closeModal: () => void;
14+
}
15+
16+
export default function ExternalWalletChainNamespace(props: ExternalWalletChainNamespaceProps) {
17+
const { button, goBack, closeModal, handleExternalWalletClick } = props;
18+
19+
const [t] = useTranslation(undefined, { i18n });
20+
21+
// chainNames should be available when selecting a chain namespace
22+
const chainNamespaces = button.chainNamespaces!.map((chainNamespace) => {
23+
const imageId = chainNamespace === "eip155" ? "evm" : chainNamespace;
24+
const displayName = chainNamespace === "eip155" ? "EVM" : chainNamespace;
25+
return {
26+
chainNamespace,
27+
displayName,
28+
imageId: `chain-${imageId}`,
29+
};
30+
});
31+
32+
return (
33+
<div>
34+
{/* Header */}
35+
<ExternalWalletHeader title={`${t("modal.external.select-chain")}`} goBack={goBack} closeModal={closeModal} />
36+
37+
{/* Wallet image */}
38+
<div className="w3a--flex w3a--justify-center w3a--my-6">
39+
<Image
40+
imageId={`login-${button.name}`}
41+
hoverImageId={`login-${button.name}`}
42+
fallbackImageId="wallet"
43+
height="100"
44+
width="100"
45+
isButton
46+
extension={button.imgExtension}
47+
/>
48+
</div>
49+
50+
{/* Description */}
51+
<p className="w3a--text-center w3a--text-sm w3a--text-app-gray-500 w3a--my-6">
52+
{t("modal.external.select-chain-description", { wallet: button.displayName })}
53+
</p>
54+
55+
{/* Chain namespace buttons */}
56+
<ul className="w3a--flex w3a--flex-col w3a--gap-3">
57+
{chainNamespaces.map(({ chainNamespace, displayName, imageId }) => (
58+
<li key={chainNamespace}>
59+
<Button
60+
variant="tertiary"
61+
type="button"
62+
onClick={() => handleExternalWalletClick({ connector: button.name, chainNamespace })}
63+
className="w3a--w-full w3a--size-xl !w3a--justify-between w3a--items-center wallet-btn"
64+
title={displayName}
65+
>
66+
<div className="w3a--flex w3a--items-center">
67+
<Image imageId={imageId} hoverImageId={imageId} fallbackImageId="wallet" height="24" width="24" isButton extension="svg" />
68+
<p className="w3a--ml-2 w3a--text-left w3a--text-sm first-letter:w3a--capitalize">{displayName}</p>
69+
</div>
70+
<span className="w3a--inline-flex w3a--items-center w3a--rounded-lg w3a--px-2 w3a--py-1 w3a--text-xs w3a--font-medium w3a--bg-app-primary-100 w3a--text-app-primary-800">
71+
{t("modal.external.installed")}
72+
</span>
73+
</Button>
74+
</li>
75+
))}
76+
</ul>
77+
</div>
78+
);
79+
}

packages/modal/src/ui/components/ExternalWallet/ExternalWalletDetails.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { ExternalButton } from "../../interfaces";
1+
import { ExternalButton, ExternalWalletEventType } from "../../interfaces";
2+
import ExternalWalletChainNamespace from "./ExternalWalletChainNamespace";
23
import ExternalWalletConnect from "./ExternalWalletConnect";
34
import ExternalWalletInstall from "./ExternalWalletInstall";
45

@@ -7,10 +8,23 @@ interface ExternalWalletDetailProps {
78
walletConnectUri: string;
89
goBack: () => void;
910
closeModal: () => void;
11+
handleExternalWalletClick: (params: ExternalWalletEventType) => void;
1012
}
1113

1214
export default function ExternalWalletDetail(props: ExternalWalletDetailProps) {
13-
const { connectButton, walletConnectUri, goBack, closeModal } = props;
15+
const { connectButton, walletConnectUri, goBack, closeModal, handleExternalWalletClick } = props;
16+
17+
// Select chain namespace for injected wallets
18+
if (connectButton.hasInjectedWallet) {
19+
return (
20+
<ExternalWalletChainNamespace
21+
handleExternalWalletClick={handleExternalWalletClick}
22+
button={connectButton}
23+
goBack={goBack}
24+
closeModal={closeModal}
25+
/>
26+
);
27+
}
1428

1529
return (
1630
<div>

packages/modal/src/ui/components/ExternalWallets.tsx

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import bowser from "bowser";
33
import { ChangeEvent, useEffect, useMemo, useState } from "react";
44
import { useTranslation } from "react-i18next";
55

6-
import { ExternalButton, MODAL_STATUS, ModalStatusType } from "../interfaces";
6+
import { ExternalButton, ExternalWalletEventType, MODAL_STATUS, ModalStatusType } from "../interfaces";
77
import i18n from "../localeImport";
88
import ExternalWalletButton from "./ExternalWallet/ExternalWalletButton";
99
import ExternalWalletDetail from "./ExternalWallet/ExternalWalletDetails";
@@ -12,13 +12,13 @@ import Loader from "./Loader";
1212

1313
interface ExternalWalletsProps {
1414
hideExternalWallets: () => void;
15-
handleExternalWalletClick: (params: { connector: string }) => void;
15+
handleExternalWalletClick: (params: ExternalWalletEventType) => void;
1616
closeModal: () => void;
1717
config: Record<string, BaseConnectorConfig>;
1818
walletConnectUri: string | undefined;
1919
showBackButton: boolean;
2020
modalStatus: ModalStatusType;
21-
chainNamespace: ChainNamespaceType;
21+
chainNamespaces: ChainNamespaceType[];
2222
walletRegistry?: WalletRegistry;
2323
}
2424

@@ -33,7 +33,7 @@ function formatIOSMobile(params: { uri: string; link?: string }) {
3333
return "";
3434
}
3535

36-
export default function ExternalWallet(props: ExternalWalletsProps) {
36+
export default function ExternalWallets(props: ExternalWalletsProps) {
3737
const {
3838
hideExternalWallets,
3939
handleExternalWalletClick,
@@ -42,7 +42,7 @@ export default function ExternalWallet(props: ExternalWalletsProps) {
4242
walletConnectUri,
4343
showBackButton,
4444
modalStatus,
45-
chainNamespace,
45+
chainNamespaces,
4646
walletRegistry,
4747
} = props;
4848
const [externalButtons, setExternalButtons] = useState<ExternalButton[]>([]);
@@ -116,6 +116,10 @@ export default function ExternalWallet(props: ExternalWalletsProps) {
116116
href = universalLink || deepLink;
117117
}
118118

119+
const registryNamespaces = new Set(walletRegistryItem.chains?.map((chain) => chain.split(":")[0]));
120+
const injectedChainNamespaces = new Set(walletRegistryItem.injected?.map((injected) => injected.namespace));
121+
const availableChainNamespaces = chainNamespaces.filter((x) => registryNamespaces.has(x) || injectedChainNamespaces.has(x));
122+
119123
const button: ExternalButton = {
120124
name: wallet,
121125
displayName: walletRegistryItem.name,
@@ -125,13 +129,11 @@ export default function ExternalWallet(props: ExternalWalletsProps) {
125129
hasInstallLinks: Object.keys(walletRegistryItem.app || {}).length > 0,
126130
walletRegistryItem,
127131
imgExtension: walletRegistryItem.imgExtension || "svg",
132+
chainNamespaces: availableChainNamespaces,
128133
};
129134
// const isBrowserExtensionAvailable = walletRegistryItem.app?.chrome || walletRegistryItem.app?.firefox || walletRegistryItem.app?.edge;
130135
if (!button.hasInjectedWallet && !button.hasWalletConnect && !button.hasInstallLinks) return acc;
131-
132-
const chainNamespaces = new Set(walletRegistryItem.chains?.map((chain) => chain.split(":")[0]));
133-
const injectedChainNamespaces = new Set(walletRegistryItem.injected?.map((injected) => injected.namespace));
134-
if (!chainNamespaces.has(chainNamespace) && !injectedChainNamespaces.has(chainNamespace)) return acc;
136+
if (availableChainNamespaces.length === 0) return acc;
135137

136138
acc.push(button);
137139
return acc;
@@ -196,15 +198,16 @@ export default function ExternalWallet(props: ExternalWalletsProps) {
196198
setExternalButtons(buttons);
197199
setTotalExternalWallets(buttons.length);
198200
}
199-
}, [config, deviceDetails, adapterVisibilityMap, walletRegistry, walletSearch, chainNamespace, walletDiscoverySupported]);
201+
}, [config, deviceDetails, adapterVisibilityMap, walletRegistry, walletSearch, chainNamespaces, walletDiscoverySupported]);
200202

201203
const handleWalletClick = (button: ExternalButton) => {
202-
// if has injected wallet, connect to injected wallet
203-
// if doesn't have wallet connect & doesn't have install links, must be a custom adapter
204-
if (button.hasInjectedWallet || (!button.hasWalletConnect && !button.hasInstallLinks)) {
204+
// if has injected wallet and single chain namespace, connect to injected wallet
205+
const isInjectedConnectorAndSingleChainNamespace = button.hasInjectedWallet && button.chainNamespaces?.length === 1;
206+
// if doesn't have wallet connect & doesn't have install links, must be a custom connector
207+
const isCustomConnector = !button.hasInjectedWallet && !button.hasWalletConnect && !button.hasInstallLinks;
208+
if (isInjectedConnectorAndSingleChainNamespace || isCustomConnector) {
205209
handleExternalWalletClick({ connector: button.name });
206210
} else {
207-
// else, show wallet detail
208211
setSelectedButton(button);
209212
}
210213
};
@@ -295,6 +298,7 @@ export default function ExternalWallet(props: ExternalWalletsProps) {
295298
goBack={() => setSelectedButton(null)}
296299
walletConnectUri={walletConnectUri}
297300
closeModal={closeModal}
301+
handleExternalWalletClick={handleExternalWalletClick}
298302
/>
299303
))
300304
)}

packages/modal/src/ui/components/Modal.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ interface ModalProps {
1919
stateListener: SafeEventEmitter<StateEmitterEvents>;
2020
appLogo?: string;
2121
appName?: string;
22-
chainNamespace: ChainNamespaceType;
22+
chainNamespaces: ChainNamespaceType[];
2323
walletRegistry?: WalletRegistry;
2424
handleSocialLoginClick: (params: SocialLoginEventType) => void;
2525
handleExternalWalletClick: (params: ExternalWalletEventType) => void;
@@ -58,7 +58,7 @@ export default function Modal(props: ModalProps) {
5858
stateListener,
5959
appLogo,
6060
appName,
61-
chainNamespace,
61+
chainNamespaces,
6262
walletRegistry,
6363
handleSocialLoginClick,
6464
handleExternalWalletClick,
@@ -249,7 +249,7 @@ export default function Modal(props: ModalProps) {
249249
modalStatus={modalState.status}
250250
showBackButton={areSocialLoginsVisible || isEmailPasswordlessLoginVisible || isSmsPasswordlessLoginVisible}
251251
handleExternalWalletClick={preHandleExternalWalletClick}
252-
chainNamespace={chainNamespace}
252+
chainNamespaces={chainNamespaces}
253253
walletConnectUri={modalState.walletConnectUri}
254254
config={modalState.externalWalletsConfig}
255255
hideExternalWallets={() =>

packages/modal/src/ui/i18n/dutch.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
"external.installed": "Geïnstalleerd",
2222
"external.no-wallets-found": "Geen portefeuilles gevonden",
2323
"external.search-wallet": "Zoeken door {{count}} portemonnees...",
24+
"external.select-chain": "Ketting selecteren",
25+
"external.select-chain-description": "Deze {{wallet}} portemonnee ondersteunt meerdere ketens. Selecteer welke ketting u wilt verbinden",
2426
"external.title": "Externe portemonnee",
2527
"external.walletconnect-connect": "Verbinden",
2628
"external.walletconnect-copy": "Scan met een WalletConnect-ondersteunde portemonnee of klik op de QR-code om naar uw klembord te kopiëren.",

0 commit comments

Comments
 (0)