@imtbl/wallet 2.12.7-alpha.8 → 2.12.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.
- package/package.json +4 -4
- package/src/connectWallet.test.ts +463 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@imtbl/wallet",
|
|
3
|
-
"version": "2.12.7
|
|
3
|
+
"version": "2.12.7",
|
|
4
4
|
"description": "Wallet SDK for Immutable",
|
|
5
5
|
"author": "Immutable",
|
|
6
6
|
"bugs": "https://github.com/immutable/ts-immutable-sdk/issues",
|
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
}
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@imtbl/auth": "2.12.7
|
|
29
|
-
"@imtbl/generated-clients": "2.12.7
|
|
30
|
-
"@imtbl/metrics": "2.12.7
|
|
28
|
+
"@imtbl/auth": "2.12.7",
|
|
29
|
+
"@imtbl/generated-clients": "2.12.7",
|
|
30
|
+
"@imtbl/metrics": "2.12.7",
|
|
31
31
|
"viem": "~2.18.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
@@ -1,17 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
1
|
+
// Mock Auth with configurable behavior
|
|
2
|
+
const mockAuthInstance = {
|
|
3
|
+
getConfig: jest.fn().mockReturnValue({
|
|
4
|
+
authenticationDomain: 'https://auth.immutable.com',
|
|
5
|
+
passportDomain: 'https://passport.immutable.com',
|
|
6
|
+
oidcConfiguration: {
|
|
7
|
+
clientId: 'client',
|
|
8
|
+
redirectUri: 'https://redirect',
|
|
9
|
+
},
|
|
10
|
+
}),
|
|
11
|
+
getUser: jest.fn().mockResolvedValue({ profile: { sub: 'user' } }),
|
|
12
|
+
getUserOrLogin: jest.fn().mockResolvedValue({ profile: { sub: 'user' }, accessToken: 'token' }),
|
|
13
|
+
loginCallback: jest.fn().mockResolvedValue(undefined),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const Auth = jest.fn().mockImplementation(() => mockAuthInstance);
|
|
14
17
|
|
|
18
|
+
jest.mock('@imtbl/auth', () => {
|
|
15
19
|
const TypedEventEmitter = jest.fn().mockImplementation(() => ({
|
|
16
20
|
emit: jest.fn(),
|
|
17
21
|
on: jest.fn(),
|
|
@@ -70,26 +74,461 @@ const createGetUserMock = () => jest.fn().mockResolvedValue({
|
|
|
70
74
|
describe('connectWallet', () => {
|
|
71
75
|
beforeEach(() => {
|
|
72
76
|
jest.clearAllMocks();
|
|
77
|
+
Auth.mockClear();
|
|
78
|
+
mockAuthInstance.getUser.mockClear();
|
|
79
|
+
mockAuthInstance.getUserOrLogin.mockClear();
|
|
80
|
+
mockAuthInstance.loginCallback.mockClear();
|
|
73
81
|
});
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
describe('with external getUser (existing tests)', () => {
|
|
84
|
+
it('announces provider by default', async () => {
|
|
85
|
+
const getUser = createGetUserMock();
|
|
86
|
+
|
|
87
|
+
const provider = await connectWallet({ getUser, chains: [zkEvmChain] });
|
|
88
|
+
|
|
89
|
+
expect(ZkEvmProvider).toHaveBeenCalled();
|
|
90
|
+
expect(announceProvider).toHaveBeenCalledWith({
|
|
91
|
+
info: expect.any(Object),
|
|
92
|
+
provider,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('does not announce provider when disabled', async () => {
|
|
97
|
+
const getUser = createGetUserMock();
|
|
98
|
+
|
|
99
|
+
await connectWallet({ getUser, chains: [zkEvmChain], announceProvider: false });
|
|
77
100
|
|
|
78
|
-
|
|
101
|
+
expect(announceProvider).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('uses provided getUser when supplied', async () => {
|
|
105
|
+
const getUser = createGetUserMock();
|
|
79
106
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
107
|
+
await connectWallet({ getUser, chains: [zkEvmChain] });
|
|
108
|
+
|
|
109
|
+
// Should NOT create internal Auth instance
|
|
110
|
+
expect(Auth).not.toHaveBeenCalled();
|
|
111
|
+
// Should use the provided getUser
|
|
112
|
+
expect(getUser).toHaveBeenCalled();
|
|
84
113
|
});
|
|
85
114
|
});
|
|
86
115
|
|
|
87
|
-
|
|
88
|
-
|
|
116
|
+
describe('default auth (no getUser provided)', () => {
|
|
117
|
+
describe('Auth instance creation', () => {
|
|
118
|
+
it('creates internal Auth instance when getUser is not provided', async () => {
|
|
119
|
+
await connectWallet({ chains: [zkEvmChain] });
|
|
120
|
+
|
|
121
|
+
expect(Auth).toHaveBeenCalledTimes(1);
|
|
122
|
+
expect(Auth).toHaveBeenCalledWith(
|
|
123
|
+
expect.objectContaining({
|
|
124
|
+
scope: 'openid profile email offline_access transact',
|
|
125
|
+
audience: 'platform_api',
|
|
126
|
+
authenticationDomain: 'https://auth.immutable.com',
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('uses getUserOrLogin from internal Auth', async () => {
|
|
132
|
+
await connectWallet({ chains: [zkEvmChain] });
|
|
133
|
+
|
|
134
|
+
// Internal Auth's getUserOrLogin should be called during setup
|
|
135
|
+
expect(mockAuthInstance.getUserOrLogin).toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('derives passportDomain from chain apiUrl', async () => {
|
|
139
|
+
const customChain = {
|
|
140
|
+
...zkEvmChain,
|
|
141
|
+
apiUrl: 'https://api.custom.immutable.com',
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
await connectWallet({ chains: [customChain] });
|
|
145
|
+
|
|
146
|
+
expect(Auth).toHaveBeenCalledWith(
|
|
147
|
+
expect.objectContaining({
|
|
148
|
+
passportDomain: 'https://passport.custom.immutable.com',
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('uses provided passportDomain if specified in chain config', async () => {
|
|
154
|
+
const customChain = {
|
|
155
|
+
...zkEvmChain,
|
|
156
|
+
passportDomain: 'https://custom-passport.immutable.com',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
await connectWallet({ chains: [customChain] });
|
|
89
160
|
|
|
90
|
-
|
|
161
|
+
expect(Auth).toHaveBeenCalledWith(
|
|
162
|
+
expect.objectContaining({
|
|
163
|
+
passportDomain: 'https://custom-passport.immutable.com',
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
});
|
|
91
167
|
|
|
92
|
-
|
|
168
|
+
it('uses default redirect URI fallback', async () => {
|
|
169
|
+
await connectWallet({ chains: [zkEvmChain] });
|
|
170
|
+
|
|
171
|
+
expect(Auth).toHaveBeenCalledWith(
|
|
172
|
+
expect.objectContaining({
|
|
173
|
+
redirectUri: 'https://auth.immutable.com/im-logged-in',
|
|
174
|
+
popupRedirectUri: 'https://auth.immutable.com/im-logged-in',
|
|
175
|
+
logoutRedirectUri: 'https://auth.immutable.com/im-logged-in',
|
|
176
|
+
}),
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('passes popupOverlayOptions to Auth', async () => {
|
|
181
|
+
const popupOverlayOptions = {
|
|
182
|
+
disableGenericPopupOverlay: true,
|
|
183
|
+
disableBlockedPopupOverlay: false,
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
await connectWallet({ chains: [zkEvmChain], popupOverlayOptions });
|
|
187
|
+
|
|
188
|
+
expect(Auth).toHaveBeenCalledWith(
|
|
189
|
+
expect.objectContaining({
|
|
190
|
+
popupOverlayOptions,
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('passes crossSdkBridgeEnabled to Auth', async () => {
|
|
196
|
+
await connectWallet({ chains: [zkEvmChain], crossSdkBridgeEnabled: true });
|
|
197
|
+
|
|
198
|
+
expect(Auth).toHaveBeenCalledWith(
|
|
199
|
+
expect.objectContaining({
|
|
200
|
+
crossSdkBridgeEnabled: true,
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
describe('clientId auto-detection', () => {
|
|
207
|
+
it('uses sandbox client ID for testnet chain (chainId 13473)', async () => {
|
|
208
|
+
const testnetChain = {
|
|
209
|
+
...zkEvmChain,
|
|
210
|
+
chainId: 13473,
|
|
211
|
+
apiUrl: 'https://api.sandbox.immutable.com',
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
await connectWallet({ chains: [testnetChain] });
|
|
215
|
+
|
|
216
|
+
expect(Auth).toHaveBeenCalledWith(
|
|
217
|
+
expect.objectContaining({
|
|
218
|
+
clientId: 'mjtCL8mt06BtbxSkp2vbrYStKWnXVZfo', // Sandbox client ID
|
|
219
|
+
}),
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('uses production client ID for mainnet chain (chainId 13371)', async () => {
|
|
224
|
+
const mainnetChain = {
|
|
225
|
+
chainId: 13371,
|
|
226
|
+
rpcUrl: 'https://rpc.immutable.com',
|
|
227
|
+
relayerUrl: 'https://relayer.immutable.com',
|
|
228
|
+
apiUrl: 'https://api.immutable.com',
|
|
229
|
+
name: 'Immutable zkEVM Mainnet',
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
await connectWallet({ chains: [mainnetChain] });
|
|
233
|
+
|
|
234
|
+
expect(Auth).toHaveBeenCalledWith(
|
|
235
|
+
expect.objectContaining({
|
|
236
|
+
clientId: 'PtQRK4iRJ8GkXjiz6xfImMAYhPhW0cYk', // Production client ID
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('detects sandbox from apiUrl containing "sandbox"', async () => {
|
|
242
|
+
const sandboxChain = {
|
|
243
|
+
chainId: 99999, // unknown chainId
|
|
244
|
+
rpcUrl: 'https://rpc.custom.com',
|
|
245
|
+
relayerUrl: 'https://relayer.custom.com',
|
|
246
|
+
apiUrl: 'https://api.sandbox.custom.com', // "sandbox" in URL
|
|
247
|
+
name: 'Custom Sandbox Chain',
|
|
248
|
+
magicPublishableApiKey: 'pk_test_123',
|
|
249
|
+
magicProviderId: 'provider-123',
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
await connectWallet({ chains: [sandboxChain] });
|
|
253
|
+
|
|
254
|
+
expect(Auth).toHaveBeenCalledWith(
|
|
255
|
+
expect.objectContaining({
|
|
256
|
+
clientId: 'mjtCL8mt06BtbxSkp2vbrYStKWnXVZfo', // Sandbox client ID
|
|
257
|
+
}),
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('detects sandbox from apiUrl containing "testnet"', async () => {
|
|
262
|
+
const testnetChain = {
|
|
263
|
+
chainId: 99999,
|
|
264
|
+
rpcUrl: 'https://rpc.testnet.custom.com',
|
|
265
|
+
relayerUrl: 'https://relayer.testnet.custom.com',
|
|
266
|
+
apiUrl: 'https://api.testnet.custom.com', // "testnet" in URL
|
|
267
|
+
name: 'Custom Testnet Chain',
|
|
268
|
+
magicPublishableApiKey: 'pk_test_123',
|
|
269
|
+
magicProviderId: 'provider-123',
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
await connectWallet({ chains: [testnetChain] });
|
|
273
|
+
|
|
274
|
+
expect(Auth).toHaveBeenCalledWith(
|
|
275
|
+
expect.objectContaining({
|
|
276
|
+
clientId: 'mjtCL8mt06BtbxSkp2vbrYStKWnXVZfo', // Sandbox client ID
|
|
277
|
+
}),
|
|
278
|
+
);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('uses provided clientId when explicitly set', async () => {
|
|
282
|
+
const customClientId = 'custom-client-id-123';
|
|
283
|
+
|
|
284
|
+
await connectWallet({ chains: [zkEvmChain], clientId: customClientId });
|
|
285
|
+
|
|
286
|
+
expect(Auth).toHaveBeenCalledWith(
|
|
287
|
+
expect.objectContaining({
|
|
288
|
+
clientId: customClientId,
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('prefers provided clientId over auto-detected one', async () => {
|
|
294
|
+
const customClientId = 'custom-client-id-456';
|
|
295
|
+
const mainnetChain = {
|
|
296
|
+
chainId: 13371,
|
|
297
|
+
rpcUrl: 'https://rpc.immutable.com',
|
|
298
|
+
relayerUrl: 'https://relayer.immutable.com',
|
|
299
|
+
apiUrl: 'https://api.immutable.com',
|
|
300
|
+
name: 'Immutable zkEVM Mainnet',
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
await connectWallet({ chains: [mainnetChain], clientId: customClientId });
|
|
304
|
+
|
|
305
|
+
// Should use custom, not production client ID
|
|
306
|
+
expect(Auth).toHaveBeenCalledWith(
|
|
307
|
+
expect.objectContaining({
|
|
308
|
+
clientId: customClientId,
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('popup callback handling', () => {
|
|
315
|
+
it('sets up message listener for popup callback', async () => {
|
|
316
|
+
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
|
|
317
|
+
|
|
318
|
+
await connectWallet({ chains: [zkEvmChain] });
|
|
319
|
+
|
|
320
|
+
expect(addEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function));
|
|
321
|
+
|
|
322
|
+
addEventListenerSpy.mockRestore();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('handles OAuth callback message with code and state', async () => {
|
|
326
|
+
let messageHandler: ((event: MessageEvent) => void) | null = null;
|
|
327
|
+
const addEventListenerSpy = jest.spyOn(window, 'addEventListener').mockImplementation((event, handler) => {
|
|
328
|
+
if (event === 'message') {
|
|
329
|
+
messageHandler = handler as (event: MessageEvent) => void;
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
const replaceStateSpy = jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
|
|
334
|
+
|
|
335
|
+
await connectWallet({ chains: [zkEvmChain] });
|
|
336
|
+
|
|
337
|
+
expect(messageHandler).not.toBeNull();
|
|
338
|
+
|
|
339
|
+
// Simulate popup callback message
|
|
340
|
+
const callbackMessage = {
|
|
341
|
+
data: {
|
|
342
|
+
code: 'auth-code-123',
|
|
343
|
+
state: 'state-456',
|
|
344
|
+
},
|
|
345
|
+
} as MessageEvent;
|
|
346
|
+
|
|
347
|
+
// Trigger the message event
|
|
348
|
+
const promise = messageHandler!(callbackMessage);
|
|
349
|
+
|
|
350
|
+
// Wait for async operations
|
|
351
|
+
await promise;
|
|
352
|
+
await Promise.resolve();
|
|
353
|
+
|
|
354
|
+
// Should call Auth.loginCallback
|
|
355
|
+
expect(mockAuthInstance.loginCallback).toHaveBeenCalled();
|
|
356
|
+
|
|
357
|
+
// Should update browser history with code/state
|
|
358
|
+
expect(replaceStateSpy).toHaveBeenCalledTimes(2);
|
|
359
|
+
|
|
360
|
+
addEventListenerSpy.mockRestore();
|
|
361
|
+
replaceStateSpy.mockRestore();
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('ignores messages without code and state', async () => {
|
|
365
|
+
let messageHandler: ((event: MessageEvent) => void) | null = null;
|
|
366
|
+
const addEventListenerSpy = jest.spyOn(window, 'addEventListener').mockImplementation((event, handler) => {
|
|
367
|
+
if (event === 'message') {
|
|
368
|
+
messageHandler = handler as (event: MessageEvent) => void;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
await connectWallet({ chains: [zkEvmChain] });
|
|
373
|
+
|
|
374
|
+
// Simulate non-callback message
|
|
375
|
+
const regularMessage = {
|
|
376
|
+
data: {
|
|
377
|
+
someOtherData: 'value',
|
|
378
|
+
},
|
|
379
|
+
} as MessageEvent;
|
|
380
|
+
|
|
381
|
+
messageHandler!(regularMessage);
|
|
382
|
+
|
|
383
|
+
await Promise.resolve();
|
|
384
|
+
|
|
385
|
+
// Should NOT call Auth.loginCallback
|
|
386
|
+
expect(mockAuthInstance.loginCallback).not.toHaveBeenCalled();
|
|
387
|
+
|
|
388
|
+
addEventListenerSpy.mockRestore();
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('updates query string with code and state during callback', async () => {
|
|
392
|
+
let messageHandler: ((event: MessageEvent) => void) | null = null;
|
|
393
|
+
const addEventListenerSpy = jest.spyOn(window, 'addEventListener').mockImplementation((event, handler) => {
|
|
394
|
+
if (event === 'message') {
|
|
395
|
+
messageHandler = handler as (event: MessageEvent) => void;
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const replaceStateSpy = jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
|
|
400
|
+
|
|
401
|
+
// Mock window.location.search
|
|
402
|
+
Object.defineProperty(window, 'location', {
|
|
403
|
+
value: { search: '?existing=param' },
|
|
404
|
+
writable: true,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
await connectWallet({ chains: [zkEvmChain] });
|
|
408
|
+
|
|
409
|
+
const callbackMessage = {
|
|
410
|
+
data: {
|
|
411
|
+
code: 'test-code',
|
|
412
|
+
state: 'test-state',
|
|
413
|
+
},
|
|
414
|
+
} as MessageEvent;
|
|
415
|
+
|
|
416
|
+
const promise = messageHandler!(callbackMessage);
|
|
417
|
+
await promise;
|
|
418
|
+
await Promise.resolve();
|
|
419
|
+
|
|
420
|
+
// First call: add code and state
|
|
421
|
+
expect(replaceStateSpy).toHaveBeenNthCalledWith(
|
|
422
|
+
1,
|
|
423
|
+
null,
|
|
424
|
+
'',
|
|
425
|
+
expect.stringContaining('code=test-code'),
|
|
426
|
+
);
|
|
427
|
+
expect(replaceStateSpy).toHaveBeenNthCalledWith(
|
|
428
|
+
1,
|
|
429
|
+
null,
|
|
430
|
+
'',
|
|
431
|
+
expect.stringContaining('state=test-state'),
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
// Second call: remove code and state after callback
|
|
435
|
+
expect(replaceStateSpy).toHaveBeenNthCalledWith(
|
|
436
|
+
2,
|
|
437
|
+
null,
|
|
438
|
+
'',
|
|
439
|
+
expect.not.stringContaining('code='),
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
addEventListenerSpy.mockRestore();
|
|
443
|
+
replaceStateSpy.mockRestore();
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
describe('provider creation with default auth', () => {
|
|
448
|
+
it('creates provider successfully without getUser', async () => {
|
|
449
|
+
const provider = await connectWallet({ chains: [zkEvmChain] });
|
|
450
|
+
|
|
451
|
+
expect(provider).toBeDefined();
|
|
452
|
+
expect(ZkEvmProvider).toHaveBeenCalled();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('passes getUser function to ZkEvmProvider', async () => {
|
|
456
|
+
await connectWallet({ chains: [zkEvmChain] });
|
|
457
|
+
|
|
458
|
+
const zkEvmProviderCall = (ZkEvmProvider as jest.Mock).mock.calls[0][0];
|
|
459
|
+
expect(zkEvmProviderCall.getUser).toEqual(expect.any(Function));
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
it('passes clientId to ZkEvmProvider', async () => {
|
|
463
|
+
await connectWallet({ chains: [zkEvmChain] });
|
|
464
|
+
|
|
465
|
+
const zkEvmProviderCall = (ZkEvmProvider as jest.Mock).mock.calls[0][0];
|
|
466
|
+
expect(zkEvmProviderCall.clientId).toBe('mjtCL8mt06BtbxSkp2vbrYStKWnXVZfo');
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('works with custom chain configuration', async () => {
|
|
470
|
+
const customChain = {
|
|
471
|
+
chainId: 99999,
|
|
472
|
+
rpcUrl: 'https://rpc.custom.com',
|
|
473
|
+
relayerUrl: 'https://relayer.custom.com',
|
|
474
|
+
apiUrl: 'https://api.custom.com',
|
|
475
|
+
passportDomain: 'https://passport.custom.com',
|
|
476
|
+
name: 'Custom Chain',
|
|
477
|
+
magicPublishableApiKey: 'pk_test_custom',
|
|
478
|
+
magicProviderId: 'provider-custom',
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
const provider = await connectWallet({ chains: [customChain] });
|
|
482
|
+
|
|
483
|
+
expect(provider).toBeDefined();
|
|
484
|
+
expect(Auth).toHaveBeenCalledWith(
|
|
485
|
+
expect.objectContaining({
|
|
486
|
+
passportDomain: 'https://passport.custom.com',
|
|
487
|
+
}),
|
|
488
|
+
);
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
describe('error handling', () => {
|
|
493
|
+
it('handles auth failure gracefully', async () => {
|
|
494
|
+
mockAuthInstance.getUserOrLogin.mockRejectedValueOnce(new Error('Auth failed'));
|
|
495
|
+
|
|
496
|
+
const provider = await connectWallet({ chains: [zkEvmChain] });
|
|
497
|
+
|
|
498
|
+
// Should still create provider (user will be null)
|
|
499
|
+
expect(provider).toBeDefined();
|
|
500
|
+
expect(ZkEvmProvider).toHaveBeenCalledWith(
|
|
501
|
+
expect.objectContaining({
|
|
502
|
+
user: null,
|
|
503
|
+
}),
|
|
504
|
+
);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
it('handles loginCallback failure gracefully', async () => {
|
|
508
|
+
let messageHandler: ((event: MessageEvent) => void) | null = null;
|
|
509
|
+
const addEventListenerSpy = jest.spyOn(window, 'addEventListener').mockImplementation((event, handler) => {
|
|
510
|
+
if (event === 'message') {
|
|
511
|
+
messageHandler = handler as (event: MessageEvent) => void;
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
mockAuthInstance.loginCallback.mockRejectedValueOnce(new Error('Callback failed'));
|
|
516
|
+
|
|
517
|
+
await connectWallet({ chains: [zkEvmChain] });
|
|
518
|
+
|
|
519
|
+
const callbackMessage = {
|
|
520
|
+
data: {
|
|
521
|
+
code: 'test-code',
|
|
522
|
+
state: 'test-state',
|
|
523
|
+
},
|
|
524
|
+
} as MessageEvent;
|
|
525
|
+
|
|
526
|
+
// Should not throw (error is handled internally)
|
|
527
|
+
await expect(messageHandler!(callbackMessage)).rejects.toThrow('Callback failed');
|
|
528
|
+
|
|
529
|
+
addEventListenerSpy.mockRestore();
|
|
530
|
+
});
|
|
531
|
+
});
|
|
93
532
|
});
|
|
94
533
|
|
|
95
534
|
describe('provider selection', () => {
|