@gluwa/connect-kit 0.2.0-next.9 → 0.2.1-next.4
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 +37 -0
- package/assets/wc.png +0 -0
- package/dist/{chunk-VE5LB7BN.js → chunk-TMKSC7ZT.js} +9 -2
- package/dist/credit-connect.js +1 -1
- package/dist/index.css +178 -122
- package/dist/index.d.ts +96 -26
- package/dist/index.js +681 -266
- package/dist/package.json +1 -1
- package/package.json +6 -6
- package/src/ConnectKitProvider.tsx +8 -1
- package/src/api/wrappers.ts +361 -0
- package/src/components/qrArea/index.tsx +25 -3
- package/src/components/qrArea/style.scss +61 -2
- package/src/components/toast/index.tsx +41 -0
- package/src/components/toast/style.scss +108 -0
- package/src/core/config.ts +23 -2
- package/src/core/deeplinkSignal.ts +25 -0
- package/src/creditConnectConnector.ts +15 -5
- package/src/events/account.ts +15 -18
- package/src/hooks/useConnectBanner.ts +79 -0
- package/src/hooks/useConnectTimeout.ts +21 -9
- package/src/hooks/useMetaMaskExtension.ts +2 -1
- package/src/hooks/useWCState.ts +7 -2
- package/src/hooks/useWagmiConnect.ts +41 -14
- package/src/index.ts +29 -24
- package/src/layout/allWallets/index.tsx +16 -2
- 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 +40 -16
- package/src/layout/connectDialog/idle/style.scss +42 -51
- package/src/layout/connectDialog/index.tsx +174 -37
- package/src/layout/connectDialog/metaMask/index.tsx +3 -2
- package/src/layout/connectDialog/qr/index.tsx +3 -1
- package/src/layout/connectDialog/style.scss +0 -48
- package/src/layout/connectDialog/timeout/index.tsx +0 -5
- package/src/layout/connectDialog/timeout/style.scss +0 -5
- package/src/layout/connectDialog/walletQR/index.tsx +3 -1
- package/src/layout/detailPanel/index.tsx +39 -6
- package/src/layout/detailPanel/style.scss +2 -1
- package/src/layout/selectorPanel/index.tsx +1 -0
- package/src/types.ts +20 -0
- package/src/utils/platform.ts +7 -0
- package/src/api/account.ts +0 -21
- package/src/api/asset.ts +0 -10
- package/src/api/balance.ts +0 -10
- package/src/api/chain.ts +0 -17
- package/src/api/contract.ts +0 -33
- package/src/api/transaction.ts +0 -12
- package/src/components/snackbar/index.tsx +0 -25
- package/src/components/snackbar/style.scss +0 -51
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
@use '../../style/mixin.scss' as *;
|
|
2
|
+
|
|
3
|
+
.ck-toast-stack {
|
|
4
|
+
position: fixed;
|
|
5
|
+
top: clamp(20px, 9.2vh, 99px);
|
|
6
|
+
left: 50%;
|
|
7
|
+
z-index: 1001;
|
|
8
|
+
display: flex;
|
|
9
|
+
flex-direction: column;
|
|
10
|
+
align-items: center;
|
|
11
|
+
gap: 8px;
|
|
12
|
+
max-width: calc(100vw - 40px);
|
|
13
|
+
transform: translateX(-50%);
|
|
14
|
+
|
|
15
|
+
@include tablet {
|
|
16
|
+
top: clamp(
|
|
17
|
+
calc(env(safe-area-inset-top, 0px) + 20px),
|
|
18
|
+
calc(env(safe-area-inset-top, 0px) + 20px),
|
|
19
|
+
calc(env(safe-area-inset-top, 0px) + 20px)
|
|
20
|
+
);
|
|
21
|
+
left: 20px;
|
|
22
|
+
right: 20px;
|
|
23
|
+
align-items: stretch;
|
|
24
|
+
max-width: none;
|
|
25
|
+
transform: none;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.ck-toast {
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
gap: 16px;
|
|
34
|
+
width: max-content;
|
|
35
|
+
min-width: min(400px, calc(100vw - 40px));
|
|
36
|
+
max-width: 100%;
|
|
37
|
+
padding: 12px 16px;
|
|
38
|
+
border-radius: 8px;
|
|
39
|
+
background: var(--semantic-paper-sheet);
|
|
40
|
+
border: 1px solid var(--semantic-divider-divider-1);
|
|
41
|
+
animation: ck-toast-in 0.2s ease;
|
|
42
|
+
|
|
43
|
+
@include tablet {
|
|
44
|
+
width: 100%;
|
|
45
|
+
min-width: 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@keyframes ck-toast-in {
|
|
50
|
+
from {
|
|
51
|
+
opacity: 0;
|
|
52
|
+
transform: translateY(8px);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
to {
|
|
56
|
+
opacity: 1;
|
|
57
|
+
transform: translateY(0);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.ck-toast-content {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
gap: 8px;
|
|
65
|
+
|
|
66
|
+
@include tablet {
|
|
67
|
+
flex: 1;
|
|
68
|
+
min-width: 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.ck-toast-icon {
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
width: 24px;
|
|
77
|
+
height: 24px;
|
|
78
|
+
flex-shrink: 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.ck-toast-icon--success {
|
|
82
|
+
color: var(--semantic-information-success);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.ck-toast-icon--warning {
|
|
86
|
+
color: var(--semantic-information-caution);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.ck-toast-message {
|
|
90
|
+
@include textToken('body3-l1-medium');
|
|
91
|
+
color: var(--scale-gray-900);
|
|
92
|
+
white-space: nowrap;
|
|
93
|
+
|
|
94
|
+
@include tablet {
|
|
95
|
+
flex: 1;
|
|
96
|
+
min-width: 0;
|
|
97
|
+
white-space: normal;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.ck-root .ck-toast-close {
|
|
102
|
+
display: flex;
|
|
103
|
+
align-items: center;
|
|
104
|
+
justify-content: center;
|
|
105
|
+
flex-shrink: 0;
|
|
106
|
+
color: var(--scale-gray-700);
|
|
107
|
+
cursor: pointer;
|
|
108
|
+
}
|
package/src/core/config.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createConfig, createStorage } from 'wagmi';
|
|
2
2
|
import { metaMask, walletConnect, injected } from 'wagmi/connectors';
|
|
3
|
+
import { hasInjectedMobileProvider } from '../utils/platform';
|
|
4
|
+
import { markDeeplinkOpened } from './deeplinkSignal';
|
|
3
5
|
import type { Config } from 'wagmi';
|
|
4
6
|
import type { ConnectKitConfig } from '../types';
|
|
5
7
|
import { creditConnectConnector } from '../creditConnectConnector';
|
|
@@ -44,11 +46,26 @@ export const initConfig = (kitConfig: ConnectKitConfig): Config => {
|
|
|
44
46
|
connectors.push(walletConnect({ projectId: kitConfig.wcProjectId, showQrModal: false }));
|
|
45
47
|
}
|
|
46
48
|
|
|
49
|
+
// MetaMask는 metaMaskSDK 커넥터 하나로만 연결한다.
|
|
50
|
+
// generic injected 커넥터를 함께 두면 같은 window.ethereum에 이중 바인딩되어
|
|
51
|
+
// wagmi connections 가 2개가 되고, dApp의 다중연결 감지 로직이 오작동한다.
|
|
47
52
|
connectors.push(
|
|
48
|
-
metaMask({
|
|
49
|
-
|
|
53
|
+
metaMask({
|
|
54
|
+
headless: true,
|
|
55
|
+
useDeeplink: true,
|
|
56
|
+
storage: { enabled: true },
|
|
57
|
+
openDeeplink: (url) => {
|
|
58
|
+
markDeeplinkOpened();
|
|
59
|
+
window.location.replace(url);
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
50
62
|
);
|
|
51
63
|
|
|
64
|
+
// 인앱 지갑 웹뷰(모바일 + 호스트 window.ethereum 주입)
|
|
65
|
+
if (hasInjectedMobileProvider()) {
|
|
66
|
+
connectors.push(injected({ shimDisconnect: false }));
|
|
67
|
+
}
|
|
68
|
+
|
|
52
69
|
if (kitConfig.creditConnect) {
|
|
53
70
|
connectors.push(
|
|
54
71
|
creditConnectConnector({
|
|
@@ -73,6 +90,10 @@ export const initConfig = (kitConfig: ConnectKitConfig): Config => {
|
|
|
73
90
|
connectors,
|
|
74
91
|
transports: kitConfig.transports,
|
|
75
92
|
storage: isBrowser ? createStorage({ storage: window.localStorage }) : undefined,
|
|
93
|
+
// EIP-6963 자동 탐지 비활성화: 켜두면 MetaMask 가 io.metamask(type='injected')로
|
|
94
|
+
// 다시 발견되어 metaMaskSDK 와 이중 커넥터가 되고 connections 가 2개로 잡힌다.
|
|
95
|
+
// MetaMask 는 위 metaMaskSDK 커넥터로만 붙이므로 탐지가 필요 없다.
|
|
96
|
+
multiInjectedProviderDiscovery: false,
|
|
76
97
|
});
|
|
77
98
|
slot.kitConfig = kitConfig;
|
|
78
99
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
type Signal = { opened: boolean };
|
|
2
|
+
|
|
3
|
+
const SIGNAL_KEY = Symbol.for('@gluwa/connect-kit:deeplink-signal');
|
|
4
|
+
|
|
5
|
+
type GlobalWithSignal = typeof globalThis & {
|
|
6
|
+
[SIGNAL_KEY]?: Signal;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const getSignal = (): Signal => {
|
|
10
|
+
const g = globalThis as GlobalWithSignal;
|
|
11
|
+
if (!g[SIGNAL_KEY]) {
|
|
12
|
+
g[SIGNAL_KEY] = { opened: false };
|
|
13
|
+
}
|
|
14
|
+
return g[SIGNAL_KEY];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const resetDeeplinkSignal = (): void => {
|
|
18
|
+
getSignal().opened = false;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const markDeeplinkOpened = (): void => {
|
|
22
|
+
getSignal().opened = true;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const hasDeeplinkOpened = (): boolean => getSignal().opened;
|
|
@@ -29,7 +29,10 @@ const CONNECTOR_TYPE = 'credit-connect';
|
|
|
29
29
|
const castConnectResult = <withCapabilities extends boolean>(value: unknown) =>
|
|
30
30
|
value as {
|
|
31
31
|
accounts: withCapabilities extends true
|
|
32
|
-
? ReadonlyArray<{
|
|
32
|
+
? ReadonlyArray<{
|
|
33
|
+
address: Address;
|
|
34
|
+
capabilities: Record<string, unknown>;
|
|
35
|
+
}>
|
|
33
36
|
: readonly Address[];
|
|
34
37
|
chainId: number;
|
|
35
38
|
};
|
|
@@ -92,7 +95,7 @@ export const creditConnectConnector = (
|
|
|
92
95
|
if (state.status === 'CONNECTING' && state.connectingPayload) {
|
|
93
96
|
// eslint-disable-next-line no-console
|
|
94
97
|
console.log('[CC connector] emit display_uri', {
|
|
95
|
-
|
|
98
|
+
ready: true,
|
|
96
99
|
});
|
|
97
100
|
options.onDisplayUri?.(state.connectingPayload);
|
|
98
101
|
emitWagmiMessage?.('display_uri', state.connectingPayload);
|
|
@@ -109,7 +112,9 @@ export const creditConnectConnector = (
|
|
|
109
112
|
|
|
110
113
|
manager.on('sessionExpired', (payload) => {
|
|
111
114
|
// eslint-disable-next-line no-console
|
|
112
|
-
console.log('[CC connector] session expired → emit session_expired',
|
|
115
|
+
console.log('[CC connector] session expired → emit session_expired', {
|
|
116
|
+
reason: payload.reason,
|
|
117
|
+
});
|
|
113
118
|
emitWagmiMessage?.('session_expired', payload);
|
|
114
119
|
});
|
|
115
120
|
|
|
@@ -318,7 +323,10 @@ export const creditConnectConnector = (
|
|
|
318
323
|
withCapabilities?: withCapabilities | boolean;
|
|
319
324
|
}): Promise<{
|
|
320
325
|
accounts: withCapabilities extends true
|
|
321
|
-
? ReadonlyArray<{
|
|
326
|
+
? ReadonlyArray<{
|
|
327
|
+
address: Address;
|
|
328
|
+
capabilities: Record<string, unknown>;
|
|
329
|
+
}>
|
|
322
330
|
: readonly Address[];
|
|
323
331
|
chainId: number;
|
|
324
332
|
}> {
|
|
@@ -412,7 +420,9 @@ export const creditConnectConnector = (
|
|
|
412
420
|
);
|
|
413
421
|
|
|
414
422
|
if (!supported) {
|
|
415
|
-
const err = new Error(`Unsupported chain: ${chainId}`) as Error & {
|
|
423
|
+
const err = new Error(`Unsupported chain: ${chainId}`) as Error & {
|
|
424
|
+
code?: number;
|
|
425
|
+
};
|
|
416
426
|
err.code = 4902;
|
|
417
427
|
throw err;
|
|
418
428
|
}
|
package/src/events/account.ts
CHANGED
|
@@ -1,51 +1,48 @@
|
|
|
1
1
|
import {
|
|
2
|
-
watchAccount as wagmiWatchAccount,
|
|
3
|
-
watchConnections as wagmiWatchConnections,
|
|
4
2
|
reconnect as wagmiReconnect,
|
|
5
3
|
connect as wagmiConnect,
|
|
6
4
|
getAccount as wagmiGetAccount,
|
|
7
5
|
disconnect as wagmiDisconnect,
|
|
8
|
-
type WatchAccountParameters,
|
|
9
|
-
type WatchConnectionsParameters,
|
|
10
6
|
type ReconnectReturnType,
|
|
11
7
|
} from '@wagmi/core';
|
|
12
8
|
import { getConfig, getKitConfig } from '../core/config';
|
|
13
9
|
|
|
14
|
-
export const watchAccount = (params: WatchAccountParameters): (() => void) => {
|
|
15
|
-
return wagmiWatchAccount(getConfig(), params);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export const watchConnections = (params: WatchConnectionsParameters): (() => void) => {
|
|
19
|
-
return wagmiWatchConnections(getConfig(), params);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
10
|
export const reconnect = async (): Promise<ReconnectReturnType> => {
|
|
23
11
|
const config = getConfig();
|
|
24
12
|
|
|
25
13
|
if (getKitConfig().autoConnectInjected !== false) {
|
|
26
14
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
// 자동 재연결
|
|
16
|
+
// 인앱 지갑 웹뷰: 호스트 주입 provider 는 injected 커넥터로만
|
|
17
|
+
const injectedConnector = config.connectors.find(
|
|
18
|
+
(connector) => connector.type === 'injected',
|
|
19
|
+
);
|
|
20
|
+
const metaMaskConnector = config.connectors.find(
|
|
21
|
+
(connector) => connector.id === 'metaMaskSDK',
|
|
22
|
+
);
|
|
23
|
+
const target = injectedConnector ?? metaMaskConnector;
|
|
24
|
+
if (target) {
|
|
25
|
+
const provider = (await target.getProvider()) as
|
|
30
26
|
| { request?: (args: { method: string }) => Promise<unknown> }
|
|
31
27
|
| undefined;
|
|
32
28
|
const accounts = provider?.request
|
|
33
29
|
? await provider.request({ method: 'eth_accounts' })
|
|
34
30
|
: [];
|
|
35
31
|
if (Array.isArray(accounts) && accounts.length > 0) {
|
|
36
|
-
await wagmiConnect(config, { connector:
|
|
32
|
+
await wagmiConnect(config, { connector: target });
|
|
37
33
|
}
|
|
38
34
|
}
|
|
39
35
|
} catch (error) {
|
|
40
36
|
// eslint-disable-next-line no-console
|
|
41
|
-
console.warn('
|
|
37
|
+
console.warn('auto-connect skipped (non-fatal):', error);
|
|
42
38
|
}
|
|
43
39
|
}
|
|
44
40
|
|
|
45
41
|
if (wagmiGetAccount(config).status === 'connected') {
|
|
46
42
|
return [];
|
|
47
43
|
}
|
|
48
|
-
|
|
44
|
+
const result = await wagmiReconnect(config);
|
|
45
|
+
return result;
|
|
49
46
|
};
|
|
50
47
|
|
|
51
48
|
export const disconnectAll = async (
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { ConnectBanner, ConnectBannerSiteKey } from '../types';
|
|
3
|
+
|
|
4
|
+
interface ConnectBannersResponse {
|
|
5
|
+
banners: ConnectBanner[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
interface UseConnectBannerParams {
|
|
9
|
+
open: boolean;
|
|
10
|
+
siteKey: ConnectBannerSiteKey;
|
|
11
|
+
url: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface UseConnectBannerResult {
|
|
15
|
+
banner: ConnectBanner | null;
|
|
16
|
+
loading: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const pickRandomBanner = (banners: ConnectBanner[]): ConnectBanner | null =>
|
|
20
|
+
banners[Math.floor(Math.random() * banners.length)] ?? null;
|
|
21
|
+
|
|
22
|
+
export const useConnectBanner = ({
|
|
23
|
+
open,
|
|
24
|
+
siteKey,
|
|
25
|
+
url,
|
|
26
|
+
}: UseConnectBannerParams): UseConnectBannerResult => {
|
|
27
|
+
const [candidates, setCandidates] = useState<ConnectBanner[]>([]);
|
|
28
|
+
const [banner, setBanner] = useState<ConnectBanner | null>(null);
|
|
29
|
+
const [loading, setLoading] = useState(true);
|
|
30
|
+
const wasOpenRef = useRef(open);
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!url) {
|
|
34
|
+
setCandidates([]);
|
|
35
|
+
setBanner(null);
|
|
36
|
+
setLoading(false);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const controller = new AbortController();
|
|
41
|
+
setCandidates([]);
|
|
42
|
+
setBanner(null);
|
|
43
|
+
setLoading(true);
|
|
44
|
+
|
|
45
|
+
fetch(url, { signal: controller.signal })
|
|
46
|
+
.then(async (response) => {
|
|
47
|
+
if (!response.ok) throw new Error(`Failed to fetch connect banners: ${response.status}`);
|
|
48
|
+
return (await response.json()) as ConnectBannersResponse;
|
|
49
|
+
})
|
|
50
|
+
.then(({ banners }) => {
|
|
51
|
+
if (controller.signal.aborted) return;
|
|
52
|
+
if (!Array.isArray(banners)) {
|
|
53
|
+
setLoading(false);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const nextCandidates = banners.filter((item) => item.siteKey !== siteKey);
|
|
58
|
+
setCandidates(nextCandidates);
|
|
59
|
+
setBanner(pickRandomBanner(nextCandidates));
|
|
60
|
+
setLoading(false);
|
|
61
|
+
})
|
|
62
|
+
.catch(() => {
|
|
63
|
+
// API 실패 시 기존 Idle placeholder를 유지한다.
|
|
64
|
+
if (!controller.signal.aborted) setLoading(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return () => controller.abort();
|
|
68
|
+
}, [siteKey, url]);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const wasOpen = wasOpenRef.current;
|
|
72
|
+
wasOpenRef.current = open;
|
|
73
|
+
|
|
74
|
+
// 닫힌 dialog 안에서 다음 배너 이미지를 미리 렌더링한다.
|
|
75
|
+
if (wasOpen && !open) setBanner(pickRandomBanner(candidates));
|
|
76
|
+
}, [candidates, open]);
|
|
77
|
+
|
|
78
|
+
return { banner, loading };
|
|
79
|
+
};
|
|
@@ -3,16 +3,19 @@ import { type ConnectorId } from '../types';
|
|
|
3
3
|
|
|
4
4
|
// MM / CC 폴백용 기본 타임아웃. (CC 는 원칙적으로 서버 push 로 만료가 오지만,
|
|
5
5
|
// 서버 값이 없거나 신호가 유실된 경우를 대비한 폴백 용도로도 쓸 수 있음.)
|
|
6
|
-
export const DEFAULT_CONNECT_TIMEOUT_MS =
|
|
6
|
+
export const DEFAULT_CONNECT_TIMEOUT_MS = 300_000;
|
|
7
7
|
|
|
8
8
|
interface UseConnectTimeoutParams {
|
|
9
9
|
/** 현재 연결 시도 중인 커넥터. null = 유휴 상태. */
|
|
10
10
|
connectorId: ConnectorId | null;
|
|
11
11
|
/** 타임아웃 감시 활성화 여부 — pending/QR 표시 중일 때 true. */
|
|
12
12
|
active: boolean;
|
|
13
|
+
/** Wagmi connect promise가 실제로 대기 중인지 여부. */
|
|
14
|
+
isConnecting: boolean;
|
|
13
15
|
/**
|
|
14
|
-
* 외부 이벤트 기반
|
|
16
|
+
* 외부 이벤트 기반 실패/만료 신호. 값이 증가할 때마다 연결 실패로 간주.
|
|
15
17
|
* - CC: dialog 가 connector 의 `session_expired`(서버 push) 를 받아 올림.
|
|
18
|
+
* - MM: connect promise reject.
|
|
16
19
|
* - WC: dialog 가 WC connect promise reject(프로토콜 ~5분 만료 등) 를 받아 올림.
|
|
17
20
|
*/
|
|
18
21
|
expiredSignal?: number;
|
|
@@ -23,6 +26,8 @@ interface UseConnectTimeoutParams {
|
|
|
23
26
|
interface UseConnectTimeoutResult {
|
|
24
27
|
/** 타임아웃 발생 여부 — true 면 dialog 가 <Timeout> 을 렌더. */
|
|
25
28
|
timedOut: boolean;
|
|
29
|
+
/** 다른 지갑 연결을 시작하기 전에 이전 타임아웃 상태를 즉시 초기화. */
|
|
30
|
+
reset: () => void;
|
|
26
31
|
/** "Try Again" 버튼에 연결할 함수 — 상태 리셋 + onRetry 실행. */
|
|
27
32
|
retry: () => void;
|
|
28
33
|
}
|
|
@@ -36,6 +41,7 @@ interface UseConnectTimeoutResult {
|
|
|
36
41
|
export const useConnectTimeout = ({
|
|
37
42
|
connectorId,
|
|
38
43
|
active,
|
|
44
|
+
isConnecting,
|
|
39
45
|
expiredSignal = 0,
|
|
40
46
|
onRetry,
|
|
41
47
|
}: UseConnectTimeoutParams): UseConnectTimeoutResult => {
|
|
@@ -54,16 +60,18 @@ export const useConnectTimeout = ({
|
|
|
54
60
|
// MM 만 client 타이머로 강제 (승인 대기에 네이티브 상한이 없음).
|
|
55
61
|
// CC(서버 push) / WC(connect reject) 는 아래 expiredSignal effect 에서 처리.
|
|
56
62
|
useEffect(() => {
|
|
57
|
-
if (!active || connectorId !== 'METAMASK') return;
|
|
58
|
-
const timer = setTimeout(
|
|
63
|
+
if (!active || !isConnecting || connectorId !== 'METAMASK') return;
|
|
64
|
+
const timer = setTimeout(() => {
|
|
65
|
+
fire();
|
|
66
|
+
}, DEFAULT_CONNECT_TIMEOUT_MS);
|
|
59
67
|
return () => clearTimeout(timer);
|
|
60
|
-
}, [active, connectorId, fire]);
|
|
68
|
+
}, [active, isConnecting, connectorId, fire]);
|
|
61
69
|
|
|
62
|
-
// 외부
|
|
70
|
+
// 외부 실패/만료 신호 처리 (CC 서버 push / MM·WC connect reject).
|
|
63
71
|
// signal 값이 올라가면(새 만료 이벤트) 발화.
|
|
64
72
|
const prevSignal = useRef(expiredSignal);
|
|
65
73
|
useEffect(() => {
|
|
66
|
-
if (!active ||
|
|
74
|
+
if (!active || !connectorId) {
|
|
67
75
|
prevSignal.current = expiredSignal;
|
|
68
76
|
return;
|
|
69
77
|
}
|
|
@@ -76,7 +84,11 @@ export const useConnectTimeout = ({
|
|
|
76
84
|
const retry = useCallback(() => {
|
|
77
85
|
setTimedOut(false);
|
|
78
86
|
onRetry?.();
|
|
79
|
-
}, [onRetry]);
|
|
87
|
+
}, [connectorId, onRetry]);
|
|
88
|
+
|
|
89
|
+
const reset = useCallback(() => {
|
|
90
|
+
setTimedOut(false);
|
|
91
|
+
}, []);
|
|
80
92
|
|
|
81
|
-
return { timedOut, retry };
|
|
93
|
+
return { timedOut, reset, retry };
|
|
82
94
|
};
|
|
@@ -13,7 +13,8 @@ export const useMetaMaskExtension = (): boolean => {
|
|
|
13
13
|
|
|
14
14
|
useEffect(() => {
|
|
15
15
|
const handleAnnouncement = (event: Event): void => {
|
|
16
|
-
|
|
16
|
+
const isMetaMask = isMetaMaskAnnouncement(event);
|
|
17
|
+
if (isMetaMask) setIsInstalled(true);
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
// 응답 listener를 먼저 등록한 뒤 설치된 provider 목록을 요청.
|
package/src/hooks/useWCState.ts
CHANGED
|
@@ -11,7 +11,10 @@ const sortCreditWalletFirst = (list: WCWallet[]): WCWallet[] =>
|
|
|
11
11
|
return 0;
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
const fetchWCWalletList = async (
|
|
14
|
+
const fetchWCWalletList = async (
|
|
15
|
+
projectId: string,
|
|
16
|
+
signal: AbortSignal | undefined,
|
|
17
|
+
): Promise<WCWallet[]> => {
|
|
15
18
|
// WC Explorer API 가 page 경계에서 같은 wallet 을 중복 emit 하는 케이스 관찰됨
|
|
16
19
|
// (paginate 사이 sort/dedup 보장 X). id 기준 first-write-wins dedup.
|
|
17
20
|
const byId = new Map<string, WCWallet>();
|
|
@@ -106,7 +109,9 @@ export const useWCState = (): WCState => {
|
|
|
106
109
|
}, []);
|
|
107
110
|
|
|
108
111
|
const loadWalletList = useCallback(async (projectId: string): Promise<void> => {
|
|
109
|
-
if (loadedRef.current)
|
|
112
|
+
if (loadedRef.current) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
110
115
|
loadedRef.current = true;
|
|
111
116
|
const controller = new AbortController();
|
|
112
117
|
abortControllerRef.current = controller;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { useEffect, useRef, useCallback } from 'react';
|
|
2
2
|
import { useConnect, useConfig } from 'wagmi';
|
|
3
|
+
import { getAccount } from '@wagmi/core';
|
|
3
4
|
import type { ConnectorId, ConnectResult } from '../types';
|
|
5
|
+
import { resolveConnectorId } from '../connector-meta';
|
|
6
|
+
import { resetDeeplinkSignal } from '../core/deeplinkSignal';
|
|
4
7
|
|
|
5
8
|
type WagmiConnectorLike = {
|
|
6
9
|
id: string;
|
|
@@ -73,6 +76,7 @@ export const useWagmiConnect = ({
|
|
|
73
76
|
}: Options): UseWagmiConnectResult => {
|
|
74
77
|
const config = useConfig();
|
|
75
78
|
const pendingConnectorId = useRef<ConnectorId | null>(null);
|
|
79
|
+
const pendingAttemptId = useRef<string | null>(null);
|
|
76
80
|
const attemptCounter = useRef(0);
|
|
77
81
|
|
|
78
82
|
const onConnectRef = useRef(onConnect);
|
|
@@ -89,17 +93,27 @@ export const useWagmiConnect = ({
|
|
|
89
93
|
const { connectAsync, reset, isPending } = useConnect();
|
|
90
94
|
|
|
91
95
|
useEffect(() => {
|
|
92
|
-
const handler = (data: unknown): void => {
|
|
93
|
-
const msg = data as { type?: string; data?: unknown };
|
|
94
|
-
if (msg?.type === 'display_uri' && typeof msg.data === 'string') {
|
|
95
|
-
onQrUriRef.current(msg.data);
|
|
96
|
-
}
|
|
97
|
-
if (msg?.type === 'session_expired') {
|
|
98
|
-
onSessionExpiredRef.current?.();
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
|
|
102
96
|
const unsubscribers = config.connectors.map((connector) => {
|
|
97
|
+
const emitterConnectorId = resolveConnectorId(connector.id);
|
|
98
|
+
const handler = (data: unknown): void => {
|
|
99
|
+
const msg = data as { type?: string; data?: unknown };
|
|
100
|
+
const pendingId = pendingConnectorId.current;
|
|
101
|
+
const belongsToPendingAttempt =
|
|
102
|
+
pendingId !== null && emitterConnectorId !== null && pendingId === emitterConnectorId;
|
|
103
|
+
|
|
104
|
+
if (msg?.type === 'display_uri' && typeof msg.data === 'string') {
|
|
105
|
+
if (!belongsToPendingAttempt) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
onQrUriRef.current(msg.data);
|
|
109
|
+
}
|
|
110
|
+
if (msg?.type === 'session_expired') {
|
|
111
|
+
if (pendingId !== null && !belongsToPendingAttempt) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
onSessionExpiredRef.current?.();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
103
117
|
connector.emitter.on('message', handler);
|
|
104
118
|
return (): void => {
|
|
105
119
|
connector.emitter.off('message', handler);
|
|
@@ -117,26 +131,38 @@ export const useWagmiConnect = ({
|
|
|
117
131
|
(connectorId: ConnectorId): void => {
|
|
118
132
|
const connector = resolveWagmiConnector(config.connectors, connectorId);
|
|
119
133
|
if (!connector) {
|
|
134
|
+
pendingAttemptId.current = null;
|
|
120
135
|
onErrorRef.current(new Error(`No wagmi connector found for ${connectorId}`), connectorId);
|
|
121
136
|
return;
|
|
122
137
|
}
|
|
123
138
|
|
|
124
139
|
onQrUriRef.current(null);
|
|
140
|
+
resetDeeplinkSignal();
|
|
125
141
|
const attemptId = ++attemptCounter.current;
|
|
126
142
|
pendingConnectorId.current = connectorId;
|
|
127
143
|
connectAsync({ connector })
|
|
128
144
|
.then((result) => {
|
|
129
|
-
if (attemptCounter.current !== attemptId)
|
|
130
|
-
|
|
145
|
+
if (attemptCounter.current !== attemptId) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
// connectAsync 의 resolve 값에 계정이 비어 오는 경우가 있다 (MetaMask 모바일에서
|
|
149
|
+
// authorized 직후 accounts 채워짐이 한 tick 늦는 race). 그때도 wagmi 스토어에는
|
|
150
|
+
// 이미 주소가 들어와 있으므로 '연결됐는가'의 원본인 스토어를 fallback 으로 쓴다.
|
|
151
|
+
// 이 fallback 이 없으면 성공한 연결 위에 실패/타임아웃 화면이 덮인다.
|
|
152
|
+
const address = result.accounts?.[0] ?? getAccount(config).address;
|
|
131
153
|
if (!address) {
|
|
132
154
|
throw new Error('Connected but no account returned');
|
|
133
155
|
}
|
|
134
156
|
pendingConnectorId.current = null;
|
|
157
|
+
pendingAttemptId.current = null;
|
|
135
158
|
onConnectRef.current({ address, connectorId });
|
|
136
159
|
})
|
|
137
160
|
.catch((error) => {
|
|
138
|
-
if (attemptCounter.current !== attemptId)
|
|
161
|
+
if (attemptCounter.current !== attemptId) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
139
164
|
pendingConnectorId.current = null;
|
|
165
|
+
pendingAttemptId.current = null;
|
|
140
166
|
onErrorRef.current(error as Error, connectorId);
|
|
141
167
|
});
|
|
142
168
|
},
|
|
@@ -146,9 +172,10 @@ export const useWagmiConnect = ({
|
|
|
146
172
|
const cancelConnect = useCallback((): void => {
|
|
147
173
|
attemptCounter.current += 1;
|
|
148
174
|
pendingConnectorId.current = null;
|
|
175
|
+
pendingAttemptId.current = null;
|
|
149
176
|
onQrUriRef.current(null);
|
|
150
177
|
reset();
|
|
151
|
-
}, [reset]);
|
|
178
|
+
}, [isPending, reset]);
|
|
152
179
|
|
|
153
180
|
return { triggerConnect, cancelConnect, isConnecting: isPending };
|
|
154
181
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { getAccount, signMessage, signTypedData } from './api/account';
|
|
6
|
-
import { watchAsset } from './api/asset';
|
|
7
|
-
import { watchAccount, watchConnections, reconnect, disconnectAll } from './events/account';
|
|
1
|
+
import * as actions from '@wagmi/core/actions';
|
|
2
|
+
import * as wrappers from './api/wrappers';
|
|
3
|
+
import { reconnect, disconnectAll } from './events/account';
|
|
4
|
+
import { getConfig } from './core/config';
|
|
8
5
|
|
|
9
6
|
export { ConnectKitProvider, useConnectKit } from './ConnectKitProvider';
|
|
10
7
|
export type { ConnectKitContextValue, ConnectKitProviderProps } from './ConnectKitProvider';
|
|
@@ -14,6 +11,7 @@ export { initConfig } from './core/config';
|
|
|
14
11
|
export type {
|
|
15
12
|
ConnectErrorContext,
|
|
16
13
|
ConnectErrorReason,
|
|
14
|
+
ConnectBannerSiteKey,
|
|
17
15
|
ConnectModalProps,
|
|
18
16
|
ConnectKitConfig,
|
|
19
17
|
ConnectKitTheme,
|
|
@@ -25,25 +23,32 @@ export type {
|
|
|
25
23
|
WCWallet,
|
|
26
24
|
} from './types';
|
|
27
25
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
26
|
+
type StripConfig<T> = T extends (config: any, ...rest: infer A) => infer R
|
|
27
|
+
? (...rest: A) => R
|
|
28
|
+
: never;
|
|
29
|
+
|
|
30
|
+
type Bridge = { [K in keyof typeof actions]: StripConfig<(typeof actions)[K]> };
|
|
31
|
+
|
|
32
|
+
const bridged = Object.fromEntries(
|
|
33
|
+
Object.entries(actions).map(([name, fn]) => [
|
|
34
|
+
name,
|
|
35
|
+
(...args: unknown[]) => (fn as (...a: unknown[]) => unknown)(getConfig(), ...args),
|
|
36
|
+
]),
|
|
37
|
+
) as Bridge;
|
|
38
|
+
|
|
39
|
+
type KitActions = Omit<Bridge, keyof typeof wrappers | 'reconnect' | 'disconnectAll'> &
|
|
40
|
+
typeof wrappers & {
|
|
41
|
+
reconnect: typeof reconnect;
|
|
42
|
+
disconnectAll: typeof disconnectAll;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const connectKit: { readonly actions: KitActions } = {
|
|
46
|
+
actions: {
|
|
47
|
+
...bridged,
|
|
48
|
+
...wrappers,
|
|
44
49
|
reconnect,
|
|
45
50
|
disconnectAll,
|
|
46
51
|
},
|
|
47
|
-
}
|
|
52
|
+
};
|
|
48
53
|
|
|
49
54
|
export type * from '@wagmi/core';
|