Browse Source

代码优化

fattiger 3 weeks ago
parent
commit
b1e6c04949
100 changed files with 1835 additions and 1613 deletions
  1. 40 0
      craco.config.js
  2. 11 7
      package-lock.json
  3. 2 0
      package.json
  4. 1 1
      public/download.html
  5. 8 6
      public/index.html
  6. 10 3
      public/ios-download.html
  7. 2 2
      public/js/anonymous.js
  8. 1 8
      public/js/com.devtool.js
  9. 4 4
      public/js/dmig.js
  10. 14 4
      public/js/load.m.js
  11. 57 70
      public/js/pixel_h5.js
  12. 142 74
      public/js/push_manager.js
  13. 67 48
      public/pwa/service-work.js
  14. 1 1
      public/pwa/startup.html
  15. 9 7
      public/service-worker.js
  16. 25 8
      scripts/generate-sites.js
  17. 56 48
      src/App.js
  18. 62 19
      src/Global.js
  19. 61 43
      src/api/request.js
  20. 2 2
      src/components/AutoBanner/BannerPagination.jsx
  21. 19 17
      src/components/AutoBanner/index.jsx
  22. 23 24
      src/components/AutoWinList/index.jsx
  23. 2 2
      src/components/CheckBox/index.jsx
  24. 31 9
      src/components/CountDown/CountDown.jsx
  25. 38 69
      src/components/CountDown/CountDownText.jsx
  26. 13 7
      src/components/CountDown/ExpireTimesText.jsx
  27. 3 2
      src/components/DraggableNode/index.jsx
  28. 11 10
      src/components/DropCoins.jsx
  29. 12 11
      src/components/GameList/index.jsx
  30. 2 2
      src/components/GameRoom/index.jsx
  31. 16 16
      src/components/GameSmallList/index.jsx
  32. 20 34
      src/components/GameTabs/index.jsx
  33. 4 1
      src/components/Loading/LoadingCircular.jsx
  34. 1 3
      src/components/Logo/index.jsx
  35. 4 4
      src/components/LottieEx.jsx
  36. 7 2
      src/components/MailNew.jsx
  37. 20 14
      src/components/MobileCodeInput/index.jsx
  38. 2 5
      src/components/MsgBox/index.jsx
  39. 4 2
      src/components/Popup.jsx
  40. 17 7
      src/components/SubScreen/index.jsx
  41. 1 1
      src/components/SvgIcon/index.jsx
  42. 1 2
      src/components/TabIcon/index.jsx
  43. 3 1
      src/components/TransText/index.jsx
  44. 4 12
      src/components/TruncatedText.jsx
  45. 10 30
      src/components/UInput/MultiNumPassInput.jsx
  46. 18 11
      src/components/UInput/PhoneInput.jsx
  47. 1 1
      src/constants/giftPacks.js
  48. 0 1
      src/constants/localkey.js
  49. 3 1
      src/constants/region.js
  50. 2 3
      src/contexts/shopContext.js
  51. 4 4
      src/contexts/sidebarContext.js
  52. 86 56
      src/contexts/themeContext.js
  53. 7 4
      src/contexts/withdrawMissionContext.js
  54. 10 3
      src/features/actions/hallClient.js
  55. 5 3
      src/features/actions/payAction.js
  56. 2 2
      src/features/actions/vipAction.js
  57. 1 2
      src/features/activitySlice.js
  58. 9 13
      src/features/api.js
  59. 0 1
      src/features/checkInSlice.js
  60. 169 110
      src/features/colyseus/CSClient.js
  61. 2 2
      src/features/colyseus/CSClientWrap.js
  62. 3 2
      src/features/gameListSlice.js
  63. 1 3
      src/features/luckyRollSlice.js
  64. 46 40
      src/features/mainSlice.js
  65. 16 8
      src/features/paySlice.js
  66. 17 12
      src/features/protectSlice.js
  67. 6 35
      src/features/redemptionCodeSlice.js
  68. 3 3
      src/features/redpackSlice.js
  69. 3 3
      src/features/socketSlice.js
  70. 1 3
      src/features/superBallSlice.js
  71. 3 3
      src/features/worldCupSlice.js
  72. 1 1
      src/hooks/useAuthLockRoute.js
  73. 4 15
      src/hooks/useAuthRoute.js
  74. 2 2
      src/hooks/useBlockSystemBack.js
  75. 55 61
      src/hooks/useEdgeGesture.js
  76. 28 41
      src/hooks/useElementScroll.js
  77. 23 21
      src/hooks/useFreeWithdraw.js
  78. 30 25
      src/hooks/useGameIFrame.js
  79. 15 51
      src/hooks/useGlobalClick.js
  80. 19 11
      src/hooks/useMatchProgress.js
  81. 64 40
      src/hooks/usePaymentActions.js
  82. 84 85
      src/hooks/usePaymentMethods.js
  83. 16 19
      src/hooks/usePreviousRoute.js
  84. 4 3
      src/hooks/useRecallGift.js
  85. 1 0
      src/hooks/useScrollLock.js
  86. 28 29
      src/hooks/useSuperBall.js
  87. 11 9
      src/hooks/useVipConfig.js
  88. 9 4
      src/hooks/useVisibilityChange.js
  89. 22 21
      src/hooks/useWithdrawInfo.js
  90. 6 2
      src/hooks/useWithdrawPending.js
  91. 8 2
      src/index.js
  92. 7 28
      src/layout/BottomNav/index.jsx
  93. 4 4
      src/layout/BottomNav/index2Backup.jsx
  94. 3 2
      src/layout/BreadCrumb/index.jsx
  95. 0 5
      src/layout/Footer/index.jsx
  96. 15 7
      src/layout/GameSearchPanel/index.jsx
  97. 38 35
      src/layout/Layout.jsx
  98. 67 113
      src/layout/Navbar/index.jsx
  99. 9 5
      src/layout/Routes/RouteContainer.jsx
  100. 1 1
      src/layout/Routes/RouteWrapper.jsx

+ 40 - 0
craco.config.js

@@ -6,6 +6,46 @@ module.exports = {
             // 设置 publicPath
             // webpackConfig.output.publicPath = 'http://192.168.1.4:8081/';
 
+            // These SVGs are consumed as URLs. Bypass SVGR so Babel does not turn large
+            // flag/sprite sources into unnecessary React components first.
+            const oneOf = webpackConfig.module.rules.find(rule => Array.isArray(rule.oneOf))?.oneOf;
+            const svgRuleIndex = oneOf?.findIndex(rule => rule.test?.test?.("icon.svg"));
+            if (svgRuleIndex >= 0) {
+                oneOf.splice(svgRuleIndex, 0, {
+                    test: /[\\/]assets[\\/](?:i18n[\\/][^\\/]+|symbol(?:-defs|-mobile))\.svg$/i,
+                    type: "asset/resource",
+                });
+            }
+
+            // Split existing initial modules into deterministic cacheable files. They are
+            // still loaded before the entry point, so runtime behavior remains unchanged.
+            const initialChunkMaxSize = 1000000;
+            const currentCacheGroups = webpackConfig.optimization.splitChunks?.cacheGroups || {};
+            webpackConfig.optimization.splitChunks = {
+                ...webpackConfig.optimization.splitChunks,
+                cacheGroups: {
+                    ...currentCacheGroups,
+                    initialVendors: {
+                        test: /[\\/]node_modules[\\/]/,
+                        chunks: "initial",
+                        enforce: true,
+                        maxSize: initialChunkMaxSize,
+                        minSize: 0,
+                        priority: 30,
+                        reuseExistingChunk: true,
+                    },
+                    initialApp: {
+                        test: module => module.resource?.startsWith(path.resolve(__dirname, "src")),
+                        chunks: "initial",
+                        enforce: true,
+                        maxSize: initialChunkMaxSize,
+                        minSize: 0,
+                        priority: 20,
+                        reuseExistingChunk: true,
+                    },
+                },
+            };
+
             // 返回修改后的配置
             return webpackConfig;
         },

+ 11 - 7
package-lock.json

@@ -108,6 +108,7 @@
         "@craco/craco": "^7.1.0",
         "ajv": "^8.17.1",
         "ajv-keywords": "^5.1.0",
+        "baseline-browser-mapping": "^2.11.21",
         "compress-create-react-app": "^1.3.1"
       }
     },
