diff --git a/.changeset/eleven-tigers-call.md b/.changeset/eleven-tigers-call.md
new file mode 100644
index 0000000000..94b7f75104
--- /dev/null
+++ b/.changeset/eleven-tigers-call.md
@@ -0,0 +1,22 @@
+---
+'@reown/appkit-adapter-solana': patch
+'@reown/appkit-adapter-wagmi': patch
+'@reown/appkit-scaffold-ui': patch
+'@reown/appkit': patch
+'@reown/appkit-core': patch
+'@reown/appkit-siwe': patch
+'@reown/appkit-adapter-ethers': patch
+'@reown/appkit-adapter-ethers5': patch
+'@reown/appkit-utils': patch
+'@reown/appkit-cdn': patch
+'@reown/appkit-cli': patch
+'@reown/appkit-common': patch
+'@reown/appkit-experimental': patch
+'@reown/appkit-polyfills': patch
+'@reown/appkit-siwx': patch
+'@reown/appkit-ui': patch
+'@reown/appkit-wallet': patch
+'@reown/appkit-wallet-button': patch
+---
+
+Fixed an issue where connectors did not remain connected after page refresh despite being connected previously
\ No newline at end of file
diff --git a/.changeset/metal-coins-guess.md b/.changeset/metal-coins-guess.md
new file mode 100644
index 0000000000..a888e6c270
--- /dev/null
+++ b/.changeset/metal-coins-guess.md
@@ -0,0 +1,22 @@
+---
+'@reown/appkit-scaffold-ui': patch
+'@reown/appkit-core': patch
+'@reown/appkit-siwx': patch
+'@reown/appkit-adapter-ethers': patch
+'@reown/appkit-adapter-ethers5': patch
+'@reown/appkit-adapter-solana': patch
+'@reown/appkit-adapter-wagmi': patch
+'@reown/appkit': patch
+'@reown/appkit-utils': patch
+'@reown/appkit-cdn': patch
+'@reown/appkit-cli': patch
+'@reown/appkit-common': patch
+'@reown/appkit-experimental': patch
+'@reown/appkit-polyfills': patch
+'@reown/appkit-siwe': patch
+'@reown/appkit-ui': patch
+'@reown/appkit-wallet': patch
+'@reown/appkit-wallet-button': patch
+---
+
+Fix logic for authentication header on CloudAuthSIWX
diff --git a/.changeset/popular-items-sneeze.md b/.changeset/popular-items-sneeze.md
new file mode 100644
index 0000000000..038211c49c
--- /dev/null
+++ b/.changeset/popular-items-sneeze.md
@@ -0,0 +1,22 @@
+---
+'@reown/appkit-adapter-solana': patch
+'@reown/appkit-adapter-wagmi': patch
+'@reown/appkit-scaffold-ui': patch
+'@reown/appkit': patch
+'@reown/appkit-core': patch
+'@reown/appkit-siwe': patch
+'@reown/appkit-adapter-ethers': patch
+'@reown/appkit-adapter-ethers5': patch
+'@reown/appkit-utils': patch
+'@reown/appkit-cdn': patch
+'@reown/appkit-cli': patch
+'@reown/appkit-common': patch
+'@reown/appkit-experimental': patch
+'@reown/appkit-polyfills': patch
+'@reown/appkit-siwx': patch
+'@reown/appkit-ui': patch
+'@reown/appkit-wallet': patch
+'@reown/appkit-wallet-button': patch
+---
+
+Fixed an issue where Coinbase Wallet wasn't working on iOS safari
diff --git a/.changeset/sixty-foxes-raise.md b/.changeset/sixty-foxes-raise.md
new file mode 100644
index 0000000000..e877e7fc2e
--- /dev/null
+++ b/.changeset/sixty-foxes-raise.md
@@ -0,0 +1,22 @@
+---
+'@reown/appkit-adapter-ethers5': patch
+'@reown/appkit-adapter-ethers': patch
+'@reown/appkit-adapter-solana': patch
+'@reown/appkit-adapter-wagmi': patch
+'@reown/appkit-utils': patch
+'@reown/appkit-scaffold-ui': patch
+'@reown/appkit': patch
+'@reown/appkit-common': patch
+'@reown/appkit-core': patch
+'@reown/appkit-siwe': patch
+'@reown/appkit-siwx': patch
+'@reown/appkit-ui': patch
+'@reown/appkit-cdn': patch
+'@reown/appkit-cli': patch
+'@reown/appkit-experimental': patch
+'@reown/appkit-polyfills': patch
+'@reown/appkit-wallet': patch
+'@reown/appkit-wallet-button': patch
+---
+
+Improve send flow UX with better error handling
diff --git a/.changeset/tame-students-own.md b/.changeset/tame-students-own.md
new file mode 100644
index 0000000000..ca60cb78c9
--- /dev/null
+++ b/.changeset/tame-students-own.md
@@ -0,0 +1,22 @@
+---
+'@reown/appkit-adapter-ethers5': patch
+'@reown/appkit-adapter-ethers': patch
+'@reown/appkit-adapter-solana': patch
+'@reown/appkit-adapter-wagmi': patch
+'@reown/appkit-utils': patch
+'@reown/appkit-scaffold-ui': patch
+'@reown/appkit': patch
+'@reown/appkit-common': patch
+'@reown/appkit-core': patch
+'@reown/appkit-ui': patch
+'@reown/appkit-cdn': patch
+'@reown/appkit-cli': patch
+'@reown/appkit-experimental': patch
+'@reown/appkit-polyfills': patch
+'@reown/appkit-siwe': patch
+'@reown/appkit-siwx': patch
+'@reown/appkit-wallet': patch
+'@reown/appkit-wallet-button': patch
+---
+
+Fixed an issue where connector id from Local Storage wasn't in sync
diff --git a/.changeset/tiny-apples-hang.md b/.changeset/tiny-apples-hang.md
new file mode 100644
index 0000000000..2a0affc300
--- /dev/null
+++ b/.changeset/tiny-apples-hang.md
@@ -0,0 +1,22 @@
+---
+'@reown/appkit-adapter-solana': patch
+'@reown/appkit-adapter-wagmi': patch
+'@reown/appkit-scaffold-ui': patch
+'@reown/appkit': patch
+'@reown/appkit-core': patch
+'@reown/appkit-siwe': patch
+'@reown/appkit-adapter-ethers': patch
+'@reown/appkit-adapter-ethers5': patch
+'@reown/appkit-utils': patch
+'@reown/appkit-cdn': patch
+'@reown/appkit-cli': patch
+'@reown/appkit-common': patch
+'@reown/appkit-experimental': patch
+'@reown/appkit-polyfills': patch
+'@reown/appkit-siwx': patch
+'@reown/appkit-ui': patch
+'@reown/appkit-wallet': patch
+'@reown/appkit-wallet-button': patch
+---
+
+Fixed an issue where adapters and connectors were not synchronized
\ No newline at end of file
diff --git a/apps/builder/app/page.tsx b/apps/builder/app/page.tsx
index 3eb2e2aca8..61bcddb332 100644
--- a/apps/builder/app/page.tsx
+++ b/apps/builder/app/page.tsx
@@ -12,7 +12,7 @@ export default function Page() {
'page-container flex flex-col-reverse items-center md:items-start md:flex-row p-4 bg-background gap-4 pt-10 md:pt-4 h-full overflow-auto'
)}
>
-
+
diff --git a/apps/builder/components/branding-header.tsx b/apps/builder/components/branding-header.tsx
index 6f9dcaa1ae..a044806ea0 100644
--- a/apps/builder/components/branding-header.tsx
+++ b/apps/builder/components/branding-header.tsx
@@ -18,7 +18,9 @@ export function BrandingHeader({ className }: { className?: string }) {
/>
-
AppKit demo
+
+ AppKit Demo
+
Use our AppKit demo to test and design onchain UX
diff --git a/apps/builder/components/configuration-sections/section-design.tsx b/apps/builder/components/configuration-sections/section-design.tsx
index b0ca33ae7b..7276e920fc 100644
--- a/apps/builder/components/configuration-sections/section-design.tsx
+++ b/apps/builder/components/configuration-sections/section-design.tsx
@@ -16,11 +16,6 @@ export function SectionDesign() {
const { fontFamily, mixColor, accentColor, borderRadius } = useSnapshot(ThemeStore.state)
const [radius, setRadius] = React.useState('M')
- function handleColorPickerClick(inputId: string) {
- const input = document.getElementById(inputId)
- input?.click()
- }
-
function handleAccentColorChange(e: React.ChangeEvent
) {
const newColor = e.target.value
if (/^#[0-9A-F]{6}$/i.test(newColor)) {
@@ -150,15 +145,14 @@ export function SectionDesign() {
))}
-
+
@@ -202,15 +196,14 @@ export function SectionDesign() {
))}
-
+
diff --git a/apps/builder/providers/appkit-context-provider.tsx b/apps/builder/providers/appkit-context-provider.tsx
index 931ce74535..c21402fd02 100644
--- a/apps/builder/providers/appkit-context-provider.tsx
+++ b/apps/builder/providers/appkit-context-provider.tsx
@@ -11,6 +11,7 @@ import { UniqueIdentifier } from '@dnd-kit/core'
import { defaultCustomizationConfig } from '@/lib/config'
import { useTheme } from 'next-themes'
import { inter } from '@/lib/fonts'
+import { Toaster } from 'sonner'
interface AppKitProviderProps {
children: ReactNode
@@ -128,10 +129,6 @@ export const ContextProvider: React.FC = ({ children }) =>
updateThemeMode(defaultCustomizationConfig.themeMode)
}
- useEffect(() => {
- setTheme(theme as ThemeMode)
- }, [])
-
useEffect(() => {
if (initialized) {
const connectMethodsOrder = appKit?.getConnectMethodsOrder()
@@ -140,6 +137,10 @@ export const ContextProvider: React.FC = ({ children }) =>
}
}, [initialized])
+ useEffect(() => {
+ appKit?.setThemeMode(theme as ThemeMode)
+ }, [])
+
const socialsEnabled = Array.isArray(features.socials)
return (
@@ -173,6 +174,7 @@ export const ContextProvider: React.FC = ({ children }) =>
resetConfigs
}}
>
+
{children}
)
diff --git a/apps/builder/public/.well-known/walletconnect.txt b/apps/builder/public/.well-known/walletconnect.txt
new file mode 100644
index 0000000000..828db6890a
--- /dev/null
+++ b/apps/builder/public/.well-known/walletconnect.txt
@@ -0,0 +1 @@
+4596612d-2141-48aa-9987-0ce8526e3a25=cd9cfc50fcb49c77b511f53fcdd336589c05f1ec6e6cc5d4fbf4ebe7f8b9cb07
\ No newline at end of file
diff --git a/packages/adapters/ethers/src/client.ts b/packages/adapters/ethers/src/client.ts
index 7a485601c9..69f739ac0a 100644
--- a/packages/adapters/ethers/src/client.ts
+++ b/packages/adapters/ethers/src/client.ts
@@ -256,7 +256,7 @@ export class EthersAdapter extends AdapterBlueprint {
connectors.forEach(connector => {
const key = connector === 'coinbase' ? 'coinbaseWalletSDK' : connector
- const injectedConnector = connector === ConstantsUtil.INJECTED_CONNECTOR_ID
+ const injectedConnector = connector === CommonConstantsUtil.CONNECTOR_ID.INJECTED
if (this.namespace) {
this.addConnector({
@@ -301,7 +301,7 @@ export class EthersAdapter extends AdapterBlueprint {
const existingConnector = this.connectors?.find(c => c.name === info?.name)
if (!existingConnector) {
- const type = PresetsUtil.ConnectorTypesMap[ConstantsUtil.EIP6963_CONNECTOR_ID]
+ const type = PresetsUtil.ConnectorTypesMap[CommonConstantsUtil.CONNECTOR_ID.EIP6963]
if (type && this.namespace) {
this.addConnector({
@@ -389,7 +389,7 @@ export class EthersAdapter extends AdapterBlueprint {
throw new Error('Provider not found')
}
- if (params.id === ConstantsUtil.AUTH_CONNECTOR_ID) {
+ if (params.id === CommonConstantsUtil.CONNECTOR_ID.AUTH) {
const provider = connector['provider'] as W3mFrameProvider
const { address, accounts } = await provider.connect()
diff --git a/packages/adapters/ethers5/src/client.ts b/packages/adapters/ethers5/src/client.ts
index 78173cca04..95f65cf9cc 100644
--- a/packages/adapters/ethers5/src/client.ts
+++ b/packages/adapters/ethers5/src/client.ts
@@ -257,7 +257,7 @@ export class Ethers5Adapter extends AdapterBlueprint {
connectors.forEach(connector => {
const key = connector === 'coinbase' ? 'coinbaseWalletSDK' : connector
- const injectedConnector = connector === ConstantsUtil.INJECTED_CONNECTOR_ID
+ const injectedConnector = connector === CommonConstantsUtil.CONNECTOR_ID.INJECTED
if (this.namespace) {
this.addConnector({
@@ -302,7 +302,7 @@ export class Ethers5Adapter extends AdapterBlueprint {
const existingConnector = this.connectors?.find(c => c.name === info?.name)
if (!existingConnector) {
- const type = PresetsUtil.ConnectorTypesMap[ConstantsUtil.EIP6963_CONNECTOR_ID]
+ const type = PresetsUtil.ConnectorTypesMap[CommonConstantsUtil.CONNECTOR_ID.EIP6963]
if (type && this.namespace) {
this.addConnector({
@@ -380,7 +380,7 @@ export class Ethers5Adapter extends AdapterBlueprint {
throw new Error('Provider not found')
}
- if (params.id === ConstantsUtil.AUTH_CONNECTOR_ID) {
+ if (params.id === CommonConstantsUtil.CONNECTOR_ID.AUTH) {
const provider = connector['provider'] as W3mFrameProvider
const { address, accounts } = await provider.connect()
diff --git a/packages/adapters/solana/src/client.ts b/packages/adapters/solana/src/client.ts
index 51a4f8a141..eb5d5f1d67 100644
--- a/packages/adapters/solana/src/client.ts
+++ b/packages/adapters/solana/src/client.ts
@@ -13,7 +13,7 @@ import {
type ConnectorType,
type Provider
} from '@reown/appkit-core'
-import { ConstantsUtil, ErrorUtil, PresetsUtil } from '@reown/appkit-utils'
+import { ErrorUtil, PresetsUtil } from '@reown/appkit-utils'
import { SolConstantsUtil } from '@reown/appkit-utils/solana'
import type { W3mFrameProvider } from '@reown/appkit-wallet'
import { AdapterBlueprint } from '@reown/appkit/adapters'
@@ -68,6 +68,11 @@ export class SolanaAdapter extends AdapterBlueprint {
})
}
+ // We don't need to set auth provider since we already set it in syncConnectors
+ public override setAuthProvider() {
+ return undefined
+ }
+
public syncConnectors(options: AppKitOptions, appKit: AppKit) {
if (!options.projectId) {
AlertController.open(ErrorUtil.ALERT_ERRORS.PROJECT_ID_NOT_CONFIGURED, 'error')
@@ -101,7 +106,7 @@ export class SolanaAdapter extends AdapterBlueprint {
})
this.addConnector({
- id: ConstantsUtil.AUTH_CONNECTOR_ID,
+ id: CommonConstantsUtil.CONNECTOR_ID.AUTH,
type: 'AUTH',
provider: this.authProvider as unknown as W3mFrameProvider,
name: 'Auth',
@@ -111,7 +116,7 @@ export class SolanaAdapter extends AdapterBlueprint {
}
// Add Coinbase Wallet if available
- if (typeof window !== 'undefined' && 'coinbaseSolana' in window) {
+ if (CoreHelperUtil.isClient() && 'coinbaseSolana' in window) {
this.addConnector({
id: 'coinbaseWallet',
type: 'EXTERNAL',
@@ -123,7 +128,7 @@ export class SolanaAdapter extends AdapterBlueprint {
}),
name: 'Coinbase Wallet',
chain: this.namespace as ChainNamespace,
- explorerId: PresetsUtil.ConnectorExplorerIds[ConstantsUtil.COINBASE_SDK_CONNECTOR_ID],
+ explorerId: PresetsUtil.ConnectorExplorerIds[CommonConstantsUtil.CONNECTOR_ID.COINBASE_SDK],
chains: []
})
}
@@ -333,7 +338,7 @@ export class SolanaAdapter extends AdapterBlueprint {
public async switchNetwork(params: AdapterBlueprint.SwitchNetworkParams): Promise {
const { caipNetwork, provider, providerType } = params
- if (providerType === 'ID_AUTH') {
+ if (providerType === CommonConstantsUtil.CONNECTOR_ID.AUTH) {
await (provider as unknown as W3mFrameProvider).switchNetwork(caipNetwork.id)
const user = await (provider as unknown as W3mFrameProvider).getUser({
chainId: caipNetwork.id
diff --git a/packages/adapters/solana/src/providers/AuthProvider.ts b/packages/adapters/solana/src/providers/AuthProvider.ts
index 95a870df30..024863296a 100644
--- a/packages/adapters/solana/src/providers/AuthProvider.ts
+++ b/packages/adapters/solana/src/providers/AuthProvider.ts
@@ -1,4 +1,3 @@
-import { ConstantsUtil } from '@reown/appkit-utils'
import type {
AnyTransaction,
Connection,
@@ -15,6 +14,7 @@ import { withSolanaNamespace } from '../utils/withSolanaNamespace.js'
import base58 from 'bs58'
import { isVersionedTransaction } from '@solana/wallet-adapter-base'
import type { CaipNetwork, ChainNamespace } from '@reown/appkit-common'
+import { ConstantsUtil } from '@reown/appkit-common'
export type AuthProviderConfig = {
getProvider: () => W3mFrameProvider
@@ -26,7 +26,7 @@ export type AuthProviderConfig = {
}
export class AuthProvider extends ProviderEventEmitter implements Provider, ProviderAuthMethods {
- public readonly name = ConstantsUtil.AUTH_CONNECTOR_ID
+ public readonly name = ConstantsUtil.CONNECTOR_ID.AUTH
public readonly type = 'AUTH'
private readonly getProvider: AuthProviderConfig['getProvider']
diff --git a/packages/adapters/wagmi/src/client.ts b/packages/adapters/wagmi/src/client.ts
index fd2cbe43f9..bf11c49589 100644
--- a/packages/adapters/wagmi/src/client.ts
+++ b/packages/adapters/wagmi/src/client.ts
@@ -1,5 +1,5 @@
import type UniversalProvider from '@walletconnect/universal-provider'
-import type { AppKitNetwork, BaseNetwork, CaipNetwork } from '@reown/appkit-common'
+import type { AppKitNetwork, BaseNetwork, CaipNetwork, ChainNamespace } from '@reown/appkit-common'
import { AdapterBlueprint } from '@reown/appkit/adapters'
import { CoreHelperUtil } from '@reown/appkit-core'
import {
@@ -27,7 +27,8 @@ import {
getAccount,
prepareTransactionRequest,
reconnect,
- watchPendingTransactions
+ watchPendingTransactions,
+ watchConnectors
} from '@wagmi/core'
import { type Chain } from '@wagmi/core/chains'
@@ -45,7 +46,7 @@ import {
type ConnectorType,
type Provider
} from '@reown/appkit-core'
-import { CaipNetworksUtil, ConstantsUtil, PresetsUtil } from '@reown/appkit-utils'
+import { CaipNetworksUtil, PresetsUtil } from '@reown/appkit-utils'
import {
formatUnits,
parseUnits,
@@ -104,7 +105,7 @@ export class WagmiAdapter extends AdapterBlueprint {
throw new Error('WagmiAdapter:getAccounts - connector is undefined')
}
- if (connector.id === ConstantsUtil.AUTH_CONNECTOR_ID) {
+ if (connector.id === CommonConstantsUtil.CONNECTOR_ID.AUTH) {
const provider = connector['provider'] as W3mFrameProvider
const { address, accounts } = await provider.connect()
@@ -186,7 +187,6 @@ export class WagmiAdapter extends AdapterBlueprint {
}
}
})
-
watchConnections(this.wagmiConfig, {
onChange: connections => {
if (connections.length === 0) {
@@ -232,9 +232,7 @@ export class WagmiAdapter extends AdapterBlueprint {
customConnectors.push(
authConnector({
chains: this.wagmiChains,
- options: { projectId: options.projectId },
- provider: this.availableConnectors.find(c => c.id === ConstantsUtil.AUTH_CONNECTOR_ID)
- ?.provider as W3mFrameProvider
+ options: { projectId: options.projectId }
})
)
}
@@ -358,40 +356,48 @@ export class WagmiAdapter extends AdapterBlueprint {
return formatUnits(params.value, params.decimals)
}
- public syncConnectors(options: AppKitOptions, appKit: AppKit) {
- this.addWagmiConnectors(options, appKit)
+ private addWagmiConnector(connector: Connector, options: AppKitOptions) {
+ /*
+ * We don't need to set auth connector or walletConnect connector
+ * from wagmi since we already set it in chain adapter blueprint
+ */
+ if (
+ connector.id === CommonConstantsUtil.CONNECTOR_ID.AUTH ||
+ connector.id === CommonConstantsUtil.CONNECTOR_ID.WALLET_CONNECT
+ ) {
+ return
+ }
- const connectors = this.wagmiConfig.connectors.map(connector => ({
- ...connector,
- chain: this.namespace
- }))
+ this.addConnector({
+ id: connector.id,
+ explorerId: PresetsUtil.ConnectorExplorerIds[connector.id],
+ imageUrl: options?.connectorImages?.[connector.id] ?? connector.icon,
+ name: PresetsUtil.ConnectorNamesMap[connector.id] ?? connector.name,
+ imageId: PresetsUtil.ConnectorImageIds[connector.id],
+ type: PresetsUtil.ConnectorTypesMap[connector.type] ?? 'EXTERNAL',
+ info:
+ connector.id === CommonConstantsUtil.CONNECTOR_ID.INJECTED
+ ? undefined
+ : { rdns: connector.id },
+ chain: this.namespace as ChainNamespace,
+ chains: []
+ })
+ }
- const uniqueIds = new Set()
- const filteredConnectors = connectors.filter(item => {
- const isDuplicate = uniqueIds.has(item.id)
- uniqueIds.add(item.id)
+ public syncConnectors(options: AppKitOptions, appKit: AppKit) {
+ // Add wagmi connectors
+ this.addWagmiConnectors(options, appKit)
- return !isDuplicate
- })
+ // Add current wagmi connectors to chain adapter blueprint
+ this.wagmiConfig.connectors.forEach(connector => this.addWagmiConnector(connector, options))
- filteredConnectors.forEach(connector => {
- const shouldSkip = ConstantsUtil.AUTH_CONNECTOR_ID === connector.id
-
- const injectedConnector = connector.id === ConstantsUtil.INJECTED_CONNECTOR_ID
-
- if (!shouldSkip && this.namespace) {
- this.addConnector({
- id: connector.id,
- explorerId: PresetsUtil.ConnectorExplorerIds[connector.id],
- imageUrl: options?.connectorImages?.[connector.id] ?? connector.icon,
- name: PresetsUtil.ConnectorNamesMap[connector.id] ?? connector.name,
- imageId: PresetsUtil.ConnectorImageIds[connector.id],
- type: PresetsUtil.ConnectorTypesMap[connector.type] ?? 'EXTERNAL',
- info: injectedConnector ? undefined : { rdns: connector.id },
- chain: this.namespace,
- chains: []
- })
- }
+ /*
+ * Watch for new connectors. This is needed because some EIP6963
+ * connectors are added later in the process the initial setup
+ */
+ watchConnectors(this.wagmiConfig, {
+ onChange: connectors =>
+ connectors.forEach(connector => this.addWagmiConnector(connector, options))
})
}
@@ -443,7 +449,7 @@ export class WagmiAdapter extends AdapterBlueprint {
throw new Error('connectionControllerClient:connectExternal - connector is undefined')
}
- if (provider && info && connector.id === ConstantsUtil.EIP6963_CONNECTOR_ID) {
+ if (provider && info && connector.id === CommonConstantsUtil.CONNECTOR_ID.EIP6963) {
// @ts-expect-error Exists on EIP6963Connector
connector.setEip6963Wallet?.({ provider, info })
}
diff --git a/packages/adapters/wagmi/src/connectors/AuthConnector.ts b/packages/adapters/wagmi/src/connectors/AuthConnector.ts
index a82190a9a6..ddab72db2c 100644
--- a/packages/adapters/wagmi/src/connectors/AuthConnector.ts
+++ b/packages/adapters/wagmi/src/connectors/AuthConnector.ts
@@ -3,7 +3,7 @@ import { W3mFrameProvider } from '@reown/appkit-wallet'
import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common'
import { SwitchChainError, getAddress } from 'viem'
import type { Address } from 'viem'
-import { ConstantsUtil, ErrorUtil } from '@reown/appkit-utils'
+import { ErrorUtil } from '@reown/appkit-utils'
import { NetworkUtil } from '@reown/appkit-common'
import { W3mFrameProviderSingleton } from '@reown/appkit/auth-provider'
import { AlertController } from '@reown/appkit-core'
@@ -16,7 +16,6 @@ interface W3mFrameProviderOptions {
export type AuthParameters = {
chains?: CreateConfigParameters['chains']
options: W3mFrameProviderOptions
- provider: W3mFrameProvider
}
// -- Connector ------------------------------------------------------------------------------------
@@ -32,9 +31,9 @@ export function authConnector(parameters: AuthParameters) {
}
return createConnector(config => ({
- id: ConstantsUtil.AUTH_CONNECTOR_ID,
+ id: CommonConstantsUtil.CONNECTOR_ID.AUTH,
name: 'AppKit Auth',
- type: 'ID_AUTH',
+ type: 'AUTH',
chain: CommonConstantsUtil.CHAIN.EVM,
async connect(options = {}) {
diff --git a/packages/adapters/wagmi/src/connectors/AuthConnectorExport.ts b/packages/adapters/wagmi/src/connectors/AuthConnectorExport.ts
index 279e382bc7..e724c12deb 100644
--- a/packages/adapters/wagmi/src/connectors/AuthConnectorExport.ts
+++ b/packages/adapters/wagmi/src/connectors/AuthConnectorExport.ts
@@ -1,8 +1,5 @@
import type { CreateConfigParameters } from '@wagmi/core'
import { authConnector as authConnectorWagmi } from './AuthConnector.js'
-import { ErrorUtil } from '@reown/appkit-utils'
-import { AlertController } from '@reown/appkit-core'
-import { W3mFrameProviderSingleton } from '@reown/appkit/auth-provider'
interface W3mFrameProviderOptions {
projectId: string
@@ -14,13 +11,5 @@ export type AuthParameters = {
}
export function authConnector(parameters: AuthParameters) {
- return authConnectorWagmi({
- ...parameters,
- provider: W3mFrameProviderSingleton.getInstance({
- projectId: parameters.options.projectId,
- onTimeout: () => {
- AlertController.open(ErrorUtil.ALERT_ERRORS.SOCIALS_TIMEOUT, 'error')
- }
- })
- })
+ return authConnectorWagmi(parameters)
}
diff --git a/packages/adapters/wagmi/src/tests/client.test.ts b/packages/adapters/wagmi/src/tests/client.test.ts
index c1bacc5be2..00e4b4e4ba 100644
--- a/packages/adapters/wagmi/src/tests/client.test.ts
+++ b/packages/adapters/wagmi/src/tests/client.test.ts
@@ -18,9 +18,11 @@ import {
watchPendingTransactions,
http
} from '@wagmi/core'
+import * as wagmiCore from '@wagmi/core'
import { mainnet } from '@wagmi/core/chains'
import { CaipNetworksUtil } from '@reown/appkit-utils'
import type UniversalProvider from '@walletconnect/universal-provider'
+import { mockAppKit } from './mocks/AppKit'
vi.mock('@wagmi/core', async () => {
const actual = await vi.importActual('@wagmi/core')
@@ -97,6 +99,26 @@ describe('WagmiAdapter', () => {
expect(adapter.namespace).toBe('eip155')
})
+ it('should set wagmi connectors', () => {
+ vi.spyOn(wagmiCore, 'watchConnectors').mockImplementation(vi.fn())
+
+ adapter.syncConnectors({ networks: [mainnet], projectId: 'YOUR_PROJECT_ID' }, mockAppKit)
+
+ expect(adapter.connectors).toStrictEqual([
+ {
+ chain: 'eip155',
+ chains: [],
+ explorerId: undefined,
+ id: 'test-connector',
+ imageId: undefined,
+ imageUrl: undefined,
+ info: { rdns: 'test-connector' },
+ name: undefined,
+ type: 'EXTERNAL'
+ }
+ ])
+ })
+
it('should not set info property for injected connector', () => {
const mockConnectors = [
{
diff --git a/packages/appkit-utils/src/ConstantsUtil.ts b/packages/appkit-utils/src/ConstantsUtil.ts
index 2eb1cbf938..8864974cd3 100644
--- a/packages/appkit-utils/src/ConstantsUtil.ts
+++ b/packages/appkit-utils/src/ConstantsUtil.ts
@@ -1,14 +1,6 @@
import type { ChainNamespace } from '@reown/appkit-common'
export const ConstantsUtil = {
- WALLET_CONNECT_CONNECTOR_ID: 'walletConnect',
- INJECTED_CONNECTOR_ID: 'injected',
- WALLET_STANDARD_CONNECTOR_ID: 'announced',
- COINBASE_CONNECTOR_ID: 'coinbaseWallet',
- COINBASE_SDK_CONNECTOR_ID: 'coinbaseWalletSDK',
- SAFE_CONNECTOR_ID: 'safe',
- LEDGER_CONNECTOR_ID: 'ledger',
-
/* Connector names */
METMASK_CONNECTOR_NAME: 'MetaMask',
TRUST_CONNECTOR_NAME: 'Trust Wallet',
@@ -20,8 +12,6 @@ export const ConstantsUtil = {
BITGET_CONNECTOR_NAME: 'Bitget Wallet',
FRONTIER_CONNECTOR_NAME: 'Frontier',
- EIP6963_CONNECTOR_ID: 'eip6963',
- AUTH_CONNECTOR_ID: 'ID_AUTH',
EIP155: 'eip155' as ChainNamespace,
ADD_CHAIN_METHOD: 'wallet_addEthereumChain',
EIP6963_ANNOUNCE_EVENT: 'eip6963:announceProvider',
diff --git a/packages/appkit-utils/src/PresetsUtil.ts b/packages/appkit-utils/src/PresetsUtil.ts
index 7a72ba02b9..c2a1e35aa0 100644
--- a/packages/appkit-utils/src/PresetsUtil.ts
+++ b/packages/appkit-utils/src/PresetsUtil.ts
@@ -1,15 +1,16 @@
import type { ConnectorType } from '@reown/appkit-core'
+import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common'
import { ConstantsUtil } from './ConstantsUtil.js'
export const PresetsUtil = {
ConnectorExplorerIds: {
- [ConstantsUtil.COINBASE_CONNECTOR_ID]:
+ [CommonConstantsUtil.CONNECTOR_ID.COINBASE]:
'fd20dc426fb37566d803205b19bbc1d4096b248ac04548e3cfb6b3a38bd033aa',
- [ConstantsUtil.COINBASE_SDK_CONNECTOR_ID]:
+ [CommonConstantsUtil.CONNECTOR_ID.COINBASE_SDK]:
'fd20dc426fb37566d803205b19bbc1d4096b248ac04548e3cfb6b3a38bd033aa',
- [ConstantsUtil.SAFE_CONNECTOR_ID]:
+ [CommonConstantsUtil.CONNECTOR_ID.SAFE]:
'225affb176778569276e484e1b92637ad061b01e13a048b35a9d280c3b58970f',
- [ConstantsUtil.LEDGER_CONNECTOR_ID]:
+ [CommonConstantsUtil.CONNECTOR_ID.LEDGER]:
'19177a98252e07ddfc9af2083ba8e07ef627cb6103467ffebb3f8f4205fd7927',
/* Connector names */
@@ -84,28 +85,28 @@ export const PresetsUtil = {
} as Record,
ConnectorImageIds: {
- [ConstantsUtil.COINBASE_CONNECTOR_ID]: '0c2840c3-5b04-4c44-9661-fbd4b49e1800',
- [ConstantsUtil.COINBASE_SDK_CONNECTOR_ID]: '0c2840c3-5b04-4c44-9661-fbd4b49e1800',
- [ConstantsUtil.SAFE_CONNECTOR_ID]: '461db637-8616-43ce-035a-d89b8a1d5800',
- [ConstantsUtil.LEDGER_CONNECTOR_ID]: '54a1aa77-d202-4f8d-0fb2-5d2bb6db0300',
- [ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID]: 'ef1a1fcf-7fe8-4d69-bd6d-fda1345b4400',
- [ConstantsUtil.INJECTED_CONNECTOR_ID]: '07ba87ed-43aa-4adf-4540-9e6a2b9cae00'
+ [CommonConstantsUtil.CONNECTOR_ID.COINBASE]: '0c2840c3-5b04-4c44-9661-fbd4b49e1800',
+ [CommonConstantsUtil.CONNECTOR_ID.COINBASE_SDK]: '0c2840c3-5b04-4c44-9661-fbd4b49e1800',
+ [CommonConstantsUtil.CONNECTOR_ID.SAFE]: '461db637-8616-43ce-035a-d89b8a1d5800',
+ [CommonConstantsUtil.CONNECTOR_ID.LEDGER]: '54a1aa77-d202-4f8d-0fb2-5d2bb6db0300',
+ [CommonConstantsUtil.CONNECTOR_ID.WALLET_CONNECT]: 'ef1a1fcf-7fe8-4d69-bd6d-fda1345b4400',
+ [CommonConstantsUtil.CONNECTOR_ID.INJECTED]: '07ba87ed-43aa-4adf-4540-9e6a2b9cae00'
} as Record,
ConnectorNamesMap: {
- [ConstantsUtil.INJECTED_CONNECTOR_ID]: 'Browser Wallet',
- [ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID]: 'WalletConnect',
- [ConstantsUtil.COINBASE_CONNECTOR_ID]: 'Coinbase',
- [ConstantsUtil.COINBASE_SDK_CONNECTOR_ID]: 'Coinbase',
- [ConstantsUtil.LEDGER_CONNECTOR_ID]: 'Ledger',
- [ConstantsUtil.SAFE_CONNECTOR_ID]: 'Safe'
+ [CommonConstantsUtil.CONNECTOR_ID.INJECTED]: 'Browser Wallet',
+ [CommonConstantsUtil.CONNECTOR_ID.WALLET_CONNECT]: 'WalletConnect',
+ [CommonConstantsUtil.CONNECTOR_ID.COINBASE]: 'Coinbase',
+ [CommonConstantsUtil.CONNECTOR_ID.COINBASE_SDK]: 'Coinbase',
+ [CommonConstantsUtil.CONNECTOR_ID.LEDGER]: 'Ledger',
+ [CommonConstantsUtil.CONNECTOR_ID.SAFE]: 'Safe'
} as Record,
ConnectorTypesMap: {
- [ConstantsUtil.INJECTED_CONNECTOR_ID]: 'INJECTED',
- [ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID]: 'WALLET_CONNECT',
- [ConstantsUtil.EIP6963_CONNECTOR_ID]: 'ANNOUNCED',
- [ConstantsUtil.AUTH_CONNECTOR_ID]: 'AUTH'
+ [CommonConstantsUtil.CONNECTOR_ID.INJECTED]: 'INJECTED',
+ [CommonConstantsUtil.CONNECTOR_ID.WALLET_CONNECT]: 'WALLET_CONNECT',
+ [CommonConstantsUtil.CONNECTOR_ID.EIP6963]: 'ANNOUNCED',
+ [CommonConstantsUtil.CONNECTOR_ID.AUTH]: 'AUTH'
} as Record,
WalletConnectRpcChainIds: [
diff --git a/packages/appkit/src/adapters/ChainAdapterBlueprint.ts b/packages/appkit/src/adapters/ChainAdapterBlueprint.ts
index 8a8f034fc4..f78d385f45 100644
--- a/packages/appkit/src/adapters/ChainAdapterBlueprint.ts
+++ b/packages/appkit/src/adapters/ChainAdapterBlueprint.ts
@@ -1,5 +1,6 @@
import {
getW3mThemeVariables,
+ ConstantsUtil as CommonConstantsUtil,
type CaipAddress,
type CaipNetwork,
type ChainNamespace
@@ -19,16 +20,22 @@ import {
} from '@reown/appkit-core'
import type UniversalProvider from '@walletconnect/universal-provider'
import type { W3mFrameProvider } from '@reown/appkit-wallet'
-import { ConstantsUtil, PresetsUtil } from '@reown/appkit-utils'
+import { PresetsUtil } from '@reown/appkit-utils'
import type { AppKitOptions } from '../utils/index.js'
import type { AppKit } from '../client.js'
import { snapshot } from 'valtio/vanilla'
-type EventName = 'disconnect' | 'accountChanged' | 'switchNetwork' | 'pendingTransactions'
+type EventName =
+ | 'disconnect'
+ | 'accountChanged'
+ | 'switchNetwork'
+ | 'connectors'
+ | 'pendingTransactions'
type EventData = {
disconnect: () => void
accountChanged: { address: string; chainId?: number | string }
switchNetwork: { address?: string; chainId: number | string }
+ connectors: ChainAdapterConnector[]
pendingTransactions: () => void
}
type EventCallback = (data: EventData[T]) => void
@@ -92,11 +99,11 @@ export abstract class AdapterBlueprint<
*/
public setUniversalProvider(universalProvider: UniversalProvider) {
this.addConnector({
- id: ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID,
+ id: CommonConstantsUtil.CONNECTOR_ID.WALLET_CONNECT,
type: 'WALLET_CONNECT',
- name: PresetsUtil.ConnectorNamesMap[ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID],
+ name: PresetsUtil.ConnectorNamesMap[CommonConstantsUtil.CONNECTOR_ID.WALLET_CONNECT],
provider: universalProvider,
- imageId: PresetsUtil.ConnectorImageIds[ConstantsUtil.WALLET_CONNECT_CONNECTOR_ID],
+ imageId: PresetsUtil.ConnectorImageIds[CommonConstantsUtil.CONNECTOR_ID.WALLET_CONNECT],
chain: this.namespace,
chains: []
} as unknown as Connector)
@@ -108,11 +115,11 @@ export abstract class AdapterBlueprint<
*/
public setAuthProvider(authProvider: W3mFrameProvider): void {
this.addConnector({
- id: ConstantsUtil.AUTH_CONNECTOR_ID,
+ id: CommonConstantsUtil.CONNECTOR_ID.AUTH,
type: 'AUTH',
name: 'Auth',
provider: authProvider,
- imageId: PresetsUtil.ConnectorImageIds[ConstantsUtil.AUTH_CONNECTOR_ID],
+ imageId: PresetsUtil.ConnectorImageIds[CommonConstantsUtil.CONNECTOR_ID.AUTH],
chain: this.namespace,
chains: []
} as unknown as Connector)
@@ -123,9 +130,9 @@ export abstract class AdapterBlueprint<
* @param {...Connector} connectors - The connectors to add
*/
protected addConnector(...connectors: Connector[]) {
- if (connectors.some(connector => connector.id === 'ID_AUTH')) {
+ if (connectors.some(connector => connector.id === CommonConstantsUtil.CONNECTOR_ID.AUTH)) {
const authConnector = connectors.find(
- connector => connector.id === 'ID_AUTH'
+ connector => connector.id === CommonConstantsUtil.CONNECTOR_ID.AUTH
) as AuthConnector
const optionsState = snapshot(OptionsController.state)
@@ -155,6 +162,8 @@ export abstract class AdapterBlueprint<
return true
})
+
+ this.emit('connectors', this.availableConnectors)
}
protected setStatus(status: AccountControllerState['status'], chainNamespace?: ChainNamespace) {
diff --git a/packages/appkit/src/adapters/index.ts b/packages/appkit/src/adapters/index.ts
index 9babae1e07..f09b2ba1f4 100644
--- a/packages/appkit/src/adapters/index.ts
+++ b/packages/appkit/src/adapters/index.ts
@@ -1 +1,2 @@
export { AdapterBlueprint } from './ChainAdapterBlueprint.js'
+export type { ChainAdapterConnector } from './ChainAdapterConnector.js'
diff --git a/packages/appkit/src/client.ts b/packages/appkit/src/client.ts
index f7c5672afd..7738ccac83 100644
--- a/packages/appkit/src/client.ts
+++ b/packages/appkit/src/client.ts
@@ -12,7 +12,6 @@ import {
type UseAppKitNetworkReturn,
type NetworkControllerClient,
type ConnectionControllerClient,
- ConstantsUtil as CoreConstantsUtil,
type ConnectorType,
type WriteContractArgs,
type Provider,
@@ -20,6 +19,7 @@ import {
type EstimateGasTransactionArgs,
type AccountControllerState,
type AdapterNetworkState,
+ ConstantsUtil as CoreConstantsUtil,
type Features,
SIWXUtil,
type ConnectionStatus,
@@ -50,12 +50,12 @@ import {
} from '@reown/appkit-core'
import { setColorTheme, setThemeVariables } from '@reown/appkit-ui'
import {
- ConstantsUtil,
type CaipNetwork,
type ChainNamespace,
type CaipAddress,
type CaipNetworkId,
NetworkUtil,
+ ConstantsUtil,
ParseUtil
} from '@reown/appkit-common'
import type { AppKitOptions } from './utils/TypesUtil.js'
@@ -214,14 +214,10 @@ export class AppKit {
) {
this.caipNetworks = this.extendCaipNetworks(options)
this.defaultCaipNetwork = this.extendDefaultCaipNetwork(options)
- await this.initControllers(options)
- this.createAuthProvider()
- await this.createUniversalProvider()
+ this.initControllers(options)
this.createClients()
ChainController.initialize(options.adapters ?? [], this.caipNetworks)
- this.chainAdapters = await this.createAdapters(
- options.adapters as unknown as AdapterBlueprint[]
- )
+ this.chainAdapters = this.createAdapters(options.adapters as unknown as AdapterBlueprint[])
await this.initChainAdapters()
this.syncRequestedNetworks()
await this.initOrContinue()
@@ -662,7 +658,7 @@ export class AppKit {
}
// -- Private ------------------------------------------------------------------
- private async initControllers(
+ private initControllers(
options: AppKitOptions & {
adapters?: ChainAdapter[]
} & {
@@ -684,8 +680,6 @@ export class AppKit {
return
}
- this.adapters = options.adapters
-
const defaultMetaData = this.getDefaultMetaData()
if (!options.metadata && defaultMetaData) {
@@ -742,12 +736,7 @@ export class AppKit {
throw new Error('Cannot set both `siweConfig` and `siwx` options')
}
- const siwe = await import('@reown/appkit-siwe')
- if (typeof siwe.mapToSIWX !== 'function') {
- throw new Error('Please update the `@reown/appkit-siwe` package to the latest version')
- }
-
- OptionsController.setSIWX(siwe.mapToSIWX(options.siweConfig))
+ OptionsController.setSIWX(options.siweConfig.mapToSIWX())
}
}
}
@@ -815,9 +804,7 @@ export class AppKit {
connectWalletConnect: async (onUri: (uri: string) => void) => {
const adapter = this.getAdapter(ChainController.state.activeChain as ChainNamespace)
- this.universalProvider?.on('display_uri', (uri: string) => {
- onUri(uri)
- })
+ this.universalProvider?.on('display_uri', onUri)
this.setClientId(
(await this.universalProvider?.client?.core?.crypto?.getClientId()) || null
@@ -861,37 +848,16 @@ export class AppKit {
throw new Error('Adapter not found')
}
- let res: AdapterBlueprint.ConnectResult | undefined = undefined
- try {
- res = await adapter.connect({
- id,
- info,
- type,
- provider,
- chainId: caipNetwork?.id || this.getCaipNetwork()?.id,
- rpcUrl:
- caipNetwork?.rpcUrls?.default?.http?.[0] ||
- this.getCaipNetwork()?.rpcUrls?.default?.http?.[0]
- })
- /**
- * In some cases with wagmi connectors, the connector is already connected
- * which throws an `Is already connected`error. In such cases, we need to reconnect
- * to restore the session.
- * We check if the reconnect method exists (which it does for wagmi connectors) and if so
- * we attempt to reconnect and restore the session state.
- */
- } catch (error) {
- if (!adapter?.reconnect) {
- throw new Error('Adapter is not able to connect')
- }
- await adapter.reconnect({
- id,
- info,
- type,
- provider,
- chainId: this.getCaipNetwork()?.id
- })
- }
+ const res = await adapter.connect({
+ id,
+ info,
+ type,
+ provider,
+ chainId: caipNetwork?.id || this.getCaipNetwork()?.id,
+ rpcUrl:
+ caipNetwork?.rpcUrls?.default?.http?.[0] ||
+ this.getCaipNetwork()?.rpcUrls?.default?.http?.[0]
+ })
if (res) {
this.syncProvider({
@@ -930,6 +896,8 @@ export class AppKit {
await adapter?.disconnect({ provider, providerType })
+ ProviderUtil.resetChain(ChainController.state.activeChain as ChainNamespace)
+
this.setStatus('disconnected', ChainController.state.activeChain as ChainNamespace)
},
checkInstalled: (ids?: string[]) => {
@@ -1084,7 +1052,7 @@ export class AppKit {
try {
ChainController.state.activeChain = caipNetwork.chainNamespace
await this.connectionControllerClient?.connectExternal?.({
- id: UtilConstantsUtil.AUTH_CONNECTOR_ID,
+ id: ConstantsUtil.CONNECTOR_ID.AUTH,
provider: this.authProvider,
chain: caipNetwork.chainNamespace,
chainId: caipNetwork.id,
@@ -1205,8 +1173,8 @@ export class AppKit {
}
})
provider.onNotConnected(() => {
- const connectedConnector = StorageUtil.getConnectedConnector()
- const isConnectedWithAuth = connectedConnector === UtilConstantsUtil.AUTH_CONNECTOR_ID
+ const connectorId = StorageUtil.getConnectedConnectorId()
+ const isConnectedWithAuth = connectorId === ConstantsUtil.CONNECTOR_ID.AUTH
if (!isConnected && isConnectedWithAuth) {
this.setCaipAddress(undefined, ChainController.state.activeChain as ChainNamespace)
this.setLoading(false)
@@ -1220,7 +1188,7 @@ export class AppKit {
this.syncProvider({
type: UtilConstantsUtil.CONNECTOR_TYPE_AUTH as ConnectorType,
provider,
- id: UtilConstantsUtil.AUTH_CONNECTOR_ID,
+ id: ConstantsUtil.CONNECTOR_ID.AUTH,
chainNamespace: namespace
})
@@ -1270,8 +1238,8 @@ export class AppKit {
if (isConnected && this.connectionControllerClient?.connectExternal) {
await this.connectionControllerClient?.connectExternal({
- id: UtilConstantsUtil.AUTH_CONNECTOR_ID,
- info: { name: UtilConstantsUtil.AUTH_CONNECTOR_ID },
+ id: ConstantsUtil.CONNECTOR_ID.AUTH,
+ info: { name: ConstantsUtil.CONNECTOR_ID.AUTH },
type: UtilConstantsUtil.CONNECTOR_TYPE_AUTH as ConnectorType,
provider,
chainId: ChainController.state.activeCaipNetwork?.id
@@ -1451,9 +1419,7 @@ export class AppKit {
ProviderUtil.setProvider(chainNamespace, this.universalProvider)
}
- StorageUtil.setConnectedConnector(
- UtilConstantsUtil.CONNECTOR_TYPE_WALLET_CONNECT as ConnectorType
- )
+ StorageUtil.setConnectedConnectorId(ConstantsUtil.CONNECTOR_ID.WALLET_CONNECT)
let address = ''
@@ -1538,7 +1504,7 @@ export class AppKit {
ProviderUtil.setProviderId(chainNamespace, type)
ProviderUtil.setProvider(chainNamespace, provider)
- StorageUtil.setConnectedConnector(id as ConnectorType)
+ StorageUtil.setConnectedConnectorId(id)
}
private async syncAccount({
@@ -1597,15 +1563,15 @@ export class AppKit {
}
private syncConnectedWalletInfo(chainNamespace: ChainNamespace) {
- const currentActiveWallet = StorageUtil.getConnectedConnector()
+ const connectorId = StorageUtil.getConnectedConnectorId()
const providerType = ProviderUtil.state.providerIds[chainNamespace]
if (
providerType === UtilConstantsUtil.CONNECTOR_TYPE_ANNOUNCED ||
providerType === UtilConstantsUtil.CONNECTOR_TYPE_INJECTED
) {
- if (currentActiveWallet) {
- const connector = this.getConnectors().find(c => c.id === currentActiveWallet)
+ if (connectorId) {
+ const connector = this.getConnectors().find(c => c.id === connectorId)
if (connector?.info) {
this.setConnectedWalletInfo({ ...connector.info }, chainNamespace)
@@ -1624,17 +1590,19 @@ export class AppKit {
chainNamespace
)
}
- } else if (providerType === UtilConstantsUtil.COINBASE_CONNECTOR_ID) {
- const connector = this.getConnectors().find(
- c => c.id === UtilConstantsUtil.COINBASE_CONNECTOR_ID
- )
+ } else if (connectorId) {
+ if (connectorId === ConstantsUtil.CONNECTOR_ID.COINBASE) {
+ const connector = this.getConnectors().find(
+ c => c.id === ConstantsUtil.CONNECTOR_ID.COINBASE
+ )
- this.setConnectedWalletInfo(
- { name: 'Coinbase Wallet', icon: this.getConnectorImage(connector) },
- chainNamespace
- )
- } else if (currentActiveWallet) {
- this.setConnectedWalletInfo({ name: currentActiveWallet }, chainNamespace)
+ this.setConnectedWalletInfo(
+ { name: 'Coinbase Wallet', icon: this.getConnectorImage(connector) },
+ chainNamespace
+ )
+ }
+
+ this.setConnectedWalletInfo({ name: connectorId }, chainNamespace)
}
}
@@ -1714,20 +1682,16 @@ export class AppKit {
}
private async syncExistingConnection() {
- const connectedConnector = StorageUtil.getConnectedConnector() as ConnectorType
+ const connectorId = StorageUtil.getConnectedConnectorId()
const activeNamespace = StorageUtil.getActiveNamespace()
- if (connectedConnector === UtilConstantsUtil.CONNECTOR_TYPE_WALLET_CONNECT && activeNamespace) {
+ if (connectorId === ConstantsUtil.CONNECTOR_ID.WALLET_CONNECT && activeNamespace) {
this.syncWalletConnectAccount()
- } else if (
- connectedConnector &&
- connectedConnector !== UtilConstantsUtil.CONNECTOR_TYPE_W3M_AUTH &&
- activeNamespace
- ) {
+ } else if (connectorId && connectorId !== ConstantsUtil.CONNECTOR_ID.AUTH && activeNamespace) {
this.setStatus('connecting', activeNamespace as ChainNamespace)
const adapter = this.getAdapter(activeNamespace as ChainNamespace)
const res = await adapter?.syncConnection({
- id: connectedConnector,
+ id: connectorId,
chainId: this.getCaipNetwork()?.id,
namespace: activeNamespace as ChainNamespace,
rpcUrl: this.getCaipNetwork()?.rpcUrls?.default?.http?.[0] as string
@@ -1736,7 +1700,7 @@ export class AppKit {
if (res) {
const accounts = await adapter?.getAccounts({
namespace: activeNamespace as ChainNamespace,
- id: connectedConnector
+ id: connectorId
})
this.syncProvider({ ...res, chainNamespace: activeNamespace as ChainNamespace })
await this.syncAccount({ ...res, chainNamespace: activeNamespace as ChainNamespace })
@@ -1751,7 +1715,7 @@ export class AppKit {
this.setUnsupportedNetwork(res.chainId)
}
}
- } else if (connectedConnector !== UtilConstantsUtil.CONNECTOR_TYPE_W3M_AUTH) {
+ } else if (connectorId !== ConstantsUtil.CONNECTOR_ID.AUTH) {
this.setStatus('disconnected', ChainController.state.activeChain as ChainNamespace)
}
}
@@ -1763,7 +1727,7 @@ export class AppKit {
private createUniversalProvider() {
if (
!this.universalProviderInitPromise &&
- typeof window !== 'undefined' &&
+ CoreHelperUtil.isClient() &&
this.options?.projectId
) {
this.universalProviderInitPromise = this.initializeUniversalAdapter()
@@ -1815,6 +1779,7 @@ export class AppKit {
OptionsController.setUsingInjectedUniversalProvider(Boolean(this.options?.universalProvider))
this.universalProvider =
this.options.universalProvider ?? (await UniversalProvider.init(universalProviderOptions))
+ this.listenWalletConnect()
}
public async getUniversalProvider() {
@@ -1830,14 +1795,14 @@ export class AppKit {
}
private createAuthProvider() {
- const emailEnabled =
+ const isEmailEnabled =
this.options?.features?.email === undefined
? CoreConstantsUtil.DEFAULT_FEATURES.email
: this.options?.features?.email
- const socialsEnabled = this.options?.features?.socials
+ const isSocialsEnabled = this.options?.features?.socials
? this.options?.features?.socials?.length > 0
: CoreConstantsUtil.DEFAULT_FEATURES.socials
- if (this.options?.projectId && (emailEnabled || socialsEnabled)) {
+ if (!this.authProvider && this.options?.projectId && (isEmailEnabled || isSocialsEnabled)) {
this.authProvider = W3mFrameProviderSingleton.getInstance({
projectId: this.options.projectId,
onTimeout: () => {
@@ -1848,11 +1813,23 @@ export class AppKit {
}
}
- private async createAdapters(blueprints?: AdapterBlueprint[]): Promise {
- if (!this.universalProvider) {
- this.universalProvider = await this.getUniversalProvider()
+ private async createUniversalProviderForAdapter(chainNamespace: ChainNamespace) {
+ await this.getUniversalProvider()
+
+ if (this.universalProvider) {
+ this.chainAdapters?.[chainNamespace]?.setUniversalProvider?.(this.universalProvider)
+ }
+ }
+
+ private createAuthProviderForAdapter(chainNamespace: ChainNamespace) {
+ this.createAuthProvider()
+
+ if (this.authProvider) {
+ this.chainAdapters?.[chainNamespace]?.setAuthProvider?.(this.authProvider)
}
+ }
+ private createAdapters(blueprints?: AdapterBlueprint[]) {
this.syncRequestedNetworks()
return this.chainNamespaces.reduce((adapters, namespace) => {
@@ -1866,25 +1843,11 @@ export class AppKit {
projectId: this.options?.projectId,
networks: this.caipNetworks
})
- if (this.universalProvider) {
- adapters[namespace].setUniversalProvider(this.universalProvider)
- }
- if (this.authProvider) {
- adapters[namespace].setAuthProvider(this.authProvider)
- }
-
- adapters[namespace].syncConnectors(this.options, this)
} else {
adapters[namespace] = new UniversalAdapter({
namespace,
networks: this.caipNetworks
})
- if (this.universalProvider) {
- adapters[namespace].setUniversalProvider(this.universalProvider)
- }
- if (this.authProvider) {
- adapters[namespace].setAuthProvider(this.authProvider)
- }
}
ChainController.state.chains.set(namespace, {
@@ -1901,18 +1864,26 @@ export class AppKit {
}, {} as Adapters)
}
+ private async createConnectorsForAdapter(namespace: ChainNamespace) {
+ await this.createUniversalProviderForAdapter(namespace)
+ this.createAuthProviderForAdapter(namespace)
+ }
+
+ private onConnectors(chainNamespace: ChainNamespace) {
+ const adapter = this.getAdapter(chainNamespace)
+
+ adapter?.on('connectors', this.setConnectors.bind(this))
+ }
+
private async initChainAdapters() {
await Promise.all(
- // eslint-disable-next-line @typescript-eslint/require-await
this.chainNamespaces.map(async namespace => {
- if (this.options) {
- this.listenAdapter(namespace)
-
- this.setConnectors(this.chainAdapters?.[namespace]?.connectors || [])
- }
+ this.onConnectors(namespace)
+ this.listenAdapter(namespace)
+ this.chainAdapters?.[namespace].syncConnectors(this.options, this)
+ await this.createConnectorsForAdapter(namespace)
})
)
- this.listenWalletConnect()
}
private setDefaultNetwork() {
diff --git a/packages/appkit/src/tests/appkit.test.ts b/packages/appkit/src/tests/appkit.test.ts
index c0b65b0555..dee3921d8f 100644
--- a/packages/appkit/src/tests/appkit.test.ts
+++ b/packages/appkit/src/tests/appkit.test.ts
@@ -1,6 +1,6 @@
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { AppKit } from '../client'
-import { mainnet, polygon } from '../networks/index.js'
+import { base, mainnet, polygon, solana } from '../networks/index.js'
import {
AccountController,
ModalController,
@@ -18,26 +18,32 @@ import {
ConnectorController,
ChainController,
type Connector,
- StorageUtil,
CoreHelperUtil,
- AlertController
+ AlertController,
+ StorageUtil
} from '@reown/appkit-core'
-import {
- SafeLocalStorage,
- SafeLocalStorageKeys,
- type CaipNetwork,
- type SafeLocalStorageItems
-} from '@reown/appkit-common'
+import { SafeLocalStorage, SafeLocalStorageKeys, type CaipNetwork } from '@reown/appkit-common'
import { mockOptions } from './mocks/Options'
import { UniversalAdapter } from '../universal-adapter/client'
import type { AdapterBlueprint } from '../adapters/ChainAdapterBlueprint'
import { ProviderUtil } from '../store'
-import { ErrorUtil } from '@reown/appkit-utils'
+import { CaipNetworksUtil, ErrorUtil } from '@reown/appkit-utils'
+import mockUniversalAdapter from './mocks/Adapter'
import { UniversalProvider } from '@walletconnect/universal-provider'
+import mockProvider from './mocks/UniversalProvider'
// Mock all controllers and UniversalAdapterClient
vi.mock('@reown/appkit-core')
vi.mock('../universal-adapter/client')
+vi.mock('../client.ts', async () => {
+ const actual = await vi.importActual('../client.ts')
+
+ return {
+ ...actual,
+ initOrContinue: vi.fn(),
+ syncExistingConnection: vi.fn()
+ }
+})
vi.mocked(global).window = { location: { origin: '' } } as any
vi.mocked(global).document = {
@@ -61,23 +67,32 @@ describe('Base', () => {
} as any
vi.mocked(ConnectorController).getConnectors = vi.fn().mockReturnValue([])
+ vi.mocked(CaipNetworksUtil).extendCaipNetworks = vi.fn().mockReturnValue([])
+
appKit = new AppKit(mockOptions)
})
describe('Base Initialization', () => {
- it('should initialize controllers with required provided options', () => {
- expect(OptionsController.setSdkVersion).toHaveBeenCalledWith(mockOptions.sdkVersion)
- expect(OptionsController.setProjectId).toHaveBeenCalledWith(mockOptions.projectId)
- expect(OptionsController.setMetadata).toHaveBeenCalledWith(mockOptions.metadata)
-
+ it('should initialize controllers', () => {
const copyMockOptions = { ...mockOptions }
+
delete copyMockOptions.adapters
- expect(EventsController.sendEvent).toHaveBeenCalledWith(mockOptions)
- })
+ expect(EventsController.sendEvent).toHaveBeenCalledOnce()
+ expect(EventsController.sendEvent).toHaveBeenCalledWith({
+ type: 'track',
+ event: 'INITIALIZE',
+ properties: {
+ ...copyMockOptions,
+ networks: copyMockOptions.networks.map(n => n.id),
+ siweConfig: {
+ options: copyMockOptions.siweConfig?.options || {}
+ }
+ }
+ })
- it('should initialize adapters in ChainController', () => {
- expect(ChainController.initialize).toHaveBeenCalledWith(mockOptions.adapters)
+ expect(ChainController.initialize).toHaveBeenCalledOnce()
+ expect(ChainController.initialize).toHaveBeenCalledWith(mockOptions.adapters, [])
})
it('should set EIP6963 enabled by default', () => {
@@ -513,9 +528,15 @@ describe('Base', () => {
})
it('should switch network when requested', async () => {
+ vi.mocked(CaipNetworksUtil).extendCaipNetworks = vi
+ .fn()
+ .mockReturnValue([{ id: mainnet.id, name: mainnet.name }])
+
+ const mockAppKit = new AppKit(mockOptions)
+
vi.mocked(ChainController.switchActiveNetwork).mockResolvedValue(undefined)
- await appKit.switchNetwork(mainnet)
+ await mockAppKit.switchNetwork(mainnet)
expect(ChainController.switchActiveNetwork).toHaveBeenCalledWith(
expect.objectContaining({
@@ -524,7 +545,7 @@ describe('Base', () => {
})
)
- await appKit.switchNetwork(polygon)
+ await mockAppKit.switchNetwork(polygon)
expect(ChainController.switchActiveNetwork).toHaveBeenCalledTimes(1)
})
@@ -536,6 +557,11 @@ describe('Base', () => {
} as Connector
vi.mocked(ConnectorController.getConnectors).mockReturnValue([mockConnector])
+ vi.mocked(StorageUtil.getActiveNetworkProps).mockReturnValue({
+ namespace: 'eip155',
+ chainId: '1',
+ caipNetworkId: '1'
+ })
const mockAccountData = {
address: '0x123',
@@ -543,14 +569,7 @@ describe('Base', () => {
chainNamespace: 'eip155' as const
}
- vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation(
- (key: keyof SafeLocalStorageItems) => {
- if (key === SafeLocalStorageKeys.CONNECTED_CONNECTOR) {
- return mockConnector.id
- }
- return undefined
- }
- )
+ vi.spyOn(StorageUtil, 'getConnectedConnectorId').mockReturnValue(mockConnector.id)
await appKit['syncAccount'](mockAccountData)
@@ -568,6 +587,13 @@ describe('Base', () => {
chainId: '1',
chainNamespace: 'eip155' as const
}
+
+ vi.mocked(StorageUtil.getActiveNetworkProps).mockReturnValue({
+ namespace: 'eip155',
+ chainId: '1',
+ caipNetworkId: '1'
+ })
+
vi.mocked(BlockchainApiController.fetchIdentity).mockResolvedValue({
name: 'John Doe',
avatar: null
@@ -585,27 +611,35 @@ describe('Base', () => {
})
it('should disconnect correctly', async () => {
+ vi.mocked(CaipNetworksUtil.extendCaipNetworks).mockReturnValue([
+ { id: 'eip155:1', chainNamespace: 'eip155' } as CaipNetwork
+ ])
+
vi.mocked(ChainController).state = {
chains: new Map([['eip155', { namespace: 'eip155' }]]),
activeChain: 'eip155'
} as any
const mockRemoveItem = vi.fn()
- vi.spyOn(SafeLocalStorage, 'removeItem').mockImplementation(mockRemoveItem)
- await appKit.disconnect()
+ vi.spyOn(SafeLocalStorage, 'removeItem').mockImplementation(mockRemoveItem)
- expect(mockRemoveItem).toHaveBeenCalledWith(SafeLocalStorageKeys.CONNECTED_CONNECTOR)
- expect(mockRemoveItem).toHaveBeenCalledWith(SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK_ID)
+ const appKit = new AppKit({
+ ...mockOptions,
+ networks: [base],
+ projectId: 'YOUR_PROJECT_ID',
+ adapters: [mockUniversalAdapter]
+ })
- expect(AccountController.resetAccount).toHaveBeenCalledWith('eip155')
+ await appKit.disconnect()
+ expect(mockUniversalAdapter.disconnect).toHaveBeenCalled()
expect(AccountController.setStatus).toHaveBeenCalledWith('disconnected', 'eip155')
- expect(AccountController.resetAccount).toHaveBeenCalledWith('eip155')
})
it('should set unsupported chain when synced chainId is not supported', async () => {
- const isClientSpy = vi.spyOn(CoreHelperUtil, 'isClient').mockReturnValue(true)
+ vi.mocked(StorageUtil.getConnectedConnectorId).mockReturnValue('EXTERNAL')
+ vi.mocked(StorageUtil.getActiveNamespace).mockReturnValue('eip155')
vi.mocked(ChainController).state = {
chains: new Map([['eip155', { namespace: 'eip155' }]]),
activeChain: 'eip155'
@@ -627,12 +661,14 @@ describe('Base', () => {
vi.spyOn(appKit as any, 'getAdapter').mockReturnValue(mockAdapter)
- vi.spyOn(StorageUtil, 'setConnectedConnector').mockImplementation(vi.fn())
+ vi.spyOn(StorageUtil, 'setConnectedConnectorId').mockImplementation(vi.fn())
+
+ vi.spyOn(appKit as any, 'syncAccount').mockImplementation(vi.fn())
vi.spyOn(appKit as any, 'setUnsupportedNetwork').mockImplementation(vi.fn())
vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation((key: string) => {
- if (key === SafeLocalStorageKeys.CONNECTED_CONNECTOR) {
+ if (key === SafeLocalStorageKeys.CONNECTED_CONNECTOR_ID) {
return 'test-wallet'
}
if (key === SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK_ID) {
@@ -646,7 +682,6 @@ describe('Base', () => {
await (appKit as any).syncExistingConnection()
expect((appKit as any).setUnsupportedNetwork).toHaveBeenCalled()
- expect(isClientSpy).toHaveBeenCalled()
})
it('should not show unsupported chain UI when allowUnsupportedChain is true', async () => {
@@ -675,12 +710,10 @@ describe('Base', () => {
vi.spyOn(appKit as any, 'getAdapter').mockReturnValue(mockAdapter)
- vi.spyOn(StorageUtil, 'setConnectedConnector').mockImplementation(vi.fn())
-
vi.spyOn(appKit as any, 'setUnsupportedNetwork').mockImplementation(vi.fn())
vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation((key: string) => {
- if (key === SafeLocalStorageKeys.CONNECTED_CONNECTOR) {
+ if (key === SafeLocalStorageKeys.CONNECTED_CONNECTOR_ID) {
return 'test-wallet'
}
if (key === SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK_ID) {
@@ -722,14 +755,12 @@ describe('Base', () => {
describe('syncExistingConnection', () => {
it('should set status to "connecting" and sync the connection when a connector and namespace are present', async () => {
vi.mocked(CoreHelperUtil.isClient).mockReturnValueOnce(true)
- vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation(key => {
- if (key === SafeLocalStorageKeys.CONNECTED_CONNECTOR) {
- return 'test-wallet'
- }
- if (key === SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK_ID) {
- return 'eip155:1'
- }
- return undefined
+ vi.spyOn(StorageUtil, 'getActiveNamespace').mockReturnValue('eip155')
+ vi.spyOn(StorageUtil, 'getConnectedConnectorId').mockReturnValue('test-connector')
+ vi.mocked(StorageUtil.getActiveNetworkProps).mockReturnValue({
+ namespace: 'eip155',
+ chainId: '1',
+ caipNetworkId: '1'
})
const mockAdapter = {
@@ -763,7 +794,7 @@ describe('Base', () => {
it('should set status to "disconnected" if the connector is set to "AUTH" and the adapter fails to sync', async () => {
vi.mocked(CoreHelperUtil.isClient).mockReturnValueOnce(true)
vi.spyOn(SafeLocalStorage, 'getItem').mockImplementation(key => {
- if (key === SafeLocalStorageKeys.CONNECTED_CONNECTOR) {
+ if (key === SafeLocalStorageKeys.CONNECTED_CONNECTOR_ID) {
return 'AUTH'
}
if (key === SafeLocalStorageKeys.ACTIVE_CAIP_NETWORK_ID) {
@@ -827,22 +858,38 @@ describe('Base', () => {
})
it('should call syncConnectors when initializing adapters', async () => {
- const createAdapters = (appKit as any).createAdapters.bind(appKit)
+ vi.mocked(CaipNetworksUtil.extendCaipNetworks).mockReturnValue([
+ { id: 'eip155:1', chainNamespace: 'eip155' } as CaipNetwork
+ ])
- vi.spyOn(appKit as any, 'createUniversalProvider').mockResolvedValue(undefined)
+ const appKit = new AppKit({
+ ...mockOptions,
+ networks: [base],
+ projectId: 'YOUR_PROJECT_ID',
+ adapters: [mockAdapter]
+ })
- await createAdapters([mockAdapter])
+ const initChainAdapters = (appKit as any).initChainAdapters.bind(appKit)
- expect(mockAdapter.syncConnectors).toHaveBeenCalledWith(
- expect.objectContaining({
- projectId: mockOptions.projectId,
- metadata: mockOptions.metadata
- }),
- expect.any(Object)
- )
+ vi.spyOn(appKit as any, 'createConnectorsForAdapter').mockResolvedValue(undefined)
+
+ await initChainAdapters([mockAdapter])
+
+ expect(mockAdapter.syncConnectors).toHaveBeenCalled()
})
it('should create UniversalAdapter when no blueprint is provided for namespace', async () => {
+ vi.mocked(CaipNetworksUtil.extendCaipNetworks).mockReturnValue([
+ { id: 'eip155:1', chainNamespace: 'eip155' } as CaipNetwork
+ ])
+
+ const appKit = new AppKit({
+ ...mockOptions,
+ networks: [mainnet],
+ projectId: 'YOUR_PROJECT_ID',
+ adapters: [mockAdapter]
+ })
+
const createAdapters = (appKit as any).createAdapters.bind(appKit)
vi.spyOn(appKit as any, 'createUniversalProvider').mockResolvedValue(undefined)
@@ -857,13 +904,25 @@ describe('Base', () => {
const adapters = await createAdapters([])
expect(adapters.eip155).toBeDefined()
- expect(mockUniversalAdapter.setUniversalProvider).toHaveBeenCalled()
+
+ expect(UniversalAdapter).toHaveBeenCalledWith({
+ namespace: 'eip155',
+ networks: [{ id: 'eip155:1', chainNamespace: 'eip155' } as CaipNetwork]
+ })
})
it('should initialize UniversalProvider when not provided in options', () => {
+ vi.mocked(CaipNetworksUtil.extendCaipNetworks).mockReturnValue([
+ { id: 'eip155:1', chainNamespace: 'eip155' } as CaipNetwork
+ ])
+ vi.spyOn(CoreHelperUtil, 'isClient').mockReturnValue(true)
+
const upSpy = vi.spyOn(UniversalProvider, 'init')
+
new AppKit({
...mockOptions,
+ projectId: '123',
+ networks: [mainnet],
adapters: [mockAdapter]
})
@@ -872,11 +931,18 @@ describe('Base', () => {
})
it('should not initialize UniversalProvider when provided in options', async () => {
- const up = await UniversalProvider.init({})
+ vi.mocked(CaipNetworksUtil.extendCaipNetworks).mockReturnValue([
+ { id: 'eip155:1', chainNamespace: 'eip155' } as CaipNetwork
+ ])
+ vi.spyOn(CoreHelperUtil, 'isClient').mockReturnValue(true)
+
const upSpy = vi.spyOn(UniversalProvider, 'init')
+
new AppKit({
...mockOptions,
- universalProvider: up,
+ projectId: 'test',
+ networks: [mainnet],
+ universalProvider: mockProvider,
adapters: [mockAdapter]
})
@@ -885,7 +951,10 @@ describe('Base', () => {
})
it('should initialize multiple adapters for different namespaces', async () => {
- const createAdapters = (appKit as any).createAdapters.bind(appKit)
+ vi.mocked(CaipNetworksUtil.extendCaipNetworks).mockReturnValue([
+ { id: '1', chainNamespace: 'eip155' } as CaipNetwork,
+ { id: 'solana', chainNamespace: 'solana' } as CaipNetwork
+ ])
const mockSolanaAdapter = {
namespace: 'solana',
@@ -899,6 +968,15 @@ describe('Base', () => {
emit: vi.fn()
} as unknown as AdapterBlueprint
+ const appKit = new AppKit({
+ ...mockOptions,
+ networks: [mainnet, solana],
+ projectId: 'YOUR_PROJECT_ID',
+ adapters: [mockSolanaAdapter, mockAdapter]
+ })
+
+ const createAdapters = (appKit as any).createAdapters.bind(appKit)
+
vi.spyOn(appKit as any, 'createUniversalProvider').mockResolvedValue(undefined)
const adapters = await createAdapters([mockAdapter, mockSolanaAdapter])
@@ -910,29 +988,47 @@ describe('Base', () => {
})
it('should set universal provider and auth provider for each adapter', async () => {
- const createAdapters = (appKit as any).createAdapters.bind(appKit)
+ vi.mocked(CaipNetworksUtil.extendCaipNetworks).mockReturnValue([
+ { id: '1', chainNamespace: 'eip155' } as CaipNetwork
+ ])
+
+ const appKit = new AppKit({
+ ...mockOptions,
+ networks: [mainnet],
+ projectId: 'YOUR_PROJECT_ID',
+ adapters: [mockAdapter]
+ })
const mockUniversalProvider = {
on: vi.fn(),
off: vi.fn(),
emit: vi.fn()
}
- vi.spyOn(appKit as any, 'createUniversalProvider').mockResolvedValue(undefined)
- vi.spyOn(appKit as any, 'getUniversalProvider').mockResolvedValue(mockUniversalProvider)
- await createAdapters([mockAdapter])
+ vi.spyOn(appKit as any, 'initialize').mockResolvedValue(undefined)
+ vi.spyOn(CoreHelperUtil, 'isClient').mockReturnValue(true)
+ vi.spyOn(UniversalProvider, 'init').mockResolvedValue(mockUniversalProvider as any)
- expect(mockAdapter.setUniversalProvider).toHaveBeenCalledWith(
- expect.objectContaining({
- on: expect.any(Function),
- off: expect.any(Function),
- emit: expect.any(Function)
- })
- )
+ const initChainAdapters = (appKit as any).initChainAdapters.bind(appKit)
+
+ await initChainAdapters([mockAdapter])
+
+ expect(mockAdapter.setUniversalProvider).toHaveBeenCalled()
expect(mockAdapter.setAuthProvider).toHaveBeenCalled()
})
it('should update ChainController state with initialized adapters', async () => {
+ vi.mocked(CaipNetworksUtil.extendCaipNetworks).mockReturnValue([
+ { id: '1', chainNamespace: 'eip155' } as CaipNetwork
+ ])
+
+ const appKit = new AppKit({
+ ...mockOptions,
+ networks: [mainnet],
+ projectId: 'YOUR_PROJECT_ID',
+ adapters: [mockAdapter]
+ })
+
const createAdapters = (appKit as any).createAdapters.bind(appKit)
vi.spyOn(appKit as any, 'createUniversalProvider').mockResolvedValue(undefined)
diff --git a/packages/appkit/src/tests/mocks/Options.ts b/packages/appkit/src/tests/mocks/Options.ts
index 6db58d5e31..ac248a4015 100644
--- a/packages/appkit/src/tests/mocks/Options.ts
+++ b/packages/appkit/src/tests/mocks/Options.ts
@@ -2,10 +2,19 @@ import type { ChainAdapter } from '@reown/appkit-core'
import type { AppKitOptions } from '../../utils/index.js'
import { mainnet, solana } from '../../networks/index.js'
import type { SdkVersion } from '@reown/appkit-core'
+import { vi } from 'vitest'
export const mockOptions = {
projectId: 'test-project-id',
- adapters: [{ chainNamespace: 'eip155' } as unknown as ChainAdapter],
+ adapters: [
+ {
+ chainNamespace: 'eip155',
+ construct: vi.fn(),
+ on: vi.fn(),
+ syncConnectors: vi.fn(),
+ setAuthProvider: vi.fn()
+ } as unknown as ChainAdapter
+ ],
networks: [mainnet, solana],
metadata: {
name: 'Test App',
diff --git a/packages/appkit/src/tests/siwe.test.ts b/packages/appkit/src/tests/siwe.test.ts
index d38919ac28..e58a7d677f 100644
--- a/packages/appkit/src/tests/siwe.test.ts
+++ b/packages/appkit/src/tests/siwe.test.ts
@@ -1,12 +1,13 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
+ ChainController,
ConnectionController,
ModalController,
OptionsController,
RouterController,
SIWXUtil
} from '@reown/appkit-core'
-import { AppKit } from '@reown/appkit'
+import { AppKit, type CaipNetwork } from '@reown/appkit'
import * as networks from '@reown/appkit/networks'
import { createSIWEConfig, type AppKitSIWEClient } from '@reown/appkit-siwe'
import { mockUniversalAdapter } from './mocks/Adapter'
@@ -84,7 +85,12 @@ describe('SIWE mapped to SIWX', () => {
})
it('should initializeIfEnabled', async () => {
- vi.spyOn(siweConfig.methods, 'getSession').mockResolvedValueOnce(null)
+ vi.spyOn(ChainController, 'checkIfSupportedNetwork').mockReturnValue(true)
+
+ OptionsController.state.siwx = {
+ getSessions: vi.fn().mockResolvedValueOnce([])
+ } as any
+
await SIWXUtil.initializeIfEnabled()
expect(RouterController.state.view).toBe('SIWXSignMessage')
@@ -96,6 +102,12 @@ describe('SIWE mapped to SIWX', () => {
const createMessageSpy = vi.spyOn(siweConfig.methods, 'createMessage')
const verifyMessageSpy = vi.spyOn(siweConfig.methods, 'verifyMessage')
+ vi.spyOn(ChainController, 'getActiveCaipNetwork').mockReturnValue({
+ id: '1',
+ name: 'Ethereum',
+ caipNetworkId: 'eip155:1'
+ } as unknown as CaipNetwork)
+
await SIWXUtil.requestSignMessage()
expect(getNonceSpy).toHaveBeenCalled()
diff --git a/packages/common/src/utils/ConstantsUtil.ts b/packages/common/src/utils/ConstantsUtil.ts
index 7289ef1a4b..4c90c578d3 100644
--- a/packages/common/src/utils/ConstantsUtil.ts
+++ b/packages/common/src/utils/ConstantsUtil.ts
@@ -6,6 +6,18 @@ export const ConstantsUtil = {
BLOCKCHAIN_API_RPC_URL: 'https://rpc.walletconnect.org',
PULSE_API_URL: 'https://pulse.walletconnect.org',
W3M_API_URL: 'https://api.web3modal.org',
+ /* Connector IDs */
+ CONNECTOR_ID: {
+ WALLET_CONNECT: 'walletConnect',
+ INJECTED: 'injected',
+ WALLET_STANDARD: 'announced',
+ COINBASE: 'coinbaseWallet',
+ COINBASE_SDK: 'coinbaseWalletSDK',
+ SAFE: 'safe',
+ LEDGER: 'ledger',
+ EIP6963: 'eip6963',
+ AUTH: 'ID_AUTH'
+ },
CHAIN: {
EVM: 'eip155',
SOLANA: 'solana',
diff --git a/packages/common/src/utils/SafeLocalStorage.ts b/packages/common/src/utils/SafeLocalStorage.ts
index a854fdbe7e..45b3da68e5 100644
--- a/packages/common/src/utils/SafeLocalStorage.ts
+++ b/packages/common/src/utils/SafeLocalStorage.ts
@@ -4,12 +4,14 @@ export type SafeLocalStorageItems = {
'@appkit/solana_wallet': string
'@appkit/solana_caip_chain': string
'@appkit/active_caip_network_id': string
- '@appkit/connected_connector': string
+ '@appkit/connected_connector_id': string
'@appkit/connected_social': string
'@appkit/connected_social_username': string
'@appkit/recent_wallets': string
'@appkit/active_namespace': string
'@appkit/connection_status': string
+ '@appkit/siwx-auth-token': string
+ '@appkit/siwx-nonce-token': string
/*
* DO NOT CHANGE: @walletconnect/universal-provider requires us to set this specific key
* This value is a stringified version of { href: stiring; name: string }
@@ -23,14 +25,16 @@ export const SafeLocalStorageKeys = {
SOLANA_WALLET: '@appkit/solana_wallet',
SOLANA_CAIP_CHAIN: '@appkit/solana_caip_chain',
ACTIVE_CAIP_NETWORK_ID: '@appkit/active_caip_network_id',
- CONNECTED_CONNECTOR: '@appkit/connected_connector',
+ CONNECTED_CONNECTOR_ID: '@appkit/connected_connector_id',
CONNECTED_SOCIAL: '@appkit/connected_social',
CONNECTED_SOCIAL_USERNAME: '@appkit/connected_social_username',
RECENT_WALLETS: '@appkit/recent_wallets',
DEEPLINK_CHOICE: 'WALLETCONNECT_DEEPLINK_CHOICE',
ACTIVE_NAMESPACE: '@appkit/active_namespace',
- CONNECTION_STATUS: '@appkit/connection_status'
-} as const
+ CONNECTION_STATUS: '@appkit/connection_status',
+ SIWX_AUTH_TOKEN: '@appkit/siwx-auth-token',
+ SIWX_NONCE_TOKEN: '@appkit/siwx-nonce-token'
+} as const satisfies Record
export const SafeLocalStorage = {
setItem(
diff --git a/packages/common/tests/SafeLocalStorage.test.ts b/packages/common/tests/SafeLocalStorage.test.ts
index 369a70f7c8..e209844773 100644
--- a/packages/common/tests/SafeLocalStorage.test.ts
+++ b/packages/common/tests/SafeLocalStorage.test.ts
@@ -60,7 +60,7 @@ describe('SafeLocalStorage safe', () => {
})
it('getItem should return undefined if the value not exist', () => {
- expect(SafeLocalStorage.getItem('@appkit/connected_connector')).toBe(undefined)
- expect(getItem).toHaveBeenCalledWith('@appkit/connected_connector')
+ expect(SafeLocalStorage.getItem('@appkit/connected_connector_id')).toBe(undefined)
+ expect(getItem).toHaveBeenCalledWith('@appkit/connected_connector_id')
})
})
diff --git a/packages/core/src/controllers/ChainController.ts b/packages/core/src/controllers/ChainController.ts
index 089c28ccc4..22fd3a7499 100644
--- a/packages/core/src/controllers/ChainController.ts
+++ b/packages/core/src/controllers/ChainController.ts
@@ -546,7 +546,7 @@ export const ChainController = {
throw new Error(failures.map(f => f.reason.message).join(', '))
}
- StorageUtil.deleteConnectedConnector()
+ StorageUtil.deleteConnectedConnectorId()
ConnectionController.resetWcConnection()
EventsController.sendEvent({
type: 'track',
diff --git a/packages/core/src/controllers/ConnectionController.ts b/packages/core/src/controllers/ConnectionController.ts
index ff8ff0d115..00e48ceae7 100644
--- a/packages/core/src/controllers/ConnectionController.ts
+++ b/packages/core/src/controllers/ConnectionController.ts
@@ -15,7 +15,7 @@ import { type W3mFrameTypes } from '@reown/appkit-wallet'
import { ModalController } from './ModalController.js'
import { ConnectorController } from './ConnectorController.js'
import { EventsController } from './EventsController.js'
-import type { CaipNetwork, ChainNamespace } from '@reown/appkit-common'
+import { ConstantsUtil, type CaipNetwork, type ChainNamespace } from '@reown/appkit-common'
import { SIWXUtil } from '../utils/SIWXUtil.js'
// -- Types --------------------------------------------- //
@@ -98,7 +98,7 @@ export const ConnectionController = {
},
async connectWalletConnect() {
- StorageUtil.setConnectedConnector('WALLET_CONNECT')
+ StorageUtil.setConnectedConnectorId(ConstantsUtil.CONNECTOR_ID.WALLET_CONNECT)
if (CoreHelperUtil.isTelegram()) {
if (wcConnectionPromise) {
@@ -147,7 +147,7 @@ export const ConnectionController = {
async reconnectExternal(options: ConnectExternalOptions) {
await this._getClient()?.reconnectExternal?.(options)
- StorageUtil.setConnectedConnector(options.type === 'AUTH' ? 'ID_AUTH' : options.type)
+ StorageUtil.setConnectedConnectorId(options.id)
},
async setPreferredAccountType(accountType: W3mFrameTypes.AccountType) {
diff --git a/packages/core/src/controllers/ConnectorController.ts b/packages/core/src/controllers/ConnectorController.ts
index fef906384d..640631cadd 100644
--- a/packages/core/src/controllers/ConnectorController.ts
+++ b/packages/core/src/controllers/ConnectorController.ts
@@ -1,7 +1,7 @@
import { subscribeKey as subKey } from 'valtio/vanilla/utils'
import { proxy, ref, snapshot } from 'valtio/vanilla'
import type { AuthConnector, Connector } from '../utils/TypeUtil.js'
-import { getW3mThemeVariables } from '@reown/appkit-common'
+import { ConstantsUtil, getW3mThemeVariables } from '@reown/appkit-common'
import { OptionsController } from './OptionsController.js'
import { ThemeController } from './ThemeController.js'
import { ChainController } from './ChainController.js'
@@ -63,7 +63,7 @@ export const ConnectorController = {
connectorsByNameMap.forEach(keyConnectors => {
const firstItem = keyConnectors[0]
- const isAuthConnector = firstItem?.id === 'ID_AUTH'
+ const isAuthConnector = firstItem?.id === ConstantsUtil.CONNECTOR_ID.AUTH
if (keyConnectors.length > 1) {
mergedConnectors.push({
@@ -131,7 +131,7 @@ export const ConnectorController = {
},
addConnector(connector: Connector | AuthConnector) {
- if (connector.id === 'ID_AUTH') {
+ if (connector.id === ConstantsUtil.CONNECTOR_ID.AUTH) {
const authConnector = connector as AuthConnector
const optionsState = snapshot(OptionsController.state) as typeof OptionsController.state
@@ -144,7 +144,7 @@ export const ConnectorController = {
projectId: optionsState.projectId,
sdkType: optionsState.sdkType
})
- authConnector.provider.syncTheme({
+ authConnector?.provider?.syncTheme({
themeMode,
themeVariables,
w3mThemeVariables: getW3mThemeVariables(themeVariables, themeMode)
@@ -157,7 +157,7 @@ export const ConnectorController = {
getAuthConnector(): AuthConnector | undefined {
const activeNamespace = ChainController.state.activeChain
- const authConnector = state.connectors.find(c => c.id === 'ID_AUTH')
+ const authConnector = state.connectors.find(c => c.id === ConstantsUtil.CONNECTOR_ID.AUTH)
if (!authConnector) {
return undefined
}
diff --git a/packages/core/src/controllers/SendController.ts b/packages/core/src/controllers/SendController.ts
index d634e90eb1..62fb7bf55a 100644
--- a/packages/core/src/controllers/SendController.ts
+++ b/packages/core/src/controllers/SendController.ts
@@ -1,6 +1,6 @@
import { subscribeKey as subKey } from 'valtio/vanilla/utils'
import { proxy, ref, subscribe as sub } from 'valtio/vanilla'
-import { type Balance, type CaipAddress } from '@reown/appkit-common'
+import { NumberUtil, type Balance, type CaipAddress } from '@reown/appkit-common'
import { ContractUtil } from '@reown/appkit-common'
import { RouterController } from './RouterController.js'
import { AccountController } from './AccountController.js'
@@ -10,6 +10,7 @@ import { CoreHelperUtil } from '../utils/CoreHelperUtil.js'
import { EventsController } from './EventsController.js'
import { W3mFrameRpcConstants } from '@reown/appkit-wallet'
import { ChainController } from './ChainController.js'
+import { SwapApiUtil } from '../utils/SwapApiUtil.js'
// -- Types --------------------------------------------- //
@@ -34,6 +35,7 @@ export interface SendControllerState {
receiverProfileImageUrl?: string
gasPrice?: bigint
gasPriceInUSD?: number
+ networkBalanceInUSD?: string
loading: boolean
}
@@ -88,6 +90,10 @@ export const SendController = {
state.gasPriceInUSD = gasPriceInUSD
},
+ setNetworkBalanceInUsd(networkBalanceInUSD: SendControllerState['networkBalanceInUSD']) {
+ state.networkBalanceInUSD = networkBalanceInUSD
+ },
+
setLoading(loading: SendControllerState['loading']) {
state.loading = loading
},
@@ -154,6 +160,54 @@ export const SendController = {
}
},
+ async fetchNetworkBalance() {
+ const balances = await SwapApiUtil.getMyTokensWithBalance()
+
+ if (!balances) {
+ return
+ }
+
+ const networkToken = balances.find(
+ token => token.address === ChainController.getActiveNetworkTokenAddress()
+ )
+
+ if (!networkToken) {
+ return
+ }
+
+ state.networkBalanceInUSD = networkToken
+ ? NumberUtil.multiply(networkToken.quantity.numeric, networkToken.price).toString()
+ : '0'
+ },
+
+ isInsufficientNetworkTokenForGas(networkBalanceInUSD: string, gasPriceInUSD: number | undefined) {
+ const gasPrice = gasPriceInUSD || '0'
+
+ if (NumberUtil.bigNumber(networkBalanceInUSD).isZero()) {
+ return true
+ }
+
+ return NumberUtil.bigNumber(NumberUtil.bigNumber(gasPrice)).isGreaterThan(networkBalanceInUSD)
+ },
+
+ hasInsufficientGasFunds() {
+ let insufficientNetworkTokenForGas = true
+ if (
+ AccountController.state.preferredAccountType ===
+ W3mFrameRpcConstants.ACCOUNT_TYPES.SMART_ACCOUNT
+ ) {
+ // Smart Accounts may pay gas in any ERC20 token
+ insufficientNetworkTokenForGas = false
+ } else if (state.networkBalanceInUSD) {
+ insufficientNetworkTokenForGas = this.isInsufficientNetworkTokenForGas(
+ state.networkBalanceInUSD,
+ state.gasPriceInUSD
+ )
+ }
+
+ return insufficientNetworkTokenForGas
+ },
+
async sendNativeToken(params: TxParams) {
RouterController.pushTransactionStack({
view: 'Account',
diff --git a/packages/core/src/controllers/SwapController.ts b/packages/core/src/controllers/SwapController.ts
index 4b6a1364c0..9047b35821 100644
--- a/packages/core/src/controllers/SwapController.ts
+++ b/packages/core/src/controllers/SwapController.ts
@@ -16,6 +16,7 @@ import { EventsController } from './EventsController.js'
import { W3mFrameRpcConstants } from '@reown/appkit-wallet'
import { StorageUtil } from '../utils/StorageUtil.js'
import { ChainController } from './ChainController.js'
+import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common'
// -- Constants ---------------------------------------- //
export const INITIAL_GAS_LIMIT = 150000
@@ -174,7 +175,7 @@ export const SwapController = {
const caipAddress = ChainController.state.activeCaipAddress
const address = CoreHelperUtil.getPlainAddress(caipAddress)
const networkAddress = ChainController.getActiveNetworkTokenAddress()
- const type = StorageUtil.getConnectedConnector()
+ const connectorId = StorageUtil.getConnectedConnectorId()
if (!address) {
throw new Error('No address found to swap the tokens from.')
@@ -202,7 +203,7 @@ export const SwapController = {
invalidSourceTokenAmount,
availableToSwap:
caipAddress && !invalidToToken && !invalidSourceToken && !invalidSourceTokenAmount,
- isAuthConnector: type === 'ID_AUTH'
+ isAuthConnector: connectorId === CommonConstantsUtil.CONNECTOR_ID.AUTH
}
},
diff --git a/packages/core/src/utils/SIWXUtil.ts b/packages/core/src/utils/SIWXUtil.ts
index ec9152a16f..d6739bed11 100644
--- a/packages/core/src/utils/SIWXUtil.ts
+++ b/packages/core/src/utils/SIWXUtil.ts
@@ -10,6 +10,7 @@ import UniversalProvider from '@walletconnect/universal-provider'
import { EventsController } from '../controllers/EventsController.js'
import { AccountController } from '../controllers/AccountController.js'
import { W3mFrameRpcConstants } from '@reown/appkit-wallet'
+import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common'
import { StorageUtil } from './StorageUtil.js'
/**
@@ -88,7 +89,9 @@ export const SIWXUtil = {
const message = siwxMessage.toString()
- if (StorageUtil.getConnectedConnector() === 'ID_AUTH') {
+ const connectorId = StorageUtil.getConnectedConnectorId()
+
+ if (connectorId === CommonConstantsUtil.CONNECTOR_ID.AUTH) {
RouterController.pushTransactionStack({
view: null,
goBack: false,
diff --git a/packages/core/src/utils/StorageUtil.ts b/packages/core/src/utils/StorageUtil.ts
index 9ac41ba2c7..bd35b0ec27 100644
--- a/packages/core/src/utils/StorageUtil.ts
+++ b/packages/core/src/utils/StorageUtil.ts
@@ -5,7 +5,7 @@ import {
type CaipNetworkId,
type ChainNamespace
} from '@reown/appkit-common'
-import type { WcWallet, ConnectorType, SocialProvider, ConnectionStatus } from './TypeUtil.js'
+import type { WcWallet, SocialProvider, ConnectionStatus } from './TypeUtil.js'
// -- Utility -----------------------------------------------------------------
export const StorageUtil = {
@@ -87,11 +87,11 @@ export const StorageUtil = {
}
},
- deleteConnectedConnector() {
+ deleteConnectedConnectorId() {
try {
- SafeLocalStorage.removeItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR)
+ SafeLocalStorage.removeItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR_ID)
} catch {
- console.info('Unable to delete connected connector')
+ console.info('Unable to delete connected connector id')
}
},
@@ -123,11 +123,11 @@ export const StorageUtil = {
return []
},
- setConnectedConnector(connectorType: ConnectorType) {
+ setConnectedConnectorId(connectorId: string) {
try {
- SafeLocalStorage.setItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR, connectorType)
+ SafeLocalStorage.setItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR_ID, connectorId)
} catch {
- console.info('Unable to set Connected Connector')
+ console.info('Unable to set Connected Connector Id')
}
},
@@ -143,11 +143,11 @@ export const StorageUtil = {
return undefined
},
- getConnectedConnector() {
+ getConnectedConnectorId() {
try {
- return SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR) as ConnectorType
+ return SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR_ID)
} catch {
- console.info('Unable to get connected connector')
+ console.info('Unable to get connected connector id')
}
return undefined
diff --git a/packages/core/tests/controllers/ChainController.test.ts b/packages/core/tests/controllers/ChainController.test.ts
index 7ea680f8e8..29787c875e 100644
--- a/packages/core/tests/controllers/ChainController.test.ts
+++ b/packages/core/tests/controllers/ChainController.test.ts
@@ -369,7 +369,7 @@ describe('ChainController', () => {
const resetAccountSpy = vi.spyOn(ChainController, 'resetAccount')
const resetNetworkSpy = vi.spyOn(ChainController, 'resetNetwork')
- const deleteConnectorSpy = vi.spyOn(StorageUtil, 'deleteConnectedConnector')
+ const deleteConnectorSpy = vi.spyOn(StorageUtil, 'deleteConnectedConnectorId')
const resetWcConnectionSpy = vi.spyOn(ConnectionController, 'resetWcConnection')
const sendEventSpy = vi.spyOn(EventsController, 'sendEvent')
diff --git a/packages/core/tests/controllers/ConnectionController.test.ts b/packages/core/tests/controllers/ConnectionController.test.ts
index e9da837d3b..a00af3b8b8 100644
--- a/packages/core/tests/controllers/ConnectionController.test.ts
+++ b/packages/core/tests/controllers/ConnectionController.test.ts
@@ -17,7 +17,7 @@ const chain = CommonConstantsUtil.CHAIN.EVM
const walletConnectUri = 'wc://uri?=123'
const externalId = 'coinbaseWallet'
const type = 'WALLET_CONNECT' as ConnectorType
-const storageSpy = vi.spyOn(StorageUtil, 'setConnectedConnector')
+const storageSpy = vi.spyOn(StorageUtil, 'setConnectedConnectorId')
const client: ConnectionControllerClient = {
connectWalletConnect: async onUri => {
@@ -109,7 +109,7 @@ describe('ConnectionController', () => {
await ConnectionController.connectWalletConnect()
expect(ConnectionController.state.wcUri).toEqual(walletConnectUri)
expect(ConnectionController.state.wcPairingExpiry).toEqual(ConstantsUtil.FOUR_MINUTES_MS)
- expect(storageSpy).toHaveBeenCalledWith('WALLET_CONNECT')
+ expect(storageSpy).toHaveBeenCalledWith('walletConnect')
expect(clientConnectWalletConnectSpy).toHaveBeenCalled()
// Just in case
@@ -119,7 +119,6 @@ describe('ConnectionController', () => {
it('connectExternal() should trigger internal client call and set connector in storage', async () => {
const options = { id: externalId, type }
await ConnectionController.connectExternal(options, chain)
- expect(storageSpy).toHaveBeenCalledWith(type)
expect(clientConnectExternalSpy).toHaveBeenCalledWith(options)
})
diff --git a/packages/core/tests/utils/StorageUtil.test.ts b/packages/core/tests/utils/StorageUtil.test.ts
index 62afc5905e..cda7e93cfc 100644
--- a/packages/core/tests/utils/StorageUtil.test.ts
+++ b/packages/core/tests/utils/StorageUtil.test.ts
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, afterEach, beforeEach, beforeAll, afterAll } from 'vitest'
import { StorageUtil } from '../../src/utils/StorageUtil'
-import type { WcWallet, ConnectorType, SocialProvider } from '../../src/utils/TypeUtil'
+import type { WcWallet, SocialProvider } from '../../src/utils/TypeUtil'
import { SafeLocalStorage } from '@reown/appkit-common'
import { SafeLocalStorageKeys } from '@reown/appkit-common'
@@ -147,19 +147,21 @@ describe('StorageUtil', () => {
})
})
- describe('setConnectedConnector', () => {
+ describe('setConnectedConnectorId', () => {
it('should set connected connector', () => {
- const connector: ConnectorType = 'INJECTED'
- StorageUtil.setConnectedConnector(connector)
- expect(SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR)).toBe(connector)
+ const connectorId = 'io.metamask'
+ StorageUtil.setConnectedConnectorId(connectorId)
+ expect(SafeLocalStorage.getItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR_ID)).toBe(
+ connectorId
+ )
})
})
describe('getConnectedConnector', () => {
it('should get connected connector', () => {
- const connector: ConnectorType = 'INJECTED'
- SafeLocalStorage.setItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR, connector)
- expect(StorageUtil.getConnectedConnector()).toBe(connector)
+ const connectorId = 'io.metamask'
+ SafeLocalStorage.setItem(SafeLocalStorageKeys.CONNECTED_CONNECTOR_ID, connectorId)
+ expect(StorageUtil.getConnectedConnectorId()).toBe(connectorId)
})
})
diff --git a/packages/scaffold-ui/src/modal/w3m-modal/index.ts b/packages/scaffold-ui/src/modal/w3m-modal/index.ts
index 919b7482d7..a7e9522cdb 100644
--- a/packages/scaffold-ui/src/modal/w3m-modal/index.ts
+++ b/packages/scaffold-ui/src/modal/w3m-modal/index.ts
@@ -57,8 +57,14 @@ export class W3mModal extends LitElement {
public override firstUpdated() {
OptionsController.setEnableEmbedded(this.enableEmbedded)
- if (this.enableEmbedded && this.caipAddress) {
- ModalController.close()
+ if (this.caipAddress) {
+ if (this.enableEmbedded) {
+ ModalController.close()
+
+ return
+ }
+
+ this.onNewAddress(this.caipAddress)
}
}
diff --git a/packages/scaffold-ui/src/partials/w3m-account-auth-button/index.ts b/packages/scaffold-ui/src/partials/w3m-account-auth-button/index.ts
index 8192090a5a..f4cc8b83fc 100644
--- a/packages/scaffold-ui/src/partials/w3m-account-auth-button/index.ts
+++ b/packages/scaffold-ui/src/partials/w3m-account-auth-button/index.ts
@@ -7,6 +7,7 @@ import {
StorageUtil,
type SocialProvider
} from '@reown/appkit-core'
+import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common'
@customElement('w3m-account-auth-button')
export class W3mAccountAuthButton extends LitElement {
@@ -17,10 +18,10 @@ export class W3mAccountAuthButton extends LitElement {
// -- Render -------------------------------------------- //
public override render() {
- const type = StorageUtil.getConnectedConnector()
+ const connectorId = StorageUtil.getConnectedConnectorId()
const authConnector = ConnectorController.getAuthConnector()
- if (!authConnector || type !== 'ID_AUTH') {
+ if (!authConnector || connectorId !== CommonConstantsUtil.CONNECTOR_ID.AUTH) {
this.style.cssText = `display: none`
return null
diff --git a/packages/scaffold-ui/src/partials/w3m-account-default-widget/index.ts b/packages/scaffold-ui/src/partials/w3m-account-default-widget/index.ts
index e8d528c199..0448d66686 100644
--- a/packages/scaffold-ui/src/partials/w3m-account-default-widget/index.ts
+++ b/packages/scaffold-ui/src/partials/w3m-account-default-widget/index.ts
@@ -226,10 +226,14 @@ export class W3mAccountDefaultWidget extends LitElement {
}
private authCardTemplate() {
- const type = StorageUtil.getConnectedConnector()
+ const connectorId = StorageUtil.getConnectedConnectorId()
const authConnector = ConnectorController.getAuthConnector()
const { origin } = location
- if (!authConnector || type !== 'ID_AUTH' || origin.includes(CommonConstantsUtil.SECURE_SITE)) {
+ if (
+ !authConnector ||
+ connectorId !== ConstantsUtil.CONNECTOR_ID.AUTH ||
+ origin.includes(CommonConstantsUtil.SECURE_SITE)
+ ) {
return null
}
diff --git a/packages/scaffold-ui/src/views/w3m-account-settings-view/index.ts b/packages/scaffold-ui/src/views/w3m-account-settings-view/index.ts
index ef08edcdfb..b5c2cffbe2 100644
--- a/packages/scaffold-ui/src/views/w3m-account-settings-view/index.ts
+++ b/packages/scaffold-ui/src/views/w3m-account-settings-view/index.ts
@@ -18,6 +18,7 @@ import { LitElement, html } from 'lit'
import { state } from 'lit/decorators.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import { W3mFrameRpcConstants } from '@reown/appkit-wallet'
+import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common'
@customElement('w3m-account-settings-view')
export class W3mAccountSettingsView extends LitElement {
@@ -152,10 +153,15 @@ export class W3mAccountSettingsView extends LitElement {
// -- Private ------------------------------------------- //
private chooseNameButtonTemplate() {
- const type = StorageUtil.getConnectedConnector()
+ const connectorId = StorageUtil.getConnectedConnectorId()
const authConnector = ConnectorController.getAuthConnector()
const hasNetworkSupport = ChainController.checkIfNamesSupported()
- if (!hasNetworkSupport || !authConnector || type !== 'ID_AUTH' || this.profileName) {
+ if (
+ !hasNetworkSupport ||
+ !authConnector ||
+ connectorId !== CommonConstantsUtil.CONNECTOR_ID.AUTH ||
+ this.profileName
+ ) {
return null
}
@@ -175,10 +181,14 @@ export class W3mAccountSettingsView extends LitElement {
}
private authCardTemplate() {
- const type = StorageUtil.getConnectedConnector()
+ const connectorId = StorageUtil.getConnectedConnectorId()
const authConnector = ConnectorController.getAuthConnector()
const { origin } = location
- if (!authConnector || type !== 'ID_AUTH' || origin.includes(ConstantsUtil.SECURE_SITE)) {
+ if (
+ !authConnector ||
+ connectorId !== CommonConstantsUtil.CONNECTOR_ID.AUTH ||
+ origin.includes(ConstantsUtil.SECURE_SITE)
+ ) {
return null
}
@@ -214,10 +224,14 @@ export class W3mAccountSettingsView extends LitElement {
private togglePreferredAccountBtnTemplate() {
const networkEnabled = ChainController.checkIfSmartAccountEnabled()
- const type = StorageUtil.getConnectedConnector()
+ const connectorId = StorageUtil.getConnectedConnectorId()
const authConnector = ConnectorController.getAuthConnector()
- if (!authConnector || type !== 'ID_AUTH' || !networkEnabled) {
+ if (
+ !authConnector ||
+ connectorId !== CommonConstantsUtil.CONNECTOR_ID.AUTH ||
+ !networkEnabled
+ ) {
return null
}
diff --git a/packages/scaffold-ui/src/views/w3m-account-view/index.ts b/packages/scaffold-ui/src/views/w3m-account-view/index.ts
index c87a2bf37d..40152fcaf9 100644
--- a/packages/scaffold-ui/src/views/w3m-account-view/index.ts
+++ b/packages/scaffold-ui/src/views/w3m-account-view/index.ts
@@ -1,17 +1,18 @@
import { ConnectorController, StorageUtil } from '@reown/appkit-core'
import { customElement } from '@reown/appkit-ui'
import { LitElement, html } from 'lit'
+import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common'
@customElement('w3m-account-view')
export class W3mAccountView extends LitElement {
// -- Render -------------------------------------------- //
public override render() {
- const connectedConnectorType = StorageUtil.getConnectedConnector()
+ const connectorId = StorageUtil.getConnectedConnectorId()
const authConnector = ConnectorController.getAuthConnector()
return html`
- ${authConnector && connectedConnectorType === 'ID_AUTH'
+ ${authConnector && connectorId === CommonConstantsUtil.CONNECTOR_ID.AUTH
? this.walletFeaturesTemplate()
: this.defaultTemplate()}
`
diff --git a/packages/scaffold-ui/src/views/w3m-connecting-external-view/index.ts b/packages/scaffold-ui/src/views/w3m-connecting-external-view/index.ts
index d93de9c20b..e61973094f 100644
--- a/packages/scaffold-ui/src/views/w3m-connecting-external-view/index.ts
+++ b/packages/scaffold-ui/src/views/w3m-connecting-external-view/index.ts
@@ -5,7 +5,7 @@ import {
EventsController,
ModalController
} from '@reown/appkit-core'
-import { ConstantsUtil } from '@reown/appkit-utils'
+import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common'
import { customElement } from '@reown/appkit-ui'
import { W3mConnectingWidget } from '../../utils/w3m-connecting-widget/index.js'
@@ -54,7 +54,7 @@ export class W3mConnectingExternalView extends W3mConnectingWidget {
* Instead of opening a popup in first render for `W3mConnectingWidget`, we need to trigger connection for Coinbase connector specifically when users select it.
* And if there is an error, this condition will be skipped and the connection will be triggered as usual because we have `Try again` button in this view which is a user interaction as well.
*/
- if (this.connector.id !== ConstantsUtil.COINBASE_SDK_CONNECTOR_ID || !this.error) {
+ if (this.connector.id !== CommonConstantsUtil.CONNECTOR_ID.COINBASE_SDK || !this.error) {
await ConnectionController.connectExternal(this.connector, this.connector.chain)
EventsController.sendEvent({
diff --git a/packages/scaffold-ui/src/views/w3m-network-switch-view/index.ts b/packages/scaffold-ui/src/views/w3m-network-switch-view/index.ts
index ffe6c414b3..faa783cdee 100644
--- a/packages/scaffold-ui/src/views/w3m-network-switch-view/index.ts
+++ b/packages/scaffold-ui/src/views/w3m-network-switch-view/index.ts
@@ -6,6 +6,7 @@ import {
StorageUtil
} from '@reown/appkit-core'
import { customElement } from '@reown/appkit-ui'
+import { ConstantsUtil as CommonConstantsUtil } from '@reown/appkit-common'
import { LitElement, html } from 'lit'
import { state } from 'lit/decorators.js'
import { ifDefined } from 'lit/directives/if-defined.js'
@@ -95,9 +96,9 @@ export class W3mNetworkSwitchView extends LitElement {
// -- Private ------------------------------------------- //
private getSubLabel() {
- const type = StorageUtil.getConnectedConnector()
+ const connectorId = StorageUtil.getConnectedConnectorId()
const authConnector = ConnectorController.getAuthConnector()
- if (authConnector && type === 'AUTH') {
+ if (authConnector && connectorId === CommonConstantsUtil.CONNECTOR_ID.AUTH) {
return ''
}
@@ -107,9 +108,9 @@ export class W3mNetworkSwitchView extends LitElement {
}
private getLabel() {
- const type = StorageUtil.getConnectedConnector()
+ const connectorId = StorageUtil.getConnectedConnectorId()
const authConnector = ConnectorController.getAuthConnector()
- if (authConnector && type === 'AUTH') {
+ if (authConnector && connectorId === CommonConstantsUtil.CONNECTOR_ID.AUTH) {
return `Switching to ${this.network?.name ?? 'Unknown'} network...`
}
diff --git a/packages/scaffold-ui/src/views/w3m-networks-view/index.ts b/packages/scaffold-ui/src/views/w3m-networks-view/index.ts
index b0bf928d44..d8dc52bbb3 100644
--- a/packages/scaffold-ui/src/views/w3m-networks-view/index.ts
+++ b/packages/scaffold-ui/src/views/w3m-networks-view/index.ts
@@ -1,4 +1,4 @@
-import { type CaipNetwork } from '@reown/appkit-common'
+import { ConstantsUtil, type CaipNetwork } from '@reown/appkit-common'
import {
AccountController,
AssetUtil,
@@ -135,9 +135,9 @@ export class W3mNetworksView extends LitElement {
const approvedCaipNetworkIds = ChainController.getAllApprovedCaipNetworkIds()
const supportsAllNetworks =
ChainController.getNetworkProp('supportsAllNetworks', networkNamespace) !== false
- const type = StorageUtil.getConnectedConnector()
+ const connectorId = StorageUtil.getConnectedConnectorId()
const authConnector = ConnectorController.getAuthConnector()
- const isConnectedWithAuth = type === 'ID_AUTH' && authConnector
+ const isConnectedWithAuth = connectorId === ConstantsUtil.CONNECTOR_ID.AUTH && authConnector
if (!isNamespaceConnected || supportsAllNetworks || isConnectedWithAuth) {
return false
@@ -160,7 +160,8 @@ export class W3mNetworksView extends LitElement {
network.chainNamespace
)
const isCurrentNetworkConnected = AccountController.state.caipAddress
- const isAuthConnected = StorageUtil.getConnectedConnector() === 'ID_AUTH'
+ const isAuthConnected =
+ StorageUtil.getConnectedConnectorId() === ConstantsUtil.CONNECTOR_ID.AUTH
if (
isDifferentNamespace &&
diff --git a/packages/scaffold-ui/src/views/w3m-switch-address-view/index.ts b/packages/scaffold-ui/src/views/w3m-switch-address-view/index.ts
index 9ee72bb870..ce74ddc6e1 100644
--- a/packages/scaffold-ui/src/views/w3m-switch-address-view/index.ts
+++ b/packages/scaffold-ui/src/views/w3m-switch-address-view/index.ts
@@ -13,6 +13,7 @@ import { state } from 'lit/decorators.js'
import styles from './styles.js'
import { ifDefined } from 'lit/directives/if-defined.js'
import type { CaipAddress } from '@reown/appkit-common'
+import { ConstantsUtil } from '@reown/appkit-common'
@customElement('w3m-switch-address-view')
export class W3mSwitchAddressView extends LitElement {
@@ -28,10 +29,10 @@ export class W3mSwitchAddressView extends LitElement {
public readonly currentAddress: string = AccountController.state.address || ''
- private connectedConnector = StorageUtil.getConnectedConnector()
+ private connectorId = StorageUtil.getConnectedConnectorId()
// Only show icon for AUTH accounts
- private shouldShowIcon = this.connectedConnector === 'AUTH'
+ private shouldShowIcon = this.connectorId === ConstantsUtil.CONNECTOR_ID.AUTH
private caipNetwork = ChainController.state.activeCaipNetwork
diff --git a/packages/scaffold-ui/src/views/w3m-wallet-send-view/index.ts b/packages/scaffold-ui/src/views/w3m-wallet-send-view/index.ts
index ac4417df3e..233a23b4d2 100644
--- a/packages/scaffold-ui/src/views/w3m-wallet-send-view/index.ts
+++ b/packages/scaffold-ui/src/views/w3m-wallet-send-view/index.ts
@@ -39,6 +39,7 @@ export class W3mWalletSendView extends LitElement {
| 'Add Amount'
| 'Insufficient Funds'
| 'Incorrect Value'
+ | 'Insufficient Gas Funds'
| 'Invalid Address' = 'Preview Send'
public constructor() {
@@ -106,6 +107,8 @@ export class W3mWalletSendView extends LitElement {
private async fetchNetworkPrice() {
await SwapController.getNetworkTokenPrice()
const gas = await SwapController.getInitialGasPrice()
+ await SendController.fetchNetworkBalance()
+
if (gas?.gasPrice && gas?.gasPriceInUSD) {
SendController.setGasPrice(gas.gasPrice)
SendController.setGasPriceInUsd(gas.gasPriceInUSD)
@@ -130,6 +133,10 @@ export class W3mWalletSendView extends LitElement {
this.message = 'Add Address'
}
+ if (SendController.hasInsufficientGasFunds()) {
+ this.message = 'Insufficient Gas Funds'
+ }
+
if (
this.sendTokenAmount &&
this.token &&
diff --git a/packages/scaffold-ui/test/modal/w3m-swap-preview-view.test.ts b/packages/scaffold-ui/test/modal/w3m-swap-preview-view.test.ts
new file mode 100644
index 0000000000..a17b5fd9b8
--- /dev/null
+++ b/packages/scaffold-ui/test/modal/w3m-swap-preview-view.test.ts
@@ -0,0 +1,285 @@
+import { expect as expectChai, html, fixture } from '@open-wc/testing'
+import { describe, it, afterEach, beforeEach, vi, expect } from 'vitest'
+import { W3mSwapPreviewView } from '../../src/views/w3m-swap-preview-view'
+import {
+ SwapController,
+ RouterController,
+ AccountController,
+ ChainController,
+ type SwapTokenWithBalance,
+ type SwapControllerState,
+ type ChainControllerState,
+ type AccountControllerState
+} from '@reown/appkit-core'
+
+const mockToken: SwapTokenWithBalance = {
+ address: 'eip155:1:0x123',
+ symbol: 'TEST',
+ name: 'Test Token',
+ quantity: {
+ numeric: '100',
+ decimals: '18'
+ },
+ decimals: 18,
+ logoUri: 'https://example.com/icon.png',
+ price: 10,
+ value: 1000
+}
+
+const mockChainState: ChainControllerState = {
+ activeChain: 'eip155',
+ activeCaipNetwork: {
+ id: 1,
+ name: 'Ethereum',
+ chainNamespace: 'eip155',
+ caipNetworkId: 'eip155:1',
+ nativeCurrency: {
+ name: 'Ethereum',
+ symbol: 'ETH',
+ decimals: 18
+ },
+ rpcUrls: {
+ default: {
+ http: ['https://ethereum.rpc.com']
+ }
+ }
+ },
+ activeCaipAddress: 'eip155:1:0x123456789abcdef123456789abcdef123456789a',
+ chains: new Map(),
+ universalAdapter: {
+ networkControllerClient: {
+ switchCaipNetwork: vi.fn(),
+ getApprovedCaipNetworksData: vi.fn()
+ },
+ connectionControllerClient: {
+ connectWalletConnect: vi.fn(),
+ connectExternal: vi.fn(),
+ reconnectExternal: vi.fn(),
+ checkInstalled: vi.fn(),
+ disconnect: vi.fn(),
+ signMessage: vi.fn(),
+ sendTransaction: vi.fn(),
+ estimateGas: vi.fn(),
+ parseUnits: vi.fn(),
+ formatUnits: vi.fn(),
+ writeContract: vi.fn(),
+ getEnsAddress: vi.fn(),
+ getEnsAvatar: vi.fn(),
+ grantPermissions: vi.fn(),
+ revokePermissions: vi.fn(),
+ getCapabilities: vi.fn()
+ }
+ },
+ noAdapters: false
+}
+
+const mockSwapState: SwapControllerState = {
+ initializing: false,
+ initialized: true,
+ loadingQuote: false,
+ loadingPrices: false,
+ loadingTransaction: false,
+ loadingApprovalTransaction: false,
+ loadingBuildTransaction: false,
+ fetchError: false,
+ approvalTransaction: undefined,
+ swapTransaction: {
+ data: '0x123',
+ to: '0x456',
+ gas: BigInt(21000),
+ gasPrice: BigInt(1000000000),
+ value: BigInt(0),
+ toAmount: '10'
+ },
+ transactionError: undefined,
+ sourceToken: mockToken,
+ sourceTokenAmount: '1',
+ sourceTokenPriceInUSD: 10,
+ toToken: { ...mockToken, symbol: 'USDT' },
+ toTokenAmount: '10',
+ toTokenPriceInUSD: 1,
+ networkPrice: '0',
+ networkBalanceInUSD: '0',
+ networkTokenSymbol: '',
+ inputError: undefined,
+ slippage: 0.5,
+ tokens: [mockToken],
+ suggestedTokens: undefined,
+ popularTokens: undefined,
+ foundTokens: undefined,
+ myTokensWithBalance: [mockToken],
+ tokensPriceMap: {},
+ gasFee: '0',
+ gasPriceInUSD: 2,
+ priceImpact: undefined,
+ maxSlippage: undefined,
+ providerFee: undefined
+}
+
+const mockAccountState: AccountControllerState = {
+ balanceSymbol: 'ETH',
+ address: '0x123',
+ currentTab: 0,
+ addressLabels: new Map(),
+ allAccounts: []
+}
+
+describe('W3mSwapPreviewView', () => {
+ beforeEach(() => {
+ class MockIntersectionObserver implements IntersectionObserver {
+ readonly root: Element | null = null
+ readonly rootMargin: string = '0px'
+ readonly thresholds: ReadonlyArray = [0]
+
+ constructor(private callback: IntersectionObserverCallback) {}
+
+ observe() {
+ this.callback(
+ [
+ {
+ isIntersecting: true,
+ intersectionRatio: 1,
+ boundingClientRect: {} as DOMRectReadOnly,
+ intersectionRect: {} as DOMRectReadOnly,
+ rootBounds: null,
+ target: document.createElement('div'),
+ time: Date.now()
+ }
+ ],
+ this
+ )
+ }
+
+ unobserve() {}
+ disconnect() {}
+ takeRecords(): IntersectionObserverEntry[] {
+ return []
+ }
+ }
+
+ global.IntersectionObserver = MockIntersectionObserver as unknown as typeof IntersectionObserver
+
+ // Mock controller states and methods
+ vi.spyOn(SwapController, 'state', 'get').mockReturnValue(mockSwapState)
+ vi.spyOn(ChainController, 'state', 'get').mockReturnValue(mockChainState)
+ vi.spyOn(AccountController, 'state', 'get').mockReturnValue(mockAccountState)
+ vi.spyOn(SwapController, 'getTransaction').mockImplementation(
+ async () => mockSwapState.swapTransaction
+ )
+ vi.spyOn(SwapController, 'sendTransactionForApproval').mockImplementation(async () => undefined)
+ vi.spyOn(SwapController, 'sendTransactionForSwap').mockImplementation(async () => undefined)
+ vi.spyOn(RouterController, 'goBack').mockImplementation(() => {})
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('should render initial state with token details', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const tokenButtons = element.shadowRoot?.querySelectorAll('wui-token-button')
+ expectChai(tokenButtons?.length).to.equal(2)
+
+ const sourceTokenButton = tokenButtons?.[0]
+ expectChai(sourceTokenButton?.getAttribute('text')).to.include('TEST')
+ expectChai(sourceTokenButton?.getAttribute('imageSrc')).to.equal(mockToken.logoUri)
+
+ const toTokenButton = tokenButtons?.[1]
+ expectChai(toTokenButton?.getAttribute('text')).to.include('USDT')
+ })
+
+ it('should handle approval transaction', async () => {
+ const approvalTransaction = {
+ data: '0x789',
+ to: '0xabc',
+ gas: BigInt(21000),
+ gasPrice: BigInt(1000000000),
+ value: BigInt(0),
+ toAmount: '10'
+ }
+
+ vi.spyOn(SwapController, 'state', 'get').mockReturnValue({
+ ...mockSwapState,
+ approvalTransaction
+ })
+
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const actionButton = element.shadowRoot?.querySelector('.action-button') as HTMLElement
+ expectChai(actionButton?.textContent?.trim()).to.include('Approve')
+
+ await actionButton?.click()
+ await element.updateComplete
+
+ expect(SwapController.sendTransactionForApproval).toHaveBeenCalledWith(approvalTransaction)
+ })
+
+ it('should handle swap transaction', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const actionButton = element.shadowRoot?.querySelector('.action-button') as HTMLElement
+ expectChai(actionButton?.textContent?.trim()).to.include('Swap')
+
+ await actionButton?.click()
+ await element.updateComplete
+
+ expect(SwapController.sendTransactionForSwap).toHaveBeenCalledWith(
+ mockSwapState.swapTransaction
+ )
+ })
+
+ it('should handle cancel action', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const cancelButton = element.shadowRoot?.querySelector('.cancel-button') as HTMLElement
+ await cancelButton?.click()
+ await element.updateComplete
+
+ expect(RouterController.goBack).toHaveBeenCalled()
+ })
+
+ it('should show loading state', async () => {
+ vi.spyOn(SwapController, 'state', 'get').mockReturnValue({
+ ...mockSwapState,
+ loadingTransaction: true
+ })
+
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const actionButton = element.shadowRoot?.querySelector('.action-button')
+ expectChai(actionButton?.hasAttribute('loading')).to.be.true
+ expectChai(actionButton?.hasAttribute('disabled')).to.be.true
+ })
+
+ it('should cleanup interval on disconnect', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ const clearIntervalSpy = vi.spyOn(window, 'clearInterval')
+ element.disconnectedCallback()
+
+ expect(clearIntervalSpy).toHaveBeenCalled()
+ })
+})
diff --git a/packages/scaffold-ui/test/modal/w3m-swap-select-token-view.test.ts b/packages/scaffold-ui/test/modal/w3m-swap-select-token-view.test.ts
new file mode 100644
index 0000000000..ac452bcce9
--- /dev/null
+++ b/packages/scaffold-ui/test/modal/w3m-swap-select-token-view.test.ts
@@ -0,0 +1,258 @@
+import { expect, html, fixture } from '@open-wc/testing'
+import { describe, it, afterEach, beforeEach, vi } from 'vitest'
+import { W3mSwapSelectTokenView } from '../../src/views/w3m-swap-select-token-view'
+import {
+ SwapController,
+ RouterController,
+ type SwapTokenWithBalance,
+ type RouterControllerState,
+ type SwapControllerState
+} from '@reown/appkit-core'
+
+const mockToken: SwapTokenWithBalance = {
+ address: 'eip155:1:0x123',
+ symbol: 'TEST',
+ name: 'Test Token',
+ quantity: {
+ numeric: '100',
+ decimals: '18'
+ },
+ decimals: 18,
+ logoUri: 'https://example.com/icon.png',
+ price: 10,
+ value: 1000
+}
+
+const mockTokens: SwapTokenWithBalance[] = [
+ mockToken,
+ {
+ ...mockToken,
+ symbol: 'USDT',
+ name: 'USD Tether',
+ address: 'eip155:1:0x456' as `eip155:${string}:${string}`
+ },
+ {
+ ...mockToken,
+ symbol: 'DAI',
+ name: 'Dai Stablecoin',
+ address: 'eip155:1:0x789' as `eip155:${string}:${string}`
+ }
+]
+
+const mockRouterState: RouterControllerState = {
+ view: 'SwapSelectToken',
+ history: ['Connect', 'SwapSelectToken'],
+ data: {
+ target: 'sourceToken'
+ },
+ transactionStack: []
+}
+
+const mockSwapState: SwapControllerState = {
+ initializing: false,
+ initialized: true,
+ loadingQuote: false,
+ loadingPrices: false,
+ loadingTransaction: false,
+ loadingApprovalTransaction: false,
+ loadingBuildTransaction: false,
+ fetchError: false,
+ approvalTransaction: undefined,
+ swapTransaction: undefined,
+ transactionError: undefined,
+ sourceToken: mockToken,
+ sourceTokenAmount: '1',
+ sourceTokenPriceInUSD: 10,
+ toToken: { ...mockToken, symbol: 'USDT' },
+ toTokenAmount: '10',
+ toTokenPriceInUSD: 1,
+ networkPrice: '0',
+ networkBalanceInUSD: '0',
+ networkTokenSymbol: '',
+ inputError: undefined,
+ slippage: 0.5,
+ tokens: [mockToken],
+ suggestedTokens: mockTokens,
+ popularTokens: mockTokens,
+ foundTokens: undefined,
+ myTokensWithBalance: mockTokens,
+ tokensPriceMap: {},
+ gasFee: '0',
+ gasPriceInUSD: 2,
+ priceImpact: undefined,
+ maxSlippage: undefined,
+ providerFee: undefined
+}
+
+describe('W3mSwapSelectTokenView', () => {
+ beforeEach(() => {
+ // Mock IntersectionObserver
+ class MockIntersectionObserver implements IntersectionObserver {
+ readonly root: Element | null = null
+ readonly rootMargin: string = '0px'
+ readonly thresholds: ReadonlyArray = [0]
+
+ constructor(private callback: IntersectionObserverCallback) {}
+
+ observe() {
+ // Simulate an intersection
+ this.callback(
+ [
+ {
+ isIntersecting: true,
+ intersectionRatio: 1,
+ boundingClientRect: {} as DOMRectReadOnly,
+ intersectionRect: {} as DOMRectReadOnly,
+ rootBounds: null,
+ target: document.createElement('div'),
+ time: Date.now()
+ }
+ ],
+ this
+ )
+ }
+
+ unobserve() {}
+ disconnect() {}
+ takeRecords(): IntersectionObserverEntry[] {
+ return []
+ }
+ }
+
+ global.IntersectionObserver = MockIntersectionObserver as unknown as typeof IntersectionObserver
+
+ // Mock controller states and methods
+ vi.spyOn(SwapController, 'state', 'get').mockReturnValue(mockSwapState)
+ vi.spyOn(RouterController, 'state', 'get').mockReturnValue(mockRouterState)
+ vi.spyOn(SwapController, 'setSourceToken').mockImplementation(() => {})
+ vi.spyOn(SwapController, 'setToToken').mockImplementation(() => {})
+ vi.spyOn(SwapController, 'swapTokens').mockImplementation(async () => {})
+ vi.spyOn(RouterController, 'goBack').mockImplementation(() => {})
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('should render initial state with token lists', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const searchInput = element.shadowRoot?.querySelector(
+ '[data-testid="swap-select-token-search-input"]'
+ )
+ expect(searchInput).to.exist
+
+ const suggestedTokens = element.shadowRoot?.querySelectorAll('wui-token-button')
+ expect(suggestedTokens?.length).to.equal(mockTokens.length)
+
+ const yourTokens = element.shadowRoot?.querySelectorAll('wui-token-list-item')
+ expect(yourTokens?.length).to.equal(mockTokens.length * 2)
+ })
+
+ it('should handle token search', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const searchInput = element.shadowRoot?.querySelector(
+ '[data-testid="swap-select-token-search-input"]'
+ )
+ searchInput?.dispatchEvent(new CustomEvent('inputChange', { detail: 'USDT' }))
+
+ await element.updateComplete
+
+ const tokenItems = element.shadowRoot?.querySelectorAll('wui-token-list-item')
+ const visibleTokens = Array.from(tokenItems || []).filter(item => !item.hasAttribute('hidden'))
+
+ expect(visibleTokens.length).to.be.greaterThan(0)
+ visibleTokens.forEach(token => {
+ expect(token.getAttribute('symbol')?.toLowerCase()).to.include('usdt')
+ })
+ })
+
+ it('should select source token and go back', async () => {
+ const setSourceTokenSpy = vi.spyOn(SwapController, 'setSourceToken')
+ const goBackSpy = vi.spyOn(RouterController, 'goBack')
+
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const tokenItem = element.shadowRoot?.querySelector(
+ '[data-testid="swap-select-token-item-DAI"]'
+ ) as HTMLElement
+ tokenItem?.click()
+
+ expect(setSourceTokenSpy.mock.calls.length).to.equal(1)
+ expect(goBackSpy.mock.calls.length).to.equal(1)
+ })
+
+ it('should select destination token and trigger swap', async () => {
+ vi.spyOn(RouterController, 'state', 'get').mockReturnValue({
+ ...mockRouterState,
+ data: {
+ target: 'toToken'
+ }
+ })
+
+ const setToTokenSpy = vi.spyOn(SwapController, 'setToToken')
+ const swapTokensSpy = vi.spyOn(SwapController, 'swapTokens')
+ const goBackSpy = vi.spyOn(RouterController, 'goBack')
+
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const tokenItem = element.shadowRoot?.querySelector(
+ '[data-testid="swap-select-token-item-DAI"]'
+ ) as HTMLElement
+ tokenItem?.click()
+
+ expect(setToTokenSpy.mock.calls.length).to.equal(1)
+ expect(swapTokensSpy.mock.calls.length).to.equal(1)
+ expect(goBackSpy.mock.calls.length).to.equal(1)
+ })
+
+ it('should handle scroll events', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const suggestedTokensContainer = element.shadowRoot?.querySelector(
+ '.suggested-tokens-container'
+ ) as HTMLElement
+ const tokensList = element.shadowRoot?.querySelector('.tokens') as HTMLElement
+
+ suggestedTokensContainer?.dispatchEvent(new Event('scroll'))
+ tokensList?.dispatchEvent(new Event('scroll'))
+
+ expect(
+ suggestedTokensContainer?.style.getPropertyValue('--suggested-tokens-scroll--left-opacity')
+ ).to.exist
+ expect(tokensList?.style.getPropertyValue('--tokens-scroll--top-opacity')).to.exist
+ })
+
+ it('should cleanup event listeners on disconnect', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ const removeEventListenerSpy = vi.spyOn(HTMLElement.prototype, 'removeEventListener')
+
+ element.disconnectedCallback()
+
+ expect(removeEventListenerSpy.mock.calls.length).to.equal(2)
+ })
+})
diff --git a/packages/scaffold-ui/test/modal/w3m-swap-view.test.ts b/packages/scaffold-ui/test/modal/w3m-swap-view.test.ts
new file mode 100644
index 0000000000..072ed6ceb4
--- /dev/null
+++ b/packages/scaffold-ui/test/modal/w3m-swap-view.test.ts
@@ -0,0 +1,221 @@
+import { expect, html, fixture } from '@open-wc/testing'
+import { describe, it, afterEach, beforeEach, vi } from 'vitest'
+import { W3mSwapView } from '../../src/views/w3m-swap-view'
+import {
+ SwapController,
+ RouterController,
+ ChainController,
+ type SwapTokenWithBalance,
+ type ChainControllerState
+} from '@reown/appkit-core'
+
+const mockToken: SwapTokenWithBalance = {
+ address: 'eip155:1:0x123',
+ symbol: 'TEST',
+ name: 'Test Token',
+ quantity: {
+ numeric: '100',
+ decimals: '18'
+ },
+ decimals: 18,
+ logoUri: 'https://example.com/icon.png',
+ price: 10,
+ value: 1000
+}
+
+const mockChainState: ChainControllerState = {
+ activeChain: 'eip155',
+ activeCaipNetwork: {
+ id: 1,
+ name: 'Ethereum',
+ chainNamespace: 'eip155',
+ caipNetworkId: 'eip155:1',
+ nativeCurrency: {
+ name: 'Ethereum',
+ symbol: 'ETH',
+ decimals: 18
+ },
+ rpcUrls: {
+ default: {
+ http: ['https://ethereum.rpc.com']
+ }
+ }
+ },
+ activeCaipAddress: 'eip155:1:0x123456789abcdef123456789abcdef123456789a',
+ chains: new Map(),
+ universalAdapter: {
+ networkControllerClient: {
+ switchCaipNetwork: vi.fn(),
+ getApprovedCaipNetworksData: vi.fn()
+ },
+ connectionControllerClient: {
+ connectWalletConnect: vi.fn(),
+ connectExternal: vi.fn(),
+ reconnectExternal: vi.fn(),
+ checkInstalled: vi.fn(),
+ disconnect: vi.fn(),
+ signMessage: vi.fn(),
+ sendTransaction: vi.fn(),
+ estimateGas: vi.fn(),
+ parseUnits: vi.fn(),
+ formatUnits: vi.fn(),
+ writeContract: vi.fn(),
+ getEnsAddress: vi.fn(),
+ getEnsAvatar: vi.fn(),
+ grantPermissions: vi.fn(),
+ revokePermissions: vi.fn(),
+ getCapabilities: vi.fn()
+ }
+ },
+ noAdapters: false
+}
+
+describe('W3mSwapView', () => {
+ beforeEach(() => {
+ vi.spyOn(SwapController, 'state', 'get').mockReturnValue({
+ initializing: false,
+ initialized: true,
+ loadingQuote: false,
+ loadingPrices: false,
+ loadingTransaction: false,
+ loadingApprovalTransaction: false,
+ loadingBuildTransaction: false,
+ fetchError: false,
+ approvalTransaction: undefined,
+ swapTransaction: undefined,
+ transactionError: undefined,
+ sourceToken: mockToken,
+ sourceTokenAmount: '1',
+ sourceTokenPriceInUSD: 10,
+ toToken: { ...mockToken, symbol: 'USDT' },
+ toTokenAmount: '10',
+ toTokenPriceInUSD: 1,
+ networkPrice: '0',
+ networkBalanceInUSD: '0',
+ networkTokenSymbol: '',
+ inputError: undefined,
+ slippage: 0.5,
+ tokens: [mockToken],
+ suggestedTokens: undefined,
+ popularTokens: undefined,
+ foundTokens: undefined,
+ myTokensWithBalance: [mockToken],
+ tokensPriceMap: {},
+ gasFee: '0',
+ gasPriceInUSD: 2,
+ priceImpact: undefined,
+ maxSlippage: undefined,
+ providerFee: undefined
+ })
+
+ vi.spyOn(ChainController, 'state', 'get').mockReturnValue(mockChainState)
+
+ vi.spyOn(SwapController, 'initializeState').mockImplementation(async () => {})
+ vi.spyOn(SwapController, 'getNetworkTokenPrice').mockImplementation(async () => {})
+ vi.spyOn(SwapController, 'getMyTokensWithBalance').mockImplementation(async () => {})
+ vi.spyOn(SwapController, 'swapTokens').mockImplementation(async () => {})
+ vi.spyOn(SwapController, 'switchTokens').mockImplementation(() => {})
+ vi.spyOn(SwapController, 'resetState').mockImplementation(() => {})
+ vi.spyOn(RouterController, 'push').mockImplementation(() => {})
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('should render initial state with token details', async () => {
+ const element = await fixture(html``)
+
+ await element.updateComplete
+
+ const swapInputs = element.shadowRoot?.querySelectorAll('w3m-swap-input')
+ expect(swapInputs?.length).to.equal(2)
+
+ const sourceInput = swapInputs?.[0]
+ expect(sourceInput?.value).to.equal('1')
+ expect(sourceInput?.target).to.equal('sourceToken')
+
+ const toInput = swapInputs?.[1]
+ expect(toInput?.value).to.equal('10')
+ expect(toInput?.target).to.equal('toToken')
+ })
+
+ it('should handle token switching', async () => {
+ const element = await fixture(html``)
+
+ await element.updateComplete
+
+ const switchTokensSpy = vi.spyOn(SwapController, 'switchTokens')
+ const switchButton = element.shadowRoot?.querySelector(
+ '.replace-tokens-button-container button'
+ ) as HTMLElement
+ switchButton?.click()
+
+ expect(switchTokensSpy.mock.calls.length).to.equal(1)
+ })
+
+ it('should show loading state when initializing', async () => {
+ vi.spyOn(SwapController, 'state', 'get').mockReturnValue({
+ ...SwapController.state,
+ initialized: false
+ })
+
+ const element = await fixture(html``)
+
+ await element.updateComplete
+
+ const skeletons = element.shadowRoot?.querySelectorAll('w3m-swap-input-skeleton')
+ expect(skeletons?.length).to.equal(2)
+ })
+
+ it('should handle swap preview navigation', async () => {
+ const element = await fixture(html``)
+
+ await element.updateComplete
+
+ const routerPushSpy = vi.spyOn(RouterController, 'push')
+ const actionButton = element.shadowRoot?.querySelector(
+ '[data-testid="swap-action-button"]'
+ ) as HTMLElement
+ actionButton?.click()
+
+ expect(routerPushSpy.mock.calls[0]?.[0]).to.equal('SwapPreview')
+ })
+
+ it('should show error state in action button', async () => {
+ vi.spyOn(SwapController, 'state', 'get').mockReturnValue({
+ ...SwapController.state,
+ inputError: 'Insufficient balance'
+ })
+
+ const element = await fixture(html``)
+
+ await element.updateComplete
+
+ const actionButton = element.shadowRoot?.querySelector('[data-testid="swap-action-button"]')
+ expect(actionButton?.textContent?.trim()).to.equal('Insufficient balance')
+ })
+
+ it('should handle loading states', async () => {
+ vi.spyOn(SwapController, 'state', 'get').mockReturnValue({
+ ...SwapController.state,
+ loadingQuote: true
+ })
+
+ const element = await fixture(html``)
+
+ await element.updateComplete
+
+ const actionButton = element.shadowRoot?.querySelector('wui-button')
+ expect(actionButton?.loading).to.be.true
+ })
+
+ it('should cleanup on disconnect', async () => {
+ const element = await fixture(html``)
+
+ const clearIntervalSpy = vi.spyOn(window, 'clearInterval')
+ element.disconnectedCallback()
+
+ expect(clearIntervalSpy.mock.calls.length).to.equal(1)
+ })
+})
diff --git a/packages/scaffold-ui/test/modal/w3m-wallet-send-preview-view.test.ts b/packages/scaffold-ui/test/modal/w3m-wallet-send-preview-view.test.ts
new file mode 100644
index 0000000000..4e7e093d9d
--- /dev/null
+++ b/packages/scaffold-ui/test/modal/w3m-wallet-send-preview-view.test.ts
@@ -0,0 +1,238 @@
+import { expect, html, fixture } from '@open-wc/testing'
+import {
+ ChainController,
+ RouterController,
+ SendController,
+ type NetworkControllerClient,
+ type ConnectionControllerClient,
+ type ChainAdapter
+} from '@reown/appkit-core'
+import { W3mWalletSendPreviewView } from '../../src/views/w3m-wallet-send-preview-view'
+import { describe, it, afterEach, beforeEach, vi, expect as viExpect } from 'vitest'
+import type { Balance, CaipNetwork, ChainNamespace, CaipAddress } from '@reown/appkit-common'
+
+const mockToken: Balance = {
+ address: '0x123',
+ symbol: 'TEST',
+ name: 'Test Token',
+ quantity: {
+ numeric: '100',
+ decimals: '18'
+ },
+ price: 10,
+ chainId: 'eip155:1',
+ iconUrl: 'https://example.com/icon.png',
+ value: 1000
+}
+
+const mockNetwork: CaipNetwork = {
+ id: 1,
+ name: 'Ethereum',
+ chainNamespace: 'eip155',
+ caipNetworkId: 'eip155:1',
+ nativeCurrency: {
+ name: 'Ethereum',
+ symbol: 'ETH',
+ decimals: 18
+ },
+ rpcUrls: {
+ default: {
+ http: ['https://ethereum.rpc.com']
+ }
+ }
+}
+
+const mockSendControllerState = {
+ token: mockToken,
+ sendTokenAmount: 5,
+ receiverAddress: '0x456',
+ gasPriceInUSD: 2.5,
+ loading: false
+}
+
+const mockNetworkControllerClient: NetworkControllerClient = {
+ switchCaipNetwork: vi.fn(),
+ getApprovedCaipNetworksData: vi.fn().mockResolvedValue({
+ approvedCaipNetworkIds: ['eip155:1'],
+ supportsAllNetworks: true
+ })
+}
+
+const mockConnectionControllerClient: ConnectionControllerClient = {
+ connectWalletConnect: vi.fn(),
+ connectExternal: vi.fn(),
+ reconnectExternal: vi.fn(),
+ checkInstalled: vi.fn(),
+ disconnect: vi.fn(),
+ signMessage: vi.fn(),
+ sendTransaction: vi.fn(),
+ estimateGas: vi.fn(),
+ parseUnits: vi.fn(),
+ formatUnits: vi.fn(),
+ writeContract: vi.fn(),
+ getEnsAddress: vi.fn(),
+ getEnsAvatar: vi.fn(),
+ grantPermissions: vi.fn(),
+ revokePermissions: vi.fn(),
+ getCapabilities: vi.fn()
+}
+
+const mockChainAdapter: ChainAdapter = {
+ namespace: 'eip155' as ChainNamespace,
+ networkControllerClient: mockNetworkControllerClient,
+ connectionControllerClient: mockConnectionControllerClient
+}
+
+const mockChainControllerState = {
+ activeChain: 'eip155' as ChainNamespace,
+ activeCaipNetwork: mockNetwork,
+ activeCaipAddress: 'eip155:1:0x123456789abcdef123456789abcdef123456789a' as CaipAddress,
+ chains: new Map([['eip155' as ChainNamespace, mockChainAdapter]]),
+ universalAdapter: {
+ networkControllerClient: mockNetworkControllerClient,
+ connectionControllerClient: mockConnectionControllerClient
+ },
+ noAdapters: false
+}
+
+describe('W3mWalletSendPreviewView', () => {
+ beforeEach(() => {
+ // Mock SendController state
+ vi.spyOn(SendController, 'state', 'get').mockReturnValue(mockSendControllerState)
+
+ // Mock ChainController state
+ vi.spyOn(ChainController, 'state', 'get').mockReturnValue(mockChainControllerState)
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('should render initial state with token details', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const previewItems = element.shadowRoot?.querySelectorAll('wui-preview-item')
+ expect(previewItems?.length).to.equal(2)
+
+ // Check token preview
+ const tokenPreview = previewItems?.[0]
+ expect(tokenPreview?.text).to.equal('5 TEST')
+ expect(tokenPreview?.imageSrc).to.equal(mockToken.iconUrl)
+
+ const valueText = element.shadowRoot?.querySelector('wui-text[variant="paragraph-400"]')
+ expect(valueText?.textContent?.trim()).to.equal('$50.00')
+ })
+
+ it('should display truncated address when no profile name', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const addressPreview = element.shadowRoot?.querySelectorAll('wui-preview-item')?.[1]
+ expect(addressPreview?.text).to.contain('0x45')
+ expect(addressPreview?.address).to.equal('0x456')
+ expect(addressPreview?.isAddress).to.be.true
+ })
+
+ it('should display profile name when available', async () => {
+ vi.spyOn(SendController, 'state', 'get').mockReturnValue({
+ ...mockSendControllerState,
+ receiverProfileName: 'Test User',
+ receiverProfileImageUrl: 'https://example.com/profile.jpg'
+ })
+
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const addressPreview = element.shadowRoot?.querySelectorAll('wui-preview-item')?.[1]
+ expect(addressPreview?.text).to.equal('Test User')
+ expect(addressPreview?.imageSrc).to.equal('https://example.com/profile.jpg')
+ expect(addressPreview?.address).to.equal('0x456')
+ expect(addressPreview?.isAddress).to.be.true
+ })
+
+ it('should handle send action', async () => {
+ const sendSpy = vi.spyOn(SendController, 'sendToken')
+
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const button = element.shadowRoot?.querySelector('.sendButton') as HTMLElement
+ button?.click()
+
+ viExpect(sendSpy).toHaveBeenCalled()
+ })
+
+ it('should handle cancel action', async () => {
+ const routerSpy = vi.spyOn(RouterController, 'goBack')
+
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const button = element.shadowRoot?.querySelector('.cancelButton') as HTMLElement
+ button?.click()
+
+ viExpect(routerSpy).toHaveBeenCalled()
+ })
+
+ it('should display network fee', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const detailsElement = element.shadowRoot?.querySelector('w3m-wallet-send-details')
+ expect(detailsElement).to.exist
+ expect(detailsElement?.networkFee).to.equal(2.5)
+ expect(detailsElement?.receiverAddress).to.equal('0x456')
+ expect(detailsElement?.caipNetwork).to.deep.equal(mockNetwork)
+ })
+
+ it('should cleanup subscriptions on disconnect', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ const unsubscribeSpy = vi.fn()
+ element['unsubscribe'] = [unsubscribeSpy]
+
+ element.disconnectedCallback()
+ viExpect(unsubscribeSpy).toHaveBeenCalled()
+ })
+
+ it('should update when token details change', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const newToken = { ...mockToken, price: 20 }
+ vi.spyOn(SendController, 'state', 'get').mockReturnValue({
+ ...mockSendControllerState,
+ token: newToken
+ })
+
+ element['token'] = newToken
+ await element.updateComplete
+
+ const valueText = element.shadowRoot?.querySelector('wui-text[variant="paragraph-400"]')
+ expect(valueText?.textContent?.trim()).to.equal('$100.00')
+ })
+})
diff --git a/packages/scaffold-ui/test/modal/w3m-wallet-send-select-token-view.test.ts b/packages/scaffold-ui/test/modal/w3m-wallet-send-select-token-view.test.ts
new file mode 100644
index 0000000000..32cae54dca
--- /dev/null
+++ b/packages/scaffold-ui/test/modal/w3m-wallet-send-select-token-view.test.ts
@@ -0,0 +1,182 @@
+import { expect, html, fixture } from '@open-wc/testing'
+import {
+ AccountController,
+ ChainController,
+ RouterController,
+ SendController
+} from '@reown/appkit-core'
+import { W3mSendSelectTokenView } from '../../src/views/w3m-wallet-send-select-token-view'
+import { describe, it, afterEach, beforeEach, vi, expect as viExpect } from 'vitest'
+import type { Balance, CaipNetwork } from '@reown/appkit-common'
+
+const mockTokens: Balance[] = [
+ {
+ address: '0x123',
+ symbol: 'TEST1',
+ name: 'Test Token 1',
+ quantity: {
+ numeric: '100',
+ decimals: '18'
+ },
+ price: 1,
+ chainId: 'eip155:1',
+ iconUrl: 'https://example.com/icon1.png',
+ value: 100
+ },
+ {
+ address: '0x456',
+ symbol: 'TEST2',
+ name: 'Test Token 2',
+ quantity: {
+ numeric: '200',
+ decimals: '18'
+ },
+ price: 2,
+ chainId: 'eip155:1',
+ iconUrl: 'https://example.com/icon2.png',
+ value: 400
+ },
+ {
+ address: '0x789',
+ symbol: 'TEST3',
+ name: 'Different Chain Token',
+ quantity: {
+ numeric: '300',
+ decimals: '18'
+ },
+ price: 1,
+ chainId: 'eip155:2',
+ iconUrl: 'https://example.com/icon3.png',
+ value: 300
+ }
+]
+
+const mockNetwork: CaipNetwork = {
+ id: 1,
+ name: 'Ethereum',
+ chainNamespace: 'eip155',
+ caipNetworkId: 'eip155:1',
+ nativeCurrency: {
+ name: 'Ethereum',
+ symbol: 'ETH',
+ decimals: 18
+ },
+ rpcUrls: {
+ default: {
+ http: ['https://ethereum.rpc.com']
+ }
+ }
+}
+
+describe('W3mSendSelectTokenView', () => {
+ beforeEach(() => {
+ vi.spyOn(AccountController.state, 'tokenBalance', 'get').mockReturnValue(mockTokens)
+ vi.spyOn(ChainController.state, 'activeCaipNetwork', 'get').mockReturnValue(mockNetwork)
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ })
+
+ it('should render initial state with filtered tokens by chain', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const tokenElements = element.shadowRoot?.querySelectorAll('wui-list-token')
+ expect(tokenElements?.length).to.equal(2)
+
+ const searchInput = element.shadowRoot?.querySelector('wui-input-text')
+ expect(searchInput).to.exist
+ })
+
+ it('should filter tokens by search input', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ element['search'] = 'Test Token 1'
+ await element.updateComplete
+
+ const tokenElements = element.shadowRoot?.querySelectorAll('wui-list-token')
+ expect(tokenElements?.length).to.equal(1)
+ expect(tokenElements?.[0]?.getAttribute('tokenName')).to.equal('Test Token 1')
+ })
+
+ it('should show empty state when no tokens match filter', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ element['search'] = 'Non Existent Token'
+ await element.updateComplete
+
+ const noTokensText = element.shadowRoot?.querySelector('wui-text[color="fg-100"]')
+ expect(noTokensText?.textContent?.trim()).to.equal('No tokens found')
+ })
+
+ it('should handle token selection', async () => {
+ const routerSpy = vi.spyOn(RouterController, 'goBack')
+ const sendControllerSpy = vi.spyOn(SendController, 'setToken')
+
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const tokenElements = element.shadowRoot?.querySelectorAll('wui-list-token')
+ tokenElements?.[0]?.click()
+
+ viExpect(sendControllerSpy).toHaveBeenCalledWith(mockTokens[0])
+ viExpect(routerSpy).toHaveBeenCalled()
+ })
+
+ it('should navigate to OnRampProviders on buy click', async () => {
+ const routerSpy = vi.spyOn(RouterController, 'push')
+ vi.spyOn(AccountController.state, 'tokenBalance', 'get').mockReturnValue([])
+
+ const element = await fixture(
+ html``
+ )
+
+ await element.updateComplete
+
+ const buyLink = element.shadowRoot?.querySelector('wui-link')
+ buyLink?.click()
+
+ viExpect(routerSpy).toHaveBeenCalledWith('OnRampProviders')
+ })
+
+ it('should cleanup subscriptions on disconnect', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ const unsubscribeSpy = vi.fn()
+ element['unsubscribe'] = [unsubscribeSpy]
+
+ element.disconnectedCallback()
+ viExpect(unsubscribeSpy).toHaveBeenCalled()
+ })
+
+ it('should show all tokens when search is cleared', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ element['search'] = 'Test Token 1'
+ await element.updateComplete
+
+ let tokenElements = element.shadowRoot?.querySelectorAll('wui-list-token')
+ expect(tokenElements?.length).to.equal(1)
+
+ element['search'] = ''
+ await element.updateComplete
+
+ tokenElements = element.shadowRoot?.querySelectorAll('wui-list-token')
+ expect(tokenElements?.length).to.equal(2)
+ })
+})
diff --git a/packages/scaffold-ui/test/modal/w3m-wallet-send-view.test.ts b/packages/scaffold-ui/test/modal/w3m-wallet-send-view.test.ts
new file mode 100644
index 0000000000..a985b9574d
--- /dev/null
+++ b/packages/scaffold-ui/test/modal/w3m-wallet-send-view.test.ts
@@ -0,0 +1,164 @@
+import { expect, html, fixture } from '@open-wc/testing'
+import { SendController, RouterController, SwapController } from '@reown/appkit-core'
+import { W3mWalletSendView } from '../../src/views/w3m-wallet-send-view'
+import { describe, it, afterEach, beforeEach, vi, expect as viExpect } from 'vitest'
+import type { Balance } from '@reown/appkit-common'
+
+const mockToken: Balance = {
+ address: '0x123',
+ symbol: 'TEST',
+ name: 'Test Token',
+ quantity: {
+ numeric: '100',
+ decimals: '18'
+ },
+ price: 1,
+ chainId: '1',
+ iconUrl: 'https://example.com/icon.png'
+}
+
+describe('W3mWalletSendView', () => {
+ beforeEach(() => {
+ vi.spyOn(SwapController, 'getNetworkTokenPrice').mockResolvedValue()
+ vi.spyOn(SwapController, 'getInitialGasPrice').mockResolvedValue({
+ gasPrice: BigInt(1000),
+ gasPriceInUSD: 0.1
+ })
+ })
+
+ afterEach(() => {
+ vi.clearAllMocks()
+ SendController.resetSend()
+ })
+
+ it('should render initial state correctly', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ expect(element).to.exist
+ const button = element.shadowRoot?.querySelector('wui-button')
+ expect(button).to.exist
+ expect(button?.textContent?.trim()).to.equal('Select Token')
+ expect(button?.disabled).to.be.true
+ })
+
+ it('should update message when token is selected', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ SendController.setToken(mockToken)
+ await element.updateComplete
+ await element.render()
+
+ const button = element.shadowRoot?.querySelector('wui-button')
+ expect(button?.textContent?.trim()).to.equal('Add Amount')
+ expect(button?.disabled).to.be.true
+ })
+
+ it('should update message when amount is set', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ SendController.setToken(mockToken)
+ SendController.setTokenAmount(50)
+ SendController.setNetworkBalanceInUsd('100')
+
+ await element.updateComplete
+ await element.render()
+
+ const button = element.shadowRoot?.querySelector('wui-button')
+ expect(button?.textContent?.trim()).to.equal('Add Address')
+ expect(button?.disabled).to.be.true
+ })
+
+ it('should show insufficient funds message when amount exceeds balance', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ SendController.setToken(mockToken)
+ SendController.setTokenAmount(150)
+ SendController.setReceiverAddress('0x456')
+ await element.updateComplete
+ await element.render()
+
+ const button = element.shadowRoot?.querySelector('wui-button')
+ expect(button?.textContent?.trim()).to.equal('Insufficient Funds')
+ expect(button?.disabled).to.be.true
+ })
+
+ it('should show invalid address message for incorrect address', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ SendController.setToken(mockToken)
+ SendController.setTokenAmount(50)
+ SendController.setGasPrice(BigInt(1))
+ SendController.setNetworkBalanceInUsd('100')
+
+ SendController.setReceiverAddress('invalid-address')
+ await element.updateComplete
+ await element.render()
+
+ const button = element.shadowRoot?.querySelector('wui-button')
+ expect(button?.textContent?.trim()).to.equal('Invalid Address')
+ expect(button?.disabled).to.be.true
+ })
+
+ it('should enable preview when all inputs are valid', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ SendController.setToken(mockToken)
+ SendController.setTokenAmount(50)
+ SendController.setReceiverAddress('0x123456789abcdef123456789abcdef123456789a')
+ await element.updateComplete
+ await element.render()
+
+ const button = element.shadowRoot?.querySelector('wui-button')
+ expect(button?.textContent?.trim()).to.equal('Preview Send')
+ expect(button?.disabled).to.be.false
+ })
+
+ it('should navigate to preview on button click', async () => {
+ const routerSpy = vi.spyOn(RouterController, 'push')
+ const element = await fixture(
+ html``
+ )
+
+ SendController.setToken(mockToken)
+ SendController.setTokenAmount(50)
+ SendController.setReceiverAddress('0x123456789abcdef123456789abcdef123456789a')
+ await element.updateComplete
+ await element.render()
+
+ const button = element.shadowRoot?.querySelector('wui-button')
+ button?.click()
+
+ viExpect(routerSpy).toHaveBeenCalledWith('WalletSendPreview')
+ })
+
+ it('should fetch network price on initialization', async () => {
+ await fixture(html``)
+
+ viExpect(SwapController.getNetworkTokenPrice).toHaveBeenCalled()
+ viExpect(SwapController.getInitialGasPrice).toHaveBeenCalled()
+ })
+
+ it('should cleanup subscriptions on disconnect', async () => {
+ const element = await fixture(
+ html``
+ )
+
+ const unsubscribeSpy = vi.fn()
+ element['unsubscribe'] = [unsubscribeSpy]
+
+ element.disconnectedCallback()
+ viExpect(unsubscribeSpy).toHaveBeenCalled()
+ })
+})
diff --git a/packages/siwe/src/client.ts b/packages/siwe/src/client.ts
index 04bbbcc4ae..e790541e96 100644
--- a/packages/siwe/src/client.ts
+++ b/packages/siwe/src/client.ts
@@ -7,7 +7,7 @@ import type {
SIWESession,
SIWEVerifyMessageArgs
} from '../core/utils/TypeUtils.js'
-
+import { mapToSIWX } from '../src/mapToSIWX.js'
import { SIWXUtil } from '@reown/appkit-core'
import { ConstantsUtil } from '../core/utils/ConstantsUtil.js'
@@ -41,6 +41,10 @@ export class AppKitSIWEClient {
this.methods = siweConfigMethods
}
+ public mapToSIWX() {
+ return mapToSIWX(this)
+ }
+
async getNonce(address?: string) {
const nonce = await this.methods.getNonce(address)
if (!nonce) {
diff --git a/packages/siwx/src/configs/CloudAuthSIWX.ts b/packages/siwx/src/configs/CloudAuthSIWX.ts
index 95713b9d0d..bef753568c 100644
--- a/packages/siwx/src/configs/CloudAuthSIWX.ts
+++ b/packages/siwx/src/configs/CloudAuthSIWX.ts
@@ -1,4 +1,10 @@
-import { ConstantsUtil, type CaipNetworkId } from '@reown/appkit-common'
+import {
+ ConstantsUtil,
+ type CaipNetworkId,
+ SafeLocalStorage,
+ SafeLocalStorageKeys,
+ type SafeLocalStorageItems
+} from '@reown/appkit-common'
import {
AccountController,
ApiController,
@@ -18,11 +24,17 @@ import { InformalMessenger } from '../index.js'
* WARNING: The Claud Auth is only available in EVM networks.
*/
export class CloudAuthSIWX implements SIWXConfig {
- private readonly localStorageKey: string
+ private readonly localAuthStorageKey: keyof SafeLocalStorageItems
+ private readonly localNonceStorageKey: keyof SafeLocalStorageItems
private readonly messenger: SIWXMessenger
constructor(params: CloudAuthSIWX.ConstructorParams = {}) {
- this.localStorageKey = params.localStorageKey || '@appkit/siwx-token'
+ this.localAuthStorageKey =
+ (params.localAuthStorageKey as keyof SafeLocalStorageItems) ||
+ SafeLocalStorageKeys.SIWX_AUTH_TOKEN
+ this.localNonceStorageKey =
+ (params.localNonceStorageKey as keyof SafeLocalStorageItems) ||
+ SafeLocalStorageKeys.SIWX_NONCE_TOKEN
this.messenger = new InformalMessenger({
domain: typeof document === 'undefined' ? 'Unknown Domain' : document.location.host,
@@ -37,13 +49,17 @@ export class CloudAuthSIWX implements SIWXConfig {
}
async addSession(session: SIWXSession): Promise {
- const response = await this.request('authenticate', {
- message: session.message,
- signature: session.signature,
- clientId: this.getClientId(),
- walletInfo: this.getWalletInfo()
- })
- this.setStorageToken(response.token)
+ const response = await this.request(
+ 'authenticate',
+ {
+ message: session.message,
+ signature: session.signature,
+ clientId: this.getClientId(),
+ walletInfo: this.getWalletInfo()
+ },
+ 'nonceJwt'
+ )
+ this.setStorageToken(response.token, this.localAuthStorageKey)
}
async getSessions(chainId: CaipNetworkId, address: string): Promise {
@@ -72,12 +88,12 @@ export class CloudAuthSIWX implements SIWXConfig {
}
async revokeSession(_chainId: CaipNetworkId, _address: string): Promise {
- return Promise.resolve(this.clearStorageToken())
+ return Promise.resolve(this.clearStorageTokens())
}
async setSessions(sessions: SIWXSession[]): Promise {
if (sessions.length === 0) {
- this.clearStorageToken()
+ this.clearStorageTokens()
} else {
const session = (sessions.find(
s => s.data.chainId === ChainController.getActiveCaipNetwork()?.caipNetworkId
@@ -89,21 +105,31 @@ export class CloudAuthSIWX implements SIWXConfig {
private async request(
key: Key,
- params: CloudAuthSIWX.Requests[Key]['body']
+ params: CloudAuthSIWX.Requests[Key]['body'],
+ tokenType: 'authJwt' | 'nonceJwt' = 'authJwt'
): Promise {
const { projectId, st, sv } = this.getSDKProperties()
- const token = this.getStorageToken()
+
+ const token =
+ tokenType === 'nonceJwt'
+ ? this.getStorageToken(this.localNonceStorageKey)
+ : this.getStorageToken(this.localAuthStorageKey)
+
+ const jwtHeader: { 'x-nonce-jwt': string } | { Authorization: string } =
+ tokenType === 'nonceJwt'
+ ? {
+ 'x-nonce-jwt': `Bearer ${token}`
+ }
+ : {
+ Authorization: `Bearer ${token}`
+ }
const response = await fetch(
`${ConstantsUtil.W3M_API_URL}/auth/v1/${key}?projectId=${projectId}&st=${st}&sv=${sv}`,
{
method: RequestMethod[key],
body: params ? JSON.stringify(params) : undefined,
- headers: token
- ? {
- Authorization: `Bearer ${token}`
- }
- : undefined
+ headers: token ? jwtHeader : undefined
}
)
@@ -114,22 +140,23 @@ export class CloudAuthSIWX implements SIWXConfig {
throw new Error(await response.text())
}
- private getStorageToken(): string | undefined {
- return localStorage.getItem(this.localStorageKey) || undefined
+ private getStorageToken(key: keyof SafeLocalStorageItems): string | undefined {
+ return SafeLocalStorage.getItem(key)
}
- private setStorageToken(token: string): void {
- localStorage.setItem(this.localStorageKey, token)
+ private setStorageToken(token: string, key: keyof SafeLocalStorageItems): void {
+ SafeLocalStorage.setItem(key, token)
}
- private clearStorageToken(): void {
- localStorage.removeItem(this.localStorageKey)
+ private clearStorageTokens(): void {
+ SafeLocalStorage.removeItem(this.localAuthStorageKey)
+ SafeLocalStorage.removeItem(this.localNonceStorageKey)
}
private async getNonce(): Promise {
const { nonce, token } = await this.request('nonce', undefined)
- this.setStorageToken(token)
+ this.setStorageToken(token, this.localNonceStorageKey)
return nonce
}
@@ -171,9 +198,14 @@ export namespace CloudAuthSIWX {
export type ConstructorParams = {
/**
* The key to use for storing the session token in local storage.
- * @default '@appkit/siwx-token'
+ * @default '@appkit/siwx-auth-token'
+ */
+ localAuthStorageKey?: string
+ /**
+ * The key to use for storing the nonce token in local storage.
+ * @default '@appkit/siwx-nonce-token'
*/
- localStorageKey?: string
+ localNonceStorageKey?: string
}
export type Request = {
diff --git a/packages/siwx/tests/configs/CloudAuthSIWX.test.ts b/packages/siwx/tests/configs/CloudAuthSIWX.test.ts
index c91126de7f..8d6a294a86 100644
--- a/packages/siwx/tests/configs/CloudAuthSIWX.test.ts
+++ b/packages/siwx/tests/configs/CloudAuthSIWX.test.ts
@@ -80,7 +80,7 @@ Issued At: 2024-12-05T16:02:32.905Z`)
}
)
- expect(setItemSpy).toHaveBeenCalledWith('@appkit/siwx-token', 'mock_token')
+ expect(setItemSpy).toHaveBeenCalledWith('@appkit/siwx-nonce-token', 'mock_token')
})
it('should throw an text error if response is not json', async () => {
@@ -137,12 +137,12 @@ Issued At: 2024-12-05T16:02:32.905Z`)
{
body: '{"message":"Hello AppKit!","signature":"0x3c70e0a2d87f677dc0c3faf98fdf6313e99a3d9191bb79f7ecfce0c2cf46b7b33fd4c4bb83bca82fe872e35963382027d0d18018342d7dc36a675918cb73e9061c","clientId":null}',
headers: {
- Authorization: 'Bearer mock_nonce_token'
+ 'x-nonce-jwt': 'Bearer mock_nonce_token'
},
method: 'POST'
}
)
- expect(setItemSpy).toHaveBeenCalledWith('@appkit/siwx-token', 'mock_authenticate_token')
+ expect(setItemSpy).toHaveBeenCalledWith('@appkit/siwx-auth-token', 'mock_authenticate_token')
})
it('should use correct client id', async () => {
@@ -162,7 +162,7 @@ Issued At: 2024-12-05T16:02:32.905Z`)
{
body: '{"message":"Hello AppKit!","signature":"0x3c70e0a2d87f677dc0c3faf98fdf6313e99a3d9191bb79f7ecfce0c2cf46b7b33fd4c4bb83bca82fe872e35963382027d0d18018342d7dc36a675918cb73e9061c","clientId":"mock_client_id"}',
headers: {
- Authorization: 'Bearer mock_nonce_token'
+ 'x-nonce-jwt': 'Bearer mock_nonce_token'
},
method: 'POST'
}
@@ -188,7 +188,7 @@ Issued At: 2024-12-05T16:02:32.905Z`)
{
body: '{"message":"Hello AppKit!","signature":"0x3c70e0a2d87f677dc0c3faf98fdf6313e99a3d9191bb79f7ecfce0c2cf46b7b33fd4c4bb83bca82fe872e35963382027d0d18018342d7dc36a675918cb73e9061c","walletInfo":{"name":"mock_wallet_name","icon":"mock_wallet_icon"}}',
headers: {
- Authorization: 'Bearer mock_nonce_token'
+ 'x-nonce-jwt': 'Bearer mock_nonce_token'
},
method: 'POST'
}
@@ -280,7 +280,7 @@ Issued At: 2024-12-05T16:02:32.905Z`)
await siwx.revokeSession('eip155:1', '0x1234567890abcdef1234567890abcdef12345678')
- expect(removeItemSpy).toHaveBeenCalledWith('@appkit/siwx-token')
+ expect(removeItemSpy).toHaveBeenCalledWith('@appkit/siwx-auth-token')
})
})
@@ -290,7 +290,7 @@ Issued At: 2024-12-05T16:02:32.905Z`)
await siwx.setSessions([])
- expect(removeItemSpy).toHaveBeenCalledWith('@appkit/siwx-token')
+ expect(removeItemSpy).toHaveBeenCalledWith('@appkit/siwx-auth-token')
})
it('adds a session with default first item', async () => {
diff --git a/packages/ui/src/composites/wui-list-account/index.ts b/packages/ui/src/composites/wui-list-account/index.ts
index 9f36b8ece0..2e56feb35e 100644
--- a/packages/ui/src/composites/wui-list-account/index.ts
+++ b/packages/ui/src/composites/wui-list-account/index.ts
@@ -14,6 +14,7 @@ import {
ChainController,
StorageUtil
} from '@reown/appkit-core'
+import { ConstantsUtil } from '@reown/appkit-common'
@customElement('wui-list-account')
export class WuiListAccount extends LitElement {
@@ -24,7 +25,7 @@ export class WuiListAccount extends LitElement {
@property() public accountType = ''
- private connectedConnector = StorageUtil.getConnectedConnector()
+ private connectorId = StorageUtil.getConnectedConnectorId()
private labels = AccountController.state.addressLabels
@@ -68,7 +69,7 @@ export class WuiListAccount extends LitElement {
const label = this.getLabel()
// Only show icon for AUTH accounts
- this.shouldShowIcon = this.connectedConnector === 'ID_AUTH'
+ this.shouldShowIcon = this.connectorId === ConstantsUtil.CONNECTOR_ID.AUTH
return html`
void
- private connectedConnector = StorageUtil.getConnectedConnector()
+ private connectorId = StorageUtil.getConnectedConnectorId()
- private shouldShowIcon = this.connectedConnector === 'AUTH'
+ private shouldShowIcon = this.connectorId === ConstantsUtil.CONNECTOR_ID.AUTH
// -- Render -------------------------------------------- //
public override render() {