@zerodev/wallet-react-ui 0.0.11 → 0.0.13

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 (81) hide show
  1. package/README.md +38 -7
  2. package/dist/_types/auth/authStoreSlice.d.ts +31 -0
  3. package/dist/_types/auth/authStoreSlice.d.ts.map +1 -1
  4. package/dist/_types/auth/brandAssets.d.ts +2 -0
  5. package/dist/_types/auth/brandAssets.d.ts.map +1 -0
  6. package/dist/_types/auth/components/WalletSheet/index.d.ts +20 -0
  7. package/dist/_types/auth/components/WalletSheet/index.d.ts.map +1 -0
  8. package/dist/_types/auth/hooks/useAuth.d.ts +5 -0
  9. package/dist/_types/auth/hooks/useAuth.d.ts.map +1 -1
  10. package/dist/_types/auth/hooks/useWalletConnectPairing.d.ts +19 -0
  11. package/dist/_types/auth/hooks/useWalletConnectPairing.d.ts.map +1 -0
  12. package/dist/_types/auth/hooks/useWalletInfo.d.ts +32 -0
  13. package/dist/_types/auth/hooks/useWalletInfo.d.ts.map +1 -0
  14. package/dist/_types/auth/index.d.ts.map +1 -1
  15. package/dist/_types/auth/pages/SignUp/InstalledWallets.d.ts.map +1 -1
  16. package/dist/_types/auth/pages/SignUp/MoreWallets.d.ts.map +1 -1
  17. package/dist/_types/auth/pages/SignUp/Wallet.d.ts.map +1 -1
  18. package/dist/_types/auth/pages/SignUp/WalletConnect.d.ts +5 -0
  19. package/dist/_types/auth/pages/SignUp/WalletConnect.d.ts.map +1 -0
  20. package/dist/_types/auth/pages/SignUp/context.d.ts +4 -0
  21. package/dist/_types/auth/pages/SignUp/context.d.ts.map +1 -1
  22. package/dist/_types/auth/pages/SignUp/index.d.ts +2 -0
  23. package/dist/_types/auth/pages/SignUp/index.d.ts.map +1 -1
  24. package/dist/_types/auth/pages/Verifying.d.ts.map +1 -1
  25. package/dist/_types/auth/pages/WalletConnecting.d.ts +11 -0
  26. package/dist/_types/auth/pages/WalletConnecting.d.ts.map +1 -0
  27. package/dist/_types/auth/types.d.ts +1 -1
  28. package/dist/_types/auth/types.d.ts.map +1 -1
  29. package/dist/_types/auth/utils/isCancellationError.d.ts +4 -0
  30. package/dist/_types/auth/utils/isCancellationError.d.ts.map +1 -1
  31. package/dist/_types/auth/utils/isMobile.d.ts +2 -0
  32. package/dist/_types/auth/utils/isMobile.d.ts.map +1 -0
  33. package/dist/_types/auth/utils/isResourceUnavailableError.d.ts +12 -0
  34. package/dist/_types/auth/utils/isResourceUnavailableError.d.ts.map +1 -0
  35. package/dist/_types/auth/utils/isZeroDevWalletConnect.d.ts +6 -0
  36. package/dist/_types/auth/utils/isZeroDevWalletConnect.d.ts.map +1 -0
  37. package/dist/_types/auth/utils/walletDeepLink.d.ts +18 -0
  38. package/dist/_types/auth/utils/walletDeepLink.d.ts.map +1 -0
  39. package/dist/_types/auth/walletGuide.d.ts +21 -3
  40. package/dist/_types/auth/walletGuide.d.ts.map +1 -1
  41. package/dist/_types/connector.d.ts.map +1 -1
  42. package/dist/_types/index.d.ts +3 -0
  43. package/dist/_types/index.d.ts.map +1 -1
  44. package/dist/_types/zeroDevWalletConnect.d.ts +13 -0
  45. package/dist/_types/zeroDevWalletConnect.d.ts.map +1 -0
  46. package/dist/index.cjs +3 -3
  47. package/dist/index.cjs.map +1 -1
  48. package/dist/index.mjs +1228 -729
  49. package/dist/index.mjs.map +1 -1
  50. package/dist/styles.css +1 -1
  51. package/package.json +5 -4
  52. package/src/assets.d.ts +5 -0
  53. package/src/auth/authStoreSlice.ts +72 -3
  54. package/src/auth/brandAssets.ts +5 -0
  55. package/src/auth/components/WalletSheet/index.tsx +211 -0
  56. package/src/auth/hooks/useAuth.ts +7 -0
  57. package/src/auth/hooks/useWalletConnectPairing.ts +134 -0
  58. package/src/auth/hooks/useWalletInfo.ts +145 -0
  59. package/src/auth/index.tsx +3 -0
  60. package/src/auth/pages/SignUp/InstalledWallets.tsx +18 -28
  61. package/src/auth/pages/SignUp/MoreWallets.tsx +53 -30
  62. package/src/auth/pages/SignUp/Wallet.tsx +44 -30
  63. package/src/auth/pages/SignUp/WalletConnect.tsx +32 -0
  64. package/src/auth/pages/SignUp/context.tsx +4 -0
  65. package/src/auth/pages/SignUp/index.tsx +31 -0
  66. package/src/auth/pages/Verifying.tsx +28 -8
  67. package/src/auth/pages/WalletConnecting.tsx +237 -0
  68. package/src/auth/types.ts +1 -0
  69. package/src/auth/utils/isCancellationError.ts +20 -1
  70. package/src/auth/utils/isMobile.ts +9 -0
  71. package/src/auth/utils/isResourceUnavailableError.ts +29 -0
  72. package/src/auth/utils/isZeroDevWalletConnect.ts +8 -0
  73. package/src/auth/utils/walletDeepLink.ts +23 -0
  74. package/src/auth/walletGuide.ts +42 -8
  75. package/src/connector.ts +13 -4
  76. package/src/index.ts +7 -2
  77. package/src/shared/components/StatusScreen/index.tsx +1 -1
  78. package/src/zeroDevWalletConnect.ts +95 -0
  79. package/dist/_types/shared/components/TokenSummary/index.d.ts +0 -18
  80. package/dist/_types/shared/components/TokenSummary/index.d.ts.map +0 -1
  81. package/src/shared/components/TokenSummary/index.tsx +0 -50
