@gluwa/connect-kit 0.1.0-next.3 → 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.
@@ -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
 
@@ -286,6 +446,7 @@ export const creditConnectConnector = (
286
446
  isLocalDisconnect = false;
287
447
  }
288
448
  provider = null;
449
+ providerPromise = null;
289
450
  resetState();
290
451
  config.emitter.emit('disconnect');
291
452
  },
@@ -314,165 +475,20 @@ export const creditConnectConnector = (
314
475
 
315
476
  async getProvider() {
316
477
  if (provider) return provider;
317
- await ensureManagerInitialized();
318
-
319
- const createdProvider = {
320
- request: async (args) => {
321
- const { method, params: rawParams } = args as {
322
- method: string;
323
- params?: unknown[] | object;
324
- };
325
- const params = Array.isArray(rawParams)
326
- ? [...rawParams]
327
- : rawParams === undefined
328
- ? []
329
- : [rawParams];
330
- const session = getCurrentSession();
331
-
332
- const getSelectedAddressInfo = (address?: string) => {
333
- if (!session) throw new Error('No active session');
334
- const evmInfo = getEvmAddressInfo(session);
335
- if (evmInfo.length === 0) throw new Error('No EVM address found');
336
- if (!address) return evmInfo[0];
337
- const found = evmInfo.find(
338
- (info) => info.address.toLowerCase() === address.toLowerCase(),
339
- );
340
- if (!found) throw new Error(`Address not in session: ${address}`);
341
- return found;
342
- };
343
-
344
- if (method === 'eth_accounts') {
345
- if (!session) return [];
346
- return getChecksummedAccounts(session);
347
- }
348
-
349
- if (method === 'eth_chainId') {
350
- if (!session) throw new Error('No active session');
351
- if (selectedChainId !== null) return `0x${selectedChainId.toString(16)}`;
352
- const evmInfo = getEvmAddressInfo(session);
353
- if (evmInfo.length === 0) throw new Error('No EVM address found');
354
- const chainId = Number.parseInt(evmInfo[0].networkIdentifier, 10);
355
- if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${chainId}`);
356
- selectedChainId = chainId;
357
- return `0x${chainId.toString(16)}`;
358
- }
359
-
360
- if (method === 'wallet_switchEthereumChain') {
361
- const [{ chainId: rawChainId }] = (params ?? []) as [{ chainId?: string }];
362
- if (!rawChainId) throw new Error('wallet_switchEthereumChain missing chainId param');
363
- const chainId = Number.parseInt(rawChainId, 16);
364
- if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${rawChainId}`);
365
- if (!session) throw new Error('No active session');
366
-
367
- const supported = getEvmAddressInfo(session).some(
368
- (info) => Number.parseInt(info.networkIdentifier, 10) === chainId,
369
- );
370
- if (!supported) {
371
- const err = new Error(`Unsupported chain: ${chainId}`) as Error & { code?: number };
372
- err.code = 4902;
373
- throw err;
374
- }
375
- selectedChainId = chainId;
376
- config.emitter.emit('change', { chainId });
377
- return null;
378
- }
379
-
380
- if (method === 'personal_sign') {
381
- const [messageParam, addressParam] = (params ?? []) as [unknown, unknown];
382
- const message =
383
- typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
384
- const address = typeof addressParam === 'string' ? addressParam : undefined;
385
- const addressInfo = getSelectedAddressInfo(address);
386
- const response = await session?.jsonRpc.request('signMessage', {
387
- message,
388
- addressInfo,
389
- });
390
- return response?.signature;
391
- }
478
+ if (providerPromise) return providerPromise;
392
479
 
393
- if (method === 'eth_sign') {
394
- const [addressParam, messageParam] = (params ?? []) as [unknown, unknown];
395
- const address = typeof addressParam === 'string' ? addressParam : undefined;
396
- const message =
397
- typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
398
- const addressInfo = getSelectedAddressInfo(address);
399
- const response = await session?.jsonRpc.request('signMessage', {
400
- message,
401
- addressInfo,
402
- });
403
- return response?.signature;
404
- }
405
-
406
- if (method === 'eth_signTypedData' || method === 'eth_signTypedData_v4') {
407
- const [addressParam, typedDataParam] = (params ?? []) as [unknown, unknown];
408
- const address = typeof addressParam === 'string' ? addressParam : undefined;
409
- const typedData =
410
- typeof typedDataParam === 'string'
411
- ? typedDataParam
412
- : JSON.stringify(typedDataParam ?? '');
413
- const addressInfo = getSelectedAddressInfo(address);
414
- const response = await session?.jsonRpc.request('signTypedData', {
415
- typedData,
416
- addressInfo,
417
- });
418
- return response?.signature;
419
- }
480
+ providerPromise = (async (): Promise<CreditConnectProvider> => {
481
+ await ensureManagerInitialized();
482
+ const createdProvider = buildProvider();
483
+ provider = createdProvider;
484
+ return createdProvider;
485
+ })();
420
486
 
421
- if (method === 'eth_sendTransaction') {
422
- const [txParams] = (params ?? []) as [Record<string, string | undefined>];
423
- const addressInfo = getSelectedAddressInfo(txParams?.from);
424
- const response = await session?.jsonRpc.request('sendTransaction', {
425
- addressInfo,
426
- to: txParams?.to,
427
- value: txParams?.value,
428
- data: txParams?.data,
429
- gas: txParams?.gas,
430
- gasPrice: txParams?.gasPrice,
431
- maxFeePerGas: txParams?.maxFeePerGas,
432
- maxPriorityFeePerGas: txParams?.maxPriorityFeePerGas,
433
- nonce: txParams?.nonce,
434
- chainId: txParams?.chainId,
435
- });
436
- return response?.txHash;
437
- }
438
-
439
- if (method === 'credit_connect_send_message') {
440
- if (!session) throw new Error('No active session');
441
- const [messageParam] = (params ?? []) as [unknown];
442
- const message =
443
- typeof messageParam === 'string'
444
- ? messageParam
445
- : JSON.stringify(messageParam ?? '');
446
- await session.msg.sendMessage(message);
447
- return true;
448
- }
449
-
450
- if (method === 'credit_connect_jsonrpc_ping') {
451
- if (!session) throw new Error('No active session');
452
- return await session.jsonRpc.request('pingPong', {});
453
- }
454
-
455
- throw new Error(`Unsupported provider method: ${method}`);
456
- },
457
-
458
- on: (event: string, listener: ProviderListener) => {
459
- let listeners = providerListeners.get(event);
460
- if (!listeners) {
461
- listeners = new Set();
462
- providerListeners.set(event, listeners);
463
- }
464
- listeners.add(listener);
465
- return createdProvider;
466
- },
467
-
468
- removeListener: (event: string, listener: ProviderListener) => {
469
- providerListeners.get(event)?.delete(listener);
470
- return createdProvider;
471
- },
472
- } as CreditConnectProviderWithEvents;
473
-
474
- provider = createdProvider;
475
- return provider;
487
+ try {
488
+ return await providerPromise;
489
+ } finally {
490
+ providerPromise = null;
491
+ }
476
492
  },
