@zerodev/wallet-react-ui 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +124 -0
  2. package/dist/_types/auth/authStoreSlice.d.ts +33 -0
  3. package/dist/_types/auth/authStoreSlice.d.ts.map +1 -0
  4. package/dist/_types/auth/components/BlobAnimation/index.d.ts +4 -0
  5. package/dist/_types/auth/components/BlobAnimation/index.d.ts.map +1 -0
  6. package/dist/_types/auth/components/CodeInput/index.d.ts +11 -0
  7. package/dist/_types/auth/components/CodeInput/index.d.ts.map +1 -0
  8. package/dist/_types/auth/hooks/useAuth.d.ts +18 -0
  9. package/dist/_types/auth/hooks/useAuth.d.ts.map +1 -0
  10. package/dist/_types/auth/index.d.ts +4 -0
  11. package/dist/_types/auth/index.d.ts.map +1 -0
  12. package/dist/_types/auth/pages/EmailVerification.d.ts +2 -0
  13. package/dist/_types/auth/pages/EmailVerification.d.ts.map +1 -0
  14. package/dist/_types/auth/pages/ErrorScreen.d.ts +9 -0
  15. package/dist/_types/auth/pages/ErrorScreen.d.ts.map +1 -0
  16. package/dist/_types/auth/pages/OtpInput.d.ts +2 -0
  17. package/dist/_types/auth/pages/OtpInput.d.ts.map +1 -0
  18. package/dist/_types/auth/pages/SignUp.d.ts +2 -0
  19. package/dist/_types/auth/pages/SignUp.d.ts.map +1 -0
  20. package/dist/_types/auth/pages/Verifying.d.ts +2 -0
  21. package/dist/_types/auth/pages/Verifying.d.ts.map +1 -0
  22. package/dist/_types/auth/pages/WalletSelection.d.ts +2 -0
  23. package/dist/_types/auth/pages/WalletSelection.d.ts.map +1 -0
  24. package/dist/_types/auth/types.d.ts +12 -0
  25. package/dist/_types/auth/types.d.ts.map +1 -0
  26. package/dist/_types/auth/utils/url.d.ts +3 -0
  27. package/dist/_types/auth/utils/url.d.ts.map +1 -0
  28. package/dist/_types/connector.d.ts +17 -0
  29. package/dist/_types/connector.d.ts.map +1 -0
  30. package/dist/_types/index.d.ts +10 -0
  31. package/dist/_types/index.d.ts.map +1 -0
  32. package/dist/_types/shared/components/PoweredBy/index.d.ts +6 -0
  33. package/dist/_types/shared/components/PoweredBy/index.d.ts.map +1 -0
  34. package/dist/_types/shared/components/Screen/EllipticalRadial.d.ts +23 -0
  35. package/dist/_types/shared/components/Screen/EllipticalRadial.d.ts.map +1 -0
  36. package/dist/_types/shared/components/Screen/MultiRadialBackground.d.ts +4 -0
  37. package/dist/_types/shared/components/Screen/MultiRadialBackground.d.ts.map +1 -0
  38. package/dist/_types/shared/components/Screen/index.d.ts +9 -0
  39. package/dist/_types/shared/components/Screen/index.d.ts.map +1 -0
  40. package/dist/_types/shared/components/SignUpFooter/index.d.ts +8 -0
  41. package/dist/_types/shared/components/SignUpFooter/index.d.ts.map +1 -0
  42. package/dist/_types/shared/components/StatusScreen/index.d.ts +10 -0
  43. package/dist/_types/shared/components/StatusScreen/index.d.ts.map +1 -0
  44. package/dist/_types/shared/components/TopNav/index.d.ts +10 -0
  45. package/dist/_types/shared/components/TopNav/index.d.ts.map +1 -0
  46. package/dist/_types/shared/hooks/useKitStore.d.ts +11 -0
  47. package/dist/_types/shared/hooks/useKitStore.d.ts.map +1 -0
  48. package/dist/_types/shared/utils/common.d.ts +5 -0
  49. package/dist/_types/shared/utils/common.d.ts.map +1 -0
  50. package/dist/_types/shared/utils/store.d.ts +4 -0
  51. package/dist/_types/shared/utils/store.d.ts.map +1 -0
  52. package/dist/_types/store.d.ts +25 -0
  53. package/dist/_types/store.d.ts.map +1 -0
  54. package/dist/_types/types.d.ts +38 -0
  55. package/dist/_types/types.d.ts.map +1 -0
  56. package/dist/blob.webm +0 -0
  57. package/dist/error.webp +0 -0
  58. package/dist/index.cjs +4 -0
  59. package/dist/index.cjs.map +1 -0
  60. package/dist/index.mjs +1374 -0
  61. package/dist/index.mjs.map +1 -0
  62. package/dist/loading.webp +0 -0
  63. package/dist/send.webp +0 -0
  64. package/dist/styles.css +2 -0
  65. package/dist/success.webp +0 -0
  66. package/package.json +96 -0
  67. package/src/assets.d.ts +14 -0
  68. package/src/auth/authStoreSlice.ts +180 -0
  69. package/src/auth/components/BlobAnimation/index.tsx +24 -0
  70. package/src/auth/components/CodeInput/index.tsx +118 -0
  71. package/src/auth/hooks/useAuth.ts +32 -0
  72. package/src/auth/index.tsx +108 -0
  73. package/src/auth/pages/EmailVerification.tsx +76 -0
  74. package/src/auth/pages/ErrorScreen.tsx +53 -0
  75. package/src/auth/pages/OtpInput.tsx +124 -0
  76. package/src/auth/pages/SignUp.tsx +303 -0
  77. package/src/auth/pages/Verifying.tsx +108 -0
  78. package/src/auth/pages/WalletSelection.tsx +53 -0
  79. package/src/auth/types.ts +23 -0
  80. package/src/auth/utils/url.ts +12 -0
  81. package/src/connector.ts +182 -0
  82. package/src/index.ts +25 -0
  83. package/src/shared/components/PoweredBy/index.tsx +26 -0
  84. package/src/shared/components/Screen/EllipticalRadial.tsx +65 -0
  85. package/src/shared/components/Screen/MultiRadialBackground.tsx +323 -0
  86. package/src/shared/components/Screen/index.tsx +65 -0
  87. package/src/shared/components/SignUpFooter/index.tsx +66 -0
  88. package/src/shared/components/StatusScreen/index.tsx +49 -0
  89. package/src/shared/components/TopNav/index.tsx +45 -0
  90. package/src/shared/hooks/useKitStore.ts +20 -0
  91. package/src/shared/utils/common.ts +26 -0
  92. package/src/shared/utils/store.ts +9 -0
  93. package/src/store.ts +44 -0
  94. package/src/types.ts +48 -0
