UNPKG

@reown/appkit-controllers

Version:

The full stack toolkit to build onchain app UX.

369 lines • 16.4 kB
import { proxy, ref, snapshot, subscribe as sub } from 'valtio/vanilla'; import { subscribeKey as subKey } from 'valtio/vanilla/utils'; import { AVAILABLE_NAMESPACES, ConstantsUtil, getW3mThemeVariables } from '@reown/appkit-common'; import { W3mFrameRpcConstants } from '@reown/appkit-wallet/utils'; import { getPreferredAccountType } from '../utils/ChainControllerUtil.js'; import { ConnectorUtil } from '../utils/ConnectorUtil.js'; import { MobileWalletUtil } from '../utils/MobileWallet.js'; import { StorageUtil } from '../utils/StorageUtil.js'; import { withErrorBoundary } from '../utils/withErrorBoundary.js'; import { ApiController } from './ApiController.js'; import { ChainController } from './ChainController.js'; import { ModalController } from './ModalController.js'; import { OptionsController } from './OptionsController.js'; import { RouterController } from './RouterController.js'; import { ThemeController } from './ThemeController.js'; const defaultActiveConnectors = Object.fromEntries(AVAILABLE_NAMESPACES.map(namespace => [namespace, undefined])); const defaultFilterByNamespaceMap = Object.fromEntries(AVAILABLE_NAMESPACES.map(namespace => [namespace, true])); // -- State --------------------------------------------- // const state = proxy({ allConnectors: [], connectors: [], activeConnector: undefined, filterByNamespace: undefined, activeConnectorIds: defaultActiveConnectors, filterByNamespaceMap: defaultFilterByNamespaceMap }); // -- Controller ---------------------------------------- // const controller = { state, subscribe(callback) { return sub(state, () => { callback(state); }); }, subscribeKey(key, callback) { return subKey(state, key, callback); }, initialize(namespaces) { namespaces.forEach(namespace => { const connectorId = StorageUtil.getConnectedConnectorId(namespace); if (connectorId) { ConnectorController.setConnectorId(connectorId, namespace); } }); }, setActiveConnector(connector) { if (connector) { state.activeConnector = ref(connector); } }, setConnectors(connectors) { const newConnectors = connectors.filter(newConnector => !state.allConnectors.some(existingConnector => existingConnector.id === newConnector.id && ConnectorController.getConnectorName(existingConnector.name) === ConnectorController.getConnectorName(newConnector.name) && existingConnector.chain === newConnector.chain)); /** * We are reassigning the state of the proxy to a new array of new objects, ConnectorController can cause issues. So it is better to use ref in ConnectorController case. * Check more about proxy on https://valtio.dev/docs/api/basic/proxy#Gotchas * Check more about ref on https://valtio.dev/docs/api/basic/ref */ newConnectors.forEach(connector => { if (connector.type !== 'MULTI_CHAIN') { state.allConnectors.push(ref(connector)); } }); const enabledNamespaces = ConnectorController.getEnabledNamespaces(); const connectorsFilteredByNamespaces = ConnectorController.getEnabledConnectors(enabledNamespaces); state.connectors = ConnectorController.mergeMultiChainConnectors(connectorsFilteredByNamespaces); }, filterByNamespaces(enabledNamespaces) { Object.keys(state.filterByNamespaceMap).forEach(namespace => { state.filterByNamespaceMap[namespace] = false; }); enabledNamespaces.forEach(namespace => { state.filterByNamespaceMap[namespace] = true; }); ConnectorController.updateConnectorsForEnabledNamespaces(); }, filterByNamespace(namespace, enabled) { state.filterByNamespaceMap[namespace] = enabled; ConnectorController.updateConnectorsForEnabledNamespaces(); }, updateConnectorsForEnabledNamespaces() { const enabledNamespaces = ConnectorController.getEnabledNamespaces(); const enabledConnectors = ConnectorController.getEnabledConnectors(enabledNamespaces); const areAllNamespacesEnabled = ConnectorController.areAllNamespacesEnabled(); state.connectors = ConnectorController.mergeMultiChainConnectors(enabledConnectors); if (areAllNamespacesEnabled) { ApiController.clearFilterByNamespaces(); } else { ApiController.filterByNamespaces(enabledNamespaces); } }, getEnabledNamespaces() { return Object.entries(state.filterByNamespaceMap) .filter(([_, enabled]) => enabled) .map(([namespace]) => namespace); }, getEnabledConnectors(enabledNamespaces) { return state.allConnectors.filter(connector => enabledNamespaces.includes(connector.chain)); }, areAllNamespacesEnabled() { return Object.values(state.filterByNamespaceMap).every(enabled => enabled); }, mergeMultiChainConnectors(connectors) { const connectorsByNameMap = ConnectorController.generateConnectorMapByName(connectors); const mergedConnectors = []; connectorsByNameMap.forEach(keyConnectors => { const firstItem = keyConnectors[0]; const isAuthConnector = firstItem?.id === ConstantsUtil.CONNECTOR_ID.AUTH; if (keyConnectors.length > 1 && firstItem) { mergedConnectors.push({ name: firstItem.name, imageUrl: firstItem.imageUrl, imageId: firstItem.imageId, connectors: [...keyConnectors], type: isAuthConnector ? 'AUTH' : 'MULTI_CHAIN', // These values are just placeholders, we don't use them in multi-chain connector select screen chain: 'eip155', id: firstItem?.id || '' }); } else if (firstItem) { mergedConnectors.push(firstItem); } }); return mergedConnectors; }, generateConnectorMapByName(connectors) { const connectorsByNameMap = new Map(); connectors.forEach(connector => { const { name } = connector; const connectorName = ConnectorController.getConnectorName(name); if (!connectorName) { return; } const connectorsByName = connectorsByNameMap.get(connectorName) || []; const haveSameConnector = connectorsByName.find(c => c.chain === connector.chain); if (!haveSameConnector) { connectorsByName.push(connector); } connectorsByNameMap.set(connectorName, connectorsByName); }); return connectorsByNameMap; }, getConnectorName(name) { if (!name) { return name; } const nameOverrideMap = { 'Trust Wallet': 'Trust' }; return nameOverrideMap[name] || name; }, getUniqueConnectorsByName(connectors) { const uniqueConnectors = []; connectors.forEach(c => { if (!uniqueConnectors.find(uc => uc.chain === c.chain)) { uniqueConnectors.push(c); } }); return uniqueConnectors; }, addConnector(connector) { if (connector.id === ConstantsUtil.CONNECTOR_ID.AUTH) { const authConnector = connector; const optionsState = snapshot(OptionsController.state); const themeMode = ThemeController.getSnapshot().themeMode; const themeVariables = ThemeController.getSnapshot().themeVariables; authConnector?.provider?.syncDappData?.({ metadata: optionsState.metadata, sdkVersion: optionsState.sdkVersion, projectId: optionsState.projectId, sdkType: optionsState.sdkType }); authConnector?.provider?.syncTheme({ themeMode, themeVariables, w3mThemeVariables: getW3mThemeVariables(themeVariables, themeMode) }); ConnectorController.setConnectors([connector]); } else { ConnectorController.setConnectors([connector]); } }, getAuthConnector(chainNamespace) { const activeNamespace = chainNamespace || ChainController.state.activeChain; const authConnector = state.connectors.find(c => c.id === ConstantsUtil.CONNECTOR_ID.AUTH); if (!authConnector) { return undefined; } if (authConnector?.connectors?.length) { const connector = authConnector.connectors.find(c => c.chain === activeNamespace); return connector; } return authConnector; }, getAnnouncedConnectorRdns() { return state.connectors.filter(c => c.type === 'ANNOUNCED').map(c => c.info?.rdns); }, getConnectorById(id) { const sortedConnectors = ConnectorUtil.sortConnectorsByPriority(state.allConnectors); return sortedConnectors.find(c => c.id === id); }, getConnector({ id, namespace }) { const namespaceToUse = namespace || ChainController.state.activeChain; const connectorsByNamespace = state.allConnectors.filter(c => c.chain === namespaceToUse); const sortedConnectorsByNamespace = ConnectorUtil.sortConnectorsByPriority(connectorsByNamespace); const connector = sortedConnectorsByNamespace.find(c => c.id === id || c.explorerId === id); return connector; }, syncIfAuthConnector(connector) { if (connector.id !== 'AUTH') { return; } const authConnector = connector; const optionsState = snapshot(OptionsController.state); const themeMode = ThemeController.getSnapshot().themeMode; const themeVariables = ThemeController.getSnapshot().themeVariables; authConnector?.provider?.syncDappData?.({ metadata: optionsState.metadata, sdkVersion: optionsState.sdkVersion, sdkType: optionsState.sdkType, projectId: optionsState.projectId }); authConnector.provider.syncTheme({ themeMode, themeVariables, w3mThemeVariables: getW3mThemeVariables(themeVariables, themeMode) }); }, /** * Returns the connectors filtered by namespace. * @param namespace - The namespace to filter the connectors by. * @returns ConnectorWithProviders[]. */ getConnectorsByNamespace(namespace) { const namespaceConnectors = state.allConnectors.filter(connector => connector.chain === namespace); return ConnectorController.mergeMultiChainConnectors(namespaceConnectors); }, canSwitchToSmartAccount(namespace) { const isSmartAccountEnabled = ChainController.checkIfSmartAccountEnabled(); return (isSmartAccountEnabled && getPreferredAccountType(namespace) === W3mFrameRpcConstants.ACCOUNT_TYPES.EOA); }, selectWalletConnector(wallet) { const redirectView = RouterController.state.data?.redirectView; const namespace = ChainController.state.activeChain; const connector = namespace ? ConnectorController.getConnector({ id: wallet.id, namespace }) : undefined; MobileWalletUtil.handleMobileDeeplinkRedirect(connector?.explorerId || wallet.id, ChainController.state.activeChain, { isCoinbaseDisabled: OptionsController.state.enableCoinbase === false }); if (connector) { RouterController.push('ConnectingExternal', { connector, wallet, redirectView }); } else { RouterController.push('ConnectingWalletConnect', { wallet, redirectView }); } }, /** * Returns the connectors. If a namespace is provided, the connectors are filtered by namespace. * @param namespace - The namespace to filter the connectors by. If not provided, all connectors are returned. * @returns ConnectorWithProviders[]. */ getConnectors(namespace) { if (namespace) { return ConnectorController.getConnectorsByNamespace(namespace); } return ConnectorController.mergeMultiChainConnectors(state.allConnectors); }, /** * Sets the filter by namespace and updates the connectors. * @param namespace - The namespace to filter the connectors by. */ setFilterByNamespace(namespace) { state.filterByNamespace = namespace; state.connectors = ConnectorController.getConnectors(namespace); ApiController.setFilterByNamespace(namespace); }, setConnectorId(connectorId, namespace) { if (connectorId) { state.activeConnectorIds = { ...state.activeConnectorIds, [namespace]: connectorId }; StorageUtil.setConnectedConnectorId(namespace, connectorId); } }, removeConnectorId(namespace) { state.activeConnectorIds = { ...state.activeConnectorIds, [namespace]: undefined }; StorageUtil.deleteConnectedConnectorId(namespace); }, getConnectorId(namespace) { if (!namespace) { return undefined; } return state.activeConnectorIds[namespace]; }, isConnected(namespace) { if (!namespace) { return Object.values(state.activeConnectorIds).some(id => Boolean(id)); } return Boolean(state.activeConnectorIds[namespace]); }, resetConnectorIds() { state.activeConnectorIds = { ...defaultActiveConnectors }; }, extendConnectorsWithExplorerWallets(explorerWallets) { state.allConnectors.forEach(connector => { const explorerWallet = explorerWallets.find(wallet => wallet.id === connector.id || (wallet.rdns && wallet.rdns === connector.info?.rdns)); if (explorerWallet) { connector.explorerWallet = explorerWallet; } }); const enabledNamespaces = ConnectorController.getEnabledNamespaces(); const enabledConnectors = ConnectorController.getEnabledConnectors(enabledNamespaces); state.connectors = ConnectorController.mergeMultiChainConnectors(enabledConnectors); }, /** * Opens the connect modal and waits until the user connects their wallet. * @param params - Connection parameters. * @returns Promise resolving to the connected wallet's CAIP address. */ async connect(params = {}) { const { namespace } = params; ConnectorController.setFilterByNamespace(namespace); RouterController.push('Connect', { addWalletForNamespace: namespace }); return new Promise((resolve, reject) => { if (namespace) { const unsubscribeChainController = ChainController.subscribeChainProp('accountState', val => { if (val?.caipAddress) { resolve({ caipAddress: val?.caipAddress }); unsubscribeChainController(); } }, namespace); const unsubscribeModalController = ModalController.subscribeKey('open', val => { if (!val) { reject(new Error('Modal closed')); unsubscribeModalController(); } }); } else { const unsubscribeChainController = ChainController.subscribeKey('activeCaipAddress', val => { if (val) { resolve({ caipAddress: val }); unsubscribeChainController(); } }); const unsubscribeModalController = ModalController.subscribeKey('open', val => { if (!val) { reject(new Error('Modal closed')); unsubscribeModalController(); } }); } }); } }; // Export the controller wrapped with our error boundary export const ConnectorController = withErrorBoundary(controller); //# sourceMappingURL=ConnectorController.js.map