477
493
 
478
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();
package/src/index.ts CHANGED
@@ -1,10 +1,21 @@
1
+ import { readContract, writeContract } from './api/contract';
2
+ import { getBalance } from './api/balance';
3
+ import { switchChain, getPublicClient } from './api/chain';
4
+ import { waitForTransactionReceipt } from './api/transaction';
5
+ import { getAccount, signMessage, signTypedData } from './api/account';
6
+ import { watchAccount, watchConnections, reconnect, disconnectAll } from './events/account';
7
+
1
8
  export { ConnectModal } from './ConnectModal';
2
9
  export { ConnectKitProvider, useConnectKit } from './ConnectKitProvider';
3
10
  export type { ConnectKitContextValue, ConnectKitProviderProps } from './ConnectKitProvider';
11
+
12
+ export { initConfig } from './core/config';
13
+
4
14
  export type {
5
15
  ConnectErrorContext,
6
16
  ConnectErrorReason,
7
17
  ConnectModalProps,
18
+ ConnectKitConfig,
8
19
  Connectors,
9
20
  ConnectResult,
10
21
  ConnectorId,
@@ -12,3 +23,25 @@ export type {
12
23
  WCSubView,
13
24
  WCWallet,
14
25
  } from './types';
26
+
27
+ export const connectKit = {
28
+ api: {
29
+ readContract,
30
+ writeContract,
31
+ getBalance,
32
+ switchChain,
33
+ getPublicClient,
34
+ waitForTransactionReceipt,
35
+ getAccount,
36
+ signMessage,
37
+ signTypedData,
38
+ },
39
+ events: {
40
+ watchAccount,
41
+ watchConnections,
42
+ reconnect,
43
+ disconnectAll,
44
+ },
45
+ } as const;
46
+
47
+ export type * from '@wagmi/core';
package/src/types.ts CHANGED
@@ -1,7 +1,16 @@
1
1
  import type { ReactNode } from 'react';
2
+ import type { Chain, Transport } from 'viem';
3
+ import type { CreateConnectorFn } from 'wagmi';
2
4
 
3
5
  export type ConnectorId = 'CREDIT_WALLET' | 'CREDIT_CONNECT' | 'METAMASK' | 'WALLET_CONNECT';
4
6
 
7
+ export interface ConnectKitConfig {
8
+ chains: [Chain, ...Chain[]];
9
+ transports: Record<number, Transport>;
10
+ wcProjectId?: string;
11
+ extraConnectors?: CreateConnectorFn[];
12
+ }
13
+
5
14
  export type CreditWalletStrategy = 'walletConnect' | 'creditConnect';
6
15
 
7
16
  export interface Connectors {
@@ -1,16 +1,13 @@
1
- // 모바일 기기 감지
2
1
  export const isMobileDevice = (): boolean => {
3
2
  if (typeof window === 'undefined') return false;
4
3
  return /android|iphone|ipad|ipod/i.test(navigator.userAgent);
5
4
  };
6
5
 
7
- // 딥링크 열기 (walletConnect가 지원하는 지갑앱)
8
6
  export const tryOpenDeepLink = (uri: string, deepLinkBase: string): void => {
9
7
  const sep = deepLinkBase.endsWith('/') ? '' : '/';
10
- window.location.href = `${deepLinkBase}${sep}wc?uri=${encodeURIComponent(uri)}`;
8
+ window.location.replace(`${deepLinkBase}${sep}wc?uri=${encodeURIComponent(uri)}`);
11
9
  };
12
10
 
13
- // metaMask extension 감지
14
11
  export const detectMetaMaskExtension = (): boolean =>
15
12
  typeof window !== 'undefined' &&
16
13
  Boolean((window as Window & { ethereum?: { isMetaMask?: boolean } }).ethereum?.isMetaMask);
@@ -12,7 +12,6 @@ interface CreditWalletViewProps {
12
12
  export const CreditWalletView: FC<CreditWalletViewProps> = ({ connectorId, qrUri }) => {
13
13
  const { downloadUrl, logoUrl, deepLinkBase } = CONNECTOR_META[connectorId];
14
14
  const isMobile = useMemo(() => isMobileDevice(), []);
15
- // WC 세션 만료로 qrUri가 재발급되더라도 딥링크는 최초 1회만 실행
16
15
  const hasRedirectedRef = useRef(false);
17
16
 
18
17
  useEffect(() => {
@@ -1,4 +1,4 @@
1
- import { type FC, useEffect, useMemo } from 'react';
1
+ import { type FC, useEffect, useMemo, useRef } from 'react';
2
2
  import { isMobileDevice, tryOpenDeepLink } from '../utils/platform';
3
3
  import { CONNECTOR_META } from '../connector-meta';
4
4
  import { QRFrame, CopyLinkButton } from '../components/QRFrame';
@@ -13,9 +13,11 @@ interface MetaMaskViewProps {
13
13
 
14
14
  export const MetaMaskView: FC<MetaMaskViewProps> = ({ qrUri, hasExtension, logoUrl }) => {
15
15
  const isMobile = useMemo(() => isMobileDevice(), []);
16
+ const hasRedirectedRef = useRef(false);
16
17
 
17
18
  useEffect(() => {
18
- if (isMobile && qrUri && deepLinkBase) {
19
+ if (isMobile && qrUri && deepLinkBase && !hasRedirectedRef.current) {
20
+ hasRedirectedRef.current = true;
19
21
  tryOpenDeepLink(qrUri, deepLinkBase);
20
22
  }
21
23
  }, [isMobile, qrUri]);