From 5d4e0d230a40879d39f332d2cfb37d90ce27c03e Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Tue, 5 May 2026 23:33:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20CPA=20=E6=89=8B?= =?UTF-8?q?=E6=9C=BA=E5=8F=B7=E6=B3=A8=E5=86=8C=E6=8F=90=E7=A4=BA=E9=80=BB?= =?UTF-8?q?=E8=BE=91=E5=8F=8A=E7=9B=B8=E5=85=B3=E6=B5=8B=E8=AF=95=E7=94=A8?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sidepanel/sidepanel.js | 124 ++++++++++++++++-- tests/sidepanel-icloud-provider.test.js | 4 + ...epanel-phone-verification-settings.test.js | 92 +++++++++++++ 3 files changed, 211 insertions(+), 9 deletions(-) diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 5e81e45..dcb921d 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -708,6 +708,8 @@ const DEFAULT_MAIL_2925_MODE = MAIL_2925_MODE_PROVIDE; const NEW_USER_GUIDE_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-new-user-guide-prompt-dismissed'; const AUTO_SKIP_FAILURES_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-skip-failures-prompt-dismissed'; const AUTO_RUN_FALLBACK_RISK_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-run-fallback-risk-prompt-dismissed'; +const CPA_PHONE_SIGNUP_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-cpa-phone-signup-prompt-dismissed'; +const CPA_PHONE_SIGNUP_WARNING_MESSAGE = 'CPA 未适配手机号注册模式,认证成功后无法使用。请使用 SUB2API,或者认证成功后重新登录一遍进行解决。'; const PHONE_VERIFICATION_SECTION_EXPANDED_STORAGE_KEY = 'multipage-phone-verification-section-expanded'; let phoneVerificationSectionExpanded = false; @@ -1788,10 +1790,73 @@ function setAutoRunFallbackRiskPromptDismissed(dismissed) { setPromptDismissed(AUTO_RUN_FALLBACK_RISK_PROMPT_DISMISSED_STORAGE_KEY, dismissed); } +function isCpaPhoneSignupPromptDismissed() { + return isPromptDismissed(CPA_PHONE_SIGNUP_PROMPT_DISMISSED_STORAGE_KEY); +} + +function setCpaPhoneSignupPromptDismissed(dismissed) { + setPromptDismissed(CPA_PHONE_SIGNUP_PROMPT_DISMISSED_STORAGE_KEY, dismissed); +} + function shouldWarnAutoRunFallbackRisk(totalRuns, autoRunSkipFailures) { return totalRuns >= AUTO_RUN_FALLBACK_RISK_WARNING_MIN_RUNS; } +function shouldWarnCpaPhoneSignup(signupMethod = null, panelMode = null) { + const resolvedSignupMethod = normalizeSignupMethod( + signupMethod ?? ( + typeof getSelectedSignupMethod === 'function' + ? getSelectedSignupMethod() + : DEFAULT_SIGNUP_METHOD + ) + ); + const resolvedPanelMode = normalizePanelMode( + panelMode ?? ( + typeof getSelectedPanelMode === 'function' + ? getSelectedPanelMode() + : 'cpa' + ) + ); + + return resolvedSignupMethod === SIGNUP_METHOD_PHONE + && resolvedPanelMode === 'cpa' + && !isCpaPhoneSignupPromptDismissed(); +} + +async function openCpaPhoneSignupWarningModal() { + const result = await openConfirmModalWithOption({ + title: 'CPA 手机号注册提醒', + message: CPA_PHONE_SIGNUP_WARNING_MESSAGE, + confirmLabel: '继续', + optionLabel: '不再提醒', + }); + + return { + confirmed: result.confirmed, + dismissPrompt: result.optionChecked, + }; +} + +async function confirmCpaPhoneSignupIfNeeded(options = {}) { + const signupMethod = Object.prototype.hasOwnProperty.call(options, 'signupMethod') + ? options.signupMethod + : null; + const panelMode = Object.prototype.hasOwnProperty.call(options, 'panelMode') + ? options.panelMode + : null; + + if (!shouldWarnCpaPhoneSignup(signupMethod, panelMode)) { + return true; + } + + const result = await openCpaPhoneSignupWarningModal(); + if (result.dismissPrompt) { + setCpaPhoneSignupPromptDismissed(true); + } + + return result.confirmed; +} + async function openAutoSkipFailuresConfirmModal() { const result = await openConfirmModalWithOption({ title: '自动重试说明', @@ -6633,6 +6698,21 @@ function normalizeSignupMethod(value = '') { : 'email'; } +function normalizePanelMode(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + if (normalized === 'sub2api' || normalized === 'codex2api') { + return normalized; + } + return 'cpa'; +} + +function getSelectedPanelMode() { + const selectedValue = typeof selectPanelMode !== 'undefined' && selectPanelMode + ? selectPanelMode.value + : (typeof latestState !== 'undefined' ? latestState?.panelMode : ''); + return normalizePanelMode(selectedValue || 'cpa'); +} + function getSelectedSignupMethod() { const activeButton = signupMethodButtons.find((button) => button.classList.contains('is-active')); return normalizeSignupMethod(activeButton?.dataset.signupMethod || latestState?.signupMethod || DEFAULT_SIGNUP_METHOD); @@ -7652,7 +7732,7 @@ function applySettingsState(state) { inputVpsUrl.value = state?.vpsUrl || ''; inputVpsPassword.value = state?.vpsPassword || ''; setLocalCpaStep9Mode(state?.localCpaStep9Mode); - selectPanelMode.value = state?.panelMode || 'cpa'; + selectPanelMode.value = normalizePanelMode(state?.panelMode || 'cpa'); inputSub2ApiUrl.value = state?.sub2apiUrl || ''; inputSub2ApiEmail.value = state?.sub2apiEmail || ''; inputSub2ApiPassword.value = state?.sub2apiPassword || ''; @@ -9409,8 +9489,12 @@ async function handleDeleteSub2ApiGroup(groupName) { } function updatePanelModeUI() { - const useSub2Api = selectPanelMode.value === 'sub2api'; - const useCodex2Api = selectPanelMode.value === 'codex2api'; + const panelMode = getSelectedPanelMode(); + if (selectPanelMode) { + selectPanelMode.value = panelMode; + } + const useSub2Api = panelMode === 'sub2api'; + const useCodex2Api = panelMode === 'codex2api'; const useCpa = !useSub2Api && !useCodex2Api; rowVpsUrl.style.display = useCpa ? '' : 'none'; rowVpsPassword.style.display = useCpa ? '' : 'none'; @@ -10714,8 +10798,8 @@ async function startAutoRunFromCurrentSettings() { const delayMinutes = normalizeAutoDelayMinutes(inputAutoDelayMinutes.value); inputAutoDelayMinutes.value = String(delayMinutes); btnAutoRun.innerHTML = delayEnabled - ? ' 璁″垝涓?..' - : ' 杩愯涓?..'; + ? ' 计划中...' + : ' 运行中...'; const response = await chrome.runtime.sendMessage({ type: delayEnabled ? 'SCHEDULE_AUTO_RUN' : 'AUTO_RUN', source: 'sidepanel', @@ -11133,7 +11217,20 @@ checkboxAutoDeleteIcloud?.addEventListener('change', () => { saveSettings({ silent: true }).catch(() => { }); }); -selectPanelMode.addEventListener('change', () => { +selectPanelMode.addEventListener('change', async () => { + const previousPanelMode = normalizePanelMode(latestState?.panelMode || 'cpa'); + const nextPanelMode = normalizePanelMode(selectPanelMode.value); + selectPanelMode.value = nextPanelMode; + const confirmed = await confirmCpaPhoneSignupIfNeeded({ + signupMethod: getSelectedSignupMethod(), + panelMode: nextPanelMode, + }); + if (!confirmed) { + selectPanelMode.value = previousPanelMode; + updatePanelModeUI(); + return; + } + syncLatestState({ panelMode: nextPanelMode }); updatePanelModeUI(); markSettingsDirty(true); saveSettings({ silent: true }).catch(() => { }); @@ -12045,11 +12142,20 @@ inputPhoneVerificationEnabled?.addEventListener('change', () => { }); signupMethodButtons.forEach((button) => { - button.addEventListener('click', () => { + button.addEventListener('click', async () => { if (button.disabled) { return; } - setSignupMethod(button.dataset.signupMethod); + const nextSignupMethod = normalizeSignupMethod(button.dataset.signupMethod); + const confirmed = await confirmCpaPhoneSignupIfNeeded({ + signupMethod: nextSignupMethod, + panelMode: getSelectedPanelMode(), + }); + if (!confirmed) { + updateSignupMethodUI(); + return; + } + setSignupMethod(nextSignupMethod); updateSignupMethodUI(); markSettingsDirty(true); saveSettings({ silent: true }).catch(() => { }); @@ -12636,7 +12742,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { setLocalCpaStep9Mode(message.payload.localCpaStep9Mode); } if (message.payload.panelMode !== undefined) { - selectPanelMode.value = message.payload.panelMode || 'cpa'; + selectPanelMode.value = normalizePanelMode(message.payload.panelMode || 'cpa'); updatePanelModeUI(); } if ( diff --git a/tests/sidepanel-icloud-provider.test.js b/tests/sidepanel-icloud-provider.test.js index 000f44a..2e9a922 100644 --- a/tests/sidepanel-icloud-provider.test.js +++ b/tests/sidepanel-icloud-provider.test.js @@ -456,6 +456,10 @@ function syncAutoRunState() {} function syncPasswordField() {} function renderStepStatuses() {} function setLocalCpaStep9Mode() {} +function normalizePanelMode(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return normalized === 'sub2api' || normalized === 'codex2api' ? normalized : 'cpa'; +} function isCustomMailProvider() { return false; } function setMail2925Mode() {} function normalizeIcloudFetchMode(value) { return String(value || '') === 'always_new' ? 'always_new' : 'reuse_existing'; } diff --git a/tests/sidepanel-phone-verification-settings.test.js b/tests/sidepanel-phone-verification-settings.test.js index 1300225..90719b6 100644 --- a/tests/sidepanel-phone-verification-settings.test.js +++ b/tests/sidepanel-phone-verification-settings.test.js @@ -149,6 +149,96 @@ test('sidepanel source wires runtime signup phone field to background sync messa assert.match(sidepanelSource, /inputSignupPhone\.addEventListener\('input'[\s\S]*signupPhoneInputDirty = true/); }); +test('sidepanel warns once before using phone signup with CPA source', async () => { + assert.match( + sidepanelSource, + /signupMethodButtons\.forEach\(\(button\) => \{[\s\S]*await confirmCpaPhoneSignupIfNeeded\(\{[\s\S]*signupMethod: nextSignupMethod,[\s\S]*panelMode: getSelectedPanelMode\(\),/ + ); + assert.match( + sidepanelSource, + /selectPanelMode\.addEventListener\('change', async \(\) => \{[\s\S]*await confirmCpaPhoneSignupIfNeeded\(\{[\s\S]*signupMethod: getSelectedSignupMethod\(\),[\s\S]*panelMode: nextPanelMode,/ + ); + + const bundle = [ + extractFunction('normalizeSignupMethod'), + extractFunction('normalizePanelMode'), + extractFunction('isPromptDismissed'), + extractFunction('setPromptDismissed'), + extractFunction('isCpaPhoneSignupPromptDismissed'), + extractFunction('setCpaPhoneSignupPromptDismissed'), + extractFunction('shouldWarnCpaPhoneSignup'), + extractFunction('openCpaPhoneSignupWarningModal'), + extractFunction('confirmCpaPhoneSignupIfNeeded'), + ].join('\n'); + + const api = new Function(` +const SIGNUP_METHOD_PHONE = 'phone'; +const SIGNUP_METHOD_EMAIL = 'email'; +const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL; +const CPA_PHONE_SIGNUP_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-cpa-phone-signup-prompt-dismissed'; +const CPA_PHONE_SIGNUP_WARNING_MESSAGE = 'CPA 未适配手机号注册模式,认证成功后无法使用。请使用 SUB2API,或者认证成功后重新登录一遍进行解决。'; +const storage = new Map(); +const localStorage = { + getItem(key) { + return storage.has(key) ? storage.get(key) : null; + }, + setItem(key, value) { + storage.set(key, String(value)); + }, + removeItem(key) { + storage.delete(key); + }, +}; +let selectedSignupMethod = 'phone'; +let selectedPanelMode = 'cpa'; +let capturedOptions = null; +let modalResult = { confirmed: true, optionChecked: false }; +function getSelectedSignupMethod() { + return selectedSignupMethod; +} +function getSelectedPanelMode() { + return selectedPanelMode; +} +async function openConfirmModalWithOption(options) { + capturedOptions = options; + return modalResult; +} +${bundle} +return { + shouldWarnCpaPhoneSignup, + confirmCpaPhoneSignupIfNeeded, + getCapturedOptions() { + return capturedOptions; + }, + getDismissed() { + return localStorage.getItem(CPA_PHONE_SIGNUP_PROMPT_DISMISSED_STORAGE_KEY); + }, + setModalResult(result) { + modalResult = result; + }, +}; +`)(); + + assert.equal(api.shouldWarnCpaPhoneSignup('phone', 'cpa'), true); + assert.equal(api.shouldWarnCpaPhoneSignup('email', 'cpa'), false); + assert.equal(api.shouldWarnCpaPhoneSignup('phone', 'sub2api'), false); + assert.equal(api.shouldWarnCpaPhoneSignup('phone', 'codex2api'), false); + + const firstResult = await api.confirmCpaPhoneSignupIfNeeded({ signupMethod: 'phone', panelMode: 'cpa' }); + assert.equal(firstResult, true); + assert.equal(api.getCapturedOptions().title, 'CPA 手机号注册提醒'); + assert.equal(api.getCapturedOptions().message, 'CPA 未适配手机号注册模式,认证成功后无法使用。请使用 SUB2API,或者认证成功后重新登录一遍进行解决。'); + assert.equal(api.getCapturedOptions().confirmLabel, '继续'); + assert.equal(api.getCapturedOptions().optionLabel, '不再提醒'); + assert.equal(api.getDismissed(), null); + + api.setModalResult({ confirmed: false, optionChecked: true }); + const secondResult = await api.confirmCpaPhoneSignupIfNeeded({ signupMethod: 'phone', panelMode: 'cpa' }); + assert.equal(secondResult, false); + assert.equal(api.getDismissed(), '1'); + assert.equal(api.shouldWarnCpaPhoneSignup('phone', 'cpa'), false); +}); + test('manual step 3 uses phone identity without requiring registration email', () => { const api = new Function(` let latestState = { signupMethod: 'phone', phoneVerificationEnabled: true, signupPhoneNumber: '+441111111111', accountIdentifierType: 'phone', accountIdentifier: '+441111111111' }; @@ -713,6 +803,8 @@ function syncHeroSmsFallbackSelectionOrderFromSelect() { return [{ id: 52, label: 'Thailand' }, { id: 16, label: 'United Kingdom' }]; } function getSelectedSignupMethod() { return 'phone'; } +${extractFunction('normalizePanelMode')} +${extractFunction('getSelectedPanelMode')} function getSelectedFiveSimCountries() { return [{ id: 'thailand', code: 'thailand', label: 'Thailand' }, { id: 'vietnam', code: 'vietnam', label: 'Vietnam' }]; }