@gluwa/connect-kit 0.1.0-next.2 → 0.2.0-next.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.
@@ -14,7 +14,7 @@ export const QRFrame: FC<QRFrameProps> = ({ qrUri, logoUrl }) => (
14
14
  size={196}
15
15
  level="H"
16
16
  imageSettings={
17
- logoUrl ? { src: logoUrl, width: 40, height: 40, excavate: true } : undefined
17
+ logoUrl ? { src: logoUrl, width: 50, height: 50, excavate: true } : undefined
18
18
  }
19
19
  />
20
20
  ) : (
@@ -34,7 +34,7 @@ export const CopyLinkButton: FC<CopyLinkButtonProps> = ({ qrUri }) => {
34
34
  type="button"
35
35
  className="ck-copy-link"
36
36
  onClick={() => {
37
- navigator.clipboard.writeText(qrUri);
37
+ navigator.clipboard?.writeText(qrUri).catch(() => {});
38
38
  }}
39
39
  >
40
40
  Copy link
@@ -0,0 +1,83 @@
1
+ import { createConfig, createStorage } from 'wagmi';
2
+ import { metaMask, walletConnect, injected } from 'wagmi/connectors';
3
+ import type { Config } from 'wagmi';
4
+ import type { ConnectKitConfig } from '../types';
5
+
6
+ type KitSlot = {
7
+ config: Config | null;
8
+ kitConfig: ConnectKitConfig | null;
9
+ };
10
+
11
+ const SLOT_KEY = Symbol.for('@gluwa/connect-kit:slot');
12
+
13
+ type GlobalWithSlot = typeof globalThis & {
14
+ [SLOT_KEY]?: KitSlot;
15
+ };
16
+
17
+ const getSlot = (): KitSlot => {
18
+ const g = globalThis as GlobalWithSlot;
19
+ if (!g[SLOT_KEY]) {
20
+ g[SLOT_KEY] = { config: null, kitConfig: null };
21
+ }
22
+ return g[SLOT_KEY];
23
+ };
24
+
25
+ export const initConfig = (kitConfig: ConnectKitConfig): Config => {
26
+ const slot = getSlot();
27
+ if (slot.config) {
28
+ if (slot.kitConfig !== kitConfig) {
29
+ // eslint-disable-next-line no-console
30
+ console.warn(
31
+ '[connect-kit] initConfig called again with a different kitConfig — new chains/transports ignored. Call resetConfig() first to re-init.',
32
+ );
33
+ }
34
+ return slot.config;
35
+ }
36
+
37
+ const connectors = [];
38
+
39
+ if (kitConfig.wcProjectId) {
40
+ connectors.push(walletConnect({ projectId: kitConfig.wcProjectId, showQrModal: false }));
41
+ }
42
+
43
+ connectors.push(metaMask({ storage: { enabled: true } }), injected({ shimDisconnect: false }));
44
+
45
+ if (kitConfig.extraConnectors?.length) {
46
+ connectors.push(...kitConfig.extraConnectors);
47
+ }
48
+
49
+ const isBrowser = typeof window !== 'undefined';
50
+
51
+ slot.config = createConfig({
52
+ ssr: true,
53
+ chains: kitConfig.chains,
54
+ connectors,
55
+ transports: kitConfig.transports,
56
+ storage: isBrowser ? createStorage({ storage: window.localStorage }) : undefined,
57
+ });
58
+ slot.kitConfig = kitConfig;
59
+
60
+ return slot.config;
61
+ };
62
+
63
+ export const getConfig = (): Config => {
64
+ const slot = getSlot();
65
+ if (!slot.config) {
66
+ throw new Error('ConnectKit config not initialized. Make sure ConnectKitProvider is mounted.');
67
+ }
68
+ return slot.config;
69
+ };
70
+
71
+ export const getKitConfig = (): ConnectKitConfig => {
72
+ const slot = getSlot();
73
+ if (!slot.kitConfig) {
74
+ throw new Error('ConnectKit config not initialized. Make sure ConnectKitProvider is mounted.');
75
+ }
76
+ return slot.kitConfig;
77
+ };
78
+
79
+ export const resetConfig = (): void => {
80
+ const slot = getSlot();
81
+ slot.config = null;
82
+ slot.kitConfig = null;
83
+ };
@@ -0,0 +1,2 @@
1
+ export { creditConnectConnector } from './creditConnectConnector';
2
+ export type { CreditConnectConnectorOptions } from './creditConnectConnector';
@@ -41,6 +41,7 @@ export const creditConnectConnector = (
41
41
  let selectedChainId: number | null = null;
42
42
  let connectPromise: Promise<boolean> | null = null;
43
43
  let provider: CreditConnectProvider | null = null;
44
+ let providerPromise: Promise<CreditConnectProvider> | null = null;
44
45
  let isLocalDisconnect = false;
45
46
 
46
47
  let emitWagmiMessage: ((type: string, data: unknown) => void) | null = null;
@@ -128,6 +129,163 @@ export const creditConnectConnector = (
128
129
  config.emitter.emit('disconnect');
129
130
  };
130
131
 
132
+ const buildProvider = (): CreditConnectProviderWithEvents => {
133
+ const createdProvider = {
134
+ request: async (args) => {
135
+ const { method, params: rawParams } = args as {
136
+ method: string;
137
+ params?: unknown[] | object;
138
+ };
139
+ const params = Array.isArray(rawParams)
140
+ ? [...rawParams]
141
+ : rawParams === undefined
142
+ ? []
143
+ : [rawParams];
144
+ const session = getCurrentSession();
145
+
146
+ const getSelectedAddressInfo = (address?: string) => {
147
+ if (!session) throw new Error('No active session');
148
+ const evmInfo = getEvmAddressInfo(session);
149
+ if (evmInfo.length === 0) throw new Error('No EVM address found');
150
+ if (!address) return evmInfo[0];
151
+ const found = evmInfo.find(
152
+ (info) => info.address.toLowerCase() === address.toLowerCase(),
153
+ );
154
+ if (!found) throw new Error(`Address not in session: ${address}`);
155
+ return found;
156
+ };
157
+
158
+ if (method === 'eth_accounts') {
159
+ if (!session) return [];
160
+ return getChecksummedAccounts(session);
161
+ }
162
+
163
+ if (method === 'eth_chainId') {
164
+ if (!session) throw new Error('No active session');
165
+ if (selectedChainId !== null) return `0x${selectedChainId.toString(16)}`;
166
+ const evmInfo = getEvmAddressInfo(session);
167
+ if (evmInfo.length === 0) throw new Error('No EVM address found');
168
+ const chainId = Number.parseInt(evmInfo[0].networkIdentifier, 10);
169
+ if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${chainId}`);
170
+ selectedChainId = chainId;
171
+ return `0x${chainId.toString(16)}`;
172
+ }
173
+
174
+ if (method === 'wallet_switchEthereumChain') {
175
+ const [{ chainId: rawChainId }] = (params ?? []) as [{ chainId?: string }];
176
+ if (!rawChainId) throw new Error('wallet_switchEthereumChain missing chainId param');
177
+ const chainId = Number.parseInt(rawChainId, 16);
178
+ if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${rawChainId}`);
179
+ if (!session) throw new Error('No active session');
180
+
181
+ const supported = getEvmAddressInfo(session).some(
182
+ (info) => Number.parseInt(info.networkIdentifier, 10) === chainId,
183
+ );
184
+ if (!supported) {
185
+ const err = new Error(`Unsupported chain: ${chainId}`) as Error & { code?: number };
186
+ err.code = 4902;
187
+ throw err;
188
+ }
189
+ selectedChainId = chainId;
190
+ config.emitter.emit('change', { chainId });
191
+ return null;
192
+ }
193
+
194
+ if (method === 'personal_sign') {
195
+ const [messageParam, addressParam] = (params ?? []) as [unknown, unknown];
196
+ const message =
197
+ typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
198
+ const address = typeof addressParam === 'string' ? addressParam : undefined;
199
+ const addressInfo = getSelectedAddressInfo(address);
200
+ const response = await session?.jsonRpc.request('signMessage', {
201
+ message,
202
+ addressInfo,
203
+ });
204
+ return response?.signature;
205
+ }
206
+
207
+ if (method === 'eth_sign') {
208
+ const [addressParam, messageParam] = (params ?? []) as [unknown, unknown];
209
+ const address = typeof addressParam === 'string' ? addressParam : undefined;
210
+ const message =
211
+ typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
212
+ const addressInfo = getSelectedAddressInfo(address);
213
+ const response = await session?.jsonRpc.request('signMessage', {
214
+ message,
215
+ addressInfo,
216
+ });
217
+ return response?.signature;
218
+ }
219
+
220
+ if (method === 'eth_signTypedData' || method === 'eth_signTypedData_v4') {
221
+ const [addressParam, typedDataParam] = (params ?? []) as [unknown, unknown];
222
+ const address = typeof addressParam === 'string' ? addressParam : undefined;
223
+ const typedData =
224
+ typeof typedDataParam === 'string'
225
+ ? typedDataParam
226
+ : JSON.stringify(typedDataParam ?? '');
227
+ const addressInfo = getSelectedAddressInfo(address);
228
+ const response = await session?.jsonRpc.request('signTypedData', {
229
+ typedData,
230
+ addressInfo,
231
+ });
232
+ return response?.signature;
233
+ }
234
+
235
+ if (method === 'eth_sendTransaction') {
236
+ const [txParams] = (params ?? []) as [Record<string, string | undefined>];
237
+ const addressInfo = getSelectedAddressInfo(txParams?.from);
238
+ const response = await session?.jsonRpc.request('sendTransaction', {
239
+ addressInfo,
240
+ to: txParams?.to,
241
+ value: txParams?.value,
242
+ data: txParams?.data,
243
+ gas: txParams?.gas,
244
+ gasPrice: txParams?.gasPrice,
245
+ maxFeePerGas: txParams?.maxFeePerGas,
246
+ maxPriorityFeePerGas: txParams?.maxPriorityFeePerGas,
247
+ nonce: txParams?.nonce,
248
+ chainId: txParams?.chainId,
249
+ });
250
+ return response?.txHash;
251
+ }
252
+
253
+ if (method === 'credit_connect_send_message') {
254
+ if (!session) throw new Error('No active session');
255
+ const [messageParam] = (params ?? []) as [unknown];
256
+ const message =
257
+ typeof messageParam === 'string' ? messageParam : JSON.stringify(messageParam ?? '');
258
+ await session.msg.sendMessage(message);
259
+ return true;
260
+ }
261
+
262
+ if (method === 'credit_connect_jsonrpc_ping') {
263
+ if (!session) throw new Error('No active session');
264
+ return await session.jsonRpc.request('pingPong', {});
265
+ }
266
+
267
+ throw new Error(`Unsupported provider method: ${method}`);
268
+ },
269
+
270
+ on: (event: string, listener: ProviderListener) => {
271
+ let listeners = providerListeners.get(event);
272
+ if (!listeners) {
273
+ listeners = new Set();
274
+ providerListeners.set(event, listeners);
275
+ }
276
+ listeners.add(listener);
277
+ return createdProvider;
278
+ },
279
+
280
+ removeListener: (event: string, listener: ProviderListener) => {
281
+ providerListeners.get(event)?.delete(listener);
282
+ return createdProvider;
283
+ },
284
+ } as CreditConnectProviderWithEvents;
285
+
286
+ return createdProvider;
287
+ };
288
+
131
289
  return {
132
290
  id: CONNECTOR_ID,
133
291
  name: CONNECTOR_NAME,
@@ -261,11 +419,13 @@ export const creditConnectConnector = (
261
419
  throw err;
262
420
  }
263
421
 
422
+ const chain = config.chains.find((c) => c.id === chainId);
423
+ if (!chain) throw new Error(`Chain ${chainId} not in config`);
424
+
264
425
  selectedChainId = chainId;
265
426
  config.emitter.emit('change', { chainId });
427
+ await Promise.resolve();
266
428
 
267
- const chain = config.chains.find((c) => c.id === chainId);
268
- if (!chain) throw new Error(`Chain ${chainId} not in config`);
269
429
  return chain;
270
430
  },
271
431
 
@@ -285,6 +445,8 @@ export const creditConnectConnector = (
285
445
  } finally {
286
446
  isLocalDisconnect = false;
287
447
  }
448
+ provider = null;
449
+ providerPromise = null;
288
450
  resetState();
289
451
  config.emitter.emit('disconnect');
290
452
  },
@@ -313,165 +475,20 @@ export const creditConnectConnector = (
313
475
 
314
476
  async getProvider() {
315
477
  if (provider) return provider;
316
- await ensureManagerInitialized();
317
-
318
- const createdProvider = {
319
- request: async (args) => {
320
- const { method, params: rawParams } = args as {
321
- method: string;
322
- params?: unknown[] | object;
323
- };
324
- const params = Array.isArray(rawParams)
325
- ? [...rawParams]
326
- : rawParams === undefined
327
- ? []
328
- : [rawParams];
329
- const session = getCurrentSession();
330
-
331
- const getSelectedAddressInfo = (address?: string) => {
332
- if (!session) throw new Error('No active session');
333
- const evmInfo = getEvmAddressInfo(session);
334
- if (evmInfo.length === 0) throw new Error('No EVM address found');
335
- if (!address) return evmInfo[0];
336
- const found = evmInfo.find(
337
- (info) => info.address.toLowerCase() === address.toLowerCase(),
338
- );
339
- if (!found) throw new Error(`Address not in session: ${address}`);
340
- return found;
341
- };
342
-
343
- if (method === 'eth_accounts') {
344
- if (!session) return [];
345
- return getChecksummedAccounts(session);
346
- }
347
-
348
- if (method === 'eth_chainId') {
349
- if (!session) throw new Error('No active session');
350
- if (selectedChainId !== null) return `0x${selectedChainId.toString(16)}`;
351
- const evmInfo = getEvmAddressInfo(session);
352
- if (evmInfo.length === 0) throw new Error('No EVM address found');
353
- const chainId = Number.parseInt(evmInfo[0].networkIdentifier, 10);
354
- if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${chainId}`);
355
- selectedChainId = chainId;
356
- return `0x${chainId.toString(16)}`;
357
- }
358
-
359
- if (method === 'wallet_switchEthereumChain') {
360
- const [{ chainId: rawChainId }] = (params ?? []) as [{ chainId?: string }];
361
- if (!rawChainId) throw new Error('wallet_switchEthereumChain missing chainId param');
362
- const chainId = Number.parseInt(rawChainId, 16);
363
- if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${rawChainId}`);
364
- if (!session) throw new Error('No active session');
365
-
366
- const supported = getEvmAddressInfo(session).some(
367
- (info) => Number.parseInt(info.networkIdentifier, 10) === chainId,
368
- );
369
- if (!supported) {
370
- const err = new Error(`Unsupported chain: ${chainId}`) as Error & { code?: number };
371
- err.code = 4902;
372
- throw err;
373
- }
374
- selectedChainId = chainId;
375
- config.emitter.emit('change', { chainId });
376
- return null;
377
- }
378
-
379
- if (method === 'personal_sign') {
380
- const [messageParam, addressParam] = (params ?? []) as [unknown, unknown];
381
- const message =
382
- typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
383
- const address = typeof addressParam === 'string' ? addressParam : undefined;
384
- const addressInfo = getSelectedAddressInfo(address);
385
- const response = await session?.jsonRpc.request('signMessage', {
386
- message,
387
- addressInfo,
388
- });
389
- return response?.signature;
390
- }
478
+ if (providerPromise) return providerPromise;
391
479
 
392
- if (method === 'eth_sign') {
393
- const [addressParam, messageParam] = (params ?? []) as [unknown, unknown];
394
- const address = typeof addressParam === 'string' ? addressParam : undefined;
395
- const message =
396
- typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
397
- const addressInfo = getSelectedAddressInfo(address);
398
- const response = await session?.jsonRpc.request('signMessage', {
399
- message,
400
- addressInfo,
401
- });
402
- return response?.signature;
403
- }
404
-
405
- if (method === 'eth_signTypedData' || method === 'eth_signTypedData_v4') {
406
- const [addressParam, typedDataParam] = (params ?? []) as [unknown, unknown];
407
- const address = typeof addressParam === 'string' ? addressParam : undefined;
408
- const typedData =
409
- typeof typedDataParam === 'string'
410
- ? typedDataParam
411
- : JSON.stringify(typedDataParam ?? '');
412
- const addressInfo = getSelectedAddressInfo(address);
413
- const response = await session?.jsonRpc.request('signTypedData', {
414
- typedData,
415
- addressInfo,
416
- });
417
- return response?.signature;
418
- }
480
+ providerPromise = (async (): Promise<CreditConnectProvider> => {
481
+ await ensureManagerInitialized();
482
+ const createdProvider = buildProvider();
483
+ provider = createdProvider;
484
+ return createdProvider;
485
+ })();
419
486
 
420
- if (method === 'eth_sendTransaction') {
421
- const [txParams] = (params ?? []) as [Record<string, string | undefined>];
422
- const addressInfo = getSelectedAddressInfo(txParams?.from);
423
- const response = await session?.jsonRpc.request('sendTransaction', {
424
- addressInfo,
425
- to: txParams?.to,
426
- value: txParams?.value,
427
- data: txParams?.data,
428
- gas: txParams?.gas,
429
- gasPrice: txParams?.gasPrice,
430
- maxFeePerGas: txParams?.maxFeePerGas,
431
- maxPriorityFeePerGas: txParams?.maxPriorityFeePerGas,
432
- nonce: txParams?.nonce,
433
- chainId: txParams?.chainId,
434
- });
435
- return response?.txHash;
436
- }
437
-
438
- if (method === 'credit_connect_send_message') {
439
- if (!session) throw new Error('No active session');
440
- const [messageParam] = (params ?? []) as [unknown];
441
- const message =
442
- typeof messageParam === 'string'
443
- ? messageParam
444
- : JSON.stringify(messageParam ?? '');
445
- await session.msg.sendMessage(message);
446
- return true;
447
- }
448
-
449
- if (method === 'credit_connect_jsonrpc_ping') {
450
- if (!session) throw new Error('No active session');
451
- return await session.jsonRpc.request('pingPong', {});
452
- }
453
-
454
- throw new Error(`Unsupported provider method: ${method}`);
455
- },
456
-
457
- on: (event: string, listener: ProviderListener) => {
458
- let listeners = providerListeners.get(event);
459
- if (!listeners) {
460
- listeners = new Set();
461
- providerListeners.set(event, listeners);
462
- }
463
- listeners.add(listener);
464
- return createdProvider;
465
- },
466
-
467
- removeListener: (event: string, listener: ProviderListener) => {
468
- providerListeners.get(event)?.delete(listener);
469
- return createdProvider;
470
- },
471
- } as CreditConnectProviderWithEvents;
472
-
473
- provider = createdProvider;
474
- return provider;
487
+ try {
488
+ return await providerPromise;
489
+ } finally {
490
+ providerPromise = null;
491
+ }
475
492
  },
476
493
 
477
494
  onAccountsChanged(accounts) {
@@ -0,0 +1,49 @@
1
+ import {
2
+ watchAccount as wagmiWatchAccount,
3
+ watchConnections as wagmiWatchConnections,
4
+ reconnect as wagmiReconnect,
5
+ disconnect as wagmiDisconnect,
6
+ type WatchAccountParameters,
7
+ type WatchConnectionsParameters,
8
+ type ReconnectReturnType,
9
+ } from '@wagmi/core';
10
+ import { getConfig } from '../core/config';
11
+
12
+ export const watchAccount = (params: WatchAccountParameters): (() => void) => {
13
+ return wagmiWatchAccount(getConfig(), params);
14
+ };
15
+
16
+ export const watchConnections = (params: WatchConnectionsParameters): (() => void) => {
17
+ return wagmiWatchConnections(getConfig(), params);
18
+ };
19
+
20
+ export const reconnect = (): Promise<ReconnectReturnType> => {
21
+ return wagmiReconnect(getConfig());
22
+ };
23
+
24
+ export const disconnectAll = async (
25
+ onLog?: (message: string, error?: Error) => void,
26
+ ): Promise<void> => {
27
+ const config = getConfig();
28
+ const results = await Promise.allSettled(
29
+ config.connectors.map((connector) =>
30
+ wagmiDisconnect(config, { connector }).then(() => connector.id),
31
+ ),
32
+ );
33
+
34
+ const errors: Array<{ connectorId: string; error: Error }> = [];
35
+ results.forEach((result, idx) => {
36
+ if (result.status === 'rejected') {
37
+ const error =
38
+ result.reason instanceof Error ? result.reason : new Error(String(result.reason));
39
+ const connectorId = config.connectors[idx].id;
40
+ errors.push({ connectorId, error });
41
+ onLog?.(`[connect-kit] disconnect failed for "${connectorId}": ${error.message}`, error);
42
+ }
43
+ });
44
+
45
+ if (errors.length === config.connectors.length && errors.length > 0) {
46
+ const messages = errors.map((e) => `${e.connectorId}: ${e.error.message}`).join('; ');
47
+ throw new Error(`All disconnects failed — ${messages}`);
48
+ }
49
+ };
@@ -1,18 +1,19 @@
1
- import { useState, useRef, useCallback } from 'react';
1
+ import { useState, useRef, useCallback, useEffect } from 'react';
2
2
  import type { WCWallet, WCSubView } from '../types';
3
3
 
4
- const fetchWCWalletList = async (projectId: string): Promise<WCWallet[]> => {
4
+ const fetchWCWalletList = async (projectId: string, signal?: AbortSignal): Promise<WCWallet[]> => {
5
5
  const all: WCWallet[] = [];
6
6
  let page = 1;
7
7
  const entries = 100;
8
8
 
9
9
  while (page <= 10) {
10
+ if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
10
11
  const url = new URL('https://explorer-api.walletconnect.com/v3/wallets');
11
12
  url.searchParams.set('projectId', projectId);
12
13
  url.searchParams.set('entries', String(entries));
13
14
  url.searchParams.set('page', String(page));
14
15
 
15
- const res = await fetch(url.toString());
16
+ const res = await fetch(url.toString(), { signal });
16
17
  if (!res.ok) throw new Error(`Failed to fetch wallet list: ${res.status}`);
17
18
 
18
19
  const json = (await res.json()) as {
@@ -66,7 +67,6 @@ export interface WCState {
66
67
  onSelectWallet: (wallet: WCWallet) => void;
67
68
  onSearchChange: (q: string) => void;
68
69
  onFilterToggle: () => void;
69
- // Returns true if WC handled the back navigation, false if the parent should handle it
70
70
  handleBack: () => boolean;
71
71
  resetView: () => void;
72
72
  reset: () => void;
@@ -80,18 +80,33 @@ export const useWCState = (): WCState => {
80
80
  const [filterActive, setFilterActive] = useState(false);
81
81
  const [selectedWallet, setSelectedWallet] = useState<WCWallet | null>(null);
82
82
  const loadedRef = useRef(false);
83
+ const abortControllerRef = useRef<AbortController | null>(null);
84
+ const isMountedRef = useRef(true);
85
+
86
+ useEffect(() => {
87
+ isMountedRef.current = true;
88
+ return () => {
89
+ isMountedRef.current = false;
90
+ abortControllerRef.current?.abort();
91
+ abortControllerRef.current = null;
92
+ };
93
+ }, []);
83
94
 
84
95
  const loadWalletList = useCallback(async (projectId: string): Promise<void> => {
85
96
  if (loadedRef.current) return;
86
97
  loadedRef.current = true;
98
+ const controller = new AbortController();
99
+ abortControllerRef.current = controller;
87
100
  setWalletListLoading(true);
88
101
  try {
89
- const list = await fetchWCWalletList(projectId);
90
- setWalletList(list);
91
- } catch {
92
- loadedRef.current = false;
102
+ const list = await fetchWCWalletList(projectId, controller.signal);
103
+ if (isMountedRef.current) setWalletList(list);
104
+ } catch (err) {
105
+ const isAbort = err instanceof DOMException && err.name === 'AbortError';
106
+ if (!isAbort) loadedRef.current = false;
93
107
  } finally {
94
- setWalletListLoading(false);
108
+ if (isMountedRef.current) setWalletListLoading(false);
109
+ if (abortControllerRef.current === controller) abortControllerRef.current = null;
95
110
  }
96
111
  }, []);
97
112
 
@@ -14,7 +14,14 @@ const hasWalletConnectKeyword = (value: string): boolean =>
14
14
  value.toLowerCase().includes('walletconnect');
15
15
 
16
16
  const isMetaMaskConnector = (connector: WagmiConnectorLike): boolean => {
17
- if (connector.id === 'injected' || connector.id === 'metaMaskSDK') return true;
17
+ if (connector.id === 'metaMaskSDK') return true;
18
+
19
+ if (connector.id === 'injected') {
20
+ if (typeof window === 'undefined') return false;
21
+ const eth = (window as Window & { ethereum?: { isMetaMask?: boolean } }).ethereum;
22
+ return eth?.isMetaMask === true;
23
+ }
24
+
18
25
  if (hasMetaMaskKeyword(connector.id) || hasMetaMaskKeyword(connector.name)) return true;
19
26
 
20
27
  if (connector.rdns) {
@@ -70,8 +77,8 @@ export const useWagmiConnect = ({
70
77
  }: Options): UseWagmiConnectResult => {
71
78
  const config = useConfig();
72
79
  const pendingConnectorId = useRef<ConnectorId | null>(null);
80
+ const attemptCounter = useRef(0);
73
81
 
74
- // 콜백 참조
75
82
  const onConnectRef = useRef(onConnect);
76
83
  const onErrorRef = useRef(onError);
77
84
  const onQrUriRef = useRef(onQrUri);
@@ -81,10 +88,8 @@ export const useWagmiConnect = ({
81
88
  onQrUriRef.current = onQrUri;
82
89
  }, [onConnect, onError, onQrUri]);
83
90
 
84
- // 연결 시도
85
91
  const { connectAsync, reset, isPending } = useConnect();
86
92
 
87
- // QR 코드 표시
88
93
  useEffect(() => {
89
94
  const handler = (data: unknown): void => {
90
95
  const msg = data as { type?: string; data?: unknown };
@@ -107,7 +112,6 @@ export const useWagmiConnect = ({
107
112
  };
108
113
  }, [config.connectors]);
109
114
 
110
- // 연결 트리거
111
115
  const triggerConnect = useCallback(
112
116
  (connectorId: ConnectorId): void => {
113
117
  const connector = resolveWagmiConnector(config.connectors, connectorId);
@@ -117,10 +121,11 @@ export const useWagmiConnect = ({
117
121
  }
118
122
 
119
123
  onQrUriRef.current(null);
124
+ const attemptId = ++attemptCounter.current;
120
125
  pendingConnectorId.current = connectorId;
121
126
  connectAsync({ connector })
122
127
  .then((result) => {
123
- if (pendingConnectorId.current !== connectorId) return;
128
+ if (attemptCounter.current !== attemptId) return;
124
129
  const address = result.accounts?.[0];
125
130
  if (!address) {
126
131
  throw new Error('Connected but no account returned');
@@ -129,7 +134,7 @@ export const useWagmiConnect = ({
129
134
  onConnectRef.current({ address, connectorId });
130
135
  })
131
136
  .catch((error) => {
132
- if (pendingConnectorId.current !== connectorId) return;
137
+ if (attemptCounter.current !== attemptId) return;
133
138
  pendingConnectorId.current = null;
134
139
  onErrorRef.current(error as Error, connectorId);
135
140
  });
@@ -137,8 +142,8 @@ export const useWagmiConnect = ({
137
142
  [config.connectors, connectAsync],
138
143
  );
139
144
 
140
- // 연결 취소
141
145
  const cancelConnect = useCallback((): void => {
146
+ attemptCounter.current += 1;
142
147
  pendingConnectorId.current = null;
143
148
  onQrUriRef.current(null);
144
149
  reset();