@gluwa/connect-kit 0.2.0-next.7 → 0.2.0-next.8

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 CHANGED
@@ -1,5 +1,16 @@
1
1
  # @gluwa/wallet-kit
2
2
 
3
+ ## 0.2.0-next.8
4
+
5
+ ### Patch Changes
6
+
7
+ - e3789b4: emit OnSessionExpired when credit-connect QR session times out
8
+ - Updated dependencies [e3789b4]
9
+ - @gluwa/credit-connect-hub-node-ws@0.1.0-next.4
10
+ - @gluwa/credit-connect-sdk@0.2.0-next.4
11
+ - @gluwa/credit-connect-e2ee-tweetnacl@0.2.0-next.4
12
+ - @gluwa/credit-connect-storage-local@0.2.0-next.4
13
+
3
14
  ## 0.2.0-next.7
4
15
 
5
16
  ### Patch Changes
@@ -65,6 +65,10 @@ var creditConnectConnector = /* @__PURE__ */ __name((options) => {
65
65
  emitWagmiDisconnect == null ? void 0 : emitWagmiDisconnect();
66
66
  }
67
67
  });
68
+ manager.on("sessionExpired", (payload) => {
69
+ console.log("[CC connector] session expired \u2192 emit session_expired", payload);
70
+ emitWagmiMessage == null ? void 0 : emitWagmiMessage("session_expired", payload);
71
+ });
68
72
  return manager;
69
73
  }, "createManagerIfNeeded");
70
74
  const ensureManagerInitialized = /* @__PURE__ */ __name(async () => {
@@ -1,7 +1,7 @@
1
1
  import React from "react";
2
2
  import {
3
3
  creditConnectConnector
4
- } from "./chunk-EDNRW47A.js";
4
+ } from "./chunk-VE5LB7BN.js";
5
5
  export {
6
6
  creditConnectConnector
7
7
  };
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ import React from "react";
2
2
  import {
3
3
  __name,
4
4
  creditConnectConnector
5
- } from "./chunk-EDNRW47A.js";
5
+ } from "./chunk-VE5LB7BN.js";
6
6
 
7
7
  // src/api/contract.ts
8
8
  import { readContract as wagmiReadContract, writeContract as wagmiWriteContract } from "@wagmi/core";
@@ -194,13 +194,13 @@ var disconnectAll = /* @__PURE__ */ __name(async (onLog) => {
194
194
  }, "disconnectAll");
195
195
 
196
196
  // src/ConnectKitProvider.tsx
197
- import { createContext, useCallback as useCallback3, useContext, useEffect as useEffect4, useMemo as useMemo2, useRef as useRef4, useState as useState4 } from "react";
197
+ import { createContext, useCallback as useCallback4, useContext, useEffect as useEffect5, useMemo as useMemo2, useRef as useRef5, useState as useState5 } from "react";
198
198
  import { WagmiProvider, useAccount, useConfig as useConfig3, useDisconnect, useSwitchChain } from "wagmi";
199
199
  import { getAccount as getAccount2 } from "@wagmi/core";
200
200
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
201
201
 
202
202
  // src/layout/connectDialog/index.tsx
203
- import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
203
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
204
204
  import { useConfig as useConfig2 } from "wagmi";
205
205
 
206
206
  // src/layout/connectDialog/asset/closeIcon.tsx
@@ -919,7 +919,7 @@ var getTitle = /* @__PURE__ */ __name((content, connector) => {
919
919
  return "";
920
920
  }
921
921
  }, "getTitle");
