@zerodev/wallet-react 0.0.1-alpha.0 → 0.0.1-alpha.10

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 (54) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +282 -0
  3. package/dist/_cjs/actions.js +62 -24
  4. package/dist/_cjs/connector.js +86 -52
  5. package/dist/_cjs/constants.js +1 -1
  6. package/dist/_cjs/hooks/useExportPrivateKey.js +18 -0
  7. package/dist/_cjs/hooks/useGetUserEmail.js +19 -0
  8. package/dist/_cjs/index.js +8 -1
  9. package/dist/_cjs/oauth.js +60 -55
  10. package/dist/_cjs/provider.js +5 -2
  11. package/dist/_cjs/store.js +4 -9
  12. package/dist/_cjs/utils/aaUtils.js +2 -2
  13. package/dist/_esm/actions.js +74 -27
  14. package/dist/_esm/connector.js +102 -59
  15. package/dist/_esm/constants.js +1 -1
  16. package/dist/_esm/hooks/useExportPrivateKey.js +18 -0
  17. package/dist/_esm/hooks/useGetUserEmail.js +19 -0
  18. package/dist/_esm/index.js +3 -1
  19. package/dist/_esm/oauth.js +71 -53
  20. package/dist/_esm/provider.js +5 -2
  21. package/dist/_esm/store.js +4 -10
  22. package/dist/_esm/utils/aaUtils.js +2 -2
  23. package/dist/_types/actions.d.ts +35 -6
  24. package/dist/_types/actions.d.ts.map +1 -1
  25. package/dist/_types/connector.d.ts +1 -3
  26. package/dist/_types/connector.d.ts.map +1 -1
  27. package/dist/_types/constants.d.ts +1 -1
  28. package/dist/_types/constants.d.ts.map +1 -1
  29. package/dist/_types/hooks/useExportPrivateKey.d.ts +18 -0
  30. package/dist/_types/hooks/useExportPrivateKey.d.ts.map +1 -0
  31. package/dist/_types/hooks/useGetUserEmail.d.ts +18 -0
  32. package/dist/_types/hooks/useGetUserEmail.d.ts.map +1 -0
  33. package/dist/_types/index.d.ts +4 -2
  34. package/dist/_types/index.d.ts.map +1 -1
  35. package/dist/_types/oauth.d.ts +25 -12
  36. package/dist/_types/oauth.d.ts.map +1 -1
  37. package/dist/_types/provider.d.ts.map +1 -1
  38. package/dist/_types/store.d.ts +11 -7
  39. package/dist/_types/store.d.ts.map +1 -1
  40. package/dist/_types/utils/aaUtils.d.ts +1 -1
  41. package/dist/_types/utils/aaUtils.d.ts.map +1 -1
  42. package/dist/tsconfig.build.tsbuildinfo +1 -1
  43. package/package.json +2 -2
  44. package/src/actions.ts +124 -44
  45. package/src/connector.ts +130 -70
  46. package/src/constants.ts +2 -1
  47. package/src/hooks/useExportPrivateKey.ts +57 -0
  48. package/src/hooks/useGetUserEmail.ts +52 -0
  49. package/src/index.ts +9 -2
  50. package/src/oauth.ts +97 -78
  51. package/src/provider.ts +5 -2
  52. package/src/store.ts +15 -16
  53. package/src/utils/aaUtils.ts +2 -2
  54. package/tsconfig.build.tsbuildinfo +1 -1
