@gluwa/connect-kit 0.1.0-next.1 → 0.1.0-next.3
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/chunk-DDA6FP6U.js +7 -0
- package/dist/credit-connect.d.ts +16 -0
- package/dist/credit-connect.js +399 -0
- package/dist/index.d.ts +55 -15
- package/dist/index.js +348 -432
- package/dist/package.json +13 -1
- package/package.json +10 -1
- package/src/ConnectKitProvider.tsx +198 -0
- package/src/ConnectModal.scss +40 -2
- package/src/ConnectModal.tsx +193 -61
- package/src/credit-connect.ts +2 -0
- package/src/creditConnectConnector.ts +1 -0
- package/src/index.ts +4 -2
- package/src/types.ts +14 -0
- package/src/views/CreditWalletView.tsx +5 -2
- package/src/views/SwitchChainView.tsx +67 -0
- package/tsup.config.ts +5 -1
package/src/ConnectModal.tsx
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback,
|
|
1
|
+
import { useState, useEffect, useCallback, useMemo, type FC, type ReactNode } from 'react';
|
|
2
2
|
import './ConnectModal.scss';
|
|
3
|
-
import { useConfig } from 'wagmi';
|
|
3
|
+
import { useAccount, useConfig, useDisconnect } from 'wagmi';
|
|
4
4
|
import {
|
|
5
5
|
type ConnectModalProps,
|
|
6
|
+
type ConnectErrorContext,
|
|
7
|
+
type ConnectErrorReason,
|
|
6
8
|
type Connectors,
|
|
7
9
|
type ConnectorId,
|
|
8
10
|
type WCWallet,
|
|
@@ -16,6 +18,7 @@ import { IdleView } from './views/IdleView';
|
|
|
16
18
|
import { CreditWalletView } from './views/CreditWalletView';
|
|
17
19
|
import { MetaMaskView } from './views/MetaMaskView';
|
|
18
20
|
import { WalletConnectView } from './views/WalletConnectView';
|
|
21
|
+
import { SwitchChainView } from './views/SwitchChainView';
|
|
19
22
|
|
|
20
23
|
const RECENT_KEY = 'connect-kit:recent';
|
|
21
24
|
const readRecentConnector = (): ConnectorId | null => {
|
|
@@ -32,6 +35,18 @@ const writeRecentConnector = (id: ConnectorId): void => {
|
|
|
32
35
|
} catch {}
|
|
33
36
|
};
|
|
34
37
|
|
|
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';
|
|
48
|
+
};
|
|
49
|
+
|
|
35
50
|
interface SelectorPaneProps {
|
|
36
51
|
connectors: ConnectorId[];
|
|
37
52
|
selected: ConnectorId | null;
|
|
@@ -149,6 +164,7 @@ interface DetailPaneProps {
|
|
|
149
164
|
hasMetaMaskExtension: boolean;
|
|
150
165
|
connectorLogoMap: Partial<Record<ConnectorId, string>>;
|
|
151
166
|
wc: WCState;
|
|
167
|
+
errorOverride: ReactNode | null;
|
|
152
168
|
onBack: () => void;
|
|
153
169
|
onClose: () => void;
|
|
154
170
|
}
|
|
@@ -159,6 +175,7 @@ const DetailPane: FC<DetailPaneProps> = ({
|
|
|
159
175
|
hasMetaMaskExtension,
|
|
160
176
|
connectorLogoMap,
|
|
161
177
|
wc,
|
|
178
|
+
errorOverride,
|
|
162
179
|
onBack,
|
|
163
180
|
onClose,
|
|
164
181
|
}) => {
|
|
@@ -181,35 +198,39 @@ const DetailPane: FC<DetailPaneProps> = ({
|
|
|
181
198
|
</div>
|
|
182
199
|
|
|
183
200
|
<div className="ck-pane__body">
|
|
184
|
-
{
|
|
201
|
+
{errorOverride || (
|
|
202
|
+
<>
|
|
203
|
+
{!selectedConnector && <IdleView />}
|
|
185
204
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
205
|
+
{(selectedConnector === 'CREDIT_CONNECT' || selectedConnector === 'CREDIT_WALLET') && (
|
|
206
|
+
<CreditWalletView connectorId={selectedConnector} qrUri={qrUri} />
|
|
207
|
+
)}
|
|
189
208
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
209
|
+
{selectedConnector === 'METAMASK' && (
|
|
210
|
+
<MetaMaskView
|
|
211
|
+
qrUri={qrUri}
|
|
212
|
+
hasExtension={hasMetaMaskExtension}
|
|
213
|
+
logoUrl={connectorLogoMap.METAMASK}
|
|
214
|
+
/>
|
|
215
|
+
)}
|
|
197
216
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
</>
|
|
213
234
|
)}
|
|
214
235
|
</div>
|
|
215
236
|
</div>
|
|
@@ -278,45 +299,69 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
278
299
|
onClose,
|
|
279
300
|
onLog,
|
|
280
301
|
wcProjectId,
|
|
302
|
+
requiredChainId,
|
|
303
|
+
renderConnectError,
|
|
281
304
|
}) => {
|
|
282
305
|
const [selectedConnector, setSelectedConnector] = useState<ConnectorId | null>(null);
|
|
283
|
-
const [
|
|
284
|
-
const pendingQrConnectorRef = useRef<ConnectorId | null>(null);
|
|
306
|
+
const [qrUri, setQrUri] = useState<string | null>(null);
|
|
285
307
|
const [recentConnector, setRecentConnector] = useState<ConnectorId | null>(null);
|
|
286
308
|
const [hasMetaMaskExtension, setHasMetaMaskExtension] = useState<boolean>(false);
|
|
309
|
+
const [errorState, setErrorState] = useState<{ connectorId: ConnectorId; error: Error } | null>(
|
|
310
|
+
null,
|
|
311
|
+
);
|
|
287
312
|
|
|
288
313
|
const wc = useWCState();
|
|
289
314
|
|
|
290
|
-
//
|
|
315
|
+
// wagmi 상태 — chain mismatch invariant 판정용
|
|
316
|
+
const account = useAccount();
|
|
317
|
+
const { disconnectAsync } = useDisconnect();
|
|
318
|
+
const wagmiConfig = useConfig();
|
|
319
|
+
|
|
320
|
+
const chainMismatch =
|
|
321
|
+
requiredChainId != null &&
|
|
322
|
+
account.status === 'connected' &&
|
|
323
|
+
account.chainId != null &&
|
|
324
|
+
account.chainId !== requiredChainId;
|
|
325
|
+
|
|
326
|
+
const requiredChainName = useMemo<string | undefined>(() => {
|
|
327
|
+
if (requiredChainId == null) return undefined;
|
|
328
|
+
return wagmiConfig.chains.find((chain) => chain.id === requiredChainId)?.name;
|
|
329
|
+
}, [wagmiConfig.chains, requiredChainId]);
|
|
330
|
+
|
|
331
|
+
// QR URI 수신
|
|
291
332
|
const handleQrUri = useCallback((uri: string | null) => {
|
|
292
|
-
|
|
293
|
-
const id = pendingQrConnectorRef.current;
|
|
294
|
-
if (id) setQrUriMap((prev) => ({ ...prev, [id]: uri }));
|
|
295
|
-
}
|
|
333
|
+
setQrUri(uri);
|
|
296
334
|
}, []);
|
|
297
335
|
|
|
298
336
|
// 연결 완료
|
|
299
337
|
const handleConnect = useCallback(
|
|
300
338
|
(result: Parameters<typeof onConnect>[0]) => {
|
|
339
|
+
setErrorState(null);
|
|
301
340
|
onConnect(result);
|
|
341
|
+
// chain mismatch가 있으면 모달은 SwitchChainView로 자동 전환됨 (닫지 않음)
|
|
342
|
+
// Provider 측에서 requiredChainId가 매칭될 때까지 force-open 유지
|
|
302
343
|
onClose();
|
|
303
344
|
},
|
|
304
345
|
[onConnect, onClose],
|
|
305
346
|
);
|
|
306
347
|
|
|
307
|
-
// 연결 실패
|
|
348
|
+
// 연결 실패
|
|
308
349
|
const handleError = useCallback(
|
|
309
|
-
(error:
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
});
|
|
350
|
+
(error: unknown, connectorId: ConnectorId): void => {
|
|
351
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
352
|
+
onLog?.(
|
|
353
|
+
`[connect-kit] connection failed (${connectorId}): ${normalizedError.message}`,
|
|
354
|
+
normalizedError,
|
|
355
|
+
);
|
|
356
|
+
setQrUri(null);
|
|
317
357
|
wc.resetView();
|
|
358
|
+
if (renderConnectError) {
|
|
359
|
+
setErrorState({ connectorId, error: normalizedError });
|
|
360
|
+
} else {
|
|
361
|
+
setSelectedConnector(null);
|
|
362
|
+
}
|
|
318
363
|
},
|
|
319
|
-
[onLog, wc.resetView],
|
|
364
|
+
[onLog, wc.resetView, renderConnectError],
|
|
320
365
|
);
|
|
321
366
|
|
|
322
367
|
// 연결 시도
|
|
@@ -327,7 +372,6 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
327
372
|
});
|
|
328
373
|
|
|
329
374
|
// wagmi connector icon 매핑
|
|
330
|
-
const wagmiConfig = useConfig();
|
|
331
375
|
const connectorLogoMap = useMemo(() => {
|
|
332
376
|
const map: Partial<Record<ConnectorId, string>> = {};
|
|
333
377
|
const connectorIds: ConnectorId[] = [
|
|
@@ -349,24 +393,24 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
349
393
|
setHasMetaMaskExtension(detectMetaMaskExtension());
|
|
350
394
|
}, []);
|
|
351
395
|
|
|
352
|
-
// 모달 닫힐 때 상태
|
|
396
|
+
// 모달 닫힐 때 상태 초기화
|
|
353
397
|
useEffect(() => {
|
|
354
398
|
if (!open) {
|
|
355
399
|
setSelectedConnector(null);
|
|
356
|
-
|
|
357
|
-
|
|
400
|
+
setQrUri(null);
|
|
401
|
+
setErrorState(null);
|
|
358
402
|
wc.reset();
|
|
359
403
|
cancelConnect();
|
|
360
404
|
}
|
|
361
405
|
}, [open, cancelConnect, wc.reset]);
|
|
362
406
|
|
|
363
|
-
// 커넥터 선택 —
|
|
407
|
+
// 커넥터 선택 — 선택할 때마다 QR을 초기화해 새 세션 URI가 표시되도록 함
|
|
364
408
|
const handleSelectConnector = useCallback(
|
|
365
409
|
(id: ConnectorId): void => {
|
|
366
|
-
if (id === selectedConnector) return;
|
|
367
|
-
|
|
368
|
-
pendingQrConnectorRef.current = id;
|
|
410
|
+
if (id === selectedConnector && !errorState) return;
|
|
369
411
|
setSelectedConnector(id);
|
|
412
|
+
setQrUri(null);
|
|
413
|
+
setErrorState(null);
|
|
370
414
|
wc.resetView();
|
|
371
415
|
writeRecentConnector(id);
|
|
372
416
|
setRecentConnector(id);
|
|
@@ -380,6 +424,7 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
380
424
|
},
|
|
381
425
|
[
|
|
382
426
|
selectedConnector,
|
|
427
|
+
errorState,
|
|
383
428
|
triggerConnect,
|
|
384
429
|
cancelConnect,
|
|
385
430
|
wcProjectId,
|
|
@@ -388,19 +433,105 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
388
433
|
],
|
|
389
434
|
);
|
|
390
435
|
|
|
391
|
-
// 뒤로가기
|
|
436
|
+
// 뒤로가기
|
|
392
437
|
const handleBack = useCallback((): void => {
|
|
438
|
+
if (errorState) {
|
|
439
|
+
setErrorState(null);
|
|
440
|
+
setSelectedConnector(null);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
393
443
|
if (selectedConnector === 'WALLET_CONNECT' && wc.handleBack()) return;
|
|
394
|
-
pendingQrConnectorRef.current = null;
|
|
395
444
|
setSelectedConnector(null);
|
|
445
|
+
setQrUri(null);
|
|
396
446
|
cancelConnect();
|
|
397
|
-
}, [selectedConnector, wc.handleBack, cancelConnect]);
|
|
447
|
+
}, [selectedConnector, errorState, wc.handleBack, cancelConnect]);
|
|
448
|
+
|
|
449
|
+
// chain mismatch 상태에서의 disconnect (Switch chain pane의 cancel)
|
|
450
|
+
const handleSwitchChainDisconnect = useCallback((): void => {
|
|
451
|
+
disconnectAsync().catch((err: unknown) => {
|
|
452
|
+
const normalized = err instanceof Error ? err : new Error(String(err));
|
|
453
|
+
onLog?.(`[connect-kit] disconnect failed: ${normalized.message}`, normalized);
|
|
454
|
+
});
|
|
455
|
+
}, [disconnectAsync, onLog]);
|
|
456
|
+
|
|
457
|
+
// overlay 클릭 — chain mismatch 상태에선 무시 (실수 클릭 방어)
|
|
458
|
+
const handleOverlayClick = useCallback((): void => {
|
|
459
|
+
if (chainMismatch) return;
|
|
460
|
+
onClose();
|
|
461
|
+
}, [chainMismatch, onClose]);
|
|
462
|
+
|
|
463
|
+
// close 버튼 — chain mismatch 상태에선 disconnect
|
|
464
|
+
const handleCloseClick = useCallback((): void => {
|
|
465
|
+
if (chainMismatch) {
|
|
466
|
+
handleSwitchChainDisconnect();
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
onClose();
|
|
470
|
+
}, [chainMismatch, handleSwitchChainDisconnect, onClose]);
|
|
471
|
+
|
|
472
|
+
const renderedError: ReactNode = useMemo(() => {
|
|
473
|
+
if (!errorState || !renderConnectError) return null;
|
|
474
|
+
const ctx: ConnectErrorContext = {
|
|
475
|
+
connectorId: errorState.connectorId,
|
|
476
|
+
reason: classifyError(errorState.error),
|
|
477
|
+
error: errorState.error,
|
|
478
|
+
retry: () => {
|
|
479
|
+
const id = errorState.connectorId;
|
|
480
|
+
setErrorState(null);
|
|
481
|
+
setQrUri(null);
|
|
482
|
+
cancelConnect();
|
|
483
|
+
triggerConnect(id);
|
|
484
|
+
},
|
|
485
|
+
dismiss: () => {
|
|
486
|
+
setErrorState(null);
|
|
487
|
+
setSelectedConnector(null);
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
return renderConnectError(ctx);
|
|
491
|
+
}, [errorState, renderConnectError, triggerConnect, cancelConnect]);
|
|
492
|
+
|
|
493
|
+
// chain mismatch면 단일 pane (selector/detail 숨김)
|
|
494
|
+
// requiredChainId != null 재확인은 TS narrowing용 — chainMismatch true면 이미 보장됨
|
|
495
|
+
if (chainMismatch && requiredChainId != null) {
|
|
496
|
+
return (
|
|
497
|
+
<div className="ck-root">
|
|
498
|
+
<div className="ck-overlay" onClick={handleOverlayClick} />
|
|
499
|
+
<div
|
|
500
|
+
className="ck-modal ck-modal--switch-chain"
|
|
501
|
+
role="dialog"
|
|
502
|
+
aria-modal="true"
|
|
503
|
+
aria-label="Switch network"
|
|
504
|
+
>
|
|
505
|
+
<div className="ck-pane">
|
|
506
|
+
<div className="ck-pane__header">
|
|
507
|
+
<h3 className="ck-pane__title">Switch network</h3>
|
|
508
|
+
<button
|
|
509
|
+
type="button"
|
|
510
|
+
className="ck-btn-close"
|
|
511
|
+
onClick={handleCloseClick}
|
|
512
|
+
aria-label="Cancel and disconnect"
|
|
513
|
+
/>
|
|
514
|
+
</div>
|
|
515
|
+
<div className="ck-pane__body">
|
|
516
|
+
<SwitchChainView
|
|
517
|
+
currentChainId={account.chainId}
|
|
518
|
+
requiredChainId={requiredChainId}
|
|
519
|
+
requiredChainName={requiredChainName}
|
|
520
|
+
onDisconnect={handleSwitchChainDisconnect}
|
|
521
|
+
onLog={onLog}
|
|
522
|
+
/>
|
|
523
|
+
</div>
|
|
524
|
+
</div>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
);
|
|
528
|
+
}
|
|
398
529
|
|
|
399
530
|
return (
|
|
400
531
|
<div className="ck-root">
|
|
401
|
-
<div className="ck-overlay" onClick={
|
|
532
|
+
<div className="ck-overlay" onClick={handleOverlayClick} />
|
|
402
533
|
<div
|
|
403
|
-
className={`ck-modal${selectedConnector ? ' ck-modal--has-detail' : ''}`}
|
|
534
|
+
className={`ck-modal${(selectedConnector ?? errorState) ? ' ck-modal--has-detail' : ''}`}
|
|
404
535
|
role="dialog"
|
|
405
536
|
aria-modal="true"
|
|
406
537
|
aria-label="Connect Wallet"
|
|
@@ -410,16 +541,17 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
410
541
|
selected={selectedConnector}
|
|
411
542
|
recentConnector={recentConnector}
|
|
412
543
|
onSelect={handleSelectConnector}
|
|
413
|
-
onClose={
|
|
544
|
+
onClose={handleCloseClick}
|
|
414
545
|
/>
|
|
415
546
|
<DetailPane
|
|
416
547
|
selectedConnector={selectedConnector}
|
|
417
|
-
qrUri={
|
|
548
|
+
qrUri={qrUri}
|
|
418
549
|
hasMetaMaskExtension={hasMetaMaskExtension}
|
|
419
550
|
connectorLogoMap={connectorLogoMap}
|
|
420
551
|
wc={wc}
|
|
552
|
+
errorOverride={renderedError}
|
|
421
553
|
onBack={handleBack}
|
|
422
|
-
onClose={
|
|
554
|
+
onClose={handleCloseClick}
|
|
423
555
|
/>
|
|
424
556
|
</div>
|
|
425
557
|
</div>
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
export { ConnectModal } from './ConnectModal';
|
|
2
|
-
export {
|
|
3
|
-
export type {
|
|
2
|
+
export { ConnectKitProvider, useConnectKit } from './ConnectKitProvider';
|
|
3
|
+
export type { ConnectKitContextValue, ConnectKitProviderProps } from './ConnectKitProvider';
|
|
4
4
|
export type {
|
|
5
|
+
ConnectErrorContext,
|
|
6
|
+
ConnectErrorReason,
|
|
5
7
|
ConnectModalProps,
|
|
6
8
|
Connectors,
|
|
7
9
|
ConnectResult,
|
package/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
1
3
|
export type ConnectorId = 'CREDIT_WALLET' | 'CREDIT_CONNECT' | 'METAMASK' | 'WALLET_CONNECT';
|
|
2
4
|
|
|
3
5
|
export type CreditWalletStrategy = 'walletConnect' | 'creditConnect';
|
|
@@ -28,6 +30,16 @@ export interface ConnectResult {
|
|
|
28
30
|
connectorId: ConnectorId;
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
export type ConnectErrorReason = 'rejected' | 'unsupported' | 'unknown';
|
|
34
|
+
|
|
35
|
+
export interface ConnectErrorContext {
|
|
36
|
+
connectorId: ConnectorId;
|
|
37
|
+
reason: ConnectErrorReason;
|
|
38
|
+
error: Error;
|
|
39
|
+
retry: () => void;
|
|
40
|
+
dismiss: () => void;
|
|
41
|
+
}
|
|
42
|
+
|
|
31
43
|
export interface ConnectModalProps {
|
|
32
44
|
open: boolean;
|
|
33
45
|
connectors: Connectors;
|
|
@@ -35,4 +47,6 @@ export interface ConnectModalProps {
|
|
|
35
47
|
onClose: () => void;
|
|
36
48
|
onLog?: (message: string, error?: Error) => void;
|
|
37
49
|
wcProjectId?: string;
|
|
50
|
+
requiredChainId?: number;
|
|
51
|
+
renderConnectError?: (ctx: ConnectErrorContext) => ReactNode;
|
|
38
52
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FC, useEffect, useMemo } from 'react';
|
|
1
|
+
import { type FC, useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { isMobileDevice, tryOpenDeepLink } from '../utils/platform';
|
|
3
3
|
import { CONNECTOR_META } from '../connector-meta';
|
|
4
4
|
import { type ConnectorId } from '../types';
|
|
@@ -12,9 +12,12 @@ interface CreditWalletViewProps {
|
|
|
12
12
|
export const CreditWalletView: FC<CreditWalletViewProps> = ({ connectorId, qrUri }) => {
|
|
13
13
|
const { downloadUrl, logoUrl, deepLinkBase } = CONNECTOR_META[connectorId];
|
|
14
14
|
const isMobile = useMemo(() => isMobileDevice(), []);
|
|
15
|
+
// WC 세션 만료로 qrUri가 재발급되더라도 딥링크는 최초 1회만 실행
|
|
16
|
+
const hasRedirectedRef = useRef(false);
|
|
15
17
|
|
|
16
18
|
useEffect(() => {
|
|
17
|
-
if (isMobile && qrUri && deepLinkBase) {
|
|
19
|
+
if (isMobile && qrUri && deepLinkBase && !hasRedirectedRef.current) {
|
|
20
|
+
hasRedirectedRef.current = true;
|
|
18
21
|
tryOpenDeepLink(qrUri, deepLinkBase);
|
|
19
22
|
}
|
|
20
23
|
}, [isMobile, qrUri, deepLinkBase]);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useState, type FC } from 'react';
|
|
2
|
+
import { useSwitchChain } from 'wagmi';
|
|
3
|
+
|
|
4
|
+
interface SwitchChainViewProps {
|
|
5
|
+
currentChainId: number | undefined;
|
|
6
|
+
requiredChainId: number;
|
|
7
|
+
requiredChainName?: string;
|
|
8
|
+
onDisconnect: () => void;
|
|
9
|
+
onLog?: (message: string, error?: Error) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const SwitchChainView: FC<SwitchChainViewProps> = ({
|
|
13
|
+
currentChainId,
|
|
14
|
+
requiredChainId,
|
|
15
|
+
requiredChainName,
|
|
16
|
+
onDisconnect,
|
|
17
|
+
onLog,
|
|
18
|
+
}) => {
|
|
19
|
+
const { switchChainAsync, isPending } = useSwitchChain();
|
|
20
|
+
const [error, setError] = useState<Error | null>(null);
|
|
21
|
+
|
|
22
|
+
const targetLabel = requiredChainName ?? `chain ${requiredChainId}`;
|
|
23
|
+
|
|
24
|
+
const handleSwitch = (): void => {
|
|
25
|
+
setError(null);
|
|
26
|
+
switchChainAsync({ chainId: requiredChainId }).catch((err: unknown) => {
|
|
27
|
+
const normalized = err instanceof Error ? err : new Error(String(err));
|
|
28
|
+
// wagmi's MetaMask connector handles 4902 (chain not added) via wallet_addEthereumChain
|
|
29
|
+
// automatically when chain metadata is configured. We only surface what reaches us.
|
|
30
|
+
const code =
|
|
31
|
+
typeof err === 'object' && err !== null && 'code' in err
|
|
32
|
+
? (err as { code: unknown }).code
|
|
33
|
+
: undefined;
|
|
34
|
+
if (code === 4902) {
|
|
35
|
+
onLog?.('[connect-kit] chain not registered in wallet (4902)', normalized);
|
|
36
|
+
} else {
|
|
37
|
+
onLog?.(`[connect-kit] switch chain failed: ${normalized.message}`, normalized);
|
|
38
|
+
}
|
|
39
|
+
setError(normalized);
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="ck-view ck-view--switch-chain">
|
|
45
|
+
<h4 className="ck-view__title">Wrong network</h4>
|
|
46
|
+
<p className="ck-view__description">
|
|
47
|
+
{currentChainId != null
|
|
48
|
+
? `Connected to chain ${currentChainId}. Please switch to ${targetLabel} to continue.`
|
|
49
|
+
: `Please switch to ${targetLabel} to continue.`}
|
|
50
|
+
</p>
|
|
51
|
+
<div className="ck-view__actions">
|
|
52
|
+
<button
|
|
53
|
+
type="button"
|
|
54
|
+
className="ck-btn-primary"
|
|
55
|
+
onClick={handleSwitch}
|
|
56
|
+
disabled={isPending}
|
|
57
|
+
>
|
|
58
|
+
{isPending ? 'Waiting for wallet…' : `Switch to ${targetLabel}`}
|
|
59
|
+
</button>
|
|
60
|
+
<button type="button" className="ck-btn-secondary" onClick={onDisconnect}>
|
|
61
|
+
Disconnect
|
|
62
|
+
</button>
|
|
63
|
+
</div>
|
|
64
|
+
{error && <p className="ck-view__caption ck-view__caption--error">{error.message}</p>}
|
|
65
|
+
</div>
|
|
66
|
+
);
|
|
67
|
+
};
|
package/tsup.config.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { defineConfig } from 'tsup';
|
|
|
2
2
|
import { sassPlugin } from 'esbuild-sass-plugin';
|
|
3
3
|
|
|
4
4
|
export default defineConfig({
|
|
5
|
-
entry: {
|
|
5
|
+
entry: {
|
|
6
|
+
index: 'src/index.ts',
|
|
7
|
+
'credit-connect': 'src/credit-connect.ts',
|
|
8
|
+
},
|
|
6
9
|
format: ['esm'],
|
|
7
10
|
dts: true,
|
|
8
11
|
clean: true,
|
|
@@ -13,6 +16,7 @@ export default defineConfig({
|
|
|
13
16
|
'wagmi',
|
|
14
17
|
'@wagmi/core',
|
|
15
18
|
'viem',
|
|
19
|
+
'@tanstack/react-query',
|
|
16
20
|
'@gluwa/credit-connect-sdk',
|
|
17
21
|
'@gluwa/credit-connect-sdk/dapp',
|
|
18
22
|
'@gluwa/credit-connect-sdk/storage',
|