@reown/appkit-controllers
Version:
The full stack toolkit to build onchain app UX.
164 lines • 8.14 kB
JavaScript
import { subscribeKey as subKey } from 'valtio/vanilla/utils';
import { ApiController } from '../controllers/ApiController.js';
import { ChainController } from '../controllers/ChainController.js';
import { ConnectionController } from '../controllers/ConnectionController.js';
import { ConnectorController } from '../controllers/ConnectorController.js';
import { OptionsController } from '../controllers/OptionsController.js';
import { PublicStateController } from '../controllers/PublicStateController.js';
import { ConnectUtil } from './ConnectUtil.js';
import { ConnectionControllerUtil } from './ConnectionControllerUtil.js';
import { ConnectorControllerUtil } from './ConnectorControllerUtil.js';
import { CoreHelperUtil } from './CoreHelperUtil.js';
import { MobileWalletUtil } from './MobileWallet.js';
/**
* Framework-agnostic headless wallet-list logic — the imperative core behind the
* `useAppKitWallets` React hook and the `AppKit` client's wallet methods.
*
* AppKit runs headless (no modal): the host renders its own picker and connects a
* chosen wallet programmatically. That orchestration (list / search / paginate the
* WalletGuide wallets, fetch the WalletConnect URI, and connect injected / API / mobile
* wallets) lived only inside the React hook; lifting it here lets a non-React host
* (e.g. `@walletconnect/pay-appkit`) drive the same flow through `appKit.*` methods,
* with the hook and the client sharing one tested code path.
*
* Reads/writes the global controllers directly (valtio singletons), so it takes no
* AppKit instance — exactly like {@link ConnectionControllerUtil}.
*/
export const HeadlessWalletUtil = {
/**
* Fetch / search / paginate the WalletConnect wallet list from the explorer API.
* With a `search` (or the deprecated `query`), switches to search mode; otherwise
* lists/paginates. Reads results from `ApiController.state` (see {@link getWalletList}).
*/
async fetchWallets(fetchOptions) {
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 });
}
},
/** Read the current wallet list (initial view + WalletConnect list + pagination). */
getWalletList() {
return {
wallets: ConnectUtil.getInitialWallets(),
wcWallets: ConnectUtil.getWalletConnectWallets(ApiController.state.wallets, ApiController.state.search),
page: ApiController.state.page,
count: ApiController.state.count
};
},
/** Subscribe to wallet-list changes (the WalletGuide list + search results). */
subscribeWalletList(callback) {
const unsubscribers = [
subKey(ApiController.state, 'wallets', callback),
subKey(ApiController.state, 'search', callback),
subKey(ApiController.state, 'page', callback),
subKey(ApiController.state, 'count', callback)
];
return () => unsubscribers.forEach(unsubscribe => unsubscribe());
},
/**
* Pre-fetch the WalletConnect URI. Read the result with {@link getWalletConnectUri};
* subscribe with {@link subscribeWalletConnectUri}. Call when a wallet is selected so a
* later connect can deeplink synchronously (required for iOS Safari) or render a QR. Uses
* 'auto' cache to reuse a valid URI or fetch a new one.
*/
async prefetchWalletConnectUri(_options) {
this.resetWcUri();
await ConnectionController.connectWalletConnect({ cache: 'auto' });
},
/**
* Read the current WalletConnect URI state (the QR / deeplink URI plus the fetch/error
* signals) — the symmetric read for {@link prefetchWalletConnectUri}. Reads
* `ConnectionController` directly, so a headless host gets it ungated through the AppKit
* instance without importing the controllers package.
*/
getWalletConnectUri() {
return {
wcUri: ConnectionController.state.wcUri,
wcError: Boolean(ConnectionController.state.wcError),
wcFetchingUri: ConnectionController.state.wcFetchingUri
};
},
/**
* Subscribe to WalletConnect URI state changes (wcUri / wcError / wcFetchingUri). The
* callback receives no arguments — read the current values with {@link getWalletConnectUri}.
* Returns an unsubscribe.
*/
subscribeWalletConnectUri(callback) {
const unsubscribers = [
subKey(ConnectionController.state, 'wcUri', callback),
subKey(ConnectionController.state, 'wcError', callback),
subKey(ConnectionController.state, 'wcFetchingUri', callback)
];
return () => unsubscribers.forEach(unsubscribe => unsubscribe());
},
/**
* Connect a chosen wallet programmatically (headless — no modal).
*
* Handles injected wallets, API wallets (from the "all wallets" list), and mobile
* deeplinks. For API wallets without pre-populated connectors, falls back to a lookup
* by the wallet's ID via `explorerId` matching (e.g. Coinbase/Base from "all wallets").
*/
async connect(wallet, namespace, options) {
PublicStateController.set({ connectingWallet: wallet });
const isMobileDevice = CoreHelperUtil.isMobile();
// Fall back to the active chain if no namespace is given (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 (empty `connectors`): find a connector
* by the wallet's API id (getConnector matches both `c.id` and `c.explorerId`), so
* e.g. the Base Account connector opens Coinbase's web wallet instead of falling
* through to WalletConnect. Matches headful `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) {
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;
}
},
/** Reset the WalletConnect URI + linking state (e.g. when a QR is closed). */
resetWcUri() {
ConnectionController.resetUri();
ConnectionController.setWcLinking(undefined);
},
/** Clear the `connectingWallet` state. */
resetConnectingWallet() {
PublicStateController.set({ connectingWallet: undefined });
}
};
//# sourceMappingURL=HeadlessWalletUtil.js.map