@gluwa/connect-kit 0.1.0-next.1 → 0.1.0-next.2
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/index.d.ts +45 -2
- package/dist/index.js +328 -20
- package/dist/package.json +1 -1
- package/package.json +1 -1
- package/src/ConnectKitProvider.tsx +169 -0
- package/src/ConnectModal.scss +38 -0
- package/src/ConnectModal.tsx +182 -41
- package/src/index.ts +4 -0
- package/src/types.ts +14 -0
- package/src/views/SwitchChainView.tsx +67 -0
package/CHANGELOG.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FC } from 'react';
|
|
1
|
+
import { ReactNode, FC } from 'react';
|
|
2
2
|
import { createConnector } from '@wagmi/core';
|
|
3
3
|
import { StorageProvider } from '@gluwa/credit-connect-sdk/storage';
|
|
4
4
|
import { E2EEProvider } from '@gluwa/credit-connect-sdk/e2ee';
|
|
@@ -28,6 +28,14 @@ interface ConnectResult {
|
|
|
28
28
|
address: `0x${string}`;
|
|
29
29
|
connectorId: ConnectorId;
|
|
30
30
|
}
|
|
31
|
+
type ConnectErrorReason = 'rejected' | 'unsupported' | 'unknown';
|
|
32
|
+
interface ConnectErrorContext {
|
|
33
|
+
connectorId: ConnectorId;
|
|
34
|
+
reason: ConnectErrorReason;
|
|
35
|
+
error: Error;
|
|
36
|
+
retry: () => void;
|
|
37
|
+
dismiss: () => void;
|
|
38
|
+
}
|
|
31
39
|
interface ConnectModalProps {
|
|
32
40
|
open: boolean;
|
|
33
41
|
connectors: Connectors;
|
|
@@ -35,10 +43,45 @@ interface ConnectModalProps {
|
|
|
35
43
|
onClose: () => void;
|
|
36
44
|
onLog?: (message: string, error?: Error) => void;
|
|
37
45
|
wcProjectId?: string;
|
|
46
|
+
requiredChainId?: number;
|
|
47
|
+
renderConnectError?: (ctx: ConnectErrorContext) => ReactNode;
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
declare const ConnectModal: FC<ConnectModalProps>;
|
|
41
51
|
|
|
52
|
+
interface ConnectKitProviderProps {
|
|
53
|
+
children: ReactNode;
|
|
54
|
+
connectors: Connectors;
|
|
55
|
+
wcProjectId?: string;
|
|
56
|
+
/**
|
|
57
|
+
* 연결돼 있는 동안 강제할 체인 ID. 다른 체인에 연결돼 있으면
|
|
58
|
+
* 자동으로 스위치 모달이 떠서 사용자에게 전환 또는 disconnect를 요구.
|
|
59
|
+
*/
|
|
60
|
+
requiredChainId?: number;
|
|
61
|
+
/**
|
|
62
|
+
* 연결이 정상적으로 끝나고(체인 검증까지 통과) "사용 가능한 상태"가 됐을 때 한 번 호출.
|
|
63
|
+
* 재연결(reconnect)에서는 호출되지 않음 — 그쪽은 dApp의 watchAccount 책임.
|
|
64
|
+
*/
|
|
65
|
+
onConnect?: (result: ConnectResult) => void;
|
|
66
|
+
onLog?: (message: string, error?: Error) => void;
|
|
67
|
+
/**
|
|
68
|
+
* 연결 실패(거절/미설치/기타) 시 모달 안에 표시할 fallback UI를 dApp이 직접 그리고 싶을 때.
|
|
69
|
+
* 미제공 시 기본 동작은 selector로 복귀.
|
|
70
|
+
*/
|
|
71
|
+
renderConnectError?: (ctx: ConnectErrorContext) => ReactNode;
|
|
72
|
+
}
|
|
73
|
+
interface ConnectKitContextValue {
|
|
74
|
+
isOpen: boolean;
|
|
75
|
+
open: () => void;
|
|
76
|
+
close: () => void;
|
|
77
|
+
isConnected: boolean;
|
|
78
|
+
address: `0x${string}` | undefined;
|
|
79
|
+
connectorId: ConnectorId | null;
|
|
80
|
+
disconnect: () => Promise<void>;
|
|
81
|
+
}
|
|
82
|
+
declare const ConnectKitProvider: FC<ConnectKitProviderProps>;
|
|
83
|
+
declare const useConnectKit: () => ConnectKitContextValue;
|
|
84
|
+
|
|
42
85
|
type CreditConnectConnectorOptions = {
|
|
43
86
|
projectKey: string;
|
|
44
87
|
serverUrl: string;
|
|
@@ -49,4 +92,4 @@ type CreditConnectConnectorOptions = {
|
|
|
49
92
|
};
|
|
50
93
|
declare const creditConnectConnector: (options: CreditConnectConnectorOptions) => ReturnType<typeof createConnector>;
|
|
51
94
|
|
|
52
|
-
export { ConnectModal, type ConnectModalProps, type ConnectResult, type ConnectorId, type Connectors, type CreditConnectConnectorOptions, type CreditWalletStrategy, type WCSubView, type WCWallet, creditConnectConnector };
|
|
95
|
+
export { type ConnectErrorContext, type ConnectErrorReason, type ConnectKitContextValue, ConnectKitProvider, type ConnectKitProviderProps, ConnectModal, type ConnectModalProps, type ConnectResult, type ConnectorId, type Connectors, type CreditConnectConnectorOptions, type CreditWalletStrategy, type WCSubView, type WCWallet, creditConnectConnector, useConnectKit };
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ var __defProp = Object.defineProperty;
|
|
|
3
3
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
4
4
|
|
|
5
5
|
// src/ConnectModal.tsx
|
|
6
|
-
import { useState as
|
|
6
|
+
import { useState as useState3, useEffect as useEffect4, useCallback as useCallback3, useRef as useRef3, useMemo as useMemo4 } from "react";
|
|
7
7
|
|
|
8
8
|
// src/ConnectModal.scss
|
|
9
9
|
var css = `.ck-root {
|
|
@@ -141,6 +141,42 @@ var css = `.ck-root {
|
|
|
141
141
|
.ck-btn-primary:hover {
|
|
142
142
|
background: rgba(255, 255, 255, 0.22);
|
|
143
143
|
}
|
|
144
|
+
.ck-btn-primary:disabled {
|
|
145
|
+
opacity: 0.6;
|
|
146
|
+
cursor: not-allowed;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.ck-btn-secondary {
|
|
150
|
+
display: inline-flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
justify-content: center;
|
|
153
|
+
padding: 8px 16px;
|
|
154
|
+
border-radius: 8px;
|
|
155
|
+
font-size: 14px;
|
|
156
|
+
font-weight: 500;
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
white-space: nowrap;
|
|
159
|
+
color: rgba(255, 255, 255, 0.75);
|
|
160
|
+
background: transparent;
|
|
161
|
+
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
162
|
+
}
|
|
163
|
+
.ck-btn-secondary:hover {
|
|
164
|
+
color: #fff;
|
|
165
|
+
border-color: rgba(255, 255, 255, 0.32);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.ck-view__actions {
|
|
169
|
+
display: flex;
|
|
170
|
+
flex-direction: column;
|
|
171
|
+
gap: 8px;
|
|
172
|
+
width: 100%;
|
|
173
|
+
margin-top: 16px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.ck-view__caption--error {
|
|
177
|
+
color: #ff8080;
|
|
178
|
+
margin-top: 8px;
|
|
179
|
+
}
|
|
144
180
|
|
|
145
181
|
.ck-connector-list {
|
|
146
182
|
display: flex;
|
|
@@ -584,7 +620,7 @@ var css = `.ck-root {
|
|
|
584
620
|
document.head.appendChild(document.createElement("style")).appendChild(document.createTextNode(css));
|
|
585
621
|
|
|
586
622
|
// src/ConnectModal.tsx
|
|
587
|
-
import { useConfig as useConfig2 } from "wagmi";
|
|
623
|
+
import { useAccount, useConfig as useConfig2, useDisconnect } from "wagmi";
|
|
588
624
|
|
|
589
625
|
// assets/creditwallet.png
|
|
590
626
|
var creditwallet_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAOdEVYdFNvZnR3YXJlAEZpZ21hnrGWYwAAAtNJREFUeAHVWDFs00AUfWe7oYVSWdAxQEfUCZiQqEQykDUdGBAL0IERlQGxMbAhBpDYGEBMMDCEtUtbqZXYuiHGCDEymDZqqZr4+N/OuW579jmO5aRPcs8+n68v/7//798BYwah65x23Rp8NOltjQa4UmIORfwzgbYE2kJSa+FTx/PWToyJP7iuO9eV+AiJGsqAQMsReOp5XvuwK0amJ7FalDWyc0LbtlBXpCz1gizzpmwyDHJh6JVDgpFmVjFKkJVYUw7fCx8P5GCfY/r9Ezjzl7TvvIVnGBg+FulvSEgKclUGRhM3r0b3cnsX/u8/xnE8ppcw7ggEbnMTWoj8aOJjVWdx/svz6Hnn3iscfP+pHXuhHUkCe2+/0dWCkY+E2+dC5p9xU/lMLjUgZs4e6dv/upFooanlxRN9+x9W4JNV09DZ9kQmQu7G66DNow0mN7XcDL71Da4zEjpzd4GuW2SNTXR//EKPrkHBrrbpqtA8bOXO43dII2SZJnNIoKyVPGQYbBX+noXuzF82jrcwZnB0nXbfMgwWLwxizIKDlS3KLObcotUQm5dD3BSylcYNbfTlBWvIwRCYXLoTWbIIQoyhCOmgUoQJOxRtukAZmaitY65WKNxCuRbWGIYixKK3qpsoEtoo48jhCOLEaFUvYu/lZ+M6ZMK5F/eDsN+luZKQmKm5tFDRwssHEvw9CCYa14MfacLpyNQKvA51aR2y+wtkUv2TBjtw+2zmQu10lR8KZRZoYQnLO8qULdA/mozNHs/C7MpkQs3oPnMJS/uzfhtYaIuaa6kfkIXi9QwXXKwPHZiEAuvGz1bkr3X+enUl6nUTIU4FcVFXKB1YCYTyiJ/3+yEvjNdGMchDwSkEmQyjAh06qJOQKDHSKcQjFjdKBouZT0DUc0SITx9sgTqzRVkgr8RPPsIuDVhTtN9/SFvsK8GuttgDK4/W2HUyRUt3YDV2+A99bkdtt/T4QQAAAABJRU5ErkJggg==";
|
|
@@ -1168,6 +1204,50 @@ var WalletQRView = /* @__PURE__ */ __name(({ wallet, qrUri }) => {
|
|
|
1168
1204
|
}, "Download")));
|
|
1169
1205
|
}, "WalletQRView");
|
|
1170
1206
|
|
|
1207
|
+
// src/views/SwitchChainView.tsx
|
|
1208
|
+
import { useState as useState2 } from "react";
|
|
1209
|
+
import { useSwitchChain } from "wagmi";
|
|
1210
|
+
var SwitchChainView = /* @__PURE__ */ __name(({ currentChainId, requiredChainId, requiredChainName, onDisconnect, onLog }) => {
|
|
1211
|
+
const { switchChainAsync, isPending } = useSwitchChain();
|
|
1212
|
+
const [error, setError] = useState2(null);
|
|
1213
|
+
const targetLabel = requiredChainName != null ? requiredChainName : `chain ${requiredChainId}`;
|
|
1214
|
+
const handleSwitch = /* @__PURE__ */ __name(() => {
|
|
1215
|
+
setError(null);
|
|
1216
|
+
switchChainAsync({
|
|
1217
|
+
chainId: requiredChainId
|
|
1218
|
+
}).catch((err) => {
|
|
1219
|
+
const normalized = err instanceof Error ? err : new Error(String(err));
|
|
1220
|
+
const code = typeof err === "object" && err !== null && "code" in err ? err.code : void 0;
|
|
1221
|
+
if (code === 4902) {
|
|
1222
|
+
onLog == null ? void 0 : onLog("[connect-kit] chain not registered in wallet (4902)", normalized);
|
|
1223
|
+
} else {
|
|
1224
|
+
onLog == null ? void 0 : onLog(`[connect-kit] switch chain failed: ${normalized.message}`, normalized);
|
|
1225
|
+
}
|
|
1226
|
+
setError(normalized);
|
|
1227
|
+
});
|
|
1228
|
+
}, "handleSwitch");
|
|
1229
|
+
return /* @__PURE__ */ React.createElement("div", {
|
|
1230
|
+
className: "ck-view ck-view--switch-chain"
|
|
1231
|
+
}, /* @__PURE__ */ React.createElement("h4", {
|
|
1232
|
+
className: "ck-view__title"
|
|
1233
|
+
}, "Wrong network"), /* @__PURE__ */ React.createElement("p", {
|
|
1234
|
+
className: "ck-view__description"
|
|
1235
|
+
}, currentChainId != null ? `Connected to chain ${currentChainId}. Please switch to ${targetLabel} to continue.` : `Please switch to ${targetLabel} to continue.`), /* @__PURE__ */ React.createElement("div", {
|
|
1236
|
+
className: "ck-view__actions"
|
|
1237
|
+
}, /* @__PURE__ */ React.createElement("button", {
|
|
1238
|
+
type: "button",
|
|
1239
|
+
className: "ck-btn-primary",
|
|
1240
|
+
onClick: handleSwitch,
|
|
1241
|
+
disabled: isPending
|
|
1242
|
+
}, isPending ? "Waiting for wallet\u2026" : `Switch to ${targetLabel}`), /* @__PURE__ */ React.createElement("button", {
|
|
1243
|
+
type: "button",
|
|
1244
|
+
className: "ck-btn-secondary",
|
|
1245
|
+
onClick: onDisconnect
|
|
1246
|
+
}, "Disconnect")), error && /* @__PURE__ */ React.createElement("p", {
|
|
1247
|
+
className: "ck-view__caption ck-view__caption--error"
|
|
1248
|
+
}, error.message));
|
|
1249
|
+
}, "SwitchChainView");
|
|
1250
|
+
|
|
1171
1251
|
// src/ConnectModal.tsx
|
|
1172
1252
|
var RECENT_KEY = "connect-kit:recent";
|
|
1173
1253
|
var readRecentConnector = /* @__PURE__ */ __name(() => {
|
|
@@ -1183,6 +1263,15 @@ var writeRecentConnector = /* @__PURE__ */ __name((id) => {
|
|
|
1183
1263
|
} catch {
|
|
1184
1264
|
}
|
|
1185
1265
|
}, "writeRecentConnector");
|
|
1266
|
+
var classifyError = /* @__PURE__ */ __name((error) => {
|
|
1267
|
+
var _a, _b;
|
|
1268
|
+
const code = typeof error === "object" && error !== null && "code" in error ? error.code : void 0;
|
|
1269
|
+
if (code === 4001) return "rejected";
|
|
1270
|
+
const message = (_b = (_a = error.message) == null ? void 0 : _a.toLowerCase()) != null ? _b : "";
|
|
1271
|
+
if (message.includes("user rejected") || message.includes("user denied")) return "rejected";
|
|
1272
|
+
if (message.includes("unsupported") || message.includes("not installed")) return "unsupported";
|
|
1273
|
+
return "unknown";
|
|
1274
|
+
}, "classifyError");
|
|
1186
1275
|
var SelectorPane = /* @__PURE__ */ __name(({ connectors, selected, recentConnector, onSelect, onClose }) => {
|
|
1187
1276
|
const hasRecent = recentConnector !== null && connectors.includes(recentConnector);
|
|
1188
1277
|
const recentConnectors = hasRecent ? [
|
|
@@ -1248,7 +1337,7 @@ var ConnectorItem = /* @__PURE__ */ __name(({ id, isSelected, isRecent, onSelect
|
|
|
1248
1337
|
className: "ck-connector-item__badge"
|
|
1249
1338
|
}, "Recent"));
|
|
1250
1339
|
}, "ConnectorItem");
|
|
1251
|
-
var DetailPane = /* @__PURE__ */ __name(({ selectedConnector, qrUri, hasMetaMaskExtension, connectorLogoMap, wc, onBack, onClose }) => {
|
|
1340
|
+
var DetailPane = /* @__PURE__ */ __name(({ selectedConnector, qrUri, hasMetaMaskExtension, connectorLogoMap, wc, errorOverride, onBack, onClose }) => {
|
|
1252
1341
|
const title = getDetailTitle(selectedConnector, hasMetaMaskExtension, wc.subView, wc.selectedWallet);
|
|
1253
1342
|
const showBack = selectedConnector !== null;
|
|
1254
1343
|
return /* @__PURE__ */ React.createElement("div", {
|
|
@@ -1269,7 +1358,7 @@ var DetailPane = /* @__PURE__ */ __name(({ selectedConnector, qrUri, hasMetaMask
|
|
|
1269
1358
|
"aria-label": "Close"
|
|
1270
1359
|
})), /* @__PURE__ */ React.createElement("div", {
|
|
1271
1360
|
className: "ck-pane__body"
|
|
1272
|
-
}, !selectedConnector && /* @__PURE__ */ React.createElement(IdleView, null), (selectedConnector === "CREDIT_CONNECT" || selectedConnector === "CREDIT_WALLET") && /* @__PURE__ */ React.createElement(CreditWalletView, {
|
|
1361
|
+
}, errorOverride || /* @__PURE__ */ React.createElement(React.Fragment, null, !selectedConnector && /* @__PURE__ */ React.createElement(IdleView, null), (selectedConnector === "CREDIT_CONNECT" || selectedConnector === "CREDIT_WALLET") && /* @__PURE__ */ React.createElement(CreditWalletView, {
|
|
1273
1362
|
connectorId: selectedConnector,
|
|
1274
1363
|
qrUri
|
|
1275
1364
|
}), selectedConnector === "METAMASK" && /* @__PURE__ */ React.createElement(MetaMaskView, {
|
|
@@ -1289,7 +1378,7 @@ var DetailPane = /* @__PURE__ */ __name(({ selectedConnector, qrUri, hasMetaMask
|
|
|
1289
1378
|
onSelectWallet: wc.onSelectWallet,
|
|
1290
1379
|
onSearchChange: wc.onSearchChange,
|
|
1291
1380
|
onFilterToggle: wc.onFilterToggle
|
|
1292
|
-
})));
|
|
1381
|
+
}))));
|
|
1293
1382
|
}, "DetailPane");
|
|
1294
1383
|
var getDetailTitle = /* @__PURE__ */ __name((connector, hasMetaMaskExtension, wcSubView, wcSelectedWallet) => {
|
|
1295
1384
|
if (!connector) return "";
|
|
@@ -1324,14 +1413,27 @@ var ConnectModal = /* @__PURE__ */ __name((props) => {
|
|
|
1324
1413
|
connectors: resolvedConnectors
|
|
1325
1414
|
});
|
|
1326
1415
|
}, "ConnectModal");
|
|
1327
|
-
var ConnectModalInner = /* @__PURE__ */ __name(({ open, connectors, onConnect, onClose, onLog, wcProjectId }) => {
|
|
1416
|
+
var ConnectModalInner = /* @__PURE__ */ __name(({ open, connectors, onConnect, onClose, onLog, wcProjectId, requiredChainId, renderConnectError }) => {
|
|
1328
1417
|
var _a;
|
|
1329
|
-
const [selectedConnector, setSelectedConnector] =
|
|
1330
|
-
const [qrUriMap, setQrUriMap] =
|
|
1418
|
+
const [selectedConnector, setSelectedConnector] = useState3(null);
|
|
1419
|
+
const [qrUriMap, setQrUriMap] = useState3({});
|
|
1331
1420
|
const pendingQrConnectorRef = useRef3(null);
|
|
1332
|
-
const [recentConnector, setRecentConnector] =
|
|
1333
|
-
const [hasMetaMaskExtension, setHasMetaMaskExtension] =
|
|
1421
|
+
const [recentConnector, setRecentConnector] = useState3(null);
|
|
1422
|
+
const [hasMetaMaskExtension, setHasMetaMaskExtension] = useState3(false);
|
|
1423
|
+
const [errorState, setErrorState] = useState3(null);
|
|
1334
1424
|
const wc = useWCState();
|
|
1425
|
+
const account = useAccount();
|
|
1426
|
+
const { disconnectAsync } = useDisconnect();
|
|
1427
|
+
const wagmiConfig = useConfig2();
|
|
1428
|
+
const chainMismatch = requiredChainId != null && account.status === "connected" && account.chainId != null && account.chainId !== requiredChainId;
|
|
1429
|
+
const requiredChainName = useMemo4(() => {
|
|
1430
|
+
var _a2;
|
|
1431
|
+
if (requiredChainId == null) return void 0;
|
|
1432
|
+
return (_a2 = wagmiConfig.chains.find((chain) => chain.id === requiredChainId)) == null ? void 0 : _a2.name;
|
|
1433
|
+
}, [
|
|
1434
|
+
wagmiConfig.chains,
|
|
1435
|
+
requiredChainId
|
|
1436
|
+
]);
|
|
1335
1437
|
const handleQrUri = useCallback3((uri) => {
|
|
1336
1438
|
if (uri) {
|
|
1337
1439
|
const id = pendingQrConnectorRef.current;
|
|
@@ -1342,6 +1444,7 @@ var ConnectModalInner = /* @__PURE__ */ __name(({ open, connectors, onConnect, o
|
|
|
1342
1444
|
}
|
|
1343
1445
|
}, []);
|
|
1344
1446
|
const handleConnect = useCallback3((result) => {
|
|
1447
|
+
setErrorState(null);
|
|
1345
1448
|
onConnect(result);
|
|
1346
1449
|
onClose();
|
|
1347
1450
|
}, [
|
|
@@ -1349,8 +1452,8 @@ var ConnectModalInner = /* @__PURE__ */ __name(({ open, connectors, onConnect, o
|
|
|
1349
1452
|
onClose
|
|
1350
1453
|
]);
|
|
1351
1454
|
const handleError = useCallback3((error, connectorId) => {
|
|
1352
|
-
|
|
1353
|
-
|
|
1455
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
1456
|
+
onLog == null ? void 0 : onLog(`[connect-kit] connection failed (${connectorId}): ${normalizedError.message}`, normalizedError);
|
|
1354
1457
|
setQrUriMap((prev) => {
|
|
1355
1458
|
const next = {
|
|
1356
1459
|
...prev
|
|
@@ -1359,16 +1462,24 @@ var ConnectModalInner = /* @__PURE__ */ __name(({ open, connectors, onConnect, o
|
|
|
1359
1462
|
return next;
|
|
1360
1463
|
});
|
|
1361
1464
|
wc.resetView();
|
|
1465
|
+
if (renderConnectError) {
|
|
1466
|
+
setErrorState({
|
|
1467
|
+
connectorId,
|
|
1468
|
+
error: normalizedError
|
|
1469
|
+
});
|
|
1470
|
+
} else {
|
|
1471
|
+
setSelectedConnector(null);
|
|
1472
|
+
}
|
|
1362
1473
|
}, [
|
|
1363
1474
|
onLog,
|
|
1364
|
-
wc.resetView
|
|
1475
|
+
wc.resetView,
|
|
1476
|
+
renderConnectError
|
|
1365
1477
|
]);
|
|
1366
1478
|
const { triggerConnect, cancelConnect } = useWagmiConnect({
|
|
1367
1479
|
onConnect: handleConnect,
|
|
1368
1480
|
onError: handleError,
|
|
1369
1481
|
onQrUri: handleQrUri
|
|
1370
1482
|
});
|
|
1371
|
-
const wagmiConfig = useConfig2();
|
|
1372
1483
|
const connectorLogoMap = useMemo4(() => {
|
|
1373
1484
|
const map = {};
|
|
1374
1485
|
const connectorIds = [
|
|
@@ -1394,6 +1505,7 @@ var ConnectModalInner = /* @__PURE__ */ __name(({ open, connectors, onConnect, o
|
|
|
1394
1505
|
setSelectedConnector(null);
|
|
1395
1506
|
setQrUriMap({});
|
|
1396
1507
|
pendingQrConnectorRef.current = null;
|
|
1508
|
+
setErrorState(null);
|
|
1397
1509
|
wc.reset();
|
|
1398
1510
|
cancelConnect();
|
|
1399
1511
|
}
|
|
@@ -1403,9 +1515,10 @@ var ConnectModalInner = /* @__PURE__ */ __name(({ open, connectors, onConnect, o
|
|
|
1403
1515
|
wc.reset
|
|
1404
1516
|
]);
|
|
1405
1517
|
const handleSelectConnector = useCallback3((id) => {
|
|
1406
|
-
if (id === selectedConnector) return;
|
|
1518
|
+
if (id === selectedConnector && !errorState) return;
|
|
1407
1519
|
pendingQrConnectorRef.current = id;
|
|
1408
1520
|
setSelectedConnector(id);
|
|
1521
|
+
setErrorState(null);
|
|
1409
1522
|
wc.resetView();
|
|
1410
1523
|
writeRecentConnector(id);
|
|
1411
1524
|
setRecentConnector(id);
|
|
@@ -1416,6 +1529,7 @@ var ConnectModalInner = /* @__PURE__ */ __name(({ open, connectors, onConnect, o
|
|
|
1416
1529
|
triggerConnect(id);
|
|
1417
1530
|
}, [
|
|
1418
1531
|
selectedConnector,
|
|
1532
|
+
errorState,
|
|
1419
1533
|
triggerConnect,
|
|
1420
1534
|
cancelConnect,
|
|
1421
1535
|
wcProjectId,
|
|
@@ -1423,22 +1537,112 @@ var ConnectModalInner = /* @__PURE__ */ __name(({ open, connectors, onConnect, o
|
|
|
1423
1537
|
wc.loadWalletList
|
|
1424
1538
|
]);
|
|
1425
1539
|
const handleBack = useCallback3(() => {
|
|
1540
|
+
if (errorState) {
|
|
1541
|
+
setErrorState(null);
|
|
1542
|
+
setSelectedConnector(null);
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1426
1545
|
if (selectedConnector === "WALLET_CONNECT" && wc.handleBack()) return;
|
|
1427
1546
|
pendingQrConnectorRef.current = null;
|
|
1428
1547
|
setSelectedConnector(null);
|
|
1429
1548
|
cancelConnect();
|
|
1430
1549
|
}, [
|
|
1431
1550
|
selectedConnector,
|
|
1551
|
+
errorState,
|
|
1432
1552
|
wc.handleBack,
|
|
1433
1553
|
cancelConnect
|
|
1434
1554
|
]);
|
|
1555
|
+
const handleSwitchChainDisconnect = useCallback3(() => {
|
|
1556
|
+
disconnectAsync().catch((err) => {
|
|
1557
|
+
const normalized = err instanceof Error ? err : new Error(String(err));
|
|
1558
|
+
onLog == null ? void 0 : onLog(`[connect-kit] disconnect failed: ${normalized.message}`, normalized);
|
|
1559
|
+
});
|
|
1560
|
+
}, [
|
|
1561
|
+
disconnectAsync,
|
|
1562
|
+
onLog
|
|
1563
|
+
]);
|
|
1564
|
+
const handleOverlayClick = useCallback3(() => {
|
|
1565
|
+
if (chainMismatch) return;
|
|
1566
|
+
onClose();
|
|
1567
|
+
}, [
|
|
1568
|
+
chainMismatch,
|
|
1569
|
+
onClose
|
|
1570
|
+
]);
|
|
1571
|
+
const handleCloseClick = useCallback3(() => {
|
|
1572
|
+
if (chainMismatch) {
|
|
1573
|
+
handleSwitchChainDisconnect();
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
onClose();
|
|
1577
|
+
}, [
|
|
1578
|
+
chainMismatch,
|
|
1579
|
+
handleSwitchChainDisconnect,
|
|
1580
|
+
onClose
|
|
1581
|
+
]);
|
|
1582
|
+
const renderedError = useMemo4(() => {
|
|
1583
|
+
if (!errorState || !renderConnectError) return null;
|
|
1584
|
+
const ctx = {
|
|
1585
|
+
connectorId: errorState.connectorId,
|
|
1586
|
+
reason: classifyError(errorState.error),
|
|
1587
|
+
error: errorState.error,
|
|
1588
|
+
retry: /* @__PURE__ */ __name(() => {
|
|
1589
|
+
const id = errorState.connectorId;
|
|
1590
|
+
setErrorState(null);
|
|
1591
|
+
pendingQrConnectorRef.current = id;
|
|
1592
|
+
cancelConnect();
|
|
1593
|
+
triggerConnect(id);
|
|
1594
|
+
}, "retry"),
|
|
1595
|
+
dismiss: /* @__PURE__ */ __name(() => {
|
|
1596
|
+
setErrorState(null);
|
|
1597
|
+
setSelectedConnector(null);
|
|
1598
|
+
}, "dismiss")
|
|
1599
|
+
};
|
|
1600
|
+
return renderConnectError(ctx);
|
|
1601
|
+
}, [
|
|
1602
|
+
errorState,
|
|
1603
|
+
renderConnectError,
|
|
1604
|
+
triggerConnect,
|
|
1605
|
+
cancelConnect
|
|
1606
|
+
]);
|
|
1607
|
+
if (chainMismatch && requiredChainId != null) {
|
|
1608
|
+
return /* @__PURE__ */ React.createElement("div", {
|
|
1609
|
+
className: "ck-root"
|
|
1610
|
+
}, /* @__PURE__ */ React.createElement("div", {
|
|
1611
|
+
className: "ck-overlay",
|
|
1612
|
+
onClick: handleOverlayClick
|
|
1613
|
+
}), /* @__PURE__ */ React.createElement("div", {
|
|
1614
|
+
className: "ck-modal ck-modal--switch-chain",
|
|
1615
|
+
role: "dialog",
|
|
1616
|
+
"aria-modal": "true",
|
|
1617
|
+
"aria-label": "Switch network"
|
|
1618
|
+
}, /* @__PURE__ */ React.createElement("div", {
|
|
1619
|
+
className: "ck-pane"
|
|
1620
|
+
}, /* @__PURE__ */ React.createElement("div", {
|
|
1621
|
+
className: "ck-pane__header"
|
|
1622
|
+
}, /* @__PURE__ */ React.createElement("h3", {
|
|
1623
|
+
className: "ck-pane__title"
|
|
1624
|
+
}, "Switch network"), /* @__PURE__ */ React.createElement("button", {
|
|
1625
|
+
type: "button",
|
|
1626
|
+
className: "ck-btn-close",
|
|
1627
|
+
onClick: handleCloseClick,
|
|
1628
|
+
"aria-label": "Cancel and disconnect"
|
|
1629
|
+
})), /* @__PURE__ */ React.createElement("div", {
|
|
1630
|
+
className: "ck-pane__body"
|
|
1631
|
+
}, /* @__PURE__ */ React.createElement(SwitchChainView, {
|
|
1632
|
+
currentChainId: account.chainId,
|
|
1633
|
+
requiredChainId,
|
|
1634
|
+
requiredChainName,
|
|
1635
|
+
onDisconnect: handleSwitchChainDisconnect,
|
|
1636
|
+
onLog
|
|
1637
|
+
})))));
|
|
1638
|
+
}
|
|
1435
1639
|
return /* @__PURE__ */ React.createElement("div", {
|
|
1436
1640
|
className: "ck-root"
|
|
1437
1641
|
}, /* @__PURE__ */ React.createElement("div", {
|
|
1438
1642
|
className: "ck-overlay",
|
|
1439
|
-
onClick:
|
|
1643
|
+
onClick: handleOverlayClick
|
|
1440
1644
|
}), /* @__PURE__ */ React.createElement("div", {
|
|
1441
|
-
className: `ck-modal${selectedConnector ? " ck-modal--has-detail" : ""}`,
|
|
1645
|
+
className: `ck-modal${(selectedConnector != null ? selectedConnector : errorState) ? " ck-modal--has-detail" : ""}`,
|
|
1442
1646
|
role: "dialog",
|
|
1443
1647
|
"aria-modal": "true",
|
|
1444
1648
|
"aria-label": "Connect Wallet"
|
|
@@ -1447,18 +1651,120 @@ var ConnectModalInner = /* @__PURE__ */ __name(({ open, connectors, onConnect, o
|
|
|
1447
1651
|
selected: selectedConnector,
|
|
1448
1652
|
recentConnector,
|
|
1449
1653
|
onSelect: handleSelectConnector,
|
|
1450
|
-
onClose
|
|
1654
|
+
onClose: handleCloseClick
|
|
1451
1655
|
}), /* @__PURE__ */ React.createElement(DetailPane, {
|
|
1452
1656
|
selectedConnector,
|
|
1453
1657
|
qrUri: selectedConnector ? (_a = qrUriMap[selectedConnector]) != null ? _a : null : null,
|
|
1454
1658
|
hasMetaMaskExtension,
|
|
1455
1659
|
connectorLogoMap,
|
|
1456
1660
|
wc,
|
|
1661
|
+
errorOverride: renderedError,
|
|
1457
1662
|
onBack: handleBack,
|
|
1458
|
-
onClose
|
|
1663
|
+
onClose: handleCloseClick
|
|
1459
1664
|
})));
|
|
1460
1665
|
}, "ConnectModalInner");
|
|
1461
1666
|
|
|
1667
|
+
// src/ConnectKitProvider.tsx
|
|
1668
|
+
import { createContext, useCallback as useCallback4, useContext, useEffect as useEffect5, useMemo as useMemo5, useRef as useRef4, useState as useState4 } from "react";
|
|
1669
|
+
import { useAccount as useAccount2, useDisconnect as useDisconnect2 } from "wagmi";
|
|
1670
|
+
var ConnectKitContext = /* @__PURE__ */ createContext(null);
|
|
1671
|
+
var resolveConnectorId = /* @__PURE__ */ __name((wagmiConnectorId) => {
|
|
1672
|
+
if (!wagmiConnectorId) return null;
|
|
1673
|
+
if (wagmiConnectorId === "credit-connect") return "CREDIT_CONNECT";
|
|
1674
|
+
if (wagmiConnectorId === "walletConnect") return "WALLET_CONNECT";
|
|
1675
|
+
const lower = wagmiConnectorId.toLowerCase();
|
|
1676
|
+
if (lower.includes("metamask") || wagmiConnectorId === "injected") return "METAMASK";
|
|
1677
|
+
if (lower.includes("walletconnect")) return "WALLET_CONNECT";
|
|
1678
|
+
return null;
|
|
1679
|
+
}, "resolveConnectorId");
|
|
1680
|
+
var ConnectKitProvider = /* @__PURE__ */ __name(({ children, connectors, wcProjectId, requiredChainId, onConnect, onLog, renderConnectError }) => {
|
|
1681
|
+
var _a;
|
|
1682
|
+
const [isOpen, setIsOpen] = useState4(false);
|
|
1683
|
+
const [pendingResult, setPendingResult] = useState4(null);
|
|
1684
|
+
const account = useAccount2();
|
|
1685
|
+
const { disconnectAsync } = useDisconnect2();
|
|
1686
|
+
const onConnectRef = useRef4(onConnect);
|
|
1687
|
+
useEffect5(() => {
|
|
1688
|
+
onConnectRef.current = onConnect;
|
|
1689
|
+
}, [
|
|
1690
|
+
onConnect
|
|
1691
|
+
]);
|
|
1692
|
+
const chainMismatch = requiredChainId != null && account.status === "connected" && account.chainId != null && account.chainId !== requiredChainId;
|
|
1693
|
+
const effectiveOpen = isOpen || chainMismatch;
|
|
1694
|
+
const open = useCallback4(() => {
|
|
1695
|
+
setIsOpen(true);
|
|
1696
|
+
}, []);
|
|
1697
|
+
const close = useCallback4(() => {
|
|
1698
|
+
setIsOpen(false);
|
|
1699
|
+
}, []);
|
|
1700
|
+
const disconnect = useCallback4(async () => {
|
|
1701
|
+
await disconnectAsync();
|
|
1702
|
+
}, [
|
|
1703
|
+
disconnectAsync
|
|
1704
|
+
]);
|
|
1705
|
+
const handleModalConnect = useCallback4((result) => {
|
|
1706
|
+
setPendingResult(result);
|
|
1707
|
+
}, []);
|
|
1708
|
+
useEffect5(() => {
|
|
1709
|
+
var _a2;
|
|
1710
|
+
if (!pendingResult) return;
|
|
1711
|
+
if (account.status !== "connected") {
|
|
1712
|
+
setPendingResult(null);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
if (requiredChainId != null && account.chainId !== requiredChainId) {
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
(_a2 = onConnectRef.current) == null ? void 0 : _a2.call(onConnectRef, pendingResult);
|
|
1719
|
+
setPendingResult(null);
|
|
1720
|
+
setIsOpen(false);
|
|
1721
|
+
}, [
|
|
1722
|
+
pendingResult,
|
|
1723
|
+
account.status,
|
|
1724
|
+
account.chainId,
|
|
1725
|
+
requiredChainId
|
|
1726
|
+
]);
|
|
1727
|
+
const value = useMemo5(() => {
|
|
1728
|
+
var _a2;
|
|
1729
|
+
return {
|
|
1730
|
+
isOpen: effectiveOpen,
|
|
1731
|
+
open,
|
|
1732
|
+
close,
|
|
1733
|
+
isConnected: account.status === "connected",
|
|
1734
|
+
address: account.address,
|
|
1735
|
+
connectorId: resolveConnectorId((_a2 = account.connector) == null ? void 0 : _a2.id),
|
|
1736
|
+
disconnect
|
|
1737
|
+
};
|
|
1738
|
+
}, [
|
|
1739
|
+
effectiveOpen,
|
|
1740
|
+
open,
|
|
1741
|
+
close,
|
|
1742
|
+
account.status,
|
|
1743
|
+
account.address,
|
|
1744
|
+
(_a = account.connector) == null ? void 0 : _a.id,
|
|
1745
|
+
disconnect
|
|
1746
|
+
]);
|
|
1747
|
+
return /* @__PURE__ */ React.createElement(ConnectKitContext.Provider, {
|
|
1748
|
+
value
|
|
1749
|
+
}, children, /* @__PURE__ */ React.createElement(ConnectModal, {
|
|
1750
|
+
open: effectiveOpen,
|
|
1751
|
+
connectors,
|
|
1752
|
+
wcProjectId,
|
|
1753
|
+
requiredChainId,
|
|
1754
|
+
renderConnectError,
|
|
1755
|
+
onConnect: handleModalConnect,
|
|
1756
|
+
onClose: close,
|
|
1757
|
+
onLog
|
|
1758
|
+
}));
|
|
1759
|
+
}, "ConnectKitProvider");
|
|
1760
|
+
var useConnectKit = /* @__PURE__ */ __name(() => {
|
|
1761
|
+
const ctx = useContext(ConnectKitContext);
|
|
1762
|
+
if (!ctx) {
|
|
1763
|
+
throw new Error("useConnectKit must be used within a ConnectKitProvider");
|
|
1764
|
+
}
|
|
1765
|
+
return ctx;
|
|
1766
|
+
}, "useConnectKit");
|
|
1767
|
+
|
|
1462
1768
|
// src/creditConnectConnector.ts
|
|
1463
1769
|
import { createConnector } from "@wagmi/core";
|
|
1464
1770
|
import { getAddress } from "viem";
|
|
@@ -1850,6 +2156,8 @@ var creditConnectConnector = /* @__PURE__ */ __name((options) => {
|
|
|
1850
2156
|
});
|
|
1851
2157
|
}, "creditConnectConnector");
|
|
1852
2158
|
export {
|
|
2159
|
+
ConnectKitProvider,
|
|
1853
2160
|
ConnectModal,
|
|
1854
|
-
creditConnectConnector
|
|
2161
|
+
creditConnectConnector,
|
|
2162
|
+
useConnectKit
|
|
1855
2163
|
};
|
package/dist/package.json
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type FC,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import { useAccount, useDisconnect } from 'wagmi';
|
|
13
|
+
import { ConnectModal } from './ConnectModal';
|
|
14
|
+
import type { ConnectErrorContext, ConnectResult, ConnectorId, Connectors } from './types';
|
|
15
|
+
|
|
16
|
+
export interface ConnectKitProviderProps {
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
connectors: Connectors;
|
|
19
|
+
wcProjectId?: string;
|
|
20
|
+
/**
|
|
21
|
+
* 연결돼 있는 동안 강제할 체인 ID. 다른 체인에 연결돼 있으면
|
|
22
|
+
* 자동으로 스위치 모달이 떠서 사용자에게 전환 또는 disconnect를 요구.
|
|
23
|
+
*/
|
|
24
|
+
requiredChainId?: number;
|
|
25
|
+
/**
|
|
26
|
+
* 연결이 정상적으로 끝나고(체인 검증까지 통과) "사용 가능한 상태"가 됐을 때 한 번 호출.
|
|
27
|
+
* 재연결(reconnect)에서는 호출되지 않음 — 그쪽은 dApp의 watchAccount 책임.
|
|
28
|
+
*/
|
|
29
|
+
onConnect?: (result: ConnectResult) => void;
|
|
30
|
+
onLog?: (message: string, error?: Error) => void;
|
|
31
|
+
/**
|
|
32
|
+
* 연결 실패(거절/미설치/기타) 시 모달 안에 표시할 fallback UI를 dApp이 직접 그리고 싶을 때.
|
|
33
|
+
* 미제공 시 기본 동작은 selector로 복귀.
|
|
34
|
+
*/
|
|
35
|
+
renderConnectError?: (ctx: ConnectErrorContext) => ReactNode;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ConnectKitContextValue {
|
|
39
|
+
isOpen: boolean;
|
|
40
|
+
open: () => void;
|
|
41
|
+
close: () => void;
|
|
42
|
+
isConnected: boolean;
|
|
43
|
+
address: `0x${string}` | undefined;
|
|
44
|
+
connectorId: ConnectorId | null;
|
|
45
|
+
disconnect: () => Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ConnectKitContext = createContext<ConnectKitContextValue | null>(null);
|
|
49
|
+
|
|
50
|
+
const resolveConnectorId = (wagmiConnectorId: string | undefined): ConnectorId | null => {
|
|
51
|
+
if (!wagmiConnectorId) return null;
|
|
52
|
+
if (wagmiConnectorId === 'credit-connect') return 'CREDIT_CONNECT';
|
|
53
|
+
if (wagmiConnectorId === 'walletConnect') return 'WALLET_CONNECT';
|
|
54
|
+
const lower = wagmiConnectorId.toLowerCase();
|
|
55
|
+
if (lower.includes('metamask') || wagmiConnectorId === 'injected') return 'METAMASK';
|
|
56
|
+
if (lower.includes('walletconnect')) return 'WALLET_CONNECT';
|
|
57
|
+
return null;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const ConnectKitProvider: FC<ConnectKitProviderProps> = ({
|
|
61
|
+
children,
|
|
62
|
+
connectors,
|
|
63
|
+
wcProjectId,
|
|
64
|
+
requiredChainId,
|
|
65
|
+
onConnect,
|
|
66
|
+
onLog,
|
|
67
|
+
renderConnectError,
|
|
68
|
+
}) => {
|
|
69
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
70
|
+
const [pendingResult, setPendingResult] = useState<ConnectResult | null>(null);
|
|
71
|
+
|
|
72
|
+
const account = useAccount();
|
|
73
|
+
const { disconnectAsync } = useDisconnect();
|
|
74
|
+
|
|
75
|
+
// dApp의 onConnect는 ref로 잡아 effect 재실행 노이즈 차단
|
|
76
|
+
const onConnectRef = useRef(onConnect);
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
onConnectRef.current = onConnect;
|
|
79
|
+
}, [onConnect]);
|
|
80
|
+
|
|
81
|
+
// chain mismatch invariant: 연결돼 있지만 요구 체인이 아니면 modal force-open
|
|
82
|
+
const chainMismatch =
|
|
83
|
+
requiredChainId != null &&
|
|
84
|
+
account.status === 'connected' &&
|
|
85
|
+
account.chainId != null &&
|
|
86
|
+
account.chainId !== requiredChainId;
|
|
87
|
+
|
|
88
|
+
const effectiveOpen = isOpen || chainMismatch;
|
|
89
|
+
|
|
90
|
+
const open = useCallback((): void => {
|
|
91
|
+
setIsOpen(true);
|
|
92
|
+
}, []);
|
|
93
|
+
|
|
94
|
+
const close = useCallback((): void => {
|
|
95
|
+
setIsOpen(false);
|
|
96
|
+
}, []);
|
|
97
|
+
|
|
98
|
+
const disconnect = useCallback(async (): Promise<void> => {
|
|
99
|
+
await disconnectAsync();
|
|
100
|
+
}, [disconnectAsync]);
|
|
101
|
+
|
|
102
|
+
// ConnectModal → Provider: 연결 결과를 일단 보류했다가
|
|
103
|
+
// chain 검증까지 통과한 후에만 dApp으로 forward
|
|
104
|
+
const handleModalConnect = useCallback((result: ConnectResult): void => {
|
|
105
|
+
setPendingResult(result);
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
// pendingResult가 있고, 연결 + chain 매칭이 충족되면 dApp으로 발화
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!pendingResult) return;
|
|
111
|
+
if (account.status !== 'connected') {
|
|
112
|
+
// 연결이 끊겼거나 진행 중 — pending 폐기
|
|
113
|
+
setPendingResult(null);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (requiredChainId != null && account.chainId !== requiredChainId) {
|
|
117
|
+
// chain 아직 안 맞음 — switch 후 다시 평가
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
onConnectRef.current?.(pendingResult);
|
|
121
|
+
setPendingResult(null);
|
|
122
|
+
setIsOpen(false);
|
|
123
|
+
}, [pendingResult, account.status, account.chainId, requiredChainId]);
|
|
124
|
+
|
|
125
|
+
const value = useMemo<ConnectKitContextValue>(
|
|
126
|
+
() => ({
|
|
127
|
+
isOpen: effectiveOpen,
|
|
128
|
+
open,
|
|
129
|
+
close,
|
|
130
|
+
isConnected: account.status === 'connected',
|
|
131
|
+
address: account.address,
|
|
132
|
+
connectorId: resolveConnectorId(account.connector?.id),
|
|
133
|
+
disconnect,
|
|
134
|
+
}),
|
|
135
|
+
[
|
|
136
|
+
effectiveOpen,
|
|
137
|
+
open,
|
|
138
|
+
close,
|
|
139
|
+
account.status,
|
|
140
|
+
account.address,
|
|
141
|
+
account.connector?.id,
|
|
142
|
+
disconnect,
|
|
143
|
+
],
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<ConnectKitContext.Provider value={value}>
|
|
148
|
+
{children}
|
|
149
|
+
<ConnectModal
|
|
150
|
+
open={effectiveOpen}
|
|
151
|
+
connectors={connectors}
|
|
152
|
+
wcProjectId={wcProjectId}
|
|
153
|
+
requiredChainId={requiredChainId}
|
|
154
|
+
renderConnectError={renderConnectError}
|
|
155
|
+
onConnect={handleModalConnect}
|
|
156
|
+
onClose={close}
|
|
157
|
+
onLog={onLog}
|
|
158
|
+
/>
|
|
159
|
+
</ConnectKitContext.Provider>
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const useConnectKit = (): ConnectKitContextValue => {
|
|
164
|
+
const ctx = useContext(ConnectKitContext);
|
|
165
|
+
if (!ctx) {
|
|
166
|
+
throw new Error('useConnectKit must be used within a ConnectKitProvider');
|
|
167
|
+
}
|
|
168
|
+
return ctx;
|
|
169
|
+
};
|
package/src/ConnectModal.scss
CHANGED
|
@@ -140,6 +140,44 @@
|
|
|
140
140
|
&:hover {
|
|
141
141
|
background: rgba(255, 255, 255, 0.22);
|
|
142
142
|
}
|
|
143
|
+
|
|
144
|
+
&:disabled {
|
|
145
|
+
opacity: 0.6;
|
|
146
|
+
cursor: not-allowed;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.ck-btn-secondary {
|
|
151
|
+
display: inline-flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
justify-content: center;
|
|
154
|
+
padding: 8px 16px;
|
|
155
|
+
border-radius: 8px;
|
|
156
|
+
font-size: 14px;
|
|
157
|
+
font-weight: 500;
|
|
158
|
+
cursor: pointer;
|
|
159
|
+
white-space: nowrap;
|
|
160
|
+
color: rgba(255, 255, 255, 0.75);
|
|
161
|
+
background: transparent;
|
|
162
|
+
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
163
|
+
|
|
164
|
+
&:hover {
|
|
165
|
+
color: #fff;
|
|
166
|
+
border-color: rgba(255, 255, 255, 0.32);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.ck-view__actions {
|
|
171
|
+
display: flex;
|
|
172
|
+
flex-direction: column;
|
|
173
|
+
gap: 8px;
|
|
174
|
+
width: 100%;
|
|
175
|
+
margin-top: 16px;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.ck-view__caption--error {
|
|
179
|
+
color: #ff8080;
|
|
180
|
+
margin-top: 8px;
|
|
143
181
|
}
|
|
144
182
|
|
|
145
183
|
// ── Connector list ────────────────────────────────────────
|
package/src/ConnectModal.tsx
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef, useMemo, type FC } from 'react';
|
|
1
|
+
import { useState, useEffect, useCallback, useRef, 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,15 +299,36 @@ 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
306
|
const [qrUriMap, setQrUriMap] = useState<Partial<Record<ConnectorId, string>>>({});
|
|
284
307
|
const pendingQrConnectorRef = useRef<ConnectorId | null>(null);
|
|
285
308
|
const [recentConnector, setRecentConnector] = useState<ConnectorId | null>(null);
|
|
286
309
|
const [hasMetaMaskExtension, setHasMetaMaskExtension] = useState<boolean>(false);
|
|
310
|
+
const [errorState, setErrorState] = useState<{ connectorId: ConnectorId; error: Error } | null>(
|
|
311
|
+
null,
|
|
312
|
+
);
|
|
287
313
|
|
|
288
314
|
const wc = useWCState();
|
|
289
315
|
|
|
316
|
+
// wagmi 상태 — chain mismatch invariant 판정용
|
|
317
|
+
const account = useAccount();
|
|
318
|
+
const { disconnectAsync } = useDisconnect();
|
|
319
|
+
const wagmiConfig = useConfig();
|
|
320
|
+
|
|
321
|
+
const chainMismatch =
|
|
322
|
+
requiredChainId != null &&
|
|
323
|
+
account.status === 'connected' &&
|
|
324
|
+
account.chainId != null &&
|
|
325
|
+
account.chainId !== requiredChainId;
|
|
326
|
+
|
|
327
|
+
const requiredChainName = useMemo<string | undefined>(() => {
|
|
328
|
+
if (requiredChainId == null) return undefined;
|
|
329
|
+
return wagmiConfig.chains.find((chain) => chain.id === requiredChainId)?.name;
|
|
330
|
+
}, [wagmiConfig.chains, requiredChainId]);
|
|
331
|
+
|
|
290
332
|
// QR URI 수신 — 커넥터별 캐시에 저장 (null은 무시하여 캐시 유지)
|
|
291
333
|
const handleQrUri = useCallback((uri: string | null) => {
|
|
292
334
|
if (uri) {
|
|
@@ -298,7 +340,10 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
298
340
|
// 연결 완료
|
|
299
341
|
const handleConnect = useCallback(
|
|
300
342
|
(result: Parameters<typeof onConnect>[0]) => {
|
|
343
|
+
setErrorState(null);
|
|
301
344
|
onConnect(result);
|
|
345
|
+
// chain mismatch가 있으면 모달은 SwitchChainView로 자동 전환됨 (닫지 않음)
|
|
346
|
+
// Provider 측에서 requiredChainId가 매칭될 때까지 force-open 유지
|
|
302
347
|
onClose();
|
|
303
348
|
},
|
|
304
349
|
[onConnect, onClose],
|
|
@@ -306,17 +351,25 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
306
351
|
|
|
307
352
|
// 연결 실패 — 실패한 커넥터의 QR 캐시만 삭제
|
|
308
353
|
const handleError = useCallback(
|
|
309
|
-
(error:
|
|
310
|
-
|
|
311
|
-
|
|
354
|
+
(error: unknown, connectorId: ConnectorId): void => {
|
|
355
|
+
const normalizedError = error instanceof Error ? error : new Error(String(error));
|
|
356
|
+
onLog?.(
|
|
357
|
+
`[connect-kit] connection failed (${connectorId}): ${normalizedError.message}`,
|
|
358
|
+
normalizedError,
|
|
359
|
+
);
|
|
312
360
|
setQrUriMap((prev) => {
|
|
313
361
|
const next = { ...prev };
|
|
314
362
|
delete next[connectorId];
|
|
315
363
|
return next;
|
|
316
364
|
});
|
|
317
365
|
wc.resetView();
|
|
366
|
+
if (renderConnectError) {
|
|
367
|
+
setErrorState({ connectorId, error: normalizedError });
|
|
368
|
+
} else {
|
|
369
|
+
setSelectedConnector(null);
|
|
370
|
+
}
|
|
318
371
|
},
|
|
319
|
-
[onLog, wc.resetView],
|
|
372
|
+
[onLog, wc.resetView, renderConnectError],
|
|
320
373
|
);
|
|
321
374
|
|
|
322
375
|
// 연결 시도
|
|
@@ -327,7 +380,6 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
327
380
|
});
|
|
328
381
|
|
|
329
382
|
// wagmi connector icon 매핑
|
|
330
|
-
const wagmiConfig = useConfig();
|
|
331
383
|
const connectorLogoMap = useMemo(() => {
|
|
332
384
|
const map: Partial<Record<ConnectorId, string>> = {};
|
|
333
385
|
const connectorIds: ConnectorId[] = [
|
|
@@ -355,18 +407,19 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
355
407
|
setSelectedConnector(null);
|
|
356
408
|
setQrUriMap({});
|
|
357
409
|
pendingQrConnectorRef.current = null;
|
|
410
|
+
setErrorState(null);
|
|
358
411
|
wc.reset();
|
|
359
412
|
cancelConnect();
|
|
360
413
|
}
|
|
361
414
|
}, [open, cancelConnect, wc.reset]);
|
|
362
415
|
|
|
363
|
-
// 커넥터 선택
|
|
416
|
+
// 커넥터 선택
|
|
364
417
|
const handleSelectConnector = useCallback(
|
|
365
418
|
(id: ConnectorId): void => {
|
|
366
|
-
if (id === selectedConnector) return;
|
|
367
|
-
|
|
419
|
+
if (id === selectedConnector && !errorState) return;
|
|
368
420
|
pendingQrConnectorRef.current = id;
|
|
369
421
|
setSelectedConnector(id);
|
|
422
|
+
setErrorState(null);
|
|
370
423
|
wc.resetView();
|
|
371
424
|
writeRecentConnector(id);
|
|
372
425
|
setRecentConnector(id);
|
|
@@ -380,6 +433,7 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
380
433
|
},
|
|
381
434
|
[
|
|
382
435
|
selectedConnector,
|
|
436
|
+
errorState,
|
|
383
437
|
triggerConnect,
|
|
384
438
|
cancelConnect,
|
|
385
439
|
wcProjectId,
|
|
@@ -390,17 +444,103 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
390
444
|
|
|
391
445
|
// 뒤로가기 — QR 캐시는 유지, wagmi만 reset
|
|
392
446
|
const handleBack = useCallback((): void => {
|
|
447
|
+
if (errorState) {
|
|
448
|
+
setErrorState(null);
|
|
449
|
+
setSelectedConnector(null);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
393
452
|
if (selectedConnector === 'WALLET_CONNECT' && wc.handleBack()) return;
|
|
394
453
|
pendingQrConnectorRef.current = null;
|
|
395
454
|
setSelectedConnector(null);
|
|
396
455
|
cancelConnect();
|
|
397
|
-
}, [selectedConnector, wc.handleBack, cancelConnect]);
|
|
456
|
+
}, [selectedConnector, errorState, wc.handleBack, cancelConnect]);
|
|
457
|
+
|
|
458
|
+
// chain mismatch 상태에서의 disconnect (Switch chain pane의 cancel)
|
|
459
|
+
const handleSwitchChainDisconnect = useCallback((): void => {
|
|
460
|
+
disconnectAsync().catch((err: unknown) => {
|
|
461
|
+
const normalized = err instanceof Error ? err : new Error(String(err));
|
|
462
|
+
onLog?.(`[connect-kit] disconnect failed: ${normalized.message}`, normalized);
|
|
463
|
+
});
|
|
464
|
+
}, [disconnectAsync, onLog]);
|
|
465
|
+
|
|
466
|
+
// overlay 클릭 — chain mismatch 상태에선 무시 (실수 클릭 방어)
|
|
467
|
+
const handleOverlayClick = useCallback((): void => {
|
|
468
|
+
if (chainMismatch) return;
|
|
469
|
+
onClose();
|
|
470
|
+
}, [chainMismatch, onClose]);
|
|
471
|
+
|
|
472
|
+
// close 버튼 — chain mismatch 상태에선 disconnect
|
|
473
|
+
const handleCloseClick = useCallback((): void => {
|
|
474
|
+
if (chainMismatch) {
|
|
475
|
+
handleSwitchChainDisconnect();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
onClose();
|
|
479
|
+
}, [chainMismatch, handleSwitchChainDisconnect, onClose]);
|
|
480
|
+
|
|
481
|
+
const renderedError: ReactNode = useMemo(() => {
|
|
482
|
+
if (!errorState || !renderConnectError) return null;
|
|
483
|
+
const ctx: ConnectErrorContext = {
|
|
484
|
+
connectorId: errorState.connectorId,
|
|
485
|
+
reason: classifyError(errorState.error),
|
|
486
|
+
error: errorState.error,
|
|
487
|
+
retry: () => {
|
|
488
|
+
const id = errorState.connectorId;
|
|
489
|
+
setErrorState(null);
|
|
490
|
+
pendingQrConnectorRef.current = id;
|
|
491
|
+
cancelConnect();
|
|
492
|
+
triggerConnect(id);
|
|
493
|
+
},
|
|
494
|
+
dismiss: () => {
|
|
495
|
+
setErrorState(null);
|
|
496
|
+
setSelectedConnector(null);
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
return renderConnectError(ctx);
|
|
500
|
+
}, [errorState, renderConnectError, triggerConnect, cancelConnect]);
|
|
501
|
+
|
|
502
|
+
// chain mismatch면 단일 pane (selector/detail 숨김)
|
|
503
|
+
// requiredChainId != null 재확인은 TS narrowing용 — chainMismatch true면 이미 보장됨
|
|
504
|
+
if (chainMismatch && requiredChainId != null) {
|
|
505
|
+
return (
|
|
506
|
+
<div className="ck-root">
|
|
507
|
+
<div className="ck-overlay" onClick={handleOverlayClick} />
|
|
508
|
+
<div
|
|
509
|
+
className="ck-modal ck-modal--switch-chain"
|
|
510
|
+
role="dialog"
|
|
511
|
+
aria-modal="true"
|
|
512
|
+
aria-label="Switch network"
|
|
513
|
+
>
|
|
514
|
+
<div className="ck-pane">
|
|
515
|
+
<div className="ck-pane__header">
|
|
516
|
+
<h3 className="ck-pane__title">Switch network</h3>
|
|
517
|
+
<button
|
|
518
|
+
type="button"
|
|
519
|
+
className="ck-btn-close"
|
|
520
|
+
onClick={handleCloseClick}
|
|
521
|
+
aria-label="Cancel and disconnect"
|
|
522
|
+
/>
|
|
523
|
+
</div>
|
|
524
|
+
<div className="ck-pane__body">
|
|
525
|
+
<SwitchChainView
|
|
526
|
+
currentChainId={account.chainId}
|
|
527
|
+
requiredChainId={requiredChainId}
|
|
528
|
+
requiredChainName={requiredChainName}
|
|
529
|
+
onDisconnect={handleSwitchChainDisconnect}
|
|
530
|
+
onLog={onLog}
|
|
531
|
+
/>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
);
|
|
537
|
+
}
|
|
398
538
|
|
|
399
539
|
return (
|
|
400
540
|
<div className="ck-root">
|
|
401
|
-
<div className="ck-overlay" onClick={
|
|
541
|
+
<div className="ck-overlay" onClick={handleOverlayClick} />
|
|
402
542
|
<div
|
|
403
|
-
className={`ck-modal${selectedConnector ? ' ck-modal--has-detail' : ''}`}
|
|
543
|
+
className={`ck-modal${(selectedConnector ?? errorState) ? ' ck-modal--has-detail' : ''}`}
|
|
404
544
|
role="dialog"
|
|
405
545
|
aria-modal="true"
|
|
406
546
|
aria-label="Connect Wallet"
|
|
@@ -410,7 +550,7 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
410
550
|
selected={selectedConnector}
|
|
411
551
|
recentConnector={recentConnector}
|
|
412
552
|
onSelect={handleSelectConnector}
|
|
413
|
-
onClose={
|
|
553
|
+
onClose={handleCloseClick}
|
|
414
554
|
/>
|
|
415
555
|
<DetailPane
|
|
416
556
|
selectedConnector={selectedConnector}
|
|
@@ -418,8 +558,9 @@ const ConnectModalInner: FC<ConnectModalInnerProps> = ({
|
|
|
418
558
|
hasMetaMaskExtension={hasMetaMaskExtension}
|
|
419
559
|
connectorLogoMap={connectorLogoMap}
|
|
420
560
|
wc={wc}
|
|
561
|
+
errorOverride={renderedError}
|
|
421
562
|
onBack={handleBack}
|
|
422
|
-
onClose={
|
|
563
|
+
onClose={handleCloseClick}
|
|
423
564
|
/>
|
|
424
565
|
</div>
|
|
425
566
|
</div>
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
export { ConnectModal } from './ConnectModal';
|
|
2
|
+
export { ConnectKitProvider, useConnectKit } from './ConnectKitProvider';
|
|
3
|
+
export type { ConnectKitContextValue, ConnectKitProviderProps } from './ConnectKitProvider';
|
|
2
4
|
export { creditConnectConnector } from './creditConnectConnector';
|
|
3
5
|
export type { CreditConnectConnectorOptions } from './creditConnectConnector';
|
|
4
6
|
export type {
|
|
7
|
+
ConnectErrorContext,
|
|
8
|
+
ConnectErrorReason,
|
|
5
9
|
ConnectModalProps,
|
|
6
10
|
Connectors,
|
|
7
11
|
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
|
}
|
|
@@ -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
|
+
};
|