@@ -0,0 +1,237 @@
1
+ import { type Config, connect } from '@wagmi/core'
2
+ import { Button, PoweredBy } from '@zerodev/react-ui'
3
+ import { useEffect, useRef } from 'react'
4
+ import { useConfig, useConnectors } from 'wagmi'
5
+ import { StatusScreen } from '../../shared/components/StatusScreen'
6
+ import { useKitStore } from '../../shared/hooks/useKitStore'
7
+ import type { ConnectFailure } from '../authStoreSlice'
8
+ import { useAuth } from '../hooks/useAuth'
9
+ import { isCancellationError } from '../utils/isCancellationError'
10
+ import { isResourceUnavailableError } from '../utils/isResourceUnavailableError'
11
+
12
+ /**
13
+ * Connector uids with a connect() still open, per wagmi config. Outside React
14
+ * because the request outlives this component, and keyed by connector because
15
+ * several wallets can be left unanswered at once.
16
+ */
17
+ const openRequests = new WeakMap<Config, Set<string>>()
18
+
19
+ function openFor(config: Config): Set<string> {
20
+ let uids = openRequests.get(config)
21
+ if (!uids) {
22
+ uids = new Set()
23
+ openRequests.set(config, uids)
24
+ }
25
+ return uids
26
+ }
27
+
28
+ function describeConnectError(
29
+ err: unknown,
30
+ walletName: string,
31
+ ): ConnectFailure {
32
+ if (isCancellationError(err)) {
33
+ return {
34
+ title: 'Request declined',
35
+ message: `You declined the connection request in ${walletName}.`,
36
+ pending: false,
37
+ }
38
+ }
39
+ if (isResourceUnavailableError(err)) {
40
+ // Usually nothing failed: the wallet still has the first request open
41
+ // and will not prompt again until it is answered there. But -32002 is
42
+ // only "resource unavailable", so suggest the outstanding request rather
43
+ // than assert it. Not the wallet's own message: MetaMask's names RPC
44
+ // methods and the origin ("Request of type 'wallet_requestPermissions'
45
+ // already pending for origin…") — Reown shows its own copy here too.
46
+ return {
47
+ title: `Check ${walletName}`,
48
+ message:
49
+ `Open ${walletName} and check for an outstanding request. ` +
50
+ 'If one is waiting, approve or dismiss it, then try again.',
51
+ pending: true,
52
+ }
53
+ }
54
+ // Wallets throw raw JSON-RPC objects, viem throws Errors with
55
+ // `shortMessage`. Never render "[object Object]": fall back to a fixed
56
+ // sentence plus the code, the only actionable detail such an object has.
57
+ let message: string | undefined
58
+ let code: number | undefined
59
+ if (typeof err === 'string' && err.length > 0) {
60
+ message = err
61
+ } else if (typeof err === 'object' && err !== null) {
62
+ const e = err as {
63
+ shortMessage?: unknown
64
+ message?: unknown
65
+ code?: unknown
66
+ }
67
+ const text = e.shortMessage ?? e.message
68
+ if (typeof text === 'string' && text.length > 0) message = text
69
+ if (typeof e.code === 'number') code = e.code
70
+ }
71
+ return {
72
+ title: 'Couldn’t connect',
73
+ message:
74
+ message ??
75
+ `Something went wrong while connecting to ${walletName}. Please try again.` +
76
+ (code !== undefined ? ` (code ${code})` : ''),
77
+ pending: false,
78
+ }
79
+ }
80
+
81
+ /**
82
+ * The `wallet-connecting` step: shown from picking an external wallet until it
83
+ * answers, which a locked or dismissed wallet never does — so leaving must
84
+ * always be possible.
85
+ *
86
+ * This page owns the connect() call, since React Query drops per-call
87
+ * callbacks when the sign-up page unmounts on the step change. The wallet
88
+ * button only records intent (`startWalletConnection`).
89
+ */
90
+ export function WalletConnecting() {
91
+ const {
92
+ pendingWallet,
93
+ connectError,
94
+ setConnectError,
95
+ clearPendingWallet,
96
+ goToStep,
97
+ } = useAuth()
98
+ const connectors = useConnectors()
99
+ const config = useConfig()
100
+ // Read outside React: the wallet can answer long after this unmounts.
101
+ const store = useKitStore()
102
+
103
+ const connector = pendingWallet
104
+ ? connectors.find((c) => c.uid === pendingWallet.connectorUid)
105
+ : undefined
106
+ const walletName = pendingWallet?.name ?? 'your wallet'
107
+
108
+ // Pop, don't push: pushing would leave `wallet-connecting` in the history,
109
+ // and sign-up's back arrow would remount this page and connect() again.
110
+ const leave = () => {
111
+ const auth = store.getState().auth
112
+ if (auth.stepHistory.length > 0) auth.goBack()
113
+ else auth.goToStep('sign-up')
114
+ }
115
+
116
+ const close = () => {
117
+ clearPendingWallet()
118
+ goToStep(null)
119
+ }
120
+
121
+ const attempt = () => {
122
+ // Starting an attempt means waiting again: drop the previous outcome so
123
+ // Try again shows the wallet rather than the failure it replaces.
124
+ setConnectError(null)
125
+
126
+ // Single-flight per wallet: a wallet rejects a second request rather than
127
+ // re-prompting, so re-adopt the open one — its promise still drives this
128
+ // screen.
129
+ if (connector && openFor(config).has(connector.uid)) return
130
+
131
+ // Connector gone from the config: nothing to connect to.
132
+ if (!connector) {
133
+ clearPendingWallet()
134
+ leave()
135
+ return
136
+ }
137
+ // Already the current connection: connect() would throw rather than
138
+ // prompt (its exact guard), so there is nothing to wait for. A connector
139
+ // that is connected but not current falls through on purpose — connect()
140
+ // then resolves silently and promotes it to current.
141
+ if (config.state.current === connector.uid) {
142
+ close()
143
+ return
144
+ }
145
+
146
+ // @wagmi/core's connect(), not useConnect's mutate: a promise keeps its
147
+ // handlers across remounts (Strict Mode) and after unmount, so a wallet
148
+ // answered once the user left still closes the widget.
149
+ const open = openFor(config)
150
+ open.add(connector.uid)
151
+ connect(config, { connector }).then(
152
+ () => {
153
+ open.delete(connector.uid)
154
+ close()
155
+ },
156
+ (err: unknown) => {
157
+ open.delete(connector.uid)
158
+ // The abandoned prompt is often dismissed much later, by which point
159
+ // the user has moved on. Act only while the flow is still waiting on
160
+ // THIS wallet. (Approval is not gated: the wallet is then connected,
161
+ // so ending the flow is right wherever they are.)
162
+ const auth = store.getState().auth
163
+ if (
164
+ auth.step !== 'wallet-connecting' ||
165
+ auth.pendingWallet?.connectorUid !== connector.uid
166
+ ) {
167
+ return
168
+ }
169
+ setConnectError(describeConnectError(err, walletName))
170
+ },
171
+ )
172
+ }
173
+
174
+ // wagmi can connect without our connect() promise resolving: after a reload
175
+ // the wallet's queued request belongs to a dead page, and approving it
176
+ // authorises the site through the connector's own events. Watch the store so
177
+ // the flow still closes.
178
+ useEffect(() => {
179
+ if (!connector) return
180
+ const uid = connector.uid
181
+ return config.subscribe(
182
+ (state) => (state.status === 'connected' ? state.current : null),
183
+ (currentUid) => {
184
+ if (!currentUid) return
185
+ const connected = config.state.connections.get(currentUid)?.connector
186
+ if (connected?.uid !== uid) return
187
+ const auth = store.getState().auth
188
+ auth.clearPendingWallet()
189
+ auth.goToStep(null)
190
+ },
191
+ )
192
+ }, [connector, config, store])
193
+
194
+ // Once on mount; the ref guards Strict Mode's double effect run.
195
+ const started = useRef(false)
196
+ // biome-ignore lint/correctness/useExhaustiveDependencies: mount-only kick; `attempt` is intentionally the mount-time closure
197
+ useEffect(() => {
198
+ if (started.current) return
199
+ started.current = true
200
+ attempt()
201
+ }, [])
202
+
203
+ return (
204
+ <>
205
+ <div className="zd:flex-1 zd:flex zd:flex-col zd:gap-8 zd:items-center zd:justify-center">
206
+ {connectError === null ? (
207
+ <StatusScreen imageName="loading" title={`Waiting for ${walletName}`}>
208
+ Approve or reject the request in your wallet.
209
+ <br />
210
+ Nothing showing? Open the wallet, the request may be waiting behind
211
+ its unlock screen.
212
+ </StatusScreen>
213
+ ) : (
214
+ <StatusScreen
215
+ imageName={connectError.pending ? 'loading' : 'error'}
216
+ title={connectError.title}
217
+ >
218
+ {connectError.message}
219
+ </StatusScreen>
220
+ )}
221
+ {/* EIP-1193 has no cancel, so the label promises only what it does. */}
222
+ <div className="zd:flex zd:flex-col zd:gap-1">
223
+ {connectError !== null && (
224
+ <Button action="primary" text="Try again" onClick={attempt} />
225
+ )}
226
+ <Button
227
+ action="secondary"
228
+ text="Choose another sign-in method"
229
+ onClick={leave}
230
+ />
231
+ </div>
232
+ </div>
233
+
234
+ <PoweredBy className="zd:self-center zd:pt-4 zd:pb-6" />
235
+ </>
236
+ )
237
+ }
package/src/auth/types.ts CHANGED
@@ -6,6 +6,7 @@ export type AuthStep =
6
6
  | 'otp-input'
