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() {