From d7eba5601bda5106b639617dbaa78357612fab5a Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Tue, 18 Aug 2026 16:28:19 -0700 Subject: [PATCH] perf(web): load the wallet and payment SDKs on demand ReownAppKitModal runs `new WagmiAdapter(...)`, `new SolanaAdapter()` and `createAppKit(...)` at module scope, so a single static import anywhere in the eager graph pinned @reown/*, @walletconnect/*, wagmi and @solana/web3.js into the entry chunk -- for every visitor, including everyone who never opens a wallet. @coinflowlabs/react (which bundles the nsure-ai fraud SDK) was there for the same reason. Together these were ~1.9 MB of the entry chunk. The surprising edge was auth: services/audius-sdk/auth.ts dereferenced `wagmiAdapter.wagmiConfig` at *module scope*, so every email/password user loaded the entire wallet stack to discover they did not need it. It now returns to Hedgehog before loading AppKit unless localStorage shows a persisted wallet connection. That probe deliberately errs toward loading: a false negative would silently downgrade an external-wallet user to Hedgehog, while a false positive only costs a chunk fetch. WagmiProvider stays mounted with a lightweight bootstrap config and swaps in the adapter's config once AppKit loads. Making the provider itself conditional would remount the entire app the moment a wallet appeared; swapping a context value does not. The bootstrap config sets `storage: null` so it cannot clobber the real config's persisted `wagmi.store` entry. The modals are registered lazily. This is safe because nice-modal-react's NiceModalPlaceholder filters the registry by *currently visible* ids, so a registered-but-never-shown modal never mounts and its hooks never run -- the cost was always the static import graph, not runtime. Registration moved out of each modal module on purpose: if they still self-registered, the dynamic import would overwrite MODAL_REGISTRY mid-flight and React would swap the element type under an open modal, remounting it and losing its state. The Suspense boundary is local because NiceModal.Provider mounts its placeholder outside the only boundary in routes.tsx. CoinflowPurchaseProtection in routes.tsx is now lazy too. It still renders unconditionally, so the chunk is still fetched at startup -- it is off the entry chunk's parse path rather than deferred outright. Rendering it only on purchase-capable routes would defer it properly, but that is a call for whoever owns payments, since early initialization may be deliberate. NEEDS QA: external wallet connect / disconnect / sign-in, which could not be exercised without a real wallet. The Hedgehog (email/password) path is verified. Co-Authored-By: Claude Opus 5 --- packages/web/src/app/AppProviders.tsx | 57 +++++++++++- packages/web/src/app/appkit.ts | 91 +++++++++++++++++++ packages/web/src/app/audiusChain.ts | 27 ++++++ packages/web/src/app/registerNiceModals.ts | 76 +++++++++++++++- packages/web/src/app/routes.tsx | 12 ++- .../buy-sell-modal/BuySellModal.tsx | 4 - .../CoinflowOnrampModal.tsx | 4 - .../send-tokens-modal/SendTokensModal.tsx | 4 - .../components/CoinflowWithdrawModal.tsx | 4 - .../modals/ConnectedWalletsModal.tsx | 4 - packages/web/src/services/audius-sdk/auth.ts | 15 ++- packages/web/src/store/sign-out/sagas.ts | 9 +- 12 files changed, 269 insertions(+), 38 deletions(-) create mode 100644 packages/web/src/app/appkit.ts create mode 100644 packages/web/src/app/audiusChain.ts diff --git a/packages/web/src/app/AppProviders.tsx b/packages/web/src/app/AppProviders.tsx index 10cd522e412..c90bb0492c5 100644 --- a/packages/web/src/app/AppProviders.tsx +++ b/packages/web/src/app/AppProviders.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useState, useMemo } from 'react' +import { ReactNode, useState, useMemo, useEffect } from 'react' import { FrostedSurfaceIntensity, ThemePalette } from '@audius/common/models' import { setNiceModalAdapter } from '@audius/common/services' @@ -13,7 +13,7 @@ import { RouterProvider } from 'react-router' import { PersistGate } from 'redux-persist/integration/react' -import { WagmiProvider } from 'wagmi' +import { createConfig, http, WagmiProvider } from 'wagmi' import { REACT_QUERY_DEVTOOLS_KEY, useDevToggle } from 'hooks/useDevToggle' import { useIsMobile } from 'hooks/useIsMobile' @@ -28,7 +28,12 @@ import { getThemePaletteFromStorage } from 'utils/theme/theme' -import { wagmiAdapter } from './ReownAppKitModal' +import { + hasPersistedWalletConnection, + loadAppKit, + useLoadedAppKit +} from './appkit' +import { audiusChain } from './audiusChain' import './registerNiceModals' import { createRoutes } from './routes' @@ -36,6 +41,48 @@ import { createRoutes } from './routes' // nice-modal-react without depending on the package directly. setNiceModalAdapter({ show: NiceModal.show, hide: NiceModal.hide }) +/** + * needs a Config synchronously on first render, but the real one + * is built by Reown's WagmiAdapter — which we no longer load at startup, since + * importing it drags the whole AppKit graph into the entry chunk. This minimal + * stand-in fills the gap until AppKit actually loads. + * + * `storage: null` matters: the real config persists to `wagmi.store`, and a + * second config writing that key would clobber it along with the + * hasPersistedWalletConnection() probe that reads it. + */ +const bootstrapWagmiConfig = createConfig({ + chains: [audiusChain], + transports: { [audiusChain.id]: http() }, + storage: null +}) + +/** + * Mounts WagmiProvider unconditionally and swaps in the adapter's config once + * AppKit loads. Swapping the `config` prop re-renders context consumers but does + * not unmount the subtree — making WagmiProvider itself conditional would remount + * the entire app the moment a wallet appeared. + */ +const WagmiGate = ({ children }: { children: ReactNode }) => { + const appkit = useLoadedAppKit() + + useEffect(() => { + // Restore a previously connected external wallet. Users who never connected + // one never pay for the chunk. + if (!appkit && hasPersistedWalletConnection()) { + loadAppKit() + } + }, [appkit]) + + return ( + + {children} + + ) +} + type AppProvidersProps = { children?: ReactNode } @@ -88,7 +135,7 @@ export const AppProviders = ({ children }: AppProvidersProps) => { }, [basename]) return ( - + @@ -102,6 +149,6 @@ export const AppProviders = ({ children }: AppProvidersProps) => { {reactQueryDevtoolsEnabled ? : null} - + ) } diff --git a/packages/web/src/app/appkit.ts b/packages/web/src/app/appkit.ts new file mode 100644 index 00000000000..84581aa49b6 --- /dev/null +++ b/packages/web/src/app/appkit.ts @@ -0,0 +1,91 @@ +import { useSyncExternalStore } from 'react' + +/** + * Lazy access to the Reown AppKit singletons. + * + * `ReownAppKitModal` runs `new WagmiAdapter(...)`, `new SolanaAdapter()` and + * `createAppKit(...)` at module scope, so a single static import anywhere in the + * eager graph pins `@reown/appkit`, both adapters, `@walletconnect/*` and + * `@solana/web3.js` into the entry chunk — for every visitor, including the + * majority who never touch a wallet. + * + * Everything reaching AppKit from eagerly-loaded code should go through here + * instead of importing `ReownAppKitModal` directly. Code that is already behind + * a `React.lazy` boundary (wallet pages, wallet modals) can keep importing it + * directly — those chunks are only fetched when the user gets there. + */ +type AppKitModule = typeof import('./ReownAppKitModal') + +let loaded: AppKitModule | undefined +let pending: Promise | undefined +const listeners = new Set<() => void>() + +/** Loads AppKit on demand. Memoized — concurrent callers share one import. */ +export const loadAppKit = (): Promise => { + if (loaded) return Promise.resolve(loaded) + if (!pending) { + pending = import('./ReownAppKitModal').then((mod) => { + loaded = mod + listeners.forEach((notify) => notify()) + return mod + }) + } + return pending +} + +/** + * The AppKit module if it has already loaded, else `undefined`. Never triggers + * a load — for callers that only need to act on an *existing* connection (e.g. + * disconnect on sign-out: if AppKit never loaded there is nothing to disconnect). + */ +export const getLoadedAppKit = (): AppKitModule | undefined => loaded + +const subscribe = (listener: () => void) => { + listeners.add(listener) + return () => { + listeners.delete(listener) + } +} + +const getServerSnapshot = (): AppKitModule | undefined => undefined + +/** React binding for {@link getLoadedAppKit}; re-renders once AppKit loads. */ +export const useLoadedAppKit = (): AppKitModule | undefined => + useSyncExternalStore(subscribe, getLoadedAppKit, getServerSnapshot) + +/** + * wagmi's default storage key. `WagmiAdapter` passes no `storage` override, so + * `@wagmi/core`'s `createStorage` falls back to the `wagmi` prefix. + */ +const WAGMI_STORAGE_KEY = 'wagmi.store' + +/** + * Whether this browser has a persisted external-wallet connection, i.e. whether + * AppKit needs to load at startup to restore it. + * + * Deliberately biased toward `true`. A false negative silently downgrades an + * external-wallet user to Hedgehog, which is a correctness bug; a false positive + * only costs an unnecessary chunk load. When the key is present but unreadable + * we load AppKit and let wagmi decide. + */ +export const hasPersistedWalletConnection = (): boolean => { + let raw: string | null = null + try { + raw = window.localStorage.getItem(WAGMI_STORAGE_KEY) + } catch { + // localStorage unavailable (SSR, private mode). Nothing could have been + // persisted, so there is no connection to restore. + return false + } + if (!raw) return false + try { + const { state } = JSON.parse(raw) ?? {} + if (!state) return true + // wagmi serializes `connections` as { __type: 'Map', value: [...] }. + const connections = state.connections?.value ?? state.connections + const count = Array.isArray(connections) ? connections.length : 0 + return Boolean(state.current) || count > 0 + } catch { + return true + } +} diff --git a/packages/web/src/app/audiusChain.ts b/packages/web/src/app/audiusChain.ts new file mode 100644 index 00000000000..c9d511dca5a --- /dev/null +++ b/packages/web/src/app/audiusChain.ts @@ -0,0 +1,27 @@ +import { type Chain } from 'viem' + +import { env } from 'services/env' + +/** + * Audius ACDC chain (now ports to Core). + * + * Deliberately lives outside `ReownAppKitModal`. That module constructs + * `WagmiAdapter`, `SolanaAdapter` and `createAppKit` as import-time side + * effects, so importing *any* symbol from it — even a plain config object like + * this one — pulls the whole AppKit graph (`@reown/*`, `@walletconnect/*`) + * into the importing chunk. + * + * Most consumers only need `audiusChain.id`. Keeping the definition free of + * Reown imports lets them stay out of that graph entirely. + * + * Typed against viem's `Chain` rather than `@reown/appkit/networks` so there is + * no `@reown` coupling here at all, even at the type level. + */ +export const audiusChain = { + id: env.AUDIUS_NETWORK_CHAIN_ID, + name: 'Audius', + nativeCurrency: { name: '-', symbol: '-', decimals: 18 }, + rpcUrls: { + default: { http: [`${env.API_URL}/core/erpc`] } + } +} as const satisfies Chain diff --git a/packages/web/src/app/registerNiceModals.ts b/packages/web/src/app/registerNiceModals.ts index 91ec7c5a8a8..fab98614aea 100644 --- a/packages/web/src/app/registerNiceModals.ts +++ b/packages/web/src/app/registerNiceModals.ts @@ -8,12 +8,15 @@ * * Add new NiceModal-managed modals here as they migrate. */ +import { createElement, lazy, Suspense, type ComponentType } from 'react' + +import { registerNiceModalId } from '@audius/common/services' +import NiceModal from '@ebay/nice-modal-react' + import 'components/add-cash-modal/AddCashModal' import 'components/add-to-collection/desktop/AddToCollectionModal' import 'components/album-track-remove-confirmation-modal/AlbumTrackRemoveConfirmationModal' import 'components/artist-pick-modal/ArtistPickModal' -import 'components/buy-sell-modal/BuySellModal' -import 'components/coinflow-onramp-modal/CoinflowOnrampModal' import 'components/delete-playlist-confirmation-modal/DeletePlaylistConfirmationModal' import 'components/delete-track-confirmation-modal/DeleteTrackConfirmationModal' import 'components/download-track-archive-modal/DownloadTrackArchiveModal' @@ -35,7 +38,6 @@ import 'components/replace-track-confirmation-modal/ReplaceTrackConfirmationModa import 'components/replace-track-progress-modal/ReplaceTrackProgressModal' import 'components/rewards/modals/ClaimAllRewardsModal' import 'components/rewards/modals/TopAPI' -import 'components/send-tokens-modal/SendTokensModal' import 'components/share-modal/ShareModal' import 'components/transaction-details-modal/TransactionDetailsModal' import 'components/upload-confirmation-modal/UploadConfirmationModal' @@ -45,10 +47,74 @@ import 'components/user-badges/TierExplainerModal' import 'components/wait-for-download-modal/WaitForDownloadModal' import 'components/welcome-modal/WelcomeModal' import 'components/withdraw-usdc-modal/WithdrawUSDCModal' -import 'components/withdraw-usdc-modal/components/CoinflowWithdrawModal' import 'pages/audio-page/components/modals/AudioBreakdownModal' -import 'pages/audio-page/components/modals/ConnectedWalletsModal' import 'pages/audio-page/components/modals/TransferAudioMobileDrawer' import 'pages/chat-page/components/ChatBlastModal' import 'pages/fan-club-detail-page/components/ClaimVestedCoinsModal' import 'pages/rewards-page/components/modals/ChallengeRewardsModal/ChallengeRewardsModal' + +/** + * Wallet modals, registered lazily. + * + * These three pull in the Reown AppKit graph (`@reown/*`, `@walletconnect/*`, + * `@solana/web3.js`). Importing them here for their registration side effect put + * roughly 1.5 MB of wallet SDK in the entry chunk for every visitor, including + * everyone who never opens a wallet. + * + * Registering a lazy component instead is safe because NiceModal only renders + * modals that are currently *visible* (`NiceModalPlaceholder` filters the + * registry by the visible ids), so nothing here mounts — or suspends — until the + * user actually opens one. + * + * Registration lives here rather than in each modal module on purpose: if those + * modules still self-registered, the dynamic import would overwrite + * MODAL_REGISTRY mid-flight and React would swap the element type underneath an + * open modal, remounting it and losing its state. + * + * The Suspense boundary is local because `NiceModal.Provider` mounts its + * placeholder outside the only boundary in routes.tsx. + */ +const registerLazyModal = ( + id: string, + loader: () => Promise<{ default: ComponentType }> +) => { + const LazyModal = lazy(loader) + const LazyModalBoundary = (props: Record) => + createElement(Suspense, { fallback: null }, createElement(LazyModal, props)) + LazyModalBoundary.displayName = `LazyModal(${id})` + NiceModal.register(id, LazyModalBoundary) + registerNiceModalId(id) +} + +registerLazyModal('BuySellModal', () => + import('components/buy-sell-modal/BuySellModal').then((m) => ({ + default: m.BuySellModal + })) +) +registerLazyModal( + 'SendTokensModal', + () => import('components/send-tokens-modal/SendTokensModal') +) +registerLazyModal('ConnectedWallets', () => + import('pages/audio-page/components/modals/ConnectedWalletsModal').then( + (m) => ({ default: m.ConnectedWalletsModal }) + ) +) + +/** + * Coinflow modals, registered lazily. + * + * `@coinflowlabs/react` bundles the nsure-ai fraud-detection SDK (~294 KB of + * source on its own). Neither is needed until a user actually reaches a + * purchase or withdrawal flow. + */ +registerLazyModal('CoinflowOnramp', () => + import('components/coinflow-onramp-modal/CoinflowOnrampModal').then((m) => ({ + default: m.CoinflowOnrampModal + })) +) +registerLazyModal('CoinflowWithdraw', () => + import( + 'components/withdraw-usdc-modal/components/CoinflowWithdrawModal' + ).then((m) => ({ default: m.CoinflowWithdrawModal })) +) diff --git a/packages/web/src/app/routes.tsx b/packages/web/src/app/routes.tsx index 9e643b9f085..d0ac9e5ef10 100644 --- a/packages/web/src/app/routes.tsx +++ b/packages/web/src/app/routes.tsx @@ -2,7 +2,6 @@ import React, { lazy, Suspense, useEffect } from 'react' import { SyncLocalStorageUserProvider } from '@audius/common/api' import { route } from '@audius/common/utils' -import { CoinflowPurchaseProtection } from '@coinflowlabs/react' import NiceModal from '@ebay/nice-modal-react' import type { RouteObject } from 'react-router' import { Navigate, Outlet, useNavigate } from 'react-router' @@ -24,6 +23,17 @@ import { AudiusQueryProvider } from './AudiusQueryProvider' import { ThemeProvider } from './ThemeProvider' import WebPlayer from './web-player/WebPlayer' +/** + * `@coinflowlabs/react` pulls in the nsure-ai fraud-detection SDK, which was + * landing in the entry chunk for every visitor. Nothing here is needed before + * first paint, and this already renders inside a boundary below. + */ +const CoinflowPurchaseProtection = lazy(() => + import('@coinflowlabs/react').then((m) => ({ + default: m.CoinflowPurchaseProtection + })) +) + const { PRIVATE_KEY_EXPORTER_SETTINGS_PAGE, SIGN_IN_PAGE, diff --git a/packages/web/src/components/buy-sell-modal/BuySellModal.tsx b/packages/web/src/components/buy-sell-modal/BuySellModal.tsx index 0bc4535576e..5ff9d328256 100644 --- a/packages/web/src/components/buy-sell-modal/BuySellModal.tsx +++ b/packages/web/src/components/buy-sell-modal/BuySellModal.tsx @@ -1,7 +1,6 @@ import { useCallback, useMemo, useState, useEffect } from 'react' import { buySellMessages } from '@audius/common/messages' -import { registerNiceModalId } from '@audius/common/services' import { useBuySellModal, useAddCashModal } from '@audius/common/store' import { IconJupiterLogo, @@ -117,6 +116,3 @@ export const BuySellModal = NiceModal.create(() => { ) }) - -NiceModal.register('BuySellModal', BuySellModal) -registerNiceModalId('BuySellModal') diff --git a/packages/web/src/components/coinflow-onramp-modal/CoinflowOnrampModal.tsx b/packages/web/src/components/coinflow-onramp-modal/CoinflowOnrampModal.tsx index b0f61566dff..4509251c22f 100644 --- a/packages/web/src/components/coinflow-onramp-modal/CoinflowOnrampModal.tsx +++ b/packages/web/src/components/coinflow-onramp-modal/CoinflowOnrampModal.tsx @@ -1,7 +1,6 @@ import { useCallback, useEffect, useState } from 'react' import { useCoinflowAdapter } from '@audius/common/hooks' -import { registerNiceModalId } from '@audius/common/services' import { coinflowModalUIActions, useCoinflowOnrampModal @@ -96,6 +95,3 @@ export const CoinflowOnrampModal = NiceModal.create(() => { ) }) - -NiceModal.register('CoinflowOnramp', CoinflowOnrampModal) -registerNiceModalId('CoinflowOnramp') diff --git a/packages/web/src/components/send-tokens-modal/SendTokensModal.tsx b/packages/web/src/components/send-tokens-modal/SendTokensModal.tsx index ea4e74e01c5..6a2ddd93353 100644 --- a/packages/web/src/components/send-tokens-modal/SendTokensModal.tsx +++ b/packages/web/src/components/send-tokens-modal/SendTokensModal.tsx @@ -3,7 +3,6 @@ import { useCallback, useState, useEffect, useRef } from 'react' import { useSendCoins } from '@audius/common/api' import { walletMessages } from '@audius/common/messages' import { SolanaWalletAddress, User } from '@audius/common/models' -import { registerNiceModalId } from '@audius/common/services' import { useSendTokensModal } from '@audius/common/store' import NiceModal, { useModal } from '@ebay/nice-modal-react' @@ -279,7 +278,4 @@ const SendTokensModal = NiceModal.create(() => { ) }) -NiceModal.register('SendTokensModal', SendTokensModal) -registerNiceModalId('SendTokensModal') - export default SendTokensModal diff --git a/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawModal.tsx b/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawModal.tsx index d073222d766..f79f3fb9e16 100644 --- a/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawModal.tsx +++ b/packages/web/src/components/withdraw-usdc-modal/components/CoinflowWithdrawModal.tsx @@ -1,7 +1,6 @@ import { useCallback } from 'react' import { useCoinflowWithdrawalAdapter } from '@audius/common/hooks' -import { registerNiceModalId } from '@audius/common/services' import { withdrawUSDCActions, withdrawUSDCSelectors @@ -90,6 +89,3 @@ export const CoinflowWithdrawModal = NiceModal.create(() => { ) }) - -NiceModal.register('CoinflowWithdraw', CoinflowWithdrawModal) -registerNiceModalId('CoinflowWithdraw') diff --git a/packages/web/src/pages/audio-page/components/modals/ConnectedWalletsModal.tsx b/packages/web/src/pages/audio-page/components/modals/ConnectedWalletsModal.tsx index 6ed63bcf810..c182dda943b 100644 --- a/packages/web/src/pages/audio-page/components/modals/ConnectedWalletsModal.tsx +++ b/packages/web/src/pages/audio-page/components/modals/ConnectedWalletsModal.tsx @@ -5,7 +5,6 @@ import { useRemoveAssociatedWallet } from '@audius/common/api' import { Chain } from '@audius/common/models' -import { registerNiceModalId } from '@audius/common/services' import { Button, Flex, @@ -251,6 +250,3 @@ export const ConnectedWalletsModal = NiceModal.create(() => { ) }) - -NiceModal.register('ConnectedWallets', ConnectedWalletsModal) -registerNiceModalId('ConnectedWallets') diff --git a/packages/web/src/services/audius-sdk/auth.ts b/packages/web/src/services/audius-sdk/auth.ts index 1c3c9f965e1..e0d68fbe770 100644 --- a/packages/web/src/services/audius-sdk/auth.ts +++ b/packages/web/src/services/audius-sdk/auth.ts @@ -9,13 +9,12 @@ import { import { getWalletClient } from '@wagmi/core' import { type WalletClient } from 'viem' -import { audiusChain, wagmiAdapter } from 'app/ReownAppKitModal' +import { hasPersistedWalletConnection, loadAppKit } from 'app/appkit' +import { audiusChain } from 'app/audiusChain' import { env } from '../env' import { localStorage } from '../local-storage' -const wagmiConfig = wagmiAdapter.wagmiConfig - export const getAudiusWalletClient = async (): Promise => { // Check if the user has already connected Hedgehog first... await authService.hedgehogInstance.waitUntilReady() @@ -29,9 +28,19 @@ export const getAudiusWalletClient = async (): Promise => { return createHedgehogWalletClient(authService.hedgehogInstance) } + // No external wallet has ever connected in this browser, so there is nothing + // to restore. Return before loading AppKit — this is the common path for + // every email/password user and keeps the wallet SDK out of startup. + if (!hasPersistedWalletConnection()) { + return createHedgehogWalletClient(authService.hedgehogInstance) + } + // Try the connected external wallet next... console.debug('[audiusSdk] Initializing SDK with external wallet...') + const { wagmiAdapter } = await loadAppKit() + const wagmiConfig = wagmiAdapter.wagmiConfig + // Wait for the wallet to finish connecting/reconnecting if ( wagmiConfig.state.status === 'reconnecting' || diff --git a/packages/web/src/store/sign-out/sagas.ts b/packages/web/src/store/sign-out/sagas.ts index 4cb39c0ef35..cddb02c2d1a 100644 --- a/packages/web/src/store/sign-out/sagas.ts +++ b/packages/web/src/store/sign-out/sagas.ts @@ -8,15 +8,13 @@ import { import { disconnect } from '@wagmi/core' import { takeLatest, put, call } from 'redux-saga/effects' -import { wagmiAdapter } from 'app/ReownAppKitModal' +import { getLoadedAppKit } from 'app/appkit' import { make } from 'common/store/analytics/actions' import { signOut } from 'store/sign-out/signOut' import { push } from 'utils/navigation' const { resetAccount, unsubscribeBrowserPushNotifications } = accountActions const { signOut: signOutAction } = signOutActions -const wagmiConfig = wagmiAdapter.wagmiConfig - function* watchSignOut() { const localStorage = yield* getContext('localStorage') const authService = yield* getContext('authService') @@ -24,7 +22,10 @@ function* watchSignOut() { yield takeLatest( signOutAction.type, function* (action: ReturnType) { - if (wagmiConfig.state.status === 'connected') { + // Only reachable if AppKit was loaded, which only happens once a wallet + // is actually in play — if it never loaded there is nothing to disconnect. + const wagmiConfig = getLoadedAppKit()?.wagmiAdapter.wagmiConfig + if (wagmiConfig && wagmiConfig.state.status === 'connected') { yield call(disconnect, wagmiConfig) } yield put(resetAccount())