@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.
- package/README.md +38 -7
- package/dist/_types/auth/authStoreSlice.d.ts +31 -0
- package/dist/_types/auth/authStoreSlice.d.ts.map +1 -1
- package/dist/_types/auth/brandAssets.d.ts +2 -0
- package/dist/_types/auth/brandAssets.d.ts.map +1 -0
- package/dist/_types/auth/components/WalletSheet/index.d.ts +20 -0
- package/dist/_types/auth/components/WalletSheet/index.d.ts.map +1 -0
- package/dist/_types/auth/hooks/useAuth.d.ts +5 -0
- package/dist/_types/auth/hooks/useAuth.d.ts.map +1 -1
- package/dist/_types/auth/hooks/useWalletConnectPairing.d.ts +19 -0
- package/dist/_types/auth/hooks/useWalletConnectPairing.d.ts.map +1 -0
- package/dist/_types/auth/hooks/useWalletInfo.d.ts +32 -0
- package/dist/_types/auth/hooks/useWalletInfo.d.ts.map +1 -0
- package/dist/_types/auth/index.d.ts.map +1 -1
- package/dist/_types/auth/pages/SignUp/InstalledWallets.d.ts.map +1 -1
- package/dist/_types/auth/pages/SignUp/MoreWallets.d.ts.map +1 -1
- package/dist/_types/auth/pages/SignUp/Wallet.d.ts.map +1 -1
- package/dist/_types/auth/pages/SignUp/WalletConnect.d.ts +5 -0
- package/dist/_types/auth/pages/SignUp/WalletConnect.d.ts.map +1 -0
- package/dist/_types/auth/pages/SignUp/context.d.ts +4 -0
- package/dist/_types/auth/pages/SignUp/context.d.ts.map +1 -1
- package/dist/_types/auth/pages/SignUp/index.d.ts +2 -0
- package/dist/_types/auth/pages/SignUp/index.d.ts.map +1 -1
- package/dist/_types/auth/pages/Verifying.d.ts.map +1 -1
- package/dist/_types/auth/pages/WalletConnecting.d.ts +11 -0
- package/dist/_types/auth/pages/WalletConnecting.d.ts.map +1 -0
- package/dist/_types/auth/types.d.ts +1 -1
- package/dist/_types/auth/types.d.ts.map +1 -1
- package/dist/_types/auth/utils/isCancellationError.d.ts +4 -0
- package/dist/_types/auth/utils/isCancellationError.d.ts.map +1 -1
- package/dist/_types/auth/utils/isMobile.d.ts +2 -0
- package/dist/_types/auth/utils/isMobile.d.ts.map +1 -0
- package/dist/_types/auth/utils/isResourceUnavailableError.d.ts +12 -0
- package/dist/_types/auth/utils/isResourceUnavailableError.d.ts.map +1 -0
- package/dist/_types/auth/utils/isZeroDevWalletConnect.d.ts +6 -0
- package/dist/_types/auth/utils/isZeroDevWalletConnect.d.ts.map +1 -0
- package/dist/_types/auth/utils/walletDeepLink.d.ts +18 -0
- package/dist/_types/auth/utils/walletDeepLink.d.ts.map +1 -0
- package/dist/_types/auth/walletGuide.d.ts +21 -3
- package/dist/_types/auth/walletGuide.d.ts.map +1 -1
- package/dist/_types/connector.d.ts.map +1 -1
- package/dist/_types/index.d.ts +3 -0
- package/dist/_types/index.d.ts.map +1 -1
- package/dist/_types/zeroDevWalletConnect.d.ts +13 -0
- package/dist/_types/zeroDevWalletConnect.d.ts.map +1 -0
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +1228 -729
- package/dist/index.mjs.map +1 -1
- package/dist/styles.css +1 -1
- package/package.json +5 -4
- package/src/assets.d.ts +5 -0
- package/src/auth/authStoreSlice.ts +72 -3
- package/src/auth/brandAssets.ts +5 -0
- package/src/auth/components/WalletSheet/index.tsx +211 -0
- package/src/auth/hooks/useAuth.ts +7 -0
- package/src/auth/hooks/useWalletConnectPairing.ts +134 -0
- package/src/auth/hooks/useWalletInfo.ts +145 -0
- package/src/auth/index.tsx +3 -0
- package/src/auth/pages/SignUp/InstalledWallets.tsx +18 -28
- package/src/auth/pages/SignUp/MoreWallets.tsx +53 -30
- package/src/auth/pages/SignUp/Wallet.tsx +44 -30
- package/src/auth/pages/SignUp/WalletConnect.tsx +32 -0
- package/src/auth/pages/SignUp/context.tsx +4 -0
- package/src/auth/pages/SignUp/index.tsx +31 -0
- package/src/auth/pages/Verifying.tsx +28 -8
- package/src/auth/pages/WalletConnecting.tsx +237 -0
- package/src/auth/types.ts +1 -0
- package/src/auth/utils/isCancellationError.ts +20 -1
- package/src/auth/utils/isMobile.ts +9 -0
- package/src/auth/utils/isResourceUnavailableError.ts +29 -0
- package/src/auth/utils/isZeroDevWalletConnect.ts +8 -0
- package/src/auth/utils/walletDeepLink.ts +23 -0
- package/src/auth/walletGuide.ts +42 -8
- package/src/connector.ts +13 -4
- package/src/index.ts +7 -2
- package/src/shared/components/StatusScreen/index.tsx +1 -1
- package/src/zeroDevWalletConnect.ts +95 -0
- package/dist/_types/shared/components/TokenSummary/index.d.ts +0 -18
- package/dist/_types/shared/components/TokenSummary/index.d.ts.map +0 -1
- 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
|
@@ -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
|
-
//
|
|
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,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
|
+
}
|
package/src/auth/walletGuide.ts
CHANGED
|
@@ -27,20 +27,53 @@ export type WalletGuideEntry = {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
* A live
|
|
31
|
-
*
|
|
32
|
-
* `
|
|
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: {
|
|
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.
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
-
}
|