diff --git a/packages/vechain-kit/src/components/AccountModal/Components/AccountSelector.tsx b/packages/vechain-kit/src/components/AccountModal/Components/AccountSelector.tsx
index 7227b08a..0775975a 100644
--- a/packages/vechain-kit/src/components/AccountModal/Components/AccountSelector.tsx
+++ b/packages/vechain-kit/src/components/AccountModal/Components/AccountSelector.tsx
@@ -21,7 +21,7 @@ import { AccountAvatar } from '@/components/common';
import { useState } from 'react';
import { AccountModalContentTypes } from '../Types/Types';
import { useTranslation } from 'react-i18next';
-import { useWallet, useSwitchWallet, useDAppKitWallet } from '@/hooks';
+import { useSwitchWallet } from '@/hooks';
type Props = {
wallet: Wallet;
@@ -45,9 +45,8 @@ export const AccountSelector = ({
style,
}: Props) => {
const { t } = useTranslation();
- const { connection } = useWallet();
- const { switchWallet, isSwitching, isInAppBrowser } = useSwitchWallet();
- const { isSwitchWalletEnabled } = useDAppKitWallet();
+ const { switchWallet, isSwitching, isInAppBrowser, canSwitchWallet } =
+ useSwitchWallet();
const [copied, setCopied] = useState(false);
@@ -125,9 +124,7 @@ export const AccountSelector = ({
- {(connection.isInAppBrowser && isSwitchWalletEnabled) ||
- (!connection.isInAppBrowser &&
- connection.isConnectedWithDappKit) ? (
+ {canSwitchWallet ? (
}
diff --git a/packages/vechain-kit/src/components/AccountModal/Contents/Profile/Components/ProfileCard/ProfileCard.tsx b/packages/vechain-kit/src/components/AccountModal/Contents/Profile/Components/ProfileCard/ProfileCard.tsx
index a26215e2..b091251e 100644
--- a/packages/vechain-kit/src/components/AccountModal/Contents/Profile/Components/ProfileCard/ProfileCard.tsx
+++ b/packages/vechain-kit/src/components/AccountModal/Contents/Profile/Components/ProfileCard/ProfileCard.tsx
@@ -18,7 +18,6 @@ import { AccountModalContentTypes } from '@/components/AccountModal/Types';
export type ProfileCardProps = {
address: string;
onEditClick?: () => void;
- onLogout?: () => void;
showHeader?: boolean;
showLinks?: boolean;
showDescription?: boolean;
@@ -39,7 +38,6 @@ export const ProfileCard = ({
showDisplayName = true,
reserveNameDescriptionSpace = false,
setCurrentContent,
- onLogout,
}: ProfileCardProps) => {
const { network } = useVeChainKitConfig();
@@ -209,7 +207,6 @@ export const ProfileCard = ({
)}
{
const { t } = useTranslation();
- const { account, disconnect, connection } = useWallet();
- const { switchWallet, isSwitching, isInAppBrowser } = useSwitchWallet();
- const { isSwitchWalletEnabled } = useDAppKitWallet();
+ const { account, disconnect } = useWallet();
+ const { switchWallet, isSwitching, isInAppBrowser, canSwitchWallet } =
+ useSwitchWallet();
const { hasAnyBalance, formattedBalance } = useTotalBalance({
address: account?.address,
});
@@ -110,18 +109,14 @@ export const ProfileContent = ({
address={account?.address ?? ''}
showHeader={false}
setCurrentContent={setCurrentContent}
- onLogout={() => {
- disconnect();
- onLogoutSuccess?.();
- }}
/>
-
+
}
- colorScheme="red"
onClick={async () => {
handleSwitchWallet();
}}
@@ -173,11 +164,10 @@ export const ProfileContent = ({
) : (
}
- colorScheme="red"
onClick={() =>
setCurrentContent({
type: 'disconnect-confirm',
diff --git a/packages/vechain-kit/src/components/AccountModal/Contents/SelectWallet/SelectWalletContent.tsx b/packages/vechain-kit/src/components/AccountModal/Contents/SelectWallet/SelectWalletContent.tsx
index 07eabf54..08bd48af 100644
--- a/packages/vechain-kit/src/components/AccountModal/Contents/SelectWallet/SelectWalletContent.tsx
+++ b/packages/vechain-kit/src/components/AccountModal/Contents/SelectWallet/SelectWalletContent.tsx
@@ -50,8 +50,20 @@ export const SelectWalletContent = ({
}: Props) => {
const { t } = useTranslation();
const { isolatedView } = useAccountModalOptions();
- const { account, disconnect } = useWallet();
- const { disconnect: dappKitDisconnect } = useDAppKitWallet();
+ const {
+ account,
+ accounts: kitAccounts,
+ setActiveAccount,
+ connection,
+ disconnect,
+ } = useWallet();
+ const {
+ disconnect: dappKitDisconnect,
+ switchWallet: dappKitSwitchWallet,
+ requestPermissions: dappKitRequestPermissions,
+ revokeAccount: dappKitRevokeAccount,
+ availableMethods: dappKitAvailableMethods,
+ } = useDAppKitWallet();
const { open: openDappKitModal } = useDAppKitWalletModal();
const { getStoredWallets, setActiveWallet, removeWallet } =
useSwitchWallet();
@@ -60,17 +72,65 @@ export const SelectWalletContent = ({
const textSecondary = useToken('colors', 'vechain-kit-text-secondary');
- const [wallets, setWallets] = useState(getStoredWallets());
- const walletsHashRef = useRef(hashWallets(getStoredWallets()));
+ // On desktop dapp-kit, use `kitAccounts` as the source of truth;
+ // otherwise fall back to legacy storage. Use a stable primitive key in
+ // dep arrays — `kitAccounts` reference changes on every valtio write.
+ const kitAccountsRef = useRef(kitAccounts);
+ kitAccountsRef.current = kitAccounts;
+ const kitAccountsKey = useMemo(
+ () =>
+ kitAccounts
+ .map((a) => a.address.toLowerCase())
+ .sort()
+ .join('|'),
+ [kitAccounts],
+ );
+
+ const useDappKitAccountsAsSource = useMemo(
+ () =>
+ connection.isConnectedWithDappKit &&
+ !connection.isInAppBrowser &&
+ kitAccounts.length > 0,
+ [
+ connection.isConnectedWithDappKit,
+ connection.isInAppBrowser,
+ kitAccounts.length,
+ ],
+ );
+
+ const initialWallets = useMemo(() => {
+ if (useDappKitAccountsAsSource) {
+ const activeLower = account?.address?.toLowerCase();
+ return kitAccountsRef.current.map((a) => ({
+ address: a.address,
+ connectedAt: Date.now(),
+ isActive: a.address.toLowerCase() === activeLower,
+ }));
+ }
+ return getStoredWallets();
+ }, [useDappKitAccountsAsSource, kitAccountsKey, account?.address, getStoredWallets]);
+
+ const [wallets, setWallets] = useState(initialWallets);
+ const walletsHashRef = useRef(hashWallets(initialWallets));
// Function to refresh wallets list
const refreshWallets = useCallback(() => {
+ if (useDappKitAccountsAsSource) {
+ const activeLower = account?.address?.toLowerCase();
+ const next: StoredWallet[] = kitAccountsRef.current.map((a) => ({
+ address: a.address,
+ connectedAt: Date.now(),
+ isActive: a.address.toLowerCase() === activeLower,
+ }));
+ setWallets(next);
+ walletsHashRef.current = hashWallets(next);
+ return;
+ }
const updatedWallets = getStoredWallets();
setWallets(updatedWallets);
walletsHashRef.current = hashWallets(updatedWallets);
- }, [getStoredWallets]);
+ }, [useDappKitAccountsAsSource, kitAccountsKey, account?.address, getStoredWallets]);
- // Refresh wallets list when account changes (new wallet connected) or when wallets are updated
useEffect(() => {
refreshWallets();
}, [refreshWallets, account?.address]);
@@ -143,14 +203,16 @@ export const SelectWalletContent = ({
return;
}
- // Ensure the wallet that was previously active is saved
- // Metadata will be fetched dynamically when needed
- if (activeWallet) {
- saveWallet(activeWallet.address);
+ if (useDappKitAccountsAsSource) {
+ // Dapp-kit v2: switch without re-signing.
+ setActiveAccount(address);
+ } else {
+ if (activeWallet) {
+ saveWallet(activeWallet.address);
+ }
+ setActiveWallet(address);
}
- setActiveWallet(address);
-
// Refresh wallets list immediately after switch
setTimeout(() => {
refreshWallets();
@@ -172,12 +234,14 @@ export const SelectWalletContent = ({
[
activeWalletAddress,
activeWallet,
- account,
+ useDappKitAccountsAsSource,
+ setActiveAccount,
setActiveWallet,
refresh,
setCurrentContent,
refreshWallets,
saveWallet,
+ returnTo,
],
);
@@ -189,6 +253,13 @@ export const SelectWalletContent = ({
const remainingWallets = wallets.filter(
(w) => w.address.toLowerCase() !== wallet.address.toLowerCase(),
);
+ const supportsRevokeAccount =
+ useDappKitAccountsAsSource &&
+ Array.isArray(dappKitAvailableMethods) &&
+ dappKitAvailableMethods.includes(
+ 'wallet_revokeAccountPermission',
+ ) &&
+ typeof dappKitRevokeAccount === 'function';
// Navigate to remove wallet confirmation screen
setCurrentContent({
@@ -197,6 +268,29 @@ export const SelectWalletContent = ({
walletAddress: wallet.address,
walletDomain: null, // Domain will be fetched dynamically in RemoveWalletConfirmContent
onConfirm: async () => {
+ if (supportsRevokeAccount) {
+ await dappKitRevokeAccount(wallet.address);
+ setTimeout(() => {
+ refreshWallets();
+ }, 50);
+
+ if (remainingWallets.length === 0) {
+ _onLogoutSuccess?.();
+ return;
+ }
+
+ setCurrentContent({
+ type: 'select-wallet',
+ props: {
+ setCurrentContent,
+ onClose: () => {},
+ returnTo,
+ onLogoutSuccess: _onLogoutSuccess,
+ },
+ });
+ return;
+ }
+
// If removing the active wallet and there are other wallets, switch to the first one
if (isActiveWallet && remainingWallets.length > 0) {
const nextActiveWallet = remainingWallets[0];
@@ -263,12 +357,49 @@ export const SelectWalletContent = ({
wallets,
setActiveWallet,
dappKitDisconnect,
+ dappKitAvailableMethods,
+ dappKitRevokeAccount,
+ useDappKitAccountsAsSource,
],
);
+ const supportsRequestPermissions =
+ Array.isArray(dappKitAvailableMethods) &&
+ dappKitAvailableMethods.includes('wallet_requestPermissions') &&
+ typeof dappKitRequestPermissions === 'function';
+ const supportsRevokeAccount =
+ Array.isArray(dappKitAvailableMethods) &&
+ dappKitAvailableMethods.includes('wallet_revokeAccountPermission') &&
+ typeof dappKitRevokeAccount === 'function';
+
const handleAddNewWallet = useCallback(() => {
+ if (useDappKitAccountsAsSource) {
+ // VeWorld v2: prefer EIP-2255 `wallet_requestPermissions`,
+ // fall back to legacy `thor_switchWallet`.
+ if (supportsRequestPermissions) {
+ dappKitRequestPermissions()
+ .then(() => {
+ refresh();
+ })
+ .catch((e) => {
+ console.error('dapp-kit requestPermissions failed', e);
+ });
+ return;
+ }
+ dappKitSwitchWallet().catch((e) => {
+ console.error('dapp-kit switchWallet failed', e);
+ });
+ return;
+ }
openDappKitModal();
- }, [openDappKitModal]);
+ }, [
+ useDappKitAccountsAsSource,
+ supportsRequestPermissions,
+ dappKitRequestPermissions,
+ dappKitSwitchWallet,
+ openDappKitModal,
+ refresh,
+ ]);
const handleLogout = () => {
disconnect();
@@ -303,7 +434,10 @@ export const SelectWalletContent = ({
onRemove={() =>
handleRemoveWallet(activeWallet)
}
- showRemove={wallets.length > 1}
+ showRemove={
+ !useDappKitAccountsAsSource &&
+ wallets.length > 1
+ }
/>
)}
@@ -322,7 +456,10 @@ export const SelectWalletContent = ({
handleWalletSelect(wallet.address)
}
onRemove={() => handleRemoveWallet(wallet)}
- showRemove={true}
+ showRemove={
+ !useDappKitAccountsAsSource ||
+ supportsRevokeAccount
+ }
/>
))}
@@ -337,7 +474,9 @@ export const SelectWalletContent = ({
variant="vechainKitSecondary"
onClick={handleAddNewWallet}
>
- {t('Add New Wallet')}
+ {useDappKitAccountsAsSource
+ ? t('Change connected accounts')
+ : t('Add New Wallet')}
diff --git a/packages/vechain-kit/src/hooks/api/vetDomains/useGetAvatarOfAddress.ts b/packages/vechain-kit/src/hooks/api/vetDomains/useGetAvatarOfAddress.ts
index 34c9bdb6..ea16ec95 100644
--- a/packages/vechain-kit/src/hooks/api/vetDomains/useGetAvatarOfAddress.ts
+++ b/packages/vechain-kit/src/hooks/api/vetDomains/useGetAvatarOfAddress.ts
@@ -43,11 +43,12 @@ const getCrossAppAvatar = (): string | null => {
}
};
+// Lowercase the key so mixed-case and lowercase callers share the cache.
export const getAvatarOfAddressQueryKey = (address?: string) => [
'VECHAIN_KIT',
'VET_DOMAINS',
'AVATAR_OF_ADDRESS',
- address,
+ address?.toLowerCase() ?? address,
];
/**
@@ -72,7 +73,8 @@ export const useGetAvatarOfAddress = (address?: string) => {
return crossAppAvatar ?? getPicassoImage(address);
}
- const addressDomain = await getAddressDomain(address, {
+ // Lowercase to bypass ethers' strict EIP-55 check.
+ const addressDomain = await getAddressDomain(address.toLowerCase(), {
networkUrl: network.nodeUrl,
});
if (!addressDomain) {
diff --git a/packages/vechain-kit/src/hooks/api/vetDomains/useVechainDomain.ts b/packages/vechain-kit/src/hooks/api/vetDomains/useVechainDomain.ts
index f09db1db..d1dc4401 100644
--- a/packages/vechain-kit/src/hooks/api/vetDomains/useVechainDomain.ts
+++ b/packages/vechain-kit/src/hooks/api/vetDomains/useVechainDomain.ts
@@ -11,9 +11,10 @@ interface VeChainDomainResult {
}
+// Lowercase the key so mixed-case and lowercase callers share the cache.
export const getVechainDomainQueryKey = (addressOrDomain?: string | null) => [
'VECHAIN_KIT_DOMAIN',
- addressOrDomain,
+ addressOrDomain?.toLowerCase() ?? addressOrDomain,
];
export const useVechainDomain = (addressOrDomain?: string | null) => {
@@ -59,20 +60,22 @@ export const useVechainDomain = (addressOrDomain?: string | null) => {
// Check if this domain is the primary domain for the address
if (address) {
- isPrimary = await isPrimaryDomain(addressOrDomain, address, {
+ isPrimary = await isPrimaryDomain(addressOrDomain, address.toLowerCase(), {
networkUrl: network.nodeUrl,
});
}
} else {
- // Input is an address, get the corresponding domain
- domain = await getAddressDomain(addressOrDomain, {
+ // Lowercase to bypass ethers' strict EIP-55 check in
+ // `@vechain/contract-getters` (fails on mixed-case input).
+ const normalizedAddress = addressOrDomain.toLowerCase();
+ domain = await getAddressDomain(normalizedAddress, {
networkUrl: network.nodeUrl,
});
address = addressOrDomain;
// If we found a domain, check if it's the primary domain
if (domain) {
- isPrimary = await isPrimaryDomain(domain, addressOrDomain, {
+ isPrimary = await isPrimaryDomain(domain, normalizedAddress, {
networkUrl: network.nodeUrl,
});
}
diff --git a/packages/vechain-kit/src/hooks/api/wallet/useSwitchWallet.ts b/packages/vechain-kit/src/hooks/api/wallet/useSwitchWallet.ts
index c77bc710..04268894 100644
--- a/packages/vechain-kit/src/hooks/api/wallet/useSwitchWallet.ts
+++ b/packages/vechain-kit/src/hooks/api/wallet/useSwitchWallet.ts
@@ -10,6 +10,17 @@ export type UseSwitchWalletReturnType = {
setActiveWallet: (address: string) => void;
removeWallet: (address: string) => void;
isInAppBrowser: boolean;
+ /**
+ * Whether the wallet-switch UI should be exposed to the user.
+ * - VeWorld in-app browser: requires the wallet to advertise
+ * `thor_switchWallet` (`isSwitchWalletEnabled`).
+ * - External browser (desktop or mobile) on dapp-kit: always true except
+ * for WalletConnect, where the session exposes a single account and
+ * doesn't support `wallet_requestPermissions` / `thor_switchWallet`,
+ * so "Switch" would only trigger a full reconnect.
+ * - Privy / social-login / cross-app: false (no dapp-kit connection).
+ */
+ canSwitchWallet: boolean;
};
/**
@@ -18,7 +29,11 @@ export type UseSwitchWalletReturnType = {
* - On desktop: Provides wallet storage functions for UI-based switching
*/
export const useSwitchWallet = (): UseSwitchWalletReturnType => {
- const { switchWallet: dappKitSwitchWallet } = useDAppKitWallet();
+ const {
+ switchWallet: dappKitSwitchWallet,
+ isSwitchWalletEnabled,
+ source: dappKitSource,
+ } = useDAppKitWallet();
const { connection } = useWallet();
const [isSwitching, setIsSwitching] = useState(false);
const {
@@ -29,6 +44,11 @@ export const useSwitchWallet = (): UseSwitchWalletReturnType => {
const isInAppBrowser = connection.isInAppBrowser;
+ const canSwitchWallet = isInAppBrowser
+ ? isSwitchWalletEnabled
+ : connection.isConnectedWithDappKit &&
+ dappKitSource !== 'wallet-connect';
+
const switchWallet = useCallback(async () => {
if (isInAppBrowser) {
// In-app browser: use dapp-kit's switchWallet
@@ -71,5 +91,6 @@ export const useSwitchWallet = (): UseSwitchWalletReturnType => {
setActiveWallet,
removeWallet: removeWalletStorage,
isInAppBrowser,
+ canSwitchWallet,
};
};
diff --git a/packages/vechain-kit/src/hooks/api/wallet/useWallet.ts b/packages/vechain-kit/src/hooks/api/wallet/useWallet.ts
index 7701ee90..72e58ca9 100644
--- a/packages/vechain-kit/src/hooks/api/wallet/useWallet.ts
+++ b/packages/vechain-kit/src/hooks/api/wallet/useWallet.ts
@@ -21,9 +21,23 @@ import { NETWORK_TYPE } from '@/config/network';
import { useAccount } from 'wagmi';
import { usePrivyCrossAppSdk } from '@/providers/PrivyCrossAppProvider';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useQueryClient } from '@tanstack/react-query';
import { useWalletMetadata } from './useWalletMetadata';
import { useWalletStorage } from './useWalletStorage';
import { isBrowser } from '@/utils/ssrUtils';
+import { getVechainDomainQueryKey } from '@/hooks/api/vetDomains/useVechainDomain';
+import { getAvatarOfAddressQueryKey } from '@/hooks/api/vetDomains/useGetAvatarOfAddress';
+
+// Normalize addresses to lowercase at the `useWallet` boundary so that the
+// case returned by `account.address` / `connectedWallet.address` is stable
+// across vechain-kit versions and dapp-kit connect flows (v1 certificate vs
+// v2 `wallet_requestPermissions`, which may return mixed case).
+// Downstream consumers (React Query keys, app-side caches, stored wallets)
+// historically treated the address as lowercase; returning a checksummed
+// address here would break that contract and silently invalidate caches
+// when switching between vechain-kit versions on the same domain.
+// Strict EIP-55 callers must opt-in explicitly via `Address.checksum`.
+const normalizeAddress = (addr: string): string => addr.toLowerCase();
export type UseWalletReturnType = {
// This will be the smart account if connected with privy, otherwise it will be wallet connected with dappkit
@@ -32,6 +46,14 @@ export type UseWalletReturnType = {
// The wallet in use between dappKitWallet, embeddedWallet and crossAppWallet
connectedWallet: Wallet;
+ /** All accounts approved by the wallet (dapp-kit multi-account); single
+ * entry for Privy / cross-app. The active one is `account`. */
+ accounts: NonNullable[];
+
+ /** Switch active account without reopening the wallet picker. No-op for
+ * Privy / cross-app. */
+ setActiveAccount: (address: string) => void;
+
// Every user connected with privy has one
smartAccount: SmartAccount;
@@ -71,8 +93,20 @@ export const useWallet = (): UseWalletReturnType => {
const { feeDelegation, network, privy } = useVeChainKitConfig();
const { user, authenticated, logout, ready } = usePrivy();
const { data: chainId } = useGetChainId();
- const { account: dappKitAccount, disconnect: dappKitDisconnect } =
- useDAppKitWallet();
+ const {
+ account: dappKitAccount,
+ accounts: dappKitAccountsRaw,
+ setActiveAccount: dappKitSetActiveAccount,
+ disconnect: dappKitDisconnect,
+ } = useDAppKitWallet();
+
+ // Fall back to `[dappKitAccount]` for dapp-kit-react < 2.2.0 or when
+ // v2 persistence didn't populate `addresses`.
+ const dappKitAccounts: string[] = useMemo(() => {
+ if (dappKitAccountsRaw && dappKitAccountsRaw.length > 0)
+ return dappKitAccountsRaw;
+ return dappKitAccount ? [dappKitAccount] : [];
+ }, [dappKitAccountsRaw, dappKitAccount]);
const { getConnectionCache, clearConnectionCache } =
useCrossAppConnectionCache();
const connectionCache = getConnectionCache();
@@ -81,8 +115,12 @@ export const useWallet = (): UseWalletReturnType => {
getActiveWallet,
saveWallet,
getStoredWallets,
+ setActiveWallet: setActiveWalletStorage,
+ removeWallet,
} = useWalletStorage();
+ const queryClient = useQueryClient();
+
const nodeUrl = useGetNodeUrl();
// Check if in-app browser (calculate before using in useState)
@@ -168,6 +206,104 @@ export const useWallet = (): UseWalletReturnType => {
? crossAppAddress
: privyEmbeddedWalletAddress;
+ // Invalidate VNS/avatar queries on dapp-kit v2 connect — `connectV2` only
+ // sets `state.address`, it doesn't trigger the VNS lookup v1 did.
+ useEffect(() => {
+ if (!dappKitAccount) return;
+ queryClient.invalidateQueries({
+ queryKey: getVechainDomainQueryKey(dappKitAccount),
+ });
+ queryClient.invalidateQueries({
+ queryKey: getAvatarOfAddressQueryKey(dappKitAccount),
+ });
+ }, [dappKitAccount, queryClient]);
+
+ // Cross-version compat: dapp-kit v2 (`wallet_requestPermissions`) and
+ // older builds of this kit may persist mixed-case, non-EIP-55 addresses
+ // under both `dappkit@vechain/v2/*` (dapp-kit) and
+ // `vechain_kit_wallets_*` / `vechain_kit_active_wallet_*` (kit). Older
+ // vechain-kit consumers on the same origin read those keys verbatim and
+ // then hit `ethers.isAddress()` strict EIP-55 checks downstream
+ // (balance/domain/avatar all fail). Normalize every relevant entry to
+ // lowercase so any reader — old or new — gets a uniformly safe value.
+ useEffect(() => {
+ if (!isBrowser() || !isConnectedWithDappKit || !dappKitAccount) return;
+
+ const normalizeStringEntry = (key: string) => {
+ try {
+ const v = window.localStorage.getItem(key);
+ if (v && v !== v.toLowerCase()) {
+ window.localStorage.setItem(key, v.toLowerCase());
+ }
+ } catch {
+ /* ignore: localStorage may be unavailable */
+ }
+ };
+
+ const normalizeJsonArrayEntry = (key: string) => {
+ try {
+ const v = window.localStorage.getItem(key);
+ if (!v) return;
+ const parsed = JSON.parse(v);
+ if (
+ Array.isArray(parsed) &&
+ parsed.some(
+ (a) => typeof a === 'string' && a !== a.toLowerCase(),
+ )
+ ) {
+ window.localStorage.setItem(
+ key,
+ JSON.stringify(
+ parsed.map((a) =>
+ typeof a === 'string' ? a.toLowerCase() : a,
+ ),
+ ),
+ );
+ }
+ } catch {
+ /* ignore: malformed JSON or unavailable storage */
+ }
+ };
+
+ const normalizeStoredWalletsEntry = (key: string) => {
+ try {
+ const v = window.localStorage.getItem(key);
+ if (!v) return;
+ const parsed = JSON.parse(v);
+ if (!Array.isArray(parsed)) return;
+ const next = parsed.map((w) => {
+ if (
+ w &&
+ typeof w === 'object' &&
+ typeof w.address === 'string'
+ ) {
+ return { ...w, address: w.address.toLowerCase() };
+ }
+ return w;
+ });
+ const changed = next.some(
+ (w, i) =>
+ w?.address !== (parsed[i] as { address?: string })?.address,
+ );
+ if (changed) {
+ window.localStorage.setItem(key, JSON.stringify(next));
+ }
+ } catch {
+ /* ignore */
+ }
+ };
+
+ normalizeStringEntry('dappkit@vechain/v2/account');
+ normalizeJsonArrayEntry('dappkit@vechain/v2/accounts');
+ normalizeStringEntry(`vechain_kit_active_wallet_${network.type}`);
+ normalizeStoredWalletsEntry(`vechain_kit_wallets_${network.type}`);
+ }, [
+ isConnectedWithDappKit,
+ dappKitAccount,
+ dappKitAccountsRaw,
+ network.type,
+ ]);
+
// For desktop dappkit wallets, check if there's a stored active wallet
// Use state to track active wallet so it updates immediately on switch
const [storedActiveWalletAddress, setStoredActiveWalletAddress] = useState<
@@ -272,7 +408,56 @@ export const useWallet = (): UseWalletReturnType => {
network.type,
);
- const { setActiveWallet: setActiveWalletStorage } = useWalletStorage();
+ const dappKitAccountsRef = useRef(dappKitAccounts);
+ dappKitAccountsRef.current = dappKitAccounts;
+
+ // Reconcile kit storage with the dapp-kit approved set.
+ // - dapp-kit v2 (`accounts` populated): full multi-account set.
+ // - dapp-kit v1 / single-account flow (`accounts` missing or empty):
+ // fall back to `[dappKitAccount]`. This handles the recovery case
+ // where the user disconnected from an older vechain-kit on the
+ // same origin and re-logged in with a single account — without
+ // pruning here, `vechain_kit_wallets_*` keeps stale multi-account
+ // entries written by a previous v2 session and the old kit's UI
+ // surfaces accounts the wallet no longer approves.
+ useEffect(() => {
+ if (
+ !isConnectedWithDappKit ||
+ isInAppBrowser ||
+ !dappKitAccount
+ ) {
+ return;
+ }
+
+ const approvedAddresses: string[] =
+ dappKitAccountsRaw && dappKitAccountsRaw.length > 0
+ ? dappKitAccountsRaw
+ : [dappKitAccount];
+
+ const stored = getStoredWallets();
+ const approvedLower = new Set(
+ approvedAddresses.map((a) => a.toLowerCase()),
+ );
+ const storedLower = new Set(
+ stored.map((w) => w.address.toLowerCase()),
+ );
+
+ approvedAddresses.forEach((addr) => {
+ if (!storedLower.has(addr.toLowerCase())) saveWallet(addr);
+ });
+ stored.forEach((w) => {
+ if (!approvedLower.has(w.address.toLowerCase()))
+ removeWallet(w.address);
+ });
+ }, [
+ isConnectedWithDappKit,
+ isInAppBrowser,
+ dappKitAccount,
+ dappKitAccountsRaw,
+ getStoredWallets,
+ saveWallet,
+ removeWallet,
+ ]);
// Track recently removed wallets to prevent them from being set as active again
const recentlyRemovedWalletsRef = useRef>(new Set());
@@ -398,7 +583,7 @@ export const useWallet = (): UseWalletReturnType => {
const account = activeAddress
? {
- address: activeAddress,
+ address: normalizeAddress(activeAddress),
domain: activeAccountMetadata.domain,
image: activeAccountMetadata.image,
isLoadingMetadata: activeAccountMetadata.isLoading,
@@ -408,7 +593,7 @@ export const useWallet = (): UseWalletReturnType => {
const connectedWallet = connectedWalletAddress
? {
- address: connectedWalletAddress,
+ address: normalizeAddress(connectedWalletAddress),
domain: connectedMetadata.domain,
image: connectedMetadata.image,
isLoadingMetadata: connectedMetadata.isLoading,
@@ -416,6 +601,55 @@ export const useWallet = (): UseWalletReturnType => {
}
: null;
+ // Approved-accounts list surfaced to consumers (dapp-kit only; Privy /
+ // cross-app always have a single wallet).
+ const accountsList: NonNullable[] = useMemo(() => {
+ if (isConnectedWithDappKit) {
+ return dappKitAccounts.map((addr) => ({
+ address: normalizeAddress(addr),
+ domain: undefined,
+ image: undefined,
+ isLoadingMetadata: false,
+ metadata: undefined,
+ }));
+ }
+ return connectedWallet ? [connectedWallet] : [];
+ }, [isConnectedWithDappKit, dappKitAccounts, connectedWallet]);
+
+ const setActiveAccount = useCallback(
+ (address: string) => {
+ if (!isConnectedWithDappKit) return;
+ if (typeof dappKitSetActiveAccount === 'function') {
+ try {
+ dappKitSetActiveAccount(address);
+ } catch (e) {
+ console.error(
+ 'setActiveAccount: dapp-kit rejected the address',
+ e,
+ );
+ return;
+ }
+ }
+ if (!isInAppBrowser) {
+ setActiveWalletStorage(address);
+ setStoredActiveWalletAddress(address);
+ if (isBrowser()) {
+ window.dispatchEvent(
+ new CustomEvent('wallet_switched', {
+ detail: { address },
+ }),
+ );
+ }
+ }
+ },
+ [
+ isConnectedWithDappKit,
+ isInAppBrowser,
+ dappKitSetActiveAccount,
+ setActiveWalletStorage,
+ ],
+ );
+
// Get smart account version
const { data: smartAccountVersion } = useGetAccountVersion(
smartAccount?.address ?? '',
@@ -433,7 +667,12 @@ export const useWallet = (): UseWalletReturnType => {
// First set connection state to false
setIsConnected(false);
- // Then perform disconnection logic
+ // Then perform disconnection logic. `dappKitDisconnect` already
+ // wipes both `dappkit@vechain/v2/*` and the legacy
+ // `dappkit@vechain/*` keys; we add the kit-side storage cleanup
+ // below so a logout always leaves a clean slate (no stale
+ // multi-account list surfacing on the next login, regardless of
+ // which vechain-kit version connects next on the same origin).
if (isConnectedWithDappKit) {
dappKitDisconnect();
} else if (isConnectedWithSocialLogin) {
@@ -442,6 +681,19 @@ export const useWallet = (): UseWalletReturnType => {
await disconnectCrossApp();
}
+ if (isBrowser()) {
+ try {
+ window.localStorage.removeItem(
+ `vechain_kit_wallets_${network.type}`,
+ );
+ window.localStorage.removeItem(
+ `vechain_kit_active_wallet_${network.type}`,
+ );
+ } catch {
+ /* ignore: localStorage may be unavailable */
+ }
+ }
+
clearConnectionCache();
if (isBrowser()) {
window.dispatchEvent(new Event('wallet_disconnected'));
@@ -457,10 +709,13 @@ export const useWallet = (): UseWalletReturnType => {
isConnectedWithCrossApp,
disconnectCrossApp,
clearConnectionCache,
+ network.type,
]);
return {
account,
+ accounts: accountsList,
+ setActiveAccount,
smartAccount: {
address: smartAccount?.address ?? '',
domain: smartAccountMetadata.domain,
diff --git a/packages/vechain-kit/src/hooks/api/wallet/useWalletStorage.ts b/packages/vechain-kit/src/hooks/api/wallet/useWalletStorage.ts
index 6c0ea8ef..74a1ad40 100644
--- a/packages/vechain-kit/src/hooks/api/wallet/useWalletStorage.ts
+++ b/packages/vechain-kit/src/hooks/api/wallet/useWalletStorage.ts
@@ -30,28 +30,48 @@ export const useWalletStorage = () => {
const keys = getStorageKeys(network.type);
const cached = getLocalStorageItem(keys.wallets);
if (!cached) return [];
- return JSON.parse(cached) as StoredWallet[];
+ try {
+ const parsed = JSON.parse(cached) as StoredWallet[];
+ // Migrate legacy mixed-case entries on read so every consumer
+ // (including older vechain-kit versions on the same origin)
+ // works with lowercase.
+ return parsed.map((w) => ({
+ ...w,
+ address:
+ typeof w.address === 'string'
+ ? w.address.toLowerCase()
+ : w.address,
+ }));
+ } catch {
+ return [];
+ }
}, [network.type, getStorageKeys]);
const getActiveWallet = useCallback((): string | null => {
if (!isBrowser()) return null;
const keys = getStorageKeys(network.type);
- return getLocalStorageItem(keys.activeWallet);
+ const stored = getLocalStorageItem(keys.activeWallet);
+ return stored ? stored.toLowerCase() : null;
}, [network.type, getStorageKeys]);
+ // Normalize at the storage boundary: older vechain-kit consumers on the
+ // same origin pass these addresses to `ethers.isAddress()` (strict
+ // EIP-55) — mixed-case input there silently fails avatar/domain/balance
+ // fetches. Persisting lowercase keeps cross-version reads safe.
const saveWallet = useCallback(
(address: string) => {
if (!isBrowser()) return;
+ const normalized = address.toLowerCase();
const keys = getStorageKeys(network.type);
const wallets = getStoredWallets();
const existingIndex = wallets.findIndex(
- (w) => w.address.toLowerCase() === address.toLowerCase(),
+ (w) => w.address.toLowerCase() === normalized,
);
const walletToSave: StoredWallet = {
- address: address,
+ address: normalized,
connectedAt:
existingIndex >= 0
? wallets[existingIndex].connectedAt
@@ -74,17 +94,18 @@ export const useWalletStorage = () => {
(address: string) => {
if (!isBrowser()) return;
+ const normalized = address.toLowerCase();
const keys = getStorageKeys(network.type);
const wallets = getStoredWallets();
- // Update all wallets to set isActive
const updatedWallets = wallets.map((w) => ({
...w,
- isActive: w.address.toLowerCase() === address.toLowerCase(),
+ address: w.address.toLowerCase(),
+ isActive: w.address.toLowerCase() === normalized,
}));
setLocalStorageItem(keys.wallets, JSON.stringify(updatedWallets));
- setLocalStorageItem(keys.activeWallet, address);
+ setLocalStorageItem(keys.activeWallet, normalized);
},
[network.type, getStorageKeys, getStoredWallets],
);
diff --git a/packages/vechain-kit/src/hooks/login/useConnectWithDappKitSource.ts b/packages/vechain-kit/src/hooks/login/useConnectWithDappKitSource.ts
index ce6e4d68..4098afa6 100644
--- a/packages/vechain-kit/src/hooks/login/useConnectWithDappKitSource.ts
+++ b/packages/vechain-kit/src/hooks/login/useConnectWithDappKitSource.ts
@@ -44,19 +44,17 @@ const extractErrorMessage = (err: unknown): string => {
/**
* Drives a dapp-kit wallet connection (setSource + connect) while reflecting
* progress in the ConnectModal's local sub-content state (loading/error).
- *
- * Uses the legacy `connect()` API rather than `connectV2()` because:
- * - WalletConnect's signer throws "not implemented" for V2.
- * - The VeWorld desktop extension also rejects V2 ("Attempt failed").
- * V2 is only reliable inside the VeWorld mobile in-app browser, which is
- * handled separately in ModalProvider.
*/
export const useConnectWithDappKitSource = (
source: WalletSource,
setCurrentContent: SetCurrentContent,
) => {
const { t } = useTranslation();
- const { setSource, connect: dappKitConnect } = useDAppKitWallet();
+ const {
+ setSource,
+ connect: connectV1,
+ connectV2,
+ } = useDAppKitWallet();
const { close: closeDappKitModal } = useDAppKitWalletModal();
const connect = useCallback(async () => {
@@ -76,7 +74,9 @@ export const useConnectWithDappKitSource = (
!window.vechain
) {
window.open(
- `${VEWORLD_UNIVERSAL_LINK}${encodeURIComponent(window.location.href)}`,
+ `${VEWORLD_UNIVERSAL_LINK}${encodeURIComponent(
+ window.location.href,
+ )}`,
'_self',
);
return;
@@ -101,7 +101,15 @@ export const useConnectWithDappKitSource = (
try {
setSource(source);
- await dappKitConnect();
+
+ // ConnectModal closes automatically when useWallet flips
+ // `connection.isConnected` to true.
+ if (source === 'veworld') {
+ await connectV2(null);
+ } else {
+ await connectV1();
+ }
+
// WalletConnect side-effect: setSource('wallet-connect') +
// connect() causes dapp-kit's signer to call
// CustomWalletConnectModal.openModal({uri}), which fires
@@ -139,7 +147,8 @@ export const useConnectWithDappKitSource = (
}, [
source,
setSource,
- dappKitConnect,
+ connectV1,
+ connectV2,
closeDappKitModal,
setCurrentContent,
t,
diff --git a/packages/vechain-kit/src/languages/en.json b/packages/vechain-kit/src/languages/en.json
index cc3a0357..f8e7c20e 100644
--- a/packages/vechain-kit/src/languages/en.json
+++ b/packages/vechain-kit/src/languages/en.json
@@ -501,5 +501,6 @@
"{{amount}} {{symbol}} is on its way to {{recipient}}.": "{{amount}} {{symbol}} is on its way to {{recipient}}.",
"NFT sent": "NFT sent",
"{{nft}} is now in {{recipient}}’s wallet.": "{{nft}} is now in {{recipient}}’s wallet.",
- "Amount adjusted from {{original}} to {{adjusted}} {{symbol}} to cover the transaction fee.": "Amount adjusted from {{original}} to {{adjusted}} {{symbol}} to cover the transaction fee."
+ "Amount adjusted from {{original}} to {{adjusted}} {{symbol}} to cover the transaction fee.": "Amount adjusted from {{original}} to {{adjusted}} {{symbol}} to cover the transaction fee.",
+ "Change connected accounts": "Change connected accounts"
}