@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.
Files changed (211) hide show
  1. package/README.md +103 -145
  2. package/ai-exported/AGENTS.md +122 -0
  3. package/ai-exported/integration/README.md +102 -0
  4. package/ai-exported/integration/ai-rules.md +136 -0
  5. package/ai-exported/integration/architecture.md +181 -0
  6. package/ai-exported/integration/examples/01-minimal-evm.tsx +75 -0
  7. package/ai-exported/integration/examples/02-multi-chain-modal.tsx +169 -0
  8. package/ai-exported/integration/examples/03-nextjs-app-router.tsx +99 -0
  9. package/ai-exported/integration/examples/04-walletconnect-setup.tsx +89 -0
  10. package/ai-exported/integration/examples/README.md +29 -0
  11. package/ai-exported/integration/recipes/batch-operations.md +223 -0
  12. package/ai-exported/integration/recipes/bridge-to-sdk.md +164 -0
  13. package/ai-exported/integration/recipes/chain-detection.md +254 -0
  14. package/ai-exported/integration/recipes/connect-button.md +156 -0
  15. package/ai-exported/integration/recipes/multi-chain-modal.md +199 -0
  16. package/ai-exported/integration/recipes/setup.md +158 -0
  17. package/ai-exported/integration/recipes/sign-message.md +137 -0
  18. package/ai-exported/integration/recipes/sub-path-imports.md +95 -0
  19. package/ai-exported/integration/recipes/switch-chain.md +141 -0
  20. package/ai-exported/integration/recipes/walletconnect-setup.md +139 -0
  21. package/ai-exported/integration/reference/api-surface.md +175 -0
  22. package/ai-exported/integration/reference/chain-support.md +78 -0
  23. package/ai-exported/integration/reference/connectors.md +74 -0
  24. package/ai-exported/integration/reference/hooks.md +204 -0
  25. package/ai-exported/integration/reference/wallet-brands.md +106 -0
  26. package/ai-exported/migration/README.md +49 -0
  27. package/ai-exported/migration/ai-rules.md +144 -0
  28. package/ai-exported/migration/breaking-changes.md +305 -0
  29. package/ai-exported/migration/checklist.md +159 -0
  30. package/ai-exported/migration/recipes/connect-button.md +166 -0
  31. package/ai-exported/migration/recipes/multi-chain-modal.md +244 -0
  32. package/ai-exported/migration/recipes/ssr-setup.md +162 -0
  33. package/ai-exported/migration/recipes/walletconnect-migration.md +168 -0
  34. package/ai-exported/migration/reference/components.md +73 -0
  35. package/ai-exported/migration/reference/config.md +307 -0
  36. package/ai-exported/migration/reference/hooks.md +278 -0
  37. package/ai-exported/migration/reference/imports.md +157 -0
  38. package/dist/XConnector-B9YQTVJ4.d.ts +146 -0
  39. package/dist/chunk-2BOUGCJ7.mjs +150 -0
  40. package/dist/chunk-2BOUGCJ7.mjs.map +1 -0
  41. package/dist/chunk-66BAUK56.mjs +202 -0
  42. package/dist/chunk-66BAUK56.mjs.map +1 -0
  43. package/dist/chunk-7ULB6DW4.mjs +102 -0
  44. package/dist/chunk-7ULB6DW4.mjs.map +1 -0
  45. package/dist/chunk-BKJB527E.mjs +125 -0
  46. package/dist/chunk-BKJB527E.mjs.map +1 -0
  47. package/dist/chunk-BXJLBR4G.mjs +88 -0
  48. package/dist/chunk-BXJLBR4G.mjs.map +1 -0
  49. package/dist/chunk-E5IAZ7E6.mjs +186 -0
  50. package/dist/chunk-E5IAZ7E6.mjs.map +1 -0
  51. package/dist/chunk-MAQ47Q52.mjs +33 -0
  52. package/dist/chunk-MAQ47Q52.mjs.map +1 -0
  53. package/dist/chunk-MXZVF5HR.mjs +34 -0
  54. package/dist/chunk-MXZVF5HR.mjs.map +1 -0
  55. package/dist/chunk-N5A2TMF6.mjs +33 -0
  56. package/dist/chunk-N5A2TMF6.mjs.map +1 -0
  57. package/dist/chunk-NY7U7OJW.mjs +64 -0
  58. package/dist/chunk-NY7U7OJW.mjs.map +1 -0
  59. package/dist/chunk-PJLEJVAU.mjs +140 -0
  60. package/dist/chunk-PJLEJVAU.mjs.map +1 -0
  61. package/dist/chunk-PLCA4ZDJ.mjs +1585 -0
  62. package/dist/chunk-PLCA4ZDJ.mjs.map +1 -0
  63. package/dist/chunk-TZMKDXFA.mjs +3 -0
  64. package/dist/chunk-TZMKDXFA.mjs.map +1 -0
  65. package/dist/chunk-X2MHIWXO.mjs +100 -0
  66. package/dist/chunk-X2MHIWXO.mjs.map +1 -0
  67. package/dist/chunk-XZ7CHO2S.mjs +41 -0
  68. package/dist/chunk-XZ7CHO2S.mjs.map +1 -0
  69. package/dist/config-OlnzyEUE.d.ts +146 -0
  70. package/dist/index.cjs +2784 -1594
  71. package/dist/index.cjs.map +1 -1
  72. package/dist/index.d.ts +768 -1498
  73. package/dist/index.mjs +463 -2004
  74. package/dist/index.mjs.map +1 -1
  75. package/dist/xchains/bitcoin/index.cjs +1927 -0
  76. package/dist/xchains/bitcoin/index.cjs.map +1 -0
  77. package/dist/xchains/bitcoin/index.d.ts +125 -0
  78. package/dist/xchains/bitcoin/index.mjs +16 -0
  79. package/dist/xchains/bitcoin/index.mjs.map +1 -0
  80. package/dist/xchains/evm/index.cjs +316 -0
  81. package/dist/xchains/evm/index.cjs.map +1 -0
  82. package/dist/xchains/evm/index.d.ts +39 -0
  83. package/dist/xchains/evm/index.mjs +5 -0
  84. package/dist/xchains/evm/index.mjs.map +1 -0
  85. package/dist/xchains/icon/index.cjs +311 -0
  86. package/dist/xchains/icon/index.cjs.map +1 -0
  87. package/dist/xchains/icon/index.d.ts +37 -0
  88. package/dist/xchains/icon/index.mjs +7 -0
  89. package/dist/xchains/icon/index.mjs.map +1 -0
  90. package/dist/xchains/injective/index.cjs +223 -0
  91. package/dist/xchains/injective/index.cjs.map +1 -0
  92. package/dist/xchains/injective/index.d.ts +35 -0
  93. package/dist/xchains/injective/index.mjs +5 -0
  94. package/dist/xchains/injective/index.mjs.map +1 -0
  95. package/dist/xchains/near/index.cjs +190 -0
  96. package/dist/xchains/near/index.cjs.map +1 -0
  97. package/dist/xchains/near/index.d.ts +34 -0
  98. package/dist/xchains/near/index.mjs +6 -0
  99. package/dist/xchains/near/index.mjs.map +1 -0
  100. package/dist/xchains/solana/index.cjs +186 -0
  101. package/dist/xchains/solana/index.cjs.map +1 -0
  102. package/dist/xchains/solana/index.d.ts +26 -0
  103. package/dist/xchains/solana/index.mjs +7 -0
  104. package/dist/xchains/solana/index.mjs.map +1 -0
  105. package/dist/xchains/stacks/index.cjs +240 -0
  106. package/dist/xchains/stacks/index.cjs.map +1 -0
  107. package/dist/xchains/stacks/index.d.ts +36 -0
  108. package/dist/xchains/stacks/index.mjs +5 -0
  109. package/dist/xchains/stacks/index.mjs.map +1 -0
  110. package/dist/xchains/stellar/index.cjs +322 -0
  111. package/dist/xchains/stellar/index.cjs.map +1 -0
  112. package/dist/xchains/stellar/index.d.ts +44 -0
  113. package/dist/xchains/stellar/index.mjs +6 -0
  114. package/dist/xchains/stellar/index.mjs.map +1 -0
  115. package/dist/xchains/sui/index.cjs +248 -0
  116. package/dist/xchains/sui/index.cjs.map +1 -0
  117. package/dist/xchains/sui/index.d.ts +37 -0
  118. package/dist/xchains/sui/index.mjs +7 -0
  119. package/dist/xchains/sui/index.mjs.map +1 -0
  120. package/docs/ADDING_A_NEW_CHAIN.md +440 -0
  121. package/docs/ARCHITECTURE.md +291 -0
  122. package/docs/BATCH_OPERATIONS.md +267 -0
  123. package/docs/CHAIN_DETECTION.md +216 -0
  124. package/docs/CONFIGURE_PROVIDER.md +360 -0
  125. package/docs/CONNECTORS.md +247 -0
  126. package/docs/CONNECT_FLOW.md +276 -0
  127. package/docs/EVM_SWITCH_CHAIN.md +161 -0
  128. package/docs/SIGN_MESSAGE.md +213 -0
  129. package/docs/SUB_PATH_EXPORTS.md +246 -0
  130. package/docs/WALLETCONNECT.md +154 -0
  131. package/docs/WALLET_MODAL.md +331 -0
  132. package/docs/WALLET_PROVIDER_BRIDGE.md +226 -0
  133. package/package.json +34 -9
  134. package/skills/SKILLS.md +84 -0
  135. package/skills/bridge-to-sdk.md +148 -0
  136. package/skills/connect-button.md +116 -0
  137. package/skills/evm-only-walletconnect.md +111 -0
  138. package/skills/multi-chain-modal.md +178 -0
  139. package/skills/setup.md +107 -0
  140. package/dist/index.d.cts +0 -1579
  141. package/src/Hydrate.ts +0 -65
  142. package/src/SodaxWalletProvider.tsx +0 -97
  143. package/src/actions/getXChainType.ts +0 -8
  144. package/src/actions/getXService.ts +0 -33
  145. package/src/actions/index.ts +0 -2
  146. package/src/assets/wallets/hana.svg +0 -6
  147. package/src/assets/wallets/havah.svg +0 -76
  148. package/src/assets/wallets/keplr.svg +0 -30
  149. package/src/assets/wallets/metamask.svg +0 -60
  150. package/src/assets/wallets/phantom.svg +0 -4
  151. package/src/assets/wallets/sui.svg +0 -20
  152. package/src/core/XConnector.ts +0 -54
  153. package/src/core/XService.ts +0 -85
  154. package/src/core/index.ts +0 -2
  155. package/src/hooks/index.ts +0 -11
  156. package/src/hooks/useEthereumChainId.ts +0 -44
  157. package/src/hooks/useEvmSwitchChain.ts +0 -91
  158. package/src/hooks/useWalletProvider.ts +0 -206
  159. package/src/hooks/useXAccount.ts +0 -51
  160. package/src/hooks/useXAccounts.ts +0 -56
  161. package/src/hooks/useXBalances.ts +0 -65
  162. package/src/hooks/useXConnect.ts +0 -118
  163. package/src/hooks/useXConnection.ts +0 -72
  164. package/src/hooks/useXConnectors.ts +0 -72
  165. package/src/hooks/useXDisconnect.ts +0 -73
  166. package/src/hooks/useXService.ts +0 -8
  167. package/src/hooks/useXSignMessage.ts +0 -82
  168. package/src/index.ts +0 -19
  169. package/src/types/index.ts +0 -22
  170. package/src/useXWagmiStore.ts +0 -116
  171. package/src/utils/index.ts +0 -21
  172. package/src/xchains/bitcoin/BitcoinXConnector.ts +0 -34
  173. package/src/xchains/bitcoin/BitcoinXService.ts +0 -40
  174. package/src/xchains/bitcoin/OKXXConnector.ts +0 -117
  175. package/src/xchains/bitcoin/UnisatXConnector.ts +0 -117
  176. package/src/xchains/bitcoin/XverseXConnector.ts +0 -232
  177. package/src/xchains/bitcoin/index.ts +0 -7
  178. package/src/xchains/bitcoin/useBitcoinXConnectors.ts +0 -14
  179. package/src/xchains/evm/EvmXConnector.ts +0 -27
  180. package/src/xchains/evm/EvmXService.ts +0 -211
  181. package/src/xchains/evm/index.ts +0 -3
  182. package/src/xchains/icon/IconHanaXConnector.ts +0 -39
  183. package/src/xchains/icon/IconXService.ts +0 -117
  184. package/src/xchains/icon/actions.ts +0 -28
  185. package/src/xchains/icon/iconex/index.tsx +0 -46
  186. package/src/xchains/icon/index.ts +0 -2
  187. package/src/xchains/injective/InjectiveXConnector.ts +0 -60
  188. package/src/xchains/injective/InjectiveXService.ts +0 -62
  189. package/src/xchains/injective/actions.ts +0 -32
  190. package/src/xchains/injective/index.ts +0 -2
  191. package/src/xchains/near/NearXConnector.ts +0 -42
  192. package/src/xchains/near/NearXService.ts +0 -46
  193. package/src/xchains/near/useNearXConnectors.ts +0 -23
  194. package/src/xchains/solana/SolanaXConnector.ts +0 -26
  195. package/src/xchains/solana/SolanaXService.ts +0 -46
  196. package/src/xchains/solana/index.ts +0 -2
  197. package/src/xchains/stacks/StacksXConnector.ts +0 -63
  198. package/src/xchains/stacks/StacksXService.ts +0 -59
  199. package/src/xchains/stacks/constants.ts +0 -42
  200. package/src/xchains/stacks/index.ts +0 -4
  201. package/src/xchains/stacks/useStacksXConnectors.ts +0 -7
  202. package/src/xchains/stellar/CustomSorobanServer.ts +0 -93
  203. package/src/xchains/stellar/StellarWalletsKitXConnector.ts +0 -53
  204. package/src/xchains/stellar/StellarXService.ts +0 -93
  205. package/src/xchains/stellar/actions.ts +0 -24
  206. package/src/xchains/stellar/index.tsx +0 -2
  207. package/src/xchains/stellar/useStellarXConnectors.ts +0 -21
  208. package/src/xchains/stellar/utils.ts +0 -49
  209. package/src/xchains/sui/SuiXConnector.ts +0 -28
  210. package/src/xchains/sui/SuiXService.ts +0 -66
  211. 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