@gluwa/connect-kit 0.1.0-next.3 → 0.2.0-next.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/assets/fonts/Inter-Medium.ttf +0 -0
  3. package/assets/fonts/Inter-Medium.woff2 +0 -0
  4. package/assets/fonts/Inter-Regular.ttf +0 -0
  5. package/assets/fonts/Inter-Regular.woff2 +0 -0
  6. package/assets/fonts/Inter-SemiBold.ttf +0 -0
  7. package/assets/fonts/Inter-SemiBold.woff2 +0 -0
  8. package/assets/graphic.png +0 -0
  9. package/assets/metamask.png +0 -0
  10. package/assets/penguinwallet.png +0 -0
  11. package/assets/wc-logo.svg +3 -0
  12. package/assets/wc-search.png +0 -0
  13. package/dist/chunk-VE5LB7BN.js +405 -0
  14. package/dist/credit-connect.js +2 -394
  15. package/dist/index.css +2075 -0
  16. package/dist/index.d.ts +58 -33
  17. package/dist/index.js +1748 -1503
  18. package/dist/package.json +16 -10
  19. package/package.json +14 -11
  20. package/src/ConnectKitProvider.tsx +133 -75
  21. package/src/api/account.ts +21 -0
  22. package/src/api/asset.ts +10 -0
  23. package/src/api/balance.ts +10 -0
  24. package/src/api/chain.ts +17 -0
  25. package/src/api/contract.ts +33 -0
  26. package/src/api/transaction.ts +12 -0
  27. package/src/assets.d.ts +5 -0
  28. package/src/components/actionItem/index.tsx +50 -0
  29. package/src/components/actionItem/style.scss +47 -0
  30. package/src/components/copyLink/index.tsx +16 -0
  31. package/src/components/copyLink/style.scss +16 -0
  32. package/src/components/qrArea/index.tsx +72 -0
  33. package/src/components/qrArea/style.scss +122 -0
  34. package/src/components/searchBar/asset/Search.tsx +20 -0
  35. package/src/components/searchBar/asset/XMarkCircle.tsx +24 -0
  36. package/src/components/searchBar/index.tsx +44 -0
  37. package/src/components/searchBar/style.scss +58 -0
  38. package/src/components/toast/index.tsx +41 -0
  39. package/src/components/toast/style.scss +108 -0
  40. package/src/components/toggle/index.tsx +27 -0
  41. package/src/components/toggle/style.scss +53 -0
  42. package/src/components/walletGrid/index.tsx +64 -0
  43. package/src/components/walletGrid/style.scss +108 -0
  44. package/src/components/walletItem/index.tsx +49 -0
  45. package/src/components/walletItem/style.scss +70 -0
  46. package/src/connector-meta.ts +16 -11
  47. package/src/core/config.ts +102 -0
  48. package/src/creditConnectConnector.ts +198 -184
  49. package/src/events/account.ts +76 -0
  50. package/src/hooks/useConnectBanner.ts +79 -0
  51. package/src/hooks/useConnectTimeout.ts +92 -0
  52. package/src/hooks/useMetaMaskExtension.ts +29 -0
  53. package/src/hooks/useWCState.ts +45 -14
  54. package/src/hooks/useWagmiConnect.ts +42 -11
  55. package/src/index.ts +38 -2
  56. package/src/layout/allWallets/index.tsx +52 -0
  57. package/src/layout/allWallets/style.scss +57 -0
  58. package/src/layout/connectDialog/asset/backIcon.tsx +24 -0
  59. package/src/layout/connectDialog/asset/checkIcon.tsx +28 -0
  60. package/src/layout/connectDialog/asset/closeIcon.tsx +21 -0
  61. package/src/layout/connectDialog/asset/copyIcon.tsx +28 -0
  62. package/src/layout/connectDialog/asset/linkIcon.tsx +24 -0
  63. package/src/layout/connectDialog/asset/spinner.tsx +21 -0
  64. package/src/layout/connectDialog/asset/warningIcon.tsx +21 -0
  65. package/src/layout/connectDialog/idle/assets/banner-placeholder.png +0 -0
  66. package/src/layout/connectDialog/idle/index.tsx +55 -0
  67. package/src/layout/connectDialog/idle/style.scss +52 -0
  68. package/src/layout/connectDialog/index.tsx +432 -0
  69. package/src/layout/connectDialog/loading/index.tsx +13 -0
  70. package/src/layout/connectDialog/loading/style.scss +19 -0
  71. package/src/layout/connectDialog/metaMask/index.tsx +61 -0
  72. package/src/layout/connectDialog/metaMask/style.scss +65 -0
  73. package/src/layout/connectDialog/qr/index.tsx +33 -0
  74. package/src/layout/connectDialog/style.scss +48 -0
  75. package/src/layout/connectDialog/timeout/index.tsx +31 -0
  76. package/src/layout/connectDialog/timeout/style.scss +64 -0
  77. package/src/layout/connectDialog/walletConnect/index.tsx +45 -0
  78. package/src/layout/connectDialog/walletConnect/style.scss +48 -0
  79. package/src/layout/connectDialog/walletQR/index.tsx +37 -0
  80. package/src/layout/detailPanel/index.tsx +175 -0
  81. package/src/layout/detailPanel/style.scss +70 -0
  82. package/src/layout/selectorPanel/index.tsx +100 -0
  83. package/src/layout/selectorPanel/style.scss +113 -0
  84. package/src/style/color.scss +417 -0
  85. package/src/style/font.scss +28 -0
  86. package/src/style/index.scss +5 -0
  87. package/src/style/mixin.scss +217 -0
  88. package/src/style/reset.scss +110 -0
  89. package/src/style/variables.scss +13 -0
  90. package/src/types.ts +40 -7
  91. package/src/utils/platform.ts +6 -7
  92. package/src/wallet-display-override.ts +15 -0
  93. package/tsconfig.json +5 -1
  94. package/tsup.config.ts +32 -2
  95. package/assets/creditwallet.png +0 -0
  96. package/dist/chunk-DDA6FP6U.js +0 -7
  97. package/src/ConnectModal.scss +0 -665
  98. package/src/ConnectModal.tsx +0 -559
  99. package/src/components/QRFrame.tsx +0 -43
  100. package/src/views/CreditWalletView.tsx +0 -59
  101. package/src/views/IdleView.tsx +0 -10
  102. package/src/views/MetaMaskView.tsx +0 -64
  103. package/src/views/SwitchChainView.tsx +0 -67
  104. package/src/views/WalletConnectView.tsx +0 -206