7
7
  | 'verifying-otp'
8
8
  | 'passkey-prompt'
9
+ | 'wallet-connecting'
9
10
  | 'oauth-in-progress'
10
11
  | 'authenticated'
11
12
  | 'error'
@@ -1,10 +1,29 @@
1
+ /**
2
+ * The user cancelled, rather than something failed: passkey prompt dismissed,
3
+ * OAuth popup closed, or a wallet connection request rejected.
4
+ */
1
5
  export function isCancellationError(err: unknown): boolean {
6
+ // EIP-1193 `4001` "user rejected", on any object: viem's
7
+ // UserRejectedRequestError, a wallet's own ProviderRpcError (an Error with
8
+ // `code`), or wagmi's rethrown raw JSON-RPC object. Walk `.cause`.
9
+ let current: unknown = err
10
+ for (let depth = 0; depth < 5 && isObject(current); depth++) {
11
+ if (current.code === 4001) return true
12
+ current = current.cause
13
+ }
14
+
2
15
  if (!(err instanceof Error)) return false
3
16
  // WebAuthn / passkey
4
17
  if (err.name === 'AbortError' || err.name === 'NotAllowedError') return true
5
- // EIP-1193 user rejection (external-wallet connect), via viem
18
+ // viem's rejection error when `code` was dropped
6
19
  if (err.name === 'UserRejectedRequestError') return true
7
20
  // OAuth (existing logic, message-based)
8
21
  const msg = err.message.toLowerCase()
9
22
  return msg.includes('oauth popup was closed')
10
23
  }