922
- var DetailPanel = /* @__PURE__ */ __name(({ selectedConnector, selectedWallet = null, content = "idle", qrUri = null, wallets = [], walletListLoading = false, walletCount, onBack, onClose, onShowAllWallets, onSelectWallet, onCopyLink }) => {
922
+ var DetailPanel = /* @__PURE__ */ __name(({ selectedConnector, selectedWallet = null, content = "idle", qrUri = null, wallets = [], walletListLoading = false, walletCount, onBack, onClose, onShowAllWallets, onSelectWallet, onCopyLink, onRetry }) => {
923
923
  const connectorMeta = selectedConnector ? CONNECTOR_META[selectedConnector] : null;
924
924
  const connectorLogoUrl = connectorMeta == null ? void 0 : connectorMeta.logoUrl;
925
925
  const connectorDownloadUrl = connectorMeta == null ? void 0 : connectorMeta.downloadUrl;
@@ -950,7 +950,9 @@ var DetailPanel = /* @__PURE__ */ __name(({ selectedConnector, selectedWallet =
950
950
  uri: qrUri,
951
951
  logoUrl: connectorLogoUrl,
952
952
  downloadUrl: connectorDownloadUrl
953
- }), content === "timeout" && /* @__PURE__ */ React.createElement(Timeout, null), (content === "metamask-ext" || content === "metamask-qr") && /* @__PURE__ */ React.createElement(MetaMask, {
953
+ }), content === "timeout" && /* @__PURE__ */ React.createElement(Timeout, {
954
+ onRetry
955
+ }), (content === "metamask-ext" || content === "metamask-qr") && /* @__PURE__ */ React.createElement(MetaMask, {
954
956
  variant: content === "metamask-ext" ? "extension" : "qr",
955
957
  logoUrl: connectorLogoUrl,
956
958
  uri: qrUri,
@@ -1060,29 +1062,36 @@ var resolveWagmiConnector = /* @__PURE__ */ __name((connectors, connectorId) =>
1060
1062
  return void 0;
1061
1063
  }
1062
1064
  }, "resolveWagmiConnector");
1063
- var useWagmiConnect = /* @__PURE__ */ __name(({ onConnect, onError, onQrUri }) => {
1065
+ var useWagmiConnect = /* @__PURE__ */ __name(({ onConnect, onError, onQrUri, onSessionExpired }) => {
1064
1066
  const config = useConfig();
1065
1067
  const pendingConnectorId = useRef(null);
1066
1068
  const attemptCounter = useRef(0);
1067
1069
  const onConnectRef = useRef(onConnect);
1068
1070
  const onErrorRef = useRef(onError);
1069
1071
  const onQrUriRef = useRef(onQrUri);
1072
+ const onSessionExpiredRef = useRef(onSessionExpired);
1070
1073
  useEffect(() => {
1071
1074
  onConnectRef.current = onConnect;
1072
1075
  onErrorRef.current = onError;
1073
1076
  onQrUriRef.current = onQrUri;
1077
+ onSessionExpiredRef.current = onSessionExpired;
1074
1078
  }, [
1075
1079
  onConnect,
1076
1080
  onError,
1077
- onQrUri
1081
+ onQrUri,
1082
+ onSessionExpired
1078
1083
  ]);
1079
1084
  const { connectAsync, reset, isPending } = useConnect();
1080
1085
  useEffect(() => {
1081
1086
  const handler = /* @__PURE__ */ __name((data) => {
1087
+ var _a;
1082
1088
  const msg = data;
1083
1089
  if ((msg == null ? void 0 : msg.type) === "display_uri" && typeof msg.data === "string") {
1084
1090
  onQrUriRef.current(msg.data);
1085
1091
  }
1092
+ if ((msg == null ? void 0 : msg.type) === "session_expired") {
1093
+ (_a = onSessionExpiredRef.current) == null ? void 0 : _a.call(onSessionExpiredRef);
1094
+ }
1086
1095
  }, "handler");
1087
1096
  const unsubscribers = config.connectors.map((connector) => {
1088
1097
  connector.emitter.on("message", handler);
@@ -1145,8 +1154,58 @@ var useWagmiConnect = /* @__PURE__ */ __name(({ onConnect, onError, onQrUri }) =
1145
1154
  };
1146
1155
  }, "useWagmiConnect");
1147
1156
 
1157
+ // src/hooks/useConnectTimeout.ts
1158
+ import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
1159
+ var DEFAULT_CONNECT_TIMEOUT_MS = 6e5;
1160
+ var useConnectTimeout = /* @__PURE__ */ __name(({ connectorId, active, expiredSignal = 0, onRetry }) => {
1161
+ const [timedOut, setTimedOut] = useState2(false);
1162
+ const fire = useCallback2(() => {
1163
+ setTimedOut(true);
1164
+ }, []);
1165
+ useEffect2(() => {
1166
+ setTimedOut(false);
1167
+ }, [
1168
+ connectorId
1169
+ ]);
1170
+ useEffect2(() => {
1171
+ if (!active || connectorId !== "METAMASK") return;
1172
+ const timer = setTimeout(fire, DEFAULT_CONNECT_TIMEOUT_MS);
1173
+ return () => clearTimeout(timer);
1174
+ }, [
1175
+ active,
1176
+ connectorId,
1177
+ fire
1178
+ ]);
1179
+ const prevSignal = useRef2(expiredSignal);
1180
+ useEffect2(() => {
1181
+ if (!active || connectorId === "METAMASK" || !connectorId) {
1182
+ prevSignal.current = expiredSignal;
1183
+ return;
1184
+ }
1185
+ if (expiredSignal !== prevSignal.current) {
1186
+ prevSignal.current = expiredSignal;
1187
+ fire();
1188
+ }
1189
+ }, [
1190
+ expiredSignal,
1191
+ connectorId,
1192
+ active,
1193
+ fire
1194
+ ]);
1195
+ const retry = useCallback2(() => {
1196
+ setTimedOut(false);
1197
+ onRetry == null ? void 0 : onRetry();
1198
+ }, [
1199
+ onRetry
1200
+ ]);
1201
+ return {
1202
+ timedOut,
1203
+ retry
1204
+ };
1205
+ }, "useConnectTimeout");
1206
+
1148
1207
  // src/hooks/useWCState.ts
1149
- import { useState as useState2, useRef as useRef2, useCallback as useCallback2, useEffect as useEffect2 } from "react";
1208
+ import { useState as useState3, useRef as useRef3, useCallback as useCallback3, useEffect as useEffect3 } from "react";
1150
1209
  var WC_EXPLORER_CREDIT_WALLET_NAME = "Credit Wallet";
1151
1210
  var sortCreditWalletFirst = /* @__PURE__ */ __name((list) => [
1152
1211
  ...list
@@ -1193,16 +1252,16 @@ var fetchWCWalletList = /* @__PURE__ */ __name(async (projectId, signal) => {
1193
1252
  return Array.from(byId.values());
1194
1253
  }, "fetchWCWalletList");
1195
1254
  var useWCState = /* @__PURE__ */ __name(() => {
1196
- const [subView, setSubView] = useState2("qr");
1197
- const [walletList, setWalletList] = useState2([]);
1198
- const [walletListLoading, setWalletListLoading] = useState2(false);
1199
- const [walletListSearch, setWalletListSearch] = useState2("");
1200
- const [filterActive, setFilterActive] = useState2(false);
1201
- const [selectedWallet, setSelectedWallet] = useState2(null);
1202
- const loadedRef = useRef2(false);
1203
- const abortControllerRef = useRef2(null);
1204
- const isMountedRef = useRef2(true);
1205
- useEffect2(() => {
1255
+ const [subView, setSubView] = useState3("qr");
1256
+ const [walletList, setWalletList] = useState3([]);
1257
+ const [walletListLoading, setWalletListLoading] = useState3(false);
1258
+ const [walletListSearch, setWalletListSearch] = useState3("");
1259
+ const [filterActive, setFilterActive] = useState3(false);
1260
+ const [selectedWallet, setSelectedWallet] = useState3(null);
1261
+ const loadedRef = useRef3(false);
1262
+ const abortControllerRef = useRef3(null);
1263
+ const isMountedRef = useRef3(true);
1264
+ useEffect3(() => {
1206
1265
  isMountedRef.current = true;
1207
1266
  return () => {
1208
1267
  var _a;
@@ -1211,7 +1270,7 @@ var useWCState = /* @__PURE__ */ __name(() => {
1211
1270
  abortControllerRef.current = null;
1212
1271
  };
1213
1272
  }, []);
1214
- const loadWalletList = useCallback2(async (projectId) => {
1273
+ const loadWalletList = useCallback3(async (projectId) => {
1215
1274
  if (loadedRef.current) return;
1216
1275
  loadedRef.current = true;
1217
1276
  const controller = new AbortController();
@@ -1229,14 +1288,14 @@ var useWCState = /* @__PURE__ */ __name(() => {
1229
1288
  if (abortControllerRef.current === controller) abortControllerRef.current = null;
1230
1289
  }
1231
1290
  }, []);
1232
- const onShowList = useCallback2(() => setSubView("list"), []);
1233
- const onSelectWallet = useCallback2((wallet) => {
1291
+ const onShowList = useCallback3(() => setSubView("list"), []);
1292
+ const onSelectWallet = useCallback3((wallet) => {
1234
1293
  setSelectedWallet(wallet);
1235
1294
  setSubView("wallet");
1236
1295
  }, []);
1237
- const onSearchChange = useCallback2((q) => setWalletListSearch(q), []);
1238
- const onFilterToggle = useCallback2(() => setFilterActive((prev) => !prev), []);
1239
- const handleBack = useCallback2(() => {
1296
+ const onSearchChange = useCallback3((q) => setWalletListSearch(q), []);
1297
+ const onFilterToggle = useCallback3(() => setFilterActive((prev) => !prev), []);
1298
+ const handleBack = useCallback3(() => {
1240
1299
  if (subView === "wallet") {
1241
1300
  setSelectedWallet(null);
1242
1301
  setSubView("list");
@@ -1250,11 +1309,11 @@ var useWCState = /* @__PURE__ */ __name(() => {
1250
1309
  }, [
1251
1310
  subView
1252
1311
  ]);
1253
- const resetView = useCallback2(() => {
1312
+ const resetView = useCallback3(() => {
1254
1313
  setSubView("qr");
1255
1314
  setSelectedWallet(null);
1256
1315
  }, []);
1257
- const reset = useCallback2(() => {
1316
+ const reset = useCallback3(() => {
1258
1317
  setSubView("qr");
1259
1318
  setWalletListSearch("");
1260
1319
  setFilterActive(false);
@@ -1302,6 +1361,7 @@ var getContentForConnector = /* @__PURE__ */ __name((id) => {
1302
1361
  if (id === "WALLET_CONNECT") return "walletconnect";
1303
1362
  return "idle";
1304
1363
  }, "getContentForConnector");
1364
+ var getInitialContentForConnector = /* @__PURE__ */ __name((id) => id === "METAMASK" && !detectMetaMaskExtension() ? "metamask-qr" : getContentForConnector(id), "getInitialContentForConnector");
1305
1365
  var ConnectDialog = /* @__PURE__ */ __name(({
1306
1366
  open,
1307
1367
  theme = "dark",
@@ -1320,24 +1380,59 @@ var ConnectDialog = /* @__PURE__ */ __name(({
1320
1380
  onConnect,
1321
1381
  onError
1322
1382
  }) => {
1323
- const dialogRef = useRef3(null);
1324
- const [selectedConnector, setSelectedConnector] = useState3(selectedConnectorProp);
1325
- const [recentConnector, setRecentConnector] = useState3(null);
1326
- const [selectedWallet, setSelectedWallet] = useState3(null);
1327
- const [step, setStep] = useState3(stepProp);
1328
- const [content, setContent] = useState3("idle");
1329
- const [showCopySnackbar, setShowCopySnackbar] = useState3(false);
1330
- const [qrUri, setQrUri] = useState3(null);
1383
+ const dialogRef = useRef4(null);
1384
+ const [selectedConnector, setSelectedConnector] = useState4(selectedConnectorProp);
1385
+ const [recentConnector, setRecentConnector] = useState4(null);
1386
+ const [selectedWallet, setSelectedWallet] = useState4(null);
1387
+ const [step, setStep] = useState4(stepProp);
1388
+ const [content, setContent] = useState4("idle");
1389
+ const [showCopySnackbar, setShowCopySnackbar] = useState4(false);
1390
+ const [qrUri, setQrUri] = useState4(null);
1391
+ const [expiredSignal, setExpiredSignal] = useState4(0);
1392
+ const contentRef = useRef4(content);
1393
+ contentRef.current = content;
1394
+ const preTimeoutContentRef = useRef4("idle");
1331
1395
  const config = useConfig2();
1332
1396
  const { triggerConnect, cancelConnect } = useWagmiConnect({
1333
1397
  onConnect: /* @__PURE__ */ __name((result) => {
1334
1398
  setRecentConnector(result.connectorId);
1335
1399
  onConnect == null ? void 0 : onConnect(result);
1336
1400
  }, "onConnect"),
1337
- onError: /* @__PURE__ */ __name((err, connectorId) => onError == null ? void 0 : onError(err, connectorId), "onError"),
1338
- onQrUri: /* @__PURE__ */ __name((uri) => setQrUri(uri), "onQrUri")
1401
+ onError: /* @__PURE__ */ __name((err, connectorId) => {
1402
+ if (connectorId === "WALLET_CONNECT") {
1403
+ setExpiredSignal((n) => n + 1);
1404
+ }
1405
+ onError == null ? void 0 : onError(err, connectorId);
1406
+ }, "onError"),
1407
+ onQrUri: /* @__PURE__ */ __name((uri) => setQrUri(uri), "onQrUri"),
1408
+ // CC connector 가 서버 OnSessionExpired 를 릴레이 → 만료 신호 증가.
1409
+ onSessionExpired: /* @__PURE__ */ __name(() => setExpiredSignal((n) => n + 1), "onSessionExpired")
1339
1410
  });
1340
- useEffect3(() => {
1411
+ const timeoutActive = step === "detail" && !!selectedConnector && content !== "timeout";
1412
+ const { timedOut, retry } = useConnectTimeout({
1413
+ connectorId: selectedConnector,
1414
+ active: timeoutActive,
1415
+ expiredSignal,
1416
+ onRetry: /* @__PURE__ */ __name(() => {
1417
+ if (!selectedConnector) return;
1418
+ cancelConnect();
1419
+ setQrUri(null);
1420
+ const restore = preTimeoutContentRef.current;
1421
+ const nextContent = restore !== "idle" && restore !== "timeout" ? restore : getInitialContentForConnector(selectedConnector);
1422
+ setContent(nextContent);
1423
+ triggerConnect(selectedConnector);
1424
+ }, "onRetry")
1425
+ });
1426
+ useEffect4(() => {
1427
+ if (!timedOut) return;
1428
+ preTimeoutContentRef.current = contentRef.current;
1429
+ cancelConnect();
1430
+ setContent("timeout");
1431
+ }, [
1432
+ timedOut,
1433
+ cancelConnect
1434
+ ]);
1435
+ useEffect4(() => {
1341
1436
  const restoreRecent = /* @__PURE__ */ __name(async () => {
1342
1437
  var _a;
1343
1438
  const wagmiId = await ((_a = config.storage) == null ? void 0 : _a.getItem("recentConnectorId"));
@@ -1351,8 +1446,7 @@ var ConnectDialog = /* @__PURE__ */ __name(({
1351
1446
  const wc = useWCState();
1352
1447
  const handleSelect = /* @__PURE__ */ __name((id) => {
1353
1448
  setSelectedConnector(id);
1354
- const initialContent = id === "METAMASK" && !detectMetaMaskExtension() ? "metamask-qr" : getContentForConnector(id);
1355
- setContent(initialContent);
1449
+ setContent(getInitialContentForConnector(id));
1356
1450
  setStep("detail");
1357
1451
  triggerConnect(id);
1358
1452
  onSelectConnector == null ? void 0 : onSelectConnector(id);
@@ -1384,7 +1478,7 @@ var ConnectDialog = /* @__PURE__ */ __name(({
1384
1478
  wc.loadWalletList(wcProjectId).catch(() => void 0);
1385
1479
  }
1386
1480
  }, "handleShowAllWallets");
1387
- useEffect3(() => {
1481
+ useEffect4(() => {
1388
1482
  const dialog = dialogRef.current;
1389
1483
  if (!dialog) return;
1390
1484
  if (open && !dialog.open) dialog.showModal();
@@ -1392,13 +1486,13 @@ var ConnectDialog = /* @__PURE__ */ __name(({
1392
1486
  }, [
1393
1487
  open
1394
1488
  ]);
1395
- const deeplinkTriedRef = useRef3(false);
1396
- useEffect3(() => {
1489
+ const deeplinkTriedRef = useRef4(false);
1490
+ useEffect4(() => {
1397
1491
  deeplinkTriedRef.current = false;
1398
1492
  }, [
1399
1493
  selectedWallet
1400
1494
  ]);
1401
- useEffect3(() => {
1495
+ useEffect4(() => {
1402
1496
  if (deeplinkTriedRef.current) return;
1403
1497
  if (!selectedWallet || !qrUri || content !== "wallet-qr") return;
1404
1498
  if (!isMobileDevice()) return;
@@ -1411,13 +1505,13 @@ var ConnectDialog = /* @__PURE__ */ __name(({
1411
1505
  qrUri,
1412
1506
  content
1413
1507
  ]);
1414
- const creditConnectDeeplinkTriedRef = useRef3(false);
1415
- useEffect3(() => {
1508
+ const creditConnectDeeplinkTriedRef = useRef4(false);
1509
+ useEffect4(() => {
1416
1510
  creditConnectDeeplinkTriedRef.current = false;
1417
1511
  }, [
1418
1512
  selectedConnector
1419
1513
  ]);
1420
- useEffect3(() => {
1514
+ useEffect4(() => {
1421
1515
  if (creditConnectDeeplinkTriedRef.current) return;
1422
1516
  if (content !== "qr" || !qrUri) return;
1423
1517
  if (selectedConnector !== "CREDIT_CONNECT") return;
@@ -1429,7 +1523,7 @@ var ConnectDialog = /* @__PURE__ */ __name(({
1429
1523
  qrUri,
1430
1524
  selectedConnector
1431
1525
  ]);
1432
- useEffect3(() => {
1526
+ useEffect4(() => {
1433
1527
  const prev = document.body.style.overflow;
1434
1528
  if (open) document.body.style.overflow = "hidden";
1435
1529
  return () => {
@@ -1438,7 +1532,7 @@ var ConnectDialog = /* @__PURE__ */ __name(({
1438
1532
  }, [
1439
1533
  open
1440
1534
  ]);
1441
- useEffect3(() => {
1535
+ useEffect4(() => {
1442
1536
  if (!open) {
1443
1537
  cancelConnect();
1444
1538
  setQrUri(null);
@@ -1451,7 +1545,7 @@ var ConnectDialog = /* @__PURE__ */ __name(({
1451
1545
  open,
1452
1546
  cancelConnect
1453
1547
  ]);
1454
- useEffect3(() => {
1548
+ useEffect4(() => {
1455
1549
  const dialog = dialogRef.current;
1456
1550
  if (!dialog) return void 0;
1457
1551
  const handleCancel = /* @__PURE__ */ __name((e) => {
@@ -1492,6 +1586,7 @@ var ConnectDialog = /* @__PURE__ */ __name(({
1492
1586
  walletCount: wc.walletList.length,
1493
1587
  onBack: handleBack,
1494
1588
  onClose,
1589
+ onRetry: retry,
1495
1590
  onShowAllWallets: handleShowAllWallets,
1496
1591
  onSelectWallet: handleSelectWallet,
1497
1592
  onCopyLink: /* @__PURE__ */ __name(() => {
@@ -1538,32 +1633,32 @@ var ConnectKitProvider = /* @__PURE__ */ __name((props) => {
1538
1633
  }, "ConnectKitProvider");
1539
1634
  var ConnectKitProviderInner = /* @__PURE__ */ __name(({ children, connectors, kitConfig, requiredChainId, theme, onConnect, onLog }) => {
1540
1635
  var _a;
1541
- const [isOpen, setIsOpen] = useState4(false);
1542
- const [pendingResult, setPendingResult] = useState4(null);
1636
+ const [isOpen, setIsOpen] = useState5(false);
1637
+ const [pendingResult, setPendingResult] = useState5(null);
1543
1638
  const account = useAccount();
1544
1639
  const wagmiConfig = useConfig3();
1545
1640
  const { disconnectAsync } = useDisconnect();
1546
1641
  const { switchChainAsync } = useSwitchChain();
1547
- const onConnectRef = useRef4(onConnect);
1548
- useEffect4(() => {
1642
+ const onConnectRef = useRef5(onConnect);
1643
+ useEffect5(() => {
1549
1644
  onConnectRef.current = onConnect;
1550
1645
  }, [
1551
1646
  onConnect
1552
1647
  ]);
1553
- const isMountedRef = useRef4(true);
1554
- useEffect4(() => {
1648
+ const isMountedRef = useRef5(true);
1649
+ useEffect5(() => {
1555
1650
  isMountedRef.current = true;
1556
1651
  return () => {
1557
1652
  isMountedRef.current = false;
1558
1653
  };
1559
1654
  }, []);
1560
1655
  const needsChainSwitch = requiredChainId != null && account.status === "connected" && account.chainId != null && account.chainId !== requiredChainId;
1561
- const [isSwitchingChain, setIsSwitchingChain] = useState4(false);
1656
+ const [isSwitchingChain, setIsSwitchingChain] = useState5(false);
1562
1657
  const effectiveOpen = isOpen;
1563
- const open = useCallback3(() => {
1658
+ const open = useCallback4(() => {
1564
1659
  setIsOpen(true);
1565
1660
  }, []);
1566
- const close = useCallback3(() => {
1661
+ const close = useCallback4(() => {
1567
1662
  setIsOpen(false);
1568
1663
  const status = getAccount2(wagmiConfig).status;
1569
1664
  if (status === "connecting" || status === "reconnecting") {
@@ -1573,12 +1668,12 @@ var ConnectKitProviderInner = /* @__PURE__ */ __name(({ children, connectors, ki
1573
1668
  wagmiConfig,
1574
1669
  disconnectAsync
1575
1670
  ]);
1576
- const disconnect = useCallback3(async () => {
1671
+ const disconnect = useCallback4(async () => {
1577
1672
  await disconnectAsync();
1578
1673
  }, [
1579
1674
  disconnectAsync
1580
1675
  ]);
1581
- const switchToRequiredChain = useCallback3(async () => {
1676
+ const switchToRequiredChain = useCallback4(async () => {
1582
1677
  if (requiredChainId == null) return;
1583
1678
  setIsSwitchingChain(true);
1584
1679
  try {
@@ -1592,15 +1687,15 @@ var ConnectKitProviderInner = /* @__PURE__ */ __name(({ children, connectors, ki
1592
1687
  requiredChainId,
1593
1688
  switchChainAsync
1594
1689
  ]);
1595
- const handleDialogConnect = useCallback3((result) => {
1690
+ const handleDialogConnect = useCallback4((result) => {
1596
1691
  setPendingResult(result);
1597
1692
  }, []);
1598
- const handleDialogError = useCallback3((error, connectorId) => {
1693
+ const handleDialogError = useCallback4((error, connectorId) => {
1599
1694
  onLog == null ? void 0 : onLog(`[connect-kit] connect failed: ${connectorId} \u2014 ${error.message}`, error);
1600
1695
  }, [
1601
1696
  onLog
1602
1697
  ]);
1603
- useEffect4(() => {
1698
+ useEffect5(() => {
1604
1699
  if (!effectiveOpen) return;
1605
1700
  if (account.status !== "connected") return;
1606
1701
  setIsOpen(false);
@@ -1608,7 +1703,7 @@ var ConnectKitProviderInner = /* @__PURE__ */ __name(({ children, connectors, ki
1608
1703
  effectiveOpen,
1609
1704
  account.status
1610
1705
  ]);
1611
- useEffect4(() => {
1706
+ useEffect5(() => {
1612
1707
  var _a2;
1613
1708
  if (!pendingResult) return;
1614
1709
  if (account.status !== "connected") {
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gluwa/connect-kit",
3
- "version": "0.2.0-next.7",
3
+ "version": "0.2.0-next.8",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gluwa/connect-kit",
3
- "version": "0.2.0-next.7",
3
+ "version": "0.2.0-next.8",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -26,10 +26,10 @@
26
26
  "qrcode.react": "^4.2.0",
27
27
  "wagmi": "^2.15.6",
28
28
  "@wagmi/core": "^2.16.7",
29
- "@gluwa/credit-connect-sdk": "0.2.0-next.3",
30
- "@gluwa/credit-connect-storage-local": "0.2.0-next.3",
31
- "@gluwa/credit-connect-e2ee-tweetnacl": "0.2.0-next.3",
32
- "@gluwa/credit-connect-hub-node-ws": "0.1.0-next.3"
29
+ "@gluwa/credit-connect-sdk": "0.2.0-next.4",
30
+ "@gluwa/credit-connect-storage-local": "0.2.0-next.4",
31
+ "@gluwa/credit-connect-e2ee-tweetnacl": "0.2.0-next.4",
32
+ "@gluwa/credit-connect-hub-node-ws": "0.1.0-next.4"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@tanstack/react-query": "^5.62.0",
@@ -43,7 +43,7 @@
43
43
  "typescript-eslint": "^8.18.2",
44
44
  "viem": "^2.31.7",
45
45
  "wagmi": "^2.15.6",
46
- "@gluwa/credit-connect-sdk": "0.2.0-next.3"
46
+ "@gluwa/credit-connect-sdk": "0.2.0-next.4"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@tanstack/react-query": ">=5.0.0",
@@ -107,6 +107,12 @@ export const creditConnectConnector = (
107
107
  }
108
108
  });
109
109
 
110
+ manager.on('sessionExpired', (payload) => {
111
+ // eslint-disable-next-line no-console
112
+ console.log('[CC connector] session expired → emit session_expired', payload);
113
+ emitWagmiMessage?.('session_expired', payload);
114
+ });
115
+
110
116
  return manager;
111
117
  };
112
118
 
@@ -0,0 +1,82 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { type ConnectorId } from '../types';
3
+
4
+ // MM / CC 폴백용 기본 타임아웃. (CC 는 원칙적으로 서버 push 로 만료가 오지만,
5
+ // 서버 값이 없거나 신호가 유실된 경우를 대비한 폴백 용도로도 쓸 수 있음.)
6
+ export const DEFAULT_CONNECT_TIMEOUT_MS = 600_000;
7
+
8
+ interface UseConnectTimeoutParams {
9
+ /** 현재 연결 시도 중인 커넥터. null = 유휴 상태. */
10
+ connectorId: ConnectorId | null;
11
+ /** 타임아웃 감시 활성화 여부 — pending/QR 표시 중일 때 true. */
12
+ active: boolean;
13
+ /**
14
+ * 외부 이벤트 기반 만료 신호 (MM 제외). 값이 증가할 때마다 "만료 발생" 으로 간주.
15
+ * - CC: dialog 가 connector 의 `session_expired`(서버 push) 를 받아 올림.
16
+ * - WC: dialog 가 WC connect promise reject(프로토콜 ~5분 만료 등) 를 받아 올림.
17
+ */
18
+ expiredSignal?: number;
19
+ /** "Try Again" 시 실행할 재연결 로직 (dialog 가 주입). */
20
+ onRetry?: () => void;
21
+ }
22
+
23
+ interface UseConnectTimeoutResult {
24
+ /** 타임아웃 발생 여부 — true 면 dialog 가 <Timeout> 을 렌더. */
25
+ timedOut: boolean;
26
+ /** "Try Again" 버튼에 연결할 함수 — 상태 리셋 + onRetry 실행. */
27
+ retry: () => void;
28
+ }
29
+
30
+ /**
31
+ * 커넥터별로 타임아웃 신호원을 분기해 하나의 `timedOut` 으로 수렴시키는 훅.
32
+ * - CC: 서버 push (`OnSessionExpired` → connector → expiredSignal).
33
+ * - MM: 네이티브 타임아웃 없음 → client 타이머(DEFAULT_CONNECT_TIMEOUT_MS).
34
+ * - WC: 프로토콜 자체 ~5분 proposal reject → connect promise reject → expiredSignal.
35
+ */
36
+ export const useConnectTimeout = ({
37
+ connectorId,
38
+ active,
39
+ expiredSignal = 0,
40
+ onRetry,
41
+ }: UseConnectTimeoutParams): UseConnectTimeoutResult => {
42
+ const [timedOut, setTimedOut] = useState(false);
43
+
44
+ const fire = useCallback(() => {
45
+ setTimedOut(true);
46
+ }, []);
47
+
48
+ // 커넥터가 바뀌면(=새 연결 시도) 타임아웃 상태 리셋.
49
+ // active 가 false 로 떨어질 때는 리셋하지 않는다 — timeout UI 표시 중 상태가 날아가지 않도록.
50
+ useEffect(() => {
51
+ setTimedOut(false);
52
+ }, [connectorId]);
53
+
54
+ // MM 만 client 타이머로 강제 (승인 대기에 네이티브 상한이 없음).
55
+ // CC(서버 push) / WC(connect reject) 는 아래 expiredSignal effect 에서 처리.
56
+ useEffect(() => {
57
+ if (!active || connectorId !== 'METAMASK') return;
58
+ const timer = setTimeout(fire, DEFAULT_CONNECT_TIMEOUT_MS);
59
+ return () => clearTimeout(timer);
60
+ }, [active, connectorId, fire]);
61
+
62
+ // 외부 만료 신호 처리 (CC 서버 push / WC connect reject) — MM 은 client 타이머라 제외.
63
+ // signal 값이 올라가면(새 만료 이벤트) 발화.
64
+ const prevSignal = useRef(expiredSignal);
65
+ useEffect(() => {
66
+ if (!active || connectorId === 'METAMASK' || !connectorId) {
67
+ prevSignal.current = expiredSignal;
68
+ return;
69
+ }
70
+ if (expiredSignal !== prevSignal.current) {
71
+ prevSignal.current = expiredSignal;
72
+ fire();
73
+ }
74
+ }, [expiredSignal, connectorId, active, fire]);
75
+
76
+ const retry = useCallback(() => {
77
+ setTimedOut(false);
78
+ onRetry?.();
79
+ }, [onRetry]);
80
+
81
+ return { timedOut, retry };
82
+ };
@@ -61,6 +61,7 @@ interface Options {
61
61
  onConnect: (result: ConnectResult) => void;
62
62
  onError: (error: Error, connectorId: ConnectorId) => void;
63
63
  onQrUri: (uri: string | null) => void;
64
+ onSessionExpired?: () => void;
64
65
  }
65
66
 
66
67
  interface UseWagmiConnectResult {
@@ -73,6 +74,7 @@ export const useWagmiConnect = ({
73
74
  onConnect,
74
75
  onError,
75
76
  onQrUri,
77
+ onSessionExpired,
76
78
  }: Options): UseWagmiConnectResult => {
77
79
  const config = useConfig();
78
80
  const pendingConnectorId = useRef<ConnectorId | null>(null);
@@ -81,11 +83,13 @@ export const useWagmiConnect = ({
81
83
  const onConnectRef = useRef(onConnect);
82
84
  const onErrorRef = useRef(onError);
83
85
  const onQrUriRef = useRef(onQrUri);
86
+ const onSessionExpiredRef = useRef(onSessionExpired);
84
87
  useEffect(() => {
85
88
  onConnectRef.current = onConnect;
86
89
  onErrorRef.current = onError;
87
90
  onQrUriRef.current = onQrUri;
88
- }, [onConnect, onError, onQrUri]);
91
+ onSessionExpiredRef.current = onSessionExpired;
92
+ }, [onConnect, onError, onQrUri, onSessionExpired]);
89
93
 
90
94
  const { connectAsync, reset, isPending } = useConnect();
91
95
 
@@ -95,6 +99,9 @@ export const useWagmiConnect = ({
95
99
  if (msg?.type === 'display_uri' && typeof msg.data === 'string') {
96
100
  onQrUriRef.current(msg.data);
97
101
  }
102
+ if (msg?.type === 'session_expired') {
103
+ onSessionExpiredRef.current?.();
104
+ }
98
105
  };
99
106
 
100
107
  const unsubscribers = config.connectors.map((connector) => {
@@ -13,6 +13,7 @@ import { DetailPanel } from '../detailPanel';
13
13
  import { Snackbar } from '../../components/snackbar';
14
14
  import { resolveConnectorId } from '../../connector-meta';
15
15
  import { useWagmiConnect } from '../../hooks/useWagmiConnect';
16
+ import { useConnectTimeout } from '../../hooks/useConnectTimeout';
16
17
  import { useWCState } from '../../hooks/useWCState';
17
18
  import {
18
19
  detectMetaMaskExtension,
@@ -47,6 +48,10 @@ const getContentForConnector = (id: ConnectorId): DetailContent => {
47
48
  return 'idle';
48
49
  };
49
50
 
51
+ // MM 은 확장 감지 시 즉시 extension 연결, 아니면 QR. (select / retry 공통)
52
+ const getInitialContentForConnector = (id: ConnectorId): DetailContent =>
53
+ id === 'METAMASK' && !detectMetaMaskExtension() ? 'metamask-qr' : getContentForConnector(id);
54
+
50
55
  export const ConnectDialog: FC<ConnectDialogProps> = ({
51
56
  open,
52
57
  theme = 'dark',
@@ -83,6 +88,14 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
83
88
  const [content, setContent] = useState<DetailContent>('idle');
84
89
  const [showCopySnackbar, setShowCopySnackbar] = useState(false);
85
90
  const [qrUri, setQrUri] = useState<string | null>(null);
91
+ // 외부 만료 신호 카운터 — CC(서버 push session_expired) / WC(connect reject) 수신 시 증가.
92
+ const [expiredSignal, setExpiredSignal] = useState(0);
93
+
94
+ // 현재 content 를 ref 로 추적 (effect deps 없이 최신값 참조용).
95
+ const contentRef = useRef<DetailContent>(content);
96
+ contentRef.current = content;
97
+ // 타임아웃 직전 화면 — retry 시 이 화면으로 복원 (예: WC 의 wallet-qr).
98
+ const preTimeoutContentRef = useRef<DetailContent>('idle');
86
99
 
87
100
  const config = useConfig();
88
101
 
@@ -95,10 +108,50 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
95
108
  setRecentConnector(result.connectorId);
96
109
  onConnect?.(result);
97
110
  },
98
- onError: (err, connectorId) => onError?.(err, connectorId),
111
+ onError: (err, connectorId) => {
112
+ // WC: connect reject(사용자 거절 / 프로토콜 ~5분 만료 / 네트워크 등)는
113
+ // 모두 timeout UI("Connection Failed")로 통일.
114
+ if (connectorId === 'WALLET_CONNECT') {
115
+ setExpiredSignal((n) => n + 1);
116
+ }
117
+ onError?.(err, connectorId);
118
+ },
99
119
  onQrUri: (uri) => setQrUri(uri),
120
+ // CC connector 가 서버 OnSessionExpired 를 릴레이 → 만료 신호 증가.
121
+ onSessionExpired: () => setExpiredSignal((n) => n + 1),
100
122
  });
101
123
 
124
+ // 연결 타임아웃 (CC=서버 push / MM=client 타이머 / WC=connect reject) → timedOut.
125
+ const timeoutActive = step === 'detail' && !!selectedConnector && content !== 'timeout';
126
+ const { timedOut, retry } = useConnectTimeout({
127
+ connectorId: selectedConnector,
128
+ active: timeoutActive,
129
+ expiredSignal,
130
+ onRetry: () => {
131
+ if (!selectedConnector) return;
132
+ cancelConnect();
133
+ setQrUri(null);
134
+ // 타임아웃 직전 화면으로 복원 (예: WC 의 wallet-qr → 선택 지갑 QR 유지).
135
+ // idle/timeout 처럼 복원 불가한 값이면 커넥터 초기 화면으로.
136
+ const restore = preTimeoutContentRef.current;
137
+ const nextContent: DetailContent =
138
+ restore !== 'idle' && restore !== 'timeout'
139
+ ? restore
140
+ : getInitialContentForConnector(selectedConnector);
141
+ setContent(nextContent);
142
+ triggerConnect(selectedConnector);
143
+ },
144
+ });
145
+
146
+ // 타임아웃 발생 → 진행 중 연결 취소 + timeout 화면 표시.
147
+ // 전환 전 화면을 저장해 retry 시 복원 (WC wallet-qr 등).
148
+ useEffect(() => {
149
+ if (!timedOut) return;
150
+ preTimeoutContentRef.current = contentRef.current;
151
+ cancelConnect();
152
+ setContent('timeout');
153
+ }, [timedOut, cancelConnect]);
154
+
102
155
  // 마운트 시 wagmi storage 의 recentConnectorId(연결 성공 시 wagmi 가 저장) 복원.
103
156
  useEffect(() => {
104
157
  const restoreRecent = async (): Promise<void> => {
@@ -115,10 +168,7 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
115
168
  const handleSelect = (id: ConnectorId) => {
116
169
  setSelectedConnector(id);
117
170
  // recent 는 클릭이 아니라 연결 성공(onConnect) 시점에 기록 — 위 useWagmiConnect onConnect 참조.
118
- // MetaMask 는 확장이 있으면 즉시 extension 연결, 아니면 QR.
119
- const initialContent: DetailContent =
120
- id === 'METAMASK' && !detectMetaMaskExtension() ? 'metamask-qr' : getContentForConnector(id);
121
- setContent(initialContent);
171
+ setContent(getInitialContentForConnector(id));
122
172
  setStep('detail');
123
173
  // wagmi connect 시작 (display_uri 도착 시 setQrUri 자동 호출)
124
174
  triggerConnect(id);
@@ -261,6 +311,7 @@ export const ConnectDialog: FC<ConnectDialogProps> = ({
261
311
  walletCount={wc.walletList.length}
262
312
  onBack={handleBack}
263
313
  onClose={onClose}
314
+ onRetry={retry}
264
315
  onShowAllWallets={handleShowAllWallets}
265
316
  onSelectWallet={handleSelectWallet}
266
317
  onCopyLink={() => {
@@ -40,6 +40,8 @@ interface DetailPanelProps {
40
40
  onShowAllWallets?: () => void;
41
41
  onSelectWallet?: (wallet: WCWallet) => void;
42
42
  onCopyLink?: () => void;
43
+ /** timeout 화면의 "Try Again" 버튼 핸들러. */
44
+ onRetry?: () => void;
43
45
  }
44
46
 
45
47
  const getTitle = (content: DetailContent, connector: ConnectorId | null): string => {
@@ -78,6 +80,7 @@ export const DetailPanel: FC<DetailPanelProps> = ({
78
80
  onShowAllWallets,
79
81
  onSelectWallet,
80
82
  onCopyLink,
83
+ onRetry,
81
84
  }) => {
82
85
  // CONNECTOR_META 에서 활성 connector 의 logo / downloadUrl 가져옴.
83
86
  const connectorMeta = selectedConnector ? CONNECTOR_META[selectedConnector] : null;
@@ -112,7 +115,7 @@ export const DetailPanel: FC<DetailPanelProps> = ({
112
115
  {content === 'qr' && (
113
116
  <QR uri={qrUri} logoUrl={connectorLogoUrl} downloadUrl={connectorDownloadUrl} />
114
117
  )}
115
- {content === 'timeout' && <Timeout />}
118
+ {content === 'timeout' && <Timeout onRetry={onRetry} />}
116
119
  {(content === 'metamask-ext' || content === 'metamask-qr') && (
117
120
  <MetaMask
118
121
  variant={content === 'metamask-ext' ? 'extension' : 'qr'}