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' }];
}