@@ -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;
@@ -83,18 +84,35 @@ export const creditConnectConnector = (
83
84
  });
84
85
 
85
86
  manager.on('changeState', (state) => {
87
+ // eslint-disable-next-line no-console
88
+ console.log('[CC connector] manager state →', state.status, {
89
+ hasConnectingPayload: Boolean(state.connectingPayload),
90
+ hasSession: 'session' in state && Boolean((state as { session?: unknown }).session),
91
+ });
86
92
  if (state.status === 'CONNECTING' && state.connectingPayload) {
93
+ // eslint-disable-next-line no-console
94
+ console.log('[CC connector] emit display_uri', {
95
+ payloadPrefix: state.connectingPayload.slice(0, 80),
96
+ });
87
97
  options.onDisplayUri?.(state.connectingPayload);
88
98
  emitWagmiMessage?.('display_uri', state.connectingPayload);
89
99
  }
90
100
 
91
101
  if (state.status === 'DISCONNECTED' && !isLocalDisconnect) {
102
+ // eslint-disable-next-line no-console
103
+ console.log('[CC connector] DISCONNECTED (non-local) → resetState + emit');
92
104
  resetState();
93
105
  emitProviderEvent('disconnect');
94
106
  emitWagmiDisconnect?.();
95
107
  }
96
108
  });
97
109
 
110
+ manager.on('sessionExpired', (payload) => {
111
+ // eslint-disable-next-line no-console
112
+ console.log('[CC connector] session expired → emit session_expired', payload);
113
+ emitWagmiMessage?.('session_expired', payload);
114
+ });
115
+
98
116
  return manager;
99
117
  };
100
118
 
@@ -128,6 +146,163 @@ export const creditConnectConnector = (
128
146
  config.emitter.emit('disconnect');
129
147
  };
130
148
 
149
+ const buildProvider = (): CreditConnectProviderWithEvents => {
150
+ const createdProvider = {
151
+ request: async (args) => {
152
+ const { method, params: rawParams } = args as {
153
+ method: string;
154
+ params?: unknown[] | object;
155
+ };
156
+ const params = Array.isArray(rawParams)
157
+ ? [...rawParams]
158
+ : rawParams === undefined
159
+ ? []
160
+ : [rawParams];
161
+ const session = getCurrentSession();
162
+
163
+ const getSelectedAddressInfo = (address?: string) => {
164
+ if (!session) throw new Error('No active session');
165
+ const evmInfo = getEvmAddressInfo(session);
166
+ if (evmInfo.length === 0) throw new Error('No EVM address found');
167
+ if (!address) return evmInfo[0];
168
+ const found = evmInfo.find(
169
+ (info) => info.address.toLowerCase() === address.toLowerCase(),
170
+ );
171
+ if (!found) throw new Error(`Address not in session: ${address}`);
172
+ return found;
173
+ };
174
+
175
+ if (method === 'eth_accounts') {
176
+ if (!session) return [];
177
+ return getChecksummedAccounts(session);
178
+ }
179
+
180
+ if (method === 'eth_chainId') {
181
+ if (!session) throw new Error('No active session');
182
+ if (selectedChainId !== null) return `0x${selectedChainId.toString(16)}`;
183
+ const evmInfo = getEvmAddressInfo(session);
184
+ if (evmInfo.length === 0) throw new Error('No EVM address found');
185
+ const chainId = Number.parseInt(evmInfo[0].networkIdentifier, 10);
186
+ if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${chainId}`);
187
+ selectedChainId = chainId;
188
+ return `0x${chainId.toString(16)}`;
189
+ }
190
+
191
+ if (method === 'wallet_switchEthereumChain') {
192
+ const [{ chainId: rawChainId }] = (params ?? []) as [{ chainId?: string }];
193
+ if (!rawChainId) throw new Error('wallet_switchEthereumChain missing chainId param');
194
+ const chainId = Number.parseInt(rawChainId, 16);
195
+ if (!Number.isFinite(chainId)) throw new Error(`Invalid chainId: ${rawChainId}`);
196
+ if (!session) throw new Error('No active session');
197
+
198
+ const supported = getEvmAddressInfo(session).some(
199
+ (info) => Number.parseInt(info.networkIdentifier, 10) === chainId,
200
+ );
201
+ if (!supported) {
202
+ const err = new Error(`Unsupported chain: ${chainId}`) as Error & { code?: number };
203
+ err.code = 4902;
204
+ throw err;
205
+ }
206
+ selectedChainId = chainId;
207
+ config.emitter.emit('change', { chainId });
208
+ return null;
209
+ }
210
+
211
+ if (method === 'personal_sign') {
212
+ const [messageParam, addressParam] = (params ?? []) as [unknown, unknown];
213
+ const message =
214
+ typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
215
+ const address = typeof addressParam === 'string' ? addressParam : undefined;
216
+ const addressInfo = getSelectedAddressInfo(address);
217
+ const response = await session?.jsonRpc.request('signMessage', {
218
+ message,
219
+ addressInfo,
220
+ });
221
+ return response?.signature;
222
+ }
223
+
224
+ if (method === 'eth_sign') {
225
+ const [addressParam, messageParam] = (params ?? []) as [unknown, unknown];
226
+ const address = typeof addressParam === 'string' ? addressParam : undefined;
227
+ const message =
228
+ typeof messageParam === 'string' ? messageParam : String(messageParam ?? '');
229
+ const addressInfo = getSelectedAddressInfo(address);
230
+ const response = await session?.jsonRpc.request('signMessage', {
231
+ message,
232
+ addressInfo,
233
+ });
234
+ return response?.signature;
235
+ }
236
+
237
+ if (method === 'eth_signTypedData' || method === 'eth_signTypedData_v4') {
238
+ const [addressParam, typedDataParam] = (params ?? []) as [unknown, unknown];
239
+ const address = typeof addressParam === 'string' ? addressParam : undefined;
240
+ const typedData =
241
+ typeof typedDataParam === 'string'
242
+ ? typedDataParam
243
+ : JSON.stringify(typedDataParam ?? '');
244
+ const addressInfo = getSelectedAddressInfo(address);
245
+ const response = await session?.jsonRpc.request('signTypedData', {
246
+ typedData,
247
+ addressInfo,
248
+ });
249
+ return response?.signature;
250
+ }
251
+
252
+ if (method === 'eth_sendTransaction') {
253
+ const [txParams] = (params ?? []) as [Record<string, string | undefined>];
254
+ const addressInfo = getSelectedAddressInfo(txParams?.from);
255
+ const response = await session?.jsonRpc.request('sendTransaction', {
256
+ addressInfo,
257
+ to: txParams?.to,
258
+ value: txParams?.value,
259
+ data: txParams?.data,
260
+ gas: txParams?.gas,
261
+ gasPrice: txParams?.gasPrice,
262
+ maxFeePerGas: txParams?.maxFeePerGas,
263
+ maxPriorityFeePerGas: txParams?.maxPriorityFeePerGas,
264
+ nonce: txParams?.nonce,
265
+ chainId: txParams?.chainId,
266
+ });
267
+ return response?.txHash;
268
+ }
269
+
270
+ if (method === 'credit_connect_send_message') {
271
+ if (!session) throw new Error('No active session');
272
+ const [messageParam] = (params ?? []) as [unknown];
273
+ const message =
274
+ typeof messageParam === 'string' ? messageParam : JSON.stringify(messageParam ?? '');
275
+ await session.msg.sendMessage(message);
276
+ return true;
277
+ }
278
+
279
+ if (method === 'credit_connect_jsonrpc_ping') {
280
+ if (!session) throw new Error('No active session');
281
+ return await session.jsonRpc.request('pingPong', {});
282
+ }
283
+
284
+ throw new Error(`Unsupported provider method: ${method}`);
285
+ },
286
+
287
+ on: (event: string, listener: ProviderListener) => {
288
+ let listeners = providerListeners.get(event);
289
+ if (!listeners) {
290
+ listeners = new Set();
291
+ providerListeners.set(event, listeners);
292
+ }
293
+ listeners.add(listener);
294
+ return createdProvider;
295
+ },
296
+
297
+ removeListener: (event: string, listener: ProviderListener) => {
298
+ providerListeners.get(event)?.delete(listener);
299
+ return createdProvider;
300
+ },
301
+ } as CreditConnectProviderWithEvents;
302
+
303
+ return createdProvider;
304
+ };
305
+
131
306
  return {
132
307
  id: CONNECTOR_ID,
133
308
  name: CONNECTOR_NAME,
@@ -185,32 +360,13 @@ export const creditConnectConnector = (
185
360
  });
186
361
  }
187
362
 
188
- if (m.state.status === 'CONNECTING' && m.state.connectingPayload) {
189
- options.onDisplayUri?.(m.state.connectingPayload);
190
-
191
- if (!connectPromise) {
192
- connectPromise = new Promise<boolean>((resolve) => {
193
- const unsub = m.on('changeState', (state) => {
194
- if (state.status === 'CONNECTED') {
195
- unsub();
196
- resolve(true);
197
- } else if (state.status === 'DISCONNECTED') {
198
- unsub();
199
- resolve(false);
200
- }
201
- });
202
- });
203
- }
204
- }
205
-
363
+ // connect 시도 stale state (CONNECTING/CONNECTING_WAITING_PAYLOAD)
364
+ // stale connectPromise 는 SDK 의 self-heal (cleanupHub + attempt-id) 에 위임.
365
+ // connector 는 단순히 새 connectNewSession() 을 호출 — 이전 attempt 는 자연 정리됨.
206
366
  if (m.state.status !== 'CONNECTED') {
207
- if (!connectPromise) {
208
- if (m.state.status !== 'DISCONNECTED') {
209
- throw new Error(`Cannot connect from ${m.state.status} state`);
210
- }
211
- connectPromise = m.connectNewSession();
212
- }
213
-
367
+ // 이전 attempt 의 await 가 살아있어도, SDK 의 attempt-id 패턴이
368
+ // 결과를 무시하도록 보장한다. connectPromise 는 무조건 새 promise 로.
369
+ connectPromise = m.connectNewSession();
214
370
  const connected = await connectPromise;
215
371
  connectPromise = null;
216
372
 
@@ -261,11 +417,13 @@ export const creditConnectConnector = (
261
417
  throw err;
262
418
  }
263
419
 
420
+ const chain = config.chains.find((c) => c.id === chainId);
421
+ if (!chain) throw new Error(`Chain ${chainId} not in config`);
422
+
264
423
  selectedChainId = chainId;
265
424
  config.emitter.emit('change', { chainId });
425
+ await Promise.resolve();
266
426
 
267
- const chain = config.chains.find((c) => c.id === chainId);
268
- if (!chain) throw new Error(`Chain ${chainId} not in config`);
269
427
  return chain;
270
428
  },
271
429
 
@@ -286,6 +444,7 @@ export const creditConnectConnector = (
286
444
  isLocalDisconnect = false;
287
445
  }
288
446
  provider = null;
447
+ providerPromise = null;
289
448
  resetState();
290
449
  config.emitter.emit('disconnect');
291
450
  },
@@ -314,165 +473,20 @@ export const creditConnectConnector = (
314
473
 
315
474
  async getProvider() {
316
475
  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
- }
476
+ if (providerPromise) return providerPromise;
348
477
 
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
- }
478
+ providerPromise = (async (): Promise<CreditConnectProvider> => {
479
+ await ensureManagerInitialized();
480
+ const createdProvider = buildProvider();
481
+ provider = createdProvider;
482
+ return createdProvider;
483
+ })();
379
484
 
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
- }
392
-
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
- }
420
-
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;
485
+ try {
486
+ return await providerPromise;
487
+ } finally {
488
+ providerPromise = null;
489
+ }
476
490
  },
477
491
 
478
492
  onAccountsChanged(accounts) {
@@ -0,0 +1,76 @@
1
+ import {
2
+ watchAccount as wagmiWatchAccount,
3
+ watchConnections as wagmiWatchConnections,
4
+ reconnect as wagmiReconnect,
5
+ connect as wagmiConnect,
6
+ getAccount as wagmiGetAccount,
7
+ disconnect as wagmiDisconnect,
8
+ type WatchAccountParameters,
9
+ type WatchConnectionsParameters,
10
+ type ReconnectReturnType,
11
+ } from '@wagmi/core';
12
+ import { getConfig, getKitConfig } from '../core/config';
13
+
14
+ export const watchAccount = (params: WatchAccountParameters): (() => void) => {
15
+ return wagmiWatchAccount(getConfig(), params);
16
+ };
17
+
18
+ export const watchConnections = (params: WatchConnectionsParameters): (() => void) => {
19
+ return wagmiWatchConnections(getConfig(), params);
20
+ };
21
+
22
+ export const reconnect = async (): Promise<ReconnectReturnType> => {
23
+ const config = getConfig();
24
+
25
+ if (getKitConfig().autoConnectInjected !== false) {
26
+ try {
27
+ const injected = config.connectors.find((connector) => connector.type === 'injected');
28
+ if (injected) {
29
+ const provider = (await injected.getProvider()) as
30
+ | { request?: (args: { method: string }) => Promise<unknown> }
31
+ | undefined;
32
+ const accounts = provider?.request
33
+ ? await provider.request({ method: 'eth_accounts' })
34
+ : [];
35
+ if (Array.isArray(accounts) && accounts.length > 0) {
36
+ await wagmiConnect(config, { connector: injected });
37
+ }
38
+ }
39
+ } catch (error) {
40
+ // eslint-disable-next-line no-console
41
+ console.warn('injected auto-connect skipped (non-fatal):', error);
42
+ }
43
+ }
44
+
45
+ if (wagmiGetAccount(config).status === 'connected') {
46
+ return [];
47
+ }
48
+ return wagmiReconnect(config);
49
+ };
50
+
51
+ export const disconnectAll = async (
52
+ onLog?: (message: string, error?: Error) => void,
53
+ ): Promise<void> => {
54
+ const config = getConfig();
55
+ const results = await Promise.allSettled(
56
+ config.connectors.map((connector) =>
57
+ wagmiDisconnect(config, { connector }).then(() => connector.id),
58
+ ),
59
+ );
60
+
61
+ const errors: Array<{ connectorId: string; error: Error }> = [];
62
+ results.forEach((result, idx) => {
63
+ if (result.status === 'rejected') {
64
+ const error =
65
+ result.reason instanceof Error ? result.reason : new Error(String(result.reason));
66
+ const connectorId = config.connectors[idx].id;
67
+ errors.push({ connectorId, error });
68
+ onLog?.(`[connect-kit] disconnect failed for "${connectorId}": ${error.message}`, error);
69
+ }
70
+ });
71
+
72
+ if (errors.length === config.connectors.length && errors.length > 0) {
73
+ const messages = errors.map((e) => `${e.connectorId}: ${e.error.message}`).join('; ');
74
+ throw new Error(`All disconnects failed — ${messages}`);
75
+ }
76
+ };
@@ -0,0 +1,79 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import type { ConnectBanner, ConnectBannerSiteKey } from '../types';
3
+
4
+ interface ConnectBannersResponse {
5
+ banners: ConnectBanner[];
6
+ }
7
+
8
+ interface UseConnectBannerParams {
9
+ open: boolean;
10
+ siteKey: ConnectBannerSiteKey;
11
+ url: string;
12
+ }
13
+
14
+ interface UseConnectBannerResult {
15
+ banner: ConnectBanner | null;
16
+ loading: boolean;
17
+ }
18
+
19
+ const pickRandomBanner = (banners: ConnectBanner[]): ConnectBanner | null =>
20
+ banners[Math.floor(Math.random() * banners.length)] ?? null;
21
+
22
+ export const useConnectBanner = ({
23
+ open,
24
+ siteKey,
25
+ url,
26
+ }: UseConnectBannerParams): UseConnectBannerResult => {
27
+ const [candidates, setCandidates] = useState<ConnectBanner[]>([]);
28
+ const [banner, setBanner] = useState<ConnectBanner | null>(null);
29
+ const [loading, setLoading] = useState(true);
30
+ const wasOpenRef = useRef(open);
31
+
32
+ useEffect(() => {
33
+ if (!url) {
34
+ setCandidates([]);
35
+ setBanner(null);
36
+ setLoading(false);
37
+ return;
38
+ }
39
+
40
+ const controller = new AbortController();
41
+ setCandidates([]);
42
+ setBanner(null);
43
+ setLoading(true);
44
+
45
+ fetch(url, { signal: controller.signal })
46
+ .then(async (response) => {
47
+ if (!response.ok) throw new Error(`Failed to fetch connect banners: ${response.status}`);
48
+ return (await response.json()) as ConnectBannersResponse;
49
+ })
50
+ .then(({ banners }) => {
51
+ if (controller.signal.aborted) return;
52
+ if (!Array.isArray(banners)) {
53
+ setLoading(false);
54
+ return;
55
+ }
56
+
57
+ const nextCandidates = banners.filter((item) => item.siteKey !== siteKey);
58
+ setCandidates(nextCandidates);
59
+ setBanner(pickRandomBanner(nextCandidates));
60
+ setLoading(false);
61
+ })
62
+ .catch(() => {
63
+ // API 실패 시 기존 Idle placeholder를 유지한다.
64
+ if (!controller.signal.aborted) setLoading(false);
65
+ });
66
+
67
+ return () => controller.abort();
68
+ }, [siteKey, url]);
69
+
70
+ useEffect(() => {
71
+ const wasOpen = wasOpenRef.current;
72
+ wasOpenRef.current = open;
73
+
74
+ // 닫힌 dialog 안에서 다음 배너 이미지를 미리 렌더링한다.
75
+ if (wasOpen && !open) setBanner(pickRandomBanner(candidates));
76
+ }, [candidates, open]);
77
+
78
+ return { banner, loading };
79
+ };