@@ -0,0 +1,52 @@
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 { query } = parameters
22
+ const config = useConfig(parameters)
23
+
24
+ return useQuery({
25
+ ...query,
26
+ queryKey: ['getUserEmail'],
27
+ queryFn: async () => {
28
+ return getUserEmail(config)
29
+ },
30
+ enabled: Boolean(config),
31
+ })
32
+ }
33
+
34
+ export declare namespace useGetUserEmail {
35
+ type Parameters<config extends Config = Config> = ConfigParameter<config> & {
36
+ query?:
37
+ | Omit<
38
+ UseQueryOptions<
39
+ getUserEmail.ReturnType,
40
+ getUserEmail.ErrorType,
41
+ getUserEmail.ReturnType
42
+ >,
43
+ 'queryKey' | 'queryFn'
44
+ >
45
+ | undefined
46
+ }
47
+
48
+ type ReturnType = UseQueryResult<
49
+ getUserEmail.ReturnType,
50
+ getUserEmail.ErrorType
51
+ >
52
+ }
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'
package/src/oauth.ts CHANGED
@@ -7,56 +7,17 @@ export const OAUTH_PROVIDERS = {
7
7
  export type OAuthProvider =
8
8
  (typeof OAUTH_PROVIDERS)[keyof typeof OAUTH_PROVIDERS]
9
9
 
10
- export type OAuthConfig = {
11
- googleClientId?: string
12
- redirectUri: string
13
- }
14
-
15
- export type OAuthFlowParams = {
10
+ export type BackendOAuthFlowParams = {
16
11
  provider: OAuthProvider
17
- clientId: string
18
- redirectUri: string
19
- nonce: string
20
- state?: Record<string, string>
12
+ backendUrl: string
13
+ projectId: string
14
+ publicKey: string
15
+ returnTo: string
21
16
  }
22
17
 
23
- const GOOGLE_AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth'
24
-
25
18
  const POPUP_WIDTH = 500
26
19
  const POPUP_HEIGHT = 600
27
20
 
28
- export function buildOAuthUrl(params: OAuthFlowParams): string {
29
- const { provider, clientId, redirectUri, nonce, state } = params
30
-
31
- if (provider !== OAUTH_PROVIDERS.GOOGLE) {
32
- throw new Error(`Unsupported OAuth provider: ${provider}`)
33
- }
34
-
35
- const authUrl = new URL(GOOGLE_AUTH_URL)
36
- authUrl.searchParams.set('client_id', clientId)
37
- authUrl.searchParams.set('redirect_uri', redirectUri)
38
- authUrl.searchParams.set('response_type', 'id_token')
39
- authUrl.searchParams.set('scope', 'openid email profile')
40
- authUrl.searchParams.set('nonce', nonce)
41
- authUrl.searchParams.set('prompt', 'select_account')
42
-
43
- let stateParam = `provider=${provider}`
44
- if (state) {
45
- const additionalState = Object.entries(state)
46
- .map(
47
- ([key, value]) =>
48
- `${encodeURIComponent(key)}=${encodeURIComponent(value)}`,
49
- )
50
- .join('&')
51
- if (additionalState) {
52
- stateParam += `&${additionalState}`
53
- }
54
- }
55
- authUrl.searchParams.set('state', stateParam)
56
-
57
- return authUrl.toString()
58
- }
59
-
60
21
  export function openOAuthPopup(url: string): Window | null {
61
22
  const width = POPUP_WIDTH
62
23
  const height = POPUP_HEIGHT
@@ -76,49 +37,107 @@ export function openOAuthPopup(url: string): Window | null {
76
37
  return authWindow
77
38
  }
78
39
 
79
- export function extractOAuthToken(url: string): string | null {
80
- const hashParams = new URLSearchParams(url.split('#')[1])
81
- let idToken = hashParams.get('id_token')
40
+ export function generateOAuthNonce(publicKey: string): string {
41
+ return sha256(publicKey as Hex).replace(/^0x/, '')
42
+ }
82
43
 
83
- if (!idToken) {
84
- const queryParams = new URLSearchParams(url.split('?')[1])
85
- idToken = queryParams.get('id_token')
44
+ /**
45
+ * Build OAuth URL that redirects to backend's OAuth endpoint
46
+ * The backend handles PKCE, client credentials, and token exchange
47
+ */
48
+ export function buildBackendOAuthUrl(params: BackendOAuthFlowParams): string {
49
+ const { provider, backendUrl, projectId, publicKey, returnTo } = params
50
+
51
+ if (provider !== OAUTH_PROVIDERS.GOOGLE) {
52
+ throw new Error(`Unsupported OAuth provider: ${provider}`)
86
53
  }
87
54
 
88
- return idToken
55
+ const oauthUrl = new URL(`${backendUrl}/oauth/google/login`)
56
+ oauthUrl.searchParams.set('project_id', projectId)
57
+ oauthUrl.searchParams.set('pub_key', publicKey.replace(/^0x/, ''))
58
+ oauthUrl.searchParams.set('return_to', returnTo)
59
+
60
+ return oauthUrl.toString()
89
61
  }
90
62
 
91
- export function pollOAuthPopup(
63
+ export type OAuthMessageData = {
64
+ type: 'oauth_success' | 'oauth_error'
65
+ error?: string
66
+ }
67
+
68
+ /**
69
+ * Listen for OAuth completion via postMessage from popup
70
+ * The popup sends a message when it detects a successful redirect
71
+ */
72
+ export function listenForOAuthMessage(
92
73
  authWindow: Window,
93
- originUrl: string,
94
- onSuccess: (token: string) => void,
74
+ expectedOrigin: string,
75
+ onSuccess: () => void,
95
76
  onError: (error: Error) => void,
96
- ): void {
97
- const interval = setInterval(() => {
98
- try {
99
- if (authWindow.closed) {
100
- clearInterval(interval)
101
- onError(new Error('Authentication window was closed'))
102
- return
103
- }
104
-
105
- const url = authWindow.location.href || ''
106
-
107
- if (url.startsWith(originUrl)) {
108
- const token = extractOAuthToken(url)
109
-
110
- if (token) {
111
- authWindow.close()
112
- clearInterval(interval)
113
- onSuccess(token)
114
- }
115
- }
116
- } catch {
117
- // Ignore cross-origin errors
77
+ ): () => void {
78
+ let cleaned = false
79
+
80
+ const handleMessage = (event: MessageEvent<OAuthMessageData>) => {
81
+ // Only trust messages from expected origin
82
+ if (event.origin !== expectedOrigin) return
83
+ if (!event.data || typeof event.data !== 'object') return
84
+
85
+ if (event.data.type === 'oauth_success') {
86
+ cleanup()
87
+ onSuccess()
88
+ } else if (event.data.type === 'oauth_error') {
89
+ cleanup()
90
+ onError(new Error(event.data.error || 'OAuth authentication failed'))
91
+ }
92
+ }
93
+
94
+ const checkWindowClosed = setInterval(() => {
95
+ if (authWindow.closed) {
96
+ cleanup()
97
+ onError(new Error('Authentication window was closed'))
118
98
  }
119
99
  }, 500)
100
+
101
+ const cleanup = () => {
102
+ if (cleaned) return
103
+ cleaned = true
104
+ window.removeEventListener('message', handleMessage)
105
+ clearInterval(checkWindowClosed)
106
+ }
107
+
108
+ window.addEventListener('message', handleMessage)
109
+
110
+ return cleanup
120
111
  }
121
112
 
122
- export function generateOAuthNonce(publicKey: string): string {
123
- return sha256(publicKey as Hex).replace(/^0x/, '')
113
+ /**
114
+ * Handle OAuth callback on the return page
115
+ * Call this on the page that receives the OAuth redirect
116
+ * It sends a postMessage to the opener and closes the window
117
+ */
118
+ export function handleOAuthCallback(successParam = 'oauth_success'): boolean {
119
+ const urlParams = new URLSearchParams(window.location.search)
120
+ const isSuccess = urlParams.get(successParam) === 'true'
121
+ const error = urlParams.get('error')
122
+
123
+ if (window.opener) {
124
+ if (isSuccess) {
125
+ window.opener.postMessage(
126
+ { type: 'oauth_success' } satisfies OAuthMessageData,
127
+ window.location.origin,
128
+ )
129
+ window.close()
130
+ return true
131
+ }
132
+ if (error) {
133
+ window.opener.postMessage(
134
+ { type: 'oauth_error', error } satisfies OAuthMessageData,
135
+ window.location.origin,
136
+ )
137
+ window.close()
138
+ return false
139
+ }
140
+ }
141
+
142
+ return false
124
143
  }
package/src/provider.ts CHANGED
@@ -115,7 +115,10 @@ export function createProvider({
115
115
 
116
116
  async request({ method, params }: { method: string; params?: any[] }) {
117
117
  const state = store.getState()
118
- const activeChainId = state.chainIds[0]
118
+ const activeChainId = state.activeChainId
119
+ if (!activeChainId) {
120
+ throw new Error('No active chain')
121
+ }
119
122
 
120
123
  switch (method) {
121
124
  case 'eth_accounts': {
@@ -219,7 +222,7 @@ export function createProvider({
219
222
  const chainId_number = parseInt(chainId, 16)
220
223
 
221
224
  // Update active chain
222
- store.getState().setActiveChain(chainId_number)
225
+ store.getState().setActiveChainId(chainId_number)
223
226
 
224
227
  // Emit chainChanged event
225
228
  emitter.emit('chainChanged', chainId)
package/src/store.ts CHANGED
@@ -10,7 +10,12 @@ import type { LocalAccount } from 'viem'
10
10
  import type { SmartAccount } from 'viem/account-abstraction'
11
11
  import { create } from 'zustand'
12
12
  import { persist, subscribeWithSelector } from 'zustand/middleware'
13
- import type { OAuthConfig } from './oauth.js'
13
+
14
+ // Internal OAuth config stored in the state (derived from connector params)
15
+ type InternalOAuthConfig = {
16
+ backendUrl: string
17
+ projectId: string
18
+ }
14
19
 
15
20
  export type ZeroDevWalletState = {
16
21
  // Core
@@ -19,15 +24,15 @@ export type ZeroDevWalletState = {
19
24
  session: ZeroDevWalletSession | null
20
25
 
21
26
  // Multi-chain support
22
- chainIds: number[] // [activeChain, ...otherChains]
27
+ activeChainId: number | null
23
28
  kernelAccounts: Map<number, SmartAccount<KernelSmartAccountImplementation>>
24
29
  kernelClients: Map<number, KernelAccountClient>
25
30
 
26
31
  // Session expiry
27
32
  isExpiring: boolean
28
33
 
29
- // OAuth config (optional)
30
- oauthConfig: OAuthConfig | null
34
+ // OAuth config (derived from connector params)
35
+ oauthConfig: InternalOAuthConfig | null
31
36
 
32
37
  // Actions
33
38
  setWallet: (wallet: ZeroDevWalletSDK) => void
@@ -38,9 +43,9 @@ export type ZeroDevWalletState = {
38
43
  ) => void
39
44
  setKernelClient: (chainId: number, client: KernelAccountClient) => void
40
45
  setSession: (session: ZeroDevWalletSession | null) => void
41
- setActiveChain: (chainId: number) => void
46
+ setActiveChainId: (chainId: number | null) => void
42
47
  setIsExpiring: (isExpiring: boolean) => void
43
- setOAuthConfig: (config: OAuthConfig | null) => void
48
+ setOAuthConfig: (config: InternalOAuthConfig | null) => void
44
49
  clear: () => void
45
50
  }
46
51
 
@@ -53,7 +58,7 @@ export const createZeroDevWalletStore = () =>
53
58
  wallet: null,
54
59
  eoaAccount: null,
55
60
  session: null,
56
- chainIds: [],
61
+ activeChainId: null,
57
62
  kernelAccounts: new Map(),
58
63
  kernelClients: new Map(),
59
64
  isExpiring: false,
@@ -78,13 +83,7 @@ export const createZeroDevWalletStore = () =>
78
83
 
79
84
  setSession: (session) => set({ session }),
80
85
 
81
- setActiveChain: (chainId) => {
82
- const { chainIds } = get()
83
- // Move chainId to front, remove duplicates
84
- set({
85
- chainIds: [chainId, ...chainIds.filter((id) => id !== chainId)],
86
- })
87
- },
86
+ setActiveChainId: (chainId) => set({ activeChainId: chainId }),
88
87
 
89
88
  setIsExpiring: (isExpiring) => set({ isExpiring }),
90
89
 
@@ -97,7 +96,7 @@ export const createZeroDevWalletStore = () =>
97
96
  kernelAccounts: new Map(),
98
97
  kernelClients: new Map(),
99
98
  isExpiring: false,
100
- chainIds: [],
99
+ activeChainId: null,
101
100
  }),
102
101
  }),
103
102
  {
@@ -105,7 +104,7 @@ export const createZeroDevWalletStore = () =>
105
104
  // Only persist session data, not clients or accounts
106
105
  partialize: (state) => ({
107
106
  session: state.session,
108
- chainIds: state.chainIds,
107
+ activeChainId: state.activeChainId,
109
108
  }),
110
109
  },
111
110
  ),
@@ -1,5 +1,5 @@
1
1
  import { ZERODEV_AA_URL } from '../constants.js'
2
2
 
3
- export function getAAUrl(chainId: number, aaUrl?: string) {
4
- return aaUrl || `${ZERODEV_AA_URL}${chainId}/chain/${chainId}`
3
+ export function getAAUrl(projectId: string, chainId: number, aaUrl?: string) {
4
+ return aaUrl || `${ZERODEV_AA_URL}${projectId}/chain/${chainId}`
5
5
  }