@gluwa/connect-kit 0.1.0-next.0
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/.eslintrc.cjs +67 -0
- package/CHANGELOG.md +18 -0
- package/README.md +0 -0
- package/assets/creditwallet.png +0 -0
- package/assets/deeplink.png +0 -0
- package/assets/graphic.png +0 -0
- package/assets/metamask.png +0 -0
- package/assets/wc.png +0 -0
- package/dist/README.md +0 -0
- package/dist/_esm-PE6HOEBI.js +3909 -0
- package/dist/ccip-UBX2BH3T.js +15 -0
- package/dist/chunk-6KUZ225H.js +5259 -0
- package/dist/chunk-EVEWD66F.js +447 -0
- package/dist/chunk-U2IU7TQD.js +45 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +1820 -0
- package/dist/package.json +64 -0
- package/dist/secp256k1-FYSVLDVL.js +2311 -0
- package/package.json +57 -0
- package/src/ConnectModal.scss +627 -0
- package/src/ConnectModal.tsx +521 -0
- package/src/assets.d.ts +4 -0
- package/src/connector-meta.ts +41 -0
- package/src/creditConnectConnector.ts +451 -0
- package/src/hooks/useWagmiConnect.ts +125 -0
- package/src/index.ts +4 -0
- package/src/types.ts +34 -0
- package/src/utils/platform.ts +16 -0
- package/src/views/CreditWalletView.tsx +69 -0
- package/src/views/IdleView.tsx +10 -0
- package/src/views/MetaMaskView.tsx +89 -0
- package/src/views/WalletConnectView.tsx +266 -0
- package/tsconfig.json +8 -0
- package/tsup.config.ts +18 -0
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef, useMemo, type FC } from 'react';
|
|
2
|
+
import './ConnectModal.scss';
|
|
3
|
+
import { useConfig } from 'wagmi';
|
|
4
|
+
import { type ConnectModalProps, type ConnectorId, type WCWallet, type WCSubView } from './types';
|
|
5
|
+
import { CONNECTOR_META } from './connector-meta';
|
|
6
|
+
import { detectMetaMaskExtension } from './utils/platform';
|
|
7
|
+
import { useWagmiConnect, WAGMI_CONNECTOR_ID } from './hooks/useWagmiConnect';
|
|
8
|
+
import { IdleView } from './views/IdleView';
|
|
9
|
+
import { CreditWalletView } from './views/CreditWalletView';
|
|
10
|
+
import { MetaMaskView } from './views/MetaMaskView';
|
|
11
|
+
import { WalletConnectView } from './views/WalletConnectView';
|
|
12
|
+
|
|
13
|
+
// 최근 연결 기록
|
|
14
|
+
const RECENT_KEY = 'connect-kit:recent';
|
|
15
|
+
const readRecentConnector = (): ConnectorId | null => {
|
|
16
|
+
try {
|
|
17
|
+
return localStorage.getItem(RECENT_KEY) as ConnectorId | null;
|
|
18
|
+
} catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const writeRecentConnector = (id: ConnectorId): void => {
|
|
24
|
+
try {
|
|
25
|
+
localStorage.setItem(RECENT_KEY, id);
|
|
26
|
+
} catch {}
|
|
27
|
+
};
|
|
28
|
+
|
|
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;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
interface SelectorPaneProps {
|
|
84
|
+
connectors: ConnectorId[];
|
|
85
|
+
selected: ConnectorId | null;
|
|
86
|
+
recentConnector: ConnectorId | null;
|
|
87
|
+
onSelect: (id: ConnectorId) => void;
|
|
88
|
+
onClose: () => void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const SelectorPane: FC<SelectorPaneProps> = ({
|
|
92
|
+
connectors,
|
|
93
|
+
selected,
|
|
94
|
+
recentConnector,
|
|
95
|
+
onSelect,
|
|
96
|
+
onClose,
|
|
97
|
+
}) => {
|
|
98
|
+
const hasRecent = recentConnector !== null && connectors.includes(recentConnector);
|
|
99
|
+
const recentConnectors = hasRecent ? [recentConnector] : [];
|
|
100
|
+
const otherConnectors = connectors.filter((id) => id !== recentConnector);
|
|
101
|
+
|
|
102
|
+
const downloadUrl = connectors
|
|
103
|
+
.map((id) => CONNECTOR_META[id].downloadUrl)
|
|
104
|
+
.find((url) => url != null);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="ck-pane ck-pane--selector">
|
|
108
|
+
<div className="ck-pane__header">
|
|
109
|
+
<h3 className="ck-pane__title">Connect Wallet</h3>
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
className="ck-btn-close ck-mobile-only"
|
|
113
|
+
onClick={onClose}
|
|
114
|
+
aria-label="Close"
|
|
115
|
+
/>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="ck-connector-list">
|
|
119
|
+
{hasRecent && (
|
|
120
|
+
<>
|
|
121
|
+
{recentConnectors.map((id) => (
|
|
122
|
+
<ConnectorItem
|
|
123
|
+
key={id}
|
|
124
|
+
id={id}
|
|
125
|
+
isSelected={selected === id}
|
|
126
|
+
isRecent
|
|
127
|
+
onSelect={onSelect}
|
|
128
|
+
/>
|
|
129
|
+
))}
|
|
130
|
+
{otherConnectors.length > 0 && (
|
|
131
|
+
<div className="ck-divider">
|
|
132
|
+
<span>OR</span>
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</>
|
|
136
|
+
)}
|
|
137
|
+
|
|
138
|
+
{otherConnectors.map((id) => (
|
|
139
|
+
<ConnectorItem
|
|
140
|
+
key={id}
|
|
141
|
+
id={id}
|
|
142
|
+
isSelected={selected === id}
|
|
143
|
+
isRecent={false}
|
|
144
|
+
onSelect={onSelect}
|
|
145
|
+
/>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{downloadUrl && (
|
|
150
|
+
<div className="ck-pane__footer">
|
|
151
|
+
<a
|
|
152
|
+
href={downloadUrl}
|
|
153
|
+
target="_blank"
|
|
154
|
+
rel="nofollow noopener noreferrer"
|
|
155
|
+
className="ck-link-no-wallet"
|
|
156
|
+
>
|
|
157
|
+
I don't have a wallet >
|
|
158
|
+
</a>
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</div>
|
|
162
|
+
);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
interface ConnectorItemProps {
|
|
166
|
+
id: ConnectorId;
|
|
167
|
+
isSelected: boolean;
|
|
168
|
+
isRecent: boolean;
|
|
169
|
+
onSelect: (id: ConnectorId) => void;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const ConnectorItem: FC<ConnectorItemProps> = ({ id, isSelected, isRecent, onSelect }) => {
|
|
173
|
+
const meta = CONNECTOR_META[id];
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<button
|
|
177
|
+
type="button"
|
|
178
|
+
className={`ck-connector-item ck-connector-item--${id.toLowerCase().replaceAll('_', '-')} ${isSelected ? 'is-active' : ''}`}
|
|
179
|
+
onClick={() => {
|
|
180
|
+
onSelect(id);
|
|
181
|
+
}}
|
|
182
|
+
>
|
|
183
|
+
{meta.logoUrl ? (
|
|
184
|
+
<img className="ck-connector-item__icon" src={meta.logoUrl} alt="" aria-hidden="true" />
|
|
185
|
+
) : (
|
|
186
|
+
<span className="ck-connector-item__icon" aria-hidden="true" />
|
|
187
|
+
)}
|
|
188
|
+
<span className="ck-connector-item__label">{meta.label}</span>
|
|
189
|
+
{isRecent && <span className="ck-connector-item__badge">Recent</span>}
|
|
190
|
+
</button>
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
interface DetailPaneProps {
|
|
195
|
+
selectedConnector: ConnectorId | null;
|
|
196
|
+
qrUri: string | null;
|
|
197
|
+
hasMetaMaskExtension: boolean;
|
|
198
|
+
connectorLogoMap: Partial<Record<ConnectorId, string>>;
|
|
199
|
+
wcSubView: WCSubView;
|
|
200
|
+
wcWalletList: WCWallet[];
|
|
201
|
+
wcWalletListLoading: boolean;
|
|
202
|
+
wcWalletListSearch: string;
|
|
203
|
+
wcSelectedWallet: WCWallet | null;
|
|
204
|
+
onBack: () => void;
|
|
205
|
+
onClose: () => void;
|
|
206
|
+
wcFilterActive: boolean;
|
|
207
|
+
onWCShowList: () => void;
|
|
208
|
+
onWCSelectWallet: (wallet: WCWallet) => void;
|
|
209
|
+
onWCSearchChange: (q: string) => void;
|
|
210
|
+
onWCFilterToggle: () => void;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const DetailPane: FC<DetailPaneProps> = ({
|
|
214
|
+
selectedConnector,
|
|
215
|
+
qrUri,
|
|
216
|
+
hasMetaMaskExtension,
|
|
217
|
+
connectorLogoMap,
|
|
218
|
+
wcSubView,
|
|
219
|
+
wcWalletList,
|
|
220
|
+
wcWalletListLoading,
|
|
221
|
+
wcWalletListSearch,
|
|
222
|
+
wcSelectedWallet,
|
|
223
|
+
wcFilterActive,
|
|
224
|
+
onBack,
|
|
225
|
+
onClose,
|
|
226
|
+
onWCShowList,
|
|
227
|
+
onWCSelectWallet,
|
|
228
|
+
onWCSearchChange,
|
|
229
|
+
onWCFilterToggle,
|
|
230
|
+
}) => {
|
|
231
|
+
const title = getDetailTitle(
|
|
232
|
+
selectedConnector,
|
|
233
|
+
hasMetaMaskExtension,
|
|
234
|
+
wcSubView,
|
|
235
|
+
wcSelectedWallet,
|
|
236
|
+
);
|
|
237
|
+
const showBack = selectedConnector !== null;
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div className="ck-pane ck-pane--detail">
|
|
241
|
+
<div className="ck-pane__header">
|
|
242
|
+
{showBack && (
|
|
243
|
+
<button type="button" className="ck-btn-back" onClick={onBack} aria-label="Back" />
|
|
244
|
+
)}
|
|
245
|
+
<h3 className="ck-pane__title">{title}</h3>
|
|
246
|
+
<button type="button" className="ck-btn-close" onClick={onClose} aria-label="Close" />
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
<div className="ck-pane__body">
|
|
250
|
+
{!selectedConnector && <IdleView />}
|
|
251
|
+
|
|
252
|
+
{(selectedConnector === 'CREDIT_CONNECT' || selectedConnector === 'CREDIT_WALLET') && (
|
|
253
|
+
<CreditWalletView connectorId={selectedConnector} qrUri={qrUri} />
|
|
254
|
+
)}
|
|
255
|
+
|
|
256
|
+
{selectedConnector === 'METAMASK' && (
|
|
257
|
+
<MetaMaskView
|
|
258
|
+
qrUri={qrUri}
|
|
259
|
+
hasExtension={hasMetaMaskExtension}
|
|
260
|
+
logoUrl={connectorLogoMap.METAMASK}
|
|
261
|
+
/>
|
|
262
|
+
)}
|
|
263
|
+
|
|
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
|
+
/>
|
|
279
|
+
)}
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
);
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const getDetailTitle = (
|
|
286
|
+
connector: ConnectorId | null,
|
|
287
|
+
hasMetaMaskExtension: boolean,
|
|
288
|
+
wcSubView: WCSubView,
|
|
289
|
+
wcSelectedWallet: WCWallet | null,
|
|
290
|
+
): string => {
|
|
291
|
+
if (!connector) return '';
|
|
292
|
+
|
|
293
|
+
if (connector === 'WALLET_CONNECT') {
|
|
294
|
+
if (wcSubView === 'list') return 'All Wallets';
|
|
295
|
+
if (wcSubView === 'wallet' && wcSelectedWallet) return `Scan with ${wcSelectedWallet.name}`;
|
|
296
|
+
return 'Scan with WalletConnect';
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (connector === 'METAMASK' && hasMetaMaskExtension) return 'MetaMask';
|
|
300
|
+
|
|
301
|
+
return CONNECTOR_META[connector].detailTitle;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export const ConnectModal: FC<ConnectModalProps> = (props) => {
|
|
305
|
+
if (
|
|
306
|
+
(props.connectors as ConnectorId[]).includes('CREDIT_WALLET') &&
|
|
307
|
+
(props.connectors as ConnectorId[]).includes('CREDIT_CONNECT')
|
|
308
|
+
) {
|
|
309
|
+
throw new Error('CREDIT_WALLET and CREDIT_CONNECT cannot be used together');
|
|
310
|
+
}
|
|
311
|
+
// open=false면 내부 컴포넌트를 마운트하지 않음
|
|
312
|
+
// → useConfig 등 wagmi hooks가 실행되지 않아 WagmiProvider 없이도 렌더링 가능
|
|
313
|
+
if (!props.open) return null;
|
|
314
|
+
return <ConnectModalInner {...props} />;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const ConnectModalInner: FC<ConnectModalProps> = ({
|
|
318
|
+
open,
|
|
319
|
+
connectors,
|
|
320
|
+
onConnect,
|
|
321
|
+
onClose,
|
|
322
|
+
onLog,
|
|
323
|
+
wcProjectId,
|
|
324
|
+
}) => {
|
|
325
|
+
const [selectedConnector, setSelectedConnector] = useState<ConnectorId | null>(null);
|
|
326
|
+
// 커넥터별 QR URI 캐시 — 만료 전까지 재사용
|
|
327
|
+
const [qrUriMap, setQrUriMap] = useState<Partial<Record<ConnectorId, string>>>({});
|
|
328
|
+
// handleQrUri 호출 시점에 어떤 커넥터의 QR인지 추적
|
|
329
|
+
const pendingQrConnectorRef = useRef<ConnectorId | null>(null);
|
|
330
|
+
const [recentConnector, setRecentConnector] = useState<ConnectorId | null>(null);
|
|
331
|
+
const [hasMetaMaskExtension, setHasMetaMaskExtension] = useState<boolean>(false);
|
|
332
|
+
|
|
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);
|
|
340
|
+
|
|
341
|
+
// QR URI 수신 — 커넥터별 캐시에 저장 (null은 무시하여 캐시 유지)
|
|
342
|
+
const handleQrUri = useCallback((uri: string | null) => {
|
|
343
|
+
if (uri) {
|
|
344
|
+
const id = pendingQrConnectorRef.current;
|
|
345
|
+
if (id) setQrUriMap((prev) => ({ ...prev, [id]: uri }));
|
|
346
|
+
}
|
|
347
|
+
}, []);
|
|
348
|
+
|
|
349
|
+
// 연결 완료
|
|
350
|
+
const handleConnect = useCallback(
|
|
351
|
+
(result: Parameters<typeof onConnect>[0]) => {
|
|
352
|
+
onConnect(result);
|
|
353
|
+
onClose();
|
|
354
|
+
},
|
|
355
|
+
[onConnect, onClose],
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
// 연결 실패 — 실패한 커넥터의 QR 캐시만 삭제
|
|
359
|
+
const handleError = useCallback(
|
|
360
|
+
(error: Error, connectorId: ConnectorId): void => {
|
|
361
|
+
onLog?.(`[connect-kit] connection failed (${connectorId}): ${error.message}`, error);
|
|
362
|
+
setSelectedConnector(null);
|
|
363
|
+
setQrUriMap((prev) => {
|
|
364
|
+
const next = { ...prev };
|
|
365
|
+
delete next[connectorId];
|
|
366
|
+
return next;
|
|
367
|
+
});
|
|
368
|
+
setWcSubView('qr');
|
|
369
|
+
setWcSelectedWallet(null);
|
|
370
|
+
},
|
|
371
|
+
[onLog],
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// 연결 시도
|
|
375
|
+
const { triggerConnect, cancelConnect } = useWagmiConnect({
|
|
376
|
+
onConnect: handleConnect,
|
|
377
|
+
onError: handleError,
|
|
378
|
+
onQrUri: handleQrUri,
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
// wagmi connector icon 매핑
|
|
382
|
+
const wagmiConfig = useConfig();
|
|
383
|
+
const connectorLogoMap = useMemo(() => {
|
|
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);
|
|
389
|
+
if (wagmiConnector?.icon) map[connectorId] = wagmiConnector.icon;
|
|
390
|
+
}
|
|
391
|
+
return map;
|
|
392
|
+
}, [wagmiConfig.connectors]);
|
|
393
|
+
|
|
394
|
+
// 최근 연결 기록 로드
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
setRecentConnector(readRecentConnector());
|
|
397
|
+
setHasMetaMaskExtension(detectMetaMaskExtension());
|
|
398
|
+
}, []);
|
|
399
|
+
|
|
400
|
+
// 모달 닫힐 때 상태 + QR 캐시 전체 초기화
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
if (!open) {
|
|
403
|
+
setSelectedConnector(null);
|
|
404
|
+
setQrUriMap({});
|
|
405
|
+
pendingQrConnectorRef.current = null;
|
|
406
|
+
setWcSubView('qr');
|
|
407
|
+
setWcWalletListSearch('');
|
|
408
|
+
setWcFilterActive(false);
|
|
409
|
+
setWcSelectedWallet(null);
|
|
410
|
+
cancelConnect();
|
|
411
|
+
}
|
|
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
|
+
}, []);
|
|
428
|
+
|
|
429
|
+
// 커넥터 선택 — 같은 커넥터 재선택 시 무시, 다른 커넥터 선택 시 이전 연결 취소 후 새 연결
|
|
430
|
+
const handleSelectConnector = useCallback(
|
|
431
|
+
(id: ConnectorId): void => {
|
|
432
|
+
if (id === selectedConnector) return;
|
|
433
|
+
|
|
434
|
+
pendingQrConnectorRef.current = id;
|
|
435
|
+
setSelectedConnector(id);
|
|
436
|
+
setWcSubView('qr');
|
|
437
|
+
setWcSelectedWallet(null);
|
|
438
|
+
writeRecentConnector(id);
|
|
439
|
+
setRecentConnector(id);
|
|
440
|
+
|
|
441
|
+
if (id === 'WALLET_CONNECT' && wcProjectId) {
|
|
442
|
+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
|
443
|
+
loadWCWalletList(wcProjectId);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
cancelConnect();
|
|
447
|
+
triggerConnect(id);
|
|
448
|
+
},
|
|
449
|
+
[selectedConnector, triggerConnect, cancelConnect, wcProjectId, loadWCWalletList],
|
|
450
|
+
);
|
|
451
|
+
|
|
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
|
+
// 뒤로가기 — QR 캐시는 유지, wagmi만 reset
|
|
467
|
+
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
|
+
}
|
|
478
|
+
}
|
|
479
|
+
pendingQrConnectorRef.current = null;
|
|
480
|
+
setSelectedConnector(null);
|
|
481
|
+
cancelConnect();
|
|
482
|
+
}, [selectedConnector, wcSubView, cancelConnect]);
|
|
483
|
+
|
|
484
|
+
return (
|
|
485
|
+
<div className="ck-root">
|
|
486
|
+
<div className="ck-overlay" onClick={onClose} />
|
|
487
|
+
<div
|
|
488
|
+
className={`ck-modal${selectedConnector ? ' ck-modal--has-detail' : ''}`}
|
|
489
|
+
role="dialog"
|
|
490
|
+
aria-modal="true"
|
|
491
|
+
aria-label="Connect Wallet"
|
|
492
|
+
>
|
|
493
|
+
<SelectorPane
|
|
494
|
+
connectors={connectors}
|
|
495
|
+
selected={selectedConnector}
|
|
496
|
+
recentConnector={recentConnector}
|
|
497
|
+
onSelect={handleSelectConnector}
|
|
498
|
+
onClose={onClose}
|
|
499
|
+
/>
|
|
500
|
+
<DetailPane
|
|
501
|
+
selectedConnector={selectedConnector}
|
|
502
|
+
qrUri={selectedConnector ? (qrUriMap[selectedConnector] ?? null) : null}
|
|
503
|
+
hasMetaMaskExtension={hasMetaMaskExtension}
|
|
504
|
+
connectorLogoMap={connectorLogoMap}
|
|
505
|
+
wcSubView={wcSubView}
|
|
506
|
+
wcWalletList={wcWalletList}
|
|
507
|
+
wcWalletListLoading={wcWalletListLoading}
|
|
508
|
+
wcWalletListSearch={wcWalletListSearch}
|
|
509
|
+
wcFilterActive={wcFilterActive}
|
|
510
|
+
wcSelectedWallet={wcSelectedWallet}
|
|
511
|
+
onBack={handleBack}
|
|
512
|
+
onClose={onClose}
|
|
513
|
+
onWCShowList={handleWCShowList}
|
|
514
|
+
onWCSelectWallet={handleWCSelectWallet}
|
|
515
|
+
onWCSearchChange={setWcWalletListSearch}
|
|
516
|
+
onWCFilterToggle={handleWCFilterToggle}
|
|
517
|
+
/>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
);
|
|
521
|
+
};
|
package/src/assets.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ConnectorId } from './types';
|
|
2
|
+
import creditWalletIcon from '../assets/creditwallet.png';
|
|
3
|
+
import metaMaskIcon from '../assets/metamask.png';
|
|
4
|
+
import wcIcon from '../assets/wc.png';
|
|
5
|
+
|
|
6
|
+
export interface ConnectorMeta {
|
|
7
|
+
label: string;
|
|
8
|
+
detailTitle: string;
|
|
9
|
+
downloadUrl?: string;
|
|
10
|
+
logoUrl?: string;
|
|
11
|
+
deepLinkBase?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const CONNECTOR_META: Record<ConnectorId, ConnectorMeta> = {
|
|
15
|
+
CREDIT_CONNECT: {
|
|
16
|
+
label: 'Continue with Credit Wallet',
|
|
17
|
+
detailTitle: 'Scan with Credit Wallet',
|
|
18
|
+
downloadUrl: 'https://creditwallet.onelink.me/Jv54',
|
|
19
|
+
logoUrl: creditWalletIcon,
|
|
20
|
+
// deepLinkBase: TODO — 앱 측 CC 딥링크 스킴 확정 후 추가
|
|
21
|
+
},
|
|
22
|
+
CREDIT_WALLET: {
|
|
23
|
+
label: 'Credit Wallet',
|
|
24
|
+
detailTitle: 'Scan with Credit Wallet',
|
|
25
|
+
downloadUrl: 'https://creditwallet.onelink.me/Jv54',
|
|
26
|
+
logoUrl: creditWalletIcon,
|
|
27
|
+
deepLinkBase: 'creditwallet://',
|
|
28
|
+
},
|
|
29
|
+
METAMASK: {
|
|
30
|
+
label: 'MetaMask',
|
|
31
|
+
detailTitle: 'Scan with MetaMask',
|
|
32
|
+
downloadUrl: 'https://metamask.io/download/',
|
|
33
|
+
logoUrl: metaMaskIcon,
|
|
34
|
+
deepLinkBase: 'metamask://',
|
|
35
|
+
},
|
|
36
|
+
WALLET_CONNECT: {
|
|
37
|
+
label: 'WalletConnect',
|
|
38
|
+
detailTitle: 'WalletConnect',
|
|
39
|
+
logoUrl: wcIcon,
|
|
40
|
+
},
|
|
41
|
+
};
|