@zerodev/wallet-react 0.0.1-alpha.6 → 0.0.1-alpha.8
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/CHANGELOG.md +19 -0
- package/README.md +16 -16
- package/dist/_cjs/actions.js +31 -21
- package/dist/_cjs/connector.js +44 -3
- package/dist/_cjs/hooks/useGetUserEmail.js +19 -0
- package/dist/_cjs/index.js +6 -1
- package/dist/_cjs/oauth.js +60 -55
- package/dist/_esm/actions.js +42 -26
- package/dist/_esm/connector.js +56 -5
- package/dist/_esm/hooks/useGetUserEmail.js +19 -0
- package/dist/_esm/index.js +2 -1
- package/dist/_esm/oauth.js +71 -53
- package/dist/_types/actions.d.ts +16 -6
- package/dist/_types/actions.d.ts.map +1 -1
- package/dist/_types/connector.d.ts +0 -2
- package/dist/_types/connector.d.ts.map +1 -1
- package/dist/_types/hooks/useGetUserEmail.d.ts +18 -0
- package/dist/_types/hooks/useGetUserEmail.d.ts.map +1 -0
- package/dist/_types/index.d.ts +3 -2
- package/dist/_types/index.d.ts.map +1 -1
- package/dist/_types/oauth.d.ts +25 -12
- package/dist/_types/oauth.d.ts.map +1 -1
- package/dist/_types/store.d.ts +7 -3
- package/dist/_types/store.d.ts.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/actions.ts +59 -40
- package/src/connector.ts +68 -7
- package/src/hooks/useGetUserEmail.ts +52 -0
- package/src/index.ts +8 -2
- package/src/oauth.ts +97 -78
- package/src/store.ts +9 -4
- 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.
|
|
3
|
+
"version": "0.0.1-alpha.8",
|
|
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.
|
|
33
|
+
"@zerodev/wallet-core": "0.0.1-alpha.7"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/react": "^19",
|
package/src/actions.ts
CHANGED
|
@@ -7,10 +7,9 @@ import {
|
|
|
7
7
|
} from '@zerodev/wallet-core'
|
|
8
8
|
import type { OAuthProvider } from './oauth.js'
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
buildBackendOAuthUrl,
|
|
11
|
+
listenForOAuthMessage,
|
|
12
12
|
openOAuthPopup,
|
|
13
|
-
pollOAuthPopup,
|
|
14
13
|
} from './oauth.js'
|
|
15
14
|
|
|
16
15
|
/**
|
|
@@ -116,12 +115,12 @@ export declare namespace loginPasskey {
|
|
|
116
115
|
|
|
117
116
|
/**
|
|
118
117
|
* Authenticate with OAuth (opens popup)
|
|
118
|
+
* Uses backend OAuth flow where the backend handles PKCE and token exchange
|
|
119
119
|
*/
|
|
120
120
|
export async function authenticateOAuth(
|
|
121
121
|
config: Config,
|
|
122
122
|
parameters: {
|
|
123
123
|
provider: OAuthProvider
|
|
124
|
-
clientId?: string
|
|
125
124
|
connector?: Connector
|
|
126
125
|
},
|
|
127
126
|
): Promise<void> {
|
|
@@ -134,38 +133,23 @@ export async function authenticateOAuth(
|
|
|
134
133
|
|
|
135
134
|
if (!wallet) throw new Error('Wallet not initialized')
|
|
136
135
|
if (!oauthConfig) {
|
|
137
|
-
throw new Error(
|
|
138
|
-
'OAuth is not configured. Please provide oauthConfig to zeroDevWallet connector.',
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Get client ID for the provider
|
|
143
|
-
let clientId = parameters.clientId
|
|
144
|
-
if (!clientId) {
|
|
145
|
-
clientId = oauthConfig.googleClientId
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (!clientId) {
|
|
149
|
-
throw new Error(`Client ID not configured for ${parameters.provider}`)
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
if (!oauthConfig.redirectUri) {
|
|
153
|
-
throw new Error('OAuth redirect URI is not configured.')
|
|
136
|
+
throw new Error('Wallet not initialized. Please wait for connector setup.')
|
|
154
137
|
}
|
|
155
138
|
|
|
156
|
-
//
|
|
139
|
+
// Get wallet public key for the OAuth flow
|
|
157
140
|
const publicKey = await wallet.getPublicKey()
|
|
158
141
|
if (!publicKey) {
|
|
159
142
|
throw new Error('Failed to get wallet public key')
|
|
160
143
|
}
|
|
161
|
-
const nonce = generateOAuthNonce(publicKey)
|
|
162
144
|
|
|
163
|
-
// Build OAuth URL
|
|
164
|
-
|
|
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({
|
|
165
148
|
provider: parameters.provider,
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
149
|
+
backendUrl: oauthConfig.backendUrl,
|
|
150
|
+
projectId: oauthConfig.projectId,
|
|
151
|
+
publicKey,
|
|
152
|
+
returnTo: `${window.location.origin}?oauth_success=true&oauth_provider=${parameters.provider}`,
|
|
169
153
|
})
|
|
170
154
|
|
|
171
155
|
// Open popup
|
|
@@ -175,18 +159,18 @@ export async function authenticateOAuth(
|
|
|
175
159
|
throw new Error(`Failed to open ${parameters.provider} login window.`)
|
|
176
160
|
}
|
|
177
161
|
|
|
178
|
-
//
|
|
162
|
+
// Listen for OAuth completion via postMessage
|
|
179
163
|
return new Promise<void>((resolve, reject) => {
|
|
180
|
-
|
|
164
|
+
const cleanup = listenForOAuthMessage(
|
|
181
165
|
authWindow,
|
|
182
166
|
window.location.origin,
|
|
183
|
-
async (
|
|
167
|
+
async () => {
|
|
184
168
|
try {
|
|
185
169
|
// Complete OAuth authentication with wallet-core
|
|
170
|
+
// The backend has stored the OAuth session in a cookie
|
|
186
171
|
await wallet.auth({
|
|
187
172
|
type: 'oauth',
|
|
188
173
|
provider: parameters.provider,
|
|
189
|
-
credential: idToken,
|
|
190
174
|
})
|
|
191
175
|
|
|
192
176
|
const [session, eoaAccount] = await Promise.all([
|
|
@@ -205,7 +189,10 @@ export async function authenticateOAuth(
|
|
|
205
189
|
reject(err)
|
|
206
190
|
}
|
|
207
191
|
},
|
|
208
|
-
|
|
192
|
+
(error) => {
|
|
193
|
+
cleanup()
|
|
194
|
+
reject(error)
|
|
195
|
+
},
|
|
209
196
|
)
|
|
210
197
|
})
|
|
211
198
|
}
|
|
@@ -213,7 +200,6 @@ export async function authenticateOAuth(
|
|
|
213
200
|
export declare namespace authenticateOAuth {
|
|
214
201
|
type Parameters = {
|
|
215
202
|
provider: OAuthProvider
|
|
216
|
-
clientId?: string
|
|
217
203
|
connector?: Connector
|
|
218
204
|
}
|
|
219
205
|
type ReturnType = void
|
|
@@ -230,7 +216,7 @@ export async function sendOTP(
|
|
|
230
216
|
emailCustomization?: { magicLinkTemplate?: string }
|
|
231
217
|
connector?: Connector
|
|
232
218
|
},
|
|
233
|
-
): Promise<{ otpId: string
|
|
219
|
+
): Promise<{ otpId: string }> {
|
|
234
220
|
const connector = parameters.connector ?? getZeroDevConnector(config)
|
|
235
221
|
|
|
236
222
|
// @ts-expect-error - getStore is a custom method
|
|
@@ -251,7 +237,6 @@ export async function sendOTP(
|
|
|
251
237
|
|
|
252
238
|
return {
|
|
253
239
|
otpId: result.otpId,
|
|
254
|
-
subOrganizationId: result.subOrganizationId,
|
|
255
240
|
}
|
|
256
241
|
}
|
|
257
242
|
|
|
@@ -261,7 +246,7 @@ export declare namespace sendOTP {
|
|
|
261
246
|
emailCustomization?: { magicLinkTemplate?: string }
|
|
262
247
|
connector?: Connector
|
|
263
248
|
}
|
|
264
|
-
type ReturnType = { otpId: string
|
|
249
|
+
type ReturnType = { otpId: string }
|
|
265
250
|
type ErrorType = Error
|
|
266
251
|
}
|
|
267
252
|
|
|
@@ -273,7 +258,6 @@ export async function verifyOTP(
|
|
|
273
258
|
parameters: {
|
|
274
259
|
code: string
|
|
275
260
|
otpId: string
|
|
276
|
-
subOrganizationId: string
|
|
277
261
|
connector?: Connector
|
|
278
262
|
},
|
|
279
263
|
): Promise<void> {
|
|
@@ -290,7 +274,6 @@ export async function verifyOTP(
|
|
|
290
274
|
mode: 'verifyOtp',
|
|
291
275
|
otpId: parameters.otpId,
|
|
292
276
|
otpCode: parameters.code,
|
|
293
|
-
subOrganizationId: parameters.subOrganizationId,
|
|
294
277
|
})
|
|
295
278
|
|
|
296
279
|
const [session, eoaAccount] = await Promise.all([
|
|
@@ -309,7 +292,6 @@ export declare namespace verifyOTP {
|
|
|
309
292
|
type Parameters = {
|
|
310
293
|
code: string
|
|
311
294
|
otpId: string
|
|
312
|
-
subOrganizationId: string
|
|
313
295
|
connector?: Connector
|
|
314
296
|
}
|
|
315
297
|
type ReturnType = void
|
|
@@ -349,6 +331,43 @@ export declare namespace refreshSession {
|
|
|
349
331
|
type ErrorType = Error
|
|
350
332
|
}
|
|
351
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Get user email
|
|
336
|
+
*/
|
|
337
|
+
export async function getUserEmail(config: Config): Promise<{ email: string }> {
|
|
338
|
+
const connector = getZeroDevConnector(config)
|
|
339
|
+
|
|
340
|
+
// @ts-expect-error - getStore is a custom method
|
|
341
|
+
const store = await connector.getStore()
|
|
342
|
+
const wallet = store.getState().wallet
|
|
343
|
+
|
|
344
|
+
if (!wallet) throw new Error('Wallet not initialized')
|
|
345
|
+
|
|
346
|
+
const oauthConfig = store.getState().oauthConfig
|
|
347
|
+
if (!oauthConfig) {
|
|
348
|
+
throw new Error('Wallet not initialized. Please wait for connector setup.')
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const session = store.getState().session
|
|
352
|
+
if (!session) {
|
|
353
|
+
throw new Error('No active session')
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Call the core SDK method
|
|
357
|
+
return await wallet.client.getUserEmail({
|
|
358
|
+
organizationId: session.organizationId,
|
|
359
|
+
projectId: oauthConfig.projectId,
|
|
360
|
+
})
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export declare namespace getUserEmail {
|
|
364
|
+
type Parameters = {
|
|
365
|
+
connector?: Connector
|
|
366
|
+
}
|
|
367
|
+
type ReturnType = { email: string }
|
|
368
|
+
type ErrorType = Error
|
|
369
|
+
}
|
|
370
|
+
|
|
352
371
|
/**
|
|
353
372
|
* Export wallet
|
|
354
373
|
*/
|
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
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
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,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
|
@@ -3,13 +3,19 @@ export { zeroDevWallet } from './connector.js'
|
|
|
3
3
|
export { useAuthenticateOAuth } from './hooks/useAuthenticateOAuth.js'
|
|
4
4
|
export { useExportPrivateKey } from './hooks/useExportPrivateKey.js'
|
|
5
5
|
export { useExportWallet } from './hooks/useExportWallet.js'
|
|
6
|
+
export { useGetUserEmail } from './hooks/useGetUserEmail.js'
|
|
6
7
|
export { useLoginPasskey } from './hooks/useLoginPasskey.js'
|
|
7
8
|
export { useRefreshSession } from './hooks/useRefreshSession.js'
|
|
8
9
|
export { useRegisterPasskey } from './hooks/useRegisterPasskey.js'
|
|
9
10
|
export { useSendOTP } from './hooks/useSendOTP.js'
|
|
10
11
|
export { useVerifyOTP } from './hooks/useVerifyOTP.js'
|
|
11
|
-
export type {
|
|
12
|
-
export {
|
|
12
|
+
export type { OAuthMessageData, OAuthProvider } from './oauth.js'
|
|
13
|
+
export {
|
|
14
|
+
buildBackendOAuthUrl,
|
|
15
|
+
handleOAuthCallback,
|
|
16
|
+
listenForOAuthMessage,
|
|
17
|
+
OAUTH_PROVIDERS,
|
|
18
|
+
} from './oauth.js'
|
|
13
19
|
export type { ZeroDevProvider } from './provider.js'
|
|
14
20
|
export type { ZeroDevWalletState } from './store.js'
|
|
15
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
|
|
11
|
-
googleClientId?: string
|
|
12
|
-
redirectUri: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export type OAuthFlowParams = {
|
|
10
|
+
export type BackendOAuthFlowParams = {
|
|
16
11
|
provider: OAuthProvider
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
40
|
+
export function generateOAuthNonce(publicKey: string): string {
|
|
41
|
+
return sha256(publicKey as Hex).replace(/^0x/, '')
|
|
42
|
+
}
|
|
82
43
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
94
|
-
onSuccess: (
|
|
74
|
+
expectedOrigin: string,
|
|
75
|
+
onSuccess: () => void,
|
|
95
76
|
onError: (error: Error) => void,
|
|
96
|
-
): void {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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/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
|
-
|
|
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
|
|
@@ -26,8 +31,8 @@ export type ZeroDevWalletState = {
|
|
|
26
31
|
// Session expiry
|
|
27
32
|
isExpiring: boolean
|
|
28
33
|
|
|
29
|
-
// OAuth config (
|
|
30
|
-
oauthConfig:
|
|
34
|
+
// OAuth config (derived from connector params)
|
|
35
|
+
oauthConfig: InternalOAuthConfig | null
|
|
31
36
|
|
|
32
37
|
// Actions
|
|
33
38
|
setWallet: (wallet: ZeroDevWalletSDK) => void
|
|
@@ -40,7 +45,7 @@ export type ZeroDevWalletState = {
|
|
|
40
45
|
setSession: (session: ZeroDevWalletSession | null) => void
|
|
41
46
|
setActiveChainId: (chainId: number | null) => void
|
|
42
47
|
setIsExpiring: (isExpiring: boolean) => void
|
|
43
|
-
setOAuthConfig: (config:
|
|
48
|
+
setOAuthConfig: (config: InternalOAuthConfig | null) => void
|
|
44
49
|
clear: () => void
|
|
45
50
|
}
|
|
46
51
|
|