@reown/appkit-controllers
Version:
The full stack toolkit to build onchain app UX.
376 lines • 17.2 kB
JavaScript
import { useCallback, useEffect, useState } from 'react';
import { useSnapshot } from 'valtio';
import { ConstantsUtil } from '@reown/appkit-common';
import { AlertController } from '../src/controllers/AlertController.js';
import { ApiController } from '../src/controllers/ApiController.js';
import { AssetController } from '../src/controllers/AssetController.js';
import { BlockchainApiController } from '../src/controllers/BlockchainApiController.js';
import { ChainController } from '../src/controllers/ChainController.js';
import { ConnectionController } from '../src/controllers/ConnectionController.js';
import { ConnectorController } from '../src/controllers/ConnectorController.js';
import { OptionsController } from '../src/controllers/OptionsController.js';
import { ProviderController } from '../src/controllers/ProviderController.js';
import { PublicStateController } from '../src/controllers/PublicStateController.js';
import { ConnectUtil } from '../src/utils/ConnectUtil.js';
import { ConnectionControllerUtil } from '../src/utils/ConnectionControllerUtil.js';
import { ConnectorControllerUtil } from '../src/utils/ConnectorControllerUtil.js';
import { CoreHelperUtil } from '../src/utils/CoreHelperUtil.js';
import { MobileWalletUtil } from '../src/utils/MobileWallet.js';
import { AssetUtil, StorageUtil } from './utils.js';
// -- Hooks ------------------------------------------------------------
export function useAppKitProvider(chainNamespace) {
const { providers, providerIds } = useSnapshot(ProviderController.state);
const walletProvider = providers[chainNamespace];
const walletProviderType = providerIds[chainNamespace];
return {
walletProvider,
walletProviderType
};
}
export function useAppKitNetworkCore() {
const { activeCaipNetwork, activeChain, chains } = useSnapshot(ChainController.state);
const networkState = activeChain ? chains.get(activeChain)?.networkState : undefined;
return {
caipNetwork: activeCaipNetwork,
chainId: activeCaipNetwork?.id,
caipNetworkId: activeCaipNetwork?.caipNetworkId,
approvedCaipNetworkIds: networkState?.approvedCaipNetworkIds,
supportsAllNetworks: networkState?.supportsAllNetworks ?? true
};
}
export function useAppKitAccount(options) {
const state = useSnapshot(ChainController.state);
const { activeConnectorIds } = useSnapshot(ConnectorController.state);
const { connections: connectionsByNamespace } = useSnapshot(ConnectionController.state);
const chainNamespace = options?.namespace || state.activeChain;
if (!chainNamespace) {
return {
allAccounts: [],
address: undefined,
caipAddress: undefined,
status: undefined,
isConnected: false,
embeddedWalletInfo: undefined
};
}
const chainAccountState = state.chains.get(chainNamespace)?.accountState;
const authConnector = ConnectorController.getAuthConnector(chainNamespace);
const activeConnectorId = activeConnectorIds[chainNamespace];
const connections = connectionsByNamespace.get(chainNamespace) ?? [];
const allAccounts = connections.flatMap(connection => {
const { caipNetwork } = connection;
return caipNetwork
? connection.accounts.map(({ address, type, publicKey }) => CoreHelperUtil.createAccount({
caipAddress: `${caipNetwork.caipNetworkId}:${address}`,
type: type || 'eoa',
publicKey
}))
: [];
});
return {
allAccounts,
caipAddress: chainAccountState?.caipAddress,
address: CoreHelperUtil.getPlainAddress(chainAccountState?.caipAddress),
isConnected: Boolean(chainAccountState?.caipAddress),
status: chainAccountState?.status,
embeddedWalletInfo: authConnector && activeConnectorId === ConstantsUtil.CONNECTOR_ID.AUTH
? {
user: chainAccountState?.user
? {
...chainAccountState.user,
/*
* Getting the username from the chain controller works well for social logins,
* but Farcaster uses a different connection flow and doesn’t emit the username via events.
* Since the username is stored in local storage before the chain controller updates,
* it’s safe to use the local storage value here.
*/
username: StorageUtil.getConnectedSocialUsername()
}
: undefined,
authProvider: chainAccountState?.socialProvider || 'email',
accountType: chainAccountState?.preferredAccountType,
isSmartAccountDeployed: Boolean(chainAccountState?.smartAccountDeployed)
}
: undefined
};
}
export function useDisconnect() {
async function disconnect(props) {
await ConnectionController.disconnect(props);
}
return { disconnect };
}
export function useAppKitConnections(namespace) {
// Snapshots to trigger re-renders on state changes
useSnapshot(ConnectionController.state);
useSnapshot(ConnectorController.state);
useSnapshot(AssetController.state);
const { activeChain } = useSnapshot(ChainController.state);
const { remoteFeatures } = useSnapshot(OptionsController.state);
const chainNamespace = namespace ?? activeChain;
const isMultiWalletEnabled = Boolean(remoteFeatures?.multiWallet);
if (!chainNamespace) {
throw new Error('No namespace found');
}
const formatConnection = useCallback((connection) => {
const connector = ConnectorController.getConnectorById(connection.connectorId);
const name = ConnectorController.getConnectorName(connector?.name);
const icon = AssetUtil.getConnectorImage(connector);
const networkImage = AssetUtil.getNetworkImage(connection.caipNetwork);
return {
name,
icon,
networkIcon: networkImage,
...connection
};
}, []);
const { connections, recentConnections } = ConnectionControllerUtil.getConnectionsData(chainNamespace);
if (!isMultiWalletEnabled) {
AlertController.open(ConstantsUtil.REMOTE_FEATURES_ALERTS.MULTI_WALLET_NOT_ENABLED.CONNECTIONS_HOOK, 'info');
return {
connections: [],
recentConnections: []
};
}
return {
connections: connections.map(formatConnection),
recentConnections: recentConnections.map(formatConnection)
};
}
export function useAppKitConnection({ namespace, onSuccess, onError }) {
const { connections, isSwitchingConnection } = useSnapshot(ConnectionController.state);
const { activeConnectorIds } = useSnapshot(ConnectorController.state);
const { activeChain } = useSnapshot(ChainController.state);
const { remoteFeatures } = useSnapshot(OptionsController.state);
const chainNamespace = namespace ?? activeChain;
if (!chainNamespace) {
throw new Error('No namespace found');
}
const isMultiWalletEnabled = Boolean(remoteFeatures?.multiWallet);
const switchConnection = useCallback(async ({ connection: _connection, address }) => {
try {
ConnectionController.setIsSwitchingConnection(true);
await ConnectionController.switchConnection({
connection: _connection,
address,
namespace: chainNamespace,
onChange({ address: newAddress, namespace: newNamespace, hasSwitchedAccount, hasSwitchedWallet }) {
onSuccess?.({
address: newAddress,
namespace: newNamespace,
hasSwitchedAccount,
hasSwitchedWallet,
hasDeletedWallet: false
});
}
});
}
catch (err) {
const error = err instanceof Error ? err : new Error('Something went wrong');
onError?.(error);
}
finally {
ConnectionController.setIsSwitchingConnection(false);
}
}, [chainNamespace, onSuccess, onError]);
const deleteConnection = useCallback(({ address, connectorId }) => {
StorageUtil.deleteAddressFromConnection({ connectorId, address, namespace: chainNamespace });
ConnectionController.syncStorageConnections();
onSuccess?.({
address,
namespace: chainNamespace,
hasSwitchedAccount: false,
hasSwitchedWallet: false,
hasDeletedWallet: true
});
}, [chainNamespace]);
if (!isMultiWalletEnabled) {
AlertController.open(ConstantsUtil.REMOTE_FEATURES_ALERTS.MULTI_WALLET_NOT_ENABLED.CONNECTION_HOOK, 'info');
return {
connection: undefined,
isPending: false,
switchConnection: () => Promise.resolve(undefined),
deleteConnection: () => ({})
};
}
const connectorId = activeConnectorIds[chainNamespace];
const connList = connections.get(chainNamespace);
const connection = connList?.find(c => c.connectorId.toLowerCase() === connectorId?.toLowerCase());
return {
connection,
isPending: isSwitchingConnection,
switchConnection,
deleteConnection
};
}
/**
* Headless hook for wallet connection.
* Provides all the data and functions needed to build a custom connect UI.
*/
export function useAppKitWallets() {
const { features, remoteFeatures } = useSnapshot(OptionsController.state);
const isHeadlessEnabled = Boolean(features?.headless && remoteFeatures?.headless);
const [isFetchingWallets, setIsFetchingWallets] = useState(false);
const [currentWcPayUrl, setCurrentWcPayUrl] = useState(undefined);
const { wcUri, wcFetchingUri, wcError } = useSnapshot(ConnectionController.state);
const { wallets: wcAllWallets, search: wcSearchWallets, page, count } = useSnapshot(ApiController.state);
const { initialized, connectingWallet } = useSnapshot(PublicStateController.state);
const { clientId: wcClientId } = useSnapshot(BlockchainApiController.state);
// Alert if headless is not enabled
useEffect(() => {
if (initialized &&
remoteFeatures?.headless !== undefined &&
(!isHeadlessEnabled || !remoteFeatures?.headless)) {
AlertController.open(ConstantsUtil.REMOTE_FEATURES_ALERTS.HEADLESS_NOT_ENABLED.DEFAULT, 'info');
}
}, [initialized, isHeadlessEnabled, remoteFeatures?.headless]);
/**
* Pre-fetches the WalletConnect URI. Call this when user selects a wallet on mobile.
* Uses 'auto' cache to reuse existing valid URI or fetch new one if expired.
*/
async function getWcUri(options) {
resetWcUri();
setCurrentWcPayUrl(options?.wcPayUrl);
await ConnectionController.connectWalletConnect({ cache: 'auto' });
}
async function fetchWallets(fetchOptions) {
setIsFetchingWallets(true);
try {
const { query, ...options } = fetchOptions ?? {};
const search = options.search ?? query;
if (search) {
await ApiController.searchWallet({ ...options, search });
}
else {
ApiController.state.search = [];
await ApiController.fetchWalletsByPage({ page: 1, ...options });
}
}
catch (error) {
// eslint-disable-next-line no-console
console.error('Failed to fetch WalletConnect wallets:', error);
}
finally {
setIsFetchingWallets(false);
}
}
/**
* Connects to the selected wallet.
*
* Handles injected wallets, API wallets (from "All Wallets" list), and mobile deeplinks.
* For API wallets without pre-populated connectors, performs a fallback lookup using
* the wallet's ID via `explorerId` matching.
*
* Note: Coinbase from "All Wallets" has empty connectors array. The fallback finds the
* Base Account connector (which has explorerId set to Coinbase's API ID) to open the
* Coinbase web wallet instead of falling through to WalletConnect.
*
* @param _wallet - The wallet item to connect to
* @param namespace - Optional chain namespace (falls back to active chain)
* @param options - Optional connection options (e.g., wcPayUrl)
*/
async function connect(_wallet, namespace, options) {
setCurrentWcPayUrl(options?.wcPayUrl);
PublicStateController.set({ connectingWallet: _wallet });
const isMobileDevice = CoreHelperUtil.isMobile();
// Fall back to active chain if namespace is not provided (matches headful behavior)
const activeNamespace = namespace || ChainController.state.activeChain;
try {
const walletConnector = _wallet?.connectors.find(c => c.chain === activeNamespace);
const connector = walletConnector && activeNamespace
? ConnectorController.getConnector({
id: walletConnector?.id,
namespace: activeNamespace
})
: undefined;
/*
* Fallback connector lookup for API wallets (e.g., from "All Wallets" list).
*
* API wallets have an empty `connectors` array, so we try to find a connector
* using the wallet's API ID directly. This is crucial for Coinbase/Base wallet:
* - The Base Account connector has `explorerId` set to Coinbase's API wallet ID
* - `ConnectorController.getConnector` checks both `c.id === id` and `c.explorerId === id`
* - This allows us to find and use the Base Account connector to open the web wallet
*
* This matches the headful AppKit behavior in `ConnectorController.selectWalletConnector`.
*/
const fallbackConnector = !connector && activeNamespace
? ConnectorController.getConnector({ id: _wallet?.id, namespace: activeNamespace })
: undefined;
if (_wallet?.isInjected && connector) {
await ConnectorControllerUtil.connectExternal(connector);
}
else if (fallbackConnector) {
// Use connector found by wallet ID (e.g., Base Account connector for Coinbase web wallet)
await ConnectorControllerUtil.connectExternal(fallbackConnector);
}
else if (isMobileDevice) {
const wcWallet = ConnectUtil.mapWalletItemToWcWallet(_wallet);
if (wcWallet.mobile_link) {
ConnectionControllerUtil.onConnectMobile(wcWallet, options?.wcPayUrl);
}
else {
MobileWalletUtil.handleMobileDeeplinkRedirect(_wallet.id, activeNamespace, {
isCoinbaseDisabled: OptionsController.state.enableCoinbase === false
});
}
}
else {
await ConnectionController.connectWalletConnect({ cache: 'never' });
}
}
catch (error) {
PublicStateController.set({ connectingWallet: undefined });
throw error;
}
}
function resetWcUri() {
ConnectionController.resetUri();
ConnectionController.setWcLinking(undefined);
setCurrentWcPayUrl(undefined);
}
function resetConnectingWallet() {
PublicStateController.set({ connectingWallet: undefined });
}
// Enhance wcUri with pay param if wcPayUrl was provided
const enhancedWcUri = currentWcPayUrl && wcUri ? CoreHelperUtil.appendPayToUri(wcUri, currentWcPayUrl) : wcUri;
if (!isHeadlessEnabled || !remoteFeatures?.headless) {
return {
wallets: [],
wcWallets: [],
isFetchingWallets: false,
isFetchingWcUri: false,
isInitialized: false,
wcUri: undefined,
wcError: false,
connectingWallet: undefined,
page: 0,
count: 0,
connect: () => Promise.resolve(),
fetchWallets: () => Promise.resolve(),
resetWcUri,
resetConnectingWallet,
getWcUri: () => Promise.resolve(),
wcClientId: null
};
}
return {
wallets: ConnectUtil.getInitialWallets(),
wcWallets: ConnectUtil.getWalletConnectWallets(wcAllWallets, wcSearchWallets),
isFetchingWallets,
isFetchingWcUri: wcFetchingUri,
isInitialized: initialized,
wcUri: enhancedWcUri,
wcError: wcError ?? false,
connectingWallet: connectingWallet,
page,
count,
connect,
fetchWallets,
resetWcUri,
resetConnectingWallet,
getWcUri,
wcClientId
};
}
//# sourceMappingURL=react.js.map