@gluwa/connect-kit 0.1.0-next.3 → 0.2.0-next.1
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 +6 -0
- package/dist/credit-connect.js +148 -133
- package/dist/index.css +561 -0
- package/dist/index.d.ts +39 -25
- package/dist/index.js +315 -655
- package/dist/package.json +11 -6
- package/package.json +8 -5
- package/src/ConnectKitProvider.tsx +75 -49
- package/src/ConnectModal.tsx +39 -25
- package/src/api/account.ts +21 -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/components/QRFrame.tsx +2 -2
- package/src/core/config.ts +83 -0
- package/src/creditConnectConnector.ts +175 -159
- package/src/events/account.ts +49 -0
- package/src/hooks/useWCState.ts +24 -9
- package/src/hooks/useWagmiConnect.ts +13 -8
- package/src/index.ts +33 -0
- package/src/types.ts +9 -0
- package/src/utils/platform.ts +1 -4
- package/src/views/CreditWalletView.tsx +0 -1
- package/src/views/MetaMaskView.tsx +4 -2
- package/src/views/SwitchChainView.tsx +9 -4
- package/tsup.config.ts +1 -1
package/dist/package.json
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gluwa/connect-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-next.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
7
|
+
"types": "./src/index.ts",
|
|
7
8
|
"default": "./src/index.ts"
|
|
8
9
|
},
|
|
9
10
|
"./credit-connect": {
|
|
11
|
+
"types": "./src/credit-connect.ts",
|
|
10
12
|
"default": "./src/credit-connect.ts"
|
|
11
13
|
},
|
|
12
14
|
"./package.json": "./package.json"
|
|
@@ -27,7 +29,10 @@
|
|
|
27
29
|
"./index.css": "./dist/index.css"
|
|
28
30
|
}
|
|
29
31
|
},
|
|
30
|
-
"sideEffects":
|
|
32
|
+
"sideEffects": [
|
|
33
|
+
"**/*.scss",
|
|
34
|
+
"**/*.css"
|
|
35
|
+
],
|
|
31
36
|
"scripts": {
|
|
32
37
|
"prepack": "pnpm build && cp package.json dist/ && cp README.md dist/",
|
|
33
38
|
"build": "tsup",
|
|
@@ -38,7 +43,9 @@
|
|
|
38
43
|
"author": "",
|
|
39
44
|
"license": "ISC",
|
|
40
45
|
"dependencies": {
|
|
41
|
-
"qrcode.react": "^
|
|
46
|
+
"qrcode.react": "^4.2.0",
|
|
47
|
+
"wagmi": "^2.15.6",
|
|
48
|
+
"@wagmi/core": "^2.16.7"
|
|
42
49
|
},
|
|
43
50
|
"devDependencies": {
|
|
44
51
|
"@gluwa/credit-connect-sdk": "workspace:*",
|
|
@@ -57,10 +64,8 @@
|
|
|
57
64
|
"peerDependencies": {
|
|
58
65
|
"@gluwa/credit-connect-sdk": "workspace:*",
|
|
59
66
|
"@tanstack/react-query": ">=5.0.0",
|
|
60
|
-
"@wagmi/core": ">=2.0.0",
|
|
61
67
|
"react": "^18.0.0 || ^19.0.0",
|
|
62
|
-
"viem": ">=2.0.0"
|
|
63
|
-
"wagmi": ">=2.0.0"
|
|
68
|
+
"viem": ">=2.0.0"
|
|
64
69
|
},
|
|
65
70
|
"peerDependenciesMeta": {
|
|
66
71
|
"@gluwa/credit-connect-sdk": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gluwa/connect-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-next.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -16,11 +16,16 @@
|
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
|
-
"sideEffects":
|
|
19
|
+
"sideEffects": [
|
|
20
|
+
"**/*.scss",
|
|
21
|
+
"**/*.css"
|
|
22
|
+
],
|
|
20
23
|
"author": "",
|
|
21
24
|
"license": "ISC",
|
|
22
25
|
"dependencies": {
|
|
23
|
-
"qrcode.react": "^
|
|
26
|
+
"qrcode.react": "^4.2.0",
|
|
27
|
+
"wagmi": "^2.15.6",
|
|
28
|
+
"@wagmi/core": "^2.16.7"
|
|
24
29
|
},
|
|
25
30
|
"devDependencies": {
|
|
26
31
|
"@tanstack/react-query": "^5.62.0",
|
|
@@ -38,10 +43,8 @@
|
|
|
38
43
|
},
|
|
39
44
|
"peerDependencies": {
|
|
40
45
|
"@tanstack/react-query": ">=5.0.0",
|
|
41
|
-
"@wagmi/core": ">=2.0.0",
|
|
42
46
|
"react": "^18.0.0 || ^19.0.0",
|
|
43
47
|
"viem": ">=2.0.0",
|
|
44
|
-
"wagmi": ">=2.0.0",
|
|
45
48
|
"@gluwa/credit-connect-sdk": "0.2.0-next.1"
|
|
46
49
|
},
|
|
47
50
|
"peerDependenciesMeta": {
|
|
@@ -9,46 +9,28 @@ import {
|
|
|
9
9
|
type FC,
|
|
10
10
|
type ReactNode,
|
|
11
11
|
} from 'react';
|
|
12
|
-
import { WagmiProvider, useAccount, useDisconnect } from 'wagmi';
|
|
13
|
-
import type { Config } from 'wagmi';
|
|
12
|
+
import { WagmiProvider, useAccount, useDisconnect, useSwitchChain } from 'wagmi';
|
|
14
13
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
15
14
|
import { ConnectModal } from './ConnectModal';
|
|
16
|
-
import
|
|
15
|
+
import { getConfig, getKitConfig, initConfig } from './core/config';
|
|
16
|
+
import type {
|
|
17
|
+
ConnectErrorContext,
|
|
18
|
+
ConnectKitConfig,
|
|
19
|
+
ConnectResult,
|
|
20
|
+
ConnectorId,
|
|
21
|
+
Connectors,
|
|
22
|
+
} from './types';
|
|
17
23
|
|
|
18
|
-
// wagmiConfig prop으로 기존 프로젝트 통합 시 내부에서 사용하는 QueryClient.
|
|
19
|
-
// 모듈 레벨에 한 번만 생성 — 연결 플로우용으로만 쓰이므로 공유해도 무방.
|
|
20
24
|
const internalQueryClient = new QueryClient();
|
|
21
25
|
|
|
22
26
|
export interface ConnectKitProviderProps {
|
|
23
27
|
children: ReactNode;
|
|
28
|
+
kitConfig?: ConnectKitConfig;
|
|
24
29
|
connectors: Connectors;
|
|
25
|
-
wcProjectId?: string;
|
|
26
|
-
/**
|
|
27
|
-
* 연결돼 있는 동안 강제할 체인 ID. 다른 체인에 연결돼 있으면
|
|
28
|
-
* 자동으로 스위치 모달이 떠서 사용자에게 전환 또는 disconnect를 요구.
|
|
29
|
-
*/
|
|
30
30
|
requiredChainId?: number;
|
|
31
|
-
/**
|
|
32
|
-
* 연결이 정상적으로 끝나고(체인 검증까지 통과) "사용 가능한 상태"가 됐을 때 한 번 호출.
|
|
33
|
-
* 재연결(reconnect)에서는 호출되지 않음 — 그쪽은 dApp의 watchAccount 책임.
|
|
34
|
-
*/
|
|
35
31
|
onConnect?: (result: ConnectResult) => void;
|
|
36
32
|
onLog?: (message: string, error?: Error) => void;
|
|
37
|
-
/**
|
|
38
|
-
* 연결 실패(거절/미설치/기타) 시 모달 안에 표시할 fallback UI를 dApp이 직접 그리고 싶을 때.
|
|
39
|
-
* 미제공 시 기본 동작은 selector로 복귀.
|
|
40
|
-
*/
|
|
41
33
|
renderConnectError?: (ctx: ConnectErrorContext) => ReactNode;
|
|
42
|
-
/**
|
|
43
|
-
* 기존 프로젝트 통합용. wagmi config를 넘기면 ConnectKitProvider 내부에서
|
|
44
|
-
* WagmiProvider를 직접 세운다.
|
|
45
|
-
*
|
|
46
|
-
* - 기존 프로젝트: 앱에 WagmiProvider가 없으므로 wagmiConfig를 넘긴다.
|
|
47
|
-
* ConnectKitProvider가 같은 config 인스턴스로 WagmiProvider를 셀프 마운트하므로
|
|
48
|
-
* 기존 wagmi 상태 store와 단일 config를 공유한다.
|
|
49
|
-
* - 신규 프로젝트: 앱이 이미 WagmiProvider를 감싸므로 이 prop은 불필요하다.
|
|
50
|
-
*/
|
|
51
|
-
wagmiConfig?: Config;
|
|
52
34
|
}
|
|
53
35
|
|
|
54
36
|
export interface ConnectKitContextValue {
|
|
@@ -74,22 +56,30 @@ const resolveConnectorId = (wagmiConnectorId: string | undefined): ConnectorId |
|
|
|
74
56
|
};
|
|
75
57
|
|
|
76
58
|
export const ConnectKitProvider: FC<ConnectKitProviderProps> = (props) => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
59
|
+
const wagmiConfig = useMemo(
|
|
60
|
+
() => (props.kitConfig ? initConfig(props.kitConfig) : getConfig()),
|
|
61
|
+
[props.kitConfig],
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const resolvedKitConfig = props.kitConfig ?? getKitConfig();
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<WagmiProvider config={wagmiConfig} reconnectOnMount={false}>
|
|
68
|
+
<QueryClientProvider client={internalQueryClient}>
|
|
69
|
+
<ConnectKitProviderInner {...props} kitConfig={resolvedKitConfig} />
|
|
70
|
+
</QueryClientProvider>
|
|
71
|
+
</WagmiProvider>
|
|
72
|
+
);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
type ConnectKitProviderInnerProps = Omit<ConnectKitProviderProps, 'kitConfig'> & {
|
|
76
|
+
kitConfig: ConnectKitConfig;
|
|
87
77
|
};
|
|
88
78
|
|
|
89
|
-
const ConnectKitProviderInner: FC<
|
|
79
|
+
const ConnectKitProviderInner: FC<ConnectKitProviderInnerProps> = ({
|
|
90
80
|
children,
|
|
91
81
|
connectors,
|
|
92
|
-
|
|
82
|
+
kitConfig,
|
|
93
83
|
requiredChainId,
|
|
94
84
|
onConnect,
|
|
95
85
|
onLog,
|
|
@@ -100,21 +90,62 @@ const ConnectKitProviderInner: FC<ConnectKitProviderProps> = ({
|
|
|
100
90
|
|
|
101
91
|
const account = useAccount();
|
|
102
92
|
const { disconnectAsync } = useDisconnect();
|
|
93
|
+
const { switchChainAsync } = useSwitchChain();
|
|
103
94
|
|
|
104
|
-
// dApp의 onConnect는 ref로 잡아 effect 재실행 노이즈 차단
|
|
105
95
|
const onConnectRef = useRef(onConnect);
|
|
106
96
|
useEffect(() => {
|
|
107
97
|
onConnectRef.current = onConnect;
|
|
108
98
|
}, [onConnect]);
|
|
109
99
|
|
|
110
|
-
// chain mismatch invariant: 연결돼 있지만 요구 체인이 아니면 modal force-open
|
|
111
100
|
const chainMismatch =
|
|
112
101
|
requiredChainId != null &&
|
|
113
102
|
account.status === 'connected' &&
|
|
114
103
|
account.chainId != null &&
|
|
115
104
|
account.chainId !== requiredChainId;
|
|
116
105
|
|
|
117
|
-
const
|
|
106
|
+
const [autoSwitchFailedKey, setAutoSwitchFailedKey] = useState<string | null>(null);
|
|
107
|
+
const autoSwitchInFlightRef = useRef(false);
|
|
108
|
+
const isMountedRef = useRef(true);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
isMountedRef.current = true;
|
|
111
|
+
return () => {
|
|
112
|
+
isMountedRef.current = false;
|
|
113
|
+
};
|
|
114
|
+
}, []);
|
|
115
|
+
|
|
116
|
+
const mismatchKey =
|
|
117
|
+
chainMismatch && account.chainId != null && requiredChainId != null
|
|
118
|
+
? `${account.chainId}->${requiredChainId}`
|
|
119
|
+
: null;
|
|
120
|
+
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!chainMismatch || requiredChainId == null) return;
|
|
123
|
+
if (autoSwitchInFlightRef.current) return;
|
|
124
|
+
if (mismatchKey != null && mismatchKey === autoSwitchFailedKey) return;
|
|
125
|
+
|
|
126
|
+
autoSwitchInFlightRef.current = true;
|
|
127
|
+
switchChainAsync({ chainId: requiredChainId })
|
|
128
|
+
.catch((err: unknown) => {
|
|
129
|
+
if (!isMountedRef.current) return;
|
|
130
|
+
const normalized = err instanceof Error ? err : new Error(String(err));
|
|
131
|
+
onLog?.(`[connect-kit] silent switch chain failed: ${normalized.message}`, normalized);
|
|
132
|
+
if (mismatchKey != null) {
|
|
133
|
+
setAutoSwitchFailedKey(mismatchKey);
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
.finally(() => {
|
|
137
|
+
autoSwitchInFlightRef.current = false;
|
|
138
|
+
});
|
|
139
|
+
}, [chainMismatch, mismatchKey, requiredChainId, autoSwitchFailedKey, switchChainAsync, onLog]);
|
|
140
|
+
|
|
141
|
+
useEffect(() => {
|
|
142
|
+
if (!chainMismatch && autoSwitchFailedKey != null) {
|
|
143
|
+
setAutoSwitchFailedKey(null);
|
|
144
|
+
}
|
|
145
|
+
}, [chainMismatch, autoSwitchFailedKey]);
|
|
146
|
+
|
|
147
|
+
const autoSwitchFailedForCurrent = mismatchKey != null && mismatchKey === autoSwitchFailedKey;
|
|
148
|
+
const effectiveOpen = isOpen || (chainMismatch && autoSwitchFailedForCurrent);
|
|
118
149
|
|
|
119
150
|
const open = useCallback((): void => {
|
|
120
151
|
setIsOpen(true);
|
|
@@ -128,22 +159,17 @@ const ConnectKitProviderInner: FC<ConnectKitProviderProps> = ({
|
|
|
128
159
|
await disconnectAsync();
|
|
129
160
|
}, [disconnectAsync]);
|
|
130
161
|
|
|
131
|
-
// ConnectModal → Provider: 연결 결과를 일단 보류했다가
|
|
132
|
-
// chain 검증까지 통과한 후에만 dApp으로 forward
|
|
133
162
|
const handleModalConnect = useCallback((result: ConnectResult): void => {
|
|
134
163
|
setPendingResult(result);
|
|
135
164
|
}, []);
|
|
136
165
|
|
|
137
|
-
// pendingResult가 있고, 연결 + chain 매칭이 충족되면 dApp으로 발화
|
|
138
166
|
useEffect(() => {
|
|
139
167
|
if (!pendingResult) return;
|
|
140
168
|
if (account.status !== 'connected') {
|
|
141
|
-
// 연결이 끊겼거나 진행 중 — pending 폐기
|
|
142
169
|
setPendingResult(null);
|
|
143
170
|
return;
|
|
144
171
|
}
|
|
145
172
|
if (requiredChainId != null && account.chainId !== requiredChainId) {
|
|
146
|
-
// chain 아직 안 맞음 — switch 후 다시 평가
|
|
147
173
|
return;
|
|
148
174
|
}
|
|
149
175
|
onConnectRef.current?.(pendingResult);
|
|
@@ -178,7 +204,7 @@ const ConnectKitProviderInner: FC<ConnectKitProviderProps> = ({
|
|
|
178
204
|
<ConnectModal
|
|
179
205
|
open={effectiveOpen}
|
|
180
206
|
connectors={connectors}
|
|
181
|
-
wcProjectId={wcProjectId}
|
|
207
|
+
wcProjectId={kitConfig.wcProjectId}
|
|
182
208
|
requiredChainId={requiredChainId}
|
|
183
209
|
renderConnectError={renderConnectError}
|
|
184
210
|
onConnect={handleModalConnect}
|
package/src/ConnectModal.tsx
CHANGED
|
@@ -21,28 +21,60 @@ import { WalletConnectView } from './views/WalletConnectView';
|
|
|
21
21
|
import { SwitchChainView } from './views/SwitchChainView';
|
|
22
22
|
|
|
23
23
|
const RECENT_KEY = 'connect-kit:recent';
|
|
24
|
+
const VALID_CONNECTOR_IDS: ReadonlySet<ConnectorId> = new Set<ConnectorId>([
|
|
25
|
+
'CREDIT_CONNECT',
|
|
26
|
+
'CREDIT_WALLET',
|
|
27
|
+
'METAMASK',
|
|
28
|
+
'WALLET_CONNECT',
|
|
29
|
+
]);
|
|
30
|
+
|
|
24
31
|
const readRecentConnector = (): ConnectorId | null => {
|
|
25
32
|
try {
|
|
26
|
-
|
|
33
|
+
const raw = localStorage.getItem(RECENT_KEY);
|
|
34
|
+
if (raw == null) return null;
|
|
35
|
+
return VALID_CONNECTOR_IDS.has(raw as ConnectorId) ? (raw as ConnectorId) : null;
|
|
27
36
|
} catch {
|
|
28
37
|
return null;
|
|
29
38
|
}
|
|
30
39
|
};
|
|
31
40
|
|
|
32
|
-
const writeRecentConnector = (
|
|
41
|
+
const writeRecentConnector = (
|
|
42
|
+
id: ConnectorId,
|
|
43
|
+
onLog?: (message: string, error?: Error) => void,
|
|
44
|
+
): void => {
|
|
33
45
|
try {
|
|
34
46
|
localStorage.setItem(RECENT_KEY, id);
|
|
35
|
-
} catch {
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const normalized = err instanceof Error ? err : new Error(String(err));
|
|
49
|
+
onLog?.(`[connect-kit] writeRecentConnector failed: ${normalized.message}`, normalized);
|
|
50
|
+
}
|
|
36
51
|
};
|
|
37
52
|
|
|
53
|
+
const REJECTED_ERROR_CODES = new Set<unknown>([
|
|
54
|
+
4001, // EIP-1193 user rejected
|
|
55
|
+
'ACTION_REJECTED', // ethers v6
|
|
56
|
+
]);
|
|
57
|
+
|
|
38
58
|
const classifyError = (error: Error): ConnectErrorReason => {
|
|
39
59
|
const code =
|
|
40
60
|
typeof error === 'object' && error !== null && 'code' in error
|
|
41
61
|
? (error as unknown as { code: unknown }).code
|
|
42
62
|
: undefined;
|
|
43
|
-
if (code
|
|
63
|
+
if (code != null && REJECTED_ERROR_CODES.has(code)) return 'rejected';
|
|
44
64
|
const message = error.message?.toLowerCase() ?? '';
|
|
45
|
-
if (
|
|
65
|
+
if (
|
|
66
|
+
message.includes('user rejected') ||
|
|
67
|
+
message.includes('user denied') ||
|
|
68
|
+
message.includes('user cancel') ||
|
|
69
|
+
message.includes('rejected by user') ||
|
|
70
|
+
message.includes('action_rejected') ||
|
|
71
|
+
message.includes('사용자가 거부') ||
|
|
72
|
+
message.includes('사용자가 취소') ||
|
|
73
|
+
message.includes('ユーザーが拒否') ||
|
|
74
|
+
message.includes('ユーザーがキャンセル')
|
|
75
|
+
) {
|
|
76
|
+
return 'rejected';
|
|
77
|
+
}
|
|
46
78
|
if (message.includes('unsupported') || message.includes('not installed')) return 'unsupported';
|
|
47
79
|
return 'unknown';
|
|
48
80
|
};
|
|
@@ -270,8 +302,6 @@ const resolveConnectorsFromConfig = (connectorsConfig?: Connectors): ConnectorId
|
|
|
270
302
|
};
|
|
271
303
|
|
|
272
304
|
export const ConnectModal: FC<ConnectModalProps> = (props) => {
|
|
273
|
-
// open=false면 내부 컴포넌트를 마운트하지 않음
|
|
274
|
-
// → useConfig 등 wagmi hooks가 실행되지 않아 WagmiProvider 없이도 렌더링 가능
|
|
275
305
|
if (!props.open) return null;
|
|
276
306
|
|
|
277
307
|
const resolvedConnectors = resolveConnectorsFromConfig(props.connectors);
|
|
@@ -312,7 +342,6 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
312
342
|
|
|
313
343
|
const wc = useWCState();
|
|
314
344
|
|
|
315
|
-
// wagmi 상태 — chain mismatch invariant 판정용
|
|
316
345
|
const account = useAccount();
|
|
317
346
|
const { disconnectAsync } = useDisconnect();
|
|
318
347
|
const wagmiConfig = useConfig();
|
|
@@ -328,24 +357,19 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
328
357
|
return wagmiConfig.chains.find((chain) => chain.id === requiredChainId)?.name;
|
|
329
358
|
}, [wagmiConfig.chains, requiredChainId]);
|
|
330
359
|
|
|
331
|
-
// QR URI 수신
|
|
332
360
|
const handleQrUri = useCallback((uri: string | null) => {
|
|
333
361
|
setQrUri(uri);
|
|
334
362
|
}, []);
|
|
335
363
|
|
|
336
|
-
// 연결 완료
|
|
337
364
|
const handleConnect = useCallback(
|
|
338
365
|
(result: Parameters<typeof onConnect>[0]) => {
|
|
339
366
|
setErrorState(null);
|
|
340
367
|
onConnect(result);
|
|
341
|
-
// chain mismatch가 있으면 모달은 SwitchChainView로 자동 전환됨 (닫지 않음)
|
|
342
|
-
// Provider 측에서 requiredChainId가 매칭될 때까지 force-open 유지
|
|
343
368
|
onClose();
|
|
344
369
|
},
|
|
345
370
|
[onConnect, onClose],
|
|
346
371
|
);
|
|
347
372
|
|
|
348
|
-
// 연결 실패
|
|
349
373
|
const handleError = useCallback(
|
|
350
374
|
(error: unknown, connectorId: ConnectorId): void => {
|
|
351
375
|
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
@@ -364,14 +388,12 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
364
388
|
[onLog, wc.resetView, renderConnectError],
|
|
365
389
|
);
|
|
366
390
|
|
|
367
|
-
// 연결 시도
|
|
368
391
|
const { triggerConnect, cancelConnect } = useWagmiConnect({
|
|
369
392
|
onConnect: handleConnect,
|
|
370
393
|
onError: handleError,
|
|
371
394
|
onQrUri: handleQrUri,
|
|
372
395
|
});
|
|
373
396
|
|
|
374
|
-
// wagmi connector icon 매핑
|
|
375
397
|
const connectorLogoMap = useMemo(() => {
|
|
376
398
|
const map: Partial<Record<ConnectorId, string>> = {};
|
|
377
399
|
const connectorIds: ConnectorId[] = [
|
|
@@ -387,13 +409,11 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
387
409
|
return map;
|
|
388
410
|
}, [wagmiConfig.connectors]);
|
|
389
411
|
|
|
390
|
-
// 최근 연결 기록 로드
|
|
391
412
|
useEffect(() => {
|
|
392
413
|
setRecentConnector(readRecentConnector());
|
|
393
414
|
setHasMetaMaskExtension(detectMetaMaskExtension());
|
|
394
415
|
}, []);
|
|
395
416
|
|
|
396
|
-
// 모달 닫힐 때 상태 초기화
|
|
397
417
|
useEffect(() => {
|
|
398
418
|
if (!open) {
|
|
399
419
|
setSelectedConnector(null);
|
|
@@ -404,7 +424,6 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
404
424
|
}
|
|
405
425
|
}, [open, cancelConnect, wc.reset]);
|
|
406
426
|
|
|
407
|
-
// 커넥터 선택 — 선택할 때마다 QR을 초기화해 새 세션 URI가 표시되도록 함
|
|
408
427
|
const handleSelectConnector = useCallback(
|
|
409
428
|
(id: ConnectorId): void => {
|
|
410
429
|
if (id === selectedConnector && !errorState) return;
|
|
@@ -412,7 +431,7 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
412
431
|
setQrUri(null);
|
|
413
432
|
setErrorState(null);
|
|
414
433
|
wc.resetView();
|
|
415
|
-
writeRecentConnector(id);
|
|
434
|
+
writeRecentConnector(id, onLog);
|
|
416
435
|
setRecentConnector(id);
|
|
417
436
|
|
|
418
437
|
if (id === 'WALLET_CONNECT' && wcProjectId) {
|
|
@@ -430,10 +449,10 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
430
449
|
wcProjectId,
|
|
431
450
|
wc.resetView,
|
|
432
451
|
wc.loadWalletList,
|
|
452
|
+
onLog,
|
|
433
453
|
],
|
|
434
454
|
);
|
|
435
455
|
|
|
436
|
-
// 뒤로가기
|
|
437
456
|
const handleBack = useCallback((): void => {
|
|
438
457
|
if (errorState) {
|
|
439
458
|
setErrorState(null);
|
|
@@ -446,7 +465,6 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
446
465
|
cancelConnect();
|
|
447
466
|
}, [selectedConnector, errorState, wc.handleBack, cancelConnect]);
|
|
448
467
|
|
|
449
|
-
// chain mismatch 상태에서의 disconnect (Switch chain pane의 cancel)
|
|
450
468
|
const handleSwitchChainDisconnect = useCallback((): void => {
|
|
451
469
|
disconnectAsync().catch((err: unknown) => {
|
|
452
470
|
const normalized = err instanceof Error ? err : new Error(String(err));
|
|
@@ -454,13 +472,11 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
454
472
|
});
|
|
455
473
|
}, [disconnectAsync, onLog]);
|
|
456
474
|
|
|
457
|
-
// overlay 클릭 — chain mismatch 상태에선 무시 (실수 클릭 방어)
|
|
458
475
|
const handleOverlayClick = useCallback((): void => {
|
|
459
476
|
if (chainMismatch) return;
|
|
460
477
|
onClose();
|
|
461
478
|
}, [chainMismatch, onClose]);
|
|
462
479
|
|
|
463
|
-
// close 버튼 — chain mismatch 상태에선 disconnect
|
|
464
480
|
const handleCloseClick = useCallback((): void => {
|
|
465
481
|
if (chainMismatch) {
|
|
466
482
|
handleSwitchChainDisconnect();
|
|
@@ -490,8 +506,6 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
490
506
|
return renderConnectError(ctx);
|
|
491
507
|
}, [errorState, renderConnectError, triggerConnect, cancelConnect]);
|
|
492
508
|
|
|
493
|
-
// chain mismatch면 단일 pane (selector/detail 숨김)
|
|
494
|
-
// requiredChainId != null 재확인은 TS narrowing용 — chainMismatch true면 이미 보장됨
|
|
495
509
|
if (chainMismatch && requiredChainId != null) {
|
|
496
510
|
return (
|
|
497
511
|
<div className="ck-root">
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAccount as wagmiGetAccount,
|
|
3
|
+
signMessage as wagmiSignMessage,
|
|
4
|
+
signTypedData as wagmiSignTypedData,
|
|
5
|
+
type GetAccountReturnType,
|
|
6
|
+
type SignMessageParameters,
|
|
7
|
+
type SignTypedDataParameters,
|
|
8
|
+
} from '@wagmi/core';
|
|
9
|
+
import { getConfig } from '../core/config';
|
|
10
|
+
|
|
11
|
+
export const getAccount = (): GetAccountReturnType => {
|
|
12
|
+
return wagmiGetAccount(getConfig());
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const signMessage = (params: SignMessageParameters): Promise<`0x${string}`> => {
|
|
16
|
+
return wagmiSignMessage(getConfig(), params);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const signTypedData = (params: SignTypedDataParameters): Promise<`0x${string}`> => {
|
|
20
|
+
return wagmiSignTypedData(getConfig(), params);
|
|
21
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getBalance as wagmiGetBalance,
|
|
3
|
+
type GetBalanceParameters,
|
|
4
|
+
type GetBalanceReturnType,
|
|
5
|
+
} from '@wagmi/core';
|
|
6
|
+
import { getConfig } from '../core/config';
|
|
7
|
+
|
|
8
|
+
export const getBalance = (params: GetBalanceParameters): Promise<GetBalanceReturnType> => {
|
|
9
|
+
return wagmiGetBalance(getConfig(), params);
|
|
10
|
+
};
|
package/src/api/chain.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
switchChain as wagmiSwitchChain,
|
|
3
|
+
getPublicClient as wagmiGetPublicClient,
|
|
4
|
+
type SwitchChainParameters,
|
|
5
|
+
type GetPublicClientParameters,
|
|
6
|
+
type GetPublicClientReturnType,
|
|
7
|
+
} from '@wagmi/core';
|
|
8
|
+
import type { Chain } from 'viem';
|
|
9
|
+
import { getConfig } from '../core/config';
|
|
10
|
+
|
|
11
|
+
export const switchChain = (params: SwitchChainParameters): Promise<Chain> => {
|
|
12
|
+
return wagmiSwitchChain(getConfig(), params);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const getPublicClient = (params?: GetPublicClientParameters): GetPublicClientReturnType => {
|
|
16
|
+
return wagmiGetPublicClient(getConfig(), params);
|
|
17
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readContract as wagmiReadContract,
|
|
3
|
+
writeContract as wagmiWriteContract,
|
|
4
|
+
type ReadContractParameters,
|
|
5
|
+
type ReadContractReturnType,
|
|
6
|
+
type WriteContractParameters,
|
|
7
|
+
type WriteContractReturnType,
|
|
8
|
+
type Config,
|
|
9
|
+
} from '@wagmi/core';
|
|
10
|
+
import type { Abi, ContractFunctionArgs, ContractFunctionName } from 'viem';
|
|
11
|
+
import { getConfig } from '../core/config';
|
|
12
|
+
|
|
13
|
+
export function readContract<
|
|
14
|
+
const abi extends Abi | readonly unknown[],
|
|
15
|
+
functionName extends ContractFunctionName<abi, 'pure' | 'view'>,
|
|
16
|
+
args extends ContractFunctionArgs<abi, 'pure' | 'view', functionName>,
|
|
17
|
+
>(
|
|
18
|
+
parameters: ReadContractParameters<abi, functionName, args, Config>,
|
|
19
|
+
): Promise<ReadContractReturnType<abi, functionName, args>> {
|
|
20
|
+
return wagmiReadContract(getConfig(), parameters as any) as Promise<
|
|
21
|
+
ReadContractReturnType<abi, functionName, args>
|
|
22
|
+
>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function writeContract<
|
|
26
|
+
const abi extends Abi | readonly unknown[],
|
|
27
|
+
functionName extends ContractFunctionName<abi, 'nonpayable' | 'payable'>,
|
|
28
|
+
args extends ContractFunctionArgs<abi, 'nonpayable' | 'payable', functionName>,
|
|
29
|
+
>(
|
|
30
|
+
parameters: WriteContractParameters<abi, functionName, args, Config>,
|
|
31
|
+
): Promise<WriteContractReturnType> {
|
|
32
|
+
return wagmiWriteContract(getConfig(), parameters as any);
|
|
33
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
waitForTransactionReceipt as wagmiWaitForTransactionReceipt,
|
|
3
|
+
type WaitForTransactionReceiptParameters,
|
|
4
|
+
type WaitForTransactionReceiptReturnType,
|
|
5
|
+
} from '@wagmi/core';
|
|
6
|
+
import { getConfig } from '../core/config';
|
|
7
|
+
|
|
8
|
+
export const waitForTransactionReceipt = (
|
|
9
|
+
params: WaitForTransactionReceiptParameters,
|
|
10
|
+
): Promise<WaitForTransactionReceiptReturnType> => {
|
|
11
|
+
return wagmiWaitForTransactionReceipt(getConfig(), params);
|
|
12
|
+
};
|
|
@@ -14,7 +14,7 @@ export const QRFrame: FC<QRFrameProps> = ({ qrUri, logoUrl }) => (
|
|
|
14
14
|
size={196}
|
|
15
15
|
level="H"
|
|
16
16
|
imageSettings={
|
|
17
|
-
logoUrl ? { src: logoUrl, width:
|
|
17
|
+
logoUrl ? { src: logoUrl, width: 50, height: 50, excavate: true } : undefined
|
|
18
18
|
}
|
|
19
19
|
/>
|
|
20
20
|
) : (
|
|
@@ -34,7 +34,7 @@ export const CopyLinkButton: FC<CopyLinkButtonProps> = ({ qrUri }) => {
|
|
|
34
34
|
type="button"
|
|
35
35
|
className="ck-copy-link"
|
|
36
36
|
onClick={() => {
|
|
37
|
-
navigator.clipboard
|
|
37
|
+
navigator.clipboard?.writeText(qrUri).catch(() => {});
|
|
38
38
|
}}
|
|
39
39
|
>
|
|
40
40
|
Copy link
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createConfig, createStorage } from 'wagmi';
|
|
2
|
+
import { metaMask, walletConnect, injected } from 'wagmi/connectors';
|
|
3
|
+
import type { Config } from 'wagmi';
|
|
4
|
+
import type { ConnectKitConfig } from '../types';
|
|
5
|
+
|
|
6
|
+
type KitSlot = {
|
|
7
|
+
config: Config | null;
|
|
8
|
+
kitConfig: ConnectKitConfig | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const SLOT_KEY = Symbol.for('@gluwa/connect-kit:slot');
|
|
12
|
+
|
|
13
|
+
type GlobalWithSlot = typeof globalThis & {
|
|
14
|
+
[SLOT_KEY]?: KitSlot;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const getSlot = (): KitSlot => {
|
|
18
|
+
const g = globalThis as GlobalWithSlot;
|
|
19
|
+
if (!g[SLOT_KEY]) {
|
|
20
|
+
g[SLOT_KEY] = { config: null, kitConfig: null };
|
|
21
|
+
}
|
|
22
|
+
return g[SLOT_KEY];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const initConfig = (kitConfig: ConnectKitConfig): Config => {
|
|
26
|
+
const slot = getSlot();
|
|
27
|
+
if (slot.config) {
|
|
28
|
+
if (slot.kitConfig !== kitConfig) {
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
30
|
+
console.warn(
|
|
31
|
+
'[connect-kit] initConfig called again with a different kitConfig — new chains/transports ignored. Call resetConfig() first to re-init.',
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return slot.config;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const connectors = [];
|
|
38
|
+
|
|
39
|
+
if (kitConfig.wcProjectId) {
|
|
40
|
+
connectors.push(walletConnect({ projectId: kitConfig.wcProjectId, showQrModal: false }));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
connectors.push(metaMask({ storage: { enabled: true } }), injected({ shimDisconnect: false }));
|
|
44
|
+
|
|
45
|
+
if (kitConfig.extraConnectors?.length) {
|
|
46
|
+
connectors.push(...kitConfig.extraConnectors);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const isBrowser = typeof window !== 'undefined';
|
|
50
|
+
|
|
51
|
+
slot.config = createConfig({
|
|
52
|
+
ssr: true,
|
|
53
|
+
chains: kitConfig.chains,
|
|
54
|
+
connectors,
|
|
55
|
+
transports: kitConfig.transports,
|
|
56
|
+
storage: isBrowser ? createStorage({ storage: window.localStorage }) : undefined,
|
|
57
|
+
});
|
|
58
|
+
slot.kitConfig = kitConfig;
|
|
59
|
+
|
|
60
|
+
return slot.config;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const getConfig = (): Config => {
|
|
64
|
+
const slot = getSlot();
|
|
65
|
+
if (!slot.config) {
|
|
66
|
+
throw new Error('ConnectKit config not initialized. Make sure ConnectKitProvider is mounted.');
|
|
67
|
+
}
|
|
68
|
+
return slot.config;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export const getKitConfig = (): ConnectKitConfig => {
|
|
72
|
+
const slot = getSlot();
|
|
73
|
+
if (!slot.kitConfig) {
|
|
74
|
+
throw new Error('ConnectKit config not initialized. Make sure ConnectKitProvider is mounted.');
|
|
75
|
+
}
|
|
76
|
+
return slot.kitConfig;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const resetConfig = (): void => {
|
|
80
|
+
const slot = getSlot();
|
|
81
|
+
slot.config = null;
|
|
82
|
+
slot.kitConfig = null;
|
|
83
|
+
};
|