@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.
- package/CHANGELOG.md +12 -0
- package/dist/index.d.ts +52 -4
- package/dist/index.js +651 -308
- package/dist/package.json +3 -1
- package/package.json +3 -1
- package/src/ConnectKitProvider.tsx +169 -0
- package/src/ConnectModal.scss +38 -0
- package/src/ConnectModal.tsx +246 -199
- package/src/components/QRFrame.tsx +43 -0
- package/src/creditConnectConnector.ts +61 -13
- package/src/hooks/useWCState.ts +150 -0
- package/src/hooks/useWagmiConnect.ts +68 -45
- package/src/index.ts +13 -1
- package/src/types.ts +22 -4
- package/src/views/CreditWalletView.tsx +4 -17
- package/src/views/MetaMaskView.tsx +5 -30
- package/src/views/SwitchChainView.tsx +67 -0
- package/src/views/WalletConnectView.tsx +10 -70
- package/tsup.config.ts +12 -1
- package/dist/_esm-PE6HOEBI.js +0 -3909
- package/dist/ccip-UBX2BH3T.js +0 -15
- package/dist/chunk-6KUZ225H.js +0 -5259
- package/dist/chunk-EVEWD66F.js +0 -447
- package/dist/chunk-U2IU7TQD.js +0 -45
- package/dist/secp256k1-FYSVLDVL.js +0 -2311
package/src/ConnectModal.tsx
CHANGED
|
@@ -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 {
|
|
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,
|
|
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
|
-
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
200
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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
|
-
|
|
235
|
-
|
|
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
|
-
{
|
|
201
|
+
{errorOverride || (
|
|
202
|
+
<>
|
|
203
|
+
{!selectedConnector && <IdleView />}
|
|
251
204
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
205
|
+
{(selectedConnector === 'CREDIT_CONNECT' || selectedConnector === 'CREDIT_WALLET') && (
|
|
206
|
+
<CreditWalletView connectorId={selectedConnector} qrUri={qrUri} />
|
|
207
|
+
)}
|
|
255
208
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
209
|
+
{selectedConnector === 'METAMASK' && (
|
|
210
|
+
<MetaMaskView
|
|
211
|
+
qrUri={qrUri}
|
|
212
|
+
hasExtension={hasMetaMaskExtension}
|
|
213
|
+
logoUrl={connectorLogoMap.METAMASK}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
263
216
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
307
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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<
|
|
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
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const
|
|
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:
|
|
361
|
-
|
|
362
|
-
|
|
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
|
-
|
|
369
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
407
|
-
|
|
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
|
-
|
|
437
|
-
|
|
422
|
+
setErrorState(null);
|
|
423
|
+
wc.resetView();
|
|
438
424
|
writeRecentConnector(id);
|
|
439
425
|
setRecentConnector(id);
|
|
440
426
|
|
|
441
427
|
if (id === 'WALLET_CONNECT' && wcProjectId) {
|
|
442
|
-
|
|
443
|
-
loadWCWalletList(wcProjectId);
|
|
428
|
+
wc.loadWalletList(wcProjectId);
|
|
444
429
|
}
|
|
445
430
|
|
|
446
431
|
cancelConnect();
|
|
447
432
|
triggerConnect(id);
|
|
448
433
|
},
|
|
449
|
-
[
|
|
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 (
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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,
|
|
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={
|
|
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={
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
wcWalletListLoading={wcWalletListLoading}
|
|
508
|
-
wcWalletListSearch={wcWalletListSearch}
|
|
509
|
-
wcFilterActive={wcFilterActive}
|
|
510
|
-
wcSelectedWallet={wcSelectedWallet}
|
|
560
|
+
wc={wc}
|
|
561
|
+
errorOverride={renderedError}
|
|
511
562
|
onBack={handleBack}
|
|
512
|
-
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
|
+
};
|