@sodax/wallet-sdk-react 1.5.7-beta → 2.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +103 -145
- package/ai-exported/AGENTS.md +122 -0
- package/ai-exported/integration/README.md +102 -0
- package/ai-exported/integration/ai-rules.md +136 -0
- package/ai-exported/integration/architecture.md +181 -0
- package/ai-exported/integration/examples/01-minimal-evm.tsx +75 -0
- package/ai-exported/integration/examples/02-multi-chain-modal.tsx +169 -0
- package/ai-exported/integration/examples/03-nextjs-app-router.tsx +99 -0
- package/ai-exported/integration/examples/04-walletconnect-setup.tsx +89 -0
- package/ai-exported/integration/examples/README.md +29 -0
- package/ai-exported/integration/recipes/batch-operations.md +223 -0
- package/ai-exported/integration/recipes/bridge-to-sdk.md +164 -0
- package/ai-exported/integration/recipes/chain-detection.md +254 -0
- package/ai-exported/integration/recipes/connect-button.md +156 -0
- package/ai-exported/integration/recipes/multi-chain-modal.md +199 -0
- package/ai-exported/integration/recipes/setup.md +158 -0
- package/ai-exported/integration/recipes/sign-message.md +137 -0
- package/ai-exported/integration/recipes/sub-path-imports.md +95 -0
- package/ai-exported/integration/recipes/switch-chain.md +141 -0
- package/ai-exported/integration/recipes/walletconnect-setup.md +139 -0
- package/ai-exported/integration/reference/api-surface.md +175 -0
- package/ai-exported/integration/reference/chain-support.md +78 -0
- package/ai-exported/integration/reference/connectors.md +74 -0
- package/ai-exported/integration/reference/hooks.md +204 -0
- package/ai-exported/integration/reference/wallet-brands.md +106 -0
- package/ai-exported/migration/README.md +49 -0
- package/ai-exported/migration/ai-rules.md +144 -0
- package/ai-exported/migration/breaking-changes.md +305 -0
- package/ai-exported/migration/checklist.md +159 -0
- package/ai-exported/migration/recipes/connect-button.md +166 -0
- package/ai-exported/migration/recipes/multi-chain-modal.md +244 -0
- package/ai-exported/migration/recipes/ssr-setup.md +162 -0
- package/ai-exported/migration/recipes/walletconnect-migration.md +168 -0
- package/ai-exported/migration/reference/components.md +73 -0
- package/ai-exported/migration/reference/config.md +307 -0
- package/ai-exported/migration/reference/hooks.md +278 -0
- package/ai-exported/migration/reference/imports.md +157 -0
- package/dist/XConnector-B9YQTVJ4.d.ts +146 -0
- package/dist/chunk-2BOUGCJ7.mjs +150 -0
- package/dist/chunk-2BOUGCJ7.mjs.map +1 -0
- package/dist/chunk-66BAUK56.mjs +202 -0
- package/dist/chunk-66BAUK56.mjs.map +1 -0
- package/dist/chunk-7ULB6DW4.mjs +102 -0
- package/dist/chunk-7ULB6DW4.mjs.map +1 -0
- package/dist/chunk-BKJB527E.mjs +125 -0
- package/dist/chunk-BKJB527E.mjs.map +1 -0
- package/dist/chunk-BXJLBR4G.mjs +88 -0
- package/dist/chunk-BXJLBR4G.mjs.map +1 -0
- package/dist/chunk-E5IAZ7E6.mjs +186 -0
- package/dist/chunk-E5IAZ7E6.mjs.map +1 -0
- package/dist/chunk-MAQ47Q52.mjs +33 -0
- package/dist/chunk-MAQ47Q52.mjs.map +1 -0
- package/dist/chunk-MXZVF5HR.mjs +34 -0
- package/dist/chunk-MXZVF5HR.mjs.map +1 -0
- package/dist/chunk-N5A2TMF6.mjs +33 -0
- package/dist/chunk-N5A2TMF6.mjs.map +1 -0
- package/dist/chunk-NY7U7OJW.mjs +64 -0
- package/dist/chunk-NY7U7OJW.mjs.map +1 -0
- package/dist/chunk-PJLEJVAU.mjs +140 -0
- package/dist/chunk-PJLEJVAU.mjs.map +1 -0
- package/dist/chunk-PLCA4ZDJ.mjs +1585 -0
- package/dist/chunk-PLCA4ZDJ.mjs.map +1 -0
- package/dist/chunk-TZMKDXFA.mjs +3 -0
- package/dist/chunk-TZMKDXFA.mjs.map +1 -0
- package/dist/chunk-X2MHIWXO.mjs +100 -0
- package/dist/chunk-X2MHIWXO.mjs.map +1 -0
- package/dist/chunk-XZ7CHO2S.mjs +41 -0
- package/dist/chunk-XZ7CHO2S.mjs.map +1 -0
- package/dist/config-OlnzyEUE.d.ts +146 -0
- package/dist/index.cjs +2784 -1594
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +768 -1498
- package/dist/index.mjs +463 -2004
- package/dist/index.mjs.map +1 -1
- package/dist/xchains/bitcoin/index.cjs +1927 -0
- package/dist/xchains/bitcoin/index.cjs.map +1 -0
- package/dist/xchains/bitcoin/index.d.ts +125 -0
- package/dist/xchains/bitcoin/index.mjs +16 -0
- package/dist/xchains/bitcoin/index.mjs.map +1 -0
- package/dist/xchains/evm/index.cjs +316 -0
- package/dist/xchains/evm/index.cjs.map +1 -0
- package/dist/xchains/evm/index.d.ts +39 -0
- package/dist/xchains/evm/index.mjs +5 -0
- package/dist/xchains/evm/index.mjs.map +1 -0
- package/dist/xchains/icon/index.cjs +311 -0
- package/dist/xchains/icon/index.cjs.map +1 -0
- package/dist/xchains/icon/index.d.ts +37 -0
- package/dist/xchains/icon/index.mjs +7 -0
- package/dist/xchains/icon/index.mjs.map +1 -0
- package/dist/xchains/injective/index.cjs +223 -0
- package/dist/xchains/injective/index.cjs.map +1 -0
- package/dist/xchains/injective/index.d.ts +35 -0
- package/dist/xchains/injective/index.mjs +5 -0
- package/dist/xchains/injective/index.mjs.map +1 -0
- package/dist/xchains/near/index.cjs +190 -0
- package/dist/xchains/near/index.cjs.map +1 -0
- package/dist/xchains/near/index.d.ts +34 -0
- package/dist/xchains/near/index.mjs +6 -0
- package/dist/xchains/near/index.mjs.map +1 -0
- package/dist/xchains/solana/index.cjs +186 -0
- package/dist/xchains/solana/index.cjs.map +1 -0
- package/dist/xchains/solana/index.d.ts +26 -0
- package/dist/xchains/solana/index.mjs +7 -0
- package/dist/xchains/solana/index.mjs.map +1 -0
- package/dist/xchains/stacks/index.cjs +240 -0
- package/dist/xchains/stacks/index.cjs.map +1 -0
- package/dist/xchains/stacks/index.d.ts +36 -0
- package/dist/xchains/stacks/index.mjs +5 -0
- package/dist/xchains/stacks/index.mjs.map +1 -0
- package/dist/xchains/stellar/index.cjs +322 -0
- package/dist/xchains/stellar/index.cjs.map +1 -0
- package/dist/xchains/stellar/index.d.ts +44 -0
- package/dist/xchains/stellar/index.mjs +6 -0
- package/dist/xchains/stellar/index.mjs.map +1 -0
- package/dist/xchains/sui/index.cjs +248 -0
- package/dist/xchains/sui/index.cjs.map +1 -0
- package/dist/xchains/sui/index.d.ts +37 -0
- package/dist/xchains/sui/index.mjs +7 -0
- package/dist/xchains/sui/index.mjs.map +1 -0
- package/docs/ADDING_A_NEW_CHAIN.md +440 -0
- package/docs/ARCHITECTURE.md +291 -0
- package/docs/BATCH_OPERATIONS.md +267 -0
- package/docs/CHAIN_DETECTION.md +216 -0
- package/docs/CONFIGURE_PROVIDER.md +360 -0
- package/docs/CONNECTORS.md +247 -0
- package/docs/CONNECT_FLOW.md +276 -0
- package/docs/EVM_SWITCH_CHAIN.md +161 -0
- package/docs/SIGN_MESSAGE.md +213 -0
- package/docs/SUB_PATH_EXPORTS.md +246 -0
- package/docs/WALLETCONNECT.md +154 -0
- package/docs/WALLET_MODAL.md +331 -0
- package/docs/WALLET_PROVIDER_BRIDGE.md +226 -0
- package/package.json +34 -9
- package/skills/SKILLS.md +84 -0
- package/skills/bridge-to-sdk.md +148 -0
- package/skills/connect-button.md +116 -0
- package/skills/evm-only-walletconnect.md +111 -0
- package/skills/multi-chain-modal.md +178 -0
- package/skills/setup.md +107 -0
- package/dist/index.d.cts +0 -1579
- package/src/Hydrate.ts +0 -65
- package/src/SodaxWalletProvider.tsx +0 -97
- package/src/actions/getXChainType.ts +0 -8
- package/src/actions/getXService.ts +0 -33
- package/src/actions/index.ts +0 -2
- package/src/assets/wallets/hana.svg +0 -6
- package/src/assets/wallets/havah.svg +0 -76
- package/src/assets/wallets/keplr.svg +0 -30
- package/src/assets/wallets/metamask.svg +0 -60
- package/src/assets/wallets/phantom.svg +0 -4
- package/src/assets/wallets/sui.svg +0 -20
- package/src/core/XConnector.ts +0 -54
- package/src/core/XService.ts +0 -85
- package/src/core/index.ts +0 -2
- package/src/hooks/index.ts +0 -11
- package/src/hooks/useEthereumChainId.ts +0 -44
- package/src/hooks/useEvmSwitchChain.ts +0 -91
- package/src/hooks/useWalletProvider.ts +0 -206
- package/src/hooks/useXAccount.ts +0 -51
- package/src/hooks/useXAccounts.ts +0 -56
- package/src/hooks/useXBalances.ts +0 -65
- package/src/hooks/useXConnect.ts +0 -118
- package/src/hooks/useXConnection.ts +0 -72
- package/src/hooks/useXConnectors.ts +0 -72
- package/src/hooks/useXDisconnect.ts +0 -73
- package/src/hooks/useXService.ts +0 -8
- package/src/hooks/useXSignMessage.ts +0 -82
- package/src/index.ts +0 -19
- package/src/types/index.ts +0 -22
- package/src/useXWagmiStore.ts +0 -116
- package/src/utils/index.ts +0 -21
- package/src/xchains/bitcoin/BitcoinXConnector.ts +0 -34
- package/src/xchains/bitcoin/BitcoinXService.ts +0 -40
- package/src/xchains/bitcoin/OKXXConnector.ts +0 -117
- package/src/xchains/bitcoin/UnisatXConnector.ts +0 -117
- package/src/xchains/bitcoin/XverseXConnector.ts +0 -232
- package/src/xchains/bitcoin/index.ts +0 -7
- package/src/xchains/bitcoin/useBitcoinXConnectors.ts +0 -14
- package/src/xchains/evm/EvmXConnector.ts +0 -27
- package/src/xchains/evm/EvmXService.ts +0 -211
- package/src/xchains/evm/index.ts +0 -3
- package/src/xchains/icon/IconHanaXConnector.ts +0 -39
- package/src/xchains/icon/IconXService.ts +0 -117
- package/src/xchains/icon/actions.ts +0 -28
- package/src/xchains/icon/iconex/index.tsx +0 -46
- package/src/xchains/icon/index.ts +0 -2
- package/src/xchains/injective/InjectiveXConnector.ts +0 -60
- package/src/xchains/injective/InjectiveXService.ts +0 -62
- package/src/xchains/injective/actions.ts +0 -32
- package/src/xchains/injective/index.ts +0 -2
- package/src/xchains/near/NearXConnector.ts +0 -42
- package/src/xchains/near/NearXService.ts +0 -46
- package/src/xchains/near/useNearXConnectors.ts +0 -23
- package/src/xchains/solana/SolanaXConnector.ts +0 -26
- package/src/xchains/solana/SolanaXService.ts +0 -46
- package/src/xchains/solana/index.ts +0 -2
- package/src/xchains/stacks/StacksXConnector.ts +0 -63
- package/src/xchains/stacks/StacksXService.ts +0 -59
- package/src/xchains/stacks/constants.ts +0 -42
- package/src/xchains/stacks/index.ts +0 -4
- package/src/xchains/stacks/useStacksXConnectors.ts +0 -7
- package/src/xchains/stellar/CustomSorobanServer.ts +0 -93
- package/src/xchains/stellar/StellarWalletsKitXConnector.ts +0 -53
- package/src/xchains/stellar/StellarXService.ts +0 -93
- package/src/xchains/stellar/actions.ts +0 -24
- package/src/xchains/stellar/index.tsx +0 -2
- package/src/xchains/stellar/useStellarXConnectors.ts +0 -21
- package/src/xchains/stellar/utils.ts +0 -49
- package/src/xchains/sui/SuiXConnector.ts +0 -28
- package/src/xchains/sui/SuiXService.ts +0 -66
- package/src/xchains/sui/index.ts +0 -2
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# WalletConnect
|
|
2
|
+
|
|
3
|
+
Default EVM wallet discovery uses [EIP-6963](https://eips.ethereum.org/EIPS/eip-6963) — only browser-extension wallets surface. Partners using enterprise custody solutions (Fireblocks, Ledger Live, etc.) cannot install browser extensions; they need WalletConnect protocol to connect.
|
|
4
|
+
|
|
5
|
+
`@sodax/wallet-sdk-react` enables WalletConnect via the `walletConnect` field on the `EVM` chain-type slot. The field extends wagmi's [`WalletConnectParameters`](https://wagmi.sh/core/api/connectors/walletConnect) directly — every wagmi option is available.
|
|
6
|
+
|
|
7
|
+
The integration point is [`EvmProvider.tsx`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/src/providers/evm/EvmProvider.tsx).
|
|
8
|
+
|
|
9
|
+
## Table of contents
|
|
10
|
+
|
|
11
|
+
1. [When you need WalletConnect](#when-you-need-walletconnect)
|
|
12
|
+
2. [Minimal setup](#minimal-setup)
|
|
13
|
+
3. [Restrict the modal — Fireblocks-only example](#restrict-the-modal--fireblocks-only-example)
|
|
14
|
+
4. [QR-modal stacking with `useWalletModal`](#qr-modal-stacking-with-usewalletmodal)
|
|
15
|
+
5. [Missing `projectId` — silent skip](#missing-projectid--silent-skip)
|
|
16
|
+
6. [WalletConnect is EVM-only](#walletconnect-is-evm-only)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## When you need WalletConnect
|
|
21
|
+
|
|
22
|
+
Add WalletConnect when the partner's user can't install a browser extension:
|
|
23
|
+
|
|
24
|
+
| Custody / wallet | Why WalletConnect |
|
|
25
|
+
|---|---|
|
|
26
|
+
| Fireblocks | No browser extension — workspace approval flows over WalletConnect |
|
|
27
|
+
| Ledger Live | Hardware-wallet wrapper, no EIP-6963 injection |
|
|
28
|
+
| Mobile-only wallets (Trust, Rainbow, MetaMask Mobile) | Browser session pairs with phone via QR |
|
|
29
|
+
| Coinbase Smart Wallet | Pop-up + WC fallback |
|
|
30
|
+
|
|
31
|
+
If your dApp only targets desktop browser wallets (MetaMask extension, Hana, Phantom EVM, etc.), you can omit `walletConnect` entirely — EIP-6963 covers them.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Minimal setup
|
|
36
|
+
|
|
37
|
+
1. Get a WalletConnect Cloud project id at [https://cloud.walletconnect.com](https://cloud.walletconnect.com).
|
|
38
|
+
2. Pass it to `config.EVM.walletConnect.projectId`.
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
import { SodaxWalletProvider, type SodaxWalletConfig } from '@sodax/wallet-sdk-react';
|
|
42
|
+
import { ChainKeys } from '@sodax/types';
|
|
43
|
+
|
|
44
|
+
const walletConfig: SodaxWalletConfig = {
|
|
45
|
+
EVM: {
|
|
46
|
+
ssr: true,
|
|
47
|
+
chains: {
|
|
48
|
+
[ChainKeys.SONIC_MAINNET]: { rpcUrl: 'https://rpc.soniclabs.com' },
|
|
49
|
+
[ChainKeys.ARBITRUM_MAINNET]: { rpcUrl: 'https://arb1.arbitrum.io/rpc' },
|
|
50
|
+
},
|
|
51
|
+
walletConnect: {
|
|
52
|
+
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID!,
|
|
53
|
+
// showQrModal, isNewChainsStale, qrModalOptions, etc.
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
A `walletConnect` connector is added to the wagmi config alongside any EIP-6963 wallets. `EvmHydrator` discovers it automatically — `useXConnectors({ xChainType: 'EVM' })` returns it in the same list as MetaMask, Hana, etc. **No UI changes required**.
|
|
60
|
+
|
|
61
|
+
The default behavior (`showQrModal: true`) lets wagmi/WalletConnect own the QR display. Pass `showQrModal: false` only if you render a custom QR modal.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Restrict the modal — Fireblocks-only example
|
|
66
|
+
|
|
67
|
+
Some partners want **only** their custody wallet visible in the QR modal — no Trust / Rainbow / MetaMask Mobile noise. Use `qrModalOptions` to filter the WalletConnect Explorer list:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
const walletConfig: SodaxWalletConfig = {
|
|
71
|
+
EVM: {
|
|
72
|
+
ssr: true,
|
|
73
|
+
chains: { [ChainKeys.SONIC_MAINNET]: { rpcUrl: 'https://rpc.soniclabs.com' } },
|
|
74
|
+
walletConnect: {
|
|
75
|
+
projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID!,
|
|
76
|
+
qrModalOptions: {
|
|
77
|
+
explorerRecommendedWalletIds: ['225affb176778569276e484e1b92637ad061b01e13a048b35a9d280c3b58970f'], // Fireblocks
|
|
78
|
+
explorerExcludedWalletIds: 'ALL', // hide everything except recommended
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Key `qrModalOptions` fields (extends `QrModalOptions` from `@walletconnect/ethereum-provider`):
|
|
86
|
+
|
|
87
|
+
| Field | Effect |
|
|
88
|
+
|-------|--------|
|
|
89
|
+
| `explorerRecommendedWalletIds` | Wallet IDs to surface at the top of the list |
|
|
90
|
+
| `explorerExcludedWalletIds` | Wallet IDs to hide. `'ALL'` hides everything except recommended |
|
|
91
|
+
| `themeMode` | `'light' \| 'dark'` |
|
|
92
|
+
| `themeVariables` | CSS custom-property overrides |
|
|
93
|
+
|
|
94
|
+
Find wallet IDs in the [WalletConnect Explorer](https://walletconnect.com/explorer) — they're the long hex strings, not the human names.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## QR-modal stacking with `useWalletModal`
|
|
99
|
+
|
|
100
|
+
When the user picks the WalletConnect connector inside `useWalletModal`'s `walletSelect` state, wagmi opens its own QR modal. While that modal is up, `useWalletModal` stays in `connecting` — two dialogs would stack.
|
|
101
|
+
|
|
102
|
+
The SDK doesn't enforce a hide policy; partners decide. The recommended pattern is to render `null` while wagmi's modal is visible:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { useWalletModal } from '@sodax/wallet-sdk-react';
|
|
106
|
+
|
|
107
|
+
function WalletModalRoot() {
|
|
108
|
+
const modal = useWalletModal();
|
|
109
|
+
|
|
110
|
+
if (
|
|
111
|
+
modal.state.kind === 'connecting' &&
|
|
112
|
+
modal.state.connector.id === 'walletConnect'
|
|
113
|
+
) {
|
|
114
|
+
return null; // let wagmi's QR modal own the screen
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ... render the rest of the state machine
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
The wagmi connector id is the literal string `'walletConnect'`. Resume rendering when the state transitions to `success` / `error`. See [`WALLET_MODAL.md`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/WALLET_MODAL.md#walletconnect-qr-modal-caveat) for the full discussion.
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Missing `projectId` — silent skip
|
|
126
|
+
|
|
127
|
+
If `walletConnect` is present but `projectId` is missing or empty, the WalletConnect connector is **silently skipped** at wagmi config time and a one-time warning is logged:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
[wallet-sdk-react] walletConnect.projectId is required — WalletConnect connector skipped.
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
EIP-6963 wallets continue to work normally — the dApp degrades gracefully to extension-only mode. There is no runtime crash.
|
|
134
|
+
|
|
135
|
+
This behavior intentionally avoids forcing every developer to plumb `projectId` through environment variables in dev — they can leave `walletConnect: { /* no projectId */ }` commented or empty during local work and re-enable for production.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## WalletConnect is EVM-only
|
|
140
|
+
|
|
141
|
+
The `walletConnect` field exists only on the `EVM` slot. Other chain families (`SOLANA`, `BITCOIN`, etc.) use their own native wallet adapters and don't share the WalletConnect protocol layer.
|
|
142
|
+
|
|
143
|
+
To support Solana via WalletConnect-equivalent protocols (Solana Wallet Standard, Mobile Wallet Adapter), use the appropriate connectors registered in the `SOLANA` chain registry — those don't go through this `walletConnect` field.
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Related docs
|
|
148
|
+
|
|
149
|
+
- [Configure SodaxWalletProvider](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONFIGURE_PROVIDER.md#walletconnect-evm-only) — full chain-type slot reference
|
|
150
|
+
- [Wallet Modal](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/WALLET_MODAL.md) — `useWalletModal` state machine + QR-stacking pattern
|
|
151
|
+
- [Connect Flow](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONNECT_FLOW.md) — discover/connect/disconnect lifecycle
|
|
152
|
+
- [Connectors](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONNECTORS.md) — `IXConnector` contract; the WC connector surfaces with `id === 'walletConnect'`
|
|
153
|
+
- [wagmi WalletConnect docs](https://wagmi.sh/core/api/connectors/walletConnect) — full `WalletConnectParameters` reference
|
|
154
|
+
- [WalletConnect Cloud](https://cloud.walletconnect.com) — get a `projectId`
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
# Wallet Modal
|
|
2
|
+
|
|
3
|
+
`useWalletModal` is a headless multi-chain wallet-connect lifecycle exposed as a discriminated state machine. It owns transitions between `closed → chainSelect → walletSelect → connecting → success | error`, dedupes concurrent connect attempts, and waits for provider-managed chains' Hydrators to land an account before signaling success. The hook is render-agnostic — pair it with any dialog, drawer, or inline UI.
|
|
4
|
+
|
|
5
|
+
For non-modal flows (single button with status + retry), use [`useConnectionFlow`](#useconnectionflow--non-modal-alternative) instead.
|
|
6
|
+
|
|
7
|
+
The canonical state union is [`WalletModalState`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/src/useWalletModalStore.ts).
|
|
8
|
+
|
|
9
|
+
## Table of contents
|
|
10
|
+
|
|
11
|
+
1. [State machine](#state-machine)
|
|
12
|
+
2. [Hook API](#hook-api)
|
|
13
|
+
3. [Rendering — switch on `state.kind`](#rendering--switch-on-statekind)
|
|
14
|
+
4. [`selectWallet` lifecycle](#selectwallet-lifecycle)
|
|
15
|
+
5. [Smart `back()` navigation](#smart-back-navigation)
|
|
16
|
+
6. [`onConnected` side-effect callback](#onconnected-side-effect-callback)
|
|
17
|
+
7. [Hydration timeout](#hydration-timeout)
|
|
18
|
+
8. [WalletConnect QR modal caveat](#walletconnect-qr-modal-caveat)
|
|
19
|
+
9. [Cancellation semantics](#cancellation-semantics)
|
|
20
|
+
10. [`useConnectionFlow` — non-modal alternative](#useconnectionflow--non-modal-alternative)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## State machine
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
open()
|
|
28
|
+
┌──────────────────────────────────────┐
|
|
29
|
+
│ ▼
|
|
30
|
+
┌─────────┐ ┌──────────────┐
|
|
31
|
+
│ closed │ ◀─── close() ─────────── │ chainSelect │
|
|
32
|
+
└─────────┘ └──────────────┘
|
|
33
|
+
▲ │
|
|
34
|
+
│ │ selectChain(chainType)
|
|
35
|
+
│ close() / back() ◀── success ◀───────┤
|
|
36
|
+
│ ▼
|
|
37
|
+
│ ┌──────────────┐
|
|
38
|
+
│ │ walletSelect │ ◀─── back() ─────┐
|
|
39
|
+
│ └──────────────┘ │
|
|
40
|
+
│ │ │
|
|
41
|
+
│ selectWallet(connector) │
|
|
42
|
+
│ │ │
|
|
43
|
+
│ ▼ │
|
|
44
|
+
│ ┌──────────────┐ │
|
|
45
|
+
│ │ connecting │ ─── back() ───────┤
|
|
46
|
+
│ └──────────────┘ │
|
|
47
|
+
│ │ │ │
|
|
48
|
+
│ ok │ │ err │
|
|
49
|
+
│ ▼ ▼ │
|
|
50
|
+
│ ┌─────────┐ ┌────────┐ │
|
|
51
|
+
└────── back() ◀──────────── │ success │ │ error │ ──── back() ──┘
|
|
52
|
+
└─────────┘ └────────┘
|
|
53
|
+
│ │
|
|
54
|
+
onConnected retry()
|
|
55
|
+
│
|
|
56
|
+
(consumer
|
|
57
|
+
calls
|
|
58
|
+
close())
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Six discriminated states:
|
|
62
|
+
|
|
63
|
+
| `state.kind` | Extra fields | Meaning |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| `'closed'` | — | Modal is dismissed |
|
|
66
|
+
| `'chainSelect'` | — | User picks a chain family (EVM / SOLANA / …) |
|
|
67
|
+
| `'walletSelect'` | `chainType` | User picks a wallet within the chosen family |
|
|
68
|
+
| `'connecting'` | `chainType`, `connector` | `selectWallet` in flight, waiting for wallet popup |
|
|
69
|
+
| `'success'` | `chainType`, `connector`, `account` | Connect resolved with a usable `XAccount` |
|
|
70
|
+
| `'error'` | `chainType`, `connector`, `error` | Connect failed; consumer can call `retry()` |
|
|
71
|
+
|
|
72
|
+
The state lives in `useWalletModalStore` — a Zustand slice **separate** from `useXWalletStore`. Modal lifecycle is ephemeral UI state with no persistence concerns; connection state is persisted independently. Direct store access is intentionally not part of the public surface — go through `useWalletModal()`.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Hook API
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { useWalletModal } from '@sodax/wallet-sdk-react';
|
|
80
|
+
|
|
81
|
+
const modal = useWalletModal({
|
|
82
|
+
onConnected: async (chainType, account) => {
|
|
83
|
+
// app side-effects after a successful connect
|
|
84
|
+
},
|
|
85
|
+
hydrationTimeoutMs: 5_000, // optional, default 5000ms
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// modal.state — current WalletModalState (discriminated by `kind`)
|
|
89
|
+
// modal.open() — closed → chainSelect
|
|
90
|
+
// modal.close() — any → closed
|
|
91
|
+
// modal.back() — smart back (see below)
|
|
92
|
+
// modal.selectChain(chainType)
|
|
93
|
+
// modal.selectWallet(connector) → Promise<XAccount | undefined>
|
|
94
|
+
// modal.retry() → Promise<XAccount | undefined> — re-runs from `error` state
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The `selectWallet` and `retry` promises **never reject**. Failures populate `state.kind === 'error'` instead — render the error branch and offer a retry button.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Rendering — switch on `state.kind`
|
|
102
|
+
|
|
103
|
+
```tsx
|
|
104
|
+
import { useEffect } from 'react';
|
|
105
|
+
import { useWalletModal } from '@sodax/wallet-sdk-react';
|
|
106
|
+
|
|
107
|
+
// Side effects (closing the modal, navigation, etc.) must run from useEffect, not directly
|
|
108
|
+
// during render — Strict Mode double-invokes renders in dev and would fire the effect twice.
|
|
109
|
+
function AutoClose({ onMount }: { onMount: () => void }) {
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
onMount();
|
|
112
|
+
}, [onMount]);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function WalletModalRoot() {
|
|
117
|
+
const modal = useWalletModal();
|
|
118
|
+
|
|
119
|
+
switch (modal.state.kind) {
|
|
120
|
+
case 'closed':
|
|
121
|
+
return <button onClick={modal.open}>Connect Wallet</button>;
|
|
122
|
+
|
|
123
|
+
case 'chainSelect':
|
|
124
|
+
return (
|
|
125
|
+
<Dialog onClose={modal.close}>
|
|
126
|
+
<ChainList onPick={modal.selectChain} />
|
|
127
|
+
</Dialog>
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
case 'walletSelect':
|
|
131
|
+
return (
|
|
132
|
+
<Dialog onClose={modal.close} onBack={modal.back}>
|
|
133
|
+
<WalletList chainType={modal.state.chainType} onPick={modal.selectWallet} />
|
|
134
|
+
</Dialog>
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
case 'connecting':
|
|
138
|
+
return (
|
|
139
|
+
<Dialog onClose={modal.close} onBack={modal.back}>
|
|
140
|
+
<Spinner connector={modal.state.connector} />
|
|
141
|
+
<p>Approve in {modal.state.connector.name}…</p>
|
|
142
|
+
</Dialog>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
case 'success':
|
|
146
|
+
// Auto-close after onConnected fires.
|
|
147
|
+
return <AutoClose onMount={modal.close} />;
|
|
148
|
+
|
|
149
|
+
case 'error':
|
|
150
|
+
return (
|
|
151
|
+
<Dialog onClose={modal.close} onBack={modal.back}>
|
|
152
|
+
<p>{modal.state.error.message}</p>
|
|
153
|
+
{!modal.state.connector.isInstalled && modal.state.connector.installUrl && (
|
|
154
|
+
<a href={modal.state.connector.installUrl}>Install {modal.state.connector.name}</a>
|
|
155
|
+
)}
|
|
156
|
+
<button onClick={modal.retry}>Retry</button>
|
|
157
|
+
</Dialog>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Render the modal in **one place** at the app root — the state lives in a Zustand slice, so any header CTA or inline button can dispatch `modal.open()` without prop drilling.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## `selectWallet` lifecycle
|
|
168
|
+
|
|
169
|
+
`selectWallet(connector)` runs:
|
|
170
|
+
|
|
171
|
+
1. **Pre-check `connector.isInstalled`** — if false, transition straight to `error` with an install hint. Avoids legacy connectors imperatively opening install pages from inside `connect()` and leaving the state stuck in `connecting`.
|
|
172
|
+
2. **Transition to `connecting`** — sets `state = { kind: 'connecting', chainType, connector }`.
|
|
173
|
+
3. **Call `useXConnect.mutateAsync(connector)`** — opens the wallet popup. Non-provider chains return the `XAccount` directly; provider-managed chains (EVM/Solana/Sui) return `undefined`.
|
|
174
|
+
4. **For provider-managed chains, wait for the Hydrator** — subscribe to `useXWalletStore` and resolve when an `xConnections[chainType]` entry appears whose **`xConnectorId` matches the picked connector's id** (not just any address — see [`Cancellation semantics`](#cancellation-semantics)).
|
|
175
|
+
5. **Transition to `success`** with the resolved account, then fire `onConnected`.
|
|
176
|
+
6. **On error or timeout**, transition to `error`.
|
|
177
|
+
|
|
178
|
+
The promise returned by `selectWallet` resolves with the `XAccount` on success or `undefined` on error/cancellation — never rejects.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Smart `back()` navigation
|
|
183
|
+
|
|
184
|
+
| From | `back()` goes to |
|
|
185
|
+
|------|------------------|
|
|
186
|
+
| `'walletSelect'` | `'chainSelect'` |
|
|
187
|
+
| `'connecting'` | `'walletSelect'` (preserves `chainType`) |
|
|
188
|
+
| `'error'` | `'walletSelect'` (preserves `chainType`) |
|
|
189
|
+
| `'success'` | `'closed'` |
|
|
190
|
+
| `'closed'` / `'chainSelect'` | no-op |
|
|
191
|
+
|
|
192
|
+
`connecting` → back preserves the chain so the user can pick a different wallet without reselecting the chain. The in-flight connect attempt is **dropped** when `back()` fires (see [`Cancellation semantics`](#cancellation-semantics)).
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## `onConnected` side-effect callback
|
|
197
|
+
|
|
198
|
+
Fires once after a successful connect initiated through the modal. Use for app-specific side effects that don't belong in the SDK:
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
const modal = useWalletModal({
|
|
202
|
+
onConnected: async (chainType, account) => {
|
|
203
|
+
await registerIfNew(chainType, account.address);
|
|
204
|
+
if (await needsTosAcceptance(account.address)) {
|
|
205
|
+
router.push('/terms');
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Throwing inside `onConnected` is **non-fatal** — it's logged and the modal stays in `success`. The connection is already persisted in the store; downgrading to `error` would mislead the UI into showing a retry button for a connect that actually succeeded.
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Hydration timeout
|
|
216
|
+
|
|
217
|
+
For provider-managed chains (EVM/Solana/Sui), the connection becomes visible only when the Hydrator observes the native SDK's status flip. `hydrationTimeoutMs` caps how long `selectWallet` waits before transitioning to `error` with `Connection did not complete. Did you close the wallet popup?`.
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
const modal = useWalletModal({
|
|
221
|
+
hydrationTimeoutMs: 15_000, // raise for slow networks / wallets
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Default: `5_000` ms. Ignored for non-provider chains (Bitcoin, ICON, Stellar, NEAR, Stacks, Injective) — those return the account directly from `connect()`.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## WalletConnect QR modal caveat
|
|
230
|
+
|
|
231
|
+
When the user picks an EVM WalletConnect connector, wagmi opens **its own** QR modal as a third-party UI. While that QR modal is visible, `useWalletModal` stays in `connecting` — two dialogs would stack visually.
|
|
232
|
+
|
|
233
|
+
The SDK doesn't bake in a hide policy because partners want different UX. Detect WC and conditionally render `null`:
|
|
234
|
+
|
|
235
|
+
```typescript
|
|
236
|
+
if (
|
|
237
|
+
modal.state.kind === 'connecting' &&
|
|
238
|
+
modal.state.connector.id === 'walletConnect'
|
|
239
|
+
) {
|
|
240
|
+
return null; // let wagmi's QR modal own the screen
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
The wagmi connector id is `'walletConnect'`. Resume rendering when the state transitions to `success` / `error`.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Cancellation semantics
|
|
249
|
+
|
|
250
|
+
The state machine handles three concurrency cases that would otherwise corrupt the UI:
|
|
251
|
+
|
|
252
|
+
**Same connector double-clicked** — `selectWallet(connector)` returns the **same in-flight promise**, so two popups don't open and two state writes don't race.
|
|
253
|
+
|
|
254
|
+
**Different connector picked mid-attempt** — starts a new attempt; the previous attempt's late resolution is dropped. The previous connect's wallet popup may still be open, but the modal won't transition to `success`/`error` for it.
|
|
255
|
+
|
|
256
|
+
**`back()` / `close()` mid-attempt** — the in-flight attempt is cancelled at the modal layer. The wallet may already have approved by the time the user cancels — in that case `xConnections[chainType]` is populated by `useXConnect` / the Hydrator independently, but the modal stays in the user's chosen state (`walletSelect` or `closed`). **The account stays connected.** If full rollback is required, call `useXDisconnect({ xChainType })` from the same handler:
|
|
257
|
+
|
|
258
|
+
```tsx
|
|
259
|
+
const disconnect = useXDisconnect();
|
|
260
|
+
|
|
261
|
+
<button
|
|
262
|
+
onClick={async () => {
|
|
263
|
+
modal.close();
|
|
264
|
+
if (modal.state.kind === 'connecting') {
|
|
265
|
+
await disconnect({ xChainType: modal.state.chainType });
|
|
266
|
+
}
|
|
267
|
+
}}
|
|
268
|
+
>
|
|
269
|
+
Cancel
|
|
270
|
+
</button>
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
The cancellation guard is implemented by reading the store directly (not the React snapshot) so user-driven transitions are observable inside the in-flight callback. See [`useWalletModal.ts`](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/src/hooks/useWalletModal.ts) for the exact `isStillCurrent` check.
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## `useConnectionFlow` — non-modal alternative
|
|
278
|
+
|
|
279
|
+
When you don't need a multi-step modal — just `connect → status → retry` for a single button — use `useConnectionFlow`. It wraps `useXConnect` + `useXDisconnect` and surfaces the error on state instead of throwing.
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
import { useConnectionFlow } from '@sodax/wallet-sdk-react';
|
|
283
|
+
|
|
284
|
+
function ConnectButton({ connector }: { connector: IXConnector }) {
|
|
285
|
+
const { status, error, activeConnector, connect, retry, reset } = useConnectionFlow();
|
|
286
|
+
|
|
287
|
+
if (status === 'error' && error) {
|
|
288
|
+
if (activeConnector && !activeConnector.isInstalled) {
|
|
289
|
+
return <a href={activeConnector.installUrl ?? '#'}>Install {activeConnector.name}</a>;
|
|
290
|
+
}
|
|
291
|
+
return (
|
|
292
|
+
<div>
|
|
293
|
+
<p>{error.message}</p>
|
|
294
|
+
<button onClick={retry}>Retry</button>
|
|
295
|
+
<button onClick={reset}>Dismiss</button>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<button onClick={() => connect(connector)} disabled={status === 'connecting'}>
|
|
302
|
+
{status === 'connecting' ? 'Waiting for wallet…' : `Connect ${connector.name}`}
|
|
303
|
+
</button>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
`useConnectionFlow` returns:
|
|
309
|
+
|
|
310
|
+
| Field | Type | Meaning |
|
|
311
|
+
|-------|------|---------|
|
|
312
|
+
| `status` | `'idle' \| 'connecting' \| 'success' \| 'error'` | Current attempt state |
|
|
313
|
+
| `error` | `Error \| null` | Last failure |
|
|
314
|
+
| `activeConnector` | `XConnector \| null` | Connector of the last attempt |
|
|
315
|
+
| `activeChainType` | `ChainType \| null` | Convenience accessor |
|
|
316
|
+
| `connect(connector)` | `Promise<XAccount \| undefined>` | Connect; never throws |
|
|
317
|
+
| `disconnect({ xChainType })` | `Promise<void>` | Same as `useXDisconnect` |
|
|
318
|
+
| `retry()` | `Promise<XAccount \| undefined>` | Re-runs the last `connect` |
|
|
319
|
+
| `reset()` | `void` | Clears `status` / `error` / `activeConnector` |
|
|
320
|
+
|
|
321
|
+
`connect()` and `retry()` never reject — errors flow into `error` so render code stays linear without `try/catch`.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Related docs
|
|
326
|
+
|
|
327
|
+
- [Connect Flow](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONNECT_FLOW.md) — the underlying `useXConnect` lifecycle
|
|
328
|
+
- [Configure SodaxWalletProvider](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONFIGURE_PROVIDER.md) — opt in chain-type slots before users can pick them
|
|
329
|
+
- [Chain Detection](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CHAIN_DETECTION.md) — `useChainGroups` for the chain picker, `useIsWalletInstalled` for filtering
|
|
330
|
+
- [WalletConnect](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/WALLETCONNECT.md) — `walletConnect` slot config + Fireblocks/custody filters
|
|
331
|
+
- [Connectors](https://github.com/icon-project/sodax-sdks/blob/main/packages/wallet-sdk-react/docs/CONNECTORS.md) — `IXConnector` contract for `selectWallet` argument
|