@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.
- package/CHANGELOG.md +68 -0
- package/assets/fonts/Inter-Medium.ttf +0 -0
- package/assets/fonts/Inter-Medium.woff2 +0 -0
- package/assets/fonts/Inter-Regular.ttf +0 -0
- package/assets/fonts/Inter-Regular.woff2 +0 -0
- package/assets/fonts/Inter-SemiBold.ttf +0 -0
- package/assets/fonts/Inter-SemiBold.woff2 +0 -0
- package/assets/graphic.png +0 -0
- package/assets/metamask.png +0 -0
- package/assets/penguinwallet.png +0 -0
- package/assets/wc-logo.svg +3 -0
- package/assets/wc-search.png +0 -0
- package/dist/chunk-VE5LB7BN.js +405 -0
- package/dist/credit-connect.js +2 -394
- package/dist/index.css +2075 -0
- package/dist/index.d.ts +58 -33
- package/dist/index.js +1748 -1503
- package/dist/package.json +16 -10
- package/package.json +14 -11
- package/src/ConnectKitProvider.tsx +133 -75
- package/src/api/account.ts +21 -0
- package/src/api/asset.ts +10 -0
- package/src/api/balance.ts +10 -0
- package/src/api/chain.ts +17 -0
- package/src/api/contract.ts +33 -0
- package/src/api/transaction.ts +12 -0
- package/src/assets.d.ts +5 -0
- package/src/components/actionItem/index.tsx +50 -0
- package/src/components/actionItem/style.scss +47 -0
- package/src/components/copyLink/index.tsx +16 -0
- package/src/components/copyLink/style.scss +16 -0
- package/src/components/qrArea/index.tsx +72 -0
- package/src/components/qrArea/style.scss +122 -0
- package/src/components/searchBar/asset/Search.tsx +20 -0
- package/src/components/searchBar/asset/XMarkCircle.tsx +24 -0
- package/src/components/searchBar/index.tsx +44 -0
- package/src/components/searchBar/style.scss +58 -0
- package/src/components/toast/index.tsx +41 -0
- package/src/components/toast/style.scss +108 -0
- package/src/components/toggle/index.tsx +27 -0
- package/src/components/toggle/style.scss +53 -0
- package/src/components/walletGrid/index.tsx +64 -0
- package/src/components/walletGrid/style.scss +108 -0
- package/src/components/walletItem/index.tsx +49 -0
- package/src/components/walletItem/style.scss +70 -0
- package/src/connector-meta.ts +16 -11
- package/src/core/config.ts +102 -0
- package/src/creditConnectConnector.ts +198 -184
- package/src/events/account.ts +76 -0
- package/src/hooks/useConnectBanner.ts +79 -0
- package/src/hooks/useConnectTimeout.ts +92 -0
- package/src/hooks/useMetaMaskExtension.ts +29 -0
- package/src/hooks/useWCState.ts +45 -14
- package/src/hooks/useWagmiConnect.ts +42 -11
- package/src/index.ts +38 -2
- package/src/layout/allWallets/index.tsx +52 -0
- package/src/layout/allWallets/style.scss +57 -0
- package/src/layout/connectDialog/asset/backIcon.tsx +24 -0
- package/src/layout/connectDialog/asset/checkIcon.tsx +28 -0
- package/src/layout/connectDialog/asset/closeIcon.tsx +21 -0
- package/src/layout/connectDialog/asset/copyIcon.tsx +28 -0
- package/src/layout/connectDialog/asset/linkIcon.tsx +24 -0
- package/src/layout/connectDialog/asset/spinner.tsx +21 -0
- package/src/layout/connectDialog/asset/warningIcon.tsx +21 -0
- package/src/layout/connectDialog/idle/assets/banner-placeholder.png +0 -0
- package/src/layout/connectDialog/idle/index.tsx +55 -0
- package/src/layout/connectDialog/idle/style.scss +52 -0
- package/src/layout/connectDialog/index.tsx +432 -0
- package/src/layout/connectDialog/loading/index.tsx +13 -0
- package/src/layout/connectDialog/loading/style.scss +19 -0
- package/src/layout/connectDialog/metaMask/index.tsx +61 -0
- package/src/layout/connectDialog/metaMask/style.scss +65 -0
- package/src/layout/connectDialog/qr/index.tsx +33 -0
- package/src/layout/connectDialog/style.scss +48 -0
- package/src/layout/connectDialog/timeout/index.tsx +31 -0
- package/src/layout/connectDialog/timeout/style.scss +64 -0
- package/src/layout/connectDialog/walletConnect/index.tsx +45 -0
- package/src/layout/connectDialog/walletConnect/style.scss +48 -0
- package/src/layout/connectDialog/walletQR/index.tsx +37 -0
- package/src/layout/detailPanel/index.tsx +175 -0
- package/src/layout/detailPanel/style.scss +70 -0
- package/src/layout/selectorPanel/index.tsx +100 -0
- package/src/layout/selectorPanel/style.scss +113 -0
- package/src/style/color.scss +417 -0
- package/src/style/font.scss +28 -0
- package/src/style/index.scss +5 -0
- package/src/style/mixin.scss +217 -0
- package/src/style/reset.scss +110 -0
- package/src/style/variables.scss +13 -0
- package/src/types.ts +40 -7
- package/src/utils/platform.ts +6 -7
- package/src/wallet-display-override.ts +15 -0
- package/tsconfig.json +5 -1
- package/tsup.config.ts +32 -2
- package/assets/creditwallet.png +0 -0
- package/dist/chunk-DDA6FP6U.js +0 -7
- package/src/ConnectModal.scss +0 -665
- package/src/ConnectModal.tsx +0 -559
- package/src/components/QRFrame.tsx +0 -43
- package/src/views/CreditWalletView.tsx +0 -59
- package/src/views/IdleView.tsx +0 -10
- package/src/views/MetaMaskView.tsx +0 -64
- package/src/views/SwitchChainView.tsx +0 -67
- package/src/views/WalletConnectView.tsx +0 -206
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { type ConnectorId } from '../types';
|
|
3
|
+
|
|
4
|
+
// MM / CC 폴백용 기본 타임아웃. (CC 는 원칙적으로 서버 push 로 만료가 오지만,
|
|
5
|
+
// 서버 값이 없거나 신호가 유실된 경우를 대비한 폴백 용도로도 쓸 수 있음.)
|
|
6
|
+
export const DEFAULT_CONNECT_TIMEOUT_MS = 300_000;
|
|
7
|
+
|
|
8
|
+
interface UseConnectTimeoutParams {
|
|
9
|
+
/** 현재 연결 시도 중인 커넥터. null = 유휴 상태. */
|
|
10
|
+
connectorId: ConnectorId | null;
|
|
11
|
+
/** 타임아웃 감시 활성화 여부 — pending/QR 표시 중일 때 true. */
|
|
12
|
+
active: boolean;
|
|
13
|
+
/** Wagmi connect promise가 실제로 대기 중인지 여부. */
|
|
14
|
+
isConnecting: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* 외부 이벤트 기반 실패/만료 신호. 값이 증가할 때마다 연결 실패로 간주.
|
|
17
|
+
* - CC: dialog 가 connector 의 `session_expired`(서버 push) 를 받아 올림.
|
|
18
|
+
* - MM: connect promise reject.
|
|
19
|
+
* - WC: dialog 가 WC connect promise reject(프로토콜 ~5분 만료 등) 를 받아 올림.
|
|
20
|
+
*/
|
|
21
|
+
expiredSignal?: number;
|
|
22
|
+
/** "Try Again" 시 실행할 재연결 로직 (dialog 가 주입). */
|
|
23
|
+
onRetry?: () => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface UseConnectTimeoutResult {
|
|
27
|
+
/** 타임아웃 발생 여부 — true 면 dialog 가 <Timeout> 을 렌더. */
|
|
28
|
+
timedOut: boolean;
|
|
29
|
+
/** 다른 지갑 연결을 시작하기 전에 이전 타임아웃 상태를 즉시 초기화. */
|
|
30
|
+
reset: () => void;
|
|
31
|
+
/** "Try Again" 버튼에 연결할 함수 — 상태 리셋 + onRetry 실행. */
|
|
32
|
+
retry: () => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 커넥터별로 타임아웃 신호원을 분기해 하나의 `timedOut` 으로 수렴시키는 훅.
|
|
37
|
+
* - CC: 서버 push (`OnSessionExpired` → connector → expiredSignal).
|
|
38
|
+
* - MM: 네이티브 타임아웃 없음 → client 타이머(DEFAULT_CONNECT_TIMEOUT_MS).
|
|
39
|
+
* - WC: 프로토콜 자체 ~5분 proposal reject → connect promise reject → expiredSignal.
|
|
40
|
+
*/
|
|
41
|
+
export const useConnectTimeout = ({
|
|
42
|
+
connectorId,
|
|
43
|
+
active,
|
|
44
|
+
isConnecting,
|
|
45
|
+
expiredSignal = 0,
|
|
46
|
+
onRetry,
|
|
47
|
+
}: UseConnectTimeoutParams): UseConnectTimeoutResult => {
|
|
48
|
+
const [timedOut, setTimedOut] = useState(false);
|
|
49
|
+
|
|
50
|
+
const fire = useCallback(() => {
|
|
51
|
+
setTimedOut(true);
|
|
52
|
+
}, []);
|
|
53
|
+
|
|
54
|
+
// 커넥터가 바뀌면(=새 연결 시도) 타임아웃 상태 리셋.
|
|
55
|
+
// active 가 false 로 떨어질 때는 리셋하지 않는다 — timeout UI 표시 중 상태가 날아가지 않도록.
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
setTimedOut(false);
|
|
58
|
+
}, [connectorId]);
|
|
59
|
+
|
|
60
|
+
// MM 만 client 타이머로 강제 (승인 대기에 네이티브 상한이 없음).
|
|
61
|
+
// CC(서버 push) / WC(connect reject) 는 아래 expiredSignal effect 에서 처리.
|
|
62
|
+
useEffect(() => {
|
|
63
|
+
if (!active || !isConnecting || connectorId !== 'METAMASK') return;
|
|
64
|
+
const timer = setTimeout(fire, DEFAULT_CONNECT_TIMEOUT_MS);
|
|
65
|
+
return () => clearTimeout(timer);
|
|
66
|
+
}, [active, isConnecting, connectorId, fire]);
|
|
67
|
+
|
|
68
|
+
// 외부 실패/만료 신호 처리 (CC 서버 push / MM·WC connect reject).
|
|
69
|
+
// signal 값이 올라가면(새 만료 이벤트) 발화.
|
|
70
|
+
const prevSignal = useRef(expiredSignal);
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!active || !connectorId) {
|
|
73
|
+
prevSignal.current = expiredSignal;
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (expiredSignal !== prevSignal.current) {
|
|
77
|
+
prevSignal.current = expiredSignal;
|
|
78
|
+
fire();
|
|
79
|
+
}
|
|
80
|
+
}, [expiredSignal, connectorId, active, fire]);
|
|
81
|
+
|
|
82
|
+
const retry = useCallback(() => {
|
|
83
|
+
setTimedOut(false);
|
|
84
|
+
onRetry?.();
|
|
85
|
+
}, [onRetry]);
|
|
86
|
+
|
|
87
|
+
const reset = useCallback(() => {
|
|
88
|
+
setTimedOut(false);
|
|
89
|
+
}, []);
|
|
90
|
+
|
|
91
|
+
return { timedOut, reset, retry };
|
|
92
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
const METAMASK_RDNS = 'io.metamask';
|
|
4
|
+
|
|
5
|
+
// EIP-6963 응답의 rdns 로 MetaMask 확장을 식별.
|
|
6
|
+
const isMetaMaskAnnouncement = (event: Event): boolean => {
|
|
7
|
+
const detail = (event as CustomEvent<{ info?: { rdns?: string }; provider?: unknown }>).detail;
|
|
8
|
+
return detail?.info?.rdns?.toLowerCase() === METAMASK_RDNS && detail.provider != null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export const useMetaMaskExtension = (): boolean => {
|
|
12
|
+
const [isInstalled, setIsInstalled] = useState(false);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const handleAnnouncement = (event: Event): void => {
|
|
16
|
+
if (isMetaMaskAnnouncement(event)) setIsInstalled(true);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// 응답 listener를 먼저 등록한 뒤 설치된 provider 목록을 요청.
|
|
20
|
+
window.addEventListener('eip6963:announceProvider', handleAnnouncement);
|
|
21
|
+
window.dispatchEvent(new Event('eip6963:requestProvider'));
|
|
22
|
+
|
|
23
|
+
return () => {
|
|
24
|
+
window.removeEventListener('eip6963:announceProvider', handleAnnouncement);
|
|
25
|
+
};
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
return isInstalled;
|
|
29
|
+
};
|
package/src/hooks/useWCState.ts
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
|
-
import { useState, useRef, useCallback } from 'react';
|
|
1
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
2
2
|
import type { WCWallet, WCSubView } from '../types';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// TODO-CRCN-11: WC에 PenguinWallet 추가 시 변경
|
|
4
|
+
import { CREDIT_WALLET_LISTING_ID, overrideWalletDisplay } from '../wallet-display-override';
|
|
5
|
+
|
|
6
|
+
const sortCreditWalletFirst = (list: WCWallet[]): WCWallet[] =>
|
|
7
|
+
[...list].sort((a, b) => {
|
|
8
|
+
// TODO-CRCN-11: WC에 PenguinWallet 추가 시 변경
|
|
9
|
+
if (a.id === CREDIT_WALLET_LISTING_ID) return -1;
|
|
10
|
+
if (b.id === CREDIT_WALLET_LISTING_ID) return 1;
|
|
11
|
+
return 0;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const fetchWCWalletList = async (projectId: string, signal?: AbortSignal): Promise<WCWallet[]> => {
|
|
15
|
+
// WC Explorer API 가 page 경계에서 같은 wallet 을 중복 emit 하는 케이스 관찰됨
|
|
16
|
+
// (paginate 사이 sort/dedup 보장 X). id 기준 first-write-wins dedup.
|
|
17
|
+
const byId = new Map<string, WCWallet>();
|
|
6
18
|
let page = 1;
|
|
7
19
|
const entries = 100;
|
|
8
20
|
|
|
9
21
|
while (page <= 10) {
|
|
22
|
+
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError');
|
|
10
23
|
const url = new URL('https://explorer-api.walletconnect.com/v3/wallets');
|
|
11
24
|
url.searchParams.set('projectId', projectId);
|
|
12
25
|
url.searchParams.set('entries', String(entries));
|
|
13
26
|
url.searchParams.set('page', String(page));
|
|
14
27
|
|
|
15
|
-
const res = await fetch(url.toString());
|
|
28
|
+
const res = await fetch(url.toString(), { signal });
|
|
16
29
|
if (!res.ok) throw new Error(`Failed to fetch wallet list: ${res.status}`);
|
|
17
30
|
|
|
18
31
|
const json = (await res.json()) as {
|
|
@@ -33,7 +46,8 @@ const fetchWCWalletList = async (projectId: string): Promise<WCWallet[]> => {
|
|
|
33
46
|
|
|
34
47
|
const listings = Object.values(json.listings ?? {});
|
|
35
48
|
for (const w of listings) {
|
|
36
|
-
|
|
49
|
+
if (byId.has(w.id)) continue;
|
|
50
|
+
byId.set(w.id, {
|
|
37
51
|
id: w.id,
|
|
38
52
|
name: w.name,
|
|
39
53
|
imageUrl: w.image_url?.md ?? w.image_url?.sm ?? '',
|
|
@@ -47,11 +61,11 @@ const fetchWCWalletList = async (projectId: string): Promise<WCWallet[]> => {
|
|
|
47
61
|
});
|
|
48
62
|
}
|
|
49
63
|
|
|
50
|
-
if (listings.length < entries ||
|
|
64
|
+
if (listings.length < entries || byId.size >= json.total) break;
|
|
51
65
|
page += 1;
|
|
52
66
|
}
|
|
53
67
|
|
|
54
|
-
return
|
|
68
|
+
return Array.from(byId.values());
|
|
55
69
|
};
|
|
56
70
|
|
|
57
71
|
export interface WCState {
|
|
@@ -66,7 +80,6 @@ export interface WCState {
|
|
|
66
80
|
onSelectWallet: (wallet: WCWallet) => void;
|
|
67
81
|
onSearchChange: (q: string) => void;
|
|
68
82
|
onFilterToggle: () => void;
|
|
69
|
-
// Returns true if WC handled the back navigation, false if the parent should handle it
|
|
70
83
|
handleBack: () => boolean;
|
|
71
84
|
resetView: () => void;
|
|
72
85
|
reset: () => void;
|
|
@@ -80,18 +93,36 @@ export const useWCState = (): WCState => {
|
|
|
80
93
|
const [filterActive, setFilterActive] = useState(false);
|
|
81
94
|
const [selectedWallet, setSelectedWallet] = useState<WCWallet | null>(null);
|
|
82
95
|
const loadedRef = useRef(false);
|
|
96
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
97
|
+
const isMountedRef = useRef(true);
|
|
98
|
+
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
isMountedRef.current = true;
|
|
101
|
+
return () => {
|
|
102
|
+
isMountedRef.current = false;
|
|
103
|
+
abortControllerRef.current?.abort();
|
|
104
|
+
abortControllerRef.current = null;
|
|
105
|
+
};
|
|
106
|
+
}, []);
|
|
83
107
|
|
|
84
108
|
const loadWalletList = useCallback(async (projectId: string): Promise<void> => {
|
|
85
109
|
if (loadedRef.current) return;
|
|
86
110
|
loadedRef.current = true;
|
|
111
|
+
const controller = new AbortController();
|
|
112
|
+
abortControllerRef.current = controller;
|
|
87
113
|
setWalletListLoading(true);
|
|
88
114
|
try {
|
|
89
|
-
const list = await fetchWCWalletList(projectId);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
115
|
+
const list = await fetchWCWalletList(projectId, controller.signal);
|
|
116
|
+
// TODO-CRCN-11: WC에 PenguinWallet 추가 시 변경
|
|
117
|
+
const sorted = sortCreditWalletFirst(list).map(overrideWalletDisplay);
|
|
118
|
+
// Credit Wallet 을 최상단으로 정렬한 상태로 state 저장 — view 측은 정렬 신경 X.
|
|
119
|
+
if (isMountedRef.current) setWalletList(sorted);
|
|
120
|
+
} catch (err) {
|
|
121
|
+
const isAbort = err instanceof DOMException && err.name === 'AbortError';
|
|
122
|
+
if (!isAbort) loadedRef.current = false;
|
|
93
123
|
} finally {
|
|
94
|
-
setWalletListLoading(false);
|
|
124
|
+
if (isMountedRef.current) setWalletListLoading(false);
|
|
125
|
+
if (abortControllerRef.current === controller) abortControllerRef.current = null;
|
|
95
126
|
}
|
|
96
127
|
}, []);
|
|
97
128
|
|
|
@@ -13,8 +13,10 @@ const hasMetaMaskKeyword = (value: string): boolean => value.toLowerCase().inclu
|
|
|
13
13
|
const hasWalletConnectKeyword = (value: string): boolean =>
|
|
14
14
|
value.toLowerCase().includes('walletconnect');
|
|
15
15
|
|
|
16
|
+
// 전역 provider 대신 wagmi connector 메타데이터로 MetaMask를 판별.
|
|
16
17
|
const isMetaMaskConnector = (connector: WagmiConnectorLike): boolean => {
|
|
17
|
-
if (connector.id === '
|
|
18
|
+
if (connector.id === 'metaMaskSDK') return true;
|
|
19
|
+
|
|
18
20
|
if (hasMetaMaskKeyword(connector.id) || hasMetaMaskKeyword(connector.name)) return true;
|
|
19
21
|
|
|
20
22
|
if (connector.rdns) {
|
|
@@ -42,7 +44,6 @@ export const resolveWagmiConnector = <T extends WagmiConnectorLike>(
|
|
|
42
44
|
case 'METAMASK':
|
|
43
45
|
return connectors.find(isMetaMaskConnector);
|
|
44
46
|
|
|
45
|
-
case 'CREDIT_WALLET':
|
|
46
47
|
case 'WALLET_CONNECT':
|
|
47
48
|
return connectors.find(isWalletConnectConnector);
|
|
48
49
|
|
|
@@ -55,11 +56,13 @@ interface Options {
|
|
|
55
56
|
onConnect: (result: ConnectResult) => void;
|
|
56
57
|
onError: (error: Error, connectorId: ConnectorId) => void;
|
|
57
58
|
onQrUri: (uri: string | null) => void;
|
|
59
|
+
onSessionExpired?: () => void;
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
interface UseWagmiConnectResult {
|
|
61
63
|
triggerConnect: (connectorId: ConnectorId) => void;
|
|
62
64
|
cancelConnect: () => void;
|
|
65
|
+
restartConnect: (connectorId: ConnectorId) => Promise<void>;
|
|
63
66
|
isConnecting: boolean;
|
|
64
67
|
}
|
|
65
68
|
|
|
@@ -67,30 +70,34 @@ export const useWagmiConnect = ({
|
|
|
67
70
|
onConnect,
|
|
68
71
|
onError,
|
|
69
72
|
onQrUri,
|
|
73
|
+
onSessionExpired,
|
|
70
74
|
}: Options): UseWagmiConnectResult => {
|
|
71
75
|
const config = useConfig();
|
|
72
76
|
const pendingConnectorId = useRef<ConnectorId | null>(null);
|
|
77
|
+
const attemptCounter = useRef(0);
|
|
73
78
|
|
|
74
|
-
// 콜백 참조
|
|
75
79
|
const onConnectRef = useRef(onConnect);
|
|
76
80
|
const onErrorRef = useRef(onError);
|
|
77
81
|
const onQrUriRef = useRef(onQrUri);
|
|
82
|
+
const onSessionExpiredRef = useRef(onSessionExpired);
|
|
78
83
|
useEffect(() => {
|
|
79
84
|
onConnectRef.current = onConnect;
|
|
80
85
|
onErrorRef.current = onError;
|
|
81
86
|
onQrUriRef.current = onQrUri;
|
|
82
|
-
|
|
87
|
+
onSessionExpiredRef.current = onSessionExpired;
|
|
88
|
+
}, [onConnect, onError, onQrUri, onSessionExpired]);
|
|
83
89
|
|
|
84
|
-
// 연결 시도
|
|
85
90
|
const { connectAsync, reset, isPending } = useConnect();
|
|
86
91
|
|
|
87
|
-
// QR 코드 표시
|
|
88
92
|
useEffect(() => {
|
|
89
93
|
const handler = (data: unknown): void => {
|
|
90
94
|
const msg = data as { type?: string; data?: unknown };
|
|
91
95
|
if (msg?.type === 'display_uri' && typeof msg.data === 'string') {
|
|
92
96
|
onQrUriRef.current(msg.data);
|
|
93
97
|
}
|
|
98
|
+
if (msg?.type === 'session_expired') {
|
|
99
|
+
onSessionExpiredRef.current?.();
|
|
100
|
+
}
|
|
94
101
|
};
|
|
95
102
|
|
|
96
103
|
const unsubscribers = config.connectors.map((connector) => {
|
|
@@ -107,7 +114,6 @@ export const useWagmiConnect = ({
|
|
|
107
114
|
};
|
|
108
115
|
}, [config.connectors]);
|
|
109
116
|
|
|
110
|
-
// 연결 트리거
|
|
111
117
|
const triggerConnect = useCallback(
|
|
112
118
|
(connectorId: ConnectorId): void => {
|
|
113
119
|
const connector = resolveWagmiConnector(config.connectors, connectorId);
|
|
@@ -117,10 +123,11 @@ export const useWagmiConnect = ({
|
|
|
117
123
|
}
|
|
118
124
|
|
|
119
125
|
onQrUriRef.current(null);
|
|
126
|
+
const attemptId = ++attemptCounter.current;
|
|
120
127
|
pendingConnectorId.current = connectorId;
|
|
121
128
|
connectAsync({ connector })
|
|
122
129
|
.then((result) => {
|
|
123
|
-
if (
|
|
130
|
+
if (attemptCounter.current !== attemptId) return;
|
|
124
131
|
const address = result.accounts?.[0];
|
|
125
132
|
if (!address) {
|
|
126
133
|
throw new Error('Connected but no account returned');
|
|
@@ -129,7 +136,7 @@ export const useWagmiConnect = ({
|
|
|
129
136
|
onConnectRef.current({ address, connectorId });
|
|
130
137
|
})
|
|
131
138
|
.catch((error) => {
|
|
132
|
-
if (
|
|
139
|
+
if (attemptCounter.current !== attemptId) return;
|
|
133
140
|
pendingConnectorId.current = null;
|
|
134
141
|
onErrorRef.current(error as Error, connectorId);
|
|
135
142
|
});
|
|
@@ -137,12 +144,36 @@ export const useWagmiConnect = ({
|
|
|
137
144
|
[config.connectors, connectAsync],
|
|
138
145
|
);
|
|
139
146
|
|
|
140
|
-
// 연결 취소
|
|
141
147
|
const cancelConnect = useCallback((): void => {
|
|
148
|
+
attemptCounter.current += 1;
|
|
142
149
|
pendingConnectorId.current = null;
|
|
143
150
|
onQrUriRef.current(null);
|
|
144
151
|
reset();
|
|
145
152
|
}, [reset]);
|
|
146
153
|
|
|
147
|
-
|
|
154
|
+
const restartConnect = useCallback(
|
|
155
|
+
async (connectorId: ConnectorId): Promise<void> => {
|
|
156
|
+
const connector = resolveWagmiConnector(config.connectors, connectorId);
|
|
157
|
+
|
|
158
|
+
cancelConnect();
|
|
159
|
+
const restartAttempt = attemptCounter.current;
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
// MetaMask SDK connector.disconnect()는 sdk.terminate()를 호출해
|
|
163
|
+
// 기존 QR/Deeplink 연결 채널을 정리한다.
|
|
164
|
+
await connector?.disconnect();
|
|
165
|
+
} catch (error) {
|
|
166
|
+
onErrorRef.current(error as Error, connectorId);
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// SDK 정리 중 다른 지갑을 선택하거나 모달을 닫았으면 재연결하지 않는다.
|
|
171
|
+
if (attemptCounter.current !== restartAttempt) return;
|
|
172
|
+
|
|
173
|
+
triggerConnect(connectorId);
|
|
174
|
+
},
|
|
175
|
+
[config.connectors, cancelConnect, triggerConnect],
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
return { triggerConnect, cancelConnect, restartConnect, isConnecting: isPending };
|
|
148
179
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,50 @@
|
|
|
1
|
-
|
|
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 { watchAsset } from './api/asset';
|
|
7
|
+
import { watchAccount, watchConnections, reconnect, disconnectAll } from './events/account';
|
|
8
|
+
|
|
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,
|
|
17
|
+
ConnectBannerSiteKey,
|
|
7
18
|
ConnectModalProps,
|
|
19
|
+
ConnectKitConfig,
|
|
20
|
+
ConnectKitTheme,
|
|
8
21
|
Connectors,
|
|
22
|
+
ConnectorKey,
|
|
9
23
|
ConnectResult,
|
|
10
24
|
ConnectorId,
|
|
11
|
-
CreditWalletStrategy,
|
|
12
25
|
WCSubView,
|
|
13
26
|
WCWallet,
|
|
14
27
|
} from './types';
|
|
28
|
+
|
|
29
|
+
export const connectKit = {
|
|
30
|
+
api: {
|
|
31
|
+
readContract,
|
|
32
|
+
writeContract,
|
|
33
|
+
getBalance,
|
|
34
|
+
switchChain,
|
|
35
|
+
getPublicClient,
|
|
36
|
+
waitForTransactionReceipt,
|
|
37
|
+
getAccount,
|
|
38
|
+
signMessage,
|
|
39
|
+
signTypedData,
|
|
40
|
+
watchAsset,
|
|
41
|
+
},
|
|
42
|
+
events: {
|
|
43
|
+
watchAccount,
|
|
44
|
+
watchConnections,
|
|
45
|
+
reconnect,
|
|
46
|
+
disconnectAll,
|
|
47
|
+
},
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
export type * from '@wagmi/core';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { type FC, useState, useMemo } from 'react';
|
|
2
|
+
import './style.scss';
|
|
3
|
+
import { type WCWallet } from '../../types';
|
|
4
|
+
import { SearchBar } from '../../components/searchBar';
|
|
5
|
+
import { Toggle } from '../../components/toggle';
|
|
6
|
+
import { WalletGrid } from '../../components/walletGrid';
|
|
7
|
+
import wcLogo from '../../../assets/wc-search.png';
|
|
8
|
+
|
|
9
|
+
interface AllWalletsProps {
|
|
10
|
+
/** Credit Wallet 이 최상단에 오도록 정렬된 상태로 전달됨 — useWCState 의 loadWalletList 에서 처리. */
|
|
11
|
+
wallets: WCWallet[];
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
onSelectWallet: (wallet: WCWallet) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const AllWallets: FC<AllWalletsProps> = ({ wallets, isLoading, onSelectWallet }) => {
|
|
17
|
+
const [search, setSearch] = useState('');
|
|
18
|
+
const [filterInstalled, setFilterInstalled] = useState(false);
|
|
19
|
+
|
|
20
|
+
const filtered = useMemo(() => {
|
|
21
|
+
const q = search.trim().toLowerCase();
|
|
22
|
+
let list = wallets;
|
|
23
|
+
if (filterInstalled) {
|
|
24
|
+
list = list.filter(
|
|
25
|
+
(w) => w.mobileNative || w.mobileUniversal || w.desktopNative || w.desktopUniversal,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
if (q) {
|
|
29
|
+
list = list.filter((w) => w.name.toLowerCase().includes(q));
|
|
30
|
+
}
|
|
31
|
+
return list;
|
|
32
|
+
}, [wallets, search, filterInstalled]);
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="ck-all-wallets">
|
|
36
|
+
<div className="ck-all-wallets-controls">
|
|
37
|
+
<SearchBar value={search} onChange={setSearch} onClear={() => setSearch('')} />
|
|
38
|
+
<div className="ck-wc-filter-btn">
|
|
39
|
+
<img className="ck-wc-filter-logo" src={wcLogo} alt="" aria-hidden="true" />
|
|
40
|
+
<Toggle checked={filterInstalled} label="" onChange={setFilterInstalled} />
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
{!isLoading && filtered.length === 0 ? (
|
|
44
|
+
<div className="ck-all-wallets-empty" role="status">
|
|
45
|
+
<p>No Wallet found</p>
|
|
46
|
+
</div>
|
|
47
|
+
) : (
|
|
48
|
+
<WalletGrid wallets={filtered} isLoading={isLoading} onSelect={onSelectWallet} />
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
@use '../../style/mixin.scss' as *;
|
|
2
|
+
|
|
3
|
+
.ck-all-wallets {
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
flex: 1;
|
|
7
|
+
min-height: 0;
|
|
8
|
+
gap: 12px;
|
|
9
|
+
|
|
10
|
+
@include tablet {
|
|
11
|
+
min-height: min(420px, 55dvh);
|
|
12
|
+
max-height: calc(100dvh - 60px);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.ck-all-wallets-controls {
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 8px;
|
|
20
|
+
flex-shrink: 0;
|
|
21
|
+
|
|
22
|
+
.ck-search {
|
|
23
|
+
min-width: 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.ck-wc-filter-btn {
|
|
28
|
+
display: inline-flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
gap: 8px;
|
|
31
|
+
padding: 12px 16px;
|
|
32
|
+
border-radius: 100px;
|
|
33
|
+
background: var(--scale-gray-100);
|
|
34
|
+
flex-shrink: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.ck-wc-filter-logo {
|
|
38
|
+
width: 24px;
|
|
39
|
+
height: 24px;
|
|
40
|
+
border-radius: 50%;
|
|
41
|
+
flex-shrink: 0;
|
|
42
|
+
object-fit: cover;
|
|
43
|
+
background: var(--static-white-main);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.ck-all-wallets-empty {
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
flex: 1;
|
|
51
|
+
|
|
52
|
+
p {
|
|
53
|
+
@include textToken('body4-l2-regular');
|
|
54
|
+
color: var(--scale-gray-600);
|
|
55
|
+
text-align: center;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type FC, type SVGProps } from 'react';
|
|
2
|
+
|
|
3
|
+
export const BackIcon: FC<SVGProps<SVGSVGElement>> = (props) => {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
width={14}
|
|
7
|
+
height={20}
|
|
8
|
+
viewBox="0 0 14 20"
|
|
9
|
+
fill="none"
|
|
10
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
11
|
+
aria-hidden="true"
|
|
12
|
+
focusable="false"
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<path
|
|
16
|
+
d="M10 4L4 10L10 16"
|
|
17
|
+
stroke="currentColor"
|
|
18
|
+
strokeWidth="1.5"
|
|
19
|
+
strokeLinecap="round"
|
|
20
|
+
strokeLinejoin="round"
|
|
21
|
+
/>
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type FC, type SVGProps } from 'react';
|
|
2
|
+
|
|
3
|
+
export const CheckIcon: FC<SVGProps<SVGSVGElement>> = (props) => {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
width={24}
|
|
7
|
+
height={24}
|
|
8
|
+
viewBox="0 0 24 24"
|
|
9
|
+
fill="none"
|
|
10
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
11
|
+
aria-hidden="true"
|
|
12
|
+
focusable="false"
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<g clipPath="url(#clip0_check)">
|
|
16
|
+
<path
|
|
17
|
+
d="M12 22C6.47715 22 2 17.5228 2 12C2 6.47715 6.47715 2 12 2C17.5228 2 22 6.47715 22 12C22 17.5228 17.5228 22 12 22ZM10.4512 15.6953C10.7879 15.973 11.2866 15.9546 11.6016 15.6396L16.7529 10.4883C17.0877 10.1536 17.0877 9.61013 16.7529 9.27539C16.4182 8.94114 15.8756 8.94091 15.541 9.27539L10.9961 13.8223L8.56836 11.3936C8.23374 11.0591 7.69116 11.0593 7.35645 11.3936C7.02171 11.7283 7.02171 12.2717 7.35645 12.6064L10.3867 15.6367C10.4075 15.6575 10.4289 15.6771 10.4512 15.6953Z"
|
|
18
|
+
fill="currentColor"
|
|
19
|
+
/>
|
|
20
|
+
</g>
|
|
21
|
+
<defs>
|
|
22
|
+
<clipPath id="clip0_check">
|
|
23
|
+
<rect width="20" height="20" fill="white" transform="translate(2 2)" />
|
|
24
|
+
</clipPath>
|
|
25
|
+
</defs>
|
|
26
|
+
</svg>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type FC, type SVGProps } from 'react';
|
|
2
|
+
|
|
3
|
+
export const CloseIcon: FC<SVGProps<SVGSVGElement>> = (props) => {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
width={20}
|
|
7
|
+
height={20}
|
|
8
|
+
viewBox="0 0 20 20"
|
|
9
|
+
fill="none"
|
|
10
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
11
|
+
aria-hidden="true"
|
|
12
|
+
focusable="false"
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<path
|
|
16
|
+
d="M15.7785 3.2787C16.0389 3.01835 16.4614 3.01835 16.7217 3.2787C16.9819 3.53899 16.9819 3.96077 16.7217 4.22108L10.9421 9.99989L16.7209 15.7787L16.7673 15.8292C16.9809 16.091 16.9658 16.4778 16.7217 16.7219C16.4776 16.9658 16.0908 16.981 15.829 16.7675L15.7785 16.7211L9.99971 10.9422L4.22172 16.7211L4.17126 16.7675C3.90947 16.981 3.52261 16.9658 3.27852 16.7219C3.03445 16.4778 3.0194 16.091 3.23295 15.8292L3.27934 15.7787L9.05733 9.99989L3.27852 4.22108C3.01842 3.96077 3.01839 3.53899 3.27852 3.2787C3.53887 3.01835 3.96137 3.01835 4.22172 3.2787L9.99971 9.05669L15.7785 3.2787Z"
|
|
17
|
+
fill="currentColor"
|
|
18
|
+
/>
|
|
19
|
+
</svg>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type FC, type SVGProps } from 'react';
|
|
2
|
+
|
|
3
|
+
export const CopyIcon: FC<SVGProps<SVGSVGElement>> = (props) => {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
width={20}
|
|
7
|
+
height={20}
|
|
8
|
+
viewBox="0 0 20 20"
|
|
9
|
+
fill="none"
|
|
10
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
11
|
+
aria-hidden="true"
|
|
12
|
+
focusable="false"
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<g clipPath="url(#clip0_copy)">
|
|
16
|
+
<path
|
|
17
|
+
d="M16.2247 6.89437C17.1245 6.89437 17.8539 7.6238 17.854 8.5236V16.2246C17.854 17.1244 17.1245 17.8538 16.2247 17.8538H8.52373C7.62388 17.8538 6.89449 17.1244 6.89449 16.2246V8.5236C6.89454 7.6238 7.62391 6.89437 8.52373 6.89437H16.2247ZM8.52373 8.22738C8.36029 8.22738 8.22755 8.36019 8.2275 8.5236V16.2246C8.2275 16.3881 8.36026 16.5208 8.52373 16.5208H16.2247C16.3882 16.5208 16.521 16.3881 16.521 16.2246V8.5236C16.5209 8.36019 16.3881 8.22738 16.2247 8.22738H8.52373ZM13.0225 2.57064C13.8439 2.65419 14.4848 3.34822 14.4848 4.19173V4.91357C14.4848 5.28173 14.1865 5.58002 13.8183 5.58008C13.4501 5.58008 13.1518 5.28176 13.1518 4.91357V4.19173C13.1518 4.04865 13.05 3.92956 12.915 3.90202L12.8556 3.8955H4.19185C4.02839 3.8955 3.89563 4.02827 3.89563 4.19173V12.8555L3.90214 12.9148C3.92968 13.0498 4.04878 13.1517 4.19185 13.1517H4.91369L4.98206 13.1549C5.31816 13.1892 5.5802 13.4731 5.5802 13.8182C5.58014 14.1633 5.31813 14.4473 4.98206 14.4814L4.91369 14.4847H4.19185C3.34833 14.4847 2.65432 13.8438 2.57076 13.0223L2.56262 12.8555V4.19173C2.56262 3.29189 3.29201 2.5625 4.19185 2.5625H12.8556L13.0225 2.57064Z"
|
|
18
|
+
fill="currentColor"
|
|
19
|
+
/>
|
|
20
|
+
</g>
|
|
21
|
+
<defs>
|
|
22
|
+
<clipPath id="clip0_copy">
|
|
23
|
+
<rect width="15.8333" height="15.8333" fill="white" transform="translate(2.5 2.5)" />
|
|
24
|
+
</clipPath>
|
|
25
|
+
</defs>
|
|
26
|
+
</svg>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type FC, type SVGProps } from 'react';
|
|
2
|
+
|
|
3
|
+
export const LinkIcon: FC<SVGProps<SVGSVGElement>> = (props) => {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
width={16}
|
|
7
|
+
height={16}
|
|
8
|
+
viewBox="0 0 16 16"
|
|
9
|
+
fill="none"
|
|
10
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
11
|
+
aria-hidden="true"
|
|
12
|
+
focusable="false"
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
<path
|
|
16
|
+
d="M6 3L11 8L6 13"
|
|
17
|
+
stroke="currentColor"
|
|
18
|
+
strokeWidth="1.5"
|
|
19
|
+
strokeLinecap="round"
|
|
20
|
+
strokeLinejoin="round"
|
|
21
|
+
/>
|
|
22
|
+
</svg>
|
|
23
|
+
);
|
|
24
|
+
};
|