Skip to content

Commit 512a293

Browse files
authored
feat(swap-widget): hook into a host-initialized AppKit instance (#12475)
1 parent d325205 commit 512a293

6 files changed

Lines changed: 121 additions & 39 deletions

File tree

‎packages/public-api/docs/swap-widget-sdk.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ function App() {
4848
## Key things to know
4949

5050
- **Wallet connection is built in.** The widget connects wallets via Reown AppKit (EVM, Bitcoin, and Solana).
51-
- **AppKit must be initialized for the widget to render.** Either pass `walletConnectProjectId` and the widget initializes AppKit for you (get a free project ID at [dashboard.reown.com](https://dashboard.reown.com)), or initialize AppKit yourself in the host app and the widget reads the shared instance — pair that with `showConnectButton={false}` to drive connection from your own UI. See the [README](https://github.com/shapeshift/web/blob/develop/packages/swap-widget/README.md#wallet-connection).
51+
- **AppKit must be initialized before the widget mounts.** Either pass `walletConnectProjectId` and the widget initializes AppKit for you (get a free project ID at [dashboard.reown.com](https://dashboard.reown.com)), or call `createAppKit()` yourself in the host app (with a wagmi EVM adapter) before mounting the widget — the widget detects the shared singleton and supplies its own providers, so you wrap it in none. Pair the host-owned path with `showConnectButton={false}` to drive connection from your own UI. This requires `@reown/appkit*`/`wagmi`/`viem` to dedupe to a single shared copy. See the [README](https://github.com/shapeshift/web/blob/develop/packages/swap-widget/README.md#wallet-connection).
5252
- **`partnerCode` drives affiliate attribution.** It is forwarded to this API as the `X-Partner-Code` header. See the [Affiliate Program guide](https://github.com/shapeshift/web/blob/develop/docs/affiliates.md).
5353
- **Chain/asset filtering** uses the `sellFilters` and `buyFilters` props (objects with `allowedChainIds` / `disabledChainIds` / `allowedAssetIds` / `disabledAssetIds`). See the README for the full prop list and examples.
5454

‎packages/swap-widget/README.md‎

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,22 @@ and provides the `WagmiProvider` / `QueryClient` it needs — you don't wrap it
9191
<SwapWidget walletConnectProjectId="your-walletconnect-project-id" />
9292
```
9393

94-
- **Reuse your app's AppKit instance.** If your host app already initializes Reown AppKit, the widget
95-
reads that shared instance from the AppKit singleton — omit `walletConnectProjectId` and just make
96-
sure AppKit is initialized before the widget mounts. Pair this with `showConnectButton={false}` to
97-
drive connection entirely from your own UI. See `src/demo/ExternalWalletApp.tsx` for a full example.
98-
99-
The header shows a built-in **Connect** button by default; set `showConnectButton={false}` if you
100-
want to hide it and drive connection from your own UI (the widget still owns the underlying AppKit
101-
instance).
94+
- **Reuse your app's existing AppKit.** If your host app already calls `createAppKit()` (with a wagmi
95+
EVM adapter), omit `walletConnectProjectId`. The widget detects the shared AppKit singleton, reads the
96+
wagmi config off it, and provides its own `WagmiProvider` / `QueryClient` from that config — **you wrap
97+
the widget in no providers of your own.** Pair this with `showConnectButton={false}` to drive
98+
connection entirely from your own UI. See `src/demo/ExternalWalletApp.tsx` for a full host example.
99+
100+
Two requirements for this mode:
101+
102+
- **Initialize AppKit _before_ the widget mounts.** The widget reads the AppKit singleton when it
103+
mounts and does not poll for late initialization — if AppKit isn't up yet, the widget renders nothing.
104+
- **Dedupe the AppKit/wagmi packages.** `@reown/appkit*`, `wagmi`, and `viem` must resolve to a single
105+
shared copy in your app, so the widget and your app share one AppKit instance and one wagmi state. A
106+
duplicated copy means the widget reads an empty store and shows no connection.
107+
108+
The header shows a built-in **Connect** button by default; set `showConnectButton={false}` to hide it
109+
and drive connection from your own UI.
102110

103111
### Supported wallet namespaces
104112

‎packages/swap-widget/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@shapeshiftoss/swap-widget",
3-
"version": "0.4.0",
3+
"version": "0.5.0",
44
"description": "Embeddable swap widget using ShapeShift API",
55
"repository": "https://github.com/shapeshift/web",
66
"license": "MIT",

‎packages/swap-widget/src/components/WalletProvider.tsx‎

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { useAppKit } from '@reown/appkit/react'
22
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
33
import type { ReactNode } from 'react'
4-
import { useCallback, useEffect, useMemo, useState } from 'react'
4+
import { useCallback, useEffect, useState } from 'react'
55
import type { Config } from 'wagmi'
66
import { WagmiProvider } from 'wagmi'
77

8-
import { getWagmiAdapter, initializeAppKit, isAppKitInitialized } from '../config/appkit'
8+
import { getActiveWagmiConfig, initializeAppKit, isAppKitInitialized } from '../config/appkit'
99
import { useSwapWallet } from '../contexts/SwapWalletContext'
1010
import { truncateAddress } from '../types'
1111

@@ -17,20 +17,24 @@ type AppKitWalletProviderProps = {
1717
}
1818

1919
export const AppKitWalletProvider = ({ projectId, children }: AppKitWalletProviderProps) => {
20-
const [isReady, setIsReady] = useState(false)
20+
// Seed from the SDK singleton so a host-initialized AppKit is picked up on the
21+
// first render (no flash of null) when it was created before the widget mounts.
22+
const [wagmiConfig, setWagmiConfig] = useState<Config | undefined>(() =>
23+
isAppKitInitialized() ? getActiveWagmiConfig() : undefined,
24+
)
2125

2226
useEffect(() => {
23-
if (projectId) initializeAppKit(projectId)
24-
if (isAppKitInitialized()) setIsReady(true)
27+
// Self-init only when no AppKit exists yet. If the host already called
28+
// createAppKit, we hook into that shared instance rather than creating a second.
29+
if (projectId && !isAppKitInitialized()) initializeAppKit(projectId)
30+
if (isAppKitInitialized()) setWagmiConfig(getActiveWagmiConfig())
2531
}, [projectId])
2632

27-
const wagmiConfig = useMemo((): Config | undefined => {
28-
if (!isReady) return undefined
29-
return getWagmiAdapter()?.wagmiConfig as unknown as Config | undefined
30-
}, [isReady])
31-
3233
if (!wagmiConfig) return null
3334

35+
// We always own the WagmiProvider / QueryClient — built from the wagmi config we
36+
// read off the shared AppKit singleton (whether self-init or host-owned). This is
37+
// what lets a host integrate by calling createAppKit() alone, wrapping nothing.
3438
return (
3539
<WagmiProvider config={wagmiConfig}>
3640
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>

‎packages/swap-widget/src/config/appkit.ts‎

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ import {
1616
polygon,
1717
solana,
1818
} from '@reown/appkit/networks'
19-
import { createAppKit } from '@reown/appkit/react'
19+
import { createAppKit, modal } from '@reown/appkit/react'
2020
import { BitcoinAdapter } from '@reown/appkit-adapter-bitcoin'
2121
import { SolanaAdapter } from '@reown/appkit-adapter-solana/react'
2222
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'
2323
import { PhantomWalletAdapter, SolflareWalletAdapter } from '@solana/wallet-adapter-wallets'
24+
import type { Config } from 'wagmi'
2425

2526
const EVM_NETWORKS: readonly AppKitNetwork[] = [
2627
mainnet,
@@ -40,17 +41,28 @@ const EVM_NETWORKS: readonly AppKitNetwork[] = [
4041

4142
const ALL_NETWORKS: readonly AppKitNetwork[] = [...EVM_NETWORKS, bitcoin, solana]
4243

43-
let wagmiAdapter: WagmiAdapter | null = null
44-
let appKitInitialized = false
44+
// AppKit keeps a module-level `modal` singleton, set by `createAppKit` whether
45+
// it's called by this widget (self-init) or by the host app. Reading it lets the
46+
// widget detect — and hook into — an AppKit the host already initialized, as long
47+
// as `@reown/appkit*` is deduped to a single shared copy in the consumer's tree.
48+
export const isAppKitInitialized = (): boolean => modal !== undefined
4549

46-
export const getWagmiAdapter = (): WagmiAdapter | null => wagmiAdapter
47-
48-
export const isAppKitInitialized = (): boolean => appKitInitialized
50+
// The active wagmi Config, read off the AppKit singleton's EVM adapter. Works in
51+
// both modes — self-init (we registered the adapter via createAppKit) and
52+
// host-owned (the host's adapter). Because the widget can pull the shared config
53+
// from the singleton, it builds its own WagmiProvider from it — so a host only
54+
// needs to call `createAppKit()`, with no WagmiProvider/QueryClient wrapping.
55+
export const getActiveWagmiConfig = (): Config | undefined => {
56+
const evmAdapter = modal?.chainAdapters?.eip155 as WagmiAdapter | undefined
57+
return evmAdapter?.wagmiConfig
58+
}
4959

5060
export const initializeAppKit = (projectId: string): void => {
51-
if (appKitInitialized) return
61+
// Already initialized — either by a previous call here, or by the host app. In
62+
// the latter case we skip entirely and reuse the host's AppKit + wagmi config.
63+
if (modal) return
5264

53-
wagmiAdapter = new WagmiAdapter({
65+
const wagmiAdapter = new WagmiAdapter({
5466
networks: [...EVM_NETWORKS],
5567
projectId,
5668
})
@@ -68,6 +80,4 @@ export const initializeAppKit = (projectId: string): void => {
6880
send: false,
6981
},
7082
})
71-
72-
appKitInitialized = true
7383
}

‎packages/swap-widget/src/demo/ExternalWalletApp.tsx‎

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
11
import './App.css'
22

3-
import { useAppKit, useAppKitAccount } from '@reown/appkit/react'
3+
import type { AppKitNetwork } from '@reown/appkit/networks'
4+
import {
5+
arbitrum,
6+
avalanche,
7+
base,
8+
bitcoin,
9+
bsc,
10+
gnosis,
11+
hyperEvm,
12+
katana,
13+
mainnet,
14+
megaeth,
15+
monad,
16+
optimism,
17+
plasma,
18+
polygon,
19+
solana,
20+
} from '@reown/appkit/networks'
21+
import { createAppKit, useAppKit, useAppKitAccount } from '@reown/appkit/react'
22+
import { BitcoinAdapter } from '@reown/appkit-adapter-bitcoin'
23+
import { SolanaAdapter } from '@reown/appkit-adapter-solana/react'
24+
import { WagmiAdapter } from '@reown/appkit-adapter-wagmi'
25+
import { PhantomWalletAdapter, SolflareWalletAdapter } from '@solana/wallet-adapter-wallets'
426
import { useCallback, useEffect, useMemo, useState } from 'react'
527

628
import { SwapWidget } from '../components/SwapWidget'
7-
import { initializeAppKit } from '../config/appkit'
829
import { truncateAddress } from '../types'
930
import { DemoCustomizer, useDemoTheme } from './DemoCustomizer'
1031
import { WidgetModal } from './WidgetModal'
@@ -180,9 +201,54 @@ const loadThemeMode = (): 'light' | 'dark' => {
180201
}
181202
}
182203

204+
const EVM_NETWORKS: readonly AppKitNetwork[] = [
205+
mainnet,
206+
polygon,
207+
arbitrum,
208+
optimism,
209+
base,
210+
avalanche,
211+
bsc,
212+
gnosis,
213+
monad,
214+
megaeth,
215+
hyperEvm,
216+
plasma,
217+
katana,
218+
]
219+
220+
const ALL_NETWORKS: readonly AppKitNetwork[] = [...EVM_NETWORKS, bitcoin, solana]
221+
222+
let appKitInitialized = false
223+
224+
const initAppKit = (projectId: string): void => {
225+
if (appKitInitialized) return
226+
appKitInitialized = true
227+
228+
const wagmiAdapter = new WagmiAdapter({
229+
networks: [...EVM_NETWORKS],
230+
projectId,
231+
})
232+
233+
const bitcoinAdapter = new BitcoinAdapter()
234+
const solanaAdapter = new SolanaAdapter({
235+
wallets: [new PhantomWalletAdapter(), new SolflareWalletAdapter()] as any,
236+
})
237+
238+
createAppKit({
239+
adapters: [wagmiAdapter, bitcoinAdapter, solanaAdapter],
240+
projectId,
241+
networks: [...ALL_NETWORKS] as [AppKitNetwork, ...AppKitNetwork[]],
242+
features: {
243+
send: false,
244+
},
245+
})
246+
}
247+
183248
export const ExternalWalletApp = () => {
184249
const [theme, setTheme] = useState<'light' | 'dark'>(loadThemeMode)
185-
const [isReady, setIsReady] = useState(false)
250+
251+
useState(() => initAppKit(PROJECT_ID))
186252

187253
useEffect(() => {
188254
try {
@@ -192,11 +258,5 @@ export const ExternalWalletApp = () => {
192258
}
193259
}, [theme])
194260

195-
useEffect(() => {
196-
initializeAppKit(PROJECT_ID)
197-
setIsReady(true)
198-
}, [])
199-
200-
if (!isReady) return null
201261
return <ExternalDemoBody theme={theme} setTheme={setTheme} />
202262
}

0 commit comments

Comments
 (0)