24
+
25
+ function isObject(
26
+ value: unknown,
27
+ ): value is { code?: unknown; cause?: unknown } {
28
+ return typeof value === 'object' && value !== null
29
+ }
@@ -0,0 +1,9 @@
1
+ export function isMobile() {
2
+ if (typeof window === 'undefined') return false
3
+ return (
4
+ window.matchMedia?.('(pointer: coarse)')?.matches ||
5
+ /Android|webOS|iPhone|iPad|iPod|BlackBerry|Opera Mini/u.test(
6
+ navigator.userAgent,
7
+ )
8
+ )
9
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * EIP-1474 `-32002` "Resource unavailable". Injected wallets send it when a
3
+ * request for this origin is already open (popup closed without answering,
4
+ * then clicked again) and they will not prompt again until it is answered —
5
+ * wagmi's injected connector reads the code the same way ("prompt is already
6
+ * open").
7
+ *
8
+ * viem wraps it as `ResourceUnavailableRpcError`; wagmi's injected connector
9
+ * rethrows the raw JSON-RPC object. Accept both and walk `.cause`.
10
+ */
11
+ export function isResourceUnavailableError(err: unknown): boolean {
12
+ let current: unknown = err
13
+ for (let depth = 0; depth < 5 && isObject(current); depth++) {
14
+ if (
15
+ current.code === -32002 ||
16
+ current.name === 'ResourceUnavailableRpcError'
17
+ ) {
18
+ return true
19
+ }
20
+ current = current.cause
21
+ }
22
+ return false
23
+ }
24
+
25
+ function isObject(
26
+ value: unknown,
27
+ ): value is { code?: unknown; name?: unknown; cause?: unknown } {
28
+ return typeof value === 'object' && value !== null
29
+ }
@@ -0,0 +1,8 @@
1
+ import type { Connector } from 'wagmi'
2
+
3
+ /** True for connectors created by `zeroDevWalletConnect` — the only ones the
4
+ * kit pairs through. A raw `walletConnect()` may have `showQrModal` enabled
5
+ * (its default), which can't be read back, so it's ignored. */
6
+ export function isZeroDevWalletConnect(connector: Connector) {
7
+ return connector.type === 'walletConnect' && 'zdWalletConnect' in connector
8
+ }
@@ -0,0 +1,23 @@
1
+ import { matchesWallet, type WalletGuideEntry } from '../walletGuide'
2
+
3
+ /**
4
+ * Wrapped deep link for the one-tap mobile redirect into `wallet`'s app, or
5
+ * null when the tap should just open the sheet: desktop, a claiming installed
6
+ * connector (direct connect wins), or no pairing URI.
7
+ */
8
+ export function walletDeepLink(params: {
9
+ wallet: WalletGuideEntry
10
+ connectors: readonly {
11
+ id: string
12
+ name?: string
13
+ type?: string
14
+ rdns?: string | readonly string[] | undefined
15
+ }[]
16
+ uri: string | null
17
+ mobile: boolean
18
+ }): string | null {
19
+ const { wallet, connectors, uri, mobile } = params
20
+ if (!wallet.mobileLink || !mobile || !uri) return null
21
+ if (connectors.some((c) => matchesWallet(c, wallet))) return null
22
+ return `${wallet.mobileLink}${encodeURIComponent(uri)}`
23
+ }
@@ -27,20 +27,53 @@ export type WalletGuideEntry = {
27
27
  }
28
28
 
29
29
  /**
30
- * A live connector "claims" a guide wallet by rdns: announced (6963)
31
- * connectors carry `id === rdns`, explicit ones (e.g. `metaMask()`) declare
32
- * `rdns` as a string or array.
30
+ * A live 6963 announcement claiming a guide wallet. Announced connectors
31
+ * carry `id === rdns`; wallets' in-app browsers announce a variant rdns
32
+ * (MetaMask mobile is `io.metamask.mobile`) but keep the wallet's exact
33
+ * name, so an announced connector matching the guide name also counts —
34
+ * the same fallback Reown AppKit and Dynamic use. The generic `injected()`
35
+ * connector (id "injected") and our embedded connector also claim type
36
+ * "injected" without being announcements.
37
+ */
38
+ export function announcesWallet(
39
+ connector: {
40
+ id: string
41
+ name?: string
42
+ type?: string
43
+ rdns?: string | readonly string[] | undefined
44
+ },
45
+ wallet: WalletGuideEntry,
46
+ ): boolean {
47
+ if (!!wallet.rdns && connector.id === wallet.rdns) return true
48
+ return (
49
+ !!wallet.rdns &&
50
+ connector.type === 'injected' &&
51
+ connector.id !== 'injected' &&
52
+ connector.id !== 'zerodev-wallet' &&
53
+ connector.name === wallet.name
54
+ )
55
+ }
56
+
57
+ /**
58
+ * A live connector "claims" a guide wallet: it announces it (see
59
+ * `announcesWallet`), or is an explicit connector (e.g. `metaMask()`)
60
+ * declaring the wallet's rdns as a string or array.
33
61
  */
34
62
  export function matchesWallet(
35
- connector: { id: string; rdns?: string | readonly string[] | undefined },
63
+ connector: {
64
+ id: string
65
+ name?: string
66
+ type?: string
67
+ rdns?: string | readonly string[] | undefined
68
+ },
36
69
  wallet: WalletGuideEntry,
37
70
  ): boolean {
71
+ if (announcesWallet(connector, wallet)) return true
38
72
  return (
39
73
  !!wallet.rdns &&
40
- (connector.id === wallet.rdns ||
41
- (Array.isArray(connector.rdns)
42
- ? connector.rdns.includes(wallet.rdns)
43
- : connector.rdns === wallet.rdns))
74
+ (Array.isArray(connector.rdns)
75
+ ? connector.rdns.includes(wallet.rdns)
76
+ : connector.rdns === wallet.rdns)
44
77
  )
45
78
  }
46
79
 
@@ -81,6 +114,7 @@ const guide = [
81
114
  name: 'Rabby Wallet',
82
115
  rdns: 'io.rabby',
83
116
  icon: 'data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20fill%3D%22none%22%20viewBox%3D%220%200%2028%2028%22%3E%3Cg%20clip-path%3D%22url(%23a)%22%3E%3Cpath%20fill%3D%22%238697FF%22%20d%3D%22M28%200H0v28h28V0Z%22%2F%3E%3Cpath%20fill%3D%22url(%23b)%22%20d%3D%22M22.54%2015.078c.677-1.514-2.673-5.744-5.874-7.506-2.017-1.365-4.12-1.178-4.545-.579-.935%201.316%203.094%202.43%205.788%203.731-.58.252-1.125.703-1.446%201.28-1.004-1.096-3.209-2.04-5.796-1.28-1.743.513-3.191%201.721-3.751%203.546a1.097%201.097%200%201%200-.445%202.1c.112%200%20.463-.075.463-.075l5.612.041c-2.244%203.56-4.018%204.081-4.018%204.698s1.697.45%202.335.22c3.05-1.1%206.327-4.531%206.89-5.519%202.36.295%204.345.33%204.786-.657Z%22%2F%3E%3Cpath%20fill%3D%22url(%23c)%22%20fill-rule%3D%22evenodd%22%20d%3D%22m17.885%2010.713.025.01c.125-.049.105-.233.07-.378-.078-.333-1.438-1.676-2.715-2.277-1.743-.82-3.025-.777-3.212-.398.356.726%201.998%201.408%203.714%202.12.723.3%201.46.606%202.118.923Z%22%20clip-rule%3D%22evenodd%22%2F%3E%3Cpath%20fill%3D%22url(%23d)%22%20fill-rule%3D%22evenodd%22%20d%3D%22M15.701%2018.036a10.296%2010.296%200%200%200-1.2-.37c.482-.862.583-2.138.128-2.945-.639-1.133-1.44-1.736-3.304-1.736-1.024%200-3.783.346-3.832%202.648-.005.242%200%20.464.017.667l5.036.037a17.264%2017.264%200%200%201-1.871%202.483c.669.172%201.221.316%201.728.448.48.125.92.24%201.38.357a21.003%2021.003%200%200%200%201.918-1.59Z%22%20clip-rule%3D%22evenodd%22%2F%3E%3Cpath%20fill%3D%22url(%23e)%22%20d%3D%22M6.848%2016.063c.206%201.75%201.2%202.435%203.232%202.638%202.032.203%203.197.067%204.749.208%201.296.118%202.453.778%202.882.55.386-.205.17-.947-.347-1.423-.67-.617-1.597-1.046-3.229-1.199.325-.89.234-2.138-.27-2.817-.731-.982-2.079-1.426-3.785-1.232-1.782.202-3.49%201.08-3.232%203.275Z%22%2F%3E%3C%2Fg%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22b%22%20x1%3D%2210.464%22%20x2%3D%2222.394%22%20y1%3D%2213.737%22%20y2%3D%2217.12%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20stop-color%3D%22%23fff%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23fff%22%2F%3E%3C%2FlinearGradient%3E%3ClinearGradient%20id%3D%22c%22%20x1%3D%2220.386%22%20x2%3D%2211.779%22%20y1%3D%2213.509%22%20y2%3D%224.879%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20stop-color%3D%22%237258DC%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23797DEA%22%20stop-opacity%3D%220%22%2F%3E%3C%2FlinearGradient%3E%3ClinearGradient%20id%3D%22d%22%20x1%3D%2215.94%22%20x2%3D%227.673%22%20y1%3D%2218.337%22%20y2%3D%2213.584%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20stop-color%3D%22%237461EA%22%2F%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23BFC2FF%22%20stop-opacity%3D%220%22%2F%3E%3C%2FlinearGradient%3E%3ClinearGradient%20id%3D%22e%22%20x1%3D%2211.177%22%20x2%3D%2216.765%22%20y1%3D%2213.648%22%20y2%3D%2220.749%22%20gradientUnits%3D%22userSpaceOnUse%22%3E%3Cstop%20stop-color%3D%22%23fff%22%2F%3E%3Cstop%20offset%3D%22.984%22%20stop-color%3D%22%23D5CEFF%22%2F%3E%3C%2FlinearGradient%3E%3CclipPath%20id%3D%22a%22%3E%3Cpath%20fill%3D%22%23fff%22%20d%3D%22M0%200h28v28H0z%22%2F%3E%3C%2FclipPath%3E%3C%2Fdefs%3E%3C%2Fsvg%3E',
117
+ mobileLink: 'rabby://wc?uri=',
84
118
  downloadUrl: 'https://rabby.io',
85
119
  },
86
120
  {
package/src/connector.ts CHANGED
@@ -106,16 +106,25 @@ export function zeroDevWallet(
106
106
  },
107
107
 
108
108
  async setup() {
109
+ // Restore a persisted OTP session BEFORE base setup so an email flow
110
+ // survives a reload. The restore is a synchronous localStorage read;
111
+ // base setup does async work (wallet creation, session validation)
112
+ // that can be slow or fail. Magic-link verification races on this —
113
+ // `Verifying` reads the session once on mount, and gating the restore
114
+ // behind base setup made it see an empty session and strip the code
115
+ // from the URL. Restoring first runs synchronously inside wagmi's
116
+ // createConfig, before any React effect can observe the store.
117
+ if (typeof window !== 'undefined') {
118
+ store.getState().auth.initialize()
119
+ }
120
+
109
121
  await connector.setup?.()
110
122
 
111
123
  // Everything below is browser-only. Skip during SSR — getProvider()
112
124
  // touches `window` for EIP-6963 discovery and would crash on the
113
- // server, and the session restore reads localStorage.
125
+ // server.
114
126
  if (typeof window === 'undefined') return
115
127
 
116
- // Restore a persisted OTP session so an email flow survives a reload.
117
- store.getState().auth.initialize()
118
-
119
128
  // Signing is pinned to background mode: the prompt-mode confirmation UI
120
129
  // is not part of this package's public surface, so requests always pass
121
130
  // through without gating. The wrapping logic below is retained but
package/src/index.ts CHANGED
@@ -6,17 +6,21 @@
6
6
  // Auth
7
7
  export { ConnectWallet } from './auth'
8
8
  export { useAuth } from './auth/hooks/useAuth'
9
+ // Connected wallet identity (call-compatible with AppKit's useWalletInfo)
10
+ export type {
11
+ WalletInfo,
12
+ WalletSource,
13
+ } from './auth/hooks/useWalletInfo.js'
14
+ export { useWalletInfo } from './auth/hooks/useWalletInfo.js'
9
15
  export { SignUp } from './auth/pages/SignUp'
10
16
  export type { AuthMethod, AuthStep, EmailAuthMethod } from './auth/types'
11
17
  export type { WalletId } from './auth/walletGuide'
12
-
13
18
  // Connector
14
19
  export type {
15
20
  // SigningConfig,
16
21
  ZeroDevKitConnectorParams,
17
22
  } from './connector.js'
18
23
  export { zeroDevWallet } from './connector.js'
19
-
20
24
  // History
21
25
  export {
22
26
  TxHistory,
@@ -24,6 +28,7 @@ export {
24
28
  type TxHistoryStep,
25
29
  } from './history/pages'
26
30
  export type { TxHistoryEntry } from './history/types'
31
+ export { zeroDevWalletConnect } from './zeroDevWalletConnect.js'
27
32
 
28
33
  // Signing
29
34
  // export type { SignatureRequestProps } from './signing'
@@ -34,7 +34,7 @@ export function StatusScreen({
34
34
  <img
35
35
  src={STATE_IMAGES[imageName]}
36
36
  alt={imageName}
37
- className="zd:w-[118px] zd:h-[118px] zd:bg-transparent"
37
+ className="zd:w-[118px] zd:h-[118px] zd:object-contain zd:bg-transparent"
38
38
  />
39
39
  <div className="zd:flex zd:flex-col zd:gap-4 zd:items-center">
40
40
  <Text className="zd:text-h2 zd:text-center zd:whitespace-pre-wrap">
@@ -0,0 +1,95 @@
1
+ import type { CreateConnectorFn } from 'wagmi'
2
+ import { type WalletConnectParameters, walletConnect } from 'wagmi/connectors'
3
+ import { isMobile } from './auth/utils/isMobile'
4
+
5
+ /**
6
+ * localStorage key `@walletconnect/sign-client` reads before publishing each
7
+ * session request — when set, the SDK deep-links back into the chosen wallet
8
+ * app (`{href}/wc?requestId=…&sessionTopic=…`) so the approval prompt is in
9
+ * front of the user. Same key AppKit writes; the SDK no-ops when it's absent.
10
+ */
11
+ const DEEPLINK_CHOICE_KEY = 'WALLETCONNECT_DEEPLINK_CHOICE'
12
+
13
+ /** The slice of `@walletconnect/ethereum-provider` this module touches. */
14
+ type WcSessionProvider = {
15
+ on: (event: 'connect' | 'disconnect', listener: () => void) => unknown
16
+ session?: {
17
+ peer?: {
18
+ metadata?: { name?: string; redirect?: { native?: string } }
19
+ }
20
+ }
21
+ }
22
+
23
+ const watched = new WeakSet<object>()
24
+
25
+ /**
26
+ * Arm the SDK's redirect-on-request with the connected wallet's own
27
+ * registered scheme (`session.peer.metadata.redirect.native`) — authoritative
28
+ * for whichever wallet actually claimed the pairing. Desktop sessions and
29
+ * wallets that register no native redirect stay unarmed.
30
+ */
31
+ function storeDeepLinkChoice(provider: WcSessionProvider) {
32
+ const metadata = provider.session?.peer?.metadata
33
+ const href = metadata?.redirect?.native
34
+ if (!href || !isMobile()) {
35
+ clearDeepLinkChoice()
36
+ return
37
+ }
38
+ try {
39
+ localStorage.setItem(
40
+ DEEPLINK_CHOICE_KEY,
41
+ JSON.stringify({ href, name: metadata?.name ?? '' }),
42
+ )
43
+ } catch {
44
+ // Storage unavailable — requests still work, minus the app hop.
45
+ }
46
+ }
47
+
48
+ function clearDeepLinkChoice() {
49
+ try {
50
+ localStorage.removeItem(DEEPLINK_CHOICE_KEY)
51
+ } catch {
52
+ // Nothing to clear if storage is unavailable.
53
+ }
54
+ }
55
+
56
+ function watchSession(provider: WcSessionProvider) {
57
+ if (watched.has(provider)) return
58
+ watched.add(provider)
59
+ provider.on('connect', () => storeDeepLinkChoice(provider))
60
+ // A stale choice would deep-link a future session to the wrong app.
61
+ provider.on('disconnect', clearDeepLinkChoice)
62
+ // Session restored from storage (page reload) — re-arm without an event.
63
+ if (provider.session) storeDeepLinkChoice(provider)
64
+ }
65
+
66
+ /**
67
+ * WalletConnect connector preconfigured for the kit. `showQrModal: false` is
68
+ * required — the kit renders its own pairing UI, and WalletConnect's bundled
69
+ * Reown modal would pop over it otherwise. All other WalletConnect
70
+ * parameters (e.g. `metadata`) are forwarded as-is.
71
+ *
72
+ * The kit only pairs through connectors created here — a raw `walletConnect()`
73
+ * in the config is ignored, since its `showQrModal` can't be read back.
74
+ */
75
+ export function zeroDevWalletConnect(
76
+ params: Omit<WalletConnectParameters, 'showQrModal'>,
77
+ ): CreateConnectorFn {
78
+ if (!params.projectId) {
79
+ throw new Error('zeroDevWalletConnect requires a WalletConnect projectId')
80
+ }
81
+ const create = walletConnect({ ...params, showQrModal: false })
82
+ return (config) => {
83
+ // Stamp the connector instance so the kit's discovery can tell a
84
+ // correctly-configured connector from a raw walletConnect(), where
85
+ // showQrModal defaults to true.
86
+ const connector = Object.assign(create(config), { zdWalletConnect: true })
87
+ const getProvider = connector.getProvider.bind(connector)
88
+ connector.getProvider = async (...args: Parameters<typeof getProvider>) => {
89
+ const provider = await getProvider(...args)
90
+ watchSession(provider as WcSessionProvider)
91
+ return provider
92
+ }
93
+ return connector
94
+ }
95
+ }
@@ -1,18 +0,0 @@
1
- export interface TokenSummaryProps {
2
- /** Token logo URL, shown in the tile floating over the card's top edge.
3
- * The tile renders empty (neutral background) when omitted. */
4
- tokenLogoUrl?: string;
5
- /** Pre-formatted fiat value, e.g. `"+$170.27"`. */
6
- fiatValue: string;
7
- /** Pre-formatted token amount, e.g. `"0.0652 ETH"`. */
8
- cryptoAmount: string;
9
- className?: string;
10
- }
11
- /**
12
- * Portfolio-value hero (Figma "Portfolio Value", `15873:58532`): a card
13
- * headed by a token logo tile that overhangs the top edge, with the fiat
14
- * value large and the crypto amount beneath. The overhang headroom is part
15
- * of the component, so consumers can stack it without clipping the tile.
16
- */
17
- export declare function TokenSummary({ tokenLogoUrl, fiatValue, cryptoAmount, className, }: TokenSummaryProps): import("react").JSX.Element;
18
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/shared/components/TokenSummary/index.tsx"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IAChC;mEAC+D;IAC/D,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,mDAAmD;IACnD,SAAS,EAAE,MAAM,CAAA;IACjB,uDAAuD;IACvD,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,EAC3B,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,SAAS,GACV,EAAE,iBAAiB,+BAyBnB"}
@@ -1,50 +0,0 @@
1
- import { cn, Text, Wrapper } from '@zerodev/react-ui'
2
-
3
- export interface TokenSummaryProps {
4
- /** Token logo URL, shown in the tile floating over the card's top edge.
5
- * The tile renders empty (neutral background) when omitted. */
6
- tokenLogoUrl?: string
7
- /** Pre-formatted fiat value, e.g. `"+$170.27"`. */
8
- fiatValue: string
9
- /** Pre-formatted token amount, e.g. `"0.0652 ETH"`. */
10
- cryptoAmount: string
11
- className?: string
12
- }
13
-
14
- /**
15
- * Portfolio-value hero (Figma "Portfolio Value", `15873:58532`): a card
16
- * headed by a token logo tile that overhangs the top edge, with the fiat
17
- * value large and the crypto amount beneath. The overhang headroom is part
18
- * of the component, so consumers can stack it without clipping the tile.
19
- */
20
- export function TokenSummary({
21
- tokenLogoUrl,
22
- fiatValue,
23
- cryptoAmount,
24
- className,
25
- }: TokenSummaryProps) {
26
- return (
27
- <div className={cn('zd:relative zd:w-full zd:pt-7', className)}>
28
- <Wrapper
29
- variant="ghost"
30
- className="zd:flex zd:w-full zd:flex-col zd:items-center zd:gap-2 zd:rounded-2xl zd:px-2 zd:pt-16 zd:pb-2"
31
- >
32
- <Text className="zd:whitespace-nowrap zd:text-h1">{fiatValue}</Text>
33
- <Text className="zd:whitespace-nowrap zd:text-center zd:text-h3">
34
- {cryptoAmount}
35
- </Text>
36
- </Wrapper>
37
- {/* Token tile — straddles the card's top edge, centred. */}
38
- <div className="zd:absolute zd:top-0 zd:left-1/2 zd:flex zd:size-18.5 zd:-translate-x-1/2 zd:items-center zd:justify-center zd:rounded-3xl zd:border zd:border-white/20 zd:bg-offWhite/80 zd:backdrop-blur-[15px]">
39
- {tokenLogoUrl && (
40
- <img
41
- src={tokenLogoUrl}
42
- alt=""
43
- aria-hidden
44
- className="zd:size-10.5 zd:object-contain"
45
- />
46
- )}
47
- </div>
48
- </div>
49
- )
50
- }