diff --git a/assets/js/signin.js b/assets/js/signin.js index cf8e7933..3298f287 100644 --- a/assets/js/signin.js +++ b/assets/js/signin.js @@ -6,7 +6,7 @@ (function () { 'use strict'; - if (!window.aksrtData || !window.AksrtRequest) { + if (!window.aksrtData) { return; } @@ -132,14 +132,27 @@ signinBusy = true; setButtonState('busy'); + var captchaRequired = !!window.aksrtData.captchaSignin; var captcha = window.AKSRTCaptcha; var mount = captchaMount(); - (captcha ? captcha.run('signin', mount) : Promise.resolve({})).then(function (payload) { + + if (captchaRequired && (!captcha || !mount)) { + signinBusy = false; + notyf('人机验证未正确加载,请刷新页面后重试', 'danger'); + setButtonState('idle'); + return; + } + if (!captchaRequired) { + doSignin({}); + return; + } + + captcha.run('signin', mount).then(function (payload) { if (payload === null) throw new Error('已取消验证'); doSignin(payload); }).catch(function (error) { signinBusy = false; - if (captcha) captcha.reset('signin', mount); + captcha.reset('signin', mount); notyf((error && error.message) || '人机验证加载中,请稍候再试', 'danger'); setButtonState('idle'); }); @@ -149,5 +162,15 @@ refresh(); } - jQuery(document).off('auto_fun.aksrt-signin').on('auto_fun.aksrt-signin', initSignin); + function boot() { + if (!window.AksrtRequest) return; + jQuery(document).off('auto_fun.aksrt-signin').on('auto_fun.aksrt-signin', initSignin); + initSignin(); + } + + if (typeof tbquire === 'function') { + tbquire(['main'], boot); + } else { + boot(); + } })(); diff --git a/src/Core/ScriptsPart1c.php b/src/Core/ScriptsPart1c.php index e51a01b1..586d5dd7 100644 --- a/src/Core/ScriptsPart1c.php +++ b/src/Core/ScriptsPart1c.php @@ -468,6 +468,7 @@ trait ScriptsPart1c { 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('aksrt_public_nonce'), 'restNonce' => wp_create_nonce('wp_rest'), + 'captchaSignin' => function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('signin'), 'visibilityNonce' => wp_create_nonce('aksrt_visibility'), 'siteUrl' => home_url('/'), 'themeUrl' => AKSRT_URI, diff --git a/tests/widget-system/captcha-runtime-harness.test.cjs b/tests/widget-system/captcha-runtime-harness.test.cjs index 34243b0e..3fa0e673 100644 --- a/tests/widget-system/captcha-runtime-harness.test.cjs +++ b/tests/widget-system/captcha-runtime-harness.test.cjs @@ -8,6 +8,7 @@ const vm = require('node:vm'); const root = path.resolve(__dirname, '..', '..'); const runtime = fs.readFileSync(path.join(root, 'assets/js/captcha-front.js'), 'utf8'); +const signinSource = fs.readFileSync(path.join(root, 'assets/js/signin.js'), 'utf8'); function createInput(name, value = '') { return { name, value, remove() {}, nodeType: 1 }; @@ -107,3 +108,59 @@ test('captcha run is single-flight until the owning business request resets', as api.reset('login', container); assert.equal(await second, null); }); + +test('sign-in waits for main before binding and invokes the signin captcha', async () => { + const listeners = {}; + const label = { textContent: '' }; + const button = { + dataset: {}, + disabled: false, + classList: { toggle() {} }, + addEventListener(name, handler) { listeners[name] = handler; }, + querySelector(selector) { return selector === '[data-signin-label]' ? label : null; }, + getAttribute(name) { + if (name === 'data-signin-idle-label') return '签到'; + if (name === 'data-signin-done-label') return '已签到'; + return ''; + }, + }; + const mount = { nodeType: 1 }; + const document = { + querySelector(selector) { return selector === '[data-signin-captcha]' ? mount : null; }, + querySelectorAll(selector) { return selector === '[data-signin-btn]' ? [button] : []; }, + }; + const jqueryApi = { off() { return this; }, on() { return this; } }; + const jQuery = () => jqueryApi; + const window = { aksrtData: { captchaSignin: true } }; + let mainReady; + let captchaCall = null; + const tbquire = (deps, callback) => { + assert.deepEqual(Array.from(deps), ['main']); + mainReady = callback; + }; + + vm.runInNewContext(signinSource, { + window, document, jQuery, tbquire, notyf() {}, Promise, JSON, Number, + }); + assert.equal(listeners.click, undefined); + assert.equal(typeof mainReady, 'function'); + + window.AksrtRequest = { + request(endpoint) { + if (endpoint === '/signin/today') return Promise.resolve({ ok: true, status: 200, json: { enabled: true, signed: false, streak: 0, reward_if_signin: 1 } }); + if (endpoint === '/signin') return Promise.resolve({ ok: true, status: 200, json: { message: '签到成功', balance: 1 } }); + throw new Error('unexpected endpoint ' + endpoint); + }, + }; + window.AKSRTCaptcha = { + run(scene, scope) { captchaCall = { scene, scope }; return Promise.resolve({ aksrt_captcha_proof: 'proof' }); }, + reset() {}, + }; + + mainReady(); + await new Promise((resolve) => setImmediate(resolve)); + assert.equal(typeof listeners.click, 'function'); + listeners.click({ preventDefault() {} }); + await new Promise((resolve) => setImmediate(resolve)); + assert.deepEqual(captchaCall, { scene: 'signin', scope: mount }); +});