diff --git a/assets/js/credits.js b/assets/js/credits.js index 493b91b4..970bf324 100644 --- a/assets/js/credits.js +++ b/assets/js/credits.js @@ -2,7 +2,7 @@ * AKSRT Credits Frontend * * 功能: - * - 签到 panel:拉取 /signin/today,按钮触发 POST /signin + * - 签到 panel:跳转至全局签到按钮 * - revenue panel:拉取 /revenue/me,复制推广链接 * * 仅在 page-profile 页面 + 启用积分时加载。 @@ -15,7 +15,6 @@ return; } var activePoll = null; - var signinBusy = false; function api(path, options) { return window.AksrtRequest.request(path, options || {}).then(function (response) { @@ -35,168 +34,6 @@ }); } - // ===== 签到 ===== - function initSignin() { - var buttons = Array.prototype.slice.call(document.querySelectorAll('[data-signin-btn]')); - var freshButtons = buttons.filter(function (button) { - return button.dataset.aksrtSigninInitialized !== '1'; - }); - if (!freshButtons.length) return; - freshButtons.forEach(function (button) { - button.dataset.aksrtSigninInitialized = '1'; - }); - - function statuses() { - return document.querySelectorAll('[data-signin-status]'); - } - - function currentButtons() { - return document.querySelectorAll('[data-signin-btn]'); - } - - function setStatus(text) { - statuses().forEach(function (status) { status.textContent = text; }); - } - - function setStatusHtml(html) { - statuses().forEach(function (status) { status.innerHTML = html; }); - } - - function setButtonState(state) { - currentButtons().forEach(function (button) { - var label = button.querySelector('[data-signin-label]'); - var text = state === 'done' - ? button.getAttribute('data-signin-done-label') || '已签到' - : (state === 'busy' ? '…' : button.getAttribute('data-signin-idle-label') || '签到'); - if (label) label.textContent = text; - button.disabled = state !== 'idle'; - button.classList.toggle('is-done', state === 'done'); - }); - } - - function refresh() { - api('/signin/today').then(function (r) { - if (!r.ok) { - var message = (r.json && r.json.message) || '签到状态加载失败 (HTTP ' + r.status + ')'; - setStatus(message); - notyf(message, 'danger'); - return; - } - var j = r.json; - if (!j.enabled) { - setStatus('签到功能未启用'); - setButtonState('done'); - return; - } - if (j.signed) { - setStatusHtml('已签到 ' + j.streak + ' 天 +' + j.reward_today); - setButtonState('done'); - } else { - setStatusHtml('连续 ' + j.streak + ' 天,+' + j.reward_if_signin); - setButtonState('idle'); - } - }).catch(function (err) { - if (window.console) console.error('[AKSRT Credits] /signin/today 请求失败:', err); - setStatus('加载失败,请刷新重试'); - notyf('签到状态加载失败,请刷新重试', 'danger'); - }); - } - - function collectCaptcha() { - var el = document.querySelector('[data-signin-captcha]'); - if (!el) return {}; - var out = {}; - var keys = [ - 'captcha_verified', 'cf_turnstile_response', - 'tencent_ticket', 'tencent_randstr', 'tencent_sid', - 'geetest_lot_number', 'geetest_captcha_output', 'geetest_pass_token', 'geetest_gen_time', - 'aliyun_captcha_verify_param', - ]; - keys.forEach(function (k) { - var inp = el.querySelector('input[name="' + k + '"]'); - if (inp && inp.value !== '') out[k] = inp.value; - }); - return out; - } - - function resetCaptcha() { - try { - var state = window.AKSRTCaptchaState; - var scene = state && state.scenes && state.scenes.signin; - if (scene && typeof scene.reset === 'function') scene.reset(); - } catch (_) {} - } - - function triggerCaptcha(onSuccess, onFail) { - var mount = document.querySelector('[data-signin-captcha]'); - var state = window.AKSRTCaptchaState; - var scene = state && state.scenes && state.scenes.signin; - - if (scene && typeof scene.verify === 'function') { - scene.verify(function (ok) { - if (ok === false) { - if (typeof onFail === 'function') onFail('已取消验证'); - return; - } - onSuccess(); - }); - return; - } - - if (mount) { - if (typeof onFail === 'function') onFail('人机验证加载中,请稍候再试'); - return; - } - - onSuccess(); - } - - freshButtons.forEach(function (button) { - button.addEventListener('click', function () { - if (signinBusy) return; - signinBusy = true; - setButtonState('busy'); - - triggerCaptcha(function () { - doSignin(); - }, function (msg) { - signinBusy = false; - resetCaptcha(); - if (msg) notyf(msg, 'danger'); - setButtonState('idle'); - }); - }); - }); - - function doSignin() { - notyf('正在签到…', 'load', '', 'aksrt-balance-signin-toast'); - api('/signin', { - method: 'POST', - body: JSON.stringify({ captcha: collectCaptcha() }) - }).then(function (r) { - signinBusy = false; - if (!r.ok) { - resetCaptcha(); - notyf((r.json && r.json.message) || '签到失败', 'danger', 5, 'aksrt-balance-signin-toast'); - setButtonState('idle'); - return; - } - notyf(r.json.message || '签到成功', 'success', 5, 'aksrt-balance-signin-toast'); - var balanceEls = document.querySelectorAll('[data-credits-value]'); - balanceEls.forEach(function (el) { el.textContent = r.json.balance.toLocaleString(); }); - setButtonState('done'); - refresh(); - }).catch(function () { - signinBusy = false; - resetCaptcha(); - notyf('网络错误,请重试', 'danger', 5, 'aksrt-balance-signin-toast'); - setButtonState('idle'); - }); - } - - refresh(); - } - // Task-list "去签到" → scroll + click the real asset-card sign-in button. function initSigninGoto() { document.querySelectorAll('[data-aksrt-signin-goto]').forEach(function (el) { @@ -857,7 +694,6 @@ window.AKSRTWithdrawInit = initWithdraw; function bootAssets() { - initSignin(); initSigninGoto(); initAffiliateRevenue(); initVipShop(); @@ -879,7 +715,6 @@ if (window.AksrtFrontend && typeof window.AksrtFrontend.register === 'function') { window.AksrtFrontend.register('credits', function () { - initSignin(); initSigninGoto(); initBalanceCharge(); initTransfer(); diff --git a/assets/js/signin.js b/assets/js/signin.js new file mode 100644 index 00000000..adba6991 --- /dev/null +++ b/assets/js/signin.js @@ -0,0 +1,200 @@ +/** + * AKSRT global sign-in controller. + * + * All sign-in buttons share one REST request, captcha and toast flow. + */ +(function () { + 'use strict'; + + if (!window.aksrtData || !window.AksrtRequest) { + return; + } + + var signinBusy = false; + + function api(path, options) { + return window.AksrtRequest.request(path, options || {}).then(function (response) { + return { + ok: response.ok, + status: response.status, + json: response.json || {} + }; + }).catch(function (error) { + var response = error && error.response ? error.response : {}; + return { + ok: false, + status: error && error.status ? error.status : (response.status || 0), + json: error && error.json ? error.json : (response.json || { message: error && error.message }) + }; + }); + } + + function initSignin() { + var buttons = Array.prototype.slice.call(document.querySelectorAll('[data-signin-btn]')); + var freshButtons = buttons.filter(function (button) { + return button.dataset.aksrtSigninInitialized !== '1'; + }); + if (!freshButtons.length) return; + + freshButtons.forEach(function (button) { + button.dataset.aksrtSigninInitialized = '1'; + }); + + function statuses() { + return document.querySelectorAll('[data-signin-status]'); + } + + function currentButtons() { + return document.querySelectorAll('[data-signin-btn]'); + } + + function setStatus(text) { + statuses().forEach(function (status) { + status.textContent = text; + }); + } + + function setStatusHtml(html) { + statuses().forEach(function (status) { + status.innerHTML = html; + }); + } + + function setButtonState(state) { + currentButtons().forEach(function (button) { + var label = button.querySelector('[data-signin-label]'); + var text = state === 'done' + ? button.getAttribute('data-signin-done-label') || '已签到' + : (state === 'busy' ? '…' : button.getAttribute('data-signin-idle-label') || '签到'); + if (label) label.textContent = text; + button.disabled = state !== 'idle'; + button.classList.toggle('is-done', state === 'done'); + }); + } + + function refresh() { + api('/signin/today').then(function (result) { + if (!result.ok) { + var message = (result.json && result.json.message) || '签到状态加载失败 (HTTP ' + result.status + ')'; + setStatus(message); + notyf(message, 'danger'); + return; + } + + var data = result.json; + if (!data.enabled) { + setStatus('签到功能未启用'); + setButtonState('done'); + return; + } + if (data.signed) { + setStatusHtml('已签到 ' + data.streak + ' 天 +' + data.reward_today); + setButtonState('done'); + return; + } + setStatusHtml('连续 ' + data.streak + ' 天,+' + data.reward_if_signin); + setButtonState('idle'); + }); + } + + function captchaMount() { + return document.querySelector('[data-signin-captcha]'); + } + + function collectCaptcha() { + var mount = captchaMount(); + if (!mount) return {}; + var payload = {}; + var keys = [ + 'captcha_verified', 'cf_turnstile_response', + 'tencent_ticket', 'tencent_randstr', 'tencent_sid', + 'geetest_lot_number', 'geetest_captcha_output', 'geetest_pass_token', 'geetest_gen_time', + 'aliyun_captcha_verify_param' + ]; + keys.forEach(function (key) { + var input = mount.querySelector('input[name="' + key + '"]'); + if (input && input.value !== '') payload[key] = input.value; + }); + return payload; + } + + function resetCaptcha() { + var state = window.AKSRTCaptchaState; + var scene = state && state.scenes && state.scenes.signin; + if (scene && typeof scene.reset === 'function') scene.reset(); + } + + function triggerCaptcha(onSuccess, onFail) { + var mount = captchaMount(); + var state = window.AKSRTCaptchaState; + var scene = state && state.scenes && state.scenes.signin; + + if (scene && typeof scene.verify === 'function') { + scene.verify(function (ok) { + if (ok === false) { + onFail('已取消验证'); + return; + } + onSuccess(); + }); + return; + } + if (mount) { + onFail('人机验证加载中,请稍候再试'); + return; + } + onSuccess(); + } + + function doSignin() { + notyf('正在签到…', 'load', '', 'aksrt-balance-signin-toast'); + api('/signin', { + method: 'POST', + body: JSON.stringify({ captcha: collectCaptcha() }) + }).then(function (result) { + signinBusy = false; + if (!result.ok) { + resetCaptcha(); + notyf((result.json && result.json.message) || '签到失败', 'danger', 5, 'aksrt-balance-signin-toast'); + setButtonState('idle'); + return; + } + + notyf(result.json.message || '签到成功', 'success', 5, 'aksrt-balance-signin-toast'); + document.querySelectorAll('[data-credits-value]').forEach(function (element) { + element.textContent = Number(result.json.balance || 0).toLocaleString(); + }); + setButtonState('done'); + refresh(); + }); + } + + freshButtons.forEach(function (button) { + button.addEventListener('click', function (event) { + event.preventDefault(); + if (signinBusy || button.disabled) return; + + signinBusy = true; + setButtonState('busy'); + triggerCaptcha(doSignin, function (message) { + signinBusy = false; + resetCaptcha(); + if (message) notyf(message, 'danger'); + setButtonState('idle'); + }); + }); + }); + + refresh(); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initSignin); + } else { + initSignin(); + } + + if (window.AksrtFrontend && typeof window.AksrtFrontend.register === 'function') { + window.AksrtFrontend.register('signin', initSignin); + } +})(); diff --git a/inc/helpers/aksrt-header-userbox.php b/inc/helpers/aksrt-header-userbox.php index b9ce29e3..d0651ca0 100644 --- a/inc/helpers/aksrt-header-userbox.php +++ b/inc/helpers/aksrt-header-userbox.php @@ -9,7 +9,7 @@ defined('ABSPATH') || exit; if (!function_exists('aksrt_header_user_checkin_btn')) : /** - * Check-in CTA for header dropdown and compact card placements (uses REST /aksrt/v1/signin). + * Check-in CTA for header dropdown and compact card placements. * * @param int $user_id Current user ID. * @param bool $compact Render a short card-friendly label. @@ -26,37 +26,48 @@ if (!function_exists('aksrt_header_user_checkin_btn')) : return ''; } - // Lightweight today status via private API if available. - $signed = false; - try { - $signin = \AKSRT\Credits\Signin::get_instance(); - // Prefer public method if present. - if (method_exists($signin, 'is_signed_today')) { - $signed = (bool) $signin->is_signed_today($user_id); - } else { - // Fallback: direct meta/table probe via rest_today logic is heavy; use option day key. - $day = wp_date('Y-m-d'); - $signed = (bool) get_user_meta($user_id, 'aksrt_signin_' . $day, true); - } - } catch (\Throwable $e) { - $signed = false; - } + $signed = \AKSRT\Credits\Signin::get_instance()->is_signed_today($user_id); $compact_class = $compact ? ' aksrt-header-checkin--compact' : ''; if ($signed) { - return ''; } - return ''; } endif; +if (!function_exists('aksrt_render_signin_captcha')) : + /** + * Render the single global captcha mount used by every sign-in button. + */ + function aksrt_render_signin_captcha(): void { + if ( + !is_user_logged_in() + || !class_exists('\AKSRT\Credits\Signin') + || !class_exists('\AKSRT\Settings\Settings') + || empty(\AKSRT\Settings\Settings::get_signin_reward_settings()['enabled']) + || !function_exists('aksrt_is_captcha_enabled_for') + || !function_exists('aksrt_render_captcha') + || !aksrt_is_captcha_enabled_for('signin') + ) { + return; + } + ?> +
+ +
+ - -
- -
- diff --git a/src/Core/ScriptsPart1b.php b/src/Core/ScriptsPart1b.php index 9531c596..76bfc6d2 100644 --- a/src/Core/ScriptsPart1b.php +++ b/src/Core/ScriptsPart1b.php @@ -139,6 +139,10 @@ trait ScriptsPart1b { $affiliate_on = is_user_logged_in() && class_exists('\\AKSRT\\Settings\\Settings') && !empty(\AKSRT\Settings\Settings::get_affiliate_settings()['enabled']); + $signin_on = is_user_logged_in() + && class_exists('\AKSRT\Credits\Signin') + && class_exists('\AKSRT\Settings\Settings') + && !empty(\AKSRT\Settings\Settings::get_signin_reward_settings()['enabled']); $vip_coordinator = get_template_directory() . '/assets/js/vip-purchase-coordinator.js'; if (($vip_on || $credits_on || $affiliate_on) && file_exists($vip_coordinator)) { wp_enqueue_script( @@ -764,6 +768,17 @@ trait ScriptsPart1b { } } + if ($signin_on) { + $signin_js_path = get_template_directory() . '/assets/js/signin.js'; + wp_enqueue_script( + 'aksrt-signin', + $base_uri . '/assets/js/signin.js', + ['aksrt-core-request', 'aksrt-notyf'], + file_exists($signin_js_path) ? (string) filemtime($signin_js_path) : $version, + true + ); + } + // Badges UI: user center + public profile (zibll medal modal/list). if (class_exists('\AKSRT\User\Badges') && \AKSRT\User\Badges::enabled()) { $is_badge_surface = $is_profile_page diff --git a/src/Credits/Signin.php b/src/Credits/Signin.php index 464fab25..ab41b86c 100644 --- a/src/Credits/Signin.php +++ b/src/Credits/Signin.php @@ -301,7 +301,7 @@ class Signin { $html .= '
' . esc_html__('今日已签到', 'aksrt'); $html .= '
+' . esc_html((string) (int) ($rec['reward'] ?? 0)) . '
'; } else { - $html .= ''; + $html .= ''; } $html .= ''; diff --git a/tests/widget-system/toast-contract.test.cjs b/tests/widget-system/toast-contract.test.cjs index 6d1e786d..7156200b 100644 --- a/tests/widget-system/toast-contract.test.cjs +++ b/tests/widget-system/toast-contract.test.cjs @@ -195,10 +195,13 @@ test('notyf CSS and WordPress enqueue match the reference contract', () => { test('balance center actions use direct fixed-id notyf flows', () => { const credits = read('assets/js/credits.js'); + const signinController = read('assets/js/signin.js'); const scripts = read('src/Core/ScriptsPart1b.php'); const css = read('assets/css/routes/user-center/credits.css'); const shell = read('assets/css/routes/user-center/shell.css'); const header = read('inc/helpers/user-center-renderers/profile-header.php'); + const headerUserbox = read('inc/helpers/aksrt-header-userbox.php'); + const signinService = read('src/Credits/Signin.php'); const signin = read('inc/helpers/user-center-renderers/credits-signin.php'); const context = read('inc/helpers/aksrt-user-center.php'); const renderers = [ @@ -217,7 +220,7 @@ test('balance center actions use direct fixed-id notyf flows', () => { } const flows = [ - [section('initSignin', 'initSigninGoto'), 'aksrt-balance-signin-toast'], + [signinController, 'aksrt-balance-signin-toast'], [section('initBalanceCharge', 'initTransfer'), 'aksrt-balance-charge-toast'], [section('initTransfer', 'initPointsExchange'), 'aksrt-balance-transfer-toast'], [section('initPointsExchange', 'initWithdraw'), 'aksrt-balance-points-toast'], @@ -232,16 +235,24 @@ test('balance center actions use direct fixed-id notyf flows', () => { }); assert.match(credits, /notyf\('等待支付结果…', 'load', '', 'aksrt-balance-charge-payment-toast'\)/); - assert.match(credits, /querySelectorAll\('\[data-signin-btn\]'\)/); - assert.match(credits, /freshButtons\.forEach[\s\S]*?addEventListener\('click'/); + assert.match(signinController, /querySelectorAll\('\[data-signin-btn\]'\)/); + assert.match(signinController, /freshButtons\.forEach[\s\S]*?addEventListener\('click'/); assert.match(credits, /onTimeout:[\s\S]*?notyf\([^;]+?'danger'[^;]+?'aksrt-balance-charge-payment-toast'\)/); - assert.match(credits, /register\('credits',[\s\S]*?initSignin\(\);[\s\S]*?initBalanceCharge\(\);[\s\S]*?initTransfer\(\);[\s\S]*?initPointsExchange\(\);[\s\S]*?initWithdraw\(\);/); - assert.match(credits, /aksrtSigninInitialized[\s\S]*?aksrtPointsExchangeInitialized[\s\S]*?aksrtWithdrawInitialized/); + assert.match(credits, /register\('credits',[\s\S]*?initSigninGoto\(\);[\s\S]*?initBalanceCharge\(\);[\s\S]*?initTransfer\(\);[\s\S]*?initPointsExchange\(\);[\s\S]*?initWithdraw\(\);/); + assert.doesNotMatch(credits, /function initSignin\(|\/signin\/today|POST \/signin/); + assert.match(signinController, /aksrtSigninInitialized/); + assert.match(credits, /aksrtPointsExchangeInitialized[\s\S]*?aksrtWithdrawInitialized/); assert.match(scripts, /'aksrt-credits',[\s\S]*?\/assets\/js\/credits\.js',[\s\S]*?\[[^\]]*'aksrt-notyf'[^\]]*\]/); + assert.match(scripts, /\$signin_on\s*=\s*is_user_logged_in\(\)[\s\S]*?'aksrt-signin',[\s\S]*?\/assets\/js\/signin\.js',[\s\S]*?'aksrt-core-request'[\s\S]*?'aksrt-notyf'/); assert.ok(header.indexOf('data-signin-btn') < header.indexOf('uc-header__message-btn')); assert.match(header, /uc-header__message-btn[^>]+aria-label="<\?php esc_attr_e\('消息通知'/); assert.doesNotMatch(header, /]*><\/i>\s*<\?php esc_html_e\('消息通知'/); - assert.match(header, /data-signin-captcha[\s\S]*?aksrt_render_captcha\('signin'\)/); + assert.doesNotMatch(header, /data-signin-captcha|aksrt_render_captcha\('signin'\)/); + assert.match(headerUserbox, /data-signin-btn[\s\S]*?data-signin-label/); + assert.match(headerUserbox, /add_action\('wp_footer', 'aksrt_render_signin_captcha'\)/); + assert.match(headerUserbox, /data-signin-captcha[\s\S]*?aksrt_render_captcha\('signin'\)/); + assert.match(signinService, /aksrt-checkin-modal__btn[^>]+data-signin-btn/); + assert.doesNotMatch(headerUserbox + signinService, /data-aksrt-checkin/); assert.doesNotMatch(signin, /data-signin-captcha|aksrt_render_captcha/); assert.match(context, /'signin_enabled'\s*=>\s*\$signin_enabled/); assert.match(context, /'signin_signed_today'\s*=>\s*\$signin_signed_today/);