@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,145 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
2
|
+
import { useAccount } from 'wagmi'
|
|
3
|
+
import {
|
|
4
|
+
matchesWallet,
|
|
5
|
+
WALLET_GUIDE,
|
|
6
|
+
type WalletGuideEntry,
|
|
7
|
+
type WalletId,
|
|
8
|
+
} from '../walletGuide'
|
|
9
|
+
|
|
10
|
+
/** How the active connection reaches the wallet. */
|
|
11
|
+
export type WalletSource = 'injected' | 'walletconnect' | 'embedded' | 'other'
|
|
12
|
+
|
|
13
|
+
export type WalletInfo = {
|
|
14
|
+
/** Human-readable wallet name ('MetaMask', 'Trust Wallet', …). */
|
|
15
|
+
name?: string | undefined
|
|
16
|
+
/** Wallet icon URL or data: URI, when one is known. */
|
|
17
|
+
icon?: string | undefined
|
|
18
|
+
/** Guide id when the wallet matches the kit's wallet guide. */
|
|
19
|
+
walletId?: WalletId | undefined
|
|
20
|
+
source: WalletSource
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** The slice of `@walletconnect/ethereum-provider` this hook reads. */
|
|
24
|
+
type WcPeerProvider = {
|
|
25
|
+
session?: {
|
|
26
|
+
peer?: {
|
|
27
|
+
metadata?: { name?: string; icons?: readonly string[] }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Guide entry for a WalletConnect peer, matched by name. Peers report their
|
|
34
|
+
* own branding ('MetaMask Wallet', 'Trust Wallet', …), so match by
|
|
35
|
+
* case-insensitive containment in either direction against the guide names.
|
|
36
|
+
*/
|
|
37
|
+
function guideEntryForPeerName(name: string): WalletGuideEntry | undefined {
|
|
38
|
+
const peer = name.toLowerCase()
|
|
39
|
+
return WALLET_GUIDE.find((wallet) => {
|
|
40
|
+
const guide = wallet.name.toLowerCase()
|
|
41
|
+
return peer.includes(guide) || guide.includes(peer)
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function guideEntryForConnector(connector: {
|
|
46
|
+
id: string
|
|
47
|
+
name?: string
|
|
48
|
+
type?: string
|
|
49
|
+
rdns?: string | readonly string[] | undefined
|
|
50
|
+
}): WalletGuideEntry | undefined {
|
|
51
|
+
return WALLET_GUIDE.find((wallet) => matchesWallet(connector, wallet))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function useResolvedWalletInfo(): WalletInfo | undefined {
|
|
55
|
+
const { connector, isConnected } = useAccount()
|
|
56
|
+
const [peerMetadata, setPeerMetadata] = useState<
|
|
57
|
+
{ name?: string; icons?: readonly string[] } | undefined
|
|
58
|
+
>(undefined)
|
|
59
|
+
|
|
60
|
+
const isWalletConnect = !!connector && connector.type === 'walletConnect'
|
|
61
|
+
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!connector || !isWalletConnect) {
|
|
64
|
+
setPeerMetadata(undefined)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
let cancelled = false
|
|
68
|
+
connector
|
|
69
|
+
.getProvider()
|
|
70
|
+
.then((provider) => {
|
|
71
|
+
if (cancelled) return
|
|
72
|
+
const metadata = (provider as WcPeerProvider).session?.peer?.metadata
|
|
73
|
+
setPeerMetadata(metadata)
|
|
74
|
+
})
|
|
75
|
+
.catch(() => {
|
|
76
|
+
// Provider unavailable (torn down mid-flight) — stay unresolved.
|
|
77
|
+
})
|
|
78
|
+
return () => {
|
|
79
|
+
cancelled = true
|
|
80
|
+
}
|
|
81
|
+
}, [connector, isWalletConnect])
|
|
82
|
+
|
|
83
|
+
// Memoized so the identity is reference-stable across renders: consumers
|
|
84
|
+
// put `walletInfo` in effect deps (analytics on wallet change), and a fresh
|
|
85
|
+
// object every render would fire those on every render instead.
|
|
86
|
+
return useMemo((): WalletInfo | undefined => {
|
|
87
|
+
if (!isConnected || !connector) return undefined
|
|
88
|
+
|
|
89
|
+
if (connector.id === 'zerodev-wallet') {
|
|
90
|
+
return {
|
|
91
|
+
name: connector.name,
|
|
92
|
+
icon: connector.icon,
|
|
93
|
+
source: 'embedded',
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isWalletConnect) {
|
|
98
|
+
const entry = peerMetadata?.name
|
|
99
|
+
? guideEntryForPeerName(peerMetadata.name)
|
|
100
|
+
: undefined
|
|
101
|
+
return {
|
|
102
|
+
name: peerMetadata?.name ?? undefined,
|
|
103
|
+
icon: peerMetadata?.icons?.[0] ?? entry?.icon,
|
|
104
|
+
walletId: entry?.id as WalletId | undefined,
|
|
105
|
+
// Identity is about the session, so a raw walletConnect() connector
|
|
106
|
+
// resolves here too — unlike pairing, which is kit-connector-only.
|
|
107
|
+
source: 'walletconnect',
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const entry = guideEntryForConnector(connector)
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
name: connector.name,
|
|
115
|
+
icon: connector.icon ?? entry?.icon,
|
|
116
|
+
walletId: entry?.id as WalletId | undefined,
|
|
117
|
+
source: connector.type === 'injected' ? 'injected' : 'other',
|
|
118
|
+
}
|
|
119
|
+
}, [connector, isConnected, isWalletConnect, peerMetadata])
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Identity of the wallet behind the active wagmi connection, for
|
|
124
|
+
* wallet-specific handling and analytics attribution. Call-compatible with
|
|
125
|
+
* AppKit's `useWalletInfo` — same name, same `{ walletInfo }` return shape,
|
|
126
|
+
* so migrating is an import swap; `walletId` and `source` are kit extras.
|
|
127
|
+
*
|
|
128
|
+
* wagmi's `useAccount().connector` already names injected wallets, but a
|
|
129
|
+
* WalletConnect connection only reports "WalletConnect" — the actual wallet
|
|
130
|
+
* on the other end (Trust, Rainbow, … on a phone) is in the session's peer
|
|
131
|
+
* metadata, which this hook reads from the provider. `walletInfo` is
|
|
132
|
+
* `undefined` while disconnected, and the WalletConnect case resolves
|
|
133
|
+
* asynchronously (briefly `name: undefined` after connect/reload).
|
|
134
|
+
*
|
|
135
|
+
* @param _namespace - Accepted for AppKit call-compatibility ('eip155');
|
|
136
|
+
* ignored — the kit is EVM-only.
|
|
137
|
+
*/
|
|
138
|
+
export function useWalletInfo(_namespace?: string): {
|
|
139
|
+
walletInfo: WalletInfo | undefined
|
|
140
|
+
} {
|
|
141
|
+
const walletInfo = useResolvedWalletInfo()
|
|
142
|
+
// The wrapper is memoized too, so the returned object is stable for
|
|
143
|
+
// consumers that depend on it whole rather than destructuring.
|
|
144
|
+
return useMemo(() => ({ walletInfo }), [walletInfo])
|
|
145
|
+
}
|
package/src/auth/index.tsx
CHANGED
|
@@ -7,6 +7,7 @@ import { ErrorScreen } from './pages/ErrorScreen'
|
|
|
7
7
|
import { OtpInput } from './pages/OtpInput'
|
|
8
8
|
import { SignUp } from './pages/SignUp'
|
|
9
9
|
import { Verifying } from './pages/Verifying'
|
|
10
|
+
import { WalletConnecting } from './pages/WalletConnecting'
|
|
10
11
|
import type { AuthStep } from './types'
|
|
11
12
|
import { hasMagicLinkCodeInUrl, stripMagicLinkCodeFromUrl } from './utils/url'
|
|
12
13
|
|
|
@@ -47,6 +48,8 @@ function renderStep(
|
|
|
47
48
|
return <OAuthCallback />
|
|
48
49
|
case 'passkey-prompt':
|
|
49
50
|
return <PasskeyPrompt />
|
|
51
|
+
case 'wallet-connecting':
|
|
52
|
+
return <WalletConnecting />
|
|
50
53
|
case 'error':
|
|
51
54
|
return <ErrorScreen />
|
|
52
55
|
default:
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Badge, ListItem, ListItemChevron } from '@zerodev/react-ui'
|
|
2
|
-
import {
|
|
2
|
+
import { useConnectors } from 'wagmi'
|
|
3
3
|
import { useAuth } from '../../hooks/useAuth'
|
|
4
|
-
import { isCancellationError } from '../../utils/isCancellationError'
|
|
5
4
|
import { matchesWallet, WALLET_GUIDE } from '../../walletGuide'
|
|
6
|
-
import {
|
|
5
|
+
import { useSignUpContext } from './context'
|
|
7
6
|
|
|
8
7
|
/** Auto-discovered rows for installed wallets: one row per announced (6963)
|
|
9
8
|
* browser extension. Renders nothing when no wallet is installed. */
|
|
@@ -19,12 +18,9 @@ export function SignUpInstalledWallets({
|
|
|
19
18
|
* curated ones. */
|
|
20
19
|
maxWallets?: number
|
|
21
20
|
}) {
|
|
22
|
-
const {
|
|
23
|
-
const { authPending, guardAgreement,
|
|
24
|
-
useSignUpContext()
|
|
21
|
+
const { startWalletConnection } = useAuth()
|
|
22
|
+
const { authPending, guardAgreement, registeredWallets } = useSignUpContext()
|
|
25
23
|
const connectors = useConnectors()
|
|
26
|
-
const { mutate: connect, isPending } = useConnect()
|
|
27
|
-
useReportPending(isPending)
|
|
28
24
|
|
|
29
25
|
// Same rule that earns the INSTALLED badge elsewhere: only a 6963
|
|
30
26
|
// announcement proves a live extension. Announcements surface as
|
|
@@ -56,38 +52,32 @@ export function SignUpInstalledWallets({
|
|
|
56
52
|
.sort((a, b) => a.rank - b.rank)
|
|
57
53
|
.slice(0, maxWallets)
|
|
58
54
|
|
|
59
|
-
|
|
55
|
+
// Hand off to the `wallet-connecting` step, which owns the connect() call.
|
|
56
|
+
const startConnect = (row: (typeof rows)[number]) => {
|
|
60
57
|
if (authPending) return
|
|
61
58
|
if (!guardAgreement()) return
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// embedded-wallet flow is done, so close it (mirrors SignUp.Wallet).
|
|
68
|
-
onSuccess: () => goToStep(null),
|
|
69
|
-
onError: (err) => {
|
|
70
|
-
if (!isCancellationError(err)) {
|
|
71
|
-
setError(err instanceof Error ? err.message : String(err))
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
},
|
|
75
|
-
)
|
|
59
|
+
startWalletConnection({
|
|
60
|
+
connectorUid: row.connector.uid,
|
|
61
|
+
name: row.name,
|
|
62
|
+
icon: row.icon,
|
|
63
|
+
})
|
|
76
64
|
}
|
|
77
65
|
|
|
78
66
|
return (
|
|
79
67
|
<>
|
|
80
|
-
{rows.map((
|
|
68
|
+
{rows.map((row) => (
|
|
81
69
|
<ListItem
|
|
82
|
-
key={connector.uid}
|
|
83
|
-
title={name}
|
|
70
|
+
key={row.connector.uid}
|
|
71
|
+
title={row.name}
|
|
84
72
|
icon={
|
|
85
|
-
icon ?
|
|
73
|
+
row.icon ? (
|
|
74
|
+
<img src={row.icon} alt="" className="zd:w-6 zd:h-6" />
|
|
75
|
+
) : null
|
|
86
76
|
}
|
|
87
77
|
subtitle={<Badge text="INSTALLED" />}
|
|
88
78
|
trailing={<ListItemChevron />}
|
|
89
79
|
disabled={authPending}
|
|
90
|
-
onClick={() => startConnect(
|
|
80
|
+
onClick={() => startConnect(row)}
|
|
91
81
|
/>
|
|
92
82
|
))}
|
|
93
83
|
</>
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { ListItem, ListItemChevron, ListItemIcon } from '@zerodev/react-ui'
|
|
2
2
|
import { useState } from 'react'
|
|
3
|
-
import {
|
|
3
|
+
import { useConnectors } from 'wagmi'
|
|
4
|
+
import { walletConnectLogo } from '../../brandAssets'
|
|
4
5
|
import {
|
|
5
6
|
WalletGridSheet,
|
|
6
7
|
type WalletTileData,
|
|
7
8
|
} from '../../components/WalletGridSheet'
|
|
8
9
|
import { useAuth } from '../../hooks/useAuth'
|
|
9
|
-
import {
|
|
10
|
-
import { matchesWallet, WALLET_GUIDE } from '../../walletGuide'
|
|
11
|
-
import {
|
|
10
|
+
import { isZeroDevWalletConnect } from '../../utils/isZeroDevWalletConnect'
|
|
11
|
+
import { announcesWallet, matchesWallet, WALLET_GUIDE } from '../../walletGuide'
|
|
12
|
+
import { useSignUpContext } from './context'
|
|
12
13
|
|
|
13
14
|
/** "More wallets" row — opens the wallet grid sheet. */
|
|
14
15
|
export function SignUpMoreWallets({
|
|
@@ -16,12 +17,10 @@ export function SignUpMoreWallets({
|
|
|
16
17
|
}: {
|
|
17
18
|
title?: string
|
|
18
19
|
}) {
|
|
19
|
-
const {
|
|
20
|
-
const { authPending, guardAgreement,
|
|
20
|
+
const { startWalletConnection } = useAuth()
|
|
21
|
+
const { authPending, guardAgreement, openWalletSheet } = useSignUpContext()
|
|
21
22
|
const [open, setOpen] = useState(false)
|
|
22
23
|
const connectors = useConnectors()
|
|
23
|
-
const { mutate: connect, isPending } = useConnect()
|
|
24
|
-
useReportPending(isPending)
|
|
25
24
|
|
|
26
25
|
// Our own connector is the embedded wallet, and walletConnect-type
|
|
27
26
|
// connectors are the mobile transport rather than a wallet — neither
|
|
@@ -30,37 +29,45 @@ export function SignUpMoreWallets({
|
|
|
30
29
|
(c) => c.id !== 'zerodev-wallet' && c.type !== 'walletConnect',
|
|
31
30
|
)
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
// Hand off to the `wallet-connecting` step, which owns the connect() call.
|
|
33
|
+
const startConnect = (
|
|
34
|
+
connector: (typeof connectors)[number],
|
|
35
|
+
name: string,
|
|
36
|
+
icon: string | undefined,
|
|
37
|
+
) => {
|
|
34
38
|
if (authPending) return
|
|
35
39
|
if (!guardAgreement()) return
|
|
36
40
|
setOpen(false)
|
|
37
|
-
|
|
38
|
-
connect(
|
|
39
|
-
{ connector },
|
|
40
|
-
{
|
|
41
|
-
// The external wallet is now the active wagmi connection — the
|
|
42
|
-
// embedded-wallet flow is done, so close it.
|
|
43
|
-
onSuccess: () => goToStep(null),
|
|
44
|
-
onError: (err) => {
|
|
45
|
-
if (!isCancellationError(err)) {
|
|
46
|
-
setError(err instanceof Error ? err.message : String(err))
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
)
|
|
41
|
+
startWalletConnection({ connectorUid: connector.uid, name, icon })
|
|
51
42
|
}
|
|
52
43
|
|
|
53
|
-
|
|
54
|
-
|
|
44
|
+
const wcEnabled = connectors.some(isZeroDevWalletConnect)
|
|
45
|
+
|
|
55
46
|
const guideTiles: WalletTileData[] = WALLET_GUIDE.map((wallet) => {
|
|
56
|
-
const
|
|
47
|
+
const announced = walletConnectors.find((c) => announcesWallet(c, wallet))
|
|
57
48
|
return {
|
|
58
49
|
key: wallet.id,
|
|
59
50
|
name: wallet.name,
|
|
60
51
|
icon: wallet.icon,
|
|
61
52
|
onSelect: () => {
|
|
62
|
-
|
|
63
|
-
|
|
53
|
+
// Announced = the wallet is live on this page (extension or its own
|
|
54
|
+
// in-app browser) — connect directly instead of a WC handoff.
|
|
55
|
+
if (announced) {
|
|
56
|
+
startConnect(announced, wallet.name, wallet.icon)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
if (wcEnabled) {
|
|
60
|
+
if (authPending) return
|
|
61
|
+
if (!guardAgreement()) return
|
|
62
|
+
setOpen(false)
|
|
63
|
+
openWalletSheet(wallet)
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
// No WC handoff available — a configured connector that claims the
|
|
67
|
+
// wallet (e.g. a vendor SDK) is the last way to connect.
|
|
68
|
+
const claimed = walletConnectors.find((c) => matchesWallet(c, wallet))
|
|
69
|
+
if (claimed) {
|
|
70
|
+
startConnect(claimed, wallet.name, wallet.icon)
|
|
64
71
|
return
|
|
65
72
|
}
|
|
66
73
|
setOpen(false)
|
|
@@ -77,9 +84,25 @@ export function SignUpMoreWallets({
|
|
|
77
84
|
key: connector.uid,
|
|
78
85
|
name: connector.name,
|
|
79
86
|
icon: connector.icon,
|
|
80
|
-
onSelect: () => startConnect(connector),
|
|
87
|
+
onSelect: () => startConnect(connector, connector.name, connector.icon),
|
|
81
88
|
}))
|
|
82
89
|
|
|
90
|
+
const walletConnectTiles: WalletTileData[] = wcEnabled
|
|
91
|
+
? [
|
|
92
|
+
{
|
|
93
|
+
key: 'walletconnect',
|
|
94
|
+
name: 'WalletConnect',
|
|
95
|
+
icon: walletConnectLogo,
|
|
96
|
+
onSelect: () => {
|
|
97
|
+
if (authPending) return
|
|
98
|
+
if (!guardAgreement()) return
|
|
99
|
+
setOpen(false)
|
|
100
|
+
openWalletSheet()
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
]
|
|
104
|
+
: []
|
|
105
|
+
|
|
83
106
|
const handleClick = () => {
|
|
84
107
|
if (authPending) return
|
|
85
108
|
if (!guardAgreement()) return
|
|
@@ -98,7 +121,7 @@ export function SignUpMoreWallets({
|
|
|
98
121
|
<WalletGridSheet
|
|
99
122
|
open={open}
|
|
100
123
|
onOpenChange={setOpen}
|
|
101
|
-
tiles={[...guideTiles, ...connectorTiles]}
|
|
124
|
+
tiles={[...walletConnectTiles, ...guideTiles, ...connectorTiles]}
|
|
102
125
|
/>
|
|
103
126
|
</>
|
|
104
127
|
)
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { Badge, ListItem, ListItemChevron } from '@zerodev/react-ui'
|
|
2
2
|
import { useLayoutEffect } from 'react'
|
|
3
|
-
import {
|
|
3
|
+
import { useConnectors } from 'wagmi'
|
|
4
4
|
import { useAuth } from '../../hooks/useAuth'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
5
|
+
import { isZeroDevWalletConnect } from '../../utils/isZeroDevWalletConnect'
|
|
6
|
+
import {
|
|
7
|
+
announcesWallet,
|
|
8
|
+
matchesWallet,
|
|
9
|
+
WALLET_GUIDE,
|
|
10
|
+
type WalletId,
|
|
11
|
+
} from '../../walletGuide'
|
|
12
|
+
import { useSignUpContext } from './context'
|
|
8
13
|
|
|
9
14
|
/** Dedicated row for a single guide wallet: connects it directly when a live
|
|
10
15
|
* connector claims it, and is a download link for the vendor page otherwise. */
|
|
@@ -17,20 +22,15 @@ export function SignUpWallet({ walletId }: { walletId: WalletId }) {
|
|
|
17
22
|
)
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
const {
|
|
21
|
-
const { authPending, guardAgreement,
|
|
25
|
+
const { startWalletConnection } = useAuth()
|
|
26
|
+
const { authPending, guardAgreement, registerWallet, openWalletSheet } =
|
|
22
27
|
useSignUpContext()
|
|
23
28
|
const connectors = useConnectors()
|
|
24
|
-
const { mutate: connect, isPending } = useConnect()
|
|
25
|
-
useReportPending(isPending)
|
|
26
29
|
useLayoutEffect(() => registerWallet(wallet.id), [registerWallet, wallet.id])
|
|
27
30
|
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
const installed = connectors.find((c) => matchesWallet(c, wallet))
|
|
32
|
-
const isAnnounced =
|
|
33
|
-
!!wallet.rdns && connectors.some((c) => c.id === wallet.rdns)
|
|
31
|
+
// Only a 6963 announcement proves a live extension (or the wallet's own
|
|
32
|
+
// in-app browser) — it earns the badge and a direct connect.
|
|
33
|
+
const announced = connectors.find((c) => announcesWallet(c, wallet))
|
|
34
34
|
|
|
35
35
|
const shared = {
|
|
36
36
|
title: wallet.name,
|
|
@@ -38,7 +38,29 @@ export function SignUpWallet({ walletId }: { walletId: WalletId }) {
|
|
|
38
38
|
trailing: <ListItemChevron />,
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
// An announcement means the wallet is right here (extension, or its own
|
|
42
|
+
// in-app browser) — connect it directly; WC pairing would bounce out of
|
|
43
|
+
// the very wallet the user is standing in.
|
|
44
|
+
if (!announced && connectors.some(isZeroDevWalletConnect)) {
|
|
45
|
+
return (
|
|
46
|
+
<ListItem
|
|
47
|
+
{...shared}
|
|
48
|
+
disabled={authPending}
|
|
49
|
+
onClick={() => {
|
|
50
|
+
if (authPending) return
|
|
51
|
+
if (!guardAgreement()) return
|
|
52
|
+
openWalletSheet(wallet)
|
|
53
|
+
}}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Same claim rule as the MoreWallets grid: announced, or — without a WC
|
|
59
|
+
// handoff — a configured connector (e.g. a vendor SDK) that claims the
|
|
60
|
+
// wallet.
|
|
61
|
+
const claimed = announced ?? connectors.find((c) => matchesWallet(c, wallet))
|
|
62
|
+
|
|
63
|
+
if (!claimed) {
|
|
42
64
|
return (
|
|
43
65
|
<ListItem {...shared} asChild>
|
|
44
66
|
{/* biome-ignore lint/a11y/useAnchorContent: the row layout (incl. the title text) is injected into the anchor via Slot */}
|
|
@@ -51,29 +73,21 @@ export function SignUpWallet({ walletId }: { walletId: WalletId }) {
|
|
|
51
73
|
)
|
|
52
74
|
}
|
|
53
75
|
|
|
76
|
+
// Hand off to the `wallet-connecting` step, which owns the connect() call.
|
|
54
77
|
const handleClick = () => {
|
|
55
78
|
if (authPending) return
|
|
56
79
|
if (!guardAgreement()) return
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// embedded-wallet flow is done, so close it (mirrors SignUp.MoreWallets).
|
|
63
|
-
onSuccess: () => goToStep(null),
|
|
64
|
-
onError: (err) => {
|
|
65
|
-
if (!isCancellationError(err)) {
|
|
66
|
-
setError(err instanceof Error ? err.message : String(err))
|
|
67
|
-
}
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
)
|
|
80
|
+
startWalletConnection({
|
|
81
|
+
connectorUid: claimed.uid,
|
|
82
|
+
name: wallet.name,
|
|
83
|
+
icon: wallet.icon,
|
|
84
|
+
})
|
|
71
85
|
}
|
|
72
86
|
|
|
73
87
|
return (
|
|
74
88
|
<ListItem
|
|
75
89
|
{...shared}
|
|
76
|
-
{...(
|
|
90
|
+
{...(!!announced && { subtitle: <Badge text="INSTALLED" /> })}
|
|
77
91
|
disabled={authPending}
|
|
78
92
|
onClick={handleClick}
|
|
79
93
|
/>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Badge, ListItem, ListItemChevron } from '@zerodev/react-ui'
|
|
2
|
+
import { useConnectors } from 'wagmi'
|
|
3
|
+
import { walletConnectLogo } from '../../brandAssets'
|
|
4
|
+
import { isZeroDevWalletConnect } from '../../utils/isZeroDevWalletConnect'
|
|
5
|
+
import { useSignUpContext } from './context'
|
|
6
|
+
|
|
7
|
+
/** Generic WalletConnect row — opens the pairing sheet with a raw-URI QR any
|
|
8
|
+
* wallet's scanner can claim. Renders nothing unless a `zeroDevWalletConnect`
|
|
9
|
+
* connector is in the wagmi config. */
|
|
10
|
+
export function SignUpWalletConnect() {
|
|
11
|
+
const { authPending, guardAgreement, openWalletSheet } = useSignUpContext()
|
|
12
|
+
const connectors = useConnectors()
|
|
13
|
+
|
|
14
|
+
if (!connectors.some(isZeroDevWalletConnect)) return null
|
|
15
|
+
|
|
16
|
+
const handleClick = () => {
|
|
17
|
+
if (authPending) return
|
|
18
|
+
if (!guardAgreement()) return
|
|
19
|
+
openWalletSheet()
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<ListItem
|
|
24
|
+
title="WalletConnect"
|
|
25
|
+
icon={<img src={walletConnectLogo} alt="" className="zd:w-6 zd:h-6" />}
|
|
26
|
+
subtitle={<Badge text="QR CODE" />}
|
|
27
|
+
trailing={<ListItemChevron />}
|
|
28
|
+
disabled={authPending}
|
|
29
|
+
onClick={handleClick}
|
|
30
|
+
/>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createContext, useContext, useEffect } from 'react'
|
|
2
2
|
import type { EmailAuthMethod } from '../../types'
|
|
3
|
+
import type { WalletGuideEntry } from '../../walletGuide'
|
|
3
4
|
|
|
4
5
|
export type SignUpContextValue = {
|
|
5
6
|
/** True while any method's auth attempt is in flight — used to disable
|
|
@@ -21,6 +22,9 @@ export type SignUpContextValue = {
|
|
|
21
22
|
registeredWallets: readonly string[]
|
|
22
23
|
/** Returns the unregister function. */
|
|
23
24
|
registerWallet: (walletId: string) => () => void
|
|
25
|
+
/** Opens the root's single WalletSheet — a wallet's connection paths, or
|
|
26
|
+
* the generic WalletConnect pairing when called without one. */
|
|
27
|
+
openWalletSheet: (wallet?: WalletGuideEntry) => void
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
export const SignUpContext = createContext<SignUpContextValue | null>(null)
|
|
@@ -2,7 +2,10 @@ import { Button, Text } from '@zerodev/react-ui'
|
|
|
2
2
|
import { type ReactNode, useCallback, useState } from 'react'
|
|
3
3
|
import { SignUpFooter } from '../../../shared/components/SignUpFooter'
|
|
4
4
|
import { BlobAnimation } from '../../components/BlobAnimation'
|
|
5
|
+
import { WalletSheet } from '../../components/WalletSheet'
|
|
6
|
+
import { useWalletConnectPairing } from '../../hooks/useWalletConnectPairing'
|
|
5
7
|
import type { EmailAuthMethod } from '../../types'
|
|
8
|
+
import type { WalletGuideEntry } from '../../walletGuide'
|
|
6
9
|
import { SignUpContext } from './context'
|
|
7
10
|
import { SignUpEmail } from './Email'
|
|
8
11
|
import { SignUpGoogle } from './Google'
|
|
@@ -10,6 +13,7 @@ import { SignUpInstalledWallets } from './InstalledWallets'
|
|
|
10
13
|
import { SignUpMoreWallets } from './MoreWallets'
|
|
11
14
|
import { SignUpPasskey } from './Passkey'
|
|
12
15
|
import { SignUpWallet } from './Wallet'
|
|
16
|
+
import { SignUpWalletConnect } from './WalletConnect'
|
|
13
17
|
|
|
14
18
|
type SignUpRootProps = {
|
|
15
19
|
children: ReactNode
|
|
@@ -51,6 +55,17 @@ function SignUpRoot({
|
|
|
51
55
|
}
|
|
52
56
|
}, [])
|
|
53
57
|
|
|
58
|
+
// The page's single WalletSheet — every wallet surface opens it via
|
|
59
|
+
// `openWalletSheet` instead of mounting a sheet of its own. The box
|
|
60
|
+
// distinguishes open-without-a-wallet (generic pairing) from closed.
|
|
61
|
+
const [walletSheet, setWalletSheet] = useState<{
|
|
62
|
+
wallet?: WalletGuideEntry | undefined
|
|
63
|
+
} | null>(null)
|
|
64
|
+
// Pairing preloads at page mount so the sheet's QR is ready on open and the
|
|
65
|
+
// mobile deep link below fires synchronously inside the tap — iOS only
|
|
66
|
+
// hands a universal link to the app for gesture-qualified navigations.
|
|
67
|
+
const pairing = useWalletConnectPairing()
|
|
68
|
+
|
|
54
69
|
const requiresAgreement = !!(termsAndConditionsUrl || privacyPolicyUrl)
|
|
55
70
|
const needsAgreement = requiresAgreement && !agreedToTerms
|
|
56
71
|
|
|
@@ -71,6 +86,13 @@ function SignUpRoot({
|
|
|
71
86
|
setError,
|
|
72
87
|
registeredWallets,
|
|
73
88
|
registerWallet,
|
|
89
|
+
openWalletSheet: (wallet) => {
|
|
90
|
+
const deepLink = wallet && pairing.deepLinkFor(wallet)
|
|
91
|
+
if (deepLink) window.location.href = deepLink
|
|
92
|
+
// Sheet opens either way — it's the fallback surface when the
|
|
93
|
+
// redirect doesn't take (and the only surface on desktop).
|
|
94
|
+
setWalletSheet({ wallet })
|
|
95
|
+
},
|
|
74
96
|
}}
|
|
75
97
|
>
|
|
76
98
|
{error !== null && (
|
|
@@ -124,6 +146,14 @@ function SignUpRoot({
|
|
|
124
146
|
/>
|
|
125
147
|
</div>
|
|
126
148
|
</div>
|
|
149
|
+
<WalletSheet
|
|
150
|
+
pairing={pairing}
|
|
151
|
+
open={walletSheet !== null}
|
|
152
|
+
onOpenChange={(open) => {
|
|
153
|
+
if (!open) setWalletSheet(null)
|
|
154
|
+
}}
|
|
155
|
+
wallet={walletSheet?.wallet}
|
|
156
|
+
/>
|
|
127
157
|
</SignUpContext.Provider>
|
|
128
158
|
)
|
|
129
159
|
}
|
|
@@ -158,6 +188,7 @@ export const SignUp = Object.assign(SignUpRoot, {
|
|
|
158
188
|
Google: SignUpGoogle,
|
|
159
189
|
Email: SignUpEmail,
|
|
160
190
|
Wallet: SignUpWallet,
|
|
191
|
+
WalletConnect: SignUpWalletConnect,
|
|
161
192
|
InstalledWallets: SignUpInstalledWallets,
|
|
162
193
|
MoreWallets: SignUpMoreWallets,
|
|
163
194
|
Divider: SignUpDivider,
|
|
@@ -15,6 +15,7 @@ export function Verifying() {
|
|
|
15
15
|
useAuth()
|
|
16
16
|
const [code] = useState<string | null>(getCodeFromUrl)
|
|
17
17
|
const [error, setError] = useState<Error | null>(null)
|
|
18
|
+
const [sessionMissing, setSessionMissing] = useState(false)
|
|
18
19
|
|
|
19
20
|
// ref to prevent useEffect firing twice in dev's StrictMode
|
|
20
21
|
const hasVerifiedRef = useRef(false)
|
|
@@ -35,14 +36,15 @@ export function Verifying() {
|
|
|
35
36
|
if (hasVerifiedRef.current || !code) return
|
|
36
37
|
hasVerifiedRef.current = true
|
|
37
38
|
|
|
38
|
-
// No active OTP session —
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
// already-authenticated
|
|
39
|
+
// No active OTP session — the link was already used, expired, or opened
|
|
40
|
+
// in a browser that never started the email flow. Skip the (doomed)
|
|
41
|
+
// mutation and show the expired-link error below instead of silently
|
|
42
|
+
// dropping the step: a blank screen gives the user nothing to act on.
|
|
43
|
+
// The already-authenticated re-tap case is covered too — hosts route
|
|
44
|
+
// connected users away on their own (wagmi reconnect), so the error only
|
|
45
|
+
// stays up for users who genuinely can't be verified.
|
|
43
46
|
if (!otpId || !otpEncryptionTargetBundle) {
|
|
44
|
-
|
|
45
|
-
goToStep(null)
|
|
47
|
+
setSessionMissing(true)
|
|
46
48
|
return
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -58,7 +60,25 @@ export function Verifying() {
|
|
|
58
60
|
</StatusScreen>
|
|
59
61
|
)}
|
|
60
62
|
|
|
61
|
-
{!error &&
|
|
63
|
+
{!error && sessionMissing && (
|
|
64
|
+
<>
|
|
65
|
+
<StatusScreen imageName="error" title="Link Expired">
|
|
66
|
+
This verification link has expired or was already used.
|
|
67
|
+
<br />
|
|
68
|
+
Please sign in again to request a new one.
|
|
69
|
+
</StatusScreen>
|
|
70
|
+
<Button
|
|
71
|
+
action="primary"
|
|
72
|
+
onClick={() => {
|
|
73
|
+
stripMagicLinkCodeFromUrl()
|
|
74
|
+
goToStep('sign-up')
|
|
75
|
+
}}
|
|
76
|
+
text="Choose another sign-in method"
|
|
77
|
+
/>
|
|
78
|
+
</>
|
|
79
|
+
)}
|
|
80
|
+
|
|
81
|
+
{!error && !sessionMissing && !code && !isVerificationLoading && (
|
|
62
82
|
<>
|
|
63
83
|
<StatusScreen imageName="error" title="Invalid Link">
|
|
64
84
|
This verification link is invalid or incomplete.
|