const i18n = require('LanguageData'); i18n.init(window.language); // languageID should be equal to the one we input in New Language ID input field const WECHAT_ON_HIDE_TARGET_ACTION = { SHARE_CHAT_MESSAGE: 8, CLOSE: 3, }; const pbStructRoot = require('./modules/room_downsync_frame_proto_bundle.forcemsg.js'); window.RoomDownsyncFrame = pbStructRoot.treasurehunterx.RoomDownsyncFrame; window.BattleColliderInfo = pbStructRoot.treasurehunterx.BattleColliderInfo; cc.Class({ extends: cc.Component, properties: { cavasNode: { default: null, type: cc.Node }, backgroundNode: { default: null, type: cc.Node }, loadingPrefab: { default: null, type: cc.Prefab }, tipsLabel: { default: null, type: cc.Label, }, downloadProgress: { default: null, type: cc.ProgressBar, }, writtenBytes: { default: null, type: cc.Label, }, expectedToWriteBytes: { default: null, type: cc.Label, }, handlerProgress: { default: null, type: cc.ProgressBar, }, handledUrlsCount: { default: null, type: cc.Label, }, toHandledUrlsCount: { default: null, type: cc.Label, }, }, // LIFE-CYCLE CALLBACKS: onLoad() { wx.onShow((res) => { console.log("+++++ wx onShow(), onShow.res ", res); window.expectedRoomId = res.query.expectedRoomId; }); wx.onHide((res) => { // Reference https://developers.weixin.qq.com/minigame/dev/api/wx.exitMiniProgram.html. console.log("+++++ wx onHide(), onHide.res: ", res); if ( WECHAT_ON_HIDE_TARGET_ACTION == res.targetAction || "back" == res.mode // After "WeChat v7.0.4 on iOS" || "close" == res.mode ) { window.clearLocalStorageAndBackToLoginScene(); } else { // Deliberately left blank. } }); const self = this; self.getRetCodeList(); self.getRegexList(); self.showTips(i18n.t("login.tips.AUTO_LOGIN_1")); self.checkIntAuthTokenExpire().then( () => { self.showTips(i18n.t("login.tips.AUTO_LOGIN_2")); const intAuthToken = JSON.parse(cc.sys.localStorage.getItem('selfPlayer')).intAuthToken; self.useTokenLogin(intAuthToken); }, () => { // 调用wx.login然后请求登录。 wx.authorize({ scope: "scope.userInfo", success() { self.showTips(i18n.t("login.tips.WECHAT_AUTHORIZED_AND_INT_AUTH_TOKEN_LOGGING_IN")); wx.login({ success(res) { console.log("wx login success, res: ", res); const code = res.code; wx.getUserInfo({ success(res) { const userInfo = res.userInfo; console.log("Get user info ok: ", userInfo); self.useWxCodeMiniGameLogin(code, userInfo); }, fail(err) { console.error(i18n.t("login.tips.AUTO_LOGIN_FAILED_WILL_USE_MANUAL_LOGIN"), err); self.showTips(i18n.t("login.tips.AUTO_LOGIN_FAILED_WILL_USE_MANUAL_LOGIN")); self.createAuthorizeThenLoginButton(); }, }) }, fail(err) { if (err) { console.error(i18n.t("login.tips.AUTO_LOGIN_FAILED_WILL_USE_MANUAL_LOGIN"), err); self.showTips(i18n.t("login.tips.AUTO_LOGIN_FAILED_WILL_USE_MANUAL_LOGIN")); self.createAuthorizeThenLoginButton(); } }, }); }, fail(err) { console.error(i18n.t("login.tips.AUTO_LOGIN_FAILED_WILL_USE_MANUAL_LOGIN"), err); self.showTips(i18n.t("login.tips.AUTO_LOGIN_FAILED_WILL_USE_MANUAL_LOGIN")); self.createAuthorizeThenLoginButton(); } }) } ); }, createAuthorizeThenLoginButton(tips) { const self = this; let sysInfo = wx.getSystemInfoSync(); //获取微信界面大小 let width = sysInfo.screenWidth; let height = sysInfo.screenHeight; let button = wx.createUserInfoButton({ type: 'text', text: '', style: { left: 0, top: 0, width: width, height: height, backgroundColor: '#00000000', //最后两位为透明度 color: '#ffffff', fontSize: 20, textAlign: "center", lineHeight: height, }, }); button.onTap((res) => { console.log(res); if (null != res.userInfo) { const userInfo = res.userInfo; self.showTips(i18n.t("login.tips.WECHAT_AUTHORIZED_AND_INT_AUTH_TOKEN_LOGGING_IN")); wx.login({ success(res) { console.log('wx.login success, res:', res); const code = res.code; self.useWxCodeMiniGameLogin(code, userInfo); button.destroy(); }, fail(err) { console.err(i18n.t("login.tips.AUTO_LOGIN_FAILED_WILL_USE_MANUAL_LOGIN"), err); self.showTips(i18n.t("login.tips.AUTO_LOGIN_FAILED_WILL_USE_MANUAL_LOGIN")); }, }); } else { self.showTips(i18n.t("login.tips.PLEASE_AUTHORIZE_WECHAT_LOGIN_FIRST")); } }) }, onDestroy() { console.log("+++++++ WechatGameLogin onDestroy()"); }, showTips(text) { if (this.tipsLabel != null) { this.tipsLabel.string = text; } else { console.log('Login scene showTips failed') } }, getRetCodeList() { const self = this; self.retCodeDict = constants.RET_CODE; }, getRegexList() { const self = this; self.regexList = { EMAIL: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/, PHONE: /^\+?[0-9]{8,14}$/, STREET_META: /^.{5,100}$/, LNG_LAT_TEXT: /^[0-9]+(\.[0-9]{4,6})$/, SEO_KEYWORD: /^.{2,50}$/, PASSWORD: /^.{6,50}$/, SMS_CAPTCHA_CODE: /^[0-9]{4}$/, ADMIN_HANDLE: /^.{4,50}$/, }; }, onSMSCaptchaGetButtonClicked(evt) { var timerEnable = true; const self = this; if (!self.checkPhoneNumber('getCaptcha')) { return; } NetworkUtils.ajax({ url: backendAddress.PROTOCOL + '://' + backendAddress.HOST + ':' + backendAddress.PORT + constants.ROUTE_PATH.API + constants.ROUTE_PATH.PLAYER + constants.ROUTE_PATH.VERSION + constants.ROUTE_PATH.SMS_CAPTCHA + constants.ROUTE_PATH.GET, type: 'GET', data: { phoneCountryCode: self.phoneCountryCodeInput.getComponent(cc.EditBox).string, phoneNum: self.phoneNumberInput.getComponent(cc.EditBox).string }, success: function(res) { switch (res.ret) { case constants.RET_CODE.OK: self.phoneNumberTips.getComponent(cc.Label).string = ''; self.captchaTips.getComponent(cc.Label).string = ''; break; case constants.RET_CODE.DUPLICATED: self.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.DUPLICATED"); break; case constants.RET_CODE.INCORRECT_PHONE_COUNTRY_CODE: case constants.RET_CODE.INCORRECT_PHONE_NUMBER: self.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.PHONE_ERR"); break; case constants.RET_CODE.IS_TEST_ACC: self.smsLoginCaptchaInput.getComponent(cc.EditBox).string = res.smsLoginCaptcha; self.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.TEST_USER"); timerEnable = false; // clearInterval(self.countdownTimer); break; case constants.RET_CODE.SMS_CAPTCHA_REQUESTED_TOO_FREQUENTLY: self.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.SMS_CAPTCHA_FREEQUENT_REQUIRE"); default: break; } if (timerEnable) self.countdownTime(self); } }); }, countdownTime(self) { self.smsLoginCaptchaButton.off('click', self.onSMSCaptchaGetButtonClicked); self.smsLoginCaptchaButton.removeChild(self.smsGetCaptchaNode); self.smsWaitCountdownNode.parent = self.smsLoginCaptchaButton; var total = 20; // Magic number self.countdownTimer = setInterval(function() { if (total === 0) { self.smsWaitCountdownNode.parent.removeChild(self.smsWaitCountdownNode); self.smsGetCaptchaNode.parent = self.smsLoginCaptchaButton; self.smsWaitCountdownNode.getChildByName('WaitTimeLabel').getComponent(cc.Label).string = 20; self.smsLoginCaptchaButton.on('click', self.onSMSCaptchaGetButtonClicked); clearInterval(self.countdownTimer); } else { total--; self.smsWaitCountdownNode.getChildByName('WaitTimeLabel').getComponent(cc.Label).string = total; } }, 1000) }, checkIntAuthTokenExpire() { return new Promise((resolve, reject) => { if (!cc.sys.localStorage.getItem("selfPlayer")) { reject(); return; } const selfPlayer = JSON.parse(cc.sys.localStorage.getItem('selfPlayer')); (selfPlayer.intAuthToken && new Date().getTime() < selfPlayer.expiresAt) ? resolve() : reject(); }) }, checkPhoneNumber(type) { const self = this; const phoneNumberRegexp = self.regexList.PHONE; var phoneNumberString = self.phoneNumberInput.getComponent(cc.EditBox).string; if (phoneNumberString) { return true; if (!phoneNumberRegexp.test(phoneNumberString)) { self.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.PHONE_ERR"); return false; } else { return true; } } else { if (type === 'getCaptcha' || type === 'login') { self.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.PHONE_ERR"); } return false; } }, checkCaptcha(type) { const self = this; const captchaRegexp = self.regexList.SMS_CAPTCHA_CODE; var captchaString = self.smsLoginCaptchaInput.getComponent(cc.EditBox).string; if (captchaString) { if (self.smsLoginCaptchaInput.getComponent(cc.EditBox).string.length !== 4 || (!captchaRegexp.test(captchaString))) { self.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.CAPTCHA_ERR"); return false; } else { return true; } } else { if (type === 'login') { self.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.CAPTCHA_ERR"); } return false; } }, useTokenLogin(_intAuthToken) { var self = this; NetworkUtils.ajax({ url: backendAddress.PROTOCOL + '://' + backendAddress.HOST + ':' + backendAddress.PORT + constants.ROUTE_PATH.API + constants.ROUTE_PATH.PLAYER + constants.ROUTE_PATH.VERSION + constants.ROUTE_PATH.INT_AUTH_TOKEN + constants.ROUTE_PATH.LOGIN, type: "POST", data: { intAuthToken: _intAuthToken }, success: function(resp) { self.onLoggedIn(resp); }, error: function(xhr, status, errMsg) { console.log("Login attempt `useTokenLogin` failed, about to execute `clearBoundRoomIdInBothVolatileAndPersistentStorage`."); window.clearBoundRoomIdInBothVolatileAndPersistentStorage() self.showTips(i18n.t("login.tips.AUTO_LOGIN_FAILED_WILL_USE_MANUAL_LOGIN")); self.createAuthorizeThenLoginButton(); }, timeout: function() { self.enableInteractiveControls(true); }, }); }, enableInteractiveControls(enabled) {}, onLoginButtonClicked(evt) { const self = this; if (!self.checkPhoneNumber('login') || !self.checkCaptcha('login')) { return; } self.loginParams = { phoneCountryCode: self.phoneCountryCodeInput.getComponent(cc.EditBox).string, phoneNum: self.phoneNumberInput.getComponent(cc.EditBox).string, smsLoginCaptcha: self.smsLoginCaptchaInput.getComponent(cc.EditBox).string }; self.enableInteractiveControls(false); NetworkUtils.ajax({ url: backendAddress.PROTOCOL + '://' + backendAddress.HOST + ':' + backendAddress.PORT + constants.ROUTE_PATH.API + constants.ROUTE_PATH.PLAYER + constants.ROUTE_PATH.VERSION + constants.ROUTE_PATH.SMS_CAPTCHA + constants.ROUTE_PATH.LOGIN, type: "POST", data: self.loginParams, success: function(resp) { self.onLoggedIn(resp); }, error: function(xhr, status, errMsg) { console.log("Login attempt `onLoginButtonClicked` failed, about to execute `clearBoundRoomIdInBothVolatileAndPersistentStorage`."); window.clearBoundRoomIdInBothVolatileAndPersistentStorage() }, timeout: function() { self.enableInteractiveControls(true); } }); }, onWechatLoggedIn(res) { const self = this; if (constants.RET_CODE.OK == res.ret) { //根据服务器返回信息设置selfPlayer self.enableInteractiveControls(false); const date = Number(res.expiresAt); const selfPlayer = { expiresAt: date, playerId: res.playerId, intAuthToken: res.intAuthToken, displayName: res.displayName, avatar: res.avatar, } cc.sys.localStorage.setItem('selfPlayer', JSON.stringify(selfPlayer)); self.useTokenLogin(res.intAuthToken); } else { cc.sys.localStorage.removeItem("selfPlayer"); window.clearBoundRoomIdInBothVolatileAndPersistentStorage(); self.showTips(i18n.t("login.tips.WECHAT_LOGIN_FAILED_TAP_SCREEN_TO_RETRY") + ", errorCode = " + res.ret); self.createAuthorizeThenLoginButton(); } }, onLoggedIn(res) { const self = this; console.log("OnLoggedIn: ", res); if (constants.RET_CODE.OK == res.ret) { if (window.isUsingX5BlinkKernelOrWebkitWeChatKernel()) { window.initWxSdk = self.initWxSdk.bind(self); window.initWxSdk(); } self.enableInteractiveControls(false); const date = Number(res.expiresAt); const selfPlayer = { expiresAt: date, playerId: res.playerId, intAuthToken: res.intAuthToken, avatar: res.avatar, displayName: res.displayName, name: res.name, } cc.sys.localStorage.setItem("selfPlayer", JSON.stringify(selfPlayer)); console.log("cc.sys.localStorage.selfPlayer = ", cc.sys.localStorage.getItem("selfPlayer")); if (self.countdownTimer) { clearInterval(self.countdownTimer); } cc.director.loadScene('default_map'); } else { console.warn('onLoggedIn failed!') cc.sys.localStorage.removeItem("selfPlayer"); window.clearBoundRoomIdInBothVolatileAndPersistentStorage(); self.enableInteractiveControls(true); switch (res.ret) { case constants.RET_CODE.DUPLICATED: this.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.DUPLICATED"); break; case constants.RET_CODE.TOKEN_EXPIRED: this.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.LOGIN_TOKEN_EXPIRED"); break; case constants.RET_CODE.SMS_CAPTCHA_NOT_MATCH: self.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.SMS_CAPTCHA_NOT_MATCH"); break; case constants.RET_CODE.INCORRECT_CAPTCHA: self.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.SMS_CAPTCHA_NOT_MATCH"); break; case constants.RET_CODE.SMS_CAPTCHA_CODE_NOT_EXISTING: self.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.SMS_CAPTCHA_NOT_MATCH"); break; case constants.RET_CODE.INCORRECT_PHONE_NUMBER: self.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.INCORRECT_PHONE_NUMBER"); break; case constants.RET_CODE.INVALID_REQUEST_PARAM: self.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.INCORRECT_PHONE_NUMBER"); break; case constants.RET_CODE.INCORRECT_PHONE_COUNTRY_CODE: case constants.RET_CODE.INCORRECT_PHONE_NUMBER: this.captchaTips.getComponent(cc.Label).string = i18n.t("login.tips.INCORRECT_PHONE_NUMBER"); break; default: break; } self.showTips(i18n.t("login.tips.AUTO_LOGIN_FAILED_WILL_USE_MANUAL_LOGIN")); self.createAuthorizeThenLoginButton(); } }, useWXCodeLogin(_code) { const self = this; NetworkUtils.ajax({ url: backendAddress.PROTOCOL + '://' + backendAddress.HOST + ':' + backendAddress.PORT + constants.ROUTE_PATH.API + constants.ROUTE_PATH.PLAYER + constants.ROUTE_PATH.VERSION + constants.ROUTE_PATH.WECHAT + constants.ROUTE_PATH.LOGIN, type: "POST", data: { code: _code }, success: function(res) { self.onWechatLoggedIn(res); }, error: function(xhr, status, errMsg) { console.log("Login attempt `onLoginButtonClicked` failed, about to execute `clearBoundRoomIdInBothVolatileAndPersistentStorage`."); cc.sys.localStorage.removeItem("selfPlayer"); window.clearBoundRoomIdInBothVolatileAndPersistentStorage(); self.showTips(i18n.t("login.tips.WECHAT_LOGIN_FAILED_TAP_SCREEN_TO_RETRY") + ", errorMsg =" + errMsg); }, timeout: function() { console.log("Login attempt `onLoginButtonClicked` timed out, about to execute `clearBoundRoomIdInBothVolatileAndPersistentStorage`."); cc.sys.localStorage.removeItem("selfPlayer"); window.clearBoundRoomIdInBothVolatileAndPersistentStorage(); self.showTips(i18n.t("login.tips.WECHAT_LOGIN_FAILED_TAP_SCREEN_TO_RETRY") + ", errorMsg =" + errMsg); }, }); }, // 对比useWxCodeLogin函数只是请求了不同url useWxCodeMiniGameLogin(_code, _userInfo) { const self = this; NetworkUtils.ajax({ url: backendAddress.PROTOCOL + '://' + backendAddress.HOST + ':' + backendAddress.PORT + constants.ROUTE_PATH.API + constants.ROUTE_PATH.PLAYER + constants.ROUTE_PATH.VERSION + constants.ROUTE_PATH.WECHATGAME + constants.ROUTE_PATH.LOGIN, type: "POST", data: { code: _code, avatarUrl: _userInfo.avatarUrl, nickName: _userInfo.nickName, }, success: function(res) { self.onWechatLoggedIn(res); }, error: function(xhr, status, errMsg) { console.log("Login attempt `onLoginButtonClicked` failed, about to execute `clearBoundRoomIdInBothVolatileAndPersistentStorage`."); cc.sys.localStorage.removeItem("selfPlayer"); window.clearBoundRoomIdInBothVolatileAndPersistentStorage(); self.showTips(i18n.t("login.tips.WECHAT_LOGIN_FAILED_TAP_SCREEN_TO_RETRY") + ", errorMsg =" + errMsg); self.createAuthorizeThenLoginButton(); }, timeout: function() { console.log("Login attempt `onLoginButtonClicked` failed, about to execute `clearBoundRoomIdInBothVolatileAndPersistentStorage`."); cc.sys.localStorage.removeItem("selfPlayer"); window.clearBoundRoomIdInBothVolatileAndPersistentStorage(); self.showTips(i18n.t("login.tips.WECHAT_LOGIN_FAILED_TAP_SCREEN_TO_RETRY")); self.createAuthorizeThenLoginButton(); }, }); }, getWechatCode(evt) { let self = this; self.showTips(""); const wechatServerEndpoint = wechatAddress.PROTOCOL + "://" + wechatAddress.HOST + ((null != wechatAddress.PORT && "" != wechatAddress.PORT.trim()) ? (":" + wechatAddress.PORT) : ""); const url = wechatServerEndpoint + constants.WECHAT.AUTHORIZE_PATH + "?" + wechatAddress.APPID_LITERAL + "&" + constants.WECHAT.REDIRECT_RUI_KEY + NetworkUtils.encode(window.location.href) + "&" + constants.WECHAT.RESPONSE_TYPE + "&" + constants.WECHAT.SCOPE + constants.WECHAT.FIN; console.log("To visit wechat auth addr: ", url); window.location.href = url; }, initWxSdk() { const selfPlayer = JSON.parse(cc.sys.localStorage.getItem('selfPlayer')); const origUrl = window.location.protocol + "//" + window.location.host + window.location.pathname; /* * The `shareLink` must * - have its 2nd-order-domain registered as trusted 2nd-order under the targetd `res.jsConfig.app_id`, and * - extracted from current window.location.href. */ const shareLink = origUrl; const updateAppMsgShareDataObj = { type: 'link', // 分享类型,music、video或link,不填默认为link dataUrl: '', // 如果type是music或video,则要提供数据链接,默认为空 title: document.title, // 分享标题 desc: 'Let\'s play together!', // 分享描述 link: shareLink + (cc.sys.localStorage.getItem('boundRoomId') ? "" : ("?expectedRoomId=" + cc.sys.localStorage.getItem('boundRoomId'))), imgUrl: origUrl + "/favicon.ico", // 分享图标 success: function() { // 设置成功 } }; const menuShareTimelineObj = { title: document.title, // 分享标题 link: shareLink + (cc.sys.localStorage.getItem('boundRoomId') ? "" : ("?expectedRoomId=" + cc.sys.localStorage.getItem('boundRoomId'))), imgUrl: origUrl + "/favicon.ico", // 分享图标 success: function() {} }; const wxConfigUrl = (window.isUsingWebkitWechatKernel() ? window.atFirstLocationHref : window.location.href); //接入微信登录接口 NetworkUtils.ajax({ "url": backendAddress.PROTOCOL + '://' + backendAddress.HOST + ':' + backendAddress.PORT + constants.ROUTE_PATH.API + constants.ROUTE_PATH.PLAYER + constants.ROUTE_PATH.VERSION + constants.ROUTE_PATH.WECHAT + constants.ROUTE_PATH.JSCONFIG, type: "POST", data: { "url": wxConfigUrl, "intAuthToken": selfPlayer.intAuthToken, }, success: function(res) { if (constants.RET_CODE.OK != res.ret) { console.warn("Failed to get `wsConfig`: ", res); return; } const jsConfig = res.jsConfig; const configData = { debug: CC_DEBUG, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId: jsConfig.app_id, // 必填,公众号的唯一标识 timestamp: jsConfig.timestamp.toString(), // 必填,生成签名的时间戳 nonceStr: jsConfig.nonce_str, // 必填,生成签名的随机串 jsApiList: ['onMenuShareAppMessage', 'onMenuShareTimeline'], signature: jsConfig.signature, // 必填,签名 }; console.log("config url: ", wxConfigUrl); console.log("wx.config: ", configData); wx.config(configData); console.log("Current window.location.href: ", window.location.href); wx.ready(function() { console.log("Here is wx.ready.") wx.onMenuShareAppMessage(updateAppMsgShareDataObj); wx.onMenuShareTimeline(menuShareTimelineObj); }); wx.error(function(res) { console.error("wx config fails and error is ", JSON.stringify(res)); }); }, error: function(xhr, status, errMsg) { console.error("Failed to get `wsConfig`: ", errMsg); }, }); }, update(dt) { const self = this; if (null != wxDownloader && 0 < wxDownloader.totalBytesExpectedToWriteForAllTasks) { self.writtenBytes.string = wxDownloader.totalBytesWrittenForAllTasks; self.expectedToWriteBytes.string = wxDownloader.totalBytesExpectedToWriteForAllTasks; self.downloadProgress.progress = 1.0*wxDownloader.totalBytesWrittenForAllTasks/wxDownloader.totalBytesExpectedToWriteForAllTasks; } const totalUrlsToHandle = (wxDownloader.immediateHandleItemCount + wxDownloader.immediateReadFromLocalCount + wxDownloader.immediatePackDownloaderCount); const totalUrlsHandled = (wxDownloader.immediateHandleItemCompleteCount + wxDownloader.immediateReadFromLocalCompleteCount + wxDownloader.immediatePackDownloaderCompleteCount); if (null != wxDownloader && 0 < totalUrlsToHandle) { self.handledUrlsCount.string = totalUrlsHandled; self.toHandledUrlsCount.string = totalUrlsToHandle; self.handlerProgress.progress = 1.0*totalUrlsHandled/totalUrlsToHandle; } } });