diff --git a/packages/web/src/app/AppProviders.tsx b/packages/web/src/app/AppProviders.tsx index 923bd42eccb..e175f063359 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, lazy, Suspense } from 'react' import { MediaProvider } from '@audius/harmony/src/contexts' import { QueryClientProvider } from '@tanstack/react-query' @@ -10,7 +10,6 @@ import { RouterProvider } from 'react-router' import { PersistGate } from 'redux-persist/integration/react' -import { WagmiProvider } from 'wagmi' import { useIsMobile } from 'hooks/useIsMobile' import { env } from 'services/env' @@ -18,9 +17,11 @@ import { queryClient } from 'services/query-client' import { configureStore } from 'store/configureStore' import { getSystemAppearance, getTheme } from 'utils/theme/theme' -import { wagmiAdapter } from './ReownAppKitModal' import { createRoutes } from './routes' +// Lazy load ReownProvider to avoid loading @reown packages until needed +const ReownProvider = lazy(() => import('./ReownProvider').then(module => ({ default: module.ReownProvider }))) + type AppProvidersProps = { children?: ReactNode } @@ -61,17 +62,19 @@ export const AppProviders = ({ children }: AppProvidersProps) => { }, [basename]) return ( - - - - - - - - - - - - + + + + + + + + + + + + + + ) } diff --git a/packages/web/src/app/ReownAppKitModal.tsx b/packages/web/src/app/ReownAppKitModal.tsx index 9aa5088c4b9..5d9d9fbb33f 100644 --- a/packages/web/src/app/ReownAppKitModal.tsx +++ b/packages/web/src/app/ReownAppKitModal.tsx @@ -1,50 +1,79 @@ -import { - mainnet, - solana, - type AppKitNetwork, - type Chain -} from '@reown/appkit/networks' -import { createAppKit } from '@reown/appkit/react' -import { SolanaAdapter } from '@reown/appkit-adapter-solana/react' -import { WagmiAdapter } from '@reown/appkit-adapter-wagmi' +// Re-export from lazy-loaded config for backward compatibility +// All new code should import from 'app/reownConfig' instead +// Note: We import audiusChain directly to avoid pulling in @reown packages +import { audiusChain } from './reownConfig' +import { getInitializedConfig } from './reownSyncInit' -import { env } from 'services/env' -import zIndex from 'utils/zIndex' +export { audiusChain } -// Audius ACDC chain (now ports to Core) -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 +// Dynamic import to avoid pulling in @reown packages at module load time +const getReownConfigDynamic = async () => { + const module = await import('./reownConfig') + return module.getReownConfig() +} + +// Lazy getters for backward compatibility (async version) +export const getWagmiAdapter = async () => { + const config = await getReownConfigDynamic() + return config.wagmiAdapter +} + +export const getAppkitModal = async () => { + const config = await getReownConfigDynamic() + return config.appkitModal +} -const projectId = env.REOWN_PROJECT_ID -const networks: [AppKitNetwork, ...AppKitNetwork[]] = [ - mainnet, - solana, - audiusChain -] -export const wagmiAdapter = new WagmiAdapter({ - networks, - projectId +export const getReownConfig = getReownConfigDynamic + +// For backward compatibility - synchronous access after ReownProvider initializes +// These will throw if accessed before ReownProvider mounts +export const wagmiAdapter = new Proxy({} as any, { + get(_target, prop) { + const config = getInitializedConfig() + return config.wagmiAdapter[prop] + }, + has(_target, prop) { + try { + const config = getInitializedConfig() + return prop in config.wagmiAdapter + } catch { + return false + } + }, + ownKeys(_target) { + const config = getInitializedConfig() + return Reflect.ownKeys(config.wagmiAdapter) + }, + getOwnPropertyDescriptor(_target, prop) { + const config = getInitializedConfig() + return Reflect.getOwnPropertyDescriptor(config.wagmiAdapter, prop) + } }) -const solanaAdapter = new SolanaAdapter() -export const appkitModal = createAppKit({ - adapters: [wagmiAdapter, solanaAdapter], - networks, - projectId, - themeVariables: { - '--w3m-z-index': zIndex.REOWN_APPKIT_MODAL // above ConnectWalletModal +// Proxy for appkitModal - maintains synchronous API after initialization +export const appkitModal = new Proxy({} as any, { + get(_target, prop) { + const config = getInitializedConfig() + const value = config.appkitModal[prop as keyof typeof config.appkitModal] + if (typeof value === 'function') { + return value.bind(config.appkitModal) + } + return value + }, + has(_target, prop) { + try { + const config = getInitializedConfig() + return prop in config.appkitModal + } catch { + return false + } + }, + ownKeys(_target) { + const config = getInitializedConfig() + return Reflect.ownKeys(config.appkitModal) }, - features: { - send: false, - swaps: false, - onramp: false, - socials: false, - email: false + getOwnPropertyDescriptor(_target, prop) { + const config = getInitializedConfig() + return Reflect.getOwnPropertyDescriptor(config.appkitModal, prop) } }) diff --git a/packages/web/src/app/ReownProvider.tsx b/packages/web/src/app/ReownProvider.tsx new file mode 100644 index 00000000000..b32e9e7b880 --- /dev/null +++ b/packages/web/src/app/ReownProvider.tsx @@ -0,0 +1,33 @@ +import { ReactNode, useEffect, useState } from 'react' +import { WagmiProvider } from 'wagmi' + +import { getReownConfig } from './reownConfig' +import { initializeReownSync } from './reownSyncInit' + +type ReownProviderProps = { + children: ReactNode +} + +/** + * Lazy-loaded provider that initializes Reown AppKit and wraps children with WagmiProvider. + * This ensures @reown packages are only loaded when this component is rendered. + */ +export const ReownProvider = ({ children }: ReownProviderProps) => { + const [wagmiConfig, setWagmiConfig] = useState(null) + + useEffect(() => { + // Eagerly initialize Reown config when provider mounts + // This ensures appkitModal and wagmiAdapter are available synchronously + getReownConfig().then((config) => { + initializeReownSync(config) + setWagmiConfig(config.wagmiAdapter.wagmiConfig) + }) + }, []) + + if (!wagmiConfig) { + return null + } + + return {children} +} + diff --git a/packages/web/src/app/reownConfig.ts b/packages/web/src/app/reownConfig.ts new file mode 100644 index 00000000000..cc505075d5c --- /dev/null +++ b/packages/web/src/app/reownConfig.ts @@ -0,0 +1,88 @@ +import { env } from 'services/env' +import zIndex from 'utils/zIndex' + +// Audius ACDC chain (now ports to Core) +// This is defined here to avoid importing Chain type from @reown +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 + +let wagmiAdapterInstance: any | null = null +let appkitModalInstance: any | null = null +let configPromise: Promise | null = null + +/** + * Lazy initialization function for Reown AppKit. + * Uses dynamic imports to ensure @reown packages are only loaded when this function is called. + */ +export const getReownConfig = async () => { + if (wagmiAdapterInstance && appkitModalInstance) { + return { + wagmiAdapter: wagmiAdapterInstance, + appkitModal: appkitModalInstance, + audiusChain + } + } + + // If already initializing, wait for that promise + if (configPromise) { + return configPromise + } + + // Use dynamic imports to lazy-load @reown packages + console.log('[Reown] Lazy loading @reown packages...') + configPromise = Promise.all([ + import('@reown/appkit/networks'), + import('@reown/appkit/react'), + import('@reown/appkit-adapter-solana/react'), + import('@reown/appkit-adapter-wagmi') + ]).then( + ([ + { mainnet, solana }, + { createAppKit }, + { SolanaAdapter }, + { WagmiAdapter } + ]) => { + const projectId = env.REOWN_PROJECT_ID + const networks = [mainnet, solana, audiusChain] + + wagmiAdapterInstance = new WagmiAdapter({ + networks, + projectId + }) + + const solanaAdapter = new SolanaAdapter() + + appkitModalInstance = createAppKit({ + adapters: [wagmiAdapterInstance, solanaAdapter], + networks, + projectId, + themeVariables: { + '--w3m-z-index': zIndex.REOWN_APPKIT_MODAL // above ConnectWalletModal + }, + features: { + send: false, + swaps: false, + onramp: false, + socials: false, + email: false + } + }) + + console.log('[Reown] @reown packages loaded successfully') + return { + wagmiAdapter: wagmiAdapterInstance, + appkitModal: appkitModalInstance, + audiusChain + } + } + ) + + return configPromise +} + diff --git a/packages/web/src/app/reownSyncInit.ts b/packages/web/src/app/reownSyncInit.ts new file mode 100644 index 00000000000..0c242019f94 --- /dev/null +++ b/packages/web/src/app/reownSyncInit.ts @@ -0,0 +1,26 @@ +// This module provides synchronous access to Reown config after it's been initialized +// It's used to bridge the async initialization with the synchronous API + +let initializedConfig: { + wagmiAdapter: any + appkitModal: any + audiusChain: any +} | null = null + +export const initializeReownSync = (config: { + wagmiAdapter: any + appkitModal: any + audiusChain: any +}) => { + initializedConfig = config +} + +export const getInitializedConfig = () => { + if (!initializedConfig) { + throw new Error( + 'Reown config not initialized. Make sure ReownProvider is mounted.' + ) + } + return initializedConfig +} + diff --git a/packages/web/src/pages/modals/Modals.tsx b/packages/web/src/pages/modals/Modals.tsx index 434652b1276..c24e0a1a1f6 100644 --- a/packages/web/src/pages/modals/Modals.tsx +++ b/packages/web/src/pages/modals/Modals.tsx @@ -9,7 +9,12 @@ import { AlbumTrackRemoveConfirmationModal } from 'components/album-track-remove import AppCTAModal from 'components/app-cta-modal/AppCTAModal' import { ArtistPickModal } from 'components/artist-pick-modal/ArtistPickModal' import BrowserPushConfirmationModal from 'components/browser-push-confirmation-modal/BrowserPushConfirmationModal' -import { BuySellModal } from 'components/buy-sell-modal/BuySellModal' +// Lazy load BuySellModal to avoid loading @reown packages until needed +const BuySellModal = lazy(() => + import('components/buy-sell-modal/BuySellModal').then((m) => ({ + default: m.BuySellModal + })) +) import CoinflowOnrampModal from 'components/coinflow-onramp-modal' import ConfirmerPreview from 'components/confirmer-preview/ConfirmerPreview' import DeletePlaylistConfirmationModal from 'components/delete-playlist-confirmation-modal/DeletePlaylistConfirmationModal' diff --git a/packages/web/src/services/audius-sdk/auth.ts b/packages/web/src/services/audius-sdk/auth.ts index 1c3c9f965e1..9aeaa702098 100644 --- a/packages/web/src/services/audius-sdk/auth.ts +++ b/packages/web/src/services/audius-sdk/auth.ts @@ -9,12 +9,16 @@ import { import { getWalletClient } from '@wagmi/core' import { type WalletClient } from 'viem' -import { audiusChain, wagmiAdapter } from 'app/ReownAppKitModal' +import { audiusChain, getWagmiAdapter } from 'app/ReownAppKitModal' import { env } from '../env' import { localStorage } from '../local-storage' -const wagmiConfig = wagmiAdapter.wagmiConfig +// Lazy-load wagmiConfig when needed (async) +const getWagmiConfig = async () => { + const adapter = await getWagmiAdapter() + return adapter.wagmiConfig +} export const getAudiusWalletClient = async (): Promise => { // Check if the user has already connected Hedgehog first... @@ -33,6 +37,7 @@ export const getAudiusWalletClient = async (): Promise => { console.debug('[audiusSdk] Initializing SDK with external wallet...') // Wait for the wallet to finish connecting/reconnecting + const wagmiConfig = await getWagmiConfig() if ( wagmiConfig.state.status === 'reconnecting' || wagmiConfig.state.status === 'connecting' diff --git a/packages/web/src/store/sign-out/sagas.ts b/packages/web/src/store/sign-out/sagas.ts index 4cb39c0ef35..c704068b3dd 100644 --- a/packages/web/src/store/sign-out/sagas.ts +++ b/packages/web/src/store/sign-out/sagas.ts @@ -15,7 +15,8 @@ import { push } from 'utils/navigation' const { resetAccount, unsubscribeBrowserPushNotifications } = accountActions const { signOut: signOutAction } = signOutActions -const wagmiConfig = wagmiAdapter.wagmiConfig +// Use synchronous accessor - by the time saga runs, ReownProvider should be initialized +const getWagmiConfig = () => wagmiAdapter.wagmiConfig function* watchSignOut() { const localStorage = yield* getContext('localStorage') @@ -24,6 +25,7 @@ function* watchSignOut() { yield takeLatest( signOutAction.type, function* (action: ReturnType) { + const wagmiConfig = getWagmiConfig() if (wagmiConfig.state.status === 'connected') { yield call(disconnect, wagmiConfig) }