@zerodev/wallet-react 0.0.1-alpha.5 → 0.0.1-alpha.7

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 (38) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +17 -14
  3. package/dist/_cjs/actions.js +53 -24
  4. package/dist/_cjs/connector.js +44 -3
  5. package/dist/_cjs/hooks/useExportPrivateKey.js +18 -0
  6. package/dist/_cjs/hooks/useGetUserEmail.js +19 -0
  7. package/dist/_cjs/index.js +8 -1
  8. package/dist/_cjs/oauth.js +60 -55
  9. package/dist/_esm/actions.js +65 -27
  10. package/dist/_esm/connector.js +56 -5
  11. package/dist/_esm/hooks/useExportPrivateKey.js +18 -0
  12. package/dist/_esm/hooks/useGetUserEmail.js +19 -0
  13. package/dist/_esm/index.js +3 -1
  14. package/dist/_esm/oauth.js +71 -53
  15. package/dist/_types/actions.d.ts +41 -6
  16. package/dist/_types/actions.d.ts.map +1 -1
  17. package/dist/_types/connector.d.ts +0 -2
  18. package/dist/_types/connector.d.ts.map +1 -1
  19. package/dist/_types/hooks/useExportPrivateKey.d.ts +18 -0
  20. package/dist/_types/hooks/useExportPrivateKey.d.ts.map +1 -0
  21. package/dist/_types/hooks/useGetUserEmail.d.ts +20 -0
  22. package/dist/_types/hooks/useGetUserEmail.d.ts.map +1 -0
  23. package/dist/_types/index.d.ts +4 -2
  24. package/dist/_types/index.d.ts.map +1 -1
  25. package/dist/_types/oauth.d.ts +25 -12
  26. package/dist/_types/oauth.d.ts.map +1 -1
  27. package/dist/_types/store.d.ts +7 -3
  28. package/dist/_types/store.d.ts.map +1 -1
  29. package/dist/tsconfig.build.tsbuildinfo +1 -1
  30. package/package.json +2 -2
  31. package/src/actions.ts +122 -44
  32. package/src/connector.ts +68 -7
  33. package/src/hooks/useExportPrivateKey.ts +57 -0
  34. package/src/hooks/useGetUserEmail.ts +54 -0
  35. package/src/index.ts +9 -2
  36. package/src/oauth.ts +97 -78
  37. package/src/store.ts +9 -4
  38. package/tsconfig.build.tsbuildinfo +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zerodev/wallet-react",
3
- "version": "0.0.1-alpha.5",
3
+ "version": "0.0.1-alpha.7",
4
4
  "description": "React hooks for ZeroDev Wallet SDK",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/_cjs/index.js",
@@ -30,7 +30,7 @@
30
30
  "wagmi": "^3.0.0",
31
31
  "zustand": "^5.0.3",
32
32
  "ox": "^0.3.0",
33
- "@zerodev/wallet-core": "0.0.1-alpha.5"
33
+ "@zerodev/wallet-core": "0.0.1-alpha.7"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/react": "^19",
package/src/actions.ts CHANGED
@@ -1,11 +1,15 @@
1
1
  import type { Config, Connector } from '@wagmi/core'
2
2
  import { connect as wagmiConnect } from '@wagmi/core/actions'
3
+ import {
4
+ createIframeStamper,
5
+ exportPrivateKey as exportPrivateKeySdk,
6
+ exportWallet as exportWalletSdk,
7
+ } from '@zerodev/wallet-core'
3
8
  import type { OAuthProvider } from './oauth.js'
4
9
  import {
5
- buildOAuthUrl,
6
- generateOAuthNonce,
10
+ buildBackendOAuthUrl,
11
+ listenForOAuthMessage,
7
12
  openOAuthPopup,
8
- pollOAuthPopup,
9
13
  } from './oauth.js'
10
14
 
11
15
  /**
@@ -111,12 +115,12 @@ export declare namespace loginPasskey {
111
115
 
112
116
  /**
113
117
  * Authenticate with OAuth (opens popup)
118
+ * Uses backend OAuth flow where the backend handles PKCE and token exchange
114
119
  */