@@ -7261,12 +7262,15 @@
       "license": "MIT"
     },
     "node_modules/baseline-browser-mapping": {
-      "version": "2.8.14",
-      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.14.tgz",
-      "integrity": "sha512-GM9c0cWWR8Ga7//Ves/9KRgTS8nLausCkP3CGiFLrnwA2CDUluXgaQqvrULoR2Ujrd/mz/lkX87F5BHFsNr5sQ==",
+      "version": "2.11.21",
+      "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.21.tgz",
+      "integrity": "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==",
       "license": "Apache-2.0",
       "bin": {
-        "baseline-browser-mapping": "dist/cli.js"
+        "baseline-browser-mapping": "dist/cli.cjs"
+      },
+      "engines": {
+        "node": ">=6.0.0"
       }
     },
     "node_modules/batch": {
@@ -7627,9 +7631,9 @@
       }
     },
     "node_modules/caniuse-lite": {
-      "version": "1.0.30001749",
-      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001749.tgz",
-      "integrity": "sha512-0rw2fJOmLfnzCRbkm8EyHL8SvI2Apu5UbnQuTsJ0ClgrH8hcwFooJ1s5R0EP8o8aVrFu8++ae29Kt9/gZAZp/Q==",
+      "version": "1.0.30001810",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz",
+      "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==",
       "funding": [
         {
           "type": "opencollective",

+ 2 - 0
package.json

@@ -112,6 +112,7 @@
     "start": "craco start",
     "build": "craco build && node scripts/hash-loadm.js && node scripts/generate-sites.js",
     "gzip": "craco build && node scripts/hash-loadm.js && node scripts/generate-sites.js && compress-cra",
+    "lint": "eslint . --ext .js,.jsx --no-cache --max-warnings=0 --report-unused-disable-directives",
     "eject": "react-scripts eject"
   },
   "eslintConfig": {
@@ -136,6 +137,7 @@
     "@craco/craco": "^7.1.0",
     "ajv": "^8.17.1",
     "ajv-keywords": "^5.1.0",
+    "baseline-browser-mapping": "^2.11.21",
     "compress-create-react-app": "^1.3.1"
   }
 }

+ 1 - 1
public/download.html

@@ -424,7 +424,7 @@
             url+="&apk="+apkurl;
             if (isIOS()){
                 let r = "googlechromes://" + url.replace(/^https?:\/\//, "");
-                if (isNewTab)window.open(r,"_blank");
+                if (isNewTab)window.open(r,"_blank","noopener,noreferrer");
                 else window.location.href = r
             }
             else if (isAndroid()){

+ 8 - 6
public/index.html

@@ -106,13 +106,15 @@
         var meta = document.querySelector('meta[name=theme-color]');
         if (meta) {
           var color="#1b1c20";
-          var preferences=localStorage.getItem('preferences');
-          if (preferences!=null){
-            var json=JSON.parse(preferences);
-            if (json&&json.theme==="light"){
-              color="#FAFAF8";
+          try {
+            var preferences=localStorage.getItem('preferences');
+            if (preferences!=null){
+              var json=JSON.parse(preferences);
+              if (json&&json.theme==="light"){
+                color="#FAFAF8";
+              }
             }
-          }
+          } catch (e) {}
           meta.setAttribute('content', color);
         }
       }

+ 10 - 3
public/ios-download.html

@@ -438,6 +438,15 @@
 <script src="/js/load.m.js"></script>
 <script>
     const DEFAULT_STORE_URL = 'https://apps.apple.com/us/app/pixiepineslotsgrid/id6758513278';
+    const getSafeStoreUrl = (value) => {
+        try {
+            const url = new URL(value);
+            const allowedHost = url.hostname === 'apps.apple.com' || url.hostname === 'itunes.apple.com';
+            return url.protocol === 'https:' && allowedHost ? url.href : DEFAULT_STORE_URL;
+        } catch (e) {
+            return DEFAULT_STORE_URL;
+        }
+    };
     let API_URL = window.themeConfig.apiHost;
     let storeUrl = DEFAULT_STORE_URL;
     let ff = null;
@@ -522,9 +531,7 @@
                 if (!err && data) {
                     try {
                         let info = JSON.parse(data);
-                        if (info.iosurl) {
-                            storeUrl = info.iosurl;
-                        }
+                        if (info.iosurl) storeUrl = getSafeStoreUrl(info.iosurl);
                     } catch (e) {
                     }
                 }

+ 2 - 2
public/js/anonymous.js

@@ -1,5 +1,5 @@
-function anonymous() {
+window.anonymous = function anonymous() {
   (function () {
     debugger;
   })();
-}
+};

+ 1 - 8
public/js/com.devtool.js

@@ -1,13 +1,12 @@
 // 检测状态存储
 let devToolsOpen = false;
-let lastCheckTime = Date.now();
 
 // 核心检测函数
 function checkDevTools() {
   // 时间差阈值(根据设备性能调整)
   const threshold = 100; // 单位:毫秒
   const startTime = Date.now();
-  anonymous();
+  window.anonymous?.();
   const endTime = Date.now();
 
   // 计算时间差(正常情况应 < 10ms)
@@ -22,16 +21,10 @@ function checkDevTools() {
     console.log('✅ 开发者工具已关闭');
   }
 
-  // 记录最后一次检测时间
-  lastCheckTime = Date.now();
 }
 
 function onDevToolsOpen() {
   console.error('警告:请不要在生产环境打开开发者工具!');
-  const arr = [];
-  while (true) {
-    arr.push(new Array(10000000));
-  }
 }
 
 // 初始化定时检测(每1s检测一次)

File diff suppressed because it is too large
+ 4 - 4
public/js/dmig.js


+ 14 - 4
public/js/load.m.js

@@ -77,6 +77,7 @@
     iconAccentColor: "#FF2400",     // 图标强调色(双色图标的第二色)
   };
 
+  /* eslint-disable no-unused-vars -- Alternate presets are retained for per-brand rollout. */
   // 蓝色主题
   const blueTheme = {
     ...defaultTheme,
@@ -384,6 +385,7 @@
     gameMenuBg: "linear-gradient(180deg, rgba(21, 128, 61, 0.85) 0%, rgba(101, 67, 2, 0.92) 30%, rgba(0, 0, 0, 0.98) 100%)",
     iconAccentColor: "#EAB308",
   };
+  /* eslint-enable no-unused-vars */
   // ═══════════════════════════════════════════════════════
   // 区域配置(Region Config)
   // 每个品牌站点通过 "region" 字段引用对应的区域配置
@@ -707,8 +709,8 @@
     }
     prand = (mult * prand + incr) % modu;
     var encStr = '';
-    for (var i = 0; i < str.length; i += 2) {
-      encStr += String.fromCharCode(parseInt(str.substring(i, i + 2), 16) ^ Math.floor((prand / modu) * 255));
+    for (var charIndex = 0; charIndex < str.length; charIndex += 2) {
+      encStr += String.fromCharCode(parseInt(str.substring(charIndex, charIndex + 2), 16) ^ Math.floor((prand / modu) * 255));
       prand = (mult * prand + incr) % modu;
     }
     return decodeURIComponent(encStr);
@@ -737,8 +739,14 @@
   const getInstallSKC = () => {
     if (window.__pwaPreSkc && window.__pwaPreSkc.s_k) return window.__pwaPreSkc;
     const CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+    const randomValues = new Uint32Array(9);
+    if (window.crypto && typeof window.crypto.getRandomValues === "function") {
+      window.crypto.getRandomValues(randomValues);
+    } else {
+      for (let i = 0; i < randomValues.length; i++) randomValues[i] = Math.floor(Math.random() * 0x100000000);
+    }
     let sk = "";
-    for (let j = 0; j < 9; j++) sk += CHARS.charAt(Math.floor(Math.random() * 62));
+    for (let j = 0; j < randomValues.length; j++) sk += CHARS.charAt(randomValues[j] % CHARS.length);
     const c = lsGet("LOCAL_ACTIVITY_C") || new URLSearchParams(window.location.search || "").get("c") || "0";
     const skc = { s_k: sk, c: String(c) };
     window.__pwaPreSkc = skc;
@@ -759,6 +767,8 @@
       try {
         const params = new URLSearchParams(value.startsWith("?") ? value : `?${value}`);
         params.delete("jumpapk");
+        params.delete("api_secret");
+        params.delete("w_lci");
         const next = params.toString();
         return next ? `?${next}` : "";
       } catch (e) {
@@ -772,7 +782,7 @@
     }
     else {
       let lsearch = localStorage.getItem(lkey);
-      if (lsearch && lsearch.includes("jumpapk")) {
+      if (lsearch && (lsearch.includes("jumpapk") || lsearch.includes("api_secret") || lsearch.includes("w_lci"))) {
         lsearch = cleanJumpApkSearch(lsearch);
         if (lsearch) localStorage.setItem(lkey, lsearch);
         else localStorage.removeItem(lkey);

+ 57 - 70
public/js/pixel_h5.js

@@ -1,27 +1,6 @@
-var pixels = [];
-
-Date.prototype.Format = function (fmt) {
-  var o = {
-    'M+': this.getMonth() + 1, //月份
-    'd+': this.getDate(), //日
-    'H+': this.getHours(), //小时
-    'm+': this.getMinutes(), //分
-    's+': this.getSeconds(), //秒
-    'q+': Math.floor((this.getMonth() + 3) / 3), //季度
-    S: this.getMilliseconds() //毫秒
-  };
-  if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (this.getFullYear() + '').substr(4 - RegExp.$1.length));
-  for (var k in o)
-    if (new RegExp('(' + k + ')').test(fmt))
-      fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length));
-  return fmt;
-};
+/* eslint-disable eqeqeq -- This legacy tracker intentionally accepts string, number, null and undefined inputs. */
 
-try {
-  var paste = new Date().Format('yyyyMMddHHmmss') + Math.ceil(Math.random() * 10000000);
-} catch (e) {
-  console.log('create paste error');
-}
+var pixels = [];
 
 // 初始化状态表
 var fbcalllist = {
@@ -92,25 +71,10 @@ function getCookie(name) {
   if (parts.length === 2) return parts.pop().split(';').shift();
 }
 
-var fbevapi = '';
 var evapi = '';
 var dualstackApi = '';
-var isdebug = false;
-
-if (
-  window.location.href.indexOf('localhost') >= 0 ||
-  window.location.href.indexOf('192.168.1.') >= 0 ||
-  window.location.href.indexOf('192.168.11.') >= 0 ||
-  window.location.href.indexOf('10.10.100.') >= 0 ||
-  window.location.href.indexOf('usa3.cjwend.in') >= 0
-) {
-  isdebug = true;
-} else {
-  isdebug = false;
-}
 
 evapi = window.themeConfig.api;
-fbevapi = evapi;
 dualstackApi = window.themeConfig.api;
 
 evapi = evapi + '/api/weblog';
@@ -125,7 +89,7 @@ if (preloadqilinid != null && preloadqilinid != '') {
 function checkAdUrlChange() {
   // 如果当前的链接里面有 pixel , gcode 参数, 则保存到 localstorage 里面
   var url = window.location.href;
-  var urlParams = new URLSearchParams(location.search);
+  var urlParams = new URLSearchParams(window.location.search);
   var pixel = urlParams.get('pixel');
   var gcode = urlParams.get('gcode');
   if ((pixel != null && pixel != '') || (gcode != null && gcode != '')) {
@@ -149,11 +113,12 @@ function getURLSearchParams() {
   }
 
   // 如果没有, 则从当前 url 里面获取
-  return new URLSearchParams(location.search);
+  return new URLSearchParams(window.location.search);
 }
 
 var hasGGSendPageView = false;
 var hasSendPageView = false;
+var fbCookieCheckAttempts = 0;
 function checkFbcAndFbp(conditionname) {
   // console.log('check ...', conditionname);
   if (hasSendPageView) {
@@ -165,6 +130,11 @@ function checkFbcAndFbp(conditionname) {
   var fbc = getCookie('_fbc');
 
   if (fbp == undefined) {
+    if (fbCookieCheckAttempts >= 500) {
+      fbcalllist.fbInitEnd();
+      return;
+    }
+    fbCookieCheckAttempts += 1;
     // console.log("fbp is null", fbc, fbp);
     setTimeout(function () {
       checkFbcAndFbp('timer check');
@@ -207,7 +177,7 @@ function loadpixel() {
     return;
   }
 
-  !(function (f, b, e, v, n, t, s) {
+  (function (f, b, e, v, n, t, s) {
     if (f.fbq) return;
     n = f.fbq = function () {
       n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
@@ -220,7 +190,7 @@ function loadpixel() {
     n.queue = [];
     t = b.createElement(e);
     t.addEventListener('load', function () {
-      fbq('init', pixel);
+      window.fbq('init', pixel);
 
       var u = navigator.userAgent;
       console.log('userAgent = ', u);
@@ -240,7 +210,7 @@ function loadpixel() {
         // fbq('init', '950445440166110'); // 备用1
         // fbq('init', '1710862789652432'); // 备用2
       }
-      fbq('track', 'PageView');
+      window.fbq('track', 'PageView');
       checkFbcAndFbp('after laod node');
     });
 
@@ -408,7 +378,7 @@ function submitFBEvent(eventName) {
   }
 
   //fb 的事件提交
-  fbq('track', eventName); //'SubmitApplication'
+  if (typeof window.fbq === 'function') window.fbq('track', eventName); //'SubmitApplication'
 }
 
 // 页面统计事件
@@ -454,8 +424,7 @@ function pageEventSubmit(event_name, during = 0, mark = '') {
   };
 }
 
-var retryCountJs = 0;
-var lastclickDownloadTime = 0;
+var downloadIframe;
 // 页面下载
 function pageDownload(download_url) {
   // 取到 fbp 之后就去下载
@@ -464,7 +433,7 @@ function pageDownload(download_url) {
     retryDownTime = retryDownTime + 1;
     if (retryDownTime > 2 * 10) {
       //超过 2 秒, 就直接请求
-      pageEventSubmit(down_type_name, 0, download_url);
+      pageEventSubmit(window.down_type_name || 'download', 0, download_url);
       // window.location.href = download_url
       todoownload(download_url);
       return;
@@ -497,6 +466,7 @@ function todoownload(download_url) {
 
 // -- google
 
+var ggClientCheckAttempts = 0;
 function checkGGClientEvent(conditionname) {
   if (hasGGSendPageView) {
     console.log('hasGGSendPageView');
@@ -504,6 +474,11 @@ function checkGGClientEvent(conditionname) {
   }
 
   if (window.gg_client_id == undefined) {
+    if (ggClientCheckAttempts >= 500) {
+      fbcalllist.ggInitEnd();
+      return;
+    }
+    ggClientCheckAttempts += 1;
     setTimeout(function () {
       checkGGClientEvent('timer check');
     }, 10);
@@ -529,7 +504,7 @@ function checkGGClientEvent(conditionname) {
 
   window.dataLayer = window.dataLayer || [];
   function gtag() {
-    dataLayer.push(arguments);
+    window.dataLayer.push(arguments);
   }
 
   script.onload = function () {
@@ -549,7 +524,7 @@ function checkGGClientEvent(conditionname) {
 //提交 GG 事件
 function submitGGEvent(eventName) {
   // 从 url 里面获取 material 参数
-  const urlParams = new URLSearchParams(location.search);
+  const urlParams = new URLSearchParams(window.location.search);
   var gcode = urlParams.get('gcode'); // 谷歌代码
   if (gcode == null || gcode == '') {
     return false;
@@ -666,7 +641,7 @@ function getFbcAndFbp(bccallback, timeOutSeconds = 5) {
     try {
       // 从 url 里面获取 material 参数
       const urlParams = getURLSearchParams(); //new URLSearchParams(location.search);
-      var fbinfo = {
+      fbinfo = {
         fbp: undefined,
         fbc: undefined,
         ready: false,
@@ -709,10 +684,10 @@ function getFbcAndFbp(bccallback, timeOutSeconds = 5) {
       // 查找 url 里面正则提取 ch_ 开头的协议
       if (apkch == '') {
         var lourl = window.location.href;
-        var reg = /ch_([^/]*)/g;
-        var match = reg.exec(lourl);
-        if (match != null && match.length >= 2) {
-          apkch = match[1];
+        var channelPattern = /ch_([^/]*)/g;
+        var channelMatch = channelPattern.exec(lourl);
+        if (channelMatch != null && channelMatch.length >= 2) {
+          apkch = channelMatch[1];
         }
       }
       fbinfo.apkch = apkch;
@@ -749,11 +724,11 @@ function getFbcAndFbp(bccallback, timeOutSeconds = 5) {
         // fb
         if (fbinfo.fbp != undefined && fbinfo.fbp != null && fbinfo.fbp != '' && fbinfo.fbp.indexOf('fb.') >= 0) {
           // "{{campaign.name}} ({{campaign.id}})"  解析出 campaign.name 和 campaign.id
-          var reg = /(.*)\((.*)\)/g;
-          var match = reg.exec(fbinfo.material);
-          if (match != null && match.length >= 3) {
-            fbinfo.campaign = match[1];
-            fbinfo.campaign_id = match[2];
+          var campaignPattern = /(.*)\((.*)\)/g;
+          var campaignMatch = campaignPattern.exec(fbinfo.material);
+          if (campaignMatch != null && campaignMatch.length >= 3) {
+            fbinfo.campaign = campaignMatch[1];
+            fbinfo.campaign_id = campaignMatch[2];
           }
         }
         // gg
@@ -767,18 +742,18 @@ function getFbcAndFbp(bccallback, timeOutSeconds = 5) {
         // fb
         if (fbinfo.fbp != undefined && fbinfo.fbp != null && fbinfo.fbp != '' && fbinfo.fbp.indexOf('fb.') >= 0) {
           // "{{adset.name}} ({{adset.id}})"  解析出 , adset.name , adset.id
-          var reg = /(.*)\((.*)\)/g;
-          var match = reg.exec(fbinfo.adgroup);
-          if (match != null && match.length >= 3) {
-            fbinfo.adset = match[1];
-            fbinfo.adset_id = match[2];
+          var adGroupPattern = /(.*)\((.*)\)/g;
+          var adGroupMatch = adGroupPattern.exec(fbinfo.adgroup);
+          if (adGroupMatch != null && adGroupMatch.length >= 3) {
+            fbinfo.adset = adGroupMatch[1];
+            fbinfo.adset_id = adGroupMatch[2];
           }
           // 从 creative 里面提取 ad  ad_id
-          reg = /(.*)\((.*)\)/g;
-          match = reg.exec(fbinfo.creative);
-          if (match != null && match.length >= 3) {
-            fbinfo.ad = match[1];
-            fbinfo.ad_id = match[2];
+          var creativePattern = /(.*)\((.*)\)/g;
+          var creativeMatch = creativePattern.exec(fbinfo.creative);
+          if (creativeMatch != null && creativeMatch.length >= 3) {
+            fbinfo.ad = creativeMatch[1];
+            fbinfo.ad_id = creativeMatch[2];
           }
         }
         // gg.
@@ -803,7 +778,7 @@ function getFbcAndFbp(bccallback, timeOutSeconds = 5) {
       console.log('parse error:', e);
     }
 
-    bccallback(fbinfo);
+    if (typeof bccallback === 'function') bccallback(fbinfo);
   });
 
   sureCheck();
@@ -826,3 +801,15 @@ function tryFbBind(userid, uuid, channel, fbclid) {
     xhr.onload = function (data) { };
   }, 10);
 }
+
+// Public API consumed by legacy landing pages and native shells.
+Object.assign(window, {
+  pixels,
+  load_pixel,
+  initPixels,
+  init_facebook_pixels,
+  copyQilinid,
+  pageDownload,
+  getFbcAndFbp,
+  tryFbBind
+});

+ 142 - 74
public/js/push_manager.js

@@ -104,6 +104,16 @@ function getSubscriptionInfo() {
   return localStorage.getItem(cacheKey) === '1';
 }
 
+function getSafeReturnUrl(value) {
+  if (typeof value !== 'string' || value.trim() === '') return '';
+  try {
+    var url = new URL(value, window.location.href);
+    return url.protocol === 'http:' || url.protocol === 'https:' ? url.href : '';
+  } catch (error) {
+    return '';
+  }
+}
+
 function getIsPostData() {
   var installed = isPwaInstall();
 
@@ -128,6 +138,26 @@ var getUniqueID = (function () {
   // 缓存 promise 避免多次执行
   let promiseInProgress = null;
 
+  function resolveUniqueID(resolve, value) {
+    let uniqueID = value;
+    if (!uniqueID) {
+      try {
+        uniqueID = getLocalUUID();
+      } catch (error) {
+        errorpushlog('Failed to create local unique ID:', error);
+      }
+    }
+    if (uniqueID) {
+      __global_unique_ID = uniqueID;
+      try {
+        setCookie('winus_uuid', uniqueID, 604800000);
+      } catch (error) {
+        errorpushlog('Failed to cache unique ID:', error);
+      }
+    }
+    resolve(uniqueID || '');
+  }
+
   return function () {
     if (__global_unique_ID) {
       return Promise.resolve(__global_unique_ID);
@@ -154,15 +184,20 @@ var getUniqueID = (function () {
     if (isInApk()) {
       // apk 环境下, 直接返回
       promiseInProgress = new Promise(resolve => {
+        let completed = false;
+        const finish = deviceId => {
+          if (completed) return;
+          completed = true;
+          clearTimeout(timeoutId);
+          resolveUniqueID(resolve, deviceId);
+        };
+        const timeoutId = setTimeout(() => finish(''), 5000);
         try {
           // IPA 环境
           if (window?.android?.isIpa) {
             // 确保回调函数在调用 getDeviceID 之前就绑定好
             window.android.deviceIdCallback = function (deviceId) {
-              console.log('deviceIdCallback', deviceId);
-              // 如果禁止了第三方cookie, 则将全局唯一标识保存到 当前域名下 cookie 中
-              if (deviceId) setCookie('winus_uuid', deviceId, 604800000);
-              resolve(deviceId);
+              finish(deviceId);
             };
             window.android.getDeviceID?.();
             return;
@@ -170,13 +205,10 @@ var getUniqueID = (function () {
 
           // 普通 APK 环境
           const deviceId = window?.android?.getDeviceID?.();
-          if (deviceId) {
-            // 如果禁止了第三方cookie, 则将全局唯一标识保存到 当前域名下 cookie 中
-            setCookie('winus_uuid', deviceId, 604800000);
-            resolve(deviceId);
-          }
+          finish(deviceId);
         } catch (error) {
-          console.log(error);
+          errorpushlog('Failed to read native device ID:', error);
+          finish('');
         }
       });
       return promiseInProgress;
@@ -185,33 +217,47 @@ var getUniqueID = (function () {
     promiseInProgress = new Promise(function (resolve) {
       var iframe = document.createElement('iframe');
 
-      iframe.src = window.location.protocol + '//' + 'cen.greatus777.com' + '/storage';
+      iframe.src = window.location.protocol + '//cen.greatus777.com/storage';
       // iframe.src = window.location.protocol + '//' + 'dev.winus777.com' + '/storage';
 
-      iframe.style.display = 'none';
+      var iframeOrigin = new URL(iframe.src).origin;
+      var timeoutId = null;
+      var completed = false;
 
-      document.body.appendChild(iframe);
+      function cleanup() {
+        if (timeoutId) clearTimeout(timeoutId);
+        window.removeEventListener('message', messageHandler);
+        if (iframe.parentNode) iframe.parentNode.removeChild(iframe);
+      }
 
-      iframe.addEventListener('load', function () {
-        iframe.contentWindow.postMessage('getUniqueID', '*');
-      });
+      function finish(value) {
+        if (completed) return;
+        completed = true;
+        cleanup();
+        resolveUniqueID(resolve, value);
+      }
 
-      function messageHandler(event) {
-        if (event && event.data && event.data !== 'undefined') {
-          const data = event.data;
-          __global_unique_ID = data.md5val;
+      iframe.style.display = 'none';
 
-          // 如果禁止了第三方cookie, 则将全局唯一标识保存到 当前域名下 cookie 中
-          setCookie('winus_uuid', __global_unique_ID, 604800000);
+      iframe.addEventListener('load', function () {
+        iframe.contentWindow?.postMessage('getUniqueID', iframeOrigin);
+      }, { once: true });
+      iframe.addEventListener('error', function () {
+        finish('');
+      }, { once: true });
 
-          resolve(__global_unique_ID);
-          window.removeEventListener('message', messageHandler);
-          document.body.removeChild(iframe);
-        }
+      function messageHandler(event) {
+        if (event.origin !== iframeOrigin || event.source !== iframe.contentWindow) return;
+        const uniqueID = event.data && typeof event.data === 'object' ? event.data.md5val : '';
+        if (uniqueID) finish(uniqueID);
       }
 
       // 监听 iframe 的消息
       window.addEventListener('message', messageHandler);
+      document.body.appendChild(iframe);
+      timeoutId = setTimeout(function () {
+        finish('');
+      }, 8000);
     });
     return promiseInProgress;
   };
@@ -272,16 +318,16 @@ function isIOS() {
 
 function openUrlUseSafari(url = "", isNewTab = true, isBFP = true) {
     if (url == null || url === "") url = window.location.href;
-    if (global.BFP != null && global.BFP !== "" && isBFP) {
-        if (url.indexOf("?") === -1) url += "?bfp=" + global.BFP;
-        else url += "&bfp=" + global.BFP;
+    if (window.BFP != null && window.BFP !== "" && isBFP) {
+        if (url.indexOf("?") === -1) url += "?bfp=" + window.BFP;
+        else url += "&bfp=" + window.BFP;
     }
     if (isIOS()) {
         const safariUrl = url.startsWith('https')
             ? url.replace('https://', 'x-safari-https://')
             : url.replace('http://', 'x-safari-https://');
         if (isNewTab) {
-            window.open(safariUrl, '_blank');
+            window.open(safariUrl, '_blank', 'noopener,noreferrer');
         } else {
             window.location.href = safariUrl;
         }
@@ -289,11 +335,11 @@ function openUrlUseSafari(url = "", isNewTab = true, isBFP = true) {
 }
 
 function openChrome() {
-  var url = location.href;
+  var url = window.location.href;
 
     if (isIOS()) {
         let r = "googlechromes://" + url.replace(/^https?:\/\//, "");
-        window.open(r, "_blank");
+        window.open(r, "_blank", "noopener,noreferrer");
     } else {
   url = url.replace('https://', '');
   url = url.replace('http://', '');
@@ -301,14 +347,16 @@ function openChrome() {
     'intent://' + url + '#Intent;scheme=https;action=android.intent.action.VIEW;component=com.android.chrome;end';
 
   var a = document.getElementById('pwaIFrame');
-  a ||
-    (((a = document.createElement('iframe')).id = 'pwaIFrame'),
-    (a.style.width = '0'),
-    (a.style.height = '0'),
-    (a.style.opacity = '0'),
-    (a.name = 'pwaIFrame'),
-    document.body.appendChild(a)),
-    (a.src = newLink);
+  if (!a) {
+    a = document.createElement('iframe');
+    a.id = 'pwaIFrame';
+    a.style.width = '0';
+    a.style.height = '0';
+    a.style.opacity = '0';
+    a.name = 'pwaIFrame';
+    document.body.appendChild(a);
+  }
+  a.src = newLink;
     }
   setTimeout(() => {
     if (!document.hidden) {
@@ -347,7 +395,7 @@ function errorpushlog(...args) {
 // 将URL Base64编码的字符串转换为Uint8Array
 function urlBase64ToUint8Array(base64String) {
   var padding = '='.repeat((4 - (base64String.length % 4)) % 4);
-  var base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
+  var base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
 
   var rawData = window.atob(base64);
   var outputArray = new Uint8Array(rawData.length);
@@ -395,9 +443,9 @@ function submitSubscription(subscription, channel, userid) {
   var xhr = new XMLHttpRequest();
   // 墨西哥订阅地址
   if (window.mode_env === 'development') {
-    xhr.open('POST', API_URL+'/api/push_subscribe', true);
+    xhr.open('POST', API_URL + '/api/push_subscribe', true);
   } else {
-    xhr.open('POST', API_URL+'/api/push_subscribe', true);
+    xhr.open('POST', API_URL + '/api/push_subscribe', true);
   }
 
   xhr.setRequestHeader('Content-Type', 'application/json');
@@ -435,7 +483,7 @@ function recordEvent(eventName, channel, remark, userid) {
   var host = 'us_' + h2 + '_' + h3 + '_' + h4;
 
   var url =
-    API_URL+'/api/weblog?host=' + host + '&act=' + eventName + '&remark=' + remark + '&orderid=' + userid;
+    API_URL + '/api/weblog?host=' + host + '&act=' + eventName + '&remark=' + remark + '&orderid=' + userid;
   var xhr = new XMLHttpRequest();
   xhr.open('GET', url, true);
   xhr.send();
@@ -449,8 +497,9 @@ function requestNotificationPermission(registration, channel, userid, machineser
     Notification.requestPermission().then(function (permission) {
       if (permission === 'granted') {
         pushmlog('User has authorized push notifications');
+        recordEvent('push_permission', channel, 'allow', userid);
         // 注册Service Worker
-        registration.pushManager
+        return registration.pushManager
           .subscribe({
             userVisibleOnly: true,
             applicationServerKey: urlBase64ToUint8Array(
@@ -475,7 +524,7 @@ function requestNotificationPermission(registration, channel, userid, machineser
                   if (+ish5 === 1) {
                     if (udatadict.returnurl) {
                       setTimeout(() => {
-                        location.href = udatadict.returnurl;
+                        window.location.href = udatadict.returnurl;
                       }, 1000);
                     }
                   } else {
@@ -495,9 +544,10 @@ function requestNotificationPermission(registration, channel, userid, machineser
                 }
               }
             });
+          })
+          .catch(function (error) {
+            errorpushlog('Failed to subscribe to push notifications:', error);
           });
-
-        recordEvent('push_permission', channel, 'allow', userid);
       } else if (permission === 'denied') {
         pushmlog('User denied push notifications', userid, machineserial, ish5, channel);
         recordEvent('push_permission', channel, 'deny', userid);
@@ -513,7 +563,7 @@ function requestNotificationPermission(registration, channel, userid, machineser
               if (+ish5 === 1) {
                 if (udatadict.returnurl) {
                   setTimeout(() => {
-                    location.href = udatadict.returnurl;
+                    window.location.href = udatadict.returnurl;
                   }, 1000);
                 }
               } else {
@@ -547,7 +597,7 @@ function requestNotificationPermission(registration, channel, userid, machineser
               if (+ish5 === 1) {
                 if (udatadict.returnurl) {
                   setTimeout(() => {
-                    location.href = udatadict.returnurl;
+                    window.location.href = udatadict.returnurl;
                   }, 1000);
                 }
               } else {
@@ -569,6 +619,8 @@ function requestNotificationPermission(registration, channel, userid, machineser
         });
         recordEvent('push_permission', channel, 'pending', userid);
       }
+    }).catch(function (error) {
+      errorpushlog('Failed to request notification permission:', error);
     });
   }
 }
@@ -588,11 +640,11 @@ function checkPushPermission(channel, userid, machineserial, type, options) {
   var endCallback = options && options.endCallback;
 
   try {
-    if (userid == undefined) {
+    if (userid == null) {
       userid = 0;
     }
 
-    if (channel == undefined) {
+    if (channel == null) {
       channel = 'default';
     }
 
@@ -605,7 +657,7 @@ function checkPushPermission(channel, userid, machineserial, type, options) {
       udatadict.fingerprint = fingerprint;
       udatadict.machineserial = machineserial;
       udatadict.type = type;
-      udatadict.returnurl = returnurl;
+      udatadict.returnurl = getSafeReturnUrl(returnurl);
 
       // 保存到 localstorage
       localStorage.setItem('pushudatadict', JSON.stringify(udatadict));
@@ -620,7 +672,8 @@ function checkPushPermission(channel, userid, machineserial, type, options) {
             isCheckingPushPermission = true;
 
             // 检查是否支持service worker
-            if (registration == undefined || registration == null || registration.pushManager == undefined) {
+            if (!registration?.pushManager) {
+              isCheckingPushPermission = false;
               !isInApk() &&
                 setTimeout(() => {
                   document.body.addEventListener('click', openChrome);
@@ -633,7 +686,7 @@ function checkPushPermission(channel, userid, machineserial, type, options) {
 
             var cacheKey = 'submit_subscription_' + udatadict.channel + '_' + udatadict.userid + '_' + installed;
 
-            registration.pushManager.getSubscription().then(function (subscription) {
+            return registration.pushManager.getSubscription().then(function (subscription) {
               isCheckingPushPermission = false; // 已经有结果了
               if (subscription) {
                 var subscriptionOld = localStorage.getItem('subscription');
@@ -643,7 +696,7 @@ function checkPushPermission(channel, userid, machineserial, type, options) {
                 // 判断 当前渠道+当前用户  是否已经上报过授权. 如果未上报过, 则上报一次
                 var cacheValue = localStorage.getItem(cacheKey);
 
-                if (cacheValue && subscriptionOld && subscriptionOld == JSON.stringify(subscription)) {
+                if (cacheValue && subscriptionOld && subscriptionOld === JSON.stringify(subscription)) {
                   pushmlog('User has already reported subscription info');
                   return;
                 }
@@ -651,7 +704,7 @@ function checkPushPermission(channel, userid, machineserial, type, options) {
                 recordEvent('push_permission', udatadict.channel, 'update', udatadict.userid);
                 localStorage.setItem('subscription', JSON.stringify(subscription)); // 更新
                 submitSubscription(subscription, udatadict.channel, udatadict.userid); // 已经授权过就再上报一次 如果未上报
-                var ispost = getIsPostData();
+                const ispost = getIsPostData();
                 if (!ispost) {
                   uploadSubscription({
                     userid: udatadict.userid || '',
@@ -669,7 +722,7 @@ function checkPushPermission(channel, userid, machineserial, type, options) {
                 // 判断 通知状态
                 if (Notification.permission === 'denied') {
                   endCallback && endCallback('denied');
-                  var ispost = getIsPostData();
+                  const ispost = getIsPostData();
                   if (!ispost) {
                     uploadSubscription({
                       userid: udatadict.userid || '',
@@ -686,10 +739,14 @@ function checkPushPermission(channel, userid, machineserial, type, options) {
                   requestNotificationPermission(registration, udatadict.channel, udatadict.userid, machineserial, type);
                 }
               }
+            }).catch(function (error) {
+              isCheckingPushPermission = false;
+              errorpushlog('Failed to read push subscription:', error);
             });
           })
           .catch(e => {
-            console.log(e);
+            isCheckingPushPermission = false;
+            errorpushlog('Service worker readiness failed:', e);
           });
     });
   } catch (error) {
@@ -701,17 +758,17 @@ function checkPushPermission(channel, userid, machineserial, type, options) {
 // 外部调用。每次
 function checkAlive(userid, vippoint, channel, score) {
   try {
-    if (udatadict.fingerprint == undefined) {
+    if (udatadict.fingerprint === undefined || udatadict.fingerprint === null) {
       var udatadictstr = localStorage.getItem('pushudatadict');
       if (udatadictstr) {
         udatadict = JSON.parse(udatadictstr);
       }
-      if (udatadict.fingerprint == undefined) {
+      if (udatadict.fingerprint === undefined || udatadict.fingerprint === null) {
         errorpushlog('udatadict.fingerprint is undefined');
         return;
       }
     }
-    if (userid == undefined) {
+    if (userid === undefined || userid === null) {
       errorpushlog('userid is undefined');
       return;
     }
@@ -756,15 +813,15 @@ function checkAlive(userid, vippoint, channel, score) {
 }
 //简单加密
 const encrypt = (str, pwd) => {
-  if (str == '') {
+  if (str === '') {
     return '';
   }
   str = encodeURIComponent(str);
-  if (!pwd || pwd == '') {
+  if (!pwd || pwd === '') {
     pwd = 'xbd4jl';
   }
   pwd = encodeURIComponent(pwd);
-  if (pwd == '' || pwd.length <= 0) {
+  if (pwd === '' || pwd.length <= 0) {
     return '';
   }
   var prand = '';
@@ -794,8 +851,8 @@ const encrypt = (str, pwd) => {
   prand = (mult * prand + incr) % modu;
   var encChr = '';
   var encStr = '';
-  for (var i = 0, len = str.length; i < len; i += 1) {
-    encChr = parseInt(str.charCodeAt(i) ^ Math.floor((prand / modu) * 255));
+  for (var strIndex = 0, strLength = str.length; strIndex < strLength; strIndex += 1) {
+    encChr = parseInt(str.charCodeAt(strIndex) ^ Math.floor((prand / modu) * 255));
     if (encChr < 16) {
       encStr += '0' + encChr.toString(16);
     } else {
@@ -812,17 +869,17 @@ const encrypt = (str, pwd) => {
 }
 //简单解密
 const decrypt = (str, pwd) => {
-  if (str == '') {
+  if (str === '') {
     return '';
   }
-  if (!pwd || pwd == '') {
+  if (!pwd || pwd === '') {
     pwd = 'xbd4jl';
   }
   pwd = encodeURIComponent(pwd);
-  if (str == undefined || str.length < 8) {
+  if (str === undefined || str === null || str.length < 8) {
     return '';
   }
-  if (pwd == undefined || pwd.length <= 0) {
+  if (pwd === undefined || pwd === null || pwd.length <= 0) {
     return '';
   }
   var prand = '';
@@ -854,8 +911,8 @@ const decrypt = (str, pwd) => {
   prand = (mult * prand + incr) % modu;
   var encChr = '';
   var encStr = '';
-  for (var i = 0, len = str.length; i < len; i += 2) {
-    encChr = parseInt(parseInt(str.substring(i, i + 2), 16) ^ Math.floor((prand / modu) * 255));
+  for (var strIndex = 0, strLength = str.length; strIndex < strLength; strIndex += 2) {
+    encChr = parseInt(parseInt(str.substring(strIndex, strIndex + 2), 16) ^ Math.floor((prand / modu) * 255));
     encStr += String.fromCharCode(encChr);
     prand = (mult * prand + incr) % modu;
   }
@@ -961,6 +1018,17 @@ function generateUUIDv7() {
               .join('');
 }
 
+// Public API consumed by the standalone PWA pages and native shells.
+Object.assign(window, {
+  getSubscriptionInfo,
+  openUrlUseSafari,
+  isInChromeBrowser,
+  checkPushPermission,
+  checkAlive,
+  localStorageRemove,
+  ProcessChannel
+});
+
 if (isIOS()) {
     window.location.href = window.location.origin + '/' + window.location.search;
-}
+}

+ 67 - 48
public/pwa/service-work.js

@@ -1,3 +1,5 @@
+/* global globalThis */
+
 const cacheName = 'spa-cache-v1';
 
 // —— Cache pressure controls ——
@@ -5,8 +7,8 @@ const MAX_NOHASH_ENTRIES = 500; // 每个无 hash 版本缓存的最大条目数
 const MAX_HASH_ENTRIES = 200; // 带 hash 静态资源缓存最大条目数
 const MAX_IMG_RESPONSE_SIZE = 1_500_000; // 超过此大小(字节)不入缓存
 // iOS/Safari 下调限额(小容量设备)
-const IS_SAFARI = /Safari\//.test(self.navigator.userAgent) && !/Chrome\//.test(self.navigator.userAgent);
-const IS_FB_BROWSER = /FBAN|FBAV|FB_IAB/i.test(self.navigator.userAgent);
+const IS_SAFARI = /Safari\//.test(globalThis.navigator.userAgent) && !/Chrome\//.test(globalThis.navigator.userAgent);
+const IS_FB_BROWSER = /FBAN|FBAV|FB_IAB/i.test(globalThis.navigator.userAgent);
 const IOS_LIMIT_FACTOR = IS_SAFARI ? 0.5 : 1;
 const FB_LIMIT_FACTOR = IS_FB_BROWSER ? 0.3 : 1;
 const DEVICE_FACTOR = Math.min(IOS_LIMIT_FACTOR, FB_LIMIT_FACTOR);
@@ -30,13 +32,13 @@ async function readAppVersion() {
 
 // —— Cookie Store 安全封装(iOS Safari 可能不支持) ——
 function hasCookieStore() {
-  return 'cookieStore' in self && self.cookieStore && typeof self.cookieStore.get === 'function';
+  return 'cookieStore' in globalThis && globalThis.cookieStore && typeof globalThis.cookieStore.get === 'function';
 }
 
 async function safeGetCookie(name) {
   if (!hasCookieStore()) return undefined;
   try {
-    const res = await self.cookieStore.get(name);
+    const res = await globalThis.cookieStore.get(name);
     return res?.value;
   } catch (e) {
     return undefined;
@@ -46,7 +48,7 @@ async function safeGetCookie(name) {
 async function safeSetCookie(name, value) {
   if (!hasCookieStore()) return;
   try {
-    await self.cookieStore.set(name, value);
+    await globalThis.cookieStore.set(name, value);
   } catch (e) {
     // ignore
   }
@@ -55,7 +57,7 @@ async function safeSetCookie(name, value) {
 async function safeDeleteCookie(name) {
   if (!hasCookieStore()) return;
   try {
-    await self.cookieStore.delete(name);
+    await globalThis.cookieStore.delete(name);
   } catch (e) {
     // ignore
   }
@@ -63,19 +65,25 @@ async function safeDeleteCookie(name) {
 
 // 定义主域名和备用域名列表
 var BACKUP_DOMAINS = ['https://winus777.com'];
-// 当前选定的域名
-let activeDomain = '';
-var ORIGINAL_DOMAIN;
+
+function getAllowedBackupDomain(value) {
+  if (typeof value !== 'string' || value.trim() === '') return undefined;
+  try {
+    const origin = new URL(value).origin;
+    return BACKUP_DOMAINS.some(domain => new URL(domain).origin === origin) ? origin : undefined;
+  } catch (error) {
+    return undefined;
+  }
+}
 
 // 安装 Service Worker 并缓存 index.html
-self.addEventListener('install', function (e) {
+globalThis.addEventListener('install', function (e) {
   e.waitUntil(
     (async () => {
       await readAppVersion();
       const cache = await caches.open(cacheName);
       let ok;
       try {
-        ORIGINAL_DOMAIN = await safeGetCookie('ORIGINAL_DOMAIN');
         ok = await cache.addAll([
           // '/index.html'
         ]);
@@ -86,15 +94,14 @@ self.addEventListener('install', function (e) {
     })()
   );
   console.log('[Service Worker] Install');
-  self.skipWaiting();
+  globalThis.skipWaiting();
 });
 
-self.addEventListener('activate', e => {
+globalThis.addEventListener('activate', e => {
   // Remove unwanted caches
   e.waitUntil(
     (async () => {
       await readAppVersion();
-      ORIGINAL_DOMAIN = await safeGetCookie('ORIGINAL_DOMAIN');
       const names = await caches.keys();
       const validNoHashName = IMG_NOHASH_CACHE_PREFIX + IMG_VERSION;
       await Promise.all(
@@ -105,18 +112,19 @@ self.addEventListener('activate', e => {
             console.log('Service Worker: Clearing Old Cache', name);
             return caches.delete(name);
           }
+          return undefined;
         })
       );
-      await self.clients.claim();
+      await globalThis.clients.claim();
     })()
   );
 });
-self.addEventListener('fetch', event => {
+globalThis.addEventListener('fetch', event => {
   const request = event.request;
   const url = new URL(request.url);
 
   // 仅处理同源请求
-  const isSameOrigin = url.origin === self.location.origin;
+  const isSameOrigin = url.origin === globalThis.location.origin;
 
   // 1) 导航请求:沿用现有逻辑
   if (request.mode === 'navigate') {
@@ -140,13 +148,13 @@ self.addEventListener('fetch', event => {
         const cache = await caches.open(noHashCacheName);
         const cached = await cache.match(request);
         const networkPromise = fetch(request)
-          .then(resp => {
+          .then(async resp => {
             if (resp && resp.ok) {
               const len = Number(resp.headers.get('content-length') || '0');
               if (len === 0 || len <= MAX_IMG_RESPONSE_SIZE) {
-                cache.put(request, resp.clone());
+                await cache.put(request, resp.clone());
                 // 限制缓存条数,删除最早的
-                enforceCacheLimit(noHashCacheName, Math.floor(MAX_NOHASH_ENTRIES * DEVICE_FACTOR));
+                await enforceCacheLimit(noHashCacheName, Math.floor(MAX_NOHASH_ENTRIES * DEVICE_FACTOR));
               }
             }
             return resp;
@@ -173,8 +181,8 @@ self.addEventListener('fetch', event => {
       if (resp && resp.ok) {
         const len = Number(resp.headers.get('content-length') || '0');
         if (len === 0 || len <= MAX_IMG_RESPONSE_SIZE) {
-          cache.put(request, resp.clone());
-          enforceCacheLimit(IMG_HASH_CACHE_NAME, Math.floor(MAX_HASH_ENTRIES * DEVICE_FACTOR));
+          await cache.put(request, resp.clone());
+          await enforceCacheLimit(IMG_HASH_CACHE_NAME, Math.floor(MAX_HASH_ENTRIES * DEVICE_FACTOR));
         }
       }
       return resp;
@@ -184,7 +192,9 @@ self.addEventListener('fetch', event => {
 
 async function handleRequest(request, url) {
   const cachedResponse = await caches.match(request);
-  const originalDomain = await safeGetCookie('ORIGINAL_DOMAIN');
+  const storedOriginalDomain = await safeGetCookie('ORIGINAL_DOMAIN');
+  const originalDomain = getAllowedBackupDomain(storedOriginalDomain);
+  if (storedOriginalDomain && !originalDomain) await safeDeleteCookie('ORIGINAL_DOMAIN');
   try {
     // 如果缓存中有匹配,则继续请求网络,并更新缓存
     return await fetchAndUpdateCache(request, originalDomain, url);
@@ -218,7 +228,7 @@ async function enforceCacheLimit(cacheName, maxEntries) {
 async function fetchAndUpdateCache(request, ORIGINAL_DOMAIN, url) {
   try {
     const networkResponse = await fetch(request);
-    if (ORIGINAL_DOMAIN && ORIGINAL_DOMAIN !== url.origin && url.origin === self.location.origin) {
+    if (ORIGINAL_DOMAIN && ORIGINAL_DOMAIN !== url.origin && url.origin === globalThis.location.origin) {
       await safeDeleteCookie('ORIGINAL_DOMAIN');
     }
     return networkResponse;
@@ -228,7 +238,7 @@ async function fetchAndUpdateCache(request, ORIGINAL_DOMAIN, url) {
 }
 
 async function handleBackupDomain(request, ORIGINAL_DOMAIN, url) {
-  if (ORIGINAL_DOMAIN && ORIGINAL_DOMAIN !== url.origin && url.origin === self.location.origin) {
+  if (ORIGINAL_DOMAIN && ORIGINAL_DOMAIN !== url.origin && url.origin === globalThis.location.origin) {
     const backupResponse = await judgeActiveDomain(ORIGINAL_DOMAIN, request);
     if (backupResponse) return backupResponse;
   }
@@ -241,28 +251,30 @@ async function fetchFromBackup(request) {
     const response = await judgeActiveDomain(domain, request);
     if (response) return response;
   }
-  // return await fetchFromLatestBackupAddress(request);
+  return new Response('Service Unavailable', { status: 503 });
 }
 
 async function judgeActiveDomain(url, originalRequest) {
+  const backupDomain = getAllowedBackupDomain(url);
+  if (!backupDomain) return undefined;
   try {
-    const newUrl = originalRequest.url.replace(self.location.origin, url);
-    const newRequest = new Request(newUrl);
+    const newUrl = originalRequest.url.replace(globalThis.location.origin, backupDomain);
+    const newRequest = new Request(newUrl, originalRequest);
     const response = await fetch(newRequest);
     if (response.ok) {
       // 成功从备用域名获取资源,更新 activeDomain,将请求的html 存入缓存
-      await safeSetCookie('ORIGINAL_DOMAIN', url);
+      await safeSetCookie('ORIGINAL_DOMAIN', backupDomain);
 
       // 刷新界面
       return response.text().then(body => {
         // 定义路径前缀映射表
         const pathReplacements = {
-          '/js': `${url}/js`,
-          '/css': `${url}/css`,
-          '/favicon': `${url}/favicon`,
-          '/cdn': `${url}/cdn`,
-          '/img': `${url}/img`,
-          '/manifest': `${url}/product/manifest`
+          '/js': `${backupDomain}/js`,
+          '/css': `${backupDomain}/css`,
+          '/favicon': `${backupDomain}/favicon`,
+          '/cdn': `${backupDomain}/cdn`,
+          '/img': `${backupDomain}/img`,
+          '/manifest': `${backupDomain}/product/manifest`
         };
 
         // 动态替换规则
@@ -271,21 +283,22 @@ async function judgeActiveDomain(url, originalRequest) {
           const regex = new RegExp(`(src|href)="${prefix}`, 'g');
           newBody = newBody.replace(regex, `$1="${replacement}`);
         }
-        newBody = newBody.replace(/src="\.\//g, `src="${url}/`);
-        newBody = newBody.replace(/href="\.\//g, `href="${url}/`);
-        return new Response(newBody, {
-          headers: response.headers
-        });
+        newBody = newBody.replace(/src="\.\//g, `src="${backupDomain}/`);
+        newBody = newBody.replace(/href="\.\//g, `href="${backupDomain}/`);
+        const headers = new Headers(response.headers);
+        headers.delete('content-length');
+        headers.delete('content-encoding');
+        return new Response(newBody, { status: response.status, statusText: response.statusText, headers });
       });
     } else {
-      console.log(`Backup domain ${url} returned status: ${response.status}`);
+      console.log(`Backup domain ${backupDomain} returned status: ${response.status}`);
     }
   } catch (error) {
-    console.log(`Backup domain ${url} failed:`, error);
+    console.log(`Backup domain ${backupDomain} failed:`, error);
   }
 }
 
-self.addEventListener('push', function (event) {
+globalThis.addEventListener('push', function (event) {
   if (event.data) {
     var notificationData = event.data.json();
     var title = notificationData.title;
@@ -306,17 +319,23 @@ self.addEventListener('push', function (event) {
       }
     };
 
-    event.waitUntil(self.registration.showNotification(title, options));
+    event.waitUntil(globalThis.registration.showNotification(title, options));
   }
 });
 
 // 处理点击消息
-self.addEventListener('notificationclick', function (event) {
+globalThis.addEventListener('notificationclick', function (event) {
   event.notification.close(); // 关闭推送通知
   // 获取推送通知的数据
   var notificationData = event.notification.data;
   // 获取要跳转的页面链接
-  var targetPage = notificationData.url; // 假设推送通知的数据中包含了要跳转的页面链接
-  // 执行页面跳转操作
-  event.waitUntil(clients.openWindow(targetPage));
+  var targetPage = notificationData?.url; // 假设推送通知的数据中包含了要跳转的页面链接
+  try {
+    const targetUrl = new URL(targetPage || '/', globalThis.location.origin);
+    if (targetUrl.protocol !== 'http:' && targetUrl.protocol !== 'https:') return;
+    // 执行页面跳转操作
+    event.waitUntil(globalThis.clients.openWindow(targetUrl.href));
+  } catch (error) {
+    console.error('[Service Worker] Invalid notification URL:', error);
+  }
 });

+ 1 - 1
public/pwa/startup.html

@@ -144,7 +144,7 @@
                 setTimeout(function () {
                     window.close();
                 });
-                window.open("target.html" + location.search);
+                window.open("target.html" + window.location.search, "_blank", "noopener,noreferrer");
             });
         }
     }, 100);

+ 9 - 7
public/service-worker.js

@@ -1,7 +1,9 @@
-self.addEventListener('install', (event) => {
-    console.log('Service Worker install success');
-  });
-  
-  self.addEventListener('activate', (event) => {
-    console.log('Service Worker active');
-  });
+/* global globalThis */
+
+globalThis.addEventListener('install', () => {
+  console.log('Service Worker install success');
+});
+
+globalThis.addEventListener('activate', () => {
+  console.log('Service Worker active');
+});

+ 25 - 8
scripts/generate-sites.js

@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-env node */
 /**
  * 多品牌静态首页生成器(构建后处理)
  * ============================================================
@@ -23,6 +23,7 @@
  */
 const fs = require("fs");
 const path = require("path");
+const vm = require("vm");
 
 const ROOT = path.resolve(__dirname, "..");
 // 配置单一数据源:public/js/load.m.js(不在别处复制一份)。
@@ -42,7 +43,8 @@ const TITLE_SUFFIX = "Online Slots & Real Money Games";
  * sites_config / regions / themes / res_path 都是 IIFE 内的私有 const,无法 require。
  * 但从 `const res_path` 到 `const getPathPrefix`(第一个用到 window/document 的函数)之间,
  * 全是纯数据声明(res_path、各主题对象、regions、DEFAULT_REGION、sites_config),
- * 不依赖任何浏览器 API。整段切出来用 new Function 求值即可,无需脆弱的正则解析对象字面量。
+ * 不依赖任何浏览器 API。整段在禁用动态代码生成、无 Node 全局对象的隔离上下文中求值,
+ * 避免构建时把配置文件当作拥有 Node 权限的任意代码执行。
  */
 function extractConfig(src) {
   const start = src.indexOf("const res_path");
@@ -51,21 +53,36 @@ function extractConfig(src) {
     throw new Error("[generate-sites] 无法在 load.m.js 中定位配置数据块(res_path … getPathPrefix)");
   }
   const block = src.slice(start, end);
-  // eslint-disable-next-line no-new-func
-  const fn = new Function(block + "\nreturn { res_path, sites_config, regions, DEFAULT_REGION };");
-  return fn();
+  const result = evaluateData(
+    block + "\n;({ res_path, sites_config, regions, DEFAULT_REGION });",
+    "config"
+  );
+  if (!result || typeof result.sites_config !== "object" || typeof result.regions !== "object") {
+    throw new Error("[generate-sites] 配置数据结构无效");
+  }
+  return result;
+}
+
+function evaluateData(source, label) {
+  const context = vm.createContext(Object.create(null), {
+    name: `generate-sites-${label}`,
+    codeGeneration: { strings: false, wasm: false },
+  });
+  const result = vm.runInContext(`"use strict";\n${source}`, context, { timeout: 1000 });
+  // Strip cross-realm prototypes and retain JSON-compatible configuration data only.
+  return JSON.parse(JSON.stringify(result));
 }
 
 /**
  * HOST_ALIAS 在 getSiteConfig() 内部,单独用正则切出对象字面量再求值。
- * 该字面量没有嵌套大括号,第一个 `}` 即结束。允许内部含注释/尾逗号(new Function 能解析)。
+ * 该字面量没有嵌套大括号,第一个 `}` 即结束。允许内部含注释/尾逗号。
  */
 function extractHostAlias(src) {
   const m = src.match(/const\s+HOST_ALIAS\s*=\s*(\{[\s\S]*?\})\s*;/);
   if (!m) return {};
   try {
-    // eslint-disable-next-line no-new-func
-    return new Function("return (" + m[1] + ");")();
+    const result = evaluateData(`(${m[1]});`, "host-alias");
+    return result && typeof result === "object" ? result : {};
   } catch (e) {
     console.warn("[generate-sites] 解析 HOST_ALIAS 失败,按空表处理:" + e.message);
     return {};

+ 56 - 48
src/App.js

@@ -102,7 +102,6 @@ import {
     reqLuckyRollVIPFreeInfo,
     reqCouponList,
     reqWithdrawBaseInfo,
-    reqRedemptionCodeInfo,
     reqEmailInfo,
 } from "@features/api";
 import { getPayFinishFlag, resetPayFinishFlag } from "@features/paySlice";
@@ -138,7 +137,7 @@ import useGlobalClick from "@hooks/useGlobalClick";
 import SoundHelp from "@utils/SoundManger";
 import { ROUTE_PATH } from "@constants/routes";
 import { getLogicalPathname } from "@api/request";
-import { getCountryCode, onChangeUser } from "@features/actions/mainAction";
+import { onChangeUser } from "@features/actions/mainAction";
 import { checkBrowserNotSupport, isInFBBrowser, isShowInstallBox, isTWA } from "@utils/UAUtils";
 import WMaintainBox from "@widgets/WMaintainBox";
 import WDownBox from "@widgets/WInstallBox/WDownBox";
@@ -164,6 +163,8 @@ const cleanJumpApkSearch = (search = "") => {
     try {
         const params = new URLSearchParams(search.startsWith("?") ? search : `?${search}`);
         params.delete("jumpapk");
+        params.delete("api_secret");
+        params.delete("w_lci");
         const next = params.toString();
         return next ? `?${next}` : "";
     } catch (e) {
@@ -175,8 +176,10 @@ const cleanJumpApkStoredParams = () => {
         const urlvars = localStorageGet("urlvars", "");
         if (urlvars) {
             const json = JSON.parse(urlvars);
-            if (json && Object.prototype.hasOwnProperty.call(json, "jumpapk")) {
+            if (json && (Object.prototype.hasOwnProperty.call(json, "jumpapk") || Object.prototype.hasOwnProperty.call(json, "api_secret") || Object.prototype.hasOwnProperty.call(json, "w_lci"))) {
                 delete json.jumpapk;
+                delete json.api_secret;
+                delete json.w_lci;
                 localStorageSet("urlvars", JSON.stringify(json));
             }
         }
@@ -186,7 +189,7 @@ const cleanJumpApkStoredParams = () => {
 
     try {
         const startParams = window.localStorage.getItem(LOCAL_START_PARAMS);
-        if (startParams && startParams.includes("jumpapk")) {
+        if (startParams && (startParams.includes("jumpapk") || startParams.includes("api_secret") || startParams.includes("w_lci"))) {
             const next = cleanJumpApkSearch(startParams);
             if (next) window.localStorage.setItem(LOCAL_START_PARAMS, next);
             else window.localStorage.removeItem(LOCAL_START_PARAMS);
@@ -247,8 +250,8 @@ const App = () => {
     }, []);
 
     const appRef = useRef(null);
-    const { theme, direction, setIsShowWheel, preLocation, gameID, showBrowserNotSupport, setShowBrowserNotSupport, isMobile } = useThemeProvider();
-    const { pathname, search, hash } = useLocation();
+    const { theme, direction, preLocation, gameID, showBrowserNotSupport, setShowBrowserNotSupport, isMobile } = useThemeProvider();
+    const { pathname, hash } = useLocation();
     const gaKey = process.env.REACT_APP_PUBLIC_GA;
     gaKey && ReactGA.initialize(gaKey);
     const navigate = useNavigate();
@@ -265,7 +268,7 @@ const App = () => {
     const popBindPhone = useSelector(getPopBindPhone);
     const [isMaintain, setIsMaintain] = useState(false);
     //是否唤起选择area
-    const [isSelectArea, setIsSelectArea] = useState(false);
+    const isSelectArea = false;
     const { checkFirstPayGift } = usePaymentActions();
     const popPwaBonus = useSelector(getPopPwaBonus);
 
@@ -351,27 +354,14 @@ const App = () => {
         global.checkUrlvars();
         cleanJumpApkStoredParams();
     };
-    const syncCookieFromLaunchParam = () => {
+    const scrubLegacyCookieParam = () => {
         try {
-            const launchCookieParam = getUrlParam("w_lci");
-            if (launchCookieParam == null || launchCookieParam === "") return;
-            const cookieString = decodeURIComponent(atob(launchCookieParam));
-            if (!cookieString) return;
-            const topDomain = window.location.hostname.split('.').slice(-2).join('.');
-            const cookieItems = cookieString.split(";").map((item) => item.trim()).filter(Boolean);
-            cookieItems.forEach((item) => {
-                const splitIndex = item.indexOf("=");
-                if (splitIndex <= 0) return;
-                const cookieKey = item.slice(0, splitIndex).trim();
-                const cookieValue = item.slice(splitIndex + 1).trim();
-                if (!cookieKey) return;
-                document.cookie = `${cookieKey}=${cookieValue}; path=/; domain=.${topDomain}; max-age=10368000;`;
-            });
             const cleanedUrl = new URL(window.location.href);
+            if (!cleanedUrl.searchParams.has("w_lci")) return;
             cleanedUrl.searchParams.delete("w_lci");
             window.history.replaceState(null, "", `${cleanedUrl.pathname}${cleanedUrl.search}${cleanedUrl.hash}`);
         } catch (e) {
-            console.error("Error parsing launch cookie:", e);
+            // Legacy cookie-in-URL migration is intentionally ignored.
         }
     };
     const reProgressierInstallBanner = () => {
@@ -455,7 +445,7 @@ const App = () => {
     };
     const appInitData = () => {
         // scroll to top on route change
-        syncCookieFromLaunchParam();
+        scrubLegacyCookieParam();
         //检查是否维护
         if (checkMaintain()) return;
         if (checkAreaSelect()) return;
@@ -492,8 +482,9 @@ const App = () => {
     };
 
     const checkLaunchHolidaysAct = () => {
-        let paying = localStorageGet("payHolidaysAct", false);
-        if (paying && JSON.parse(paying)) {
+        const paying = localStorageGet("payHolidaysAct", false);
+        const wasPaying = paying === true || paying === "true";
+        if (wasPaying) {
             localStorageRemove("payHolidaysAct");
         } else {
             if (!checkTimestampIsNextDay(LOCAL_HOLIDAYS_ACT_GAME_TS)) return false;
@@ -627,13 +618,14 @@ const App = () => {
         if (window.andapp) return false;
         if (checkIsStandalone() || isTWA()) return false;
         if (blockPopupByUserScore(true)) return false;
-        global?.downloadPrompt.show();
+        if (!global.downloadPrompt?.show) return false;
+        global.downloadPrompt.show();
         return true;
     };
 
     const checkSlotPartner = () => {
         //PS: 如果有需求注册x天之后再显示这个弹窗 直接让后端修改route接口的conf.shrePop就好
-        if (shrePopRef.current != 1) return false;
+        if (Number(shrePopRef.current) !== 1) return false;
         if (isInCurrentWeek(LOCAL_SLOTS_PARTNER_SKIP)) return false;
         if (!checkTimestampIsNextDay(LOCAL_SLOTS_PARTNER_TS)) return false;
         localStorageSet(LOCAL_SLOTS_PARTNER_TS, Date.now());
@@ -798,10 +790,19 @@ const App = () => {
             updateRecallGiftInfo();
         }
     };
+    const appInitDataRef = useRef(appInitData);
+    const reqInitDataRef = useRef(reqInitData);
+    const onGameBackRef = useRef(onGameBack);
+    const checkOpenInstallBoxRef = useRef(checkOpenInstallBox);
+    appInitDataRef.current = appInitData;
+    reqInitDataRef.current = reqInitData;
+    onGameBackRef.current = onGameBack;
+    checkOpenInstallBoxRef.current = checkOpenInstallBox;
+
     //app Effect
     //初始化
     useEffect(() => {
-        appInitData();
+        appInitDataRef.current();
         updateShareUrl();
         // 隐藏首屏 Loading
         window.hideInitialLoading && window.hideInitialLoading();
@@ -814,7 +815,7 @@ const App = () => {
     //     fb 浏览器用户(jumplater 开启场景)补跑一遍初始化数据,否则首页 freeWithdrawInfo
     //     等仍保持初始空对象,导致 HomeWithdraw 的进度条/提现按钮不显示
     useEffect(() => {
-        reqInitData();
+        reqInitDataRef.current();
     }, [userID, jumpLater]);
 
     //jumplater === 2 路由级拦截:用户在 fb/ins 内置浏览器中点击跳转到 #/freewithdraw 或
@@ -835,7 +836,7 @@ const App = () => {
             // jumplater=2 路由级拦截:直接访问 #/paymentmethods 时无具体来源页,回首页即可
             global.showInsBrowserGuide && global.showInsBrowserGuide({ handler: "deposit", args: { sourceRoute: "#/" } });
         }
-    }, [hash, jumpLater]);
+    }, [hash, jumpLater, navigate]);
 
     //跨浏览器迁移回到默认浏览器后:用户登录就绪即检查是否有残留的 pendingInsAction(withdraw / deposit),
     //若有则先弹出"安全验证已通过"确认屏,让用户感知流程连贯;点击继续后再把路由切到对应页面
@@ -882,14 +883,14 @@ const App = () => {
                 doNavigate();
             }
         }
-    }, [userID]);
+    }, [navigate, userID]);
     //监听上一次的路由
     useEffect(() => {
         if (preLocation) {
             let pathname = preLocation.pathname;
             if (pathname && pathname.startsWith(ROUTE_PATH.GAME)) {
                 //更新金币
-                onGameBack();
+                onGameBackRef.current();
             }
         }
         //检查是否维护
@@ -906,7 +907,7 @@ const App = () => {
             //停止bgm
             SoundHelp.stopBgm();
         }
-    }, [gameID]);
+    }, [dispatch, gameID]);
     //监听被服务器踢掉
     useEffect(() => {
         if (serverKick === 1) {
@@ -914,10 +915,10 @@ const App = () => {
                 navigate(ROUTE_PATH.HOME, { replace: true });
             }
         }
-    }, [serverKick]);
+    }, [navigate, pathname, serverKick]);
     //监听安装提示
     useEffect(() => {
-        checkOpenInstallBox();
+        checkOpenInstallBoxRef.current();
     }, [promoteInstall]);
     //监听打开pay
     useEffect(() => {
@@ -925,7 +926,7 @@ const App = () => {
             navigate(ROUTE_PATH.PAY, { replace: true });
             dispatch(updateOpenPay(0));
         }
-    }, [openPay]);
+    }, [dispatch, navigate, openPay]);
     useEffect(() => {
         if (payFinishFlag === 1) {
             if (hash.indexOf(ROUTE_PATH.FIRSTDEPOSIT) > -1) {
@@ -953,13 +954,13 @@ const App = () => {
             }, 0);
             dispatch(resetPayFinishFlag());
         }
-    }, [payFinishFlag]);
+    }, [dispatch, hash, navigate, payFinishFlag]);
 
     const backHomePopup = () => {
         if (userID <= 0) return;
-        if (pathname != ROUTE_PATH.HOME) return;
+        if (pathname !== ROUTE_PATH.HOME) return;
         if (hash != null && hash.length > 0) return;
-        if (gameID != 0) return;
+        if (Number(gameID) !== 0) return;
 
         if (global.ignorePopup) {
             global.ignorePopup = false;
@@ -969,37 +970,44 @@ const App = () => {
         // console.error(`>>>>>>>>> pathname: ${preLocation.pathname} hash: ${preLocation.hash}`)
         launchPopupflows();
     };
+    const backHomePopupRef = useRef(backHomePopup);
+    backHomePopupRef.current = backHomePopup;
 
     useEffect(() => {
-        backHomePopup();
+        backHomePopupRef.current();
     }, [pathname, hash]);
     //app Effect
 
     useEffect(() => {
         // console.log("onUpdateGameTime 监听已注册");
-        global.onUpdateGameTime = (golds) => {
+        const onUpdateGameTime = () => {
             const state = store.getState();
             const gameID = state.main.gameID;  // 看你 updateGameCardInfo 存哪了
-            let user_info = userInfoRef.current;
-            const last = localStorageGet(LOCAL_ENTER_GAME_TIME, "0");
-            const lastGameID = parseInt(localStorageGet(LOCAL_ENTER_GAME_ID, "0"));
+            const user_info = userInfoRef.current;
+            const last = Number.parseInt(localStorageGet(LOCAL_ENTER_GAME_TIME, "0"), 10);
+            const lastGameID = Number.parseInt(localStorageGet(LOCAL_ENTER_GAME_ID, "0"), 10);
             const now = Date.now();
             // console.log("onUpdateGameTime 触发:", last, user_info.UserID, gameID, isSendGameTimeRef.current, lastGameID);
             // now - parseInt(last) >= 3600000 -> 距离上次记录游戏时间已经超过1小时,非正常情况
             // !gameID -> 当前没有游戏ID,非正常情况
             // isSendGameTimeRef.current === gameID -> 已经记录过当前游戏的时间了,避免重复记录
             // lastGameID !== gameID -> 上次记录的游戏ID和当前游戏ID不一致,可能是在游戏界面刷新,或者用户切换了游戏但没有正常记录时间
-            if (now - parseInt(last) >= 3600000 || !gameID || isSendGameTimeRef.current === gameID || lastGameID !== gameID) return;
+            if (!Number.isFinite(last) || !Number.isFinite(lastGameID) || !user_info?.UserID ||
+                now - last >= 3600000 || !gameID || isSendGameTimeRef.current === gameID || lastGameID !== gameID) return;
 
             dispatch(reqEnterGameInterval({
                 userID: user_info.UserID,
                 game_id: gameID,  // 👈 从 store 拿当前游戏
-                time: now - parseInt(last),
+                time: now - last,
             }));
             isSendGameTimeRef.current = gameID;
             // console.log("reqEnterGameInterval:", user_info.UserID, gameID, lastGameID, now - parseInt(last));
         };
-    }, []);
+        global.onUpdateGameTime = onUpdateGameTime;
+        return () => {
+            if (global.onUpdateGameTime === onUpdateGameTime) delete global.onUpdateGameTime;
+        };
+    }, [dispatch]);
 
     const renderLayout = () => {
         //fb 内打开引导去默认浏览器

+ 62 - 19
src/Global.js

@@ -27,6 +27,44 @@ global.ignorePopup = false;
 global.freeTxTotal = 40;
 global.bindPhoneCallback = null;
 global.tempPageTitle = null;
+
+const sanitizeUrlVars = (value) => {
+    if (!value || typeof value !== "object" || Array.isArray(value)) return {};
+    const sanitized = { ...value };
+    delete sanitized.api_secret;
+    delete sanitized.w_lci;
+    return sanitized;
+};
+
+const scrubFirebaseSecretFromAddress = () => {
+    try {
+        const url = new URL(window.location.href);
+        if (!url.searchParams.has("api_secret")) return;
+        url.searchParams.delete("api_secret");
+        window.history.replaceState(window.history.state, "", url.toString());
+    } catch (e) {
+        // Keep startup resilient in embedded browsers with incomplete History/URL support.
+    }
+};
+
+const scrubStoredFirebaseSecret = () => {
+    try {
+        const cached = localStorageGet("myfire", "");
+        if (!cached) return;
+        const sanitized = sanitizeUrlVars(JSON.parse(cached));
+        if (Object.keys(sanitized).length > 0) {
+            localStorageSet("myfire", JSON.stringify(sanitized));
+        } else {
+            localStorage.removeItem("myfire");
+        }
+    } catch (e) {
+        try {
+            localStorage.removeItem("myfire");
+        } catch (storageError) {
+            // Ignore disabled/unavailable storage; this cache is optional.
+        }
+    }
+};
 global.msgBox = null;
 global.msgBoxShow = (params) => {
     if (global.msgBox) global.msgBox.show(params);
@@ -47,7 +85,13 @@ global.FF = null;
 global.getWDPass = () => {
     let pass = localStorageGet(LOCAL_WD_PASS, "");
     if (pass != null && pass !== "") {
-        pass = decrypt(pass);
+        try {
+            pass = decrypt(pass);
+        } catch (e) {
+            localStorageSet(LOCAL_WD_PASS, "");
+            localStorageSet(LOCAL_WD_PASS_SET_TIME, "");
+            return "";
+        }
         let lsettime = localStorageGet(LOCAL_WD_PASS_SET_TIME, "");
         if (lsettime != null) {
             lsettime = Number(lsettime);
@@ -250,7 +294,8 @@ global.getLocalAreaID = () => {
     if (areaID === 0) {
         let dAreaID = global.checkDomainArea();
         if (dAreaID === 0) {
-            console.error('areae id not found, use default value 1');
+            // Local development and new domains legitimately fall back to the default area.
+            console.debug('area id not found, use default value 1');
             dAreaID = 1;
         }
         if (dAreaID !== 0) {
@@ -331,22 +376,20 @@ global.hideSaveAccount = () => {
 //初始化urlvars
 global.checkUrlvars = () => {
     let urlvars = localStorageGet("urlvars", "");
-    if (urlvars == null || urlvars === "") {
-        let json = getUrlParamsAsJsonManual();
-        if (json != null) {
-            urlvars = JSON.stringify(json);
-            localStorageSet("urlvars", urlvars);
-        }
-    }
-    else {
-        let json = getUrlParamsAsJsonManual();
-        if (json != null) {
-            let orijson = JSON.parse(urlvars);
-            orijson = Object.assign({}, orijson, json);
-            urlvars = JSON.stringify(orijson);
-            localStorageSet("urlvars", urlvars);
+    let cached = {};
+    if (urlvars) {
+        try {
+            cached = sanitizeUrlVars(JSON.parse(urlvars));
+        } catch (e) {
+            cached = {};
         }
     }
+    const current = sanitizeUrlVars(getUrlParamsAsJsonManual());
+    const merged = Object.assign({}, cached, current);
+    urlvars = Object.keys(merged).length > 0 ? JSON.stringify(merged) : "";
+    localStorageSet("urlvars", urlvars);
+    scrubStoredFirebaseSecret();
+    scrubFirebaseSecretFromAddress();
     return urlvars;
 };
 global.getUrlvars = () => {
@@ -548,13 +591,13 @@ global.showMobileCodePopup = (phone, cb) => {
     global.mobileCodePopup && global.mobileCodePopup.show(phone, cb);
 };
 global.showMobileCodeAsync = async (phone) => {
-    let task = new Promise((resolve, rejesct) => {
+    const task = new Promise((resolve) => {
         global.showMobileCodePopup(phone, (res) => {
             resolve(res);
         });
     });
 
-    return await task;
+    return task;
 };
 
 global.showHWPopup = () => {//主页免费提现弹窗
@@ -598,4 +641,4 @@ global.showSsqChat = () => {
 }
 global.showWDSSecurityCheck = (pixType, amount, callBack) => {
     global.wdSecurityCheck && global.wdSecurityCheck.show(pixType, amount, callBack);
-}
+}

+ 61 - 43
src/api/request.js

@@ -7,7 +7,16 @@ import { LOCAL_ACTIVITY_C, LOCAL_IS_LOGIN, LOCAL_SIGN, LOCAL_WHEEL_INVITE_CODE }
 import { toast } from "react-toastify";
 import LZString from "lz-string";
 import { isStandalone, isTWA } from "@utils/UAUtils";
-import { normalizeClientError, reportClientLog } from "@utils/clientLog";
+import { normalizeClientError, reportClientLog, sanitizeUrlForLog } from "@utils/clientLog";
+
+const SENSITIVE_LOG_KEY = /(pass(word)?|sign|token|secret|authorization|cookie|phone|email|cpf|cashapp|paypal)/i;
+const stringifyForDebug = (value) => {
+    try {
+        return JSON.stringify(value, (key, item) => SENSITIVE_LOG_KEY.test(key) ? "[REDACTED]" : item);
+    } catch (e) {
+        return "[unserializable]";
+    }
+};
 
 const getSign = () => {
     const sign = localStorageGet(LOCAL_SIGN, "");
@@ -128,7 +137,7 @@ service.interceptors.request.use(
     },
     error => {
         // Do something with request error
-        Promise.reject(error);
+        return Promise.reject(error);
     }
 );
 
@@ -137,16 +146,27 @@ service.interceptors.response.use(
     response => {
         let data = response.data;
         if (typeof data === "string") {
-            data = LZString.decompressFromBase64(data);
-            try {
-                response.data = JSON.parse(data);
-            }
-            catch (error) {
-                console.log(error);
+            const decompressed = LZString.decompressFromBase64(data);
+            const candidates = [decompressed, data];
+            for (const candidate of candidates) {
+                if (typeof candidate !== "string" || candidate === "") continue;
+                try {
+                    response.data = JSON.parse(candidate);
+                    data = response.data;
+                    break;
+                } catch (error) {
+                    // Try the next representation; preserve the original string if neither parses.
+                }
             }
         }
-        let host = window.location.host;
-        if (host.startsWith("localhost")) console.log(response.request.responseURL, response.data);
+        if (data && typeof data === "object" && typeof data.code === "string") {
+            const code = data.code.trim();
+            if (/^-?\d+$/.test(code)) data.code = Number(code);
+        }
+        if (isDebugEnv()) {
+            const responseUrl = response.request?.responseURL || response.config?.url || "";
+            console.debug(sanitizeUrlForLog(responseUrl), stringifyForDebug(response.data));
+        }
         const requestUrl = response.config && response.config.url;
         if (!isLogRequest(requestUrl) && data && data.code != null && data.code !== 200) {
             reportClientLog("api_response_error", {
@@ -219,70 +239,68 @@ const ensureSign = async (requestUrl) => {
 
 const get = async (params = {}) => {
     await ensureSign(params.url);
-    if (params.params == null) params.params = {};
-    let comParmas = getComParams();
-    params.params = Object.assign(params.params, comParmas);
+    const requestParams = {
+        ...params,
+        params: { ...(params.params || {}), ...getComParams() }
+    };
     return service({
         method: 'get',
-        ...params
+        ...requestParams
     });
 }
 const post = async (params = {}) => {
     await ensureSign(params.url);
-    if (params.data == null) params.data = {};
-    let comParmas = getComParams();
-    params.data = Object.assign(params.data, comParmas);
-    console.warn(`post params: ${JSON.stringify(params.data)}  url:${params.url}`)
+    const requestParams = {
+        ...params,
+        data: { ...(params.data || {}), ...getComParams() }
+    };
+    if (isDebugEnv()) console.debug(`post params: ${stringifyForDebug(requestParams.data)}  url:${params.url}`);
     return service({
         method: 'post',
-        ...params
+        ...requestParams
     });
 }
 const put = async (params = {}) => {
     await ensureSign(params.url);
-    if (params.data == null) params.data = {};
-    let comParmas = getComParams();
-    params.data = Object.assign(params.data, comParmas);
+    const requestParams = {
+        ...params,
+        data: { ...(params.data || {}), ...getComParams() }
+    };
     return service({
         method: 'put',
-        ...params
+        ...requestParams
     });
 }
 const adelete = async (params = {}) => {
     await ensureSign(params.url);
-    if (params.data == null) params.data = {};
-    let comParmas = getComParams();
-    params.data = Object.assign(params.data, comParmas);
+    const requestParams = {
+        ...params,
+        data: { ...(params.data || {}), ...getComParams() }
+    };
     return service({
         method: 'delete',
-        ...params
+        ...requestParams
     });
 }
 const buildQuery = (param) => {
-    if (param != null) {
-        if (typeof (param) == "string") {
-
-        } else {
-            var query = [];
-            for (var k in param) {
-                let v = param[k];
-                if (typeof (v) == "object") v = JSON.stringify(v);
-                query.push(k + "=" + escape(v));
-            }
-            param = query.join("&");
-        }
+    if (param == null || typeof param === "string") return param;
+    const query = new URLSearchParams();
+    for (const key in param) {
+        let value = param[key];
+        if (value != null && typeof value === "object") value = JSON.stringify(value);
+        query.append(key, value == null ? "" : String(value));
     }
-
-    return param;
+    return query.toString();
 }
 const getAPKUrl = (apkUrl = null) => {
     let params = getComParams();
     let url = apkUrl == null ? BASE_API + APP_GAME_APK : apkUrl;
+    const separator = url.includes("?") ? "&" : "?";
     if (!isDebugEnv()) {
-        url += "?lz=" + LZString.compressToBase64(JSON.stringify(params));
+        url += separator + "lz=" + encodeURIComponent(LZString.compressToBase64(JSON.stringify(params)));
     }
     else {
-        url += "?" + buildQuery(params);
+        url += separator + buildQuery(params);
     }
     return url;
 }

+ 2 - 2
src/components/AutoBanner/BannerPagination.jsx

@@ -7,14 +7,14 @@ const BannerPagination = ({pageCount,swiper}) => {
         if (swiper!=null){
             let activeIndex=swiper.realIndex;
             if (activeIndex>pageCount)activeIndex=activeIndex%pageCount;
-            return activeIndex==index;
+        return activeIndex === index;
         }
         return false;
     }
     const changeIndex =(index)=>{
         // slideTo
         if (swiper!=null){
-            if (swiper.realIndex!=index){
+        if (swiper.realIndex !== index) {
                 swiper.slideTo(index);
             }
         }

+ 19 - 17
src/components/AutoBanner/index.jsx

@@ -13,23 +13,25 @@ import BannerPagination from "@components/AutoBanner/BannerPagination";
 import { useNavigate } from "react-router-dom";
 import styles from './styles.module.scss';
 import { ROUTE_PATH } from "@constants/routes";
-import { windowOpenShare } from "@utils/helpers";
+import { getSafeHttpUrl, windowOpenShare } from "@utils/helpers";
 
 const AutoBanner = ({ data, perview = 3, spaceBetween = 15, delay = 3000, speed = 1000, getExtraWidget = null }) => {
     const [swiper, setSwiper] = useState(null);
     const { isMobile } = useThemeProvider();
     const navigate = useNavigate();
-    if (isMobile()) perview = 1;
-    const [activeIndex, setActiveIndex] = useState(0);
+    const bannerData = Array.isArray(data) ? data : [];
+    const itemsPerView = isMobile() ? 1 : Math.max(1, Math.floor(Number(perview)) || 1);
+    const [, setActiveIndex] = useState(0);
     useEffect(() => {
         if (swiper) {
             swiper.update();
         }
     }, [swiper]);
     const onClickNav = (item) => {
-        let link = item.link;
-        if (link.startsWith("http")) {
-            windowOpenShare(link);
+        let link = typeof item?.link === "string" ? item.link.trim() : "";
+        if (/^https?:\/\//i.test(link)) {
+            const safeUrl = getSafeHttpUrl(link);
+            if (safeUrl) windowOpenShare(safeUrl);
         } else if (link.indexOf('/') === -1) {
             global[link] && global[link]();
         } else {
@@ -40,15 +42,15 @@ const AutoBanner = ({ data, perview = 3, spaceBetween = 15, delay = 3000, speed
         }
 
         // adjust 点击banner图片
-        if (item.bid == 1) {
+        if (Number(item.bid) === 1) {
             global.tracker.trackEvent("click_banner1");
             global.tracker.trackEvent("click_banner1_count");
         }
-        else if (item.bid == 2) {
+        else if (Number(item.bid) === 2) {
             global.tracker.trackEvent("click_banner2");
             global.tracker.trackEvent("click_banner2_count");
         }
-        else if (item.bid == 3) {
+        else if (Number(item.bid) === 3) {
             global.tracker.trackEvent("click_banner3");
             global.tracker.trackEvent("click_banner3_count");
         }
@@ -63,27 +65,27 @@ const AutoBanner = ({ data, perview = 3, spaceBetween = 15, delay = 3000, speed
         return controls;
     }
     const renderPagination = () => {
-        let pageCount = Math.ceil(data.length / perview);
+        let pageCount = Math.ceil(bannerData.length / itemsPerView);
         return (
             <BannerPagination pageCount={pageCount} swiper={swiper}></BannerPagination>
         )
     }
     const renderItems = () => {
         let itemsData = [];
-        for (let i = 0; i < data.length; i += perview) {
+        for (let i = 0; i < bannerData.length; i += itemsPerView) {
             let s = i;
-            let e = i + perview;
-            itemsData.push(data.slice(s, e));
+            let e = i + itemsPerView;
+            itemsData.push(bannerData.slice(s, e));
         }
         let lastData = itemsData[itemsData.length - 1];
         //补齐最后一组
         if (lastData != null) {
             let len = lastData.length;
-            if (len < perview) {
-                let temp = data.slice(0);
-                let addSize = perview - len;
+            if (len < itemsPerView) {
+                let temp = bannerData.slice(0);
+                let addSize = itemsPerView - len;
                 for (let i = 0; i < addSize; i++) {
-                    if (temp.length == 0) temp = data.slice(0);
+                    if (temp.length === 0) temp = bannerData.slice(0);
                     lastData.push(temp.shift());
                 }
             }

+ 23 - 24
src/components/AutoWinList/index.jsx

@@ -1,6 +1,6 @@
 import styles from './styles.module.scss';
 import {useThemeProvider} from "@contexts/themeContext";
-import React, {useRef, useState,useEffect} from 'react';
+import React, {useCallback, useRef, useState,useEffect} from 'react';
 import {Swiper, SwiperSlide} from "swiper/react";
 import {Autoplay, EffectCube, Virtual} from "swiper/modules";
 import lv_1 from "@assets/lvicon/gold_mini.png";
@@ -13,13 +13,14 @@ import LazyImgix from "@components/Imgix/LazyImgix";
 
 
 const i18n=global.i18n;
+const EMPTY_DATA = {};
 const AutoWinList=({data})=>{
     const {isMobile} = useThemeProvider();
     const [currIndex,setCurrIndex]=useState(0);
     const [swiper, setSwiper] = useState(null);
     const navigate=useNavigate();
     const lvicon=[lv_1,lv_2,lv_3];
-    if (data==null)data={};
+    if (data==null)data=EMPTY_DATA;
     // data={
     //     title:"win_list.big_wins",
     //     tabs:[
@@ -121,7 +122,7 @@ const AutoWinList=({data})=>{
                         return(
                             <button onClick={()=>{
                                 changeTab(tab.type);
-                            }} className={`${styles.tab_btn} ${tab.type==currIndex?styles.active:''}`} key={index}>
+                        }} className={`${styles.tab_btn} ${Number(tab.type) === currIndex ? styles.active : ''}`} key={index}>
                                 <div>{i18n.t(tab.title)}</div>
                             </button>
                         )
@@ -130,17 +131,17 @@ const AutoWinList=({data})=>{
             </div>
         )
     }
-    const getAutoList=()=>{
+    const getAutoList=useCallback(()=>{
         let list=[];
         let all=data.data;
         if (all!=null){
-            if (currIndex==0)list=all.slice(0);
+        if (currIndex === 0) list = all.slice(0);
             else
             {
                 for (let i = 0; i < all.length; i++) {
                     let item=all[i];
                     if (item){
-                        if (item.gtype==currIndex){
+                if (Number(item.gtype) === currIndex) {
                             list.push(item);
                         }
                     }
@@ -148,14 +149,14 @@ const AutoWinList=({data})=>{
             }
         }
         return list;
-    }
+    }, [currIndex, data]);
     const containerRef=useRef(null);
     const [rollState,setRollState]=useState(0);
-    const startRoll=()=>{
+    const startRoll=useCallback(function roll(){
         if (containerRef==null)return;
         const container = containerRef.current;
         if (container==null)return;
-        if (container.rollState==1)return;
+        if (container.rollState === 1) return;
         if (container.firstChild==null)return;
         const offsetX=container.firstChild.clientWidth;
         const endIndex=container.endIndex;
@@ -167,11 +168,9 @@ const AutoWinList=({data})=>{
             setRollState(2);
         }
         if (container.rollTimer!=null)clearTimeout(container.rollTimer)
-        container.rollTimer=setTimeout(function () {
-            startRoll();
-        },2000);
-    }
-    const initRollList=()=>{
+        container.rollTimer=setTimeout(roll,2000);
+    }, []);
+    const initRollList=useCallback(()=>{
         if (containerRef==null)return;
         const container = containerRef.current;
         if (container==null)return;
@@ -179,8 +178,8 @@ const AutoWinList=({data})=>{
         container.endIndex=list.length;
         container.currIndex=0;
         startRoll();
-    }
-    const checkReset=()=>{
+    }, [getAutoList, startRoll]);
+    const checkReset=useCallback(()=>{
         const container = containerRef.current;
         if (container!=null){
             const dis=container.scrollLeft;
@@ -191,19 +190,19 @@ const AutoWinList=({data})=>{
             container.currIndex=currIndex;
             container.scrollLeft=currIndex*width;
         }
-    }
+    }, []);
     useEffect(()=>{
         const container = containerRef.current;
         if (container==null)return;
-        if (rollState==0){
+        if (rollState === 0) {
             startRoll();
         }
-        else if (rollState==2){
+        else if (rollState === 2) {
             checkReset();
             setRollState(0);
         }
         container.rollState=rollState;
-    },[rollState])
+    },[checkReset, rollState, startRoll])
     useEffect(()=>{
         let  container=null;
         if (containerRef.current!=null){
@@ -215,7 +214,7 @@ const AutoWinList=({data})=>{
                 if (container.rollTimer!=null)clearInterval(container.rollTimer);
             }
         };
-    },[currIndex]);
+    },[initRollList]);
     const formatGolds=(golds)=>{
         golds=Number(golds);
         let str=numFormatter(golds);
@@ -225,7 +224,7 @@ const AutoWinList=({data})=>{
         let hlist=getAutoList();
         let list=hlist.concat(hlist);
         return (
-            <div onMouseEnter={()=>{setRollState(1)}} onMouseLeave={()=>setRollState(2)} className={`${styles.swiper_container} ${rollState==2?styles.swiper_stop:''}`} ref={containerRef}>
+        <div onMouseEnter={() => { setRollState(1) }} onMouseLeave={() => setRollState(2)} className={`${styles.swiper_container} ${rollState === 2 ? styles.swiper_stop : ''}`} ref={containerRef}>
                 {
                      list.map((item, index) => (
                          <div key={index} className={styles.swiper_item} onClick={()=>{
@@ -238,7 +237,7 @@ const AutoWinList=({data})=>{
                                  className={`${styles.game_icon}`}
                              />
                              <div className={styles.nick_name}>
-                                <img src={lvicon[item.lv-1]}/>
+                                <img alt="" src={lvicon[item.lv-1]}/>
                                 <div className={styles.name_txt}>
                                     {item.nick}
                                 </div>
@@ -298,7 +297,7 @@ const AutoWinList=({data})=>{
                                                 <div className={styles.amount_m}>
                                                     {formatGolds(item.win)}
                                                 </div>
-                                                <img className={styles.coin_m} src={usdimg}/>
+                                                <img alt="" className={styles.coin_m} src={usdimg}/>
                                             </div>
                                         </div>
                                     </div>

+ 2 - 2
src/components/CheckBox/index.jsx

@@ -2,10 +2,10 @@ import styles from './styles.module.scss';
 
 const CheckBox = ({ active, className = '' }) => {
     return (
-        <div className={`${styles.checkbox} ${active && styles.active} ${className}`}>
+        <div className={`${styles.checkbox} ${active ? styles.active : ""} ${className}`}>
 
         </div>
     )
 }
 
-export default CheckBox;
+export default CheckBox;

+ 31 - 9
src/components/CountDown/CountDown.jsx

@@ -7,27 +7,49 @@ const CountDown = ({ times, onEnd, type = 1, className, fitWidth = false, useImg
     const [seconds, setSeconds] = useState("");
     const [leftTimes, setLeftTimes] = useState(times);
     const timer = useRef(null);
+    const onEndRef = useRef(onEnd);
+    const endedRef = useRef(false);
 
+    useEffect(() => {
+        onEndRef.current = onEnd;
+    }, [onEnd]);
+    useEffect(() => {
+        const seconds = Number(times);
+        setLeftTimes(Number.isFinite(seconds) ? Math.max(0, Math.floor(seconds)) : 0);
+    }, [times]);
+    useEffect(() => () => {
+        if (timer.current != null) clearTimeout(timer.current);
+    }, []);
     useEffect(() => {
         if (timer.current != null) {
             clearTimeout(timer.current);
+            timer.current = null;
         }
-    }, [])
-    useEffect(() => {
-        let s = Math.max(0, Math.floor(leftTimes));
+        const value = Number(leftTimes);
+        let s = Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
         setHour(String(Math.floor(s / 3600)).padStart(2, '0'));
         setMinute(String(Math.floor((s % 3600) / 60)).padStart(2, '0'));
         setSeconds(String(s % 60).padStart(2, '0'));
 
-        if (leftTimes === 0) {
-            onEnd && onEnd();
+        if (s <= 0) {
+            if (!endedRef.current) {
+                endedRef.current = true;
+                onEndRef.current?.();
+            }
             return;
         }
+        endedRef.current = false;
 
         timer.current = setTimeout(() => {
-            setLeftTimes(leftTimes - 1);
+            setLeftTimes(current => Math.max(0, Number(current) - 1));
         }, 1000);
-    }, [leftTimes])
+        return () => {
+            if (timer.current != null) {
+                clearTimeout(timer.current);
+                timer.current = null;
+            }
+        };
+    }, [leftTimes]);
 
     if (type === 2) {
         return <span className={className}>{hour}:{minute}:{seconds}</span>;
@@ -41,7 +63,7 @@ const CountDown = ({ times, onEnd, type = 1, className, fitWidth = false, useImg
     }
 
     return (
-        <div className={`${styles.container} ${fitWidth && styles.fit_width}`}>
+        <div className={`${styles.container} ${fitWidth ? styles.fit_width : ''}`}>
             <div className={`${useImgBg ? styles.times_img : styles.times_item} ${className}`}>{hour}</div>:
             <div className={`${useImgBg ? styles.times_img : styles.times_item} ${className}`}>{minute}</div>:
             <div className={`${useImgBg ? styles.times_img : styles.times_item} ${className}`}>{seconds}</div>
@@ -49,4 +71,4 @@ const CountDown = ({ times, onEnd, type = 1, className, fitWidth = false, useImg
     )
 }
 
-export default CountDown;
+export default CountDown;

+ 38 - 69
src/components/CountDown/CountDownText.jsx

@@ -1,6 +1,28 @@
 import { stringFormat } from "@utils/helpers";
 import { useEffect, useRef, useState } from "react";
 
+const formatCountdown = (times, { onlySec, hhmmss, displayDays }) => {
+    if (typeof times !== 'number' || times <= 0) return '0s';
+
+    const totalSeconds = Math.floor(times / 1000);
+    if (onlySec) return `${totalSeconds}s`;
+
+    let h = Math.floor(totalSeconds / 3600);
+    const m = Math.floor((totalSeconds % 3600) / 60).toString().padStart(2, "0");
+    const s = (totalSeconds % 60).toString().padStart(2, "0");
+
+    if (hhmmss) {
+        const d = Math.floor(h / 24);
+        const showDays = d > 0 && displayDays;
+        if (showDays) h -= d * 24;
+        return `${showDays ? `${d}D ` : ""}${h.toString().padStart(2, '0')}:${m}:${s}`;
+    }
+
+    if (h >= 1) return `${h}h ${m}m`;
+    if (Number(m) >= 1) return `${m}m ${s}s`;
+    return `${s}s`;
+};
+
 /**
  * 
  * @param {*} end_ts 结束时间戳  || left_seconds 剩余秒数
@@ -20,96 +42,43 @@ const CountDownText = ({
         timer: null,
     })
 
-    const stopTimer = () => {
-        if (objRef.current.timer) {
-            clearInterval(objRef.current.timer);
-            objRef.current.timer = null;
-        }
-    }
-
-    const formatCountdown = (times) => {
-        if (typeof times !== 'number' || times <= 0) {
-            return '0s';
-        }
-
-        const totalSeconds = Math.floor(times / 1000);
-        if (onlySec) {
-            return `${totalSeconds}s`;
-        }
-
-        // 计算小时、分钟和秒
-        let h = Math.floor(totalSeconds / 3600);
-        let m = Math.floor((totalSeconds % 3600) / 60);
-        let s = totalSeconds % 60;
-
-        m = m.toString().padStart(2, "0");
-        s = s.toString().padStart(2, "0");
-
-
-        if (hhmmss) {
-            let d = Math.floor(h / 24);
-            let show_days = d > 0 && display_days;
-            if (show_days) {
-                h -= d * 24;
-            }
-
-            let days = show_days && `${d}D ` || ""
-            return days + `${h.toString().padStart(2, '0')}:${m}:${s}`
-        }
-
-        // (1) 大于等于1小时:显示 Xh Xm
-        if (h >= 1) {
-            return `${h}h ${m}m`;
-        }
-
-        if (m >= 1) {
-            return `${m}m ${s}s`;
-        }
-        return `${s}s`;
-    }
-
-    const setTimesStr = (str) => {
-        if (onlySec && !str) {
-            str = '0s';
-        }
-        setTimes(str);
-    }
-
-    useEffect(() => {
-        return () => {
-            stopTimer();
-        }
-    }, [])
-
     useEffect(() => {
+        const stopTimer = () => {
+            if (objRef.current.timer) {
+                clearInterval(objRef.current.timer);
+                objRef.current.timer = null;
+            }
+        };
+        const setTimesStr = (str) => setTimes(onlySec && !str ? '0s' : str);
         stopTimer();
 
-        if (end_ts === -1) {
-            end_ts = Date.now() + left_seconds * 1000;
-        }
+        const targetTime = end_ts === -1
+            ? Date.now() + Number(left_seconds || 0) * 1000
+            : end_ts;
 
-        let left = end_ts - Date.now();
+        let left = targetTime - Date.now();
         if (left <= 0) {
             setTimesStr("");
             return;
         }
 
         const updateTimess = () => {
-            let left = end_ts - Date.now();
+            let left = targetTime - Date.now();
             if (left < 0) {
                 stopTimer();
                 setTimesStr("");
                 return;
             }
 
-            let text = formatCountdown(left);
+            let text = formatCountdown(left, { onlySec, hhmmss, displayDays: display_days });
             if (lblTemplate) text = stringFormat(lblTemplate, [text]);
             setTimes(text);
         }
 
         objRef.current.timer = setInterval(updateTimess, 1000)
         updateTimess();
-    }, [end_ts, left_seconds])
+        return stopTimer;
+    }, [display_days, end_ts, hhmmss, lblTemplate, left_seconds, onlySec])
 
     return <span className={className}>
         {times}
@@ -117,4 +86,4 @@ const CountDownText = ({
 }
 
 
-export default CountDownText;
+export default CountDownText;

+ 13 - 7
src/components/CountDown/ExpireTimesText.jsx

@@ -13,20 +13,26 @@ const ExpireTimesText = ({ className, times }) => {
         }
     });
     useEffect(() => {
-        var timer = setInterval(() => {
-            var data = getCountDown(times);
+        let timer = null;
+        const update = () => {
+            const data = getCountDown(times);
             if (data.d > 0) {
                 setTimesText(`${data.d} days ${data.h}:${data.m}:${data.s}`);
             } else {
                 setTimesText(`${data.h}:${data.m}:${data.s}`);
             }
-
-        }, 1000);
+            if (Number(times) * 1000 <= Date.now() && timer != null) {
+                clearInterval(timer);
+                timer = null;
+            }
+        };
+        update();
+        if (Number(times) * 1000 > Date.now()) timer = setInterval(update, 1000);
 
         return () => {
-            if (timer) clearInterval(timer);
+            if (timer != null) clearInterval(timer);
         };
-    }, []);
+    }, [times]);
     return (
         <div className={className}>
             {timesText}
@@ -34,4 +40,4 @@ const ExpireTimesText = ({ className, times }) => {
     );
 };
 
-export default ExpireTimesText;
+export default ExpireTimesText;

+ 3 - 2
src/components/DraggableNode/index.jsx

@@ -14,6 +14,8 @@ const DraggableNode = ({ children, onClick, className }) => {
         setPosition({ x: 0, y: 0 });
     }, [orientation])
 
+    useEffect(() => () => clearTimeout(touchTimer.current), []);
+
     const handleStop = (e, data) => {
         // 吸附到最近点
         const el = nodeRef.current;
@@ -38,7 +40,6 @@ const DraggableNode = ({ children, onClick, className }) => {
 
         touchTimer.current = setTimeout(() => {
             setShouldDrag(true);
-            console.log("handleTouchStart");
             if ("vibrate" in navigator) navigator.vibrate(30);
         }, 220);
     };
@@ -126,4 +127,4 @@ const DraggableNode = ({ children, onClick, className }) => {
 }
 
 
-export default DraggableNode;
+export default DraggableNode;

+ 11 - 10
src/components/DropCoins.jsx

@@ -18,9 +18,7 @@ const DropCoins = ({ walletPos }) => {
             startY = rect.top + rect.height / 2;
         }
 
-        if (!walletPos) {
-            walletPos = { x: rootNode.width - 200, y: 50 };
-        }
+        const destination = walletPos || { x: rootNode.width - 200, y: 50 };
 
         const newCoins = Array.from({ length: 12 }).map((_, i) => {
             const angle = Math.random() * Math.PI * 2;
@@ -34,8 +32,8 @@ const DropCoins = ({ walletPos }) => {
                 offset: 70 * 0.5,
                 aniSpeed: Math.random() + 0.5,
                 aniTimes: 1.3,
-                endX: walletPos.x, // 使用全局共享的钱包坐标
-                endY: walletPos.y,
+                endX: destination.x, // 使用全局共享的钱包坐标
+                endY: destination.y,
                 delay: i * 0.02 + Math.random() * 0.01,
                 scatterX: Math.cos(angle) * radius,
                 scatterY: Math.sin(angle) * radius,
@@ -46,13 +44,13 @@ const DropCoins = ({ walletPos }) => {
         SoundHelp.playSound("sound_bonus");
     }, [walletPos])
 
-    const show = (target) => {
+    const show = useCallback((target) => {
         startCoinFly(target);
-    }
+    }, [startCoinFly]);
 
-    const hide = () => {
+    const hide = useCallback(() => {
         setCoinsList([]);
-    }
+    }, []);
 
     useEffect(() => {
         const dropCoins = {
@@ -60,7 +58,10 @@ const DropCoins = ({ walletPos }) => {
             hide
         };
         global.dropCoins = dropCoins;
-    }, [])
+        return () => {
+            if (global.dropCoins === dropCoins) delete global.dropCoins;
+        };
+    }, [hide, show])
 
     // 金币飞行结束后的回调
     const removeCoin = useCallback((coinId) => {

+ 12 - 11
src/components/GameList/index.jsx

@@ -1,5 +1,5 @@
 import styles from "@components/GameList/styles.module.scss";
-import React, { useEffect, useRef, useState } from "react";
+import React, { useCallback, useEffect, useRef, useState } from "react";
 import GameItem, { GameImgAspectRatio } from "@components/GameItem";
 import { REQ_STATUS } from "@api/config";
 import SmallLoading from "@components/SmallLoading";
@@ -86,6 +86,7 @@ const GameList = ({ data, status, reqMore, user }) => {
     const gameListRef = useRef(null);
     const [gridInfo, setGridInfo] = useState({});
     const { width } = useWindowSize();
+    const mobile = isMobile();
     const handleReset = (e) => {
         setSelected(-1);
         setRoomRow({});
@@ -97,22 +98,19 @@ const GameList = ({ data, status, reqMore, user }) => {
             resetTimer();
         }
     }, [])
-    useEffect(() => {
-        initGridsInfo();
-    }, [gameListRef, width])
-    const getDesktopCol = () => {
+    const getDesktopCol = useCallback(() => {
         if (width >= 1264 && width <= 1564) return 6;
         else if (width >= 963 && width <= 1263) return 5;
         else if (width >= 622 && width <= 962) return 4;
         else if (width <= 621) return 3;
         return 7;
-    }
-    const initGridsInfo = () => {
+    }, [width]);
+    const initGridsInfo = useCallback(() => {
         let parent = gameListRef.current;
         let totalCol = 3;
-        if (!isMobile()) totalCol = getDesktopCol();
+        if (!mobile) totalCol = getDesktopCol();
         let gridBaseRatio = GameImgAspectRatio;
-        let totalWidth = isMobile() ? 100 * totalCol : 160 * totalCol;
+        let totalWidth = mobile ? 100 * totalCol : 160 * totalCol;
         if (parent != null) totalWidth = parent.clientWidth;
         let rowGap = parent != null ? window.getComputedStyle(parent).getPropertyValue('grid-row-gap') : "0px";
         let gap = Number(rowGap.replace("px", ""));
@@ -127,7 +125,10 @@ const GameList = ({ data, status, reqMore, user }) => {
             gridsData[i] = { width, aspectRatio };
         }
         setGridInfo(gridsData);
-    }
+    }, [fontScale, getDesktopCol, mobile]);
+    useEffect(() => {
+        initGridsInfo();
+    }, [initGridsInfo]);
     const resetTimer = () => {
         if (objInfo.timer != null) {
             clearTimeout(objInfo.timer);
@@ -318,7 +319,7 @@ const GameList = ({ data, status, reqMore, user }) => {
         return (
             <div className={styles.show_more}>
                 <button className={styles.more_btn} disabled={isNomore} onClick={() => {
-                    if (status != REQ_STATUS.LOADING) {
+                if (status !== REQ_STATUS.LOADING) {
                         onClickMore();
                     }
                 }}>

+ 2 - 2
src/components/GameRoom/index.jsx

@@ -6,7 +6,7 @@ import {animated, useSpring} from '@react-spring/web';
 import React, {useEffect, useRef, useState} from "react";
 import {useNavigate} from "react-router-dom";
 import {formatCash, localStorageSet} from "@utils/helpers";
-import {LOCAL_GAME_INFO, LOCAL_GAME_URL} from "@constants/localkey";
+import { LOCAL_GAME_URL } from "@constants/localkey";
 import {ROUTE_PATH} from "@constants/routes";
 import styled from 'styled-components/macro';
 import Popup from "@components/Popup";
@@ -166,4 +166,4 @@ const GameRoom = ({open,data,height="5rem",mobile=false,user,onClose,...props})
         </>
     )
 }
-export default GameRoom;
+export default GameRoom;

+ 16 - 16
src/components/GameSmallList/index.jsx

@@ -1,10 +1,9 @@
 import styles from "@components/GameSmallList/styles.module.scss";
-import React, { useRef, useEffect, useState, Fragment } from "react";
+import React, { useCallback, useRef, useEffect, useState, Fragment } from "react";
 import { NavLink } from "react-router-dom";
 import symbol from "@assets/symbol-defs.svg";
 import { useThemeProvider } from "@contexts/themeContext";
 import GameItem, { GameImgAspectRatio } from "@components/GameItem";
-import TabIcon from "@components/TabIcon";
 import styled from 'styled-components/macro';
 import GameRoom from "@components/GameRoom";
 import { useWindowSize } from "react-use";
@@ -24,6 +23,7 @@ const objInfo = {
 };
 const GameSmallList = ({ data, user }) => {
     const { isMobile, fontScale } = useThemeProvider();
+    const mobile = isMobile();
     const gridListRef = useRef(null);
     const gridListMRef = useRef(null);
     const gridConRef = useRef(null);
@@ -105,9 +105,9 @@ const GameSmallList = ({ data, user }) => {
         const gridList = gridListRef.current;
         if (gridList != null) {
             const { scrollLeft, clientWidth, scrollWidth } = gridList;
-            if (scrollLeft == 0) setPageState(0);
+            if (scrollLeft === 0) setPageState(0);
             else {
-                if (scrollWidth - scrollLeft == clientWidth) setPageState(2);
+                if (scrollWidth - scrollLeft === clientWidth) setPageState(2);
                 else setPageState(1);
             }
         }
@@ -184,19 +184,19 @@ const GameSmallList = ({ data, user }) => {
         setRoomRow({});
         setShowPopRoom(false);
     };
-    const getDesktopCol = () => {
+    const getDesktopCol = useCallback(() => {
         if (width >= 1264 && width <= 1564) return 6;
         else if (width >= 963 && width <= 1263) return 5;
         else if (width >= 622 && width <= 962) return 4;
         else if (width <= 621) return 3;
         return 7;
-    };
-    const initGridsInfo = () => {
-        let parent = isMobile() ? gridListMRef.current : gridListRef.current;
+    }, [width]);
+    const initGridsInfo = useCallback(() => {
+        let parent = mobile ? gridListMRef.current : gridListRef.current;
         let totalCol = 3;
-        if (!isMobile()) totalCol = getDesktopCol();
+        if (!mobile) totalCol = getDesktopCol();
         let gridBaseRatio = GameImgAspectRatio;
-        let totalWidth = isMobile() ? 100 * totalCol : 160 * totalCol;
+        let totalWidth = mobile ? 100 * totalCol : 160 * totalCol;
         if (parent != null) totalWidth = parent.clientWidth;
         let rowGap = parent != null ? window.getComputedStyle(parent).getPropertyValue('grid-row-gap') : "0px";
         let gap = Number(rowGap.replace("px", ""));
@@ -211,7 +211,7 @@ const GameSmallList = ({ data, user }) => {
             gridsData[i] = { width, aspectRatio };
         }
         setGridInfo(gridsData);
-    };
+    }, [fontScale, getDesktopCol, mobile]);
     const formatMList = (list) => {
         let mList = [];
         if (list != null) {
@@ -245,7 +245,7 @@ const GameSmallList = ({ data, user }) => {
     }, []); // 空数组表示只在组件挂载和卸载时执行
     useEffect(() => {
         initGridsInfo();
-    }, [gridListRef, gridListMRef, width]);
+    }, [initGridsInfo]);
     const onChagePage = (change = 1) => {
         const gridList = gridListRef.current;
         if (gridList) {
@@ -254,8 +254,8 @@ const GameSmallList = ({ data, user }) => {
         }
     };
     const renderBtns = () => {
-        const preState = (pageState == 0);
-        const nextState = (pageState == 2);
+        const preState = (pageState === 0);
+        const nextState = (pageState === 2);
         return (
             <>
                 <button className={styles.slide_btn} disabled={preState} onClick={() => {
@@ -302,7 +302,7 @@ const GameSmallList = ({ data, user }) => {
                             let gridNum = item.gridNum != null ? item.gridNum : 1;
                             // gridNum=Math.floor(Math.random()*3)+1;
                             let whStyle = gridInfo[gridNum];
-                            if (whStyle == null) return (<></>);
+                            if (whStyle == null) return null;
                             return (
                                 <GameItem whStyle={whStyle} key={index} data={item} handleRoom={handlePopSelected}></GameItem>
                             );
@@ -343,7 +343,7 @@ const GameSmallList = ({ data, user }) => {
                     <div className={styles.grid_row_con} style={gridStyle}>
                         {
                             rowlist.map((ritem, rindex) => {
-                                if (ritem == null) return (<></>);
+                                if (ritem == null) return null;
                                 return (
                                     <div key={rindex} className={styles.grid_row}>
                                         {

+ 20 - 34
src/components/GameTabs/index.jsx

@@ -2,12 +2,11 @@ import styles from "@components/GameTabs/styles.module.scss";
 import React, { Fragment, Suspense, useEffect, useState } from "react";
 import AllWidgets from "@widgets/index";
 import SmallLoading from "@components/SmallLoading";
-import SvgIcon from "@components/SvgIcon";
 import TabIcon from "@components/TabIcon";
 import Lottie from "lottie-react";
 import arrowAni from '@assets/home/jiantou.json';
 const i18n = global.i18n;
-const GameTabs = ({ data }) => {
+const GameTabs = ({ data = {} }) => {
     const [tabIndex, setTabIndex] = useState(1);
     // data={
     //     tabs:[
@@ -24,41 +23,28 @@ const GameTabs = ({ data }) => {
     //     }
     // }
     useEffect(() => {
-        if (data && data.tabs) {
-            let selectType = -1;
-            let tabs = data.tabs;
-            let tabtype = data.tabtype;
-            if (tabtype != null) {
-                for (let i = 0; i < tabs.length; i++) {
-                    let tab = tabs[i];
-                    if (tab.tabtype === tabtype) {
-                        selectType = tabtype;
-                        break;
-                    }
-                }
-            }
-            if (selectType === -1) {
-                let first = data.tabs[0];
-                if (first) {
-                    selectType = first.tabtype;
-                }
-            }
-            setTabIndex(selectType);
-        }
-    }, []);
+        const tabs = Array.isArray(data.tabs) ? data.tabs : [];
+        const modules = Array.isArray(data.data) ? data.data : [];
+        const hasFavoriteGames = modules.some(module => (
+            module?.tabtype === 0 && Array.isArray(module.data) && module.data.length > 0
+        ));
+        const visibleTabs = tabs.filter(tab => tab?.tabtype !== 0 || hasFavoriteGames);
+        const requestedType = data.tabtype;
+        const requestedTab = visibleTabs.find(tab => tab?.tabtype === requestedType);
+        const nextTab = requestedTab?.tabtype ?? visibleTabs[0]?.tabtype ?? -1;
+
+        setTabIndex(current => current === nextTab ? current : nextTab);
+    }, [data]);
     const changeTab = (tabIndex) => {
         setTabIndex(tabIndex);
     };
     const renderTabItem = (item, index) => {
         if (item.tabtype === 0) {
-            let list = getModules(0);
-            if (!!list && list.length > 0) {
-                let like = list[0].data;
-                if (!like || like.length == 0) {
-                    if (tabIndex === 0) setTabIndex(1);
-                    return null;
-                }
-            }
+            const favoriteModules = getModules(0);
+            const hasFavoriteGames = favoriteModules.some(module => (
+                Array.isArray(module?.data) && module.data.length > 0
+            ));
+            if (!hasFavoriteGames) return null;
         }
 
         return (
@@ -79,7 +65,7 @@ const GameTabs = ({ data }) => {
     };
     const renderTabs = () => {
         let tabs = data.tabs;
-        if (tabs == null) tabs = [];
+        if (!Array.isArray(tabs)) tabs = [];
         if (tabs.length === 0) return (<></>);
         return (
             <div className={styles.game_tabs_wrapper}>
@@ -97,7 +83,7 @@ const GameTabs = ({ data }) => {
         );
     };
     const getModules = (tabtype = -1) => {
-        tabtype = tabtype == -1 ? tabIndex : tabtype;
+        tabtype = tabtype === -1 ? tabIndex : tabtype;
         let modules = data.data;
         let list = [];
         if (modules) {

+ 4 - 1
src/components/Loading/LoadingCircular.jsx

@@ -17,6 +17,9 @@ const LoadingCircular=()=>{
             hide
         };
         global.loadingCircular=loadingCircular;
+        return () => {
+            if (global.loadingCircular === loadingCircular) delete global.loadingCircular;
+        };
     },[])
     return (
         <>
@@ -30,4 +33,4 @@ const LoadingCircular=()=>{
         </>
     )
 }
-export default LoadingCircular;
+export default LoadingCircular;

+ 1 - 3
src/components/Logo/index.jsx

@@ -6,18 +6,16 @@ import { NavLink } from 'react-router-dom';
 
 // utils
 import PropTypes from 'prop-types';
-import { useThemeProvider } from "@contexts/themeContext";
 import logo from '@assets/logo/logo.png';
 
 const Logo = ({ size = 'md', className = '' }) => {
-    const { theme, isMobile } = useThemeProvider();
     // const logourl=isMobile()?(theme===APP_THEME.DARK?logo_small_black:logo_small_light):(theme===APP_THEME.DARK?logo_dark:logo_light);
     // const logourl=(theme=='dark'?logo_dark:logo_light);
     const logourl = window.themeConfig?.logo || logo;
     return (
         <NavLink className={`${styles.logo} ${styles[size]} ${className && className}`} to="/">
             {/* <img className={`${isMobile() ? styles.logo_img_m : styles.logo_img}`} src={logourl} /> */}
-            <img className={`${size == "md" ? styles.normal : ""}`} src={logourl} />
+            <img alt="" className={`${size === "md" ? styles.normal : ""}`} src={logourl} />
         </NavLink>
     );
 };

+ 4 - 4
src/components/LottieEx.jsx

@@ -8,7 +8,7 @@ const LottieEx = ({ loop, animationData, delay = 0 }) => {
         let timer = null;
         if (delay > 0) {
             timer = setTimeout(() => {
-                ref.current.play();
+                ref.current?.play();
             }, delay * 1000)
         }
 
@@ -17,15 +17,15 @@ const LottieEx = ({ loop, animationData, delay = 0 }) => {
                 clearTimeout(timer);
             }
         }
-    }, [ref])
+    }, [delay])
 
     return (
         <Lottie
             loop={loop}
             lottieRef={ref}
-            autoplay={delay == 0}
+            autoplay={Number(delay) === 0}
             animationData={animationData} />
     )
 }
 
-export default LottieEx;
+export default LottieEx;

+ 7 - 2
src/components/MailNew.jsx

@@ -15,7 +15,7 @@ const MailNew = ({...props}) => {
     }
     const resetTimer=()=>{
         if (objInfo.timerIds){
-            for (let i = 0; i < objInfo.timerIds; i++) {
+            for (let i = 0; i < objInfo.timerIds.length; i++) {
                 clearTimeout(objInfo.timerIds[i]);
             }
             objInfo.timerIds=[];
@@ -27,6 +27,10 @@ const MailNew = ({...props}) => {
             hide
         };
         global.mailNew=mailNew;
+        return () => {
+            resetTimer();
+            if (global.mailNew === mailNew) delete global.mailNew;
+        };
     },[])
     useEffect(() => {
         resetTimer();
@@ -36,6 +40,7 @@ const MailNew = ({...props}) => {
             },4000);
             objInfo.timerIds.push(timer);
         }
+        return resetTimer;
     },[isShow])
     return (
         <>
@@ -49,4 +54,4 @@ const MailNew = ({...props}) => {
         </>
     )
 }
-export default MailNew;
+export default MailNew;

+ 20 - 14
src/components/MobileCodeInput/index.jsx

@@ -1,14 +1,20 @@
-import React, { useState, useRef, useEffect } from 'react';
+import React, { useEffect, useState, useRef } from 'react';
 import styles from './styles.module.scss';
 import { APP_OPT_LENGTH } from '@constants/app';
 
 const MobileCodeInput = ({ length = APP_OPT_LENGTH, onChange, className = '', onFocus, onBlur }) => {
+    const inputLength = Number.isInteger(length) && length > 0 ? length : APP_OPT_LENGTH;
     // 初始化一个长度为6的空字符串数组
-    const [code, setCode] = useState(new Array(length).fill(''));
+    const [code, setCode] = useState(new Array(inputLength).fill(''));
     const [active, setActive] = useState(false);
     // 用于存储每个输入框的DOM引用,方便控制焦点
     const inputRefs = useRef([]);
-    const [focusedIndex, setFocusedIndex] = useState(-1);  // 新增:记录当前聚焦的输入框索引
+
+    useEffect(() => {
+        setCode(new Array(inputLength).fill(''));
+        setActive(false);
+        inputRefs.current = inputRefs.current.slice(0, inputLength);
+    }, [inputLength]);
     // 新增:处理聚焦
     const handleFocus = (index) => {
         onFocus?.(index);  // 传回聚焦的输入框索引
@@ -20,9 +26,9 @@ const MobileCodeInput = ({ length = APP_OPT_LENGTH, onChange, className = '', on
     };
 
     const handleCodeChanged = (newCode) => {
-        const completed = newCode.every((char) => char !== '') && newCode.length === length;
+        const completed = newCode.every((char) => char !== '') && newCode.length === inputLength;
         setActive(completed);
-        onChange && onChange(newCode.join(''));
+        onChange?.(newCode.join(''));
     }
 
     // 处理输入变化
@@ -38,8 +44,8 @@ const MobileCodeInput = ({ length = APP_OPT_LENGTH, onChange, className = '', on
         setCode(newCode);
 
         // 如果当前输入框有值,且不是最后一个,则焦点自动移向下一个
-        if (value && index < length - 1) {
-            inputRefs.current[index + 1].focus();
+        if (value && index < inputLength - 1) {
+            inputRefs.current[index + 1]?.focus();
         }
 
         // 检查是否所有格子都已填满,如果是则触发回调
@@ -50,7 +56,7 @@ const MobileCodeInput = ({ length = APP_OPT_LENGTH, onChange, className = '', on
     const handleKeyDown = (index, e) => {
         // 当按下退格键,且当前格子为空时,焦点自动移向上一个格子
         if (e.key === 'Backspace' && !code[index] && index > 0) {
-            inputRefs.current[index - 1].focus();
+            inputRefs.current[index - 1]?.focus();
         }
     };
 
@@ -61,10 +67,10 @@ const MobileCodeInput = ({ length = APP_OPT_LENGTH, onChange, className = '', on
         const pastedData = e.clipboardData.getData('text/plain').trim();
 
         // 过滤非数字字符,并截取到指定长度
-        const cleanData = pastedData.replace(/\D/g, '').slice(0, length);
+        const cleanData = pastedData.replace(/\D/g, '').slice(0, inputLength);
         if (!cleanData) return;
 
-        const newCode = [...code];
+        const newCode = new Array(inputLength).fill('');
         const pastedArray = cleanData.split('');
 
         // 将粘贴的数据分配到数组中
@@ -74,8 +80,8 @@ const MobileCodeInput = ({ length = APP_OPT_LENGTH, onChange, className = '', on
         setCode(newCode);
 
         // 计算粘贴后焦点应该停留在哪个输入框
-        const focusIndex = pastedArray.length < length ? pastedArray.length : length - 1;
-        inputRefs.current[focusIndex].focus();
+        const focusIndex = pastedArray.length < inputLength ? pastedArray.length : inputLength - 1;
+        inputRefs.current[focusIndex]?.focus();
 
         // 如果粘贴的长度刚好等于所需长度,触发回调
         handleCodeChanged(newCode)
@@ -95,7 +101,7 @@ const MobileCodeInput = ({ length = APP_OPT_LENGTH, onChange, className = '', on
                         onChange={(e) => handleChange(index, e)}
                         onKeyDown={(e) => handleKeyDown(index, e)}
                         onPaste={handlePaste}
-                        className={`${styles.input_box} ${active && styles.active}`}
+                        className={`${styles.input_box} ${active ? styles.active : ''}`}
                         onFocus={() => handleFocus(index)}  // 新增
                         onBlur={handleBlur}                 // 新增
                     />
@@ -103,4 +109,4 @@ const MobileCodeInput = ({ length = APP_OPT_LENGTH, onChange, className = '', on
         </div>
     );
 };
-export default MobileCodeInput;
+export default MobileCodeInput;

+ 2 - 5
src/components/MsgBox/index.jsx

@@ -9,10 +9,7 @@ const i18n = global.i18n;
 class MsgBox extends Component {
     constructor(props) {
         super(props);
-        this.init();
-    }
-    init() {
-        this.state = ({
+        this.state = {
             isShow: false,
             title: "",
             content: '',
@@ -20,7 +17,7 @@ class MsgBox extends Component {
             cancelLabel: '',
             onlySure: false,
             isLockClose: false,
-        })
+        };
     }
     show({ title = '', content = '', sureCB = null, cancelCB = null, sureLabel = '', cancelLabel = '', onlySure = false, isLockClose = false }) {
         if (title === '') title = i18n.t('msgbox.title');

+ 4 - 2
src/components/Popup.jsx

@@ -40,6 +40,7 @@ const Popup = ({
     failed = false,
     pending = false,
     bottomClose = false,
+    transitionNodeRef,
 }) => {
     let sx = {};
     if (zIndex > -1) sx.zIndex = zIndex;
@@ -54,13 +55,13 @@ const Popup = ({
     return (
         <Modal open={open} onClose={onClose} closeAfterTransition sx={sx}>
             <Fade in={open} timeout={500}>
-                <div className={`popup-container ${rootClass}`}>
+                <div ref={transitionNodeRef} className={`popup-container ${rootClass}`}>
                     <Grow in={open} timeout={timeout}>
                         <div className={`popup popup-window ${popupClass || ''} ${bg_class || ''}`} style={{ minHeight: 'unset' }}>
                             {underBg && underBg({ open })}
                             {overlay && <div className='popup-overlay' />}
 
-                            {popupBg && <img src={popupBg} className='popup-bg' />}
+                            {popupBg && <img alt="" src={popupBg} className='popup-bg' />}
                             {renderColse(btnCloseType, onClose)}
                             {children}
                         </div>
@@ -76,6 +77,7 @@ Popup.propTypes = {
     onClose: PropTypes.func.isRequired,
     children: PropTypes.node.isRequired,
     popupClass: PropTypes.string,
+    transitionNodeRef: PropTypes.shape({ current: PropTypes.any }),
     showBtnClose: PropTypes.number
 };
 

+ 17 - 7
src/components/SubScreen/index.jsx

@@ -1,7 +1,7 @@
 import PropTypes from "prop-types";
 import styles from "./styles.module.scss";
 import SvgIcon from "@components/SvgIcon";
-import { useState, useEffect } from "react";
+import { useState, useEffect, useRef } from "react";
 import { useThemeProvider } from "@contexts/themeContext";
 import CSSTransition from "react-transition-group/CSSTransition";
 import useEdgeGesture from "@hooks/useEdgeGesture";
@@ -21,12 +21,16 @@ const SubScreen = ({ open, onClose, children, route, timeout = 500, level = 0, h
         }
     }, [level, subOpen, setIsOpenSub]);
 
-    const { open: fwOpen, setOpen: setFwOpen } = useWithdrawMission();
+    const { setOpen: setFwOpen } = useWithdrawMission();
+    const reopenTimerRef = useRef(null);
+    const transitionNodeRef = useRef(null);
     const delayClose = () => {
         // 添加条件
         if (window.location.hash === "#/FWRecord") {
             global.ignorePopup = true;
-            setTimeout(() => {
+            if (reopenTimerRef.current != null) clearTimeout(reopenTimerRef.current);
+            reopenTimerRef.current = setTimeout(() => {
+                reopenTimerRef.current = null;
                 setFwOpen(true);
             }, 250);
         }
@@ -38,7 +42,13 @@ const SubScreen = ({ open, onClose, children, route, timeout = 500, level = 0, h
         delayClose();
     };
     const [subScreenClear, setSubScreenClear] = useState(false);
-    global.setSubScreenClear = setSubScreenClear;
+    useEffect(() => {
+        global.setSubScreenClear = setSubScreenClear;
+        return () => {
+            if (global.setSubScreenClear === setSubScreenClear) delete global.setSubScreenClear;
+            if (reopenTimerRef.current != null) clearTimeout(reopenTimerRef.current);
+        };
+    }, []);
 
     const navigate = useNavigate();
     const renderHeader = () => {
@@ -99,15 +109,15 @@ const SubScreen = ({ open, onClose, children, route, timeout = 500, level = 0, h
             global.tempPageTitle = null;
             setSidedGesture(0);
         };
-    }, []);
+    }, [setSidedGesture]);
     return (
-        <CSSTransition timeout={timeout} {...props} classNames={{
+        <CSSTransition timeout={timeout} {...props} nodeRef={transitionNodeRef} classNames={{
             enter: isSideGesture ? styles.subscreen_enter_gesture : styles.subscreen_enter,
             enterActive: isSideGesture ? styles.subscreen_enter_active_gesture : styles.subscreen_enter_active,
             exit: isSideGesture ? styles.subscreen_exit_active_gesture : styles.subscreen_exit,
             exitActive: isSideGesture ? styles.subscreen_exit_active_gesture : styles.subscreen_exit_active,
         }}>
-            <div className={styles.subscreen}>
+            <div ref={transitionNodeRef} className={styles.subscreen}>
                 {renderBar()}
                 {renderContent()}
             </div>

+ 1 - 1
src/components/SvgIcon/index.jsx

@@ -19,7 +19,7 @@ const SvgIcon = ({ herf, mobile = false, ...props }) => {
     return (
         <>
             {
-                formatOL()?(<img {...props} src={herf}/>):
+                formatOL()?(<img alt="" {...props} src={herf}/>):
                     (
                         <svg {...props}>
                              <use xlinkHref={formatHerf()} />

+ 1 - 2
src/components/TabIcon/index.jsx

@@ -18,7 +18,6 @@ const TabIcon = ({ herf, ...props }) => {
     const index = herf.lastIndexOf("_");
     if (index > -1) {
         const svgName = herf.slice(0, index);
-        const active = herf.slice(index) === '_light';
         const Tab = svgMaps[svgName];
         if (!!Tab) {
             return <Tab {...props} style={{ filter: "none" }} />
@@ -30,7 +29,7 @@ const TabIcon = ({ herf, ...props }) => {
     const iconName = availableIcons.includes(herf) ? herf : 'icon_Slots_light';
     const src = iconContext(`./${iconName}.png`);
     return (
-        <img {...props} src={src} />
+        <img alt="" {...props} src={src} />
     );
 };
 export default TabIcon;

+ 3 - 1
src/components/TransText/index.jsx

@@ -7,4 +7,6 @@ const TransText = (props) => {
     return (
         <Trans {...props} />
     )
-}
+}
+
+export default TransText;

+ 4 - 12
src/components/TruncatedText.jsx

@@ -98,7 +98,7 @@ var Truncate = function (_Component) {
                 timeout = this.timeout;
 
 
-            ellipsis.parentNode.removeChild(ellipsis);
+            if (ellipsis?.parentNode) ellipsis.parentNode.removeChild(ellipsis);
 
             window.removeEventListener('resize', onResize);
 
@@ -121,7 +121,7 @@ var Truncate = function (_Component) {
             test.innerHTML = 'foo<br/>bar';
 
             if (test[contentKey].replace(/\r\n|\r/g, '\n') !== 'foo\nbar') {
-                div.innerHTML = div.innerHTML.replace(/<br.*?[\/]?>/gi, '\n');
+                div.innerHTML = div.innerHTML.replace(/<br.*?[/]?>/gi, '\n');
                 text = div[contentKey];
             }
 
@@ -419,27 +419,19 @@ Truncate.defaultProps = {
 
 const TruncatedText = ({text, lines = 2, width, className}) => {
     const {fontScale} = useThemeProvider();
-    const [truncated, setTruncated] = useState(false);
     const [mounted, setMounted] = useState(false);
 
     useEffect(() => {
         setMounted(true);
-
-        return () => setMounted(false);
     }, []);
 
-    useEffect(() => {
-        setTruncated(true);
-    }, [fontScale, width]);
-
     return (
         <span className={className ? className : ''}>
             {
                 mounted && (
-                    <Truncate lines={lines}
+                    <Truncate key={`${fontScale}-${width}`} lines={lines}
                               ellipsis={(<span>...</span>)}
-                              width={width}
-                              onTruncate={() => setTruncated(!truncated)}>
+                              width={width}>
                         {text}
                     </Truncate>
                 )

+ 10 - 30
src/components/UInput/MultiNumPassInput.jsx

@@ -1,4 +1,4 @@
-import {useEffect, useRef, useState} from "react";
+import {useCallback, useEffect, useRef, useState} from "react";
 import styled from 'styled-components/macro';
 const StyledInput= styled.input`
   position: absolute;
@@ -26,36 +26,10 @@ const MultiNumPassInput=({innerRef, ...props})=>{
     const inputRef = useRef(null);
     const btnRefs = useRef([]);
 
-    useEffect(()=>{
-        initValues();
-        return ()=>{
-
-        }
-    },[]);
     useEffect(()=>{
         if (inputRef.current)inputRef.current.focus();
     },[focusIndex]);
-    useEffect(()=>{
-        if (focus){
-            if (values.length>0){
-                checkFocus(values);
-            }
-        }
-    },[focus])
-
-    const initValues=()=>{
-        let values=[];
-        let initValues=[];
-        if (value!=null){
-            initValues=value.split('');
-        }
-        for (let i = 0; i <length ; i++) {
-            values[i]=initValues[i]?initValues[i]:"";
-        }
-        setValues(values);
-        if (focus)checkFocus(values);
-    }
-    const checkFocus=(valueArr)=>{
+    const checkFocus=useCallback((valueArr)=>{
         let fIndex=-1;
         for (let i = 0; i <valueArr.length; i++) {
             let value=valueArr[i];
@@ -68,7 +42,13 @@ const MultiNumPassInput=({innerRef, ...props})=>{
         if (fIndex===-1){
             if (onFinish)onFinish(valueArr);
         }
-    }
+    }, [onFinish]);
+    useEffect(()=>{
+        const initialValues = value != null ? String(value).split('') : [];
+        const nextValues = Array.from({ length }, (_, index) => initialValues[index] || "");
+        setValues(nextValues);
+        if (focus) checkFocus(nextValues);
+    }, [checkFocus, focus, length, value]);
     const handleChange=(index,value)=>{
         let valueArr=values.slice(0);
         valueArr[index]=value;
@@ -77,7 +57,7 @@ const MultiNumPassInput=({innerRef, ...props})=>{
         }
         else
         {
-            if (index==0){
+        if (index === 0) {
                 valueArr=Array.from({ length: length }, (_, index) => "");
             }
         }

+ 18 - 11
src/components/UInput/PhoneInput.jsx

@@ -17,7 +17,7 @@ const getDefaultAreaCode = (area) => {
         let code = localStorageGet(LOCAL_DEFAULT_COUNTRY, "");
         if (!!code) {
             index = codes.indexOf(code);
-            if (index == -1) index = 0;
+        if (index === -1) index = 0;
         }
 
         return codes[index];
@@ -27,9 +27,15 @@ const getDefaultAreaCode = (area) => {
 const PhoneInput = ({ innerRef, onChangeCountry, ...props }) => {
     const area = global.getLocalAreaID();
     const [country, setCountry] = useState(getDefaultAreaCode(area));
-    const wrapClassName = props.className;
-    const message = props.message ? props.message : "";
-    props.className = props.inputClass ? props.inputClass : "";
+    const {
+        className: wrapClassName = "",
+        message = "",
+        inputClass = "",
+        value,
+        onChange,
+        ...inputProps
+    } = props;
+    const normalizedValue = value == null ? "" : String(value);
     const [showCountrySelect, setShowCountrySelect] = useState(false);
     const { isMobile } = useThemeProvider();
     const selectRef = useRef(null);
@@ -39,14 +45,12 @@ const PhoneInput = ({ innerRef, onChangeCountry, ...props }) => {
         localStorageSet(LOCAL_DEFAULT_COUNTRY, country);
 
         let max = getPhoneCodeMaxLen(country);
-        const value = props.value;
-        const onChange = props.onChange;
-        if (value.length > max) {
+        if (normalizedValue.length > max) {
             onChange && onChange({
-                target: { value: value.substring(0, max) }
+                target: { value: normalizedValue.substring(0, max) }
             });
         }
-    }, [country]);
+    }, [country, normalizedValue, onChange, onChangeCountry]);
     const onChangeAreaCode = (code) => {
         setCountry(code);
         hideASelect();
@@ -95,7 +99,7 @@ const PhoneInput = ({ innerRef, onChangeCountry, ...props }) => {
                                 onChangeAreaCode(item.areaCode)
                             }}>
                                 <div className={styles.left}>
-                                    <img className={styles.icon} src={item.icon} />
+                                    <img alt="" className={styles.icon} src={item.icon} />
                                     <span className={styles.code}>{"+" + item.areaCode}</span>
                                     <span className={styles.name}>{item.en}</span>
                                 </div>
@@ -198,7 +202,10 @@ const PhoneInput = ({ innerRef, onChangeCountry, ...props }) => {
                             type="tel"
                             ref={innerRef}
                             maxLength={getPhoneCodeMaxLen(country)}
-                            {...props}
+                            {...inputProps}
+                            className={inputClass}
+                            value={value}
+                            onChange={onChange}
                         />
                         <SvgIcon className={styles.phone_icon} herf={"icon_Phone"} />
                     </div>

+ 1 - 1
src/constants/giftPacks.js

@@ -25,7 +25,7 @@ export const GiftPacks = {
 
 /** 礼包名称 */
 export const GiftPackNames = {
-    [0]: "Store",
+    0: "Store",
     [GiftPacks.FirstPay]: "First Deposit Bonus",
     [GiftPacks.RuptcyGift]: "Broke Relief Pack",
     [GiftPacks.RecallGift]: "Comeback Pack",

+ 0 - 1
src/constants/localkey.js

@@ -1,4 +1,3 @@
-const BASE = "SG_";
 //设备id
 const LOCAL_VISTOR_ID = "LOCAL_VISTOR_ID";
 const LOCAL_USER_INFO = "USER_INFO";

+ 3 - 1
src/constants/region.js

@@ -108,7 +108,7 @@ export const isRegion = (code) => getRegionCode().toLowerCase() === String(code
 /** 是否为指定国家(不区分大小写) */
 export const isCountry = (iso) => getCountryCode().toLowerCase() === String(iso || "").toLowerCase();
 
-export default {
+const regionUtils = {
     FALLBACK_REGION,
     getThemeConfig,
     getRegion,
@@ -130,3 +130,5 @@ export default {
     isRegion,
     isCountry
 };
+
+export default regionUtils;

+ 2 - 3
src/contexts/shopContext.js

@@ -25,8 +25,7 @@ export const ShopProvider = ({children}) => {
         return () => {
             setIsLocked(false);
         }
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [filtersOpen]);
+    }, [filtersOpen, setIsLocked]);
 
     return (
         <ShopContext.Provider value={{cartOpen, setCartOpen, filtersOpen, setFiltersOpen}}>
@@ -35,4 +34,4 @@ export const ShopProvider = ({children}) => {
     )
 }
 
-export const useShopProvider = () => useContext(ShopContext);
+export const useShopProvider = () => useContext(ShopContext);

+ 4 - 4
src/contexts/sidebarContext.js

@@ -10,11 +10,12 @@ export const SidebarProvider = ({children}) => {
     const [open, setOpen] = useState(true);
     const location = useLocation();
     const setIsLocked = useScrollLock();
+    const mobile = isMobile();
 
     // close sidebar when route changes
     useEffect(() => {
-        if (isMobile())setOpen(false);
-    }, [location]);
+        if (mobile) setOpen(false);
+    }, [location, mobile]);
 
     useEffect(() => {
         setIsLocked(false);
@@ -22,8 +23,7 @@ export const SidebarProvider = ({children}) => {
         return () => {
             setIsLocked(false);
         }
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [open]);
+    }, [open, setIsLocked]);
     return (
         <SidebarContext.Provider value={{open, setOpen}}>
             {children}

+ 86 - 56
src/contexts/themeContext.js

@@ -1,4 +1,4 @@
-import { createContext, useContext, useEffect, useRef, useState } from 'react';
+import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
 import { isRtlLang } from 'rtl-detect';
 import { useWindowSize } from "react-use";
 import { APP_THEME, ENABLE_SUPERBALL } from "@constants/app";
@@ -13,7 +13,7 @@ import { getSuperBallInfo } from '@features/superBallSlice';
 import { reqSuperBallInfo } from '@features/api';
 import { jackpotStore } from '@hooks/useSuperBall';
 import { isAndroid, isIOS, isDesktop as isDesktopDevice } from '@utils/UAUtils';
-import { windowOpenShare } from "@utils/helpers";
+import { getSafeHttpUrl, windowOpenShare } from "@utils/helpers";
 
 const ThemeContext = createContext(undefined);
 const MOBILE_BREAKPOINT = 768;
@@ -22,15 +22,24 @@ const TABLET_MAX_LONG_SIDE = 1366;
 const MOBILE_FONT_SCALE_BASE = 0.88;
 const MOBILE_FONT_SCALE_MAX = 18 / 16;
 
+const loadPreferences = () => {
+    try {
+        const preferences = JSON.parse(localStorage.getItem('preferences') || '{}');
+        return preferences && typeof preferences === 'object' ? preferences : {};
+    } catch {
+        return {};
+    }
+};
+
 export const ThemeProvider = ({ children }) => {
     const { width, height } = useWindowSize();
     const page = document.documentElement;
     const isRtl = isRtlLang(navigator.language);
-    const persisted = JSON.parse(localStorage.getItem('preferences') || '{}');
-    const isTouchDevice = () => {
+    const [persisted] = useState(loadPreferences);
+    const isTouchDevice = useCallback(() => {
         return navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0 || 'ontouchstart' in window;
-    };
-    const getViewportSize = () => {
+    }, []);
+    const getViewportSize = useCallback(() => {
         const nextWidth = Number.isFinite(width) && width > 0 ? width : window.innerWidth || 0;
         const nextHeight = Number.isFinite(height) && height > 0 ? height : window.innerHeight || nextWidth;
         return {
@@ -39,29 +48,29 @@ export const ThemeProvider = ({ children }) => {
             shortSide: Math.min(nextWidth, nextHeight),
             longSide: Math.max(nextWidth, nextHeight)
         };
-    };
-    const isTabletViewport = () => {
+    }, [height, width]);
+    const isTabletViewport = useCallback(() => {
         const { shortSide, longSide } = getViewportSize();
         return shortSide >= MOBILE_BREAKPOINT && longSide <= TABLET_MAX_LONG_SIDE;
-    };
-    const isTablet = () => {
+    }, [getViewportSize]);
+    const isTablet = useCallback(() => {
         return isTabletViewport() && (isIOS() || isAndroid() || isTouchDevice());
-    };
-    const isPhoneDevice = () => {
+    }, [isTabletViewport, isTouchDevice]);
+    const isPhoneDevice = useCallback(() => {
         return (isAndroid() || isIOS()) && !isTablet();
-    };
-    const isDesktop = () => {
+    }, [isTablet]);
+    const isDesktop = useCallback(() => {
         return !isPhoneDevice() && !isTablet() && isDesktopDevice() && getViewportSize().width >= DESKTOP_BREAKPOINT;
-    };
-    const isMobile = () => {
+    }, [getViewportSize, isPhoneDevice, isTablet]);
+    const isMobile = useCallback(() => {
         return isPhoneDevice() || getViewportSize().width < MOBILE_BREAKPOINT;
-    };
-    const getMFontScale = () => {
+    }, [getViewportSize, isPhoneDevice]);
+    const getMFontScale = useCallback(() => {
         let baseWidth = getViewportSize().shortSide || 390;
         let ratio = baseWidth / 390;
         let scale = Number((MOBILE_FONT_SCALE_BASE * ratio).toFixed(2));
         return Math.min(scale, MOBILE_FONT_SCALE_MAX);
-    };
+    }, [getViewportSize]);
     const [theme, setTheme] = useState(persisted && persisted.theme ? persisted.theme : (APP_THEME.DARK));
     const [fontScale, setFontScale] = useState(isMobile() ? getMFontScale() : 1);
     const [direction, setDirection] = useState(persisted.direction || (isRtl ? 'rtl' : 'ltr'));
@@ -114,39 +123,30 @@ export const ThemeProvider = ({ children }) => {
     const [showBrowserNotSupport, setShowBrowserNotSupport] = useState(false);
     const [subScreenClear, setSubScreenClear] = useState(false);
 
-    const initWindowSize = () => {
-        const viewportSize = getViewportSize();
-        if (isMobile()) global.device = "m";
-        else global.device = "d";
-        global.isTablet = isTablet();
-        global.width = viewportSize.width;
-        global.height = viewportSize.height;
-        changeFontScale(isMobile() ? getMFontScale() : 1);
-    };
-    const stopTransition = () => {
+    const stopTransition = useCallback(() => {
         page.classList.add('no-transition');
         setTimeout(() => page.classList.remove('no-transition'), 100);
-    };
-
-    const savePreferences = () => {
-        localStorage.setItem('preferences', JSON.stringify({
-            theme,
-            fontScale,
-            direction,
-            installOpen,
-            soundOpen
-        }));
-    };
+    }, [page]);
 
     const toggleTheme = () => {
         setTheme(theme === APP_THEME.LIGHT ? APP_THEME.DARK : APP_THEME.LIGHT);
     };
 
-    const changeFontScale = (scale) => {
+    const changeFontScale = useCallback((scale) => {
         setFontScale(scale);
         page.style.setProperty('--font-scale', scale);
         stopTransition();
-    };
+    }, [page, stopTransition]);
+
+    const initWindowSize = useCallback(() => {
+        const viewportSize = getViewportSize();
+        const mobile = isMobile();
+        global.device = mobile ? "m" : "d";
+        global.isTablet = isTablet();
+        global.width = viewportSize.width;
+        global.height = viewportSize.height;
+        changeFontScale(mobile ? getMFontScale() : 1);
+    }, [changeFontScale, getMFontScale, getViewportSize, isMobile, isTablet]);
 
     const toggleDirection = () => {
         setDirection(direction === 'ltr' ? 'rtl' : 'ltr');
@@ -193,7 +193,8 @@ export const ThemeProvider = ({ children }) => {
             global.supportCenter.show();
             return;
         }
-        windowOpenShare(serviceLink);
+        const safeUrl = getSafeHttpUrl(serviceLink);
+        if (safeUrl) windowOpenShare(safeUrl);
     };
 
     const handleRoutes = (route, pathname) => {
@@ -234,23 +235,52 @@ export const ThemeProvider = ({ children }) => {
         return true;
     };
 
-    global.openFirstDeposit = openFirstDeposit;
-
     useEffect(() => {
         page.setAttribute('dir', direction);
-        savePreferences();
-    }, [theme, fontScale, direction, installOpen]);
+        try {
+            localStorage.setItem('preferences', JSON.stringify({
+                theme,
+                fontScale,
+                direction,
+                installOpen,
+                soundOpen
+            }));
+        } catch (e) {
+            // Keep the in-memory preference active when storage is unavailable.
+        }
+    }, [direction, fontScale, installOpen, page, soundOpen, theme]);
     useEffect(() => {
         initWindowSize();
-    }, [width, height]);
+    }, [initWindowSize]);
+
+    const globalActionsRef = useRef(null);
+    globalActionsRef.current = {
+        openFirstDeposit,
+        openRecommandGame,
+        openRecommandGamePopup,
+        openFixedWithdraw: (payload) => {
+            global.referralPayload = payload;
+            navigate(ROUTE_PATH.REFERRAL);
+        }
+    };
+    useEffect(() => {
+        const openFirstDepositGlobal = (...args) => globalActionsRef.current?.openFirstDeposit(...args);
+        const openRecommandGameGlobal = (...args) => globalActionsRef.current?.openRecommandGame(...args);
+        const openRecommandGamePopupGlobal = (...args) => globalActionsRef.current?.openRecommandGamePopup(...args);
+        const openFixedWithdrawGlobal = (...args) => globalActionsRef.current?.openFixedWithdraw(...args);
 
-    global.openRecommandGame = openRecommandGame;
-    global.openRecommandGamePopup = openRecommandGamePopup;
+        global.openFirstDeposit = openFirstDepositGlobal;
+        global.openRecommandGame = openRecommandGameGlobal;
+        global.openRecommandGamePopup = openRecommandGamePopupGlobal;
+        global.openFixedWithdraw = openFixedWithdrawGlobal;
 
-    global.openFixedWithdraw = (payload) => {
-        global.referralPayload = payload;
-        navigate(ROUTE_PATH.REFERRAL);
-    };
+        return () => {
+            if (global.openFirstDeposit === openFirstDepositGlobal) delete global.openFirstDeposit;
+            if (global.openRecommandGame === openRecommandGameGlobal) delete global.openRecommandGame;
+            if (global.openRecommandGamePopup === openRecommandGamePopupGlobal) delete global.openRecommandGamePopup;
+            if (global.openFixedWithdraw === openFixedWithdrawGlobal) delete global.openFixedWithdraw;
+        };
+    }, []);
 
     const freeWithdraw = useSelector(getFreeWithdraw);
     const checkShowFreeWithdraw = () => {
@@ -292,7 +322,7 @@ export const ThemeProvider = ({ children }) => {
 
         target = Math.floor(target);
         var startVal = jackpotStore.get();
-        if (startVal == 0) {
+        if (String(startVal) === "0") {
             startVal = Math.floor(target * 0.9);
             jackpotStore.set(startVal);
         }
@@ -323,7 +353,7 @@ export const ThemeProvider = ({ children }) => {
             stopRequester();
         }
 
-    }, [superBallInfo])
+    }, [dispatch, superBallInfo])
 
     const checkVipRights = (hidePopup) => {
         return true;

+ 7 - 4
src/contexts/withdrawMissionContext.js

@@ -8,9 +8,13 @@ export const WithdrawMissionProvider = ({ children }) => {
     const setIsLocked = useScrollLock();
 
     useEffect(()=>{
-        global.openWithdrawMission = ()=>{
+        const openWithdrawMission = ()=>{
             setOpen(true);
-        }
+        };
+        global.openWithdrawMission = openWithdrawMission;
+        return () => {
+            if (global.openWithdrawMission === openWithdrawMission) delete global.openWithdrawMission;
+        };
     },[])
 
     useEffect(() => {
@@ -19,8 +23,7 @@ export const WithdrawMissionProvider = ({ children }) => {
         return () => {
             setIsLocked(false);
         }
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [open]);
+    }, [open, setIsLocked]);
     return (
         <WithdrawMissionContext.Provider value={{ open, setOpen }}>
             {children}

+ 10 - 3
src/features/actions/hallClient.js

@@ -1,6 +1,6 @@
 import { APP_SERVER } from "@constants/app";
-import { connectServer, disconnectServer, onDisconnect, sendToServer } from "@features/socketSlice";
-import { reqBankruptcyGift, reqCheckinCheck, reqDailyGift, reqDailyRechargeGift, reqFirstPayGift, reqFreeWithdraw, reqFreeWithdrawMission, reqGameInfo, reqGetRecallGiftState, reqHolidaysActInfo, reqHolidaysGiftInfo, reqLuckyRollGifts, reqLuckyRollInfo, reqLuckyRollLogs, reqLuckyRollVIPFreeInfo, reqMailCheck, reqProtectCheck, reqSuperBallInfo, reqCouponList, reqRedemptionCodeInfo, reqWithdrawBaseInfo, reqEmailInfo } from "@features/api";
+import { connectServer, disconnectServer, sendToServer } from "@features/socketSlice";
+import { reqBankruptcyGift, reqCheckinCheck, reqDailyGift, reqDailyRechargeGift, reqFirstPayGift, reqFreeWithdraw, reqFreeWithdrawMission, reqGameInfo, reqGetRecallGiftState, reqHolidaysActInfo, reqHolidaysGiftInfo, reqLuckyRollGifts, reqLuckyRollInfo, reqLuckyRollLogs, reqLuckyRollVIPFreeInfo, reqMailCheck, reqProtectCheck, reqSuperBallInfo, reqCouponList, reqWithdrawBaseInfo, reqEmailInfo } from "@features/api";
 import { updatePayFinish } from "@features/paySlice";
 import { trackEvent } from "../../tracking/TrackAll";
 import { getIsBindPhone, updateServerKick, updateUserBalance } from "@features/mainSlice";
@@ -113,13 +113,18 @@ const roger = (message, dispatch) => {
                             if (data.Golds != null) {
                                 dispatch(updateUserBalance(Number(data.Golds) / global.CASH_BASE));
                             }
+                            break;
                         }
+                        default:
+                            break;
                     }
 
 
                 }
                 break;
             }
+            default:
+                break;
         }
     }
 }
@@ -141,6 +146,8 @@ const operate = (message, dispatch) => {
             handleKick(message, dispatch);
             break;
         }
+        default:
+            break;
     }
 }
 //信息处理
@@ -247,4 +254,4 @@ export const reqCollectionHandler = (dispatch, message) => {
     setTimeout(() => {
         dispatch(reqGameInfo());
     }, 1000);
-};
+};

+ 5 - 3
src/features/actions/payAction.js

@@ -2,8 +2,8 @@ import { GiftPacks } from "@constants/giftPacks";
 import { LOCAL_LAST_PAID_GIFT_ID } from "@constants/localkey"
 import { ROUTE_PATH } from "@constants/routes";
 import { checkFreeBonusGift, checkHolidaysActInfo, getwithdrawChannel } from "@features/mainSlice";
-import { checkBankRuptcyGift, checkDailyGift, checkDailyRechargeGift, checkFirstPayGift, getCurrPayGiftID, getRecallGiftState } from "@features/paySlice";
-import { localStorageGet, localStorageSet } from "@utils/helpers"
+import { checkBankRuptcyGift, checkDailyGift, checkDailyRechargeGift, checkFirstPayGift, getRecallGiftState } from "@features/paySlice";
+import { localStorageGet } from "@utils/helpers"
 
 const jumpRoute = (navigate, route) => {
     const path = route.startsWith('#') ? window.location.hash : window.location.pathname;
@@ -92,6 +92,8 @@ export const openLastPaidPoint = (navigate) => {
                     show_gift = true;
                 }
                 break
+            default:
+                break;
         }
 
         if (show_gift) global.ignorePopup = true;
@@ -131,4 +133,4 @@ export const checkWithdrawChannel = (amount) => {
 
         return tempType;
     }
-}
+}

+ 2 - 2
src/features/actions/vipAction.js

@@ -138,7 +138,7 @@ export const getVipUpgradeRes = (prevLv = -1, vip = -1, all = false) => {
 
         let freeBonus = userInfo?.InsureScore || 0;
         const preFreeBonus = upgrade?.preFreeBonus || 0;
-        if (preFreeBonus < freeBonus && prevLv == 0) {
+        if (preFreeBonus < freeBonus && Number(prevLv) === 0) {
             /// 刚升级成vip 并且之前的freebonus <100 才显示freeBonus这个选项
             let index = Math.min(2, benefits.length);
             benefits.splice(index, 0, {
@@ -192,4 +192,4 @@ export const getVipPredicts = (vip) => {
 
         return Object.values(res);
     }
-}
+}

+ 1 - 2
src/features/activitySlice.js

@@ -32,11 +32,10 @@ const activitySlice = createSlice({
         buildReqReducer(builder,reqGameActivity,reducerActivity,activityStatus);
     }
 })
-export const {}=activitySlice.actions;
 export const getActivityList=(state)=>{
     return state.activity.list;
 }
 export const getActivityStatus=(state)=>{
     return state.activity.status;
 }
-export default activitySlice.reducer;
+export default activitySlice.reducer;

+ 9 - 13
src/features/api.js

@@ -123,15 +123,13 @@ import {
 import Http, { parseQuickSavePath } from "@api/request";
 import { updateGameList } from "@features/pageSlice";
 // import {APP_CHANNEL} from "@constants/app";
-import { updateAddScore, updateInsurePass, updateUserInfo, updateUserScore } from "@features/mainSlice";
-import { checkIsStandalone, formatApiUrl, formatCash, getUrlParam, localStorageGet, sleep, getEnterPlatform } from "@utils/helpers";
-import { LOCAL_ACTIVITY_C, LOCAL_CUSTOM_LOCK, LOCAL_QUICK_SAVE_S_K, LOCAL_WHEEL_INVITE_CODE } from "@constants/localkey";
-import { toast } from "react-toastify";
+import { updateUserInfo } from "@features/mainSlice";
+import { checkIsStandalone, formatApiUrl, getUrlParam, localStorageGet, getEnterPlatform } from "@utils/helpers";
+import { LOCAL_QUICK_SAVE_S_K, LOCAL_WHEEL_INVITE_CODE } from "@constants/localkey";
 import { preDelMail } from "@features/mailSlice";
 import { getInAppBrowser, isTWA } from "@utils/UAUtils";
 import { APP_LOGIN_TYPE, ENABLE_SUPERBALL } from "@constants/app";
 import { updateBonusPack } from "@features/paySlice";
-import { updateUserTask } from "./superBallSlice";
 export const buildReqReducer = (builder, thunk, callback, statusCB = null) => {
     if (builder == null || thunk == null) return;
     builder
@@ -187,8 +185,8 @@ export const reqGetRoutes = createAsyncThunk(
         let platform = getEnterPlatform();
         params.params.platform = platform;
         // dmig/quickSave 迁移落地:把 s_k 传给后台,让 quickLoad 取回保存的环境(含 G_SIGN),否则迁移过来会是未登录。
-        // App→H5 小数据(?_mig=)已由 dmig 同步还原:URL 无 /{s_k}_{c}/,且 MIG_FORCE_RESTORE 仍在时
-        // 不要再用 localStorage 残留的旧 s_k(多半是浏览器另一个号),否则 conf.s_p 会盖掉 App 账号。
+        // 强制迁移恢复期间不要使用 localStorage 残留的旧 s_k(多半是浏览器另一个号),
+        // 否则 conf.s_p 会盖掉当前迁移账号。
         let qsInfo = parseQuickSavePath();
         let s_k = (qsInfo && qsInfo.s_k) || "";
         if (!s_k) {
@@ -618,7 +616,6 @@ export const reqWithdrawBaseInfo = createAsyncThunk(
 export const reqWithdrawInfo = createAsyncThunk(
     APP_WITHDRAW_INFO,
     async (arg, thunkAPI) => {
-        let { UserID } = arg;
         let pass = global.getWDPass();
         let params = {
             url: APP_WITHDRAW_INFO,
@@ -663,13 +660,13 @@ export const reqWithdrawRecord = createAsyncThunk(
             }
         };
 
-        if (arg.score_type != undefined) {
+        if (arg.score_type !== undefined && arg.score_type !== null) {
             params.data.score_type = arg.score_type;
         }
-        if (arg.sort_by != undefined) {
+        if (arg.sort_by !== undefined && arg.sort_by !== null) {
             params.data.sort_by = arg.sort_by;
         }
-        if (arg.sort != undefined) {
+        if (arg.sort !== undefined && arg.sort !== null) {
             params.data.sort = arg.sort;
         }
         const response = await Http.post(params);
@@ -1234,7 +1231,7 @@ export const reqProtectGet = createAsyncThunk(
             url: APP_PROTECT_GET,
         };
         const response = await Http.get(params);
-        if (response && response.code == 200) {
+        if (response && Number(response.code) === 200) {
             // let updateAddScore
             // let ProtectNum = Number(formatCash(arg.ProtectNum));
             // thunkAPI.dispatch(updateAddScore(ProtectNum));
@@ -2223,4 +2220,3 @@ export const reqEmailChangeConfirm = createAsyncThunk(
 //         return response;
 //     }
 // );
-

+ 0 - 1
src/features/checkInSlice.js

@@ -109,7 +109,6 @@ export const CheckInSlice = createSlice({
         buildReqReducer(builder, reqCheckinGet, reducerCheckinGet, checkinGetStatus);
     },
 })
-export const { } = CheckInSlice.actions;
 export const getCheckInData = (state) => {
     return state.checkIn.checkInData;
 }

+ 169 - 110
src/features/colyseus/CSClient.js

@@ -1,150 +1,209 @@
 import { Client as ColyseusClient } from "colyseus.js";
+
 class CSClient {
-    constructor(roomName, serverUrl,messageCB,connectedCB,closeCB) {
-        this.roomName=roomName;
-        this.serverUrl=serverUrl;
-        this.messageCB=messageCB;
-        this.connectedCB=connectedCB;
-        this.closeCB=closeCB;
-        this.client=new ColyseusClient(serverUrl);
-        this.callbacks=[];
-        this.retryTimer=null;
-        this.retryTimeout=1000;
-        this.maxRetry=5;
-        this.currRetry=0;
-        this.isKicked=false;
-        //ping
-        this.pingTimer=null;
-        this.pingInterval=10000;
-        this.pingTimeout=0;
-        this.pingTimeoutMax=3;
-        this.unsetRoom();
+    constructor(roomName, serverUrl, messageCB, connectedCB, closeCB) {
+        this.roomName = roomName;
+        this.serverUrl = serverUrl;
+        this.messageCB = messageCB;
+        this.connectedCB = connectedCB;
+        this.closeCB = closeCB;
+        this.client = new ColyseusClient(serverUrl);
+        this.callbacks = [];
+        this.retryTimer = null;
+        this.retryTimeout = 1000;
+        this.maxRetry = 5;
+        this.currRetry = 0;
+        this.isKicked = false;
+        this.intentionalDisconnect = false;
+        this.closeNotified = false;
+        this.connectionGeneration = 0;
+        this.connectOptions = {};
+        this.pendingConnectResolvers = new Set();
+        this.currentRoom = null;
+        this.pingTimer = null;
+        this.pingInterval = 10000;
+        this.pingTimeout = 0;
+        this.pingTimeoutMax = 3;
     }
-    clearTimer(){
-        if (this.retryTimer!=null){
+
+    clearRetryTimer() {
+        if (this.retryTimer != null) {
             clearTimeout(this.retryTimer);
             this.retryTimer = null;
         }
-        this.pingTimeout=0;
     }
-    clearPingTimer(){
-        if (this.pingTimer){
+
+    clearPingTimer() {
+        if (this.pingTimer != null) {
             clearInterval(this.pingTimer);
-            this.pingTimer=null;
+            this.pingTimer = null;
         }
+        this.pingTimeout = 0;
     }
-    startRoomPing(){
+
+    startRoomPing() {
         this.clearPingTimer();
-        let self=this;
-        this.pingTimer=setInterval(()=>{
-            if (self.pingTimeout>self.pingTimeoutMax){
-                self.clearPingTimer();
-                console.log("connecting timeout");
-                self.reconnect();
+        this.pingTimer = setInterval(() => {
+            if (this.pingTimeout >= this.pingTimeoutMax) {
+                this.clearPingTimer();
+                this.reconnect();
                 return;
             }
-            self.pingTimeout++;
-            self.send("PING","");
-        },this.pingInterval);
+            this.pingTimeout += 1;
+            this.send("PING", "");
+        }, this.pingInterval);
     }
-    unsetRoom(){
-        console.log("unsetRoomunsetRoom");
-        if(this.currentRoom){
-            this.currentRoom.removeAllListeners();
-            this.currentRoom=null;
-        }
+
+    unsetRoom(room = this.currentRoom) {
+        if (!room || this.currentRoom !== room) return;
+        room.removeAllListeners();
+        this.currentRoom = null;
         this.clearPingTimer();
     }
-    setRoom(room){
-        this.currentRoom=room;
-        this.currRetry=0;
-        let self=this;
-        room.connection.transport.ws.addEventListener("close",()=>{
-            console.log("connection closed");
-            if (!self.isKicked)self.reconnect();
+
+    setRoom(room) {
+        this.currentRoom = room;
+        this.currRetry = 0;
+        this.closeNotified = false;
+
+        room.connection.transport.ws.addEventListener("close", () => {
+            if (this.currentRoom !== room) return;
+            this.unsetRoom(room);
+            if (!this.intentionalDisconnect && !this.isKicked) this.reconnect();
         });
         room.onStateChange((newState) => {
-            // skip unregistered (null) callbacks
-            self.callbacks.filter((_) => _).forEach((callback) => callback(newState));
+            this.callbacks.filter(Boolean).forEach((callback) => callback(newState));
         });
-        room.onMessage("*",(type,message)=>{
-            //被踢了
-            if (message!=null&&message.cmd==="kick"){
-                self.isKicked=true;
-            }
-            if (type==="PONG"){
-                self.pingTimeout--;
+        room.onMessage("*", (type, message) => {
+            if (message?.cmd === "kick") this.isKicked = true;
+            if (type === "PONG") {
+                this.pingTimeout = Math.max(0, this.pingTimeout - 1);
                 return;
             }
-            let host=window.location.host;
-            if (host.startsWith("localhost")){
-                console.log("Room:"+self.roomName+" onMessage:"+" type:"+type+" message:"+JSON.stringify(message))
+            if (window.location.hostname === "localhost") {
+                console.debug("Room message", { room: this.roomName, type, message });
             }
-            if (self.messageCB)self.messageCB(type,message);
-        })
+            this.messageCB?.(type, message);
+        });
         room.onError((code, message) => {
-            console.log("couldn't join", room.name,code,message);
+            console.warn("Colyseus room error", { room: room.name, code, message });
         });
-
-        // when leaving, reset the connection state indicator
-        room.onLeave((e)=>{
-            self.unsetRoom();
+        room.onLeave(() => {
+            const wasCurrentRoom = this.currentRoom === room;
+            this.unsetRoom(room);
+            if (wasCurrentRoom && !this.intentionalDisconnect && !this.isKicked) this.reconnect();
         });
-        if (this.connectedCB)this.connectedCB(true);
+
+        this.connectedCB?.(true);
         this.startRoomPing();
     }
-    async reconnect(resolve){
-        let during=this.retryTimeout+this.currRetry*2000;
-        this.clearTimer();
+
+    reconnect(resolve) {
+        if (this.intentionalDisconnect || this.isKicked) {
+            resolve?.(false);
+            return;
+        }
+
+        this.clearRetryTimer();
         this.clearPingTimer();
-        if (this.currRetry>=this.maxRetry){
-            if (resolve)resolve(false);
-            this.currRetry=0;
-            if (this.closeCB)this.closeCB();
+        if (this.currRetry >= this.maxRetry) {
+            resolve?.(false);
+            if (!this.closeNotified) {
+                this.closeNotified = true;
+                this.closeCB?.();
+            }
             return;
         }
-        let self=this;
-        this.retryTimer=setTimeout(()=>{
-            self.currRetry++;
-            self.connect({}).then((data)=>{
-                if (resolve)resolve(data);
-            })
-        },during);
+
+        const generation = this.connectionGeneration;
+        const delay = this.retryTimeout + this.currRetry * 2000;
+        this.retryTimer = setTimeout(() => {
+            this.retryTimer = null;
+            if (generation !== this.connectionGeneration || this.intentionalDisconnect || this.isKicked) {
+                resolve?.(false);
+                return;
+            }
+            this.currRetry += 1;
+            this.connect(this.connectOptions, true).then((connected) => resolve?.(connected));
+        }, delay);
     }
-    async connect(options={}){
-        let self=this;
+
+    async connect(options = {}, isRetry = false) {
+        if (!isRetry) {
+            this.intentionalDisconnect = false;
+            this.isKicked = false;
+            this.closeNotified = false;
+            this.currRetry = 0;
+            this.connectOptions = { ...options };
+            this.connectionGeneration += 1;
+        }
+
+        const generation = this.connectionGeneration;
         this.clearPingTimer();
-        if(this.isConnected())await this.disconnect();
-        return new Promise((resolve, reject)=>{
-            self.client.joinOrCreate(this.roomName,options).then((room)=>{
-                self.setRoom(room);
-                resolve(true);
-            }).catch((e)=>{
-                self.reconnect(resolve);
-            })
+        if (this.currentRoom) {
+            const previousRoom = this.currentRoom;
+            this.unsetRoom(previousRoom);
+            try {
+                await previousRoom.leave();
+            } catch (error) {
+                console.warn("Failed to leave previous Colyseus room", error);
+            }
+        }
+
+        return new Promise((resolve) => {
+            let settled = false;
+            const finish = (connected) => {
+                if (settled) return;
+                settled = true;
+                this.pendingConnectResolvers.delete(finish);
+                resolve(connected);
+            };
+            this.pendingConnectResolvers.add(finish);
+
+            this.client.joinOrCreate(this.roomName, this.connectOptions).then((room) => {
+                if (generation !== this.connectionGeneration || this.intentionalDisconnect) {
+                    Promise.resolve(room.leave()).catch(() => null);
+                    finish(false);
+                    return;
+                }
+                this.setRoom(room);
+                finish(true);
+            }).catch(() => {
+                if (generation !== this.connectionGeneration || this.intentionalDisconnect) {
+                    finish(false);
+                    return;
+                }
+                this.reconnect(finish);
+            });
         });
     }
-    async disconnect(){
-        let self=this;
-        return new Promise((resolve, reject)=>{
-            if (self.currentRoom!=null){
-                self.currentRoom.leave();
-                self.unsetRoom();
-                resolve(true);
-            }
-            else{
-                self.unsetRoom();
-                resolve(true);
+
+    async disconnect() {
+        this.intentionalDisconnect = true;
+        this.connectionGeneration += 1;
+        this.clearRetryTimer();
+        this.clearPingTimer();
+        [...this.pendingConnectResolvers].forEach((resolve) => resolve(false));
+
+        const room = this.currentRoom;
+        this.unsetRoom(room);
+        if (room) {
+            try {
+                await room.leave();
+            } catch (error) {
+                console.warn("Failed to leave Colyseus room", error);
             }
-        })
-    }
-    send(type,message){
-        if (this.currentRoom){
-            this.currentRoom.send(type,message);
         }
+        return true;
     }
-    isConnected(){
-        return this.currentRoom!=null;
+
+    send(type, message) {
+        this.currentRoom?.send(type, message);
+    }
+
+    isConnected() {
+        return this.currentRoom != null;
     }
 }
+
 export default CSClient;

+ 2 - 2
src/features/colyseus/CSClientWrap.js

@@ -5,7 +5,7 @@ class CSClientWrap{
         this.clients={};
     }
     async connect(roomName, serverUrl,messageCB=null,connectedCB=null,closeCB=null,options={}){
-        this.disconnect(roomName);
+        await this.disconnect(roomName);
         let client=new CSClient(roomName,serverUrl,messageCB,connectedCB,closeCB);
         this.clients[roomName]=client;
         let isConnected=await client.connect(options);
@@ -14,8 +14,8 @@ class CSClientWrap{
     async disconnect(roomName){
         let client=this.clients[roomName];
         if (client!=null){
-            client.disconnect();
             delete this.clients[roomName];
+            await client.disconnect();
         }
         return true;
     }

+ 3 - 2
src/features/gameListSlice.js

@@ -26,10 +26,11 @@ export const GameList=createSlice({
         buildReqReducer(builder,reqGameList,reducerGameList,gameListStatus);
     },
 })
-export const {} = GameList.actions;
 export const getGameListStatus=(mkey)=>(state)=>{
     let fetchStatus=state.gamelist.fetchStatus;
-    let status=fetchStatus[mkey]==undefined?REQ_STATUS.NONE:fetchStatus[mkey];
+    const status = fetchStatus[mkey] === undefined || fetchStatus[mkey] === null
+        ? REQ_STATUS.NONE
+        : fetchStatus[mkey];
     return status;
 }
 

+ 1 - 3
src/features/luckyRollSlice.js

@@ -2,8 +2,6 @@ import { createSlice, createDraftSafeSelector } from "@reduxjs/toolkit";
 import { REQ_STATUS } from "@api/config";
 import { buildReqReducer, reqLuckyRollExchange, reqLuckyRollFirstFree, reqLuckyRollGifts, reqLuckyRollInfo, reqLuckyRollLogs, reqLuckyRollSaturdayFree, reqLuckyRollStart, reqLuckyRollState, reqLuckyRollSwitchMode, reqLuckyRollVIPFreeClaim, reqLuckyRollVIPFreeInfo } from "@features/api";
 
-const i18n = global.i18n;
-
 const reducerLuckyRollInfo = (state, action, arg) => {
     const payload = action.payload;
     if (payload?.code === 200 && payload?.data) {
@@ -263,4 +261,4 @@ export const selectLuckyRollWithDetails = createDraftSafeSelector(
     })
 );
 
-export default LuckyRoll.reducer;
+export default LuckyRoll.reducer;

+ 46 - 40
src/features/mainSlice.js

@@ -1,5 +1,5 @@
 import { initTrackAll, trackEvent, restoreQuickSaveEnv } from "../tracking/TrackAll";
-import { createSlice } from "@reduxjs/toolkit";
+import { createSelector, createSlice } from "@reduxjs/toolkit";
 import { routes } from "@db/routes";
 import {
     ROUTE_PATH,
@@ -54,12 +54,10 @@ import {
     LOCAL_BALANCE_POPUP_TS,
     LOCAL_CUSTOM_LOCK,
     LOCAL_DEFAULT_COUNTRY,
-    LOCAL_FAVO_IDS, LOCAL_GAME_URL,
-    LOCAL_IS_LOGIN,
+    LOCAL_GAME_URL,
     LOCAL_LAST_LOGIN_TYPE,
     LOCAL_REGISTER_DATE_TS,
     LOCAL_SIGN,
-    LOCAL_USER_INFO,
     LOCAL_VISTOR_ID,
     LOCAL_GOOGLE_REDIRECT_JUMPAPK,
     LOCAL_GOOGLE_REDIRECT_APK_ENV,
@@ -93,7 +91,7 @@ const generateRoutes = (routes, asyncRoutes = []) => {
     //路由等级
     //ppath parent path
     const _generate = (list, plevel, ppath) => {
-        Object.keys(list).map((key) => {
+        Object.keys(list).forEach((key) => {
             let route = list[key];
             if (route) {
                 if (allMap[route.path] == null) {
@@ -132,7 +130,7 @@ const parseThemeStypes = (data) => {
             let item = data[key];
             if (item != null) {
                 if (typeof item == "string") {
-                    if (item.indexOf(";") != -1) {
+                    if (item.indexOf(";") !== -1) {
                         data[key] = item.split(";");
                     }
                 }
@@ -150,10 +148,17 @@ const parseStyles = (data) => {
                 let styleid = item.styleid;
                 let style = item.style;
                 let json = {};
-                if (style != null && style != "") {
-                    json = JSON.parse(style);
-                    json['dark'] = parseThemeStypes(json['dark']);
-                    json['light'] = parseThemeStypes(json['light']);
+                if (style != null && style !== "") {
+                    try {
+                        const parsed = typeof style === "string" ? JSON.parse(style) : style;
+                        if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
+                            json = parsed;
+                            json['dark'] = parseThemeStypes(json['dark']);
+                            json['light'] = parseThemeStypes(json['light']);
+                        }
+                    } catch (e) {
+                        // A malformed optional theme must not abort route initialization.
+                    }
                 }
                 styles[styleid] = { styleid, style: json };
             }
@@ -260,7 +265,7 @@ const applyGoogleRedirectDebugConf = (googleRedirectH5, googleRedirectJumpApk, g
 };
 const reducerRoutes = (state, action) => {
     let json = action.payload;
-    if (json && json.code == 0) {
+    if (json && Number(json.code) === 0) {
         // _dmr=1 路径首次响应仅用于把 conf.s_p 中的 env 还原到本地(cookie/LS),
         // 此时本次请求未带恢复后的 sign,user 必为空;若继续更新 state 会让 isLogin=0
         // 触发 CHashRouter 的登录守卫,把用户从 #/freewithdraw 跳到 #/signin。
@@ -525,7 +530,7 @@ const parseLogin = (state, action, arg, isShowMsg = true) => {
     let code = payload.code;
     let msg = getMsg(payload);
     //success
-    if (code == 200) {
+    if (Number(code) === 200) {
         let data = payload.data;
         if (data.bb == null) {
             if (state.userInfo != null) data.bb = state.userInfo.bb;
@@ -581,7 +586,7 @@ const reducerRegGold = (state, action, arg) => {
     //payload:{"data":[],"msg":"Fail","code":301}
     let code = payload.code;
     let msg = getMsg(payload);
-    if (code == 200) {
+    if (Number(code) === 200) {
         let data = payload.data || {};
         let user = data.user;
         if (!!user) {
@@ -604,7 +609,7 @@ const reducerLogout = (state, action, arg) => {
     let payload = action.payload;
     let code = payload.code;
     let msg = getMsg(payload);
-    if (code == 200) {
+    if (Number(code) === 200) {
         state.userInfo = {};
         state.isLogin = 0;
         localStorageSet(LOCAL_SIGN, "");
@@ -691,7 +696,7 @@ const parseVipUpgrade = (state, prevLv, vip, preFreeBonus = -1) => {
 }
 const reducerGameInfo = (state, action, arg) => {
     const prevLv = state.userInfo?.vip || 0;
-    const insureScore = state.userinfo?.InsureScore || 0;
+    const insureScore = state.userInfo?.InsureScore || 0;
     parseLogin(state, action, arg, false);
 
     const vip = state.userInfo?.vip || 0;
@@ -879,13 +884,13 @@ const sendCodeStatus = (state, status, arg) => {
 const reducerBindSMS = (state, action, arg) => {
     let payload = action.payload;
     state.oldUserInfo = null;
-    if (payload.code == 301 && payload.data && payload.data.UserID) {
+    if (Number(payload.code) === 301 && payload.data && payload.data.UserID) {
         // 已有账号
         state.oldUserInfo = payload.data;
-    } else if (payload.code == 200) {
+    } else if (Number(payload.code) === 200) {
         toast.success(global.i18n.t("bindPhone.success"), { toastId: "login" });
     }
-    parseLogin(state, action, arg, payload.code != 200);
+    parseLogin(state, action, arg, Number(payload.code) !== 200);
 };
 const bindSMSStatus = (state, status, arg) => {
     state.loginStatus.bind = status;
@@ -933,7 +938,7 @@ const updateVipStatus = (state, arg) => {
     let leftDays = 0;
     let status = VIPStatus.Active;
 
-    if (vip == 0) {
+    if (Number(vip) === 0) {
         status = VIPStatus.Trial;
         if (trialEnd) {
             /// 非vip用户 体验已到期
@@ -962,6 +967,8 @@ const updateVipStatus = (state, arg) => {
                 leftDays = expireDay - lastRecharge;
             }
             break;
+        default:
+            break;
     }
 
     const vipConfig = state.vipConfig || [];
@@ -1069,11 +1076,8 @@ const reducerChangeBindPhone = (state, action, arg) => {
     }
 };
 
-let changeLimitTimer = null;
-
 const reducerChangeLimitInfo = (state, action, arg) => {
     let payload = action.payload;
-    let msg = getHttpMsg(payload);
     if (payload && payload.code === 200) {
         const data = payload.data;
         if (!!data) {
@@ -1093,7 +1097,6 @@ const setChangeBindPhoneStatus = (state, status, arg) => {
 
 const reducerResetLogin = (state, action, arg) => {
     let payload = action.payload;
-    let msg = getHttpMsg(payload);
     if (payload && payload.code === 200) {
         onLogout(state, false);
         localStorageSet(LOCAL_RESET_LOGIN_STATES, "1");
@@ -1387,6 +1390,7 @@ export const allNavs = (state) => {
 export const allTopNavs = (state) => {
     return state.main.allTopNavs;
 };
+const EMPTY_ROUTE = Object.freeze({});
 const getRouteFromPath = (state, path) => {
     let allMap = routesMap(state);
     let route = allMap[path];
@@ -1401,24 +1405,26 @@ const getRouteFromPath = (state, path) => {
 };
 export const getRoute = (path) => (state) => {
     let route = getRouteFromPath(state, path);
-    if (route == null) return {};
+    if (route == null) return EMPTY_ROUTE;
     return route;
 };
-export const getRouteChain = (path) => (state) => {
-    let map = routesMap(state);
-    let list = [];
-    let getChain = (cpath) => {
-        let route = map[cpath];
-        if (route != null) {
-            list.unshift(route);
-            if (route.ppath != null && route.ppath !== "") {
-                getChain(route.ppath);
+export const getRouteChain = (path) => createSelector(
+    [routesMap],
+    (map) => {
+        const list = [];
+        const addRouteChain = (currentPath) => {
+            const route = map[currentPath];
+            if (route != null) {
+                list.unshift(route);
+                if (route.ppath != null && route.ppath !== "") {
+                    addRouteChain(route.ppath);
+                }
             }
-        }
-    };
-    getChain(path);
-    return list;
-};
+        };
+        addRouteChain(path);
+        return list;
+    }
+);
 export const getRouteStyles = (state) => {
     let styles = state.main.styles;
     return styles;
@@ -1645,7 +1651,7 @@ export const getUserNick = (state) => {
 export const getUserRegisted = (state) => {
     let userInfo = state.main.userInfo;
     if (userInfo) {
-        return userInfo.Registed == 1;
+        return Number(userInfo.Registed) === 1;
     }
 
     return false;
@@ -1779,4 +1785,4 @@ export const getChangeLimitInfo = state => state.main.changeLimit;
 
 export const getIsAutoEnterGame = state => state.main.isAutoEnterGame;
 
-export const getJumpToH5 = state => state.main.jumpToH5;
+export const getJumpToH5 = state => state.main.jumpToH5;

+ 16 - 8
src/features/paySlice.js

@@ -190,7 +190,7 @@ const reducerRecallGiftInfo = (state, action, arg) => {
         let total_bonus = 0;
         payload.data.days?.forEach((day) => {
             total_bonus += day.amount;
-            if (day.status == 2) {
+            if (Number(day.status) === 2) {
                 // 已领取
                 claimed_bonus += day.amount;
                 claimed_days++;
@@ -202,7 +202,7 @@ const reducerRecallGiftInfo = (state, action, arg) => {
         let current_day_idx = current_day - 1;
         const days = payload.data.days;
         const next_day = days[current_day_idx + 1];
-        if (next_day && next_day.status == 1) {
+        if (next_day && Number(next_day.status) === 1) {
             /// 下一天可领取
             current_day_idx++;
             current_day++;
@@ -223,7 +223,7 @@ const reducerRecallGiftInfo = (state, action, arg) => {
             state.recallGiftInfo.claimed_count = claimed_days;
         }
 
-        if (state.recallGiftInfo.total_bonus == 0) {
+        if (Number(state.recallGiftInfo.total_bonus) === 0) {
             state.recallGiftInfo.total_bonus = total_bonus;
         }
     }
@@ -775,16 +775,25 @@ export const checkDailyGift = (state) => {
 
     return list.some(v => !v.is_completed);
 }
+const getFirstGear = (value) => {
+    if (Array.isArray(value)) return value[0] || null;
+    if (typeof value !== "string" || !value) return null;
+    try {
+        const gears = JSON.parse(value);
+        return Array.isArray(gears) ? gears[0] || null : null;
+    } catch (e) {
+        return null;
+    }
+};
 export const getFirstPay = (state) => {
     let firstconfig = state.pay.firstconfig;
-    let config = state.pay.config;
+    const config = Array.isArray(state.pay.config) ? state.pay.config : [];
     let order = null;
     if (firstconfig != null) {
         let first_pay = firstconfig.first_pay;
         let recomendar_recharge = firstconfig.recomendar_recharge;
         if (first_pay != null) {
-            let gear = JSON.parse(first_pay.gear);
-            if (gear) gear = gear[0];
+            const gear = getFirstGear(first_pay.gear);
             order = {
                 goodsName: "firstpay",
                 goodsPrice: first_pay.money,
@@ -798,8 +807,7 @@ export const getFirstPay = (state) => {
             for (let i = 0; i < config.length; i++) {
                 const elem = config[i];
                 if (elem.id === recomendar_recharge) {
-                    let gear = JSON.parse(elem.gear);
-                    if (gear) gear = gear[0];
+                    const gear = getFirstGear(elem.gear);
                     order = {
                         goodsName: "chip",
                         goodsPrice: elem.money,

+ 17 - 12
src/features/protectSlice.js

@@ -3,6 +3,14 @@ import { REQ_STATUS } from "@api/config";
 import { buildReqReducer, reqProtectCheck, reqProtectGet } from "@features/api";
 import { getHttpMsg } from "@utils/helpers";
 import { toast } from "react-toastify";
+
+const getProtectScore = (user) => {
+    if (!user) return 0;
+    const score = Number(user.vip > 0 ? user.Score : user.InsureScore);
+    const cashBase = Number(global.CASH_BASE);
+    return Number.isFinite(score) && Number.isFinite(cashBase) ? score * cashBase : 0;
+};
+
 const reducerProtectCheck = (state, action, arg) => {
     let payload = action.payload;
     if (payload && payload.code === 200) {
@@ -27,10 +35,10 @@ const reducerProtectGet = (state, action, arg) => {
         let CurProtectTimes = state.protectInfo.CurProtectTimes != null ? Number(state.protectInfo.CurProtectTimes) : 0;
         CurProtectTimes = CurProtectTimes + 1;
         state.protectInfo.CurProtectTimes = CurProtectTimes;
-        if (msg != null && msg != "") toast.success(msg, { toastId: "protectGet" });
+        if (msg != null && msg !== "") toast.success(msg, { toastId: "protectGet" });
     }
     else {
-        if (msg != null && msg != "") toast.error(msg, { toastId: "protectGet" });
+        if (msg != null && msg !== "") toast.error(msg, { toastId: "protectGet" });
     }
     if (arg.callback != null) arg.callback(payload);
 };
@@ -57,7 +65,6 @@ export const ProtectSlice = createSlice({
         buildReqReducer(builder, reqProtectGet, reducerProtectGet, protectGetStatus);
     }
 });
-export const { } = ProtectSlice.actions;
 export const getProtectInfo = (state) => {
     return state.protect.protectInfo;
 };
@@ -68,13 +75,12 @@ export const checkCanProtect = (state) => {
     const user = state.main.userInfo;
     const protectInfo = state.protect.protectInfo;
     if (user != null && protectInfo != null) {
-        if (protectInfo.can_receive != 1) {
+        if (Number(protectInfo.can_receive) !== 1) {
             /// 正在冷却
             return false;
         }
         /// 分数太多
-        let Score = user.vip > 0 ? Number(user.Score) : Number(user.InsureScore);
-        Score *= global.CASH_BASE;
+        const Score = getProtectScore(user);
         let ProtectLimit = protectInfo.ProtectLimit != null ? parseInt(protectInfo.ProtectLimit) : 0;
         let CurProtectTimes = protectInfo.CurProtectTimes != null ? parseInt(protectInfo.CurProtectTimes) : 0;
         let ProtectTimes = protectInfo.ProtectTimes != null ? parseInt(protectInfo.ProtectTimes) : 0;
@@ -86,19 +92,18 @@ export const getProtectBalance = (state) => {
     const user = state.main.userInfo;
     const protectInfo = state.protect.protectInfo;
     if (user != null && protectInfo != null) {
-        let Score = user.vip > 0 ? Number(user.Score) : Number(user.InsureScore);
-        Score *= global.CASH_BASE;
-        return Score;
+        return getProtectScore(user);
     }
     return 0;
 };
 export const getProtectReward = (state) => {
     const protectInfo = getProtectInfo(state);
     if (protectInfo != null) {
-        let protectNum = parseFloat(protectInfo.ProtectNum || 0);
+        let protectNum = Number(protectInfo.ProtectNum || 0);
         if (Number.isNaN(protectNum)) protectNum = 0;
-        return protectNum * protectInfo.Alpha;
+        const alpha = Number(protectInfo.Alpha);
+        return Number.isFinite(alpha) ? protectNum * alpha : 0;
     }
     return 0;
 };
-export default ProtectSlice.reducer;
+export default ProtectSlice.reducer;

+ 6 - 35
src/features/redemptionCodeSlice.js

@@ -1,9 +1,8 @@
-import { createSlice, createSelector } from "@reduxjs/toolkit";
+import { createSlice } from "@reduxjs/toolkit";
 import { REQ_STATUS } from "@api/config";
-import { buildReqReducer, reqEmailBindSendCode, reqEmailChangeConfirm, reqEmailChangeSendCode, reqEmailInfo, reqEmailRedeem, reqRedemptionCodeClaim, reqRedemptionCodeInfo, reqRedemptionCodeRedeem } from "@features/api";
-
-const i18n = global.i18n;
+import { buildReqReducer, reqEmailBindSendCode, reqEmailChangeConfirm, reqEmailChangeSendCode, reqEmailInfo, reqEmailRedeem } from "@features/api";
 
+/* eslint-disable no-unused-vars -- Redemption-code endpoints are temporarily disabled below. */
 const reducerRedemptionCodeInfo = (state, action, arg) => {
     const payload = action.payload;
     // const payload = {
@@ -91,6 +90,7 @@ const reducerRedemptionCodeClaim = (state, action, arg) => {
 
     }
 };
+/* eslint-enable no-unused-vars */
 
 
 const reducerEmailInfo = (state, action, arg) => {
@@ -127,20 +127,12 @@ const reducerEmailInfo = (state, action, arg) => {
 };
 
 const reducerEmailBindSendCode = (state, action, arg) => {
-    const payload = action.payload;
-    if (payload?.code === 200 && payload?.data) {
-        const data = payload.data;
-
-    } else {
-
-    }
 };
 
 const reducerEmailRedeem = (state, action, arg) => {
     const payload = action.payload;
     if (payload?.code === 200 && payload?.data) {
-        const data = payload.data;
-
+        return;
     } else if (payload?.code === 301 && payload?.data && payload?.msg === "invalid_reward_code") {
         const data = payload.data;
         state.emailInfo.reward_fail_count = data?.reward_fail_count;
@@ -151,28 +143,9 @@ const reducerEmailRedeem = (state, action, arg) => {
 };
 
 const reducerEmailChangeSendCode = (state, action, arg) => {
-    const payload = action.payload;
-    if (payload?.code === 200 && payload?.data) {
-        const data = payload.data;
-    } else {
-
-    }
 };
 
 const reducerEmailChangeConfirm = (state, action, arg) => {
-    const payload = action.payload;
-    if (payload?.code === 200 && payload?.data) {
-        const data = payload.data;
-    } else {
-
-    }
-};
-
-const redeemStatus = (state, status, arg) => {
-    state.redeemStatus = status;
-};
-const claimStatus = (state, status, arg) => {
-    state.claimStatus = status;
 };
 
 const emailBindSendCodeStatus = (state, status, arg) => {
@@ -231,8 +204,6 @@ export const RedemptionCodeSlice = createSlice({
     },
 });
 
-export const { } = RedemptionCodeSlice.actions;
-
 // 基础选择器(简单的值查找,不需要记忆化)
 export const selectRedemptionCodeInfo = state => state.redemptionCode.redemptionCodeInfo;
 
@@ -254,4 +225,4 @@ export const selectEmailChangeSendCodeStatus = state => state.redemptionCode.ema
 export const selectEmailChangeConfirmStatus = state => state.redemptionCode.emailStatus.changeConfirm;
 
 
-export default RedemptionCodeSlice.reducer;
+export default RedemptionCodeSlice.reducer;

+ 3 - 3
src/features/redpackSlice.js

@@ -56,7 +56,7 @@ const reducerRedpackRecive=(state,action,arg)=>{
         }
     }else
     {
-        let obj=payload.msg;
+        let obj=payload?.msg;
         let msg="";
         if (obj){
             if (obj.message!=null){
@@ -102,7 +102,7 @@ export const RedpackSlice = createSlice({
             }
         }),
         updateAlRecivedStatus:((state, action) => {
-            state.redPackData.status=-1;
+            if (state.redPackData) state.redPackData.status=-1;
         })
     },
     extraReducers: (builder) => {
@@ -124,4 +124,4 @@ export const getReciveStatus=(state)=>{
 export const getRedConfigStatus=(state)=>{
     return state.redpack.status.RedpackConfig;
 }
-export default RedpackSlice.reducer;
+export default RedpackSlice.reducer;

+ 3 - 3
src/features/socketSlice.js

@@ -53,7 +53,7 @@ export default Socket.reducer;
 export const connectServer=({roomName, serverUrl,messageCB=null,onRoomConnect=null,onRoomTimeoutClose=null,options={}})=>{
     return async (dispatch, getState) => {
         if (CSClientWrap.isConnected(roomName))return ;
-        let isConnected=await CSClientWrap.connect(roomName,serverUrl,(type,message)=>{
+        await CSClientWrap.connect(roomName,serverUrl,(type,message)=>{
             dispatch(onMessage({roomName,type,message}));
             if (messageCB!=null)dispatch(messageCB(type,message));
         },(connected)=>{
@@ -62,7 +62,7 @@ export const connectServer=({roomName, serverUrl,messageCB=null,onRoomConnect=nu
         },()=>{
             dispatch(onDisconnect(roomName));
             if (onRoomTimeoutClose!=null)dispatch(onRoomTimeoutClose());
-        });
+        }, options);
     }
 }
 export const disconnectServer=(roomName)=>{
@@ -76,4 +76,4 @@ export const sendToServer=({roomName,type,message=""})=>{
         CSClientWrap.send(roomName,type,message);
         dispatch(onSend({roomName,type,message}));
     }
-}
+}

+ 1 - 3
src/features/superBallSlice.js

@@ -146,8 +146,6 @@ export const SuperBall = createSlice({
     },
 })
 
-export const { } = SuperBall.actions;
-
 export const getSuperBallHistory = state => state.superBall.history;
 
 export const getSuperBallInfo = state => state.superBall.superBallInfo;
@@ -170,4 +168,4 @@ export const getClaimRewardStatus = state => state.superBall.status.ClaimRewardS
 export const getSubmitBallStatus = state => state.superBall.status.SubmitBallStatus;
 
 
-export default SuperBall.reducer;
+export default SuperBall.reducer;

+ 3 - 3
src/features/worldCupSlice.js

@@ -2,7 +2,7 @@ import { createSlice } from "@reduxjs/toolkit";
 import { REQ_STATUS } from "@api/config";
 import { buildReqReducer, reqWorldCupBetLog, reqWorldCupBetPanel, reqWorldCupBets, reqWorldCupBind, reqWorldCupFavorite, reqWorldCupInfo, reqWorldCupInviteLog, reqWorldCupMatches, reqWorldCupRewardLog, reqWorldCupWinner, } from "@features/api";
 import { toast } from "react-toastify";
-import { getHttpMsg, localStorageGet, localStorageRemove, localStorageSet } from "@utils/helpers";
+import { getHttpMsg, localStorageGet, localStorageSet } from "@utils/helpers";
 import { LOCAL_WORLD_CUP_REWARD_PAID } from "@constants/localkey";
 import { MatchStatus } from "@constants/worldCup";
 
@@ -12,7 +12,7 @@ const sortWinners = (list, sort) => {
     list.sort((a, b) => {
         const oddA = parseFloat(a.decimal_odds || 0);
         const oddB = parseFloat(b.decimal_odds || 0);
-        if (oddA == oddB) return 0;
+        if (oddA === oddB) return 0;
 
         if (sort === 'asc') {
             /// 升序  low > height
@@ -378,4 +378,4 @@ export const getWorldCupBetLogStatus = state => state.worldCup.status.BetsLog;
 export const getWorldCupBindCodeStatus = state => state.worldCup.status.BindCode;
 
 
-export default WorldCupSlice.reducer;
+export default WorldCupSlice.reducer;

+ 1 - 1
src/hooks/useAuthLockRoute.js

@@ -7,7 +7,7 @@ const useAuthLockRoute=()=>{
     const [lockPath, setLockPath] = useState("");
     //如果不是登录和注册则清除
     const unLockRoute=(path,force=false)=>{
-        if (!force&&(path==ROUTE_PATH.SIGN_IN||path==ROUTE_PATH.SIGN_UP)){
+        if (!force && (path === ROUTE_PATH.SIGN_IN || path === ROUTE_PATH.SIGN_UP)) {
             return;
         }
         setLockPath("");

+ 4 - 15
src/hooks/useAuthRoute.js

@@ -1,21 +1,10 @@
 import {useLocation} from 'react-router-dom';
-import {useEffect, useState} from 'react';
+
+const AUTH_ROUTES = ['/login', '/sign-up'];
 
 const useAuthRoute = () => {
-    const authRoutes = ['/login', '/sign-up'];
     const location = useLocation();
-    const [isAuthRoute, setIsAuthRoute] = useState(false);
-
-    useEffect(() => {
-        setIsAuthRoute(authRoutes.includes(location.pathname));
-
-        return () => {
-            setIsAuthRoute(false);
-        }
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [location]);
-
-    return isAuthRoute;
+    return AUTH_ROUTES.includes(location.pathname);
 }
 
-export default useAuthRoute
+export default useAuthRoute

+ 2 - 2
src/hooks/useBlockSystemBack.js

@@ -16,7 +16,7 @@ const useBlockSystemBack = () => {
 
             // 只要是在屏幕最左侧 30px 内按下,就标记为边缘起手
             if (touchX < 30) {
-                let tag = e.target?.tagName.toLowerCase() || "";
+                let tag = e.target?.tagName?.toLowerCase() || "";
                 isEdgeStart = true;
 
                 if (!IgnoreEls[tag]) {
@@ -47,4 +47,4 @@ const useBlockSystemBack = () => {
 
 };
 
-export default useBlockSystemBack;
+export default useBlockSystemBack;

+ 55 - 61
src/hooks/useEdgeGesture.js

@@ -1,8 +1,6 @@
-import {useEffect, useState} from "react";
+import {useCallback, useEffect, useState} from "react";
 import {localStorageGet, localStorageSet} from "@utils/helpers";
-const objInfo={
-    timer:null
-}
+
 const useEdgeGesture = () => {
     const [isSideGesture, setIsSideGesture] = useState(false);
     const getSidedGesture = () => {
@@ -12,73 +10,69 @@ const useEdgeGesture = () => {
         }
         return 0;
     }
-    const setSidedGesture = (isGuesture=0) => {
-        setIsSideGesture(isGuesture);
-        localStorageSet("IOS_GUESTURE",isGuesture);
-    }
-    let isEdgeGesture=false;
-    let isTouchingEdge = false;
-    const handleGesture=(e)=>{
-        if (global.isIOS){
-            // 获取触摸点的坐标
-            const touchX = e.touches[0].clientX;
-            const touchY = e.touches[0].clientY;
+    const setSidedGesture = useCallback((isGesture = 0) => {
+        setIsSideGesture(isGesture);
+        localStorageSet("IOS_GUESTURE", isGesture);
+    }, []);
+
+    useEffect(() => {
+        let timer = null;
+        let isEdgeGesture = false;
+        let isTouchingEdge = false;
+
+        const resetTimer = () => {
+            if (timer != null) {
+                clearTimeout(timer);
+                timer = null;
+            }
+        };
+
+        const removeTouchMove = () => {
+            document.removeEventListener('touchmove', handleGestureMove);
+        };
+
+        function handleGestureMove() {
+            if (!isEdgeGesture || isTouchingEdge) return;
+
+            isTouchingEdge = true;
+            resetTimer();
+            timer = setTimeout(() => {
+                if (isTouchingEdge) {
+                    setSidedGesture(1);
+                    isTouchingEdge = false;
+                    removeTouchMove();
+                }
+            }, 60);
+        }
+
+        const handleGesture = (event) => {
+            if (!global.isIOS || !event.touches?.[0]) return;
 
-            // 假设边缘手势区域的阈值为 20px
+            const { clientX: touchX, clientY: touchY } = event.touches[0];
             const edgeThreshold = 20;
+            isEdgeGesture = touchX < edgeThreshold
+                || touchX > window.innerWidth - edgeThreshold
+                || touchY < edgeThreshold
+                || touchY > window.innerHeight - edgeThreshold;
+            isTouchingEdge = false;
+            removeTouchMove();
 
-            // 检查触摸点是否在屏幕边缘
-            const isGesture = touchX < edgeThreshold || touchX > window.innerWidth - edgeThreshold ||
-                touchY < edgeThreshold || touchY > window.innerHeight - edgeThreshold;
-            isEdgeGesture=isGesture;
             if (isEdgeGesture) {
-                isTouchingEdge = false;
-                // 如果触摸点在边缘区域内
-                removeTouchMove();
-                document.addEventListener("touchmove",handleGestureMove);
+                document.addEventListener("touchmove", handleGestureMove);
             } else {
-                // 如果触摸点不在边缘区域内,重置触摸边缘的状态
-                isTouchingEdge = false;
                 setSidedGesture(0);
-                removeTouchMove();
-            }
-        }
-    }
-    const handleGestureMove=(e)=>{
-        if (isEdgeGesture){
-            if (!isTouchingEdge) {
-                // 如果之前没有触摸边缘,记录触摸边缘的状态,并设置一个定时器
-                isTouchingEdge = true;
-                resetTimer();
-                objInfo.timer=setTimeout(function() {
-                    if (isTouchingEdge) {
-                        // // 这里可以执行相应的逻辑
-                        setSidedGesture(1);
-                        isTouchingEdge = false;
-                        removeTouchMove();
-                    }
-                }, 60); // 设置一个时间阈值,例如 300 毫秒
             }
-        }
-    }
-    const resetTimer=()=>{
-        if (objInfo.timer!=null){
-            clearTimeout(objInfo.timer);
-            objInfo.timer=null;
-        }
-    }
-    const removeTouchMove=()=>{
-        document.removeEventListener('touchmove', handleGestureMove);
-    }
-    useEffect(() => {
+        };
+
         // 监听 touchstart 事件
         document.addEventListener('touchstart', handleGesture);
-        return ()=>{
+        return () => {
             document.removeEventListener('touchstart', handleGesture);
             removeTouchMove();
             resetTimer();
-        }
-    }, []);
-    return {isSideGesture,setIsSideGesture,getSidedGesture,setSidedGesture};
+        };
+    }, [setSidedGesture]);
+
+    return {isSideGesture, setIsSideGesture, getSidedGesture, setSidedGesture};
 }
-export default useEdgeGesture;
+export default useEdgeGesture;

+ 28 - 41
src/hooks/useElementScroll.js

@@ -3,61 +3,48 @@ import {useEffect, useState, useRef} from 'react';
 const useElementScroll = () => {
     const ref = useRef(null);
     const [isOverflown, setIsOverflown] = useState(undefined);
-    const [contentHeight, setContentHeight] = useState(undefined);
     const [isTop, setIsTop] = useState(true);
     const [isBottom, setIsBottom] = useState(false);
 
-    const handleScroll = () => {
+    useEffect(() => {
         const { current } = ref;
+        const scrollElement = current?.children?.[0];
+        if (!current || !scrollElement) return;
 
-        if (current.children[0].scrollTop === 0) {
-            current.classList.add('is-top');
-            setIsTop(true);
-        } else {
-            current.classList.remove('is-top');
-            setIsTop(false);
-        }
-
-        const inScrolledToBottom =
-            current.children[0].scrollHeight === (Math.ceil(current.children[0].scrollTop) + current.children[0].clientHeight)
-            ||
-            current.children[0].scrollHeight === (Math.floor(current.children[0].scrollTop) + current.children[0].clientHeight);
-
-        if (inScrolledToBottom) {
-            current.classList.add('is-bottom');
-            setIsBottom(true);
-        } else {
-            current.classList.remove('is-bottom');
-            setIsBottom(false);
-        }
-    }
+        const handleScroll = () => {
+            const atTop = scrollElement.scrollTop <= 0;
+            current.classList.toggle('is-top', atTop);
+            setIsTop(atTop);
 
-    useEffect(() => {
-        const { current } = ref;
+            const atBottom = scrollElement.scrollHeight <= Math.ceil(scrollElement.scrollTop) + scrollElement.clientHeight;
+            current.classList.toggle('is-bottom', atBottom);
+            setIsBottom(atBottom);
+        };
 
         const trigger = () => {
-            setContentHeight(current.children[0].scrollHeight);
-
-            const hasOverflow = current.children[0].scrollHeight > current.children[0].clientHeight;
+            const hasOverflow = scrollElement.scrollHeight > scrollElement.clientHeight;
 
             setIsOverflown(hasOverflow);
-
-            current?.classList.toggle('has-overflow', hasOverflow);
-
-            current.children[0].addEventListener('scroll', handleScroll);
+            current.classList.toggle('has-overflow', hasOverflow);
+            handleScroll();
         };
 
-        if (current) {
-
-            if ('ResizeObserver' in window) {
-                new ResizeObserver(trigger).observe(current);
-            }
-            trigger();
+        let observer = null;
+        if ('ResizeObserver' in window) {
+            observer = new ResizeObserver(trigger);
+            observer.observe(current);
+            observer.observe(scrollElement);
         }
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    } , [ref, contentHeight]);
+        scrollElement.addEventListener('scroll', handleScroll);
+        trigger();
+
+        return () => {
+            scrollElement.removeEventListener('scroll', handleScroll);
+            observer?.disconnect();
+        };
+    }, []);
 
     return { ref, isOverflown, isTop, isBottom };
 }
 
-export default useElementScroll
+export default useElementScroll

+ 23 - 21
src/hooks/useFreeWithdraw.js

@@ -5,8 +5,7 @@ import { checkWithdrawChannel } from "@features/actions/payAction";
 import { reqFreeWithdraw, reqFreeWithdrawClaim, reqWithdrawBaseInfo } from "@features/api";
 import { getFreeWithdraw, getFreeWithdrawMission, getIsBindPhone, isLogined } from "@features/mainSlice";
 import { getWithdrawBaseInfo } from "@features/paySlice";
-import { useCallback } from "react";
-import { useEffect, useRef } from "react";
+import { useCallback, useEffect, useRef } from "react";
 import { useDispatch, useSelector } from "react-redux";
 import { useNavigate } from "react-router";
 
@@ -27,38 +26,41 @@ const useFreeWithdraw = () => {
         if (!withdrawBase && isLogin) {
             dispatch(reqWithdrawBaseInfo());
         }
-    }, [withdrawBase, isLogin]);
+    }, [dispatch, withdrawBase, isLogin]);
 
 
-    const openWithdrawMission = () => {
+    const openWithdrawMission = useCallback(() => {
         if (withdrawMissionOpen) return;
 
         global.ignorePopup = true;
         navigate(ROUTE_PATH.HOME, { replace: true });
         setWithdrawMissionOpen(true);
-    };
+    }, [navigate, setWithdrawMissionOpen, withdrawMissionOpen]);
 
     const checkWithdrawInfo = useCallback((pixType = -1) => {
+        if (!withdrawBase) return false;
         pixType = pixType === -1 ? withdrawBase.PixType : pixType;
         //const withdrawBase = withdrawBaseRef.current;
         switch (parseInt(pixType)) {
             case 1:
                 //cash app
-                if (!withdrawBase.BankUserName || withdrawBase.BankUserName.length == 0) {
+                if (!withdrawBase.BankUserName || withdrawBase.BankUserName.length === 0) {
                     return false;
                 }
 
-                if (!withdrawBase.PixNum || withdrawBase.PixNum.length == 0) {
+                if (!withdrawBase.PixNum || withdrawBase.PixNum.length === 0) {
                     return false;
                 }
 
                 return true;
             case 2:
                 //paypal
-                if (!withdrawBase.EmailAddress || withdrawBase.EmailAddress.length == 0) {
+                if (!withdrawBase.EmailAddress || withdrawBase.EmailAddress.length === 0) {
                     return false;
                 }
                 return true;
+            default:
+                break;
         }
 
         return false;
@@ -66,7 +68,7 @@ const useFreeWithdraw = () => {
 
     const openWithdrawInGame = () => {
         // 完成充值  绑定手机信息后  打开任务页面
-        if (checkWithdrawInfo() && freeWithdraw.state > 2) {
+        if (checkWithdrawInfo() && freeWithdraw?.state > 2) {
             openWithdrawMission();
             return;
         }
@@ -81,7 +83,7 @@ const useFreeWithdraw = () => {
         const misson_list = [withdrawMission.stage1, withdrawMission.stage2, withdrawMission.stage3];
         const misson_idx = misson_list.findIndex((mission, idx) => {
             //已完成未领取的任务
-            return mission?.reward_status == 1;
+            return Number(mission?.reward_status) === 1;
         });
 
         if (misson_idx > -1) {
@@ -94,11 +96,11 @@ const useFreeWithdraw = () => {
         }
 
         return { stage, task_id };
-    }, [withdrawMission])
+    }, [openWithdrawMission, withdrawMission])
 
     const claimFreeWithdraw = useCallback(async (score) => {
         if (!isBindPhone) {
-            let result = await new Promise((resolve, reject) => {
+            let result = await new Promise((resolve) => {
                 global.bindPhoneRes = resolve;
                 global.showBindPhone();
             });
@@ -123,7 +125,7 @@ const useFreeWithdraw = () => {
         });
 
         return true;
-    }, [isBindPhone, checkMissionStatus])
+    }, [checkMissionStatus, dispatch, isBindPhone, navigate, openWithdrawMission])
 
     const checkFreeWithdrawInfo = useCallback(async (score) => {
         /// fixed: 2026-08-04 小额提现未使用Paypal
@@ -146,7 +148,7 @@ const useFreeWithdraw = () => {
 
         return true;
 
-    }, [withdrawBase, claimFreeWithdraw])
+    }, [claimFreeWithdraw, dispatch, withdrawBase])
 
     const claimFreeWithdrawDirect = useCallback(async (score) => {
         if (!isBindPhone) {
@@ -169,16 +171,16 @@ const useFreeWithdraw = () => {
             return false;
         }
 
-        await dispatch(reqFreeWithdrawClaim(missonState)).unwrap();
-        await dispatch(reqFreeWithdraw()).unwrap();
+        await dispatch(reqFreeWithdrawClaim(missonState)).unwrap().catch(() => null);
+        await dispatch(reqFreeWithdraw()).unwrap().catch(() => null);
 
 
         return true;
-    }, [isBindPhone, checkFreeWithdrawInfo, checkMissionStatus])
+    }, [checkFreeWithdrawInfo, checkMissionStatus, dispatch, isBindPhone, openWithdrawMission])
 
     const moveFreeWithdraw = async () => {
         if (!isBindPhone) {
-            let result = await new Promise((resolve, reject) => {
+            let result = await new Promise((resolve) => {
                 global.bindPhoneRes = resolve;
                 global.showBindPhone();
             });
@@ -191,8 +193,8 @@ const useFreeWithdraw = () => {
             return false;
         }
         missonState.claim_type = "game_balance";
-        await dispatch(reqFreeWithdrawClaim(missonState)).unwrap();
-        await dispatch(reqFreeWithdraw()).unwrap();
+        await dispatch(reqFreeWithdrawClaim(missonState)).unwrap().catch(() => null);
+        await dispatch(reqFreeWithdraw()).unwrap().catch(() => null);
         return true;
     };
 
@@ -204,4 +206,4 @@ const useFreeWithdraw = () => {
     };
 };
 
-export default useFreeWithdraw;
+export default useFreeWithdraw;

+ 30 - 25
src/hooks/useGameIFrame.js

@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useRef, useState } from "react";
 import { ROUTE_PATH } from "@constants/routes";
 import { exitFullScreen, isAndroid, isIOS, isLandscape, isPortrait } from "@utils/helpers";
 import { useNavigate } from "react-router-dom";
@@ -22,26 +22,29 @@ const useGameIFrame = () => {
     const dispatch = useDispatch();
 
     const orientation = useOrientation();
-    const onBack = () => {
+    const onBack = useCallback(() => {
         if (iFrameRef.extBackFunc != null) {
             iFrameRef.extBackFunc();
             return;
         }
         navigate(ROUTE_PATH.BACK);
-    }
-    const checkCloseCount = () => {
+    }, [navigate]);
+    const checkCloseCount = useCallback(() => {
         if (objInfo.closeCount >= 2) {
             onBack();
         }
-    }
-    const safeNavigate = (route, options) => {
+    }, [onBack]);
+    const safeNavigate = useCallback((route, options) => {
         if (isIOS()) {
             window.location.href = route;
         } else {
             navigate(route, options)
         }
-    }
-    const receiveMessage = (event) => {
+    }, [navigate]);
+    const receiveMessage = useCallback((event) => {
+        const gameWindow = iFrameRef.current?.contentWindow;
+        if (!gameWindow || event.source !== gameWindow) return;
+
         let data = event.data;
         if (data && data.cmd != null) {
             if (data.cmd === "back") {
@@ -66,8 +69,8 @@ const useGameIFrame = () => {
                 checkCloseCount();
             }
         }
-    }
-    const clearGameTimeout = () => {
+    }, [checkCloseCount, isMobile, onBack, ruptcyGift, safeNavigate]);
+    const clearGameTimeout = useCallback(() => {
         const gameFrame = iFrameRef.current;
         if (gameFrame != null) {
             if (gameFrame.loadtimeout != null) {
@@ -75,12 +78,12 @@ const useGameIFrame = () => {
                 gameFrame.loadtimeout = null;
             }
         }
-    }
-    const handleLoadGame = () => {
+    }, []);
+    const handleLoadGame = useCallback(() => {
         setIsGameLoad(true);
         clearGameTimeout();
-    }
-    const initGameEvent = () => {
+    }, [clearGameTimeout]);
+    const initGameEvent = useCallback(() => {
         const gameFrame = iFrameRef.current;
         if (gameFrame != null) {
             gameFrame.addEventListener('load', handleLoadGame);
@@ -90,8 +93,8 @@ const useGameIFrame = () => {
         }
         window.addEventListener("message", receiveMessage, false);
         objInfo.closeCount = 0;
-    }
-    const removeGameEvent = () => {
+    }, [handleLoadGame, receiveMessage]);
+    const removeGameEvent = useCallback(() => {
         const gameFrame = iFrameRef.current;
         if (gameFrame != null) {
             gameFrame.removeEventListener('load', handleLoadGame);
@@ -99,8 +102,8 @@ const useGameIFrame = () => {
         }
         window.removeEventListener("message", receiveMessage);
         objInfo.closeCount = 0;
-    }
-    const handleEnterGame = (data) => {
+    }, [clearGameTimeout, handleLoadGame, receiveMessage]);
+    const handleEnterGame = useCallback((data) => {
         objInfo.closeCount = 0;
         setIsGameLoad(false);
         let id = data.id;
@@ -111,14 +114,14 @@ const useGameIFrame = () => {
         let path = ROUTE_PATH.GAME + "/" + id;
         global.authLockPath = path;
         navigate(path);
-    }
+    }, [dispatch, navigate]);
     useEffect(() => {
         initGameEvent();
         return () => {
             removeGameEvent();
             exitFullScreen();
         };
-    }, [iFrameRef.current]);
+    }, [initGameEvent, removeGameEvent]);
 
     useEffect(() => {
         const landscape = isLandscape(orientation);
@@ -137,13 +140,13 @@ const useGameIFrame = () => {
     }, [orientation]);
 
     useEffect(() => {
-        if (gameID == 0) return;
+        if (Number(gameID) === 0) return;
 
         const ios = isIOS();
-        const isMobile = isAndroid() || ios;
+        const mobileDevice = isAndroid() || ios;
         const portrait = isPortrait();
-        const index = LandscapeGames.indexOf(gameID);
-        if (index === -1 || !isMobile || !portrait) {
+        const supportsLandscape = (LandscapeGames || []).some(id => Number(id) === Number(gameID));
+        if (!supportsLandscape || !mobileDevice || !portrait) {
             return;
         }
 
@@ -154,9 +157,11 @@ const useGameIFrame = () => {
         }
 
         return () => {
+            const landscapeScale = isLandscape(orientation) ? 0.5 : 1;
+            document.documentElement.style.setProperty('--view-size', landscapeScale);
             document.documentElement.style.setProperty('--landscape-ios', "0vh");
         }
-    }, [gameID])
+    }, [LandscapeGames, gameID, orientation])
     return {
         iFrameRef,
         isGameLoad,

+ 15 - 51
src/hooks/useGlobalClick.js

@@ -1,57 +1,21 @@
 import {useEffect} from "react";
 import SoundHelp from "@utils/SoundManger";
-const buttonCache={
 
-}
+const handleGlobalClick = (event) => {
+    const target = event.target instanceof Element ? event.target : event.target?.parentElement;
+    const button = target?.closest("button, a");
+    if (!button || button.getAttribute("clickAudio") === "false") return;
+    SoundHelp.playSound("button_click");
+};
+
 const useGlobalClick = () => {
-    const checkIsButton=(target)=>{
-        let tagName=target.tagName.toLowerCase();
-        let className=target.getAttribute("class");
-        if (buttonCache[className]!=null){
-            return buttonCache[className];
-        }
-        if (tagName==="button"||tagName==="a"){
-            if (className!=null){
-                buttonCache[className]=target;
-            }
-            return target;
-        }
-        let buttons=document.getElementsByTagName("button");
-        let alist=document.getElementsByTagName("a");
-        let checkList=[buttons,alist];
-        if (checkList){
-            for (let i = 0; i < checkList.length; i++) {
-                let btns=checkList[i];
-                if (btns){
-                    for (let j = 0; j < btns.length; j++) {
-                        let button=btns[j];
-                        if (button.contains(target)){
-                            if (className!=null){
-                                buttonCache[className]=button;
-                            }
-                            return button;
-                        }
-                    }
-                }
-            }
-        }
+    useEffect(() => {
+        document.addEventListener("click", handleGlobalClick, true);
+        return () => {
+            document.removeEventListener("click", handleGlobalClick, true);
+        };
+    }, []);
 
-        return null;
-    }
-    const onHandleClick=(e)=>{
-        let button=checkIsButton(e.target);
-        if (button){
-            let clickAudio=button.getAttribute("clickAudio");
-            if (clickAudio===false)return;
-            SoundHelp.playSound("button_click");
-        }
-    }
-    useEffect(()=>{
-        document.addEventListener("click",onHandleClick,true);
-        return ()=>{
-            document.removeEventListener("click",onHandleClick,true);
-        }
-    },[])
-    return {}
+    return {};
 }
-export default useGlobalClick;
+export default useGlobalClick;

+ 19 - 11
src/hooks/useMatchProgress.js

@@ -1,20 +1,28 @@
 import {useState, useEffect} from 'react';
 
+const normalizeMinute = (value) => Math.min(90, Math.max(0, Number(value) || 0));
+
 const useMatchProgress = (currentMinute = 0) => {
-    const [progress, setProgress] = useState(currentMinute);
+    const [progress, setProgress] = useState(() => normalizeMinute(currentMinute));
 
     useEffect(() => {
-        if (progress < 90) {
-            setInterval(() => {
-                setProgress((prevMinute) => prevMinute + 1);
-            }, 60000);
-        } else {
-            setProgress(90);
-        }
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, []);
+        setProgress(normalizeMinute(currentMinute));
+        if (normalizeMinute(currentMinute) >= 90) return;
+
+        const timer = setInterval(() => {
+            setProgress((prevMinute) => {
+                if (prevMinute >= 89) {
+                    clearInterval(timer);
+                    return 90;
+                }
+                return prevMinute + 1;
+            });
+        }, 60000);
+
+        return () => clearInterval(timer);
+    }, [currentMinute]);
 
     return progress;
 }
 
-export default useMatchProgress
+export default useMatchProgress

+ 64 - 40
src/hooks/usePaymentActions.js

@@ -38,9 +38,17 @@ const usePaymentActions = () => {
     const onPaymentSucceeded = (data) => {
         const { pay_name, price } = data;
         const json = localStorageGet(LOCAL_LAST_PAYMENT, "{}");
-        const last_payment = JSON.parse(json);
+        let last_payment = {};
+        try {
+            const parsed = JSON.parse(json);
+            if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
+                last_payment = parsed;
+            }
+        } catch (e) {
+            // Replace malformed cached payment preferences with the latest valid data.
+        }
 
-        const last_gears = last_payment.last_gears || [];
+        const last_gears = Array.isArray(last_payment.last_gears) ? [...last_payment.last_gears] : [];
         const idx = last_gears.indexOf(pay_name);
         if (idx > -1) last_gears.splice(idx, 1);
         last_gears.unshift(pay_name);
@@ -70,7 +78,6 @@ const usePaymentActions = () => {
         }
 
         data.payType = 28;
-        data.pay_method = data.pay_method;
         const onCompleted = data.onCompleted;
         delete data.onCompleted;
         let reqOrder = () => {
@@ -206,20 +213,23 @@ const usePaymentActions = () => {
             if (!isNaN(firstPayGift.time_end)) {
                 info.time_left = Math.floor((firstPayGift.time_end - Date.now()) / 1000);
             }
-            info.list = firstPayGift.list;
+            info.list = Array.isArray(firstPayGift.list) ? firstPayGift.list : [];
             info.has_purchased = firstPayGift.has_purchased;
 
-            if (info.list && info.list.length > 0) {
-                info.max_goods_prices = info.list.reduce((prev, current) => {
-                    const prev_prices = parseFloat(prev.favorable_price);
-                    const current_prices = parseFloat(current.favorable_price);
-                    return prev_prices > current_prices ? prev_prices : current_prices;
-                });
+            if (info.list.length > 0) {
+                info.max_goods_prices = info.list.reduce((maxPrice, current) => {
+                    const currentPrice = Number(current?.favorable_price);
+                    return Number.isFinite(currentPrice) ? Math.max(maxPrice, currentPrice) : maxPrice;
+                }, 0);
             }
 
             if (firstPayGift.gift_info != null) {
-                info.bonus_instantly = firstPayGift.gift_info.bonus_instantly;
-                info.total_bonus = firstPayGift.gift_info.total_bonus;
+                Object.assign(info, firstPayGift.gift_info);
+
+                const instantBonus = Number(firstPayGift.gift_info.bonus_instantly);
+                const totalBonus = Number(firstPayGift.gift_info.total_bonus);
+                info.bonus_instantly = Number.isFinite(instantBonus) ? instantBonus : 0;
+                info.total_bonus = Number.isFinite(totalBonus) ? totalBonus : 0;
                 info.gift_id = firstPayGift.gift_info?.gift_id;
                 info.gift_name = firstPayGift.gift_info?.gift_name;
 
@@ -227,7 +237,7 @@ const usePaymentActions = () => {
                 const betting_task = firstPayGift.gift_info.betting_task;
                 const day_rewards = firstPayGift.gift_info.day_rewards;
 
-                if (!!betting_bonus && betting_bonus.status === 1) {
+                if (betting_bonus && Number(betting_bonus.status) === 1) {
                     info.has_bonus = true;
                 }
 
@@ -235,57 +245,66 @@ const usePaymentActions = () => {
                 //     info.has_bonus = true;
                 // }
 
-                if (!!betting_bonus && betting_task.can_claim_count > 0) {
+                if (Number(betting_task?.can_claim_count) > 0) {
                     info.has_bonus = true;
                 }
 
-                if (!!day_rewards && day_rewards.status === 1) {
+                if (day_rewards && Number(day_rewards.status) === 1) {
                     info.has_bonus = true;
                 }
 
-                info.day_rewards_total = day_rewards.total_bonus;
-                info.vault_percent = info.total_bonus - info.bonus_instantly - day_rewards.total_bonus;
+                const dayRewardsTotal = Number(day_rewards?.total_bonus);
+                info.day_rewards_total = Number.isFinite(dayRewardsTotal) ? dayRewardsTotal : 0;
+                info.vault_percent = info.total_bonus - info.bonus_instantly - info.day_rewards_total;
                 info.day_claimed_bonus = 0;
 
                 if (info.has_purchased) {
                     ///充值后 计算已领取和未领取的奖金
-                    let received_bonus = firstPayGift.gift_info.bonus_instantly;
-                    if (!!betting_bonus) {
-                        received_bonus += betting_bonus.total_bonus - betting_bonus.remaining_bonus;
+                    let received_bonus = info.bonus_instantly;
+                    if (betting_bonus) {
+                        const bettingTotal = Number(betting_bonus.total_bonus);
+                        const bettingRemaining = Number(betting_bonus.remaining_bonus);
+                        received_bonus += (Number.isFinite(bettingTotal) ? bettingTotal : 0)
+                            - (Number.isFinite(bettingRemaining) ? bettingRemaining : 0);
                     }
 
-                    if (!!betting_task && betting_task.status === 2) {
+                    if (betting_task && Number(betting_task.status) === 2) {
                         /// 已领取
-                        received_bonus += betting_task.total_bonus;
+                        const taskBonus = Number(betting_task.total_bonus);
+                        received_bonus += Number.isFinite(taskBonus) ? taskBonus : 0;
                     }
 
-                    if (!!betting_task) {
+                    if (betting_task) {
                         info.vault_current_index = -1;
                         info.vault_max_bet_times = 0;
                         info.vault_max_bet_amount = 0;
                         info.vault_claimed_bonus = 0;
-                        info.vault_bonus = betting_task.total_bonus;
-                        const betting_task_list = betting_task.tasks || [];
+                        const vaultBonus = Number(betting_task.total_bonus);
+                        info.vault_bonus = Number.isFinite(vaultBonus) ? vaultBonus : 0;
+                        const betting_task_list = Array.isArray(betting_task.tasks) ? betting_task.tasks : [];
                         info.vault_percent = betting_task_list.reduce((sum, v, idx) => {
-                            if (v.status == 1) {
+                            if (Number(v.status) === 1) {
                                 info.vault_can_claim = true;
                             }
-                            if (v.status == 0 && info.vault_current_index == -1) {
+                            if (Number(v.status) === 0 && info.vault_current_index === -1) {
                                 info.vault_current_index = idx;
                             }
                             if (v.status >= 2) {
                                 info.vault_claimed_bonus += v.total_bonus;
                             }
 
-                            if (v.bet_pay_times > info.vault_max_bet_times) {
-                                info.vault_max_bet_times = v.bet_pay_times;
+                            const betPayTimes = Number(v.bet_pay_times);
+                            if (Number.isFinite(betPayTimes) && betPayTimes > info.vault_max_bet_times) {
+                                info.vault_max_bet_times = betPayTimes;
                             }
 
-                            if (v.required_bet > info.vault_max_bet_amount) {
-                                info.vault_max_bet_amount = v.required_bet;
+                            const requiredBet = Number(v.required_bet);
+                            if (Number.isFinite(requiredBet) && requiredBet > info.vault_max_bet_amount) {
+                                info.vault_max_bet_amount = requiredBet;
                             }
 
-                            return sum + v.bonus_percent;
+                            const bonusPercent = Number(v.bonus_percent);
+                            return sum + (Number.isFinite(bonusPercent) ? bonusPercent : 0);
                         }, 0);
 
                         info.vault_max_bet_amount = formatNum(info.vault_max_bet_amount);
@@ -298,18 +317,23 @@ const usePaymentActions = () => {
                         }
                     }
 
-                    if (!!day_rewards) {
+                    if (day_rewards) {
                         let claimed_per = 0;
                         let claimed_days = parseFloat(day_rewards.claimed_days);
                         if (Number.isNaN(claimed_days)) claimed_days = 0;
-                        const total = day_rewards.bonus.reduce((sum, v, idx) => {
-                            if (idx < claimed_days) claimed_per += v;
-                            sum += v;
+                        const dailyBonuses = Array.isArray(day_rewards.bonus) ? day_rewards.bonus : [];
+                        const total = dailyBonuses.reduce((sum, value, idx) => {
+                            const bonus = Number(value);
+                            const safeBonus = Number.isFinite(bonus) ? bonus : 0;
+                            if (idx < claimed_days) claimed_per += safeBonus;
+                            sum += safeBonus;
                             return sum;
                         }, 0);
 
                         info.day_rewards_total = total;
-                        info.day_claimed_bonus = (claimed_per / total) * day_rewards.total_bonus;
+                        info.day_claimed_bonus = total > 0
+                            ? (claimed_per / total) * (Number(day_rewards.total_bonus) || 0)
+                            : 0;
                         info.day_claimed_bonus = formatNum(info.day_claimed_bonus);
                         received_bonus += info.day_claimed_bonus;
                     }
@@ -321,8 +345,6 @@ const usePaymentActions = () => {
                     info.unlocked_bonus = Math.round(info.unlocked_bonus * 100) / 100;
                     if (info.unlocked_bonus < 0) info.unlocked_bonus = 0;
                 }
-
-                Object.assign(info, firstPayGift.gift_info)
             }
         }
         return info;
@@ -391,7 +413,7 @@ const usePaymentActions = () => {
 
     const getCouponPercent = (coupon, amount) => {
         if (InvalidCoupon(coupon)) return 0;
-        if (coupon.type_id != CouponTypeID.Percent) return 0;
+        if (Number(coupon.type_id) !== Number(CouponTypeID.Percent)) return 0;
 
         if (typeof amount !== 'number') amount = parseFloat(amount);
         if (Number.isNaN(amount)) return 0;
@@ -411,6 +433,8 @@ const usePaymentActions = () => {
                 percent = coupon.value;
                 bonus = formatNum(percent / 100 * amount);
                 break;
+            default:
+                break;
         }
 
         const maxBonus = coupon.max_bonus;

+ 84 - 85
src/hooks/usePaymentMethods.js

@@ -1,7 +1,7 @@
 import { LOCAL_LAST_PAYMENT } from "@constants/localkey";
 import { CryptoGroupId, CryptoGroupName, PaymentGears, PayTypeGroup, RecommendGears } from "@db/payment";
 import { localStorageGet } from "@utils/helpers";
-import { useMemo, useState } from "react";
+import { useEffect, useMemo, useState } from "react";
 
 
 /**
@@ -14,102 +14,98 @@ const getGearGroupName = (name, group_id) => {
     return name;
 }
 
-const usePaymentMethods = (list, recommendIdx = -1) => {
-    const [selection, setSelection] = useState({
-        gearIdx: 0,
-        subIdx: -1,
-    })
-
-    const getAllGearsMaps = (mainGears, gearGroup) => {
-        let maps = {};
-        for (let gearIdx = 0; gearIdx < mainGears.length; gearIdx++) {
-            const gear = mainGears[gearIdx];
-            const subs = gearGroup[gear] || [];
-            if (subs.length == 0) {
-                maps[gear] = {
-                    gearIdx,
-                    subIdx: -1,
-                }
-                continue;
-            }
-
-            for (let subIdx = 0; subIdx < subs.length; subIdx++) {
-                const sub_gear = subs[subIdx];
-                maps[sub_gear] = {
-                    gearIdx,
-                    subIdx,
-                }
-            }
+const getAllGearsMaps = (mainGears, gearGroup) => {
+    const maps = {};
+    for (let gearIdx = 0; gearIdx < mainGears.length; gearIdx++) {
+        const gear = mainGears[gearIdx];
+        const subs = gearGroup[gear] || [];
+        if (subs.length === 0) {
+            maps[gear] = { gearIdx, subIdx: -1 };
+            continue;
         }
-        return maps;
-    }
 
-    const setSelectionByMap = (gearMap, gears) => {
-        for (const gear of gears) {
-            if (gearMap[gear]) {
-                const item = gearMap[gear];
-                setSelection({
-                    gearIdx: item.gearIdx,
-                    subIdx: item.subIdx,
-                })
-                return true;
-            }
+        for (let subIdx = 0; subIdx < subs.length; subIdx++) {
+            maps[subs[subIdx]] = { gearIdx, subIdx };
         }
+    }
+    return maps;
+};
 
-        return false;
+const findSelectionByMap = (gearMap, gears) => {
+    for (const gear of gears) {
+        if (gearMap[gear]) return { ...gearMap[gear] };
+    }
+    return null;
+};
+
+const parseGears = (value) => {
+    if (Array.isArray(value)) return value;
+    try {
+        const parsed = JSON.parse(value || "[]");
+        return Array.isArray(parsed) ? parsed : [];
+    } catch {
+        return [];
     }
+};
 
-    const setDefaultSlection = (protects, allGearsMap) => {
+const getDefaultSelection = (protects, allGearsMap, recommendIdx) => {
         // 上次支付渠道 > 上次支付金额 > 推荐档位 > 第一个支付档位
         // 推荐档位: Crypto→USDC,Cash App→Cash Payment
         let json = localStorageGet(LOCAL_LAST_PAYMENT, "");
         if (!!json) {
-            // 上次支付渠道
-            const last_payment = JSON.parse(json);
-            const last_gears = last_payment.last_gears || [];
-            if (setSelectionByMap(allGearsMap, last_gears)) {
-                return;
-            }
-
-            //上次金额
-            const last_price = last_payment.price || 0;
-            if (protects.length > 1 && last_price > 0) {
-                let idx = protects.findIndex(goods => goods.money == last_price);
-                if (idx > -1) {
-                    const gears = protects[idx].gears || [];
-                    if (setSelectionByMap(allGearsMap, gears)) {
-                        return;
+            try {
+                // 上次支付渠道
+                const last_payment = JSON.parse(json);
+                const last_gears = last_payment.last_gears || [];
+                const previousGearSelection = findSelectionByMap(allGearsMap, last_gears);
+                if (previousGearSelection) return previousGearSelection;
+
+                //上次金额
+                const last_price = last_payment.price || 0;
+                if (protects.length > 1 && last_price > 0) {
+                    const idx = protects.findIndex(goods => Number(goods.money) === Number(last_price));
+                    if (idx > -1) {
+                        const gears = protects[idx].gears || [];
+                        const previousPriceSelection = findSelectionByMap(allGearsMap, gears);
+                        if (previousPriceSelection) return previousPriceSelection;
                     }
                 }
+            } catch {
+                // 忽略损坏的本地缓存,继续使用服务端推荐顺序。
             }
         }
 
         /// 推荐档位
-        if (setSelectionByMap(allGearsMap, RecommendGears)) {
-            return;
-        }
+        const recommendedSelection = findSelectionByMap(allGearsMap, RecommendGears);
+        if (recommendedSelection) return recommendedSelection;
 
         /// 推荐商品
         if (recommendIdx > -1 && protects[recommendIdx]) {
             const gears = protects[recommendIdx].gears || [];
-            if (setSelectionByMap(allGearsMap, gears)) {
-                return;
-            }
+            return findSelectionByMap(allGearsMap, gears);
         }
-    }
 
-    const { protects, mainGears, gearGroup, allGearsMap, cryptoGoods } = useMemo(() => {
+        return null;
+};
+
+const usePaymentMethods = (list, recommendIdx = -1) => {
+    const [selection, setSelection] = useState({
+        gearIdx: 0,
+        subIdx: -1,
+    })
+
+    const { protects, mainGears, gearGroup, cryptoGoods, defaultSelection } = useMemo(() => {
         /// allGears:  所有的1级支付渠道name集合
         /// gearGroup: 支付渠道分组情况
 
-        const goods_list = !Array.isArray(list) ? [list] : list;
+        const goods_list = Array.isArray(list) ? list : list ? [list] : [];
         const gearGroup = {}; // 格式: { gear_name: []}  value数组长度为0 则不会显示分组
         const cryptoGoods = {} // {gear_name: []}
         const protects = goods_list.map(goods => {
             let gearsMap = {};
-            const gears = JSON.parse(goods.gear || '[]');
+            const gears = parseGears(goods.gear);
             gears.forEach(g => {
-                if (g.status == 1) {
+                if (Number(g.status) === 1) {
                     // { 支付方式: [gear,gear]}
                     let gear_name = g.name;
                     gearsMap[gear_name] = true;
@@ -146,7 +142,7 @@ const usePaymentMethods = (list, recommendIdx = -1) => {
         mainGears.sort((a, b) => {
             let len1 = gearGroup[a].length;
             let len2 = gearGroup[b].length;
-            if (len1 == len2) return 0;
+            if (len1 === len2) return 0;
 
             return len1 > len2 ? 1 : -1
         })
@@ -161,22 +157,29 @@ const usePaymentMethods = (list, recommendIdx = -1) => {
 
 
         const allGearsMap = getAllGearsMaps(mainGears, gearGroup);
-        setDefaultSlection(protects, allGearsMap);
-
         return {
             protects,
             mainGears,
             gearGroup,
             allGearsMap,
             cryptoGoods,
+            defaultSelection: getDefaultSelection(protects, allGearsMap, recommendIdx),
         }
-    }, [list])
+    }, [list, recommendIdx])
+
+    useEffect(() => {
+        if (!defaultSelection) return;
+        setSelection((current) => current.gearIdx === defaultSelection.gearIdx
+            && current.subIdx === defaultSelection.subIdx
+            ? current
+            : defaultSelection);
+    }, [defaultSelection]);
 
     const onChooseMethod = (index, subIdx = -1) => {
-        if (index == -1) index = selection.gearIdx;
+        if (index === -1) index = selection.gearIdx;
         const gear_name = mainGears[index];
         let gears = gearGroup[gear_name] || [];
-        if (subIdx == -1 && gears.length > 0) {
+        if (subIdx === -1 && gears.length > 0) {
             subIdx = gears.findIndex(item => RecommendGears.includes(item));
             if (subIdx === -1) subIdx = 0;
         }
@@ -188,8 +191,8 @@ const usePaymentMethods = (list, recommendIdx = -1) => {
     }
 
     const getGearName = (gearIdx = -1, subIdx = -1) => {
-        if (gearIdx == -1) gearIdx = selection.gearIdx;
-        if (subIdx == -1) subIdx = selection.subIdx;
+        if (gearIdx === -1) gearIdx = selection.gearIdx;
+        if (subIdx === -1) subIdx = selection.subIdx;
 
         let gear_name = mainGears[gearIdx];
         let gears = gearGroup[gear_name] || [];
@@ -219,7 +222,7 @@ const usePaymentMethods = (list, recommendIdx = -1) => {
     const getChoiseGear = (goodsIdx = 0) => {
         const gear_name = getGearName();
         const gears_ary = protects[goodsIdx].gears || [];
-        return gears_ary.find(g => g.name == gear_name);
+        return gears_ary.find(g => g.name === gear_name);
     }
 
     /**
@@ -245,11 +248,6 @@ const usePaymentMethods = (list, recommendIdx = -1) => {
         }
     }
 
-    const getCurrentGearName = () => {
-        const gear_name = getGearName();
-        return gear_name;
-    }
-
     const getDepositBtnDesc = (amount) => {
         return global.i18n.t("deposit.recharge.button_gear", {
             method: getGearShortName(),
@@ -266,10 +264,11 @@ const usePaymentMethods = (list, recommendIdx = -1) => {
     const validChoice = () => {
         const gearIdx = selection.gearIdx;
         const subIdx = selection.subIdx;
-        if (gearIdx == -1) return false;
+        if (gearIdx === -1) return false;
 
-        let gears = gearGroup[gearIdx] || [];
-        if (gears.length > 0 && subIdx == -1) return false;
+        const gearName = mainGears[gearIdx];
+        const gears = gearGroup[gearName] || [];
+        if (gears.length > 0 && subIdx === -1) return false;
         return true;
     }
 
@@ -318,4 +317,4 @@ const usePaymentMethods = (list, recommendIdx = -1) => {
     }
 }
 
-export default usePaymentMethods;
+export default usePaymentMethods;

+ 16 - 19
src/hooks/usePreviousRoute.js

@@ -1,29 +1,26 @@
 import {useLocation} from 'react-router-dom';
-import {useEffect, useState} from 'react';
+import {useEffect, useRef, useState} from 'react';
 import {ROUTE_PATH} from "@constants/routes";
 const usePreviousRoute = () => {
     const location = useLocation();
     const [preLocation, setPreLocation] = useState({});
-    const [currentLocation, setCurrentLocation] = useState({});
     const [gameID, setGameID] = useState(0);
-    const parseGameID = ()=>{
-        let pathname=location.pathname;
-        let gid=0;
-        if (pathname.startsWith(ROUTE_PATH.GAME)){
-            let path="";
-            let temp=pathname.split("?");
-            path=temp.shift();
-            temp=path.split("/");
-            path=temp.pop();
-            gid=Number(path);
-        }
-        setGameID(gid);
-    }
+    const currentLocationRef = useRef({});
+    const latestLocationRef = useRef(location);
+    latestLocationRef.current = location;
+
     useEffect(() => {
-            setPreLocation({...currentLocation})
-            setCurrentLocation({...location});
-            parseGameID();
-        }, [location.pathname]);
+        setPreLocation({...currentLocationRef.current});
+        currentLocationRef.current = {...latestLocationRef.current};
+
+        const pathname = location.pathname;
+        const path = pathname.startsWith(ROUTE_PATH.GAME)
+            ? pathname.split("?")[0].split("/").pop()
+            : "0";
+        const nextGameID = Number(path);
+        setGameID(Number.isFinite(nextGameID) ? nextGameID : 0);
+    }, [location.pathname]);
+
     return {
         preLocation,
         gameID

+ 4 - 3
src/hooks/useRecallGift.js

@@ -85,14 +85,15 @@ const useRecallGift = () => {
         if (giftStateRef.current) {
             // 充值完  状态变成了Active  显示弹窗
             const last_status = giftStateRef.current.status;
-            if (last_status == GiftStatus.NotRecharg && giftState.status == GiftStatus.Active) {
+            if (Number(last_status) === Number(GiftStatus.NotRecharg)
+                && Number(giftState.status) === Number(GiftStatus.Active)) {
                 dispatch(reqGetRecallGiftInfo());
                 global.showRecallGiftInfo();
                 localStorageSet(LOCAL_RECALLGIFT_INFO_POPUP_TS, Date.now());
             }
         }
         giftStateRef.current = giftState;
-    }, [giftState])
+    }, [dispatch, giftState])
 
     useEffect(() => {
         let redPoint = false;
@@ -118,4 +119,4 @@ const useRecallGift = () => {
     }
 }
 
-export default useRecallGift;
+export default useRecallGift;

+ 1 - 0
src/hooks/useScrollLock.js

@@ -9,6 +9,7 @@ const useScrollLock = () => {
         } else {
             document.documentElement.classList.remove('no-scroll');
         }
+        return () => document.documentElement.classList.remove('no-scroll');
     }, [isLocked]);
 
     return setIsLocked

+ 28 - 29
src/hooks/useSuperBall.js

@@ -5,7 +5,6 @@ import { getSuperBallInfo, getSuperBallYesterdayInfo } from "@features/superBall
 import { checkTimestampIsNextDay, localStorageSet } from "@utils/helpers";
 import { useEffect, useRef, useState, useSyncExternalStore } from "react";
 import { useDispatch, useSelector } from "react-redux";
-import { useNavigate } from "react-router";
 
 export const SuperBallStep = {
     /**
@@ -103,26 +102,24 @@ const useSuperBall = () => {
 
     useEffect(() => {
         if (!yesterdayInfo) return;
-        let data = { ...yesterday };
-        data.balls = yesterdayInfo.my_balls_with_lucky.map(v => v.number);
-        data.participants = yesterdayInfo.completed_count;
-        data.total_balls = yesterdayInfo.total_balls;
-        data.lucky_number = yesterdayInfo.lucky_number;
-        data.base_prize = yesterdayInfo.base_reward_per_ball_display;
-        data.my_prize = yesterdayInfo.my_prize_display;
-        data.can_claim = yesterdayInfo.can_claim_yesterday;
-        data.pool_amount = yesterdayInfo.pool_amount_display;
-        data.pending_prize = yesterdayInfo.pending_prize_display;
-        data.my_multiplier = yesterdayInfo.my_multiplier;
-        data.lucky_prize = superBallInfo?.lucky_reward_per_ball;
-        data.lucky_count = 0;
-        if (yesterdayInfo.my_balls_with_lucky) {
-            var count = yesterdayInfo.my_balls_with_lucky.filter(v => v.is_lucky).length;
-            data.lucky_count = count;
-        }
-
-        setYesterday(data);
-    }, [yesterdayInfo]);
+        const ballsWithLucky = Array.isArray(yesterdayInfo.my_balls_with_lucky)
+            ? yesterdayInfo.my_balls_with_lucky
+            : [];
+        setYesterday({
+            balls: ballsWithLucky.map(v => v.number),
+            participants: yesterdayInfo.completed_count,
+            total_balls: yesterdayInfo.total_balls,
+            lucky_number: yesterdayInfo.lucky_number,
+            base_prize: yesterdayInfo.base_reward_per_ball_display,
+            my_prize: yesterdayInfo.my_prize_display,
+            can_claim: yesterdayInfo.can_claim_yesterday,
+            pool_amount: yesterdayInfo.pool_amount_display,
+            pending_prize: yesterdayInfo.pending_prize_display,
+            my_multiplier: yesterdayInfo.my_multiplier,
+            lucky_prize: superBallInfo?.lucky_reward_per_ball,
+            lucky_count: ballsWithLucky.filter(v => v.is_lucky).length,
+        });
+    }, [yesterdayInfo, superBallInfo?.lucky_reward_per_ball]);
 
     useEffect(() => {
         infoRef.current = superBallInfo;
@@ -145,10 +142,10 @@ const useSuperBall = () => {
         if (!info || !info.user_task) return false;
 
         const task_completed = info.user_task.task_completed;
-        if (typeof (task_completed) == 'boolean') {
+        if (typeof task_completed === 'boolean') {
             return task_completed;
         }
-        return parseInt(info.user_task.task_completed) == 1;
+        return parseInt(info.user_task.task_completed) === 1;
     }
 
     const getSortTiers = () => {
@@ -163,7 +160,7 @@ const useSuperBall = () => {
         if (!info || !info.user_task) return -1;
 
         const tiers = getSortTiers();
-        let index = tiers.findIndex(v => v.tier == info.user_task.tier);
+        let index = tiers.findIndex(v => String(v.tier) === String(info.user_task.tier));
         return index === tiers.length - 1;
     }
 
@@ -177,7 +174,7 @@ const useSuperBall = () => {
         }
 
         const tiers = getSortTiers();
-        let index = tiers.findIndex(v => v.tier == info.user_task.tier);
+        let index = tiers.findIndex(v => String(v.tier) === String(info.user_task.tier));
         if (index < 1) return 0;
         return tiers[index - 1].ball_count;
     }
@@ -242,7 +239,7 @@ const useSuperBall = () => {
         }
 
         let ballCount = getBallCount();
-        if (info.user_task && info.user_task.status == 1) {
+        if (info.user_task && Number(info.user_task.status) === 1) {
             /// 已领取任务奖励  判断是否选号
             if (ballCount > info.today.my_balls.length) {
                 return SuperBallStep.Picker;
@@ -256,7 +253,7 @@ const useSuperBall = () => {
 
     const claimSuperball = async () => {
         let user_task = infoRef.current?.user_task
-        if (user_task && user_task.status == 1) {
+        if (user_task && Number(user_task.status) === 1) {
             global.showSuperBallPicker(getTierBallCount(),
                 global.showSuperBallMineBalls);
             return true;
@@ -331,6 +328,8 @@ const useSuperBall = () => {
             case SuperBallStep.WaitForEnd:
                 global.showSuperBallMineBalls();
                 break;
+            default:
+                break;
         }
         return true;
     }
@@ -340,7 +339,7 @@ const useSuperBall = () => {
         //// 只有vip用户才会触发这个弹窗
         if (!isVip()) return false;
         let step = getSuperBallStep();
-        if (step != SuperBallStep.Yesterday) return false;
+        if (step !== SuperBallStep.Yesterday) return false;
         if (launch) {
             if (!checkTimestampIsNextDay(LOCAL_SUPERBALL_POPUP_TS)) {
                 return false;
@@ -391,4 +390,4 @@ const useSuperBall = () => {
 
 }
 
-export default useSuperBall;
+export default useSuperBall;

+ 11 - 9
src/hooks/useVipConfig.js

@@ -1,5 +1,5 @@
 import { getUserInfo, getUserVip, getVipConfig, getJumpToH5 } from "@features/mainSlice"
-import { formatNum, safeSubtract, getEnterPlatform } from "@utils/helpers";
+import { formatNum, getEnterPlatform } from "@utils/helpers";
 import { useCallback } from "react";
 import { useSelector } from "react-redux"
 
@@ -13,7 +13,7 @@ const useVipConfig = () => {
 
     /** 获取最近的充值奖金等级 */
     const getMinDepositBonusLv = useCallback(() => {
-        const item = vipConfig.find(v => parseFloat(v.RechargeExtraSendRate) > 0);
+        const item = vipConfig?.find(v => parseFloat(v.RechargeExtraSendRate) > 0);
         return item?.VIP;
     }, [vipConfig]);
 
@@ -31,8 +31,10 @@ const useVipConfig = () => {
 
     /** 获取当前等级提现信息汇总信息*/
     const getWithdrawTaxStat = useCallback((amount) => {
-        const current_fee = showPwaFee ? Number(vipConfig[vip].PWAWithdrawFeeRate) : Number(vipConfig[vip].WithdrawFeeRate);
-        const start_fee = showPwaFee ? Number(vipConfig[0].PWAWithdrawFeeRate) : Number(vipConfig[0].WithdrawFeeRate);
+        const currentConfig = vipConfig?.[vip] || {};
+        const initialConfig = vipConfig?.[0] || {};
+        const current_fee = showPwaFee ? Number(currentConfig.PWAWithdrawFeeRate) : Number(currentConfig.WithdrawFeeRate);
+        const start_fee = showPwaFee ? Number(initialConfig.PWAWithdrawFeeRate) : Number(initialConfig.WithdrawFeeRate);
         const start_fee_num = formatNum(start_fee / 100 * amount);
         const diff_fee = formatNum(current_fee - start_fee);
         const diff_fee_num = Math.abs(formatNum(diff_fee / 100 * amount));
@@ -40,9 +42,9 @@ const useVipConfig = () => {
         let unlock_fee = 0;
         let unlock_fee_num = 0;
         let unlock_lv = -1;
-        if (Number(vipConfig[0].WithdrawFeeRate) === Number(vipConfig[vip].WithdrawFeeRate)) {
+        if (Number(initialConfig.WithdrawFeeRate) === Number(currentConfig.WithdrawFeeRate)) {
             /// 提现手续费未变化
-            const item = vipConfig.find(v => Number(v.WithdrawFeeRate) !== Number(vipConfig[0].WithdrawFeeRate));
+            const item = vipConfig?.find(v => Number(v.WithdrawFeeRate) !== Number(initialConfig.WithdrawFeeRate));
             if (item) {
                 unlock_lv = Number(item.VIP);
 
@@ -63,7 +65,7 @@ const useVipConfig = () => {
             unlock_fee_num
         }
 
-    }, [vipConfig, vip]);
+    }, [showPwaFee, vipConfig, vip]);
 
     /**
      * PWA(官网/桌面图标,platform=3)提现基础费率:vipConfig 下发的 PWAWithdrawFeeRate,
@@ -72,7 +74,7 @@ const useVipConfig = () => {
     const getPWAWithdrawFeeRate = useCallback(() => {
         const v = Number(vipConfig?.[vip]?.PWAWithdrawFeeRate);
         return Number.isFinite(v) && v > 0 ? v : null;
-    }, [vipConfig]);
+    }, [vipConfig, vip]);
 
     return {
         vipLv: vip,
@@ -84,4 +86,4 @@ const useVipConfig = () => {
     }
 }
 
-export default useVipConfig
+export default useVipConfig

+ 9 - 4
src/hooks/useVisibilityChange.js

@@ -2,25 +2,30 @@ import { useEffect, useRef } from "react";
 
 const useVisibilityChange = (onChange) => {
     const leftTimeRef = useRef(0);
+    const onChangeRef = useRef(onChange);
+
+    useEffect(() => {
+        onChangeRef.current = onChange;
+    }, [onChange]);
 
     useEffect(() => {
         const handleVisibility = () => {
             if (document.visibilityState === "hidden") {
                 leftTimeRef.current = Date.now();
-                onChange && onChange(0);
+                onChangeRef.current?.(0);
             } else if (document.visibilityState === "visible") {
                 const diff = Date.now() - leftTimeRef.current;
-                onChange && onChange(1, diff);
+                onChangeRef.current?.(1, diff);
             }
         };
 
         document.addEventListener("visibilitychange", handleVisibility)
 
         return () => {
-            onChange && onChange(-1);
+            onChangeRef.current?.(-1);
             document.removeEventListener("visibilitychange", handleVisibility);
         };
     }, []);
 }
 
-export default useVisibilityChange;
+export default useVisibilityChange;

+ 22 - 21
src/hooks/useWithdrawInfo.js

@@ -3,7 +3,7 @@ import { WithdrawType } from "@db/payment";
 import { reqWithdrawPayout, reqWithdrawSetInfo } from "@features/api";
 import { getIsBindPhone, getJumpLater } from "@features/mainSlice";
 import { getWDSetInfoStatus, getWithdrawBaseInfo } from "@features/paySlice";
-import { checkEmailFormat, localStorageGet, localStorageRemove, localStorageSet } from "@utils/helpers";
+import { checkEmailFormat, localStorageGet, localStorageSet } from "@utils/helpers";
 import { isInFBBrowser } from "@utils/UAUtils";
 import { useCallback } from "react";
 import { useDispatch, useSelector } from "react-redux";
@@ -15,6 +15,13 @@ const useWithdrawInfo = () => {
 
     const isBind = useSelector(getIsBindPhone);
     const jumpLater = useSelector(getJumpLater);
+    const paypalEmail = baseInfo?.EmailAddress || '';
+    const cashAppName = baseInfo?.BankUserName || '';
+    const cashAppTag = baseInfo?.PixNum || '';
+    const paypalNeedCode = baseInfo?.withdraw_info_precheck?.paypal?.require_code;
+    const cashAppNeedCode = baseInfo?.withdraw_info_precheck?.cashapp?.require_code;
+    const paypalEditCd = baseInfo?.paypal_update_cd;
+    const cashAppEditCd = baseInfo?.cashapp_update_cd;
 
     const updatePaypalInfo = async (email, code) => {
         return await saveWithdrawInfo({
@@ -53,10 +60,12 @@ const useWithdrawInfo = () => {
                 //paypal
                 if (!params.email) params.email = baseInfo?.EmailAddress;
                 break;
+            default:
+                break;
         }
 
 
-        const bind_phone = baseInfo.bind_phone || "";
+        const bind_phone = baseInfo?.bind_phone || "";
         if (!onlySwitch && checkSwitchCode(params.PixType)) {
             if (bind_phone && bind_phone.length > 0) {
                 let code = await global.showMobileCodeAsync(bind_phone);
@@ -80,15 +89,15 @@ const useWithdrawInfo = () => {
             }
         }
 
-        if (res && res.code == 200) {
+        if (res && Number(res.code) === 200) {
             // localStorageRemove(LOCAL_WITHDRAW_SECURITY_TIPS);
             return true;
         }
 
         return false;
-    }, [baseInfo])
+    }, [baseInfo, checkSwitchCode, dispatch])
 
-    const checkInsBrowser = (cbType) => {
+    const checkInsBrowser = useCallback((cbType) => {
         //if (jumpLater === 1 && isInFBBrowser()) {
         if (jumpLater === 2 && isInFBBrowser()) {
             // 把描述符传给 InsBrowserGuide,仅在用户点击 Verify Now 时才写入 localStorage
@@ -96,11 +105,11 @@ const useWithdrawInfo = () => {
             return false;
         }
         return true;
-    };
+    }, [jumpLater]);
 
-    const checkSecurityTips = async (pixType, amount) => {
+    const checkSecurityTips = useCallback(async (pixType, amount) => {
         return new Promise((resolve) => {
-            const tips = baseInfo.withdraw_info_update_by_phone;
+            const tips = baseInfo?.withdraw_info_update_by_phone;
             const local = localStorageGet(LOCAL_WITHDRAW_SECURITY_TIPS, "");
             if (tips && !local) {
                 localStorageSet(LOCAL_WITHDRAW_SECURITY_TIPS, "1");
@@ -111,7 +120,7 @@ const useWithdrawInfo = () => {
             }
             resolve(true);
         })
-    }
+    }, [baseInfo]);
 
     const startWithdrawing = useCallback(async (amount, cbType) => {
         if (!checkInsBrowser(cbType)) {
@@ -126,7 +135,7 @@ const useWithdrawInfo = () => {
             global.showBindPhone((success) => {
                 if (success) {
                     //手机号校验通过后再做浏览器环境校验
-                    if (!checkInsBrowser()) return;
+                    if (!checkInsBrowser(cbType)) return;
                 }
             });
             return;
@@ -163,23 +172,15 @@ const useWithdrawInfo = () => {
         dispatch(reqWithdrawPayout({
             score: amount
         }))
-    }, [baseInfo, isBind, jumpLater])
+    }, [baseInfo, cashAppTag, checkInsBrowser, checkSecurityTips, dispatch, isBind, paypalEmail, saveWithdrawInfo])
 
     /** 获取CD剩余秒速  cbType: cashapp 1  paypal 2 */
     const getCooldownSec = useCallback((cbType) => {
-        let ts = cbType === 1 ? paypalEditCd : cashAppEditCd;
+        const ts = cbType === WithdrawType.CashApp ? cashAppEditCd : paypalEditCd;
         if (ts === 0) return 0;
         return Math.floor((ts - Date.now()) / 1000);
 
-    }, [baseInfo])
-
-    const paypalEmail = baseInfo?.EmailAddress || '';
-    const cashAppName = baseInfo?.BankUserName || '';
-    const cashAppTag = baseInfo?.PixNum || '';
-    const paypalNeedCode = baseInfo?.withdraw_info_precheck?.paypal?.require_code;
-    const cashAppNeedCode = baseInfo?.withdraw_info_precheck?.cashapp?.require_code;
-    const paypalEditCd = baseInfo?.paypal_update_cd;
-    const cashAppEditCd = baseInfo?.cashapp_update_cd;
+    }, [cashAppEditCd, paypalEditCd])
 
     return {
         setStatus,

+ 6 - 2
src/hooks/useWithdrawPending.js

@@ -10,6 +10,7 @@ const useWithdrawPending = (onConfirm) => {
 
     const replayedRef = useRef(false);
     useEffect(() => {
+        let replayTimer = null;
         if (replayedRef.current) return;
         if (!baseInfo) return; // 等账号资料从后端加载完
         replayedRef.current = true;
@@ -17,14 +18,17 @@ const useWithdrawPending = (onConfirm) => {
             const pixType = args && args.pixType;
             if (!pixType) return;
             // 延后一帧,确保组件渲染稳定
-            setTimeout(() => {
+            replayTimer = setTimeout(() => {
                 if (onConfirmRef.current) onConfirmRef.current(pixType);
             }, 0);
         })) {
             // replay 成功后显式清除(用户已通过 verified 屏确认 → 导航到此页 → 消费动作)
             clearPendingInsAction();
         }
+        return () => {
+            if (replayTimer != null) clearTimeout(replayTimer);
+        };
     }, [baseInfo]);
 }
 
-export default useWithdrawPending;
+export default useWithdrawPending;

+ 8 - 2
src/index.js

@@ -15,11 +15,17 @@ import { installClientErrorLogging } from '@utils/clientLog';
 
 installClientErrorLogging();
 
+// Explicitly retain the current v6 behaviour while silencing the opt-in notices.
+// Revisit these flags together with the eventual React Router v7 migration.
+const routerFuture = {
+    v7_startTransition: false,
+    v7_relativeSplatPath: false,
+};
+
 if (!Capacitor.isNativePlatform()) {
     // 只有在纯浏览器/PWA 才注册 SW
     window.addEventListener('load', () => {
         navigator.serviceWorker?.register('/service-worker.js')
-                 .then(reg => console.log('SW register success', reg))
                  .catch(err => console.error('SW register failed', err));
     });
 }
@@ -62,7 +68,7 @@ Sentry.init({
 const root = ReactDOM.createRoot(document.getElementById('root'));
 root.render(
     <Provider store={store}>
-        <BrowserRouter>
+        <BrowserRouter future={routerFuture}>
             <ThemeProvider>
                 <ShopProvider>
                     <App />

+ 7 - 28
src/layout/BottomNav/index.jsx

@@ -2,7 +2,7 @@
 import styles from './styles.module.scss';
 
 // components
-import { NavLink, useLocation } from 'react-router-dom';
+import { NavLink } from 'react-router-dom';
 
 // hooks
 import React, { useState, useEffect } from 'react';
@@ -10,34 +10,27 @@ import symbol from "@assets/symbol-defs.svg";
 import { ROUTE_PATH } from "@constants/routes";
 import { getLogicalPathname } from "@api/request";
 import { useSelector } from "react-redux";
-import { checkIsStandalone, checkTimestampIsNextDay } from "@utils/helpers";
+import { checkIsStandalone } from "@utils/helpers";
 import { useThemeProvider } from "@contexts/themeContext";
 import { APP_THEME } from "@constants/app";
 import { getHolidaysActInfo, getHolidaysGiftInfo } from '@features/mainSlice';
 
-import boxHoliday from "@assets/bottomNav/boxHoliday.webp";
-import iconHoliday from "@assets/bottomNav/iconHoliday.webp";
 import usePaymentActions from '@hooks/usePaymentActions';
-import { getTurnplate } from '@features/turnplateSlice';
-import { LOCAL_DAILY_GIFT_TS, LOCAL_DAILY_RECHARGE_TS } from '@constants/localkey';
 import { getDailyGift } from '@features/paySlice';
 import useRecallGift from '@hooks/useRecallGift';
-import { getMailNew } from '@features/mailSlice';
-import Lottie from "lottie-react";
-import wordCupAni from "@assets/bottomNav/worldCup.json";
 import { selectLuckyRollRedPoint } from '@features/luckyRollSlice';
 import RedPoint from '@components/RedPoint';
 const i18n = global.i18n;
 
 const BottomNav = () => {
     const isStandalone = checkIsStandalone();
-    const { theme, isShowFreeWithdraw } = useThemeProvider();
+    const { theme } = useThemeProvider();
     const { getFirstGiftStats } = usePaymentActions();
     const firstPayGift = getFirstGiftStats();
     const { recallGiftRedPoint, recallGiftActive } = useRecallGift();
 
     const isCurNav = (path) => {
-        if (path == ROUTE_PATH.DEPOSIT) {
+        if (path === ROUTE_PATH.DEPOSIT) {
             return isDeposit();
         } else {
             return getLogicalPathname() === path;
@@ -47,7 +40,7 @@ const BottomNav = () => {
     const getCurNavClass = (path) => {
         const className = `${styles.button}`;
         const curClassName = `${styles.button} ${styles.curNav}`;
-        if (path == ROUTE_PATH.DEPOSIT) {
+        if (path === ROUTE_PATH.DEPOSIT) {
             return isDeposit() ? curClassName : className;
         } else {
             return getLogicalPathname() === path ? curClassName : className;
@@ -74,9 +67,7 @@ const BottomNav = () => {
 
     const dailyGiftInfo = useSelector(getDailyGift);
     const { can_claim_bonus: dailyGiftCanClaim } = dailyGiftInfo || {};
-
-    const mailNew = useSelector(getMailNew);
-
+    const luckyRollRedPoint = useSelector(selectLuckyRollRedPoint) || {};
 
     useEffect(() => {
         let tips_count = 0;
@@ -124,7 +115,7 @@ const BottomNav = () => {
 
     }, [
         holidaysGiftUser, firstPayGift, holidaysActUser, dailyGiftCanClaim,
-        recallGiftRedPoint, recallGiftActive
+        recallGiftRedPoint, recallGiftActive, luckyRollRedPoint?.isShow
     ]);
 
 
@@ -149,19 +140,7 @@ const BottomNav = () => {
         );
     };
 
-    const renderMessageRedPoint = () => {
-        return (
-            mailNew > 0 ?
-                <>
-                    <div className={`${styles.redPoint} ${styles.redMessage}`}></div>
-                </>
-                : <></>
-        );
-    };
-
-
     // lucky roll
-    const luckyRollRedPoint = useSelector(selectLuckyRollRedPoint) || {};
     const renderLuckyRollRedPoint = () => {
         return !!luckyRollRedPoint?.isShow && <RedPoint right top />;
     };

+ 4 - 4
src/layout/BottomNav/index2Backup.jsx

@@ -35,13 +35,13 @@ const BottomNav = () => {
                 </svg>
                 <span className={styles.bottomtxt}>{i18n.t("tab.deposit")}</span>
             </NavLink>
-            <NavLink className={`${styles.button}  ${window.location.pathname == ROUTE_PATH.PROMOTION ? styles.curNav : ""}`} to={ROUTE_PATH.PROMOTION}>
+            <NavLink className={`${styles.button}  ${window.location.pathname === ROUTE_PATH.PROMOTION ? styles.curNav : ""}`} to={ROUTE_PATH.PROMOTION}>
                 <svg className={`${styles.bottomicon} ${theme === APP_THEME.LIGHT ? styles.bottomicon_light : ''}`}>
                     <use xlinkHref={`${symbol}#TabIcon_Promotion`} />
                 </svg>
                 <span className={styles.bottomtxt}>{i18n.t("tab.promotion")}</span>
             </NavLink>
-            <NavLink className={`${styles.button}  ${window.location.pathname == ROUTE_PATH.HOME ? styles.curNav : ""}`} to={ROUTE_PATH.HOME}>
+            <NavLink className={`${styles.button}  ${window.location.pathname === ROUTE_PATH.HOME ? styles.curNav : ""}`} to={ROUTE_PATH.HOME}>
                 <div className={styles.mainButton}>
                     <svg width={84} height={84} className={`${styles.bottomicon}  ${styles.bottomMainIcon}`}>
                         <use xlinkHref={`${symbol}#TabIcon_Play`} />
@@ -50,13 +50,13 @@ const BottomNav = () => {
                 <span className={`${styles.bottomtxt} ${styles.bottomMainTxt}`}
                     style={{ lineHeight: "1.5rem" }}>{i18n.t("tab.play")}</span>
             </NavLink>
-            <NavLink className={`${styles.button}  ${window.location.pathname == ROUTE_PATH.SHARE ? styles.curNav : ""}`} to={ROUTE_PATH.SHARE}>
+            <NavLink className={`${styles.button}  ${window.location.pathname === ROUTE_PATH.SHARE ? styles.curNav : ""}`} to={ROUTE_PATH.SHARE}>
                 <svg className={`${styles.bottomicon} ${theme === APP_THEME.LIGHT ? styles.bottomicon_light : ''}`}>
                     <use xlinkHref={`${symbol}#TabIcon_Share`} />
                 </svg>
                 <span className={styles.bottomtxt}>{i18n.t("tab.share")}</span>
             </NavLink>
-            <NavLink className={`${styles.button}  ${window.location.pathname == ROUTE_PATH.BONUS ? styles.curNav : ""}`} to={ROUTE_PATH.BONUS}>
+            <NavLink className={`${styles.button}  ${window.location.pathname === ROUTE_PATH.BONUS ? styles.curNav : ""}`} to={ROUTE_PATH.BONUS}>
                 <svg className={`${styles.bottomicon} ${theme === APP_THEME.LIGHT ? styles.bottomicon_light : ''}`}>
                     <use xlinkHref={`${symbol}#TabIcon_Bonus`} />
                 </svg>

+ 3 - 2
src/layout/BreadCrumb/index.jsx

@@ -1,5 +1,5 @@
 import styles from './styles.module.scss';
-import {useEffect} from "react";
+import {useEffect, useMemo} from "react";
 import {NavLink, useLocation} from "react-router-dom";
 import {useSelector} from "react-redux";
 import {getRoute, getRouteChain} from "@features/mainSlice";
@@ -10,7 +10,8 @@ const i18n=global.i18n;
 const BreadCrumb=({customChain=null, className="", pathClassName=""})=>{
     const {isMobile} = useThemeProvider();
     const {pathname} = useLocation();
-    const routeChain=useSelector(getRouteChain(pathname));
+    const routeChainSelector = useMemo(() => getRouteChain(pathname), [pathname]);
+    const routeChain=useSelector(routeChainSelector);
     const chain=customChain==null?routeChain:customChain;
     const currRoute=useSelector(getRoute(pathname));
 

+ 0 - 5
src/layout/Footer/index.jsx

@@ -1,21 +1,16 @@
 import styles from './styles.module.scss';
 import React from "react";
-import logo from '@assets/logo/logo.png';
 import fb from "@assets/footer/fb.png";
 import tg from "@assets/footer/tg.png";
 import ig from "@assets/footer/ig.png";
 import x from "@assets/footer/x.png";
 import wa from "@assets/footer/wa.png";
 import pay from "@assets/footer/pay.webp";
-import tip1 from "@assets/footer/tip1.webp";
-import tip2 from "@assets/footer/tip2.webp";
 import { Trans } from "react-i18next";
 
 const i18n = global.i18n;
 
 const Footer = () => {
-    const logourl = window.themeConfig?.logo || logo;
-    const siteTitle = window.themeConfig?.title || "USSlot777";
     
     const renderMain = () => {
         return (

+ 15 - 7
src/layout/GameSearchPanel/index.jsx

@@ -78,9 +78,10 @@ const GameSearchPanel = () => {
     const [cachedGames, setCachedGames] = useState([]);
     const searchResultCache = useRef(new Map());
     const inputRef = useRef(null);
+    const focusTimer = useRef(null);
     const remoteTimer = useRef(null);
     const hideTimer = useRef(null);
-    const lastReqKw = useRef("");
+    const remoteRequestId = useRef(0);
     const duration = 200;
     const pageData = useSelector((state) => state.page && state.page.pageData);
     const allGames = useMemo(() => collectAllGames(pageData), [pageData]);
@@ -89,9 +90,14 @@ const GameSearchPanel = () => {
 
     useEffect(() => {
         setShow(isShowSearch);
+        if (focusTimer.current) {
+            clearTimeout(focusTimer.current);
+            focusTimer.current = null;
+        }
         if (isShowSearch) {
-            setTimeout(() => {
+            focusTimer.current = setTimeout(() => {
                 if (inputRef.current) inputRef.current.focus();
+                focusTimer.current = null;
             }, 50);
         } else {
             setInputValue("");
@@ -102,12 +108,15 @@ const GameSearchPanel = () => {
 
     useEffect(() => {
         return () => {
+            remoteRequestId.current += 1;
+            if (focusTimer.current) clearTimeout(focusTimer.current);
             if (remoteTimer.current) clearTimeout(remoteTimer.current);
             if (hideTimer.current) clearTimeout(hideTimer.current);
         };
     }, []);
 
     useEffect(() => {
+        const requestId = ++remoteRequestId.current;
         if (remoteTimer.current) {
             clearTimeout(remoteTimer.current);
             remoteTimer.current = null;
@@ -131,10 +140,10 @@ const GameSearchPanel = () => {
             return;
         }
         remoteTimer.current = setTimeout(() => {
-            if (lastReqKw.current === kw) return;
-            lastReqKw.current = kw;
+            remoteTimer.current = null;
             setRemoteLoading(true);
             Http.post({ url: "/game/search", data: { keyword: kw } }).then((res) => {
+                if (requestId !== remoteRequestId.current) return;
                 const data = res && res.data;
                 let list = [];
                 if (data) {
@@ -149,12 +158,12 @@ const GameSearchPanel = () => {
                 }
                 if (list.length > 0) setCachedGames((current) => mergeCachedGames(current, list));
 
-                if (kw !== inputValue.trim()) return;
                 setRemoteResults(list);
             }).catch(() => {
+                if (requestId !== remoteRequestId.current) return;
                 setRemoteResults([]);
             }).finally(() => {
-                setRemoteLoading(false);
+                if (requestId === remoteRequestId.current) setRemoteLoading(false);
             });
         }, REMOTE_DELAY);
     }, [inputValue, localResults]);
@@ -178,7 +187,6 @@ const GameSearchPanel = () => {
         setInputValue("");
         setRemoteResults(null);
         setRemoteLoading(false);
-        lastReqKw.current = "";
         if (inputRef.current) inputRef.current.focus();
     };
 

+ 38 - 35
src/layout/Layout.jsx

@@ -3,7 +3,7 @@ import Sidebar from "@layout/Sidebar";
 import Navbar from "@layout/Navbar";
 import BottomNav from "@layout/BottomNav";
 import AppContainer from "@layout/AppContainer";
-import React, { lazy, Suspense, useEffect, useRef } from "react";
+import React, { Suspense, useCallback, useEffect, useRef } from "react";
 import LoadingScreen from "@components/LoadingScreen";
 import { useLocation, useNavigate } from "react-router-dom";
 import { useDispatch, useSelector } from "react-redux";
@@ -72,13 +72,11 @@ import WRedeemCode from "@widgets/WRedeemCode";
 import WSecurityCheck from "@widgets/WWithdrawalInfo/Security/WSecurityCheck";
 import { selectEmailInfo } from "@features/redemptionCodeSlice";
 
-const PageNotFound = lazy(() => import('@pages/PageNotFound'));
-
 const Layout = ({ ...props }) => {
     const appRef = useRef(null);
     const mainRef = useRef(null);
     const msgBoxRef = useRef(null);
-    const { isMobile, isShowWheel, setIsShowWheel, isShowWDPassBox, setIsShowWDPassBox, isOpenSub, walletPos } = useThemeProvider();
+    const { isMobile, isShowWheel, setIsShowWheel, isOpenSub, walletPos } = useThemeProvider();
     const { pathname, search, hash } = useLocation();
     const isFull = useSelector(isFullRoute(pathname));
     const inRoutes = useSelector(routesIn);
@@ -137,70 +135,75 @@ const Layout = ({ ...props }) => {
         };
     }, [pathname, hash]);
     useEffect(() => {
-        global.msgBox = msgBoxRef.current;
-    }, [msgBoxRef]);
+        const msgBox = msgBoxRef.current;
+        global.msgBox = msgBox;
+        return () => {
+            if (global.msgBox === msgBox) delete global.msgBox;
+        };
+    }, []);
     useEffect(() => {
         let routeType = ROUTE_TYPE_FULL;
         if (!mobile) routeType = ROUTE_TYPE_IN;
         dispatch(updateGameRoute(routeType));
-    }, [mobile]);
+    }, [dispatch, mobile]);
 
     // 通用提现按钮跳转
     const withdrawInfo = useSelector(getFreeWithdraw);
-    const { open: missionOpen, setOpen: setMissionOpen } = useWithdrawMission();
-    const onClickNormalWithdraw = () => {
-        if (withdrawInfo.state === 1) {
+    const { setOpen: setMissionOpen } = useWithdrawMission();
+    const onClickNormalWithdraw = useCallback(() => {
+        if (withdrawInfo?.state === 1) {
             navigate(ROUTE_PATH.FREEWITHDRAW);
-        } else if (withdrawInfo.state === 2) {
+        } else if (withdrawInfo?.state === 2) {
             navigate(ROUTE_PATH.FREEWITHDRAW);
-        } else if (withdrawInfo.state === 3) {
+        } else if (withdrawInfo?.state === 3) {
             setMissionOpen(true);
-        } else if (withdrawInfo.state === 4) {
+        } else if (withdrawInfo?.state === 4) {
             setMissionOpen(true);
-        } else {
-
         }
-    };
-
-    global.onClickNormalWithdraw = onClickNormalWithdraw;
+    }, [navigate, setMissionOpen, withdrawInfo?.state]);
 
     // 通用邮箱兑换码按钮跳转
     const emailInfo = useSelector(selectEmailInfo);
-    const isEmptyObj = (obj) => {
-        return obj && typeof obj === 'object' && Object.keys(obj).length === 0;
-    };
-    const onClickRedeemCode = () => {
-        if (isEmptyObj(emailInfo)) return;
+    const onClickRedeemCode = useCallback(() => {
+        if (emailInfo && typeof emailInfo === 'object' && Object.keys(emailInfo).length === 0) return;
 
         const pendingRechargeCount = emailInfo?.pending_recharge_reward_count;
         const vipReward = emailInfo?.vip_reward;
         const claimableRewardCount = emailInfo?.claimable_reward_count;
-        const leftEmail = emailInfo?.daily_send_left;
-
+        const rawMultiplier = Number(vipReward?.multiplier);
+        const multiplier = Number.isFinite(rawMultiplier) && rawMultiplier > 0 ? rawMultiplier : 1;
         // 兑换按钮
         if (claimableRewardCount > 0) {//存在充值解锁未领取奖励
             const payload = {
                 grant_status: "ready",
                 amount: claimableRewardCount,
-                base_amount: (claimableRewardCount / vipReward?.multiplier),
-                vip_multiplier: vipReward?.multiplier
+                base_amount: (claimableRewardCount / multiplier),
+                vip_multiplier: multiplier
             };
-            global.showRedeemResult && global.showRedeemResult(payload);
+            global.showRedeemResult?.(payload);
         } else if (pendingRechargeCount > 0) {//未充值
             const payload = {
                 grant_status: "pending_recharge",
                 amount: pendingRechargeCount,
-                base_amount: (pendingRechargeCount / vipReward?.multiplier),
-                vip_multiplier: vipReward?.multiplier
+                base_amount: (pendingRechargeCount / multiplier),
+                vip_multiplier: multiplier
             };
-            global.showRedeemResult && global.showRedeemResult(payload);
+            global.showRedeemResult?.(payload);
         } else if (emailInfo?.is_bound === false) {//未绑定邮箱
-            global.openRedeemFirstBind && global.openRedeemFirstBind();
+            global.openRedeemFirstBind?.();
         } else {
-            global.openRedeemNormal && global.openRedeemNormal();
+            global.openRedeemNormal?.();
         }
-    };
-    global.onClickRedeemCode = onClickRedeemCode;
+    }, [emailInfo]);
+
+    useEffect(() => {
+        global.onClickNormalWithdraw = onClickNormalWithdraw;
+        global.onClickRedeemCode = onClickRedeemCode;
+        return () => {
+            if (global.onClickNormalWithdraw === onClickNormalWithdraw) delete global.onClickNormalWithdraw;
+            if (global.onClickRedeemCode === onClickRedeemCode) delete global.onClickRedeemCode;
+        };
+    }, [onClickNormalWithdraw, onClickRedeemCode]);
 
 
     const renderIn = () => {

+ 67 - 113
src/layout/Navbar/index.jsx

@@ -3,7 +3,7 @@ import styles from './styles.module.scss';
 
 import {useSidebar} from "@contexts/sidebarContext";
 import {useThemeProvider} from "@contexts/themeContext";
-import {useEffect, useRef, useState} from 'react';
+import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
 import styled from 'styled-components/macro';
 import Button from "@mui/material/Button";
 import {NavLink, useLocation, useNavigate} from "react-router-dom";
@@ -73,25 +73,15 @@ const DesktopNavToggle=()=>{
     const {pathname} = useLocation();
     const [alignment, setAlignment] = useState(pathname);
     const currRoute=useSelector(getRoute(pathname));
-    const getRootRoute=(pathname)=>{
-        let route="";
-        if (currRoute){
-            let clv=currRoute.lv;
-            if (clv==null)return route;
-            for (let i = 0; i < LINKS.length; i++) {
-                let link=LINKS[i];
-                let lv=link.lv;
-                if (clv.startsWith(lv)){
-                    route=link.path;
-                    break;
-                }
-            }
-        }
-        return route;
-    }
     useEffect(() => {
-        setAlignment(getRootRoute(pathname));
-    }, [pathname]);
+        let rootRoute = "";
+        const currentLevel = currRoute?.lv;
+        if (currentLevel != null) {
+            const rootLink = LINKS.find((link) => currentLevel.startsWith(link.lv));
+            rootRoute = rootLink?.path || "";
+        }
+        setAlignment(rootRoute);
+    }, [LINKS, currRoute, pathname]);
     const handleChange = (event, newAlignment) => {
     };
     return (
@@ -218,13 +208,14 @@ const PageHeader = () => {
     const {initHeaderHeight,installOpen} = useThemeProvider();
     const {pathname} = useLocation();
     const barRef = useRef(null);
-    const chain=useSelector(getRouteChain(pathname));
+    const routeChainSelector = useMemo(() => getRouteChain(pathname), [pathname]);
+    const chain=useSelector(routeChainSelector);
     const {isMobile,showHeader,setShowHeader,setShowMHeader} = useThemeProvider();
 
-    let lastscroll=0;
+    const lastScrollRef = useRef(0);
     const isShowInstall=useSelector(getShowInstall);
 
-    const getHeaderHeight = () => {
+    const getHeaderHeight = useCallback(() => {
         const header = document.querySelector('#appbar');
         let offset = 0;
         if (barRef){
@@ -243,12 +234,12 @@ const PageHeader = () => {
         }
         if (header!=null)return `${(header.offsetHeight-offset) || 0}px`
         else return '0px';
-    }
-    const handleScroll=()=>{
+    }, []);
+    const handleScroll=useCallback(()=>{
         if (!barRef.showHeader)return;
         let scrollTop = document.documentElement.scrollTop; //滚动条滚动高度
-        let dt=scrollTop-lastscroll;
-        lastscroll=scrollTop;
+        let dt=scrollTop-lastScrollRef.current;
+        lastScrollRef.current=scrollTop;
         let show=false;
         if (dt>20){
             show=false;
@@ -261,98 +252,28 @@ const PageHeader = () => {
         if (scrollTop<20){
             setShowMHeader(true);
         }
-    }
-    const changeHeaderHeight=()=>{
-        document.documentElement.style.setProperty('--header-active', getHeaderHeight());
-        initHeaderHeight(getHeaderHeight());
-    }
-    const resizeObserver = new ResizeObserver(entries => {
-        for (let entry of entries) {
-            // 可在这里处理高度变化的逻辑
-            changeHeaderHeight();
-        }
-    });
-    const checkInstall=(l)=>{
-        //route控制
-        if (!isShowInstall){
-            return false;
-        }
-        if (!installOpen){
-            return false;
-        }
-        if (l.parent.detection.isStandalone()) {
-            return false;
-        }
-        return true;
-    }
-    const startListening=async ()=>{
-        if (window.progressier==null)return ;
-        let l=window.progressier.banners;
-        await l.parent.data.waitForData();
-        l.data = l.parent.data.params;
-        if (!l.reinstallTimer) {
-            l.reinstallTimer = setInterval(function() {
-                if (!checkInstall(l)){
-                    clearInterval(l.reinstallTimer)
-                    l.reinstallTimer=null;
-                    return;
-                }
-                l.showed = true;
-
-                clearInterval(l.reinstallTimer)
-            }, l.interval)
-        }
-        else
-        {
-            clearInterval(l.reinstallTimer)
-            l.reinstallTimer=null;
-        }
-    }
-    const checkProgressier=async ()=>{
-        if (window.progressier){
-            startListening();
-        }
-    }
-    const stopListener=()=>{
-        if (window.progressier==null)return;
-        let l=window.progressier.banners;
-        if(l){
-            if (l.reinstallTimer!=null){
-                clearInterval(l.reinstallTimer);
-                l.reinstallTimer=null;
-            }
-        }
-    }
-    const checkInstallOpen=()=>{
-        if (installOpen&&isShowInstall){
-            checkProgressier();
-        }
-        else
-        {
-            if (window.install_banner){
-                window.install_banner.remove();
-            }
-        }
-    }
+    }, [setShowMHeader]);
+    const changeHeaderHeight=useCallback(()=>{
+        const height = getHeaderHeight();
+        document.documentElement.style.setProperty('--header-active', height);
+        initHeaderHeight(height);
+    }, [getHeaderHeight, initHeaderHeight]);
     useEffect(() => {
-        // 这里的代码会在组件挂载时执行
+        const bar = barRef.current;
+        const resizeObserver = new ResizeObserver(changeHeaderHeight);
         window.addEventListener('scroll', handleScroll);
-        if (barRef.current) {
-            resizeObserver.observe(barRef.current);
-        }
+        if (bar) resizeObserver.observe(bar);
         return () => {
-            // 可选的清除函数,当组件卸载时执行
             window.removeEventListener('scroll', handleScroll);
-            if (barRef.current) {
-                resizeObserver.unobserve(barRef.current);
-            }
+            if (bar) resizeObserver.unobserve(bar);
+            resizeObserver.disconnect();
         };
-    }, []);
+    }, [changeHeaderHeight, handleScroll]);
     useEffect(()=>{
         setShowMHeader(showHeader);
         barRef.showHeader=showHeader;
         changeHeaderHeight();
-    },[showHeader]);
+    },[changeHeaderHeight, setShowMHeader, showHeader]);
 
     useEffect(()=>{
         let len=chain.length;
@@ -364,12 +285,45 @@ const PageHeader = () => {
             isShowH=true;
         }
         setShowHeader(isShowH);
-    },[pathname]);
+    },[LINKS, chain.length, pathname, setShowHeader]);
     useEffect(()=>{
-        checkInstallOpen();
-        return ()=>{
-            stopListener();
+        const banners = window.progressier?.banners;
+        let cancelled = false;
+        const stopListener = () => {
+            if (banners?.reinstallTimer != null) {
+                clearInterval(banners.reinstallTimer);
+                banners.reinstallTimer = null;
+            }
+        };
+
+        if (installOpen && isShowInstall && banners) {
+            const startListening = async () => {
+                await banners.parent?.data?.waitForData?.();
+                if (cancelled) return;
+                banners.data = banners.parent?.data?.params;
+                if (banners.reinstallTimer) {
+                    stopListener();
+                    return;
+                }
+                banners.reinstallTimer = setInterval(() => {
+                    const isStandalone = banners.parent?.detection?.isStandalone?.();
+                    if (!installOpen || !isShowInstall || isStandalone) {
+                        stopListener();
+                        return;
+                    }
+                    banners.showed = true;
+                    stopListener();
+                }, banners.interval);
+            };
+            startListening();
+        } else {
+            window.install_banner?.remove();
         }
+
+        return () => {
+            cancelled = true;
+            stopListener();
+        };
     },[installOpen,isShowInstall])
     const renderMobileInstall=()=>{
         return (

+ 9 - 5
src/layout/Routes/RouteContainer.jsx

@@ -1,5 +1,5 @@
 import {ROUTE_TYPE_FULL, ROUTE_TYPE_IN, ROUTE_TYPE_POP, ROUTE_TYPE_POP_OR_FULL,ROUTE_TYPE_CUSTOM} from "@constants/routes";
-import React from "react";
+import React, {useRef} from "react";
 import Popup from "@components/Popup";
 import SubScreen from "@components/SubScreen";
 import {useThemeProvider} from "@contexts/themeContext";
@@ -13,6 +13,8 @@ const DESKTOP_DRAWER_ROUTES = ["settings", "mail", "mailDetail"];
 const RouteContainer=(props)=>{
     const { children, route,open, onClose} = props;
     const {isMobile} = useThemeProvider();
+    const popupTransitionNodeRef = useRef(null);
+    const customTransitionNodeRef = useRef(null);
     const isDesktopDrawer = () => {
         return !isMobile() && DESKTOP_DRAWER_ROUTES.includes(route.component);
     };
@@ -30,8 +32,8 @@ const RouteContainer=(props)=>{
     const renderPop=()=>{
         const desktopClass = isMobile() ? '' : `popup-big popup-${route.component}`;
         return(
-            <CSSTransition {...props} timeout={500}>
-                <Popup {...props} popupClass={desktopClass}>
+            <CSSTransition {...props} timeout={500} nodeRef={popupTransitionNodeRef}>
+                <Popup {...props} transitionNodeRef={popupTransitionNodeRef} popupClass={desktopClass}>
                     {children}
                 </Popup>
             </CSSTransition>
@@ -93,8 +95,10 @@ const RouteContainer=(props)=>{
     const renderCustom=()=>{
         return (
             <>
-                <CSSTransition {...props} timeout={500}>
-                    {children}
+                <CSSTransition {...props} timeout={500} nodeRef={customTransitionNodeRef}>
+                    <div ref={customTransitionNodeRef} style={{display: "contents"}}>
+                        {children}
+                    </div>
                 </CSSTransition>
             </>
         );

+ 1 - 1
src/layout/Routes/RouteWrapper.jsx

@@ -28,7 +28,7 @@ const RouteWrapper = (props) => {
             if (route.type===ROUTE_TYPE_IN||route.type===ROUTE_TYPE_FULL)setLock(false);
             else setLock(true);
         }
-    }, [pathname,hash]);
+    }, [pathname,hash,route]);
     /** useMemo 缓存query,避免每次生成生的query */
     const renderIn=()=>{
         let isexit=false;

Some files were not shown because too many files changed in this diff