@@ -0,0 +1,303 @@
1
+ import { Button, Icon, Input, ListItem, Text } from '@zerodev/react-ui'
2
+ import {
3
+ useAuthenticateOAuth,
4
+ useLoginPasskey,
5
+ useRegisterPasskey,
6
+ useSendMagicLink,
7
+ useSendOTP,
8
+ } from '@zerodev/wallet-react'
9
+ import { useState } from 'react'
10
+ import { SignUpFooter } from '../../shared/components/SignUpFooter'
11
+ import { isValidEmailAddress } from '../../shared/utils/common'
12
+ import { BlobAnimation } from '../components/BlobAnimation'
13
+ import { useAuth } from '../hooks/useAuth'
14
+
15
+ function isCancellationError(err: unknown): boolean {
16
+ if (!(err instanceof Error)) return false
17
+ // WebAuthn / passkey
18
+ if (err.name === 'AbortError' || err.name === 'NotAllowedError') return true
19
+ // OAuth (existing logic, message-based)
20
+ const msg = err.message.toLowerCase()
21
+ return msg.includes('oauth popup was closed')
22
+ }
23
+
24
+ export function SignUp() {
25
+ const { goToStep, setEmail, setOtpSession, config, enabledMethods } =
26
+ useAuth()
27
+ const [agreedToTerms, setAgreedToTerms] = useState(false)
28
+ const [highlightAgreement, setHighlightAgreement] = useState(false)
29
+ const [emailInput, setEmailInput] = useState('')
30
+ const shouldUseOtp = config?.emailAuthMethod === 'otp'
31
+ const { mutateAsync: sendOtp, isPending: isSendOtpPending } = useSendOTP()
32
+ const { mutateAsync: sendMagicLink, isPending: isSendMagicLinkPending } =
33
+ useSendMagicLink()
34
+ const isEmailLoading = isSendOtpPending || isSendMagicLinkPending
35
+
36
+ const [error, setError] = useState<string | null>(null)
37
+
38
+ const { mutateAsync: authenticateOAuth, isPending: isGoogleLoading } =
39
+ useAuthenticateOAuth({
40
+ mutation: {
41
+ onSuccess: async () => {
42
+ goToStep('authenticated')
43
+ config?.onSuccess?.()
44
+ },
45
+ onError: (err) => {
46
+ config?.onError?.(err)
47
+ },
48
+ },
49
+ })
50
+
51
+ const { mutate: registerPasskey, isPending: isRegisterPasskeyPending } =
52
+ useRegisterPasskey({
53
+ mutation: {
54
+ onSuccess: () => {
55
+ goToStep('authenticated')
56
+ config?.onSuccess?.()
57
+ },
58
+ onError: (err) => {
59
+ if (!isCancellationError(err)) {
60
+ setError(err instanceof Error ? err.message : String(err))
61
+ }
62
+ config?.onError?.(err)
63
+ },
64
+ },
65
+ })
66
+
67
+ const { mutate: loginPasskey, isPending: isLoginPasskeyPending } =
68
+ useLoginPasskey({
69
+ mutation: {
70
+ onSuccess: () => {
71
+ goToStep('authenticated')
72
+ config?.onSuccess?.()
73
+ },
74
+ onError: (err) => {
75
+ if (!isCancellationError(err)) {
76
+ setError(err instanceof Error ? err.message : String(err))
77
+ }
78
+ config?.onError?.(err)
79
+ },
80
+ },
81
+ })
82
+
83
+ const anyPending =
84
+ isGoogleLoading ||
85
+ isEmailLoading ||
86
+ isRegisterPasskeyPending ||
87
+ isLoginPasskeyPending
88
+ const requiresAgreement = !!(
89
+ config?.termsAndConditionsUrl || config?.privacyPolicyUrl
90
+ )
91
+ const needsToAcceptAgreement = requiresAgreement && !agreedToTerms
92
+
93
+ const handleRegisterPasskey = () => {
94
+ if (anyPending) return
95
+ if (needsToAcceptAgreement) {
96
+ setHighlightAgreement(true)
97
+ return
98
+ }
99
+ setError(null)
100
+ registerPasskey()
101
+ }
102
+
103
+ const handleLoginPasskey = () => {
104
+ if (anyPending) return
105
+ if (needsToAcceptAgreement) {
106
+ setHighlightAgreement(true)
107
+ return
108
+ }
109
+ setError(null)
110
+ loginPasskey()
111
+ }
112
+
113
+ const handleGoogleAuth = async () => {
114
+ if (needsToAcceptAgreement) {
115
+ setHighlightAgreement(true)
116
+ return
117
+ }
118
+ setError(null)
119
+ try {
120
+ await authenticateOAuth({ provider: 'google' })
121
+ } catch (err) {
122
+ const message = err instanceof Error ? err.message : String(err)
123
+ if (!isCancellationError(err)) {
124
+ setError(message)
125
+ }
126
+ }
127
+ }
128
+
129
+ const handleEmailOtp = async () => {
130
+ if (!emailInput || anyPending) return
131
+ if (!isValidEmailAddress(emailInput)) return
132
+ if (needsToAcceptAgreement) {
133
+ setHighlightAgreement(true)
134
+ return
135
+ }
136
+
137
+ setError(null)
138
+ try {
139
+ const { otpId, otpEncryptionTargetBundle } = await sendOtp({
140
+ email: emailInput,
141
+ })
142
+ setEmail(emailInput)
143
+ setOtpSession({ otpId, otpEncryptionTargetBundle })
144
+ goToStep('otp-input')
145
+ } catch (err) {
146
+ setError(
147
+ err instanceof Error ? err.message : 'Failed to send verification code',
148
+ )
149
+ }
150
+ }
151
+
152
+ const handleEmailMagicLink = async () => {
153
+ if (!emailInput || anyPending) return
154
+ if (!isValidEmailAddress(emailInput)) return
155
+ if (needsToAcceptAgreement) {
156
+ setHighlightAgreement(true)
157
+ return
158
+ }
159
+
160
+ setError(null)
161
+ try {
162
+ const { otpId, otpEncryptionTargetBundle } = await sendMagicLink({
163
+ email: emailInput,
164
+ })
165
+ setEmail(emailInput)
166
+ setOtpSession({ otpId, otpEncryptionTargetBundle })
167
+ goToStep('email-verification')
168
+ } catch (err) {
169
+ setError(
170
+ err instanceof Error ? err.message : 'Failed to send verification code',
171
+ )
172
+ }
173
+ }
174
+
175
+ const handleEmailSubmit = shouldUseOtp ? handleEmailOtp : handleEmailMagicLink
176
+
177
+ if (error) {
178
+ return (
179
+ <div className="zd:flex zd:items-center zd:justify-center zd:h-full">
180
+ <div className="zd:flex zd:flex-col zd:gap-4 zd:max-w-md">
181
+ <Text className="zd:text-h2 zd:text-center">Error occurred</Text>
182
+ <Text className="zd:text-center zd:text-red-500">{error}</Text>
183
+ <Button
184
+ action="primary"
185
+ text="Try again"
186
+ onClick={() => setError(null)}
187
+ />
188
+ </div>
189
+ </div>
190
+ )
191
+ }
192
+
193
+ return (
194
+ <div className="zd:flex-1 zd:flex zd:flex-col zd:justify-between zd:pb-4 zd:overflow-y-auto zd:overflow-x-hidden">
195
+ <div className="zd:flex-1 zd:flex zd:flex-col zd:justify-center">
196
+ <div className="zd:px-4 zd:flex zd:flex-col zd:items-center">
197
+ <div className="zd:w-full zd:px-16 zd:py-4">
198
+ <BlobAnimation className="zd:w-full zd:pointer-events-none zd:select-none" />
199
+ </div>
200
+ <Text className="zd:text-h2 zd:text-center">
201
+ Continue to your wallet
202
+ </Text>
203
+ <Text className="zd:mt-2 zd:text-center zd:text-greyScale/50">
204
+ Choose a sign-in method to proceed
205
+ </Text>
206
+ </div>
207
+ <div className="zd:mt-6 zd:flex zd:flex-col zd:gap-4 zd:mb-4">
208
+ {enabledMethods.includes('passkey') && (
209
+ <>
210
+ <div className="zd:px-4 zd:flex zd:flex-col zd:gap-2">
211
+ <Button
212
+ action="secondary"
213
+ text="Create a passkey"
214
+ iconName="key"
215
+ trailIcon
216
+ disabled={anyPending}
217
+ onClick={handleRegisterPasskey}
218
+ />
219
+ <Button
220
+ action="secondary"
221
+ text="Log in with passkey"
222
+ iconName="key"
223
+ trailIcon
224
+ disabled={anyPending}
225
+ onClick={handleLoginPasskey}
226
+ />
227
+ </div>
228
+ <div className="zd:flex zd:items-center zd:gap-3">
229
+ <div className="zd:h-px zd:flex-1 zd:bg-greyScale/30" />
230
+ <Text className="zd:text-body3">or</Text>
231
+ <div className="zd:h-px zd:flex-1 zd:bg-greyScale/30" />
232
+ </div>
233
+ </>
234
+ )}
235
+ <div className="zd:px-4 zd:flex zd:flex-col zd:gap-2">
236
+ {enabledMethods.includes('google') && (
237
+ <ListItem
238
+ iconName="google"
239
+ title="Google"
240
+ chevron
241
+ className="zd:rounded-3xl"
242
+ disabled={anyPending}
243
+ onClick={handleGoogleAuth}
244
+ />
245
+ )}
246
+ {enabledMethods.includes('email') && (
247
+ <Input
248
+ iconName="email"
249
+ placeholder="Enter your email"
250
+ value={emailInput}
251
+ onChange={(e) => setEmailInput(e.target.value)}
252
+ type="email"
253
+ autoCapitalize="none"
254
+ autoComplete="email"
255
+ disabled={anyPending}
256
+ variant="listItemStyle"
257
+ containerClassName="zd:rounded-3xl"
258
+ onKeyDown={(e) => {
259
+ if (e.key === 'Enter' && emailInput && !anyPending) {
260
+ void handleEmailSubmit()
261
+ }
262
+ }}
263
+ >
264
+ {isEmailLoading ? (
265
+ <div className="zd:w-13 zd:h-13 zd:flex zd:items-center zd:justify-center">
266
+ <div className="zd:w-5 zd:h-5 zd:border-2 zd:border-solarOrange zd:border-t-transparent zd:rounded-full zd:animate-spin" />
267
+ </div>
268
+ ) : (
269
+ <button
270
+ type="button"
271
+ disabled={
272
+ !isValidEmailAddress(emailInput) || needsToAcceptAgreement
273
+ }
274
+ className={`zd:w-13 zd:h-13 zd:rounded-2xl zd:flex zd:items-center zd:justify-center zd:transition-colors ${
275
+ isValidEmailAddress(emailInput) && !needsToAcceptAgreement
276
+ ? 'zd:cursor-pointer'
277
+ : 'zd:cursor-not-allowed zd:opacity-50'
278
+ }`}
279
+ onClick={() => handleEmailSubmit()}
280
+ >
281
+ <Icon name="chevronRight" className="zd:text-greyScale" />
282
+ </button>
283
+ )}
284
+ </Input>
285
+ )}
286
+ </div>
287
+ </div>
288
+ </div>
289
+ <div className="zd:px-4">
290
+ <SignUpFooter
291
+ termsAndConditionsUrl={config?.termsAndConditionsUrl}
292
+ privacyPolicyUrl={config?.privacyPolicyUrl}
293
+ agreedToTerms={agreedToTerms}
294
+ setAgreedToTerms={(agreed) => {
295
+ setAgreedToTerms(agreed)
296
+ if (agreed) setHighlightAgreement(false)
297
+ }}
298
+ highlight={highlightAgreement}
299
+ />
300
+ </div>
301
+ </div>
302
+ )
303
+ }
@@ -0,0 +1,108 @@
1
+ import { Button } from '@zerodev/react-ui'
2
+ import { useVerifyMagicLink } from '@zerodev/wallet-react'
3
+ import { useEffect, useRef, useState } from 'react'
4
+ import { PoweredBy } from '../../shared/components/PoweredBy'
5
+ import { StatusScreen } from '../../shared/components/StatusScreen'
6
+ import { useAuth } from '../hooks/useAuth'
7
+ import { stripMagicLinkCodeFromUrl } from '../utils/url'
8
+
9
+ function getCodeFromUrl(): string | null {
10
+ if (typeof window === 'undefined') return null
11
+ return new URLSearchParams(window.location.search).get('code')
12
+ }
13
+
14
+ export function Verifying() {
15
+ const {
16
+ otpId,
17
+ otpEncryptionTargetBundle,
18
+ goToStep,
19
+ clearOtpSession,
20
+ config,
21
+ } = useAuth()
22
+ const [code] = useState<string | null>(getCodeFromUrl)
23
+ const [error, setError] = useState<Error | null>(null)
24
+
25
+ // ref to prevent useEffect firing twice in dev's StrictMode
26
+ const hasVerifiedRef = useRef(false)
27
+ const { mutate: verifyMagicLink, isPending: isVerificationLoading } =
28
+ useVerifyMagicLink({
29
+ mutation: {
30
+ onSuccess: async () => {
31
+ clearOtpSession()
32
+ goToStep('authenticated')
33
+ config?.onSuccess?.()
34
+ },
35
+ onError: (err) => {
36
+ setError(err)
37
+ config?.onError?.(err)
38
+ },
39
+ },
40
+ })
41
+
42
+ useEffect(() => {
43
+ if (hasVerifiedRef.current || !code) return
44
+ hasVerifiedRef.current = true
45
+
46
+ // No active OTP session — most commonly the user tapped the same
47
+ // magic link again after a successful verify, which cleared the
48
+ // session. Skip the (doomed) mutation, clean the URL, and drop
49
+ // back to step=null so the host app's own routing handles the
50
+ // already-authenticated user without flashing an error.
51
+ if (!otpId || !otpEncryptionTargetBundle) {
52
+ stripMagicLinkCodeFromUrl()
53
+ goToStep(null)
54
+ return
55
+ }
56
+
57
+ verifyMagicLink({ otpId, code, otpEncryptionTargetBundle })
58
+ }, [otpId, otpEncryptionTargetBundle, code, verifyMagicLink, goToStep])
59
+
60
+ return (
61
+ <>
62
+ <div className="zd:flex-1 zd:flex zd:flex-col zd:gap-8 zd:items-center zd:justify-center">
63
+ {!error && isVerificationLoading && (
64
+ <StatusScreen imageName="loading" title="Verifying Your Email">
65
+ Please wait while we securely connect your wallet.
66
+ </StatusScreen>
67
+ )}
68
+
69
+ {!error && !code && !isVerificationLoading && (
70
+ <>
71
+ <StatusScreen imageName="error" title="Invalid Link">
72
+ This verification link is invalid or incomplete.
73
+ <br />
74
+ Please check your email and try again with the correct link.
75
+ </StatusScreen>
76
+ <Button
77
+ action="primary"
78
+ onClick={() => {
79
+ stripMagicLinkCodeFromUrl()
80
+ goToStep('sign-up')
81
+ }}
82
+ text="Choose another sign-in method"
83
+ />
84
+ </>
85
+ )}
86
+
87
+ {error != null && (
88
+ <>
89
+ <StatusScreen imageName="error" title="Oops, something went wrong">
90
+ We couldn't complete the sign-in process. This could be due to
91
+ timeout, an expired link, or a cancelled request.
92
+ </StatusScreen>
93
+ <Button
94
+ action="primary"
95
+ onClick={() => {
96
+ stripMagicLinkCodeFromUrl()
97
+ goToStep('sign-up')
98
+ }}
99
+ text="Choose another sign-in method"
100
+ />
101
+ </>
102
+ )}
103
+ </div>
104
+
105
+ <PoweredBy className="zd:self-center zd:pt-4 zd:pb-6" />
106
+ </>
107
+ )
108
+ }
@@ -0,0 +1,53 @@
1
+ import { ListItem, Text } from '@zerodev/react-ui'
2
+ import { useConnect } from 'wagmi'
3
+ import { PoweredBy } from '../../shared/components/PoweredBy'
4
+ import { useAuth } from '../hooks/useAuth'
5
+
6
+ export function WalletSelection() {
7
+ const { goToStep } = useAuth()
8
+ const { connect, connectors, isPending } = useConnect()
9
+
10
+ const externalConnectors = connectors.filter((c) => c.id !== 'zerodev-wallet')
11
+
12
+ const handleSelect = (connector: (typeof connectors)[number]) => {
13
+ connect(
14
+ { connector },
15
+ {
16
+ onSuccess: () => {
17
+ goToStep(null)
18
+ },
19
+ },
20
+ )
21
+ }
22
+
23
+ return (
24
+ <>
25
+ <div className="zd:flex-1 zd:flex zd:flex-col zd:gap-8 zd:justify-center">
26
+ <Text className="zd:text-h2 zd:text-center">Select your wallet</Text>
27
+
28
+ <div className="zd:flex zd:flex-col zd:gap-2">
29
+ {externalConnectors.length === 0 ? (
30
+ <Text className="zd:text-center">
31
+ No wallets detected. Install a browser wallet extension to
32
+ continue.
33
+ </Text>
34
+ ) : (
35
+ externalConnectors.map((connector) => (
36
+ <ListItem
37
+ key={connector.uid}
38
+ title={connector.name}
39
+ iconName="walletOutline"
40
+ {...(connector.icon ? { imageUri: connector.icon } : {})}
41
+ disabled={isPending}
42
+ onClick={() => handleSelect(connector)}
43
+ className="zd:rounded-3xl"
44
+ />
45
+ ))
46
+ )}
47
+ </div>
48
+ </div>
49
+
50
+ <PoweredBy className="zd:self-center zd:pt-4 zd:pb-6" />
51
+ </>
52
+ )
53
+ }
@@ -0,0 +1,23 @@
1
+ export type AuthMethod = 'email' | 'google' | 'passkey'
2
+
3
+ export type AuthStep =
4
+ | 'sign-up'
5
+ | 'email-verification'
6
+ | 'otp-input'
7
+ | 'verifying-otp'
8
+ | 'passkey-prompt'
9
+ | 'oauth-in-progress'
10
+ | 'wallet-selection'
11
+ | 'authenticated'
12
+ | 'error'
13
+
14
+ export type EmailAuthMethod = 'magicLink' | 'otp'
15
+
16
+ export interface AuthConfig {
17
+ enabledMethods: AuthMethod[]
18
+ emailAuthMethod?: EmailAuthMethod
19
+ termsAndConditionsUrl?: string
20
+ privacyPolicyUrl?: string
21
+ onSuccess?: () => void
22
+ onError?: (error: unknown) => void
23
+ }
@@ -0,0 +1,12 @@
1
+ export function hasMagicLinkCodeInUrl(): boolean {
2
+ if (typeof window === 'undefined') return false
3
+ return new URLSearchParams(window.location.search).has('code')
4
+ }
5
+
6
+ export function stripMagicLinkCodeFromUrl(): void {
7
+ if (typeof window === 'undefined') return
8
+ const url = new URL(window.location.href)
9
+ if (!url.searchParams.has('code')) return
10
+ url.searchParams.delete('code')
11
+ window.history.replaceState(null, '', url.toString())
12
+ }
@@ -0,0 +1,182 @@
1
+ import type { CreateConnectorFn } from '@wagmi/core'
2
+ import type {
3
+ ZeroDevProvider,
4
+ ZeroDevWalletConnectorParams,
5
+ } from '@zerodev/wallet-react'
6
+ import {
7
+ zeroDevWallet as baseZeroDevWallet,
8
+ NotAuthenticatedError,
9
+ } from '@zerodev/wallet-react'
10
+ import type { ReactNode } from 'react'
11
+ import type { AuthConfig } from './auth/types'
12
+ import { createStore } from './store.js'
13
+ import type { Request, RequestMethod } from './types.js'
14
+
15
+ const DEFAULT_SIGNING_PROMPT_METHODS: RequestMethod[] = [
16
+ 'eth_sendTransaction',
17
+ 'wallet_sendTransaction',
18
+ 'wallet_sendCalls',
19
+ 'personal_sign',
20
+ 'eth_signTypedData_v4',
21
+ ]
22
+
23
+ export type ZeroDevKitConfig = {
24
+ auth?: AuthConfig
25
+ /**
26
+ * Optional brand logo rendered in the auth flow's top nav. When omitted,
27
+ * no logo is shown. `PoweredBy` always shows the ZeroDev mark independently.
28
+ */
29
+ logo?: ReactNode
30
+ }
31
+
32
+ export type ZeroDevKitConnectorParams = ZeroDevWalletConnectorParams & {
33
+ config?: ZeroDevKitConfig
34
+ }
35
+
36
+ function requireUserConfirmation(
37
+ store: ReturnType<typeof createStore>,
38
+ request: Request,
39
+ ): Promise<void> {
40
+ return new Promise<void>((resolve, reject) => {
41
+ store.getState().addPendingRequest({
42
+ id: crypto.randomUUID(),
43
+ ...request,
44
+ resolve,
45
+ reject,
46
+ })
47
+ })
48
+ }
49
+
50
+ /**
51
+ * Resolves when the user completes the auth flow (`step === 'authenticated'`),
52
+ * rejects when they dismiss it (`step === null`).
53
+ */
54
+ function waitForAuthFlow(store: ReturnType<typeof createStore>): Promise<void> {
55
+ return new Promise<void>((resolve, reject) => {
56
+ const unsub = store.subscribe(
57
+ (state) => state.auth.step,
58
+ (step) => {
59
+ if (step === 'authenticated') {
60
+ unsub()
61
+ resolve()
62
+ } else if (step === null) {
63
+ // User dismissed the auth flow (e.g. clicked the TopNav close
64
+ // button) — abort the pending wagmi connect.
65
+ unsub()
66
+ reject(new Error('Auth flow dismissed'))
67
+ }
68
+ },
69
+ )
70
+ })
71
+ }
72
+
73
+ export function zeroDevWallet(
74
+ params: ZeroDevKitConnectorParams,
75
+ ): CreateConnectorFn {
76
+ const baseFactory = baseZeroDevWallet(params)
77
+ const store = createStore({ logo: params.config?.logo })
78
+
79
+ // Initialize auth config if provided
80
+ if (params.config?.auth) {
81
+ store.getState().auth.initialize(params.config.auth)
82
+ }
83
+
84
+ return (wagmiConfig) => {
85
+ const connector = baseFactory(wagmiConfig)
86
+
87
+ return {
88
+ ...connector,
89
+
90
+ async connect(connectParams) {
91
+ // Try to connect first. If a session is persisted, base will load it
92
+ // and resolve — no auth UI needed. If not, it throws
93
+ // `NotAuthenticatedError` and we surface SignUp.
94
+ //
95
+ // This avoids forcing every connect to await the base store's init
96
+ // (which `isAuthorized()` deliberately skips for perf — see
97
+ // packages/react/src/core/connector.ts), and avoids flashing the auth
98
+ // UI on first paint when the session hasn't loaded yet.
99
+ try {
100
+ return await connector.connect(connectParams)
101
+ } catch (error) {
102
+ if (
103
+ connectParams?.isReconnecting ||
104
+ !params.config?.auth ||
105
+ !(error instanceof NotAuthenticatedError)
106
+ ) {
107
+ throw error
108
+ }
109
+
110
+ if (store.getState().auth.step !== null) {
111
+ throw new Error('Auth flow already in progress')
112
+ }
113
+
114
+ store.getState().auth.goToStep('sign-up')
115
+ await waitForAuthFlow(store)
116
+ return connector.connect(connectParams)
117
+ }
118
+ },
119
+
120
+ async disconnect() {
121
+ await connector.disconnect?.()
122
+ if (params.config?.auth) {
123
+ store.getState().auth.reset()
124
+ }
125
+ },
126
+
127
+ async setup() {
128
+ await connector.setup?.()
129
+
130
+ // Request-wrapping is only meaningful in the browser (it gates calls
131
+ // on UI confirmation). Skip during SSR — getProvider() touches
132
+ // `window` for EIP-6963 discovery and would crash on the server.
133
+ if (typeof window === 'undefined') return
134
+
135
+ // Signing is pinned to background mode: the prompt-mode confirmation UI
136
+ // is not part of this package's public surface, so requests always pass
137
+ // through without gating. The wrapping logic below is retained but
138
+ // unreachable.
139
+ // const signing = params.config?.signing
140
+ const signing = { mode: 'background' } as const
141
+ if (signing.mode === 'background') return
142
+
143
+ const methods =
144
+ // (signing?.mode === 'prompt' && signing.methods) ||
145
+ DEFAULT_SIGNING_PROMPT_METHODS
146
+
147
+ const provider = (await connector.getProvider()) as ZeroDevProvider
148
+ const baseRequest = provider.request.bind(provider)
149
+
150
+ // todo: move Request type to core package and use as the method and params type here
151
+ // then we can remove as RequestMethod and spread in return baseRequest
152
+ provider.request = async ({
153
+ method,
154
+ params: reqParams,
155
+ }: {
156
+ method: string
157
+ params?: unknown[]
158
+ }) => {
159
+ const { userConfirmationListenerActive } = store.getState()
160
+ if (
161
+ userConfirmationListenerActive &&
162
+ methods.includes(method as RequestMethod)
163
+ ) {
164
+ await requireUserConfirmation(store, {
165
+ method: method as RequestMethod,
166
+ params: reqParams,
167
+ } as Request)
168
+ }
169
+
170
+ return baseRequest({
171
+ method,
172
+ ...(reqParams && { params: reqParams }),
173
+ })
174
+ }
175
+ },
176
+
177
+ getKitStore() {
178
+ return store
179
+ },
180
+ }
181
+ }
182
+ }