115
120
  export async function authenticateOAuth(
116
121
  config: Config,
117
122
  parameters: {
118
123
  provider: OAuthProvider
119
- clientId?: string
120
124
  connector?: Connector
121
125
  },
122
126
  ): Promise<void> {
@@ -129,38 +133,23 @@ export async function authenticateOAuth(
129
133
 
130
134
  if (!wallet) throw new Error('Wallet not initialized')
131
135
  if (!oauthConfig) {
132
- throw new Error(
133
- 'OAuth is not configured. Please provide oauthConfig to zeroDevWallet connector.',
134
- )
135
- }
136
-
137
- // Get client ID for the provider
138
- let clientId = parameters.clientId
139
- if (!clientId) {
140
- clientId = oauthConfig.googleClientId
141
- }
142
-
143
- if (!clientId) {
144
- throw new Error(`Client ID not configured for ${parameters.provider}`)
136
+ throw new Error('Wallet not initialized. Please wait for connector setup.')
145
137
  }
146
138
 
147
- if (!oauthConfig.redirectUri) {
148
- throw new Error('OAuth redirect URI is not configured.')
149
- }
150
-
151
- // Generate nonce from wallet public key
139
+ // Get wallet public key for the OAuth flow
152
140
  const publicKey = await wallet.getPublicKey()
153
141
  if (!publicKey) {
154
142
  throw new Error('Failed to get wallet public key')
155
143
  }
156
- const nonce = generateOAuthNonce(publicKey)
157
144
 
158
- // Build OAuth URL
159
- const oauthUrl = buildOAuthUrl({
145
+ // Build OAuth URL that redirects to backend
146
+ // Use current origin as redirect - SDK auto-detects callback on any page
147
+ const oauthUrl = buildBackendOAuthUrl({
160
148
  provider: parameters.provider,
161
- clientId,
162
- redirectUri: oauthConfig.redirectUri,
163
- nonce,
149
+ backendUrl: oauthConfig.backendUrl,
150
+ projectId: oauthConfig.projectId,
151
+ publicKey,
152
+ returnTo: `${window.location.origin}?oauth_success=true&oauth_provider=${parameters.provider}`,
164
153
  })
165
154
 
166
155
  // Open popup
@@ -170,18 +159,18 @@ export async function authenticateOAuth(
170
159
  throw new Error(`Failed to open ${parameters.provider} login window.`)
171
160
  }
172
161
 
173
- // Poll for OAuth completion
162
+ // Listen for OAuth completion via postMessage
174
163
  return new Promise<void>((resolve, reject) => {
175
- pollOAuthPopup(
164
+ const cleanup = listenForOAuthMessage(
176
165
  authWindow,
177
166
  window.location.origin,
178
- async (idToken) => {
167
+ async () => {
179
168
  try {
180
169
  // Complete OAuth authentication with wallet-core
170
+ // The backend has stored the OAuth session in a cookie
181
171
  await wallet.auth({
182
172
  type: 'oauth',
183
173
  provider: parameters.provider,
184
- credential: idToken,
185
174
  })
186
175
 
187
176
  const [session, eoaAccount] = await Promise.all([
@@ -200,7 +189,10 @@ export async function authenticateOAuth(
200
189
  reject(err)
201
190
  }
202
191
  },
203
- reject,
192
+ (error) => {
193
+ cleanup()
194
+ reject(error)
195
+ },
204
196
  )
205
197
  })
206
198
  }
@@ -208,7 +200,6 @@ export async function authenticateOAuth(
208
200
  export declare namespace authenticateOAuth {
209
201
  type Parameters = {
210
202
  provider: OAuthProvider
211
- clientId?: string
212
203
  connector?: Connector
213
204
  }
214
205
  type ReturnType = void
@@ -225,7 +216,7 @@ export async function sendOTP(
225
216
  emailCustomization?: { magicLinkTemplate?: string }
226
217
  connector?: Connector
227
218
  },
228
- ): Promise<{ otpId: string; subOrganizationId: string }> {
219
+ ): Promise<{ otpId: string }> {
229
220
  const connector = parameters.connector ?? getZeroDevConnector(config)
230
221
 
231
222
  // @ts-expect-error - getStore is a custom method
@@ -246,7 +237,6 @@ export async function sendOTP(
246
237
 
247
238
  return {
248
239
  otpId: result.otpId,
249
- subOrganizationId: result.subOrganizationId,
250
240
  }
251
241
  }
252
242
 
@@ -256,7 +246,7 @@ export declare namespace sendOTP {
256
246
  emailCustomization?: { magicLinkTemplate?: string }
257
247
  connector?: Connector
258
248
  }
259
- type ReturnType = { otpId: string; subOrganizationId: string }
249
+ type ReturnType = { otpId: string }
260
250
  type ErrorType = Error
261
251
  }
262
252
 
@@ -268,7 +258,6 @@ export async function verifyOTP(
268
258
  parameters: {
269
259
  code: string
270
260
  otpId: string
271
- subOrganizationId: string
272
261
  connector?: Connector
273
262
  },
274
263
  ): Promise<void> {
@@ -285,7 +274,6 @@ export async function verifyOTP(
285
274
  mode: 'verifyOtp',
286
275
  otpId: parameters.otpId,
287
276
  otpCode: parameters.code,
288
- subOrganizationId: parameters.subOrganizationId,
289
277
  })
290
278
 
291
279
  const [session, eoaAccount] = await Promise.all([
@@ -304,7 +292,6 @@ export declare namespace verifyOTP {
304
292
  type Parameters = {
305
293
  code: string
306
294
  otpId: string
307
- subOrganizationId: string
308
295
  connector?: Connector
309
296
  }
310
297
  type ReturnType = void
@@ -344,6 +331,42 @@ export declare namespace refreshSession {
344
331
  type ErrorType = Error
345
332
  }
346
333
 
334
+ /**
335
+ * Get user email
336
+ */
337
+ export async function getUserEmail(
338
+ config: Config,
339
+ parameters: {
340
+ organizationId: string
341
+ projectId: string
342
+ connector?: Connector
343
+ },
344
+ ): Promise<{ email: string }> {
345
+ const connector = parameters.connector ?? getZeroDevConnector(config)
346
+
347
+ // @ts-expect-error - getStore is a custom method
348
+ const store = await connector.getStore()
349
+ const wallet = store.getState().wallet
350
+
351
+ if (!wallet) throw new Error('Wallet not initialized')
352
+
353
+ // Call the core SDK method
354
+ return await wallet.client.getUserEmail({
355
+ organizationId: parameters.organizationId,
356
+ projectId: parameters.projectId,
357
+ })
358
+ }
359
+
360
+ export declare namespace getUserEmail {
361
+ type Parameters = {
362
+ organizationId: string
363
+ projectId: string
364
+ connector?: Connector
365
+ }
366
+ type ReturnType = { email: string }
367
+ type ErrorType = Error
368
+ }
369
+
347
370
  /**
348
371
  * Export wallet
349
372
  */
@@ -362,10 +385,6 @@ export async function exportWallet(
362
385
 
363
386
  if (!wallet) throw new Error('Wallet not initialized')
364
387
 
365
- const { exportWallet: exportWalletSdk, createIframeStamper } = await import(
366
- '@zerodev/wallet-core'
367
- )
368
-
369
388
  const iframeContainer = document.getElementById(parameters.iframeContainerId)
370
389
  if (!iframeContainer) {
371
390
  throw new Error('Iframe container not found')
@@ -400,3 +419,62 @@ export declare namespace exportWallet {
400
419
  type ReturnType = void
401
420
  type ErrorType = Error
402
421
  }
422
+
423
+ /**
424
+ * Export private key
425
+ */
426
+ export async function exportPrivateKey(
427
+ config: Config,
428
+ parameters: {
429
+ iframeContainerId: string
430
+ address?: string
431
+ keyFormat?: 'Hexadecimal' | 'Solana'
432
+ connector?: Connector
433
+ },
434
+ ): Promise<void> {
435
+ const connector = parameters.connector ?? getZeroDevConnector(config)
436
+
437
+ // @ts-expect-error - getStore is a custom method
438
+ const store = await connector.getStore()
439
+ const wallet = store.getState().wallet
440
+
441
+ if (!wallet) throw new Error('Wallet not initialized')
442
+
443
+ const iframeContainer = document.getElementById(parameters.iframeContainerId)
444
+ if (!iframeContainer) {
445
+ throw new Error('Iframe container not found')
446
+ }
447
+
448
+ const iframeStamper = await createIframeStamper({
449
+ iframeUrl: 'https://export.turnkey.com',
450
+ iframeContainer,
451
+ iframeElementId: 'export-private-key-iframe',
452
+ })
453
+
454
+ const publicKey = await iframeStamper.init()
455
+ const { exportBundle, organizationId } = await exportPrivateKeySdk({
456
+ wallet,
457
+ targetPublicKey: publicKey,
458
+ ...(parameters.address && { address: parameters.address }),
459
+ })
460
+
461
+ const success = await iframeStamper.injectKeyExportBundle(
462
+ exportBundle,
463
+ organizationId,
464
+ parameters.keyFormat ?? 'Hexadecimal',
465
+ )
466
+ if (success !== true) {
467
+ throw new Error('Failed to inject export bundle')
468
+ }
469
+ }
470
+
471
+ export declare namespace exportPrivateKey {
472
+ type Parameters = {
473
+ iframeContainerId: string
474
+ address?: string
475
+ keyFormat?: 'Hexadecimal' | 'Solana'
476
+ connector?: Connector
477
+ }
478
+ type ReturnType = void
479
+ type ErrorType = Error
480
+ }
package/src/connector.ts CHANGED
@@ -6,13 +6,71 @@ import {
6
6
  } from '@zerodev/sdk'
7
7
  import { getEntryPoint, KERNEL_V3_3 } from '@zerodev/sdk/constants'
8
8
  import type { StorageAdapter } from '@zerodev/wallet-core'
9
- import { createZeroDevWallet } from '@zerodev/wallet-core'
9
+ import { createZeroDevWallet, KMS_SERVER_URL } from '@zerodev/wallet-core'
10
10
  import { type Chain, createPublicClient, http } from 'viem'
11
- import type { OAuthConfig } from './oauth.js'
11
+ import { handleOAuthCallback, type OAuthProvider } from './oauth.js'
12
12
  import { createProvider } from './provider.js'
13
13
  import { createZeroDevWalletStore } from './store.js'
14
14
  import { getAAUrl } from './utils/aaUtils.js'
15
15
 
16
+ // OAuth URL parameter used to detect callback
17
+ const OAUTH_SUCCESS_PARAM = 'oauth_success'
18
+ const OAUTH_PROVIDER_PARAM = 'oauth_provider'
19
+
20
+ /**
21
+ * Detect OAuth callback from URL params and handle it.
22
+ * - If in popup: sends postMessage to opener and closes
23
+ * - If not in popup: completes auth directly
24
+ */
25
+ async function detectAndHandleOAuthCallback(
26
+ wallet: Awaited<ReturnType<typeof createZeroDevWallet>>,
27
+ store: ReturnType<typeof createZeroDevWalletStore>,
28
+ ): Promise<boolean> {
29
+ if (typeof window === 'undefined') return false
30
+
31
+ const params = new URLSearchParams(window.location.search)
32
+ const isOAuthCallback = params.get(OAUTH_SUCCESS_PARAM) === 'true'
33
+
34
+ if (!isOAuthCallback) return false
35
+
36
+ // If in popup, use the existing handler to notify opener
37
+ if (window.opener) {
38
+ handleOAuthCallback(OAUTH_SUCCESS_PARAM)
39
+ return true
40
+ }
41
+
42
+ // Not in popup - complete auth directly (redirect flow)
43
+ console.log('OAuth callback detected, completing authentication...')
44
+ const provider = (params.get(OAUTH_PROVIDER_PARAM) ||
45
+ 'google') as OAuthProvider
46
+
47
+ try {
48
+ await wallet.auth({ type: 'oauth', provider })
49
+
50
+ const [session, eoaAccount] = await Promise.all([
51
+ wallet.getSession(),
52
+ wallet.toAccount(),
53
+ ])
54
+
55
+ store.getState().setEoaAccount(eoaAccount)
56
+ store.getState().setSession(session || null)
57
+
58
+ // Clean up URL params
59
+ params.delete(OAUTH_SUCCESS_PARAM)
60
+ params.delete(OAUTH_PROVIDER_PARAM)
61
+ const newUrl = params.toString()
62
+ ? `${window.location.pathname}?${params.toString()}`
63
+ : window.location.pathname
64
+ window.history.replaceState({}, '', newUrl)
65
+
66
+ console.log('OAuth authentication completed')
67
+ return true
68
+ } catch (error) {
69
+ console.error('OAuth authentication failed:', error)
70
+ return false
71
+ }
72
+ }
73
+
16
74
  export type ZeroDevWalletConnectorParams = {
17
75
  projectId: string
18
76
  organizationId?: string
@@ -23,7 +81,6 @@ export type ZeroDevWalletConnectorParams = {
23
81
  sessionStorage?: StorageAdapter
24
82
  autoRefreshSession?: boolean
25
83
  sessionWarningThreshold?: number
26
- oauthConfig?: OAuthConfig
27
84
  }
28
85
 
29
86
  export function zeroDevWallet(
@@ -75,10 +132,11 @@ export function zeroDevWallet(
75
132
  store = createZeroDevWalletStore()
76
133
  store.getState().setWallet(wallet)
77
134
 
78
- // Store OAuth config if provided
79
- if (params.oauthConfig) {
80
- store.getState().setOAuthConfig(params.oauthConfig)
81
- }
135
+ // Store OAuth config - uses proxyBaseUrl and projectId from params
136
+ store.getState().setOAuthConfig({
137
+ backendUrl: params.proxyBaseUrl || `${KMS_SERVER_URL}/api/v1`,
138
+ projectId: params.projectId,
139
+ })
82
140
 
83
141
  // Create EIP-1193 provider
84
142
  provider = createProvider({
@@ -96,6 +154,9 @@ export function zeroDevWallet(
96
154
  store.getState().setSession(session)
97
155
  }
98
156
 
157
+ // Auto-detect OAuth callback (when popup redirects back with ?oauth_success=true)
158
+ await detectAndHandleOAuthCallback(wallet, store)
159
+
99
160
  console.log('ZeroDevWallet connector initialized')
100
161
  }
101
162
 
@@ -0,0 +1,57 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type UseMutationOptions,
5
+ type UseMutationResult,
6
+ useMutation,
7
+ } from '@tanstack/react-query'
8
+ import { type Config, type ResolvedRegister, useConfig } from 'wagmi'
9
+ import { exportPrivateKey } from '../actions.js'
10
+
11
+ type ConfigParameter<config extends Config = Config> = {
12
+ config?: Config | config | undefined
13
+ }
14
+
15
+ /**
16
+ * Hook to export private key
17
+ */
18
+ export function useExportPrivateKey<
19
+ config extends Config = ResolvedRegister['config'],
20
+ context = unknown,
21
+ >(
22
+ parameters: useExportPrivateKey.Parameters<config, context> = {},
23
+ ): useExportPrivateKey.ReturnType<context> {
24
+ const { mutation } = parameters
25
+ const config = useConfig(parameters)
26
+
27
+ return useMutation({
28
+ ...mutation,
29
+ async mutationFn(variables: exportPrivateKey.Parameters) {
30
+ return exportPrivateKey(config, variables)
31
+ },
32
+ mutationKey: ['exportPrivateKey'],
33
+ })
34
+ }
35
+
36
+ export declare namespace useExportPrivateKey {
37
+ type Parameters<
38
+ config extends Config = Config,
39
+ context = unknown,
40
+ > = ConfigParameter<config> & {
41
+ mutation?:
42
+ | UseMutationOptions<
43
+ exportPrivateKey.ReturnType,
44
+ exportPrivateKey.ErrorType,
45
+ exportPrivateKey.Parameters,
46
+ context
47
+ >
48
+ | undefined
49
+ }
50
+
51
+ type ReturnType<context = unknown> = UseMutationResult<
52
+ exportPrivateKey.ReturnType,
53
+ exportPrivateKey.ErrorType,
54
+ exportPrivateKey.Parameters,
55
+ context
56
+ >
57
+ }
@@ -0,0 +1,54 @@
1
+ 'use client'
2
+
3
+ import {
4
+ type UseQueryOptions,
5
+ type UseQueryResult,
6
+ useQuery,
7
+ } from '@tanstack/react-query'
8
+ import { type Config, type ResolvedRegister, useConfig } from 'wagmi'
9
+ import { getUserEmail } from '../actions.js'
10
+
11
+ type ConfigParameter<config extends Config = Config> = {
12
+ config?: Config | config | undefined
13
+ }
14
+
15
+ /**
16
+ * Hook to fetch user email address
17
+ */
18
+ export function useGetUserEmail<
19
+ config extends Config = ResolvedRegister['config'],
20
+ >(parameters: useGetUserEmail.Parameters<config>): useGetUserEmail.ReturnType {
21
+ const { organizationId, projectId, query } = parameters
22
+ const config = useConfig(parameters)
23
+
24
+ return useQuery({
25
+ ...query,
26
+ queryKey: ['getUserEmail', { organizationId, projectId }],
27
+ queryFn: async () => {
28
+ return getUserEmail(config, { organizationId, projectId })
29
+ },
30
+ enabled: Boolean(organizationId && projectId),
31
+ })
32
+ }
33
+
34
+ export declare namespace useGetUserEmail {
35
+ type Parameters<config extends Config = Config> = ConfigParameter<config> & {
36
+ organizationId: string
37
+ projectId: string
38
+ query?:
39
+ | Omit<
40
+ UseQueryOptions<
41
+ getUserEmail.ReturnType,
42
+ getUserEmail.ErrorType,
43
+ getUserEmail.ReturnType
44
+ >,
45
+ 'queryKey' | 'queryFn'
46
+ >
47
+ | undefined
48
+ }
49
+
50
+ type ReturnType = UseQueryResult<
51
+ getUserEmail.ReturnType,
52
+ getUserEmail.ErrorType
53
+ >
54
+ }
package/src/index.ts CHANGED
@@ -1,14 +1,21 @@
1
1
  export type { ZeroDevWalletConnectorParams } from './connector.js'
2
2
  export { zeroDevWallet } from './connector.js'
3
3
  export { useAuthenticateOAuth } from './hooks/useAuthenticateOAuth.js'
4
+ export { useExportPrivateKey } from './hooks/useExportPrivateKey.js'
4
5
  export { useExportWallet } from './hooks/useExportWallet.js'
6
+ export { useGetUserEmail } from './hooks/useGetUserEmail.js'
5
7
  export { useLoginPasskey } from './hooks/useLoginPasskey.js'
6
8
  export { useRefreshSession } from './hooks/useRefreshSession.js'
7
9
  export { useRegisterPasskey } from './hooks/useRegisterPasskey.js'
8
10
  export { useSendOTP } from './hooks/useSendOTP.js'
9
11
  export { useVerifyOTP } from './hooks/useVerifyOTP.js'
10
- export type { OAuthConfig, OAuthProvider } from './oauth.js'
11
- export { OAUTH_PROVIDERS } from './oauth.js'
12
+ export type { OAuthMessageData, OAuthProvider } from './oauth.js'
13
+ export {
14
+ buildBackendOAuthUrl,
15
+ handleOAuthCallback,
16
+ listenForOAuthMessage,
17
+ OAUTH_PROVIDERS,
18
+ } from './oauth.js'
12
19
  export type { ZeroDevProvider } from './provider.js'
13
20
  export type { ZeroDevWalletState } from './store.js'
14
21
  export { createZeroDevWalletStore } from './store.js'