@gluwa/connect-kit 0.1.0-next.0 → 0.1.0-next.2

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.
@@ -1,16 +1,25 @@
1
- import { useState, useEffect, useCallback, useRef, useMemo, type FC } from 'react';
1
+ import { useState, useEffect, useCallback, useRef, useMemo, type FC, type ReactNode } from 'react';
2
2
  import './ConnectModal.scss';
3
- import { useConfig } from 'wagmi';
4
- import { type ConnectModalProps, type ConnectorId, type WCWallet, type WCSubView } from './types';
3
+ import { useAccount, useConfig, useDisconnect } from 'wagmi';
4
+ import {
5
+ type ConnectModalProps,
6
+ type ConnectErrorContext,
7
+ type ConnectErrorReason,
8
+ type Connectors,
9
+ type ConnectorId,
10
+ type WCWallet,
11
+ type WCSubView,
12
+ } from './types';
5
13
  import { CONNECTOR_META } from './connector-meta';
6
14
  import { detectMetaMaskExtension } from './utils/platform';
7
- import { useWagmiConnect, WAGMI_CONNECTOR_ID } from './hooks/useWagmiConnect';
15
+ import { useWagmiConnect, resolveWagmiConnector } from './hooks/useWagmiConnect';
16
+ import { useWCState, type WCState } from './hooks/useWCState';
8
17
  import { IdleView } from './views/IdleView';
9
18
  import { CreditWalletView } from './views/CreditWalletView';
10
19
  import { MetaMaskView } from './views/MetaMaskView';
11
20
  import { WalletConnectView } from './views/WalletConnectView';
21
+ import { SwitchChainView } from './views/SwitchChainView';
12
22
 
13
- // 최근 연결 기록
14
23
  const RECENT_KEY = 'connect-kit:recent';
15
24
  const readRecentConnector = (): ConnectorId | null => {
16
25
  try {
@@ -26,58 +35,16 @@ const writeRecentConnector = (id: ConnectorId): void => {
26
35
  } catch {}
27
36
  };
28
37
 
29
- // WC 지갑 목록 가져오기
30
- const fetchWCWalletList = async (projectId: string): Promise<WCWallet[]> => {
31
- const all: WCWallet[] = [];
32
- let page = 1;
33
- const entries = 100;
34
-
35
- while (page <= 10) {
36
- const url = new URL('https://explorer-api.walletconnect.com/v3/wallets');
37
- url.searchParams.set('projectId', projectId);
38
- url.searchParams.set('entries', String(entries));
39
- url.searchParams.set('page', String(page));
40
-
41
- const res = await fetch(url.toString());
42
- if (!res.ok) throw new Error(`Failed to fetch wallet list: ${res.status}`);
43
-
44
- const json = (await res.json()) as {
45
- listings: Record<
46
- string,
47
- {
48
- id: string;
49
- name: string;
50
- homepage?: string;
51
- image_url?: { sm?: string; md?: string };
52
- mobile?: { native?: string; universal?: string };
53
- desktop?: { native?: string; universal?: string };
54
- app?: { ios?: string; android?: string };
55
- }
56
- >;
57
- total: number;
58
- };
59
-
60
- const listings = Object.values(json.listings ?? {});
61
- for (const w of listings) {
62
- all.push({
63
- id: w.id,
64
- name: w.name,
65
- imageUrl: w.image_url?.md ?? w.image_url?.sm ?? '',
66
- mobileNative: w.mobile?.native ?? '',
67
- mobileUniversal: w.mobile?.universal ?? '',
68
- desktopNative: w.desktop?.native ?? '',
69
- desktopUniversal: w.desktop?.universal ?? '',
70
- appIos: w.app?.ios ?? '',
71
- appAndroid: w.app?.android ?? '',
72
- homepage: w.homepage ?? '',
73
- });
74
- }
75
-
76
- if (listings.length < entries || all.length >= json.total) break;
77
- page += 1;
78
- }
79
-
80
- return all;
38
+ const classifyError = (error: Error): ConnectErrorReason => {
39
+ const code =
40
+ typeof error === 'object' && error !== null && 'code' in error
41
+ ? (error as unknown as { code: unknown }).code
42
+ : undefined;
43
+ if (code === 4001) return 'rejected';
44
+ const message = error.message?.toLowerCase() ?? '';
45
+ if (message.includes('user rejected') || message.includes('user denied')) return 'rejected';
46
+ if (message.includes('unsupported') || message.includes('not installed')) return 'unsupported';
47
+ return 'unknown';
81
48
  };
82
49
 
83
50
  interface SelectorPaneProps {
@@ -196,18 +163,10 @@ interface DetailPaneProps {
196
163
  qrUri: string | null;
197
164
  hasMetaMaskExtension: boolean;
198
165
  connectorLogoMap: Partial<Record<ConnectorId, string>>;
199
- wcSubView: WCSubView;
200
- wcWalletList: WCWallet[];
201
- wcWalletListLoading: boolean;
202
- wcWalletListSearch: string;
203
- wcSelectedWallet: WCWallet | null;
166
+ wc: WCState;
167
+ errorOverride: ReactNode | null;
204
168
  onBack: () => void;
205
169
  onClose: () => void;
206
- wcFilterActive: boolean;
207
- onWCShowList: () => void;
208
- onWCSelectWallet: (wallet: WCWallet) => void;
209
- onWCSearchChange: (q: string) => void;
210
- onWCFilterToggle: () => void;
211
170
  }
212
171
 
213
172
  const DetailPane: FC<DetailPaneProps> = ({
@@ -215,24 +174,16 @@ const DetailPane: FC<DetailPaneProps> = ({
215
174
  qrUri,
216
175
  hasMetaMaskExtension,
217
176
  connectorLogoMap,
218
- wcSubView,
219
- wcWalletList,
220
- wcWalletListLoading,
221
- wcWalletListSearch,
222
- wcSelectedWallet,
223
- wcFilterActive,
177
+ wc,
178
+ errorOverride,
224
179
  onBack,
225
180
  onClose,
226
- onWCShowList,
227
- onWCSelectWallet,
228
- onWCSearchChange,
229
- onWCFilterToggle,
230
181
  }) => {
231
182
  const title = getDetailTitle(
232
183
  selectedConnector,
233
184
  hasMetaMaskExtension,
234
- wcSubView,
235
- wcSelectedWallet,
185
+ wc.subView,
186
+ wc.selectedWallet,
236
187
  );
237
188
  const showBack = selectedConnector !== null;
238
189
 
@@ -247,35 +198,39 @@ const DetailPane: FC<DetailPaneProps> = ({
247
198
  </div>
248
199
 
249
200
  <div className="ck-pane__body">
250
- {!selectedConnector && <IdleView />}
201
+ {errorOverride || (
202
+ <>
203
+ {!selectedConnector && <IdleView />}
251
204
 
252
- {(selectedConnector === 'CREDIT_CONNECT' || selectedConnector === 'CREDIT_WALLET') && (
253
- <CreditWalletView connectorId={selectedConnector} qrUri={qrUri} />
254
- )}
205
+ {(selectedConnector === 'CREDIT_CONNECT' || selectedConnector === 'CREDIT_WALLET') && (
206
+ <CreditWalletView connectorId={selectedConnector} qrUri={qrUri} />
207
+ )}
255
208
 
256
- {selectedConnector === 'METAMASK' && (
257
- <MetaMaskView
258
- qrUri={qrUri}
259
- hasExtension={hasMetaMaskExtension}
260
- logoUrl={connectorLogoMap.METAMASK}
261
- />
262
- )}
209
+ {selectedConnector === 'METAMASK' && (
210
+ <MetaMaskView
211
+ qrUri={qrUri}
212
+ hasExtension={hasMetaMaskExtension}
213
+ logoUrl={connectorLogoMap.METAMASK}
214
+ />
215
+ )}
263
216
 
264
- {selectedConnector === 'WALLET_CONNECT' && (
265
- <WalletConnectView
266
- subView={wcSubView}
267
- qrUri={qrUri}
268
- logoUrl={connectorLogoMap.WALLET_CONNECT}
269
- walletList={wcWalletList}
270
- walletListLoading={wcWalletListLoading}
271
- walletListSearch={wcWalletListSearch}
272
- walletListFilterActive={wcFilterActive}
273
- selectedWallet={wcSelectedWallet}
274
- onShowList={onWCShowList}
275
- onSelectWallet={onWCSelectWallet}
276
- onSearchChange={onWCSearchChange}
277
- onFilterToggle={onWCFilterToggle}
278
- />
217
+ {selectedConnector === 'WALLET_CONNECT' && (
218
+ <WalletConnectView
219
+ subView={wc.subView}
220
+ qrUri={qrUri}
221
+ logoUrl={connectorLogoMap.WALLET_CONNECT}
222
+ walletList={wc.walletList}
223
+ walletListLoading={wc.walletListLoading}
224
+ walletListSearch={wc.walletListSearch}
225
+ walletListFilterActive={wc.filterActive}
226
+ selectedWallet={wc.selectedWallet}
227
+ onShowList={wc.onShowList}
228
+ onSelectWallet={wc.onSelectWallet}
229
+ onSearchChange={wc.onSearchChange}
230
+ onFilterToggle={wc.onFilterToggle}
231
+ />
232
+ )}
233
+ </>
279
234
  )}
280
235
  </div>
281
236
  </div>
@@ -301,42 +256,78 @@ const getDetailTitle = (
301
256
  return CONNECTOR_META[connector].detailTitle;
302
257
  };
303
258
 
259
+ const resolveConnectorsFromConfig = (connectorsConfig?: Connectors): ConnectorId[] => {
260
+ if (!connectorsConfig) return [];
261
+
262
+ const connectors: ConnectorId[] = [];
263
+
264
+ if (connectorsConfig.creditWallet === 'walletConnect') connectors.push('CREDIT_WALLET');
265
+ if (connectorsConfig.creditWallet === 'creditConnect') connectors.push('CREDIT_CONNECT');
266
+ if (connectorsConfig.metamask) connectors.push('METAMASK');
267
+ if (connectorsConfig.walletConnect) connectors.push('WALLET_CONNECT');
268
+
269
+ return connectors;
270
+ };
271
+
304
272
  export const ConnectModal: FC<ConnectModalProps> = (props) => {
273
+ // open=false면 내부 컴포넌트를 마운트하지 않음
274
+ // → useConfig 등 wagmi hooks가 실행되지 않아 WagmiProvider 없이도 렌더링 가능
275
+ if (!props.open) return null;
276
+
277
+ const resolvedConnectors = resolveConnectorsFromConfig(props.connectors);
278
+
305
279
  if (
306
- (props.connectors as ConnectorId[]).includes('CREDIT_WALLET') &&
307
- (props.connectors as ConnectorId[]).includes('CREDIT_CONNECT')
280
+ resolvedConnectors.includes('CREDIT_WALLET') &&
281
+ resolvedConnectors.includes('CREDIT_CONNECT')
308
282
  ) {
309
283
  throw new Error('CREDIT_WALLET and CREDIT_CONNECT cannot be used together');
310
284
  }
311
- // open=false면 내부 컴포넌트를 마운트하지 않음
312
- // useConfig wagmi hooks가 실행되지 않아 WagmiProvider 없이도 렌더링 가능
313
- if (!props.open) return null;
314
- return <ConnectModalInner {...props} />;
285
+ if (resolvedConnectors.length === 0) {
286
+ throw new Error('At least one connector must be configured');
287
+ }
288
+ return <ConnectModalInner {...props} connectors={resolvedConnectors} />;
289
+ };
290
+
291
+ type ConnectModalInnerProps = Omit<ConnectModalProps, 'connectors'> & {
292
+ connectors: ConnectorId[];
315
293
  };
316
294
 
317
- const ConnectModalInner: FC<ConnectModalProps> = ({
295
+ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
318
296
  open,
319
297
  connectors,
320
298
  onConnect,
321
299
  onClose,
322
300
  onLog,
323
301
  wcProjectId,
302
+ requiredChainId,
303
+ renderConnectError,
324
304
  }) => {
325
305
  const [selectedConnector, setSelectedConnector] = useState<ConnectorId | null>(null);
326
- // 커넥터별 QR URI 캐시 — 만료 전까지 재사용
327
306
  const [qrUriMap, setQrUriMap] = useState<Partial<Record<ConnectorId, string>>>({});
328
- // handleQrUri 호출 시점에 어떤 커넥터의 QR인지 추적
329
307
  const pendingQrConnectorRef = useRef<ConnectorId | null>(null);
330
308
  const [recentConnector, setRecentConnector] = useState<ConnectorId | null>(null);
331
309
  const [hasMetaMaskExtension, setHasMetaMaskExtension] = useState<boolean>(false);
310
+ const [errorState, setErrorState] = useState<{ connectorId: ConnectorId; error: Error } | null>(
311
+ null,
312
+ );
313
+
314
+ const wc = useWCState();
315
+
316
+ // wagmi 상태 — chain mismatch invariant 판정용
317
+ const account = useAccount();
318
+ const { disconnectAsync } = useDisconnect();
319
+ const wagmiConfig = useConfig();
332
320
 
333
- const [wcSubView, setWcSubView] = useState<WCSubView>('qr');
334
- const [wcWalletList, setWcWalletList] = useState<WCWallet[]>([]);
335
- const [wcWalletListLoading, setWcWalletListLoading] = useState<boolean>(false);
336
- const [wcWalletListSearch, setWcWalletListSearch] = useState<string>('');
337
- const [wcFilterActive, setWcFilterActive] = useState<boolean>(false);
338
- const [wcSelectedWallet, setWcSelectedWallet] = useState<WCWallet | null>(null);
339
- const wcLoadedRef = useRef<boolean>(false);
321
+ const chainMismatch =
322
+ requiredChainId != null &&
323
+ account.status === 'connected' &&
324
+ account.chainId != null &&
325
+ account.chainId !== requiredChainId;
326
+
327
+ const requiredChainName = useMemo<string | undefined>(() => {
328
+ if (requiredChainId == null) return undefined;
329
+ return wagmiConfig.chains.find((chain) => chain.id === requiredChainId)?.name;
330
+ }, [wagmiConfig.chains, requiredChainId]);
340
331
 
341
332
  // QR URI 수신 — 커넥터별 캐시에 저장 (null은 무시하여 캐시 유지)
342
333
  const handleQrUri = useCallback((uri: string | null) => {
@@ -349,7 +340,10 @@ const ConnectModalInner: FC<ConnectModalProps> = ({
349
340
  // 연결 완료
350
341
  const handleConnect = useCallback(
351
342
  (result: Parameters<typeof onConnect>[0]) => {
343
+ setErrorState(null);
352
344
  onConnect(result);
345
+ // chain mismatch가 있으면 모달은 SwitchChainView로 자동 전환됨 (닫지 않음)
346
+ // Provider 측에서 requiredChainId가 매칭될 때까지 force-open 유지
353
347
  onClose();
354
348
  },
355
349
  [onConnect, onClose],
@@ -357,18 +351,25 @@ const ConnectModalInner: FC<ConnectModalProps> = ({
357
351
 
358
352
  // 연결 실패 — 실패한 커넥터의 QR 캐시만 삭제
359
353
  const handleError = useCallback(
360
- (error: Error, connectorId: ConnectorId): void => {
361
- onLog?.(`[connect-kit] connection failed (${connectorId}): ${error.message}`, error);
362
- setSelectedConnector(null);
354
+ (error: unknown, connectorId: ConnectorId): void => {
355
+ const normalizedError = error instanceof Error ? error : new Error(String(error));
356
+ onLog?.(
357
+ `[connect-kit] connection failed (${connectorId}): ${normalizedError.message}`,
358
+ normalizedError,
359
+ );
363
360
  setQrUriMap((prev) => {
364
361
  const next = { ...prev };
365
362
  delete next[connectorId];
366
363
  return next;
367
364
  });
368
- setWcSubView('qr');
369
- setWcSelectedWallet(null);
365
+ wc.resetView();
366
+ if (renderConnectError) {
367
+ setErrorState({ connectorId, error: normalizedError });
368
+ } else {
369
+ setSelectedConnector(null);
370
+ }
370
371
  },
371
- [onLog],
372
+ [onLog, wc.resetView, renderConnectError],
372
373
  );
373
374
 
374
375
  // 연결 시도
@@ -379,13 +380,16 @@ const ConnectModalInner: FC<ConnectModalProps> = ({
379
380
  });
380
381
 
381
382
  // wagmi connector icon 매핑
382
- const wagmiConfig = useConfig();
383
383
  const connectorLogoMap = useMemo(() => {
384
384
  const map: Partial<Record<ConnectorId, string>> = {};
385
- for (const [connectorId, wagmiId] of Object.entries(WAGMI_CONNECTOR_ID) as Array<
386
- [ConnectorId, string]
387
- >) {
388
- const wagmiConnector = wagmiConfig.connectors.find((c) => c.id === wagmiId);
385
+ const connectorIds: ConnectorId[] = [
386
+ 'CREDIT_CONNECT',
387
+ 'CREDIT_WALLET',
388
+ 'METAMASK',
389
+ 'WALLET_CONNECT',
390
+ ];
391
+ for (const connectorId of connectorIds) {
392
+ const wagmiConnector = resolveWagmiConnector(wagmiConfig.connectors, connectorId);
389
393
  if (wagmiConnector?.icon) map[connectorId] = wagmiConnector.icon;
390
394
  }
391
395
  return map;
@@ -403,89 +407,140 @@ const ConnectModalInner: FC<ConnectModalProps> = ({
403
407
  setSelectedConnector(null);
404
408
  setQrUriMap({});
405
409
  pendingQrConnectorRef.current = null;
406
- setWcSubView('qr');
407
- setWcWalletListSearch('');
408
- setWcFilterActive(false);
409
- setWcSelectedWallet(null);
410
+ setErrorState(null);
411
+ wc.reset();
410
412
  cancelConnect();
411
413
  }
412
- }, [open, cancelConnect]);
413
-
414
- // WC 지갑 목록 로드
415
- const loadWCWalletList = useCallback(async (projectId: string): Promise<void> => {
416
- if (wcLoadedRef.current) return;
417
- wcLoadedRef.current = true;
418
- setWcWalletListLoading(true);
419
- try {
420
- const list = await fetchWCWalletList(projectId);
421
- setWcWalletList(list);
422
- } catch {
423
- wcLoadedRef.current = false;
424
- } finally {
425
- setWcWalletListLoading(false);
426
- }
427
- }, []);
414
+ }, [open, cancelConnect, wc.reset]);
428
415
 
429
- // 커넥터 선택 — 같은 커넥터 재선택 시 무시, 다른 커넥터 선택 시 이전 연결 취소 후 새 연결
416
+ // 커넥터 선택
430
417
  const handleSelectConnector = useCallback(
431
418
  (id: ConnectorId): void => {
432
- if (id === selectedConnector) return;
433
-
419
+ if (id === selectedConnector && !errorState) return;
434
420
  pendingQrConnectorRef.current = id;
435
421
  setSelectedConnector(id);
436
- setWcSubView('qr');
437
- setWcSelectedWallet(null);
422
+ setErrorState(null);
423
+ wc.resetView();
438
424
  writeRecentConnector(id);
439
425
  setRecentConnector(id);
440
426
 
441
427
  if (id === 'WALLET_CONNECT' && wcProjectId) {
442
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
443
- loadWCWalletList(wcProjectId);
428
+ wc.loadWalletList(wcProjectId);
444
429
  }
445
430
 
446
431
  cancelConnect();
447
432
  triggerConnect(id);
448
433
  },
449
- [selectedConnector, triggerConnect, cancelConnect, wcProjectId, loadWCWalletList],
434
+ [
435
+ selectedConnector,
436
+ errorState,
437
+ triggerConnect,
438
+ cancelConnect,
439
+ wcProjectId,
440
+ wc.resetView,
441
+ wc.loadWalletList,
442
+ ],
450
443
  );
451
444
 
452
- // WC 서브뷰 핸들러
453
- const handleWCShowList = useCallback((): void => {
454
- setWcSubView('list');
455
- }, []);
456
-
457
- const handleWCSelectWallet = useCallback((wallet: WCWallet): void => {
458
- setWcSelectedWallet(wallet);
459
- setWcSubView('wallet');
460
- }, []);
461
-
462
- const handleWCFilterToggle = useCallback((): void => {
463
- setWcFilterActive((prev) => !prev);
464
- }, []);
465
-
466
445
  // 뒤로가기 — QR 캐시는 유지, wagmi만 reset
467
446
  const handleBack = useCallback((): void => {
468
- if (selectedConnector === 'WALLET_CONNECT') {
469
- if (wcSubView === 'wallet') {
470
- setWcSelectedWallet(null);
471
- setWcSubView('list');
472
- return;
473
- }
474
- if (wcSubView === 'list') {
475
- setWcSubView('qr');
476
- return;
477
- }
447
+ if (errorState) {
448
+ setErrorState(null);
449
+ setSelectedConnector(null);
450
+ return;
478
451
  }
452
+ if (selectedConnector === 'WALLET_CONNECT' && wc.handleBack()) return;
479
453
  pendingQrConnectorRef.current = null;
480
454
  setSelectedConnector(null);
481
455
  cancelConnect();
482
- }, [selectedConnector, wcSubView, cancelConnect]);
456
+ }, [selectedConnector, errorState, wc.handleBack, cancelConnect]);
457
+
458
+ // chain mismatch 상태에서의 disconnect (Switch chain pane의 cancel)
459
+ const handleSwitchChainDisconnect = useCallback((): void => {
460
+ disconnectAsync().catch((err: unknown) => {
461
+ const normalized = err instanceof Error ? err : new Error(String(err));
462
+ onLog?.(`[connect-kit] disconnect failed: ${normalized.message}`, normalized);
463
+ });
464
+ }, [disconnectAsync, onLog]);
465
+
466
+ // overlay 클릭 — chain mismatch 상태에선 무시 (실수 클릭 방어)
467
+ const handleOverlayClick = useCallback((): void => {
468
+ if (chainMismatch) return;
469
+ onClose();
470
+ }, [chainMismatch, onClose]);
471
+
472
+ // close 버튼 — chain mismatch 상태에선 disconnect
473
+ const handleCloseClick = useCallback((): void => {
474
+ if (chainMismatch) {
475
+ handleSwitchChainDisconnect();
476
+ return;
477
+ }
478
+ onClose();
479
+ }, [chainMismatch, handleSwitchChainDisconnect, onClose]);
480
+
481
+ const renderedError: ReactNode = useMemo(() => {
482
+ if (!errorState || !renderConnectError) return null;
483
+ const ctx: ConnectErrorContext = {
484
+ connectorId: errorState.connectorId,
485
+ reason: classifyError(errorState.error),
486
+ error: errorState.error,
487
+ retry: () => {
488
+ const id = errorState.connectorId;
489
+ setErrorState(null);
490
+ pendingQrConnectorRef.current = id;
491
+ cancelConnect();
492
+ triggerConnect(id);
493
+ },
494
+ dismiss: () => {
495
+ setErrorState(null);
496
+ setSelectedConnector(null);
497
+ },
498
+ };
499
+ return renderConnectError(ctx);
500
+ }, [errorState, renderConnectError, triggerConnect, cancelConnect]);
501
+
502
+ // chain mismatch면 단일 pane (selector/detail 숨김)
503
+ // requiredChainId != null 재확인은 TS narrowing용 — chainMismatch true면 이미 보장됨
504
+ if (chainMismatch && requiredChainId != null) {
505
+ return (
506
+ <div className="ck-root">
507
+ <div className="ck-overlay" onClick={handleOverlayClick} />
508
+ <div
509
+ className="ck-modal ck-modal--switch-chain"
510
+ role="dialog"
511
+ aria-modal="true"
512
+ aria-label="Switch network"
513
+ >
514
+ <div className="ck-pane">
515
+ <div className="ck-pane__header">
516
+ <h3 className="ck-pane__title">Switch network</h3>
517
+ <button
518
+ type="button"
519
+ className="ck-btn-close"
520
+ onClick={handleCloseClick}
521
+ aria-label="Cancel and disconnect"
522
+ />
523
+ </div>
524
+ <div className="ck-pane__body">
525
+ <SwitchChainView
526
+ currentChainId={account.chainId}
527
+ requiredChainId={requiredChainId}
528
+ requiredChainName={requiredChainName}
529
+ onDisconnect={handleSwitchChainDisconnect}
530
+ onLog={onLog}
531
+ />
532
+ </div>
533
+ </div>
534
+ </div>
535
+ </div>
536
+ );
537
+ }
483
538
 
484
539
  return (
485
540
  <div className="ck-root">
486
- <div className="ck-overlay" onClick={onClose} />
541
+ <div className="ck-overlay" onClick={handleOverlayClick} />
487
542
  <div
488
- className={`ck-modal${selectedConnector ? ' ck-modal--has-detail' : ''}`}
543
+ className={`ck-modal${(selectedConnector ?? errorState) ? ' ck-modal--has-detail' : ''}`}
489
544
  role="dialog"
490
545
  aria-modal="true"
491
546
  aria-label="Connect Wallet"
@@ -495,25 +550,17 @@ const ConnectModalInner: FC<ConnectModalProps> = ({
495
550
  selected={selectedConnector}
496
551
  recentConnector={recentConnector}
497
552
  onSelect={handleSelectConnector}
498
- onClose={onClose}
553
+ onClose={handleCloseClick}
499
554
  />
500
555
  <DetailPane
501
556
  selectedConnector={selectedConnector}
502
557
  qrUri={selectedConnector ? (qrUriMap[selectedConnector] ?? null) : null}
503
558
  hasMetaMaskExtension={hasMetaMaskExtension}
504
559
  connectorLogoMap={connectorLogoMap}
505
- wcSubView={wcSubView}
506
- wcWalletList={wcWalletList}
507
- wcWalletListLoading={wcWalletListLoading}
508
- wcWalletListSearch={wcWalletListSearch}
509
- wcFilterActive={wcFilterActive}
510
- wcSelectedWallet={wcSelectedWallet}
560
+ wc={wc}
561
+ errorOverride={renderedError}
511
562
  onBack={handleBack}
512
- onClose={onClose}
513
- onWCShowList={handleWCShowList}
514
- onWCSelectWallet={handleWCSelectWallet}
515
- onWCSearchChange={setWcWalletListSearch}
516
- onWCFilterToggle={handleWCFilterToggle}
563
+ onClose={handleCloseClick}
517
564
  />
518
565
  </div>
519
566
  </div>
@@ -0,0 +1,43 @@
1
+ import { type FC } from 'react';
2
+ import { QRCodeSVG } from 'qrcode.react';
3
+
4
+ interface QRFrameProps {
5
+ qrUri: string | null;
6
+ logoUrl?: string;
7
+ }
8
+
9
+ export const QRFrame: FC<QRFrameProps> = ({ qrUri, logoUrl }) => (
10
+ <div className={`ck-qr-frame ${!qrUri ? 'ck-qr-frame--loading' : ''}`}>
11
+ {qrUri ? (
12
+ <QRCodeSVG
13
+ value={qrUri}
14
+ size={196}
15
+ level="H"
16
+ imageSettings={
17
+ logoUrl ? { src: logoUrl, width: 40, height: 40, excavate: true } : undefined
18
+ }
19
+ />
20
+ ) : (
21
+ <div className="ck-qr-spinner" aria-label="Loading QR code" />
22
+ )}
23
+ </div>
24
+ );
25
+
26
+ interface CopyLinkButtonProps {
27
+ qrUri: string | null;
28
+ }
29
+
30
+ export const CopyLinkButton: FC<CopyLinkButtonProps> = ({ qrUri }) => {
31
+ if (!qrUri) return null;
32
+ return (
33
+ <button
34
+ type="button"
35
+ className="ck-copy-link"
36
+ onClick={() => {
37
+ navigator.clipboard.writeText(qrUri);
38
+ }}
39
+ >
40
+ Copy link
41
+ </button>
42
+ );
43
+ };