From 4c2ab31b0745cb4a97b2eacc74493a353c9ebec8 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Fri, 15 May 2026 18:23:41 +0800 Subject: [PATCH] =?UTF-8?q?=E9=99=90=E5=88=B6=E6=89=8B=E6=9C=BA=E5=8F=B7?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E6=B5=81=E7=A8=8B=E4=BD=BF=E7=94=A8=E5=8F=B7?= =?UTF-8?q?=E7=A0=81=E5=A4=8D=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 34 +++ background/message-router.js | 42 ++++ background/phone-verification-flow.js | 40 ++- docs/md/手机号注册禁用号码复用开发方案.md | 126 ++++++++++ sidepanel/sidepanel.css | 13 + sidepanel/sidepanel.js | 197 +++++++++++++-- .../background-message-router-module.test.js | 132 ++++++++++ tests/phone-verification-flow.test.js | 235 ++++++++++++++++++ ...epanel-phone-verification-settings.test.js | 136 +++++++++- 9 files changed, 929 insertions(+), 26 deletions(-) create mode 100644 docs/md/手机号注册禁用号码复用开发方案.md diff --git a/background.js b/background.js index d91fd6a..931dc03 100644 --- a/background.js +++ b/background.js @@ -1565,6 +1565,33 @@ function resolveSignupMethod(state = {}) { return method === SIGNUP_METHOD_PHONE && canUsePhoneSignup(state) ? SIGNUP_METHOD_PHONE : SIGNUP_METHOD_EMAIL; } +function hasSignupPhoneActivationState(state = {}) { + return Boolean( + state?.signupPhoneActivation + || state?.signupPhoneCompletedActivation + || String(state?.signupPhoneNumber || '').trim() + ); +} + +function isPhoneSignupIdentityStateForReuse(state = {}) { + if (resolveSignupMethod(state) === SIGNUP_METHOD_PHONE) { + return true; + } + + const runtimeActive = ( + (typeof isAutoRunLockedState === 'function' && isAutoRunLockedState(state)) + || (typeof isAutoRunPausedState === 'function' && isAutoRunPausedState(state)) + || (typeof isAutoRunScheduledState === 'function' && isAutoRunScheduledState(state)) + || Boolean(state?.autoRunning) + ); + if (!runtimeActive) { + return false; + } + + const identifierType = String(state?.accountIdentifierType || '').trim().toLowerCase(); + return identifierType === 'phone' || hasSignupPhoneActivationState(state); +} + async function ensureResolvedSignupMethodForRun(options = {}) { const state = await getState(); const force = Boolean(options.force); @@ -7255,6 +7282,10 @@ async function finalizePhoneActivationAfterSuccessfulFlow(state) { } async function clearFreeReusablePhoneActivation() { + const state = await getState(); + if (isPhoneSignupIdentityStateForReuse(state)) { + throw new Error('\u624b\u673a\u53f7\u6ce8\u518c\u6a21\u5f0f\u4e0b\u4e0d\u80fd\u4fee\u6539\u767d\u5ad6\u590d\u7528\u624b\u673a\u53f7\uff0c\u8bf7\u5207\u6362\u90ae\u7bb1\u6ce8\u518c\u540e\u518d\u4f7f\u7528\u3002'); + } await setState({ freeReusablePhoneActivation: null }); broadcastDataUpdate({ freeReusablePhoneActivation: null }); await addLog('已清除白嫖复用手机号记录。', 'ok'); @@ -7345,6 +7376,9 @@ async function setFreeReusablePhoneActivation(record = {}) { throw new Error('请先填写白嫖复用手机号。'); } const state = await getState(); + if (isPhoneSignupIdentityStateForReuse(state)) { + throw new Error('\u624b\u673a\u53f7\u6ce8\u518c\u6a21\u5f0f\u4e0b\u4e0d\u80fd\u8bb0\u5f55\u767d\u5ad6\u590d\u7528\u624b\u673a\u53f7\uff0c\u8bf7\u5207\u6362\u90ae\u7bb1\u6ce8\u518c\u540e\u518d\u4f7f\u7528\u3002'); + } const localActivation = findLocalHeroSmsActivationForPhone(state, phoneNumber); const activationId = String( record.activationId diff --git a/background/message-router.js b/background/message-router.js index 8ac54ac..41d0033 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -188,6 +188,42 @@ verifyHotmailAccount, } = deps; + function preserveKeyFromState(updates, currentState, key) { + if (!Object.prototype.hasOwnProperty.call(updates, key)) { + return; + } + if (currentState?.[key] !== undefined) { + updates[key] = currentState[key]; + } else { + delete updates[key]; + } + } + + function preservePhoneReuseSettingsForPhoneSignup(updates, currentState = {}) { + if (!updates || typeof updates !== 'object') { + return; + } + + if ( + Object.prototype.hasOwnProperty.call(updates, 'phoneSmsReuseEnabled') + || Object.prototype.hasOwnProperty.call(updates, 'heroSmsReuseEnabled') + ) { + const currentReuseEnabled = currentState?.phoneSmsReuseEnabled ?? currentState?.heroSmsReuseEnabled; + if (currentReuseEnabled !== undefined) { + const normalizedReuseEnabled = Boolean(currentReuseEnabled); + updates.phoneSmsReuseEnabled = normalizedReuseEnabled; + updates.heroSmsReuseEnabled = normalizedReuseEnabled; + } else { + delete updates.phoneSmsReuseEnabled; + delete updates.heroSmsReuseEnabled; + } + } + + preserveKeyFromState(updates, currentState, 'freePhoneReuseEnabled'); + preserveKeyFromState(updates, currentState, 'freePhoneReuseAutoEnabled'); + preserveKeyFromState(updates, currentState, 'phonePreferredActivation'); + } + async function appendManualAccountRunRecordIfNeeded(status, stateOverride = null, reason = '') { if (typeof appendAccountRunRecord !== 'function') { return null; @@ -1248,6 +1284,12 @@ ) { updates.signupMethod = resolveSignupMethod(nextSignupState); } + const nextPersistedSignupMethod = Object.prototype.hasOwnProperty.call(updates, 'signupMethod') + ? updates.signupMethod + : currentState?.signupMethod; + if (normalizeSignupMethod(nextPersistedSignupMethod) === 'phone') { + preservePhoneReuseSettingsForPhoneSignup(updates, currentState); + } const modeChanged = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled') && Boolean(currentState?.plusModeEnabled) !== Boolean(updates.plusModeEnabled); const plusPaymentChanged = Object.prototype.hasOwnProperty.call(updates, 'plusPaymentMethod') diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index f61bfb3..694ca02 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -923,6 +923,9 @@ } function isPhoneSmsReuseEnabled(state = {}) { + if (isPhoneSignupIdentityState(state)) { + return false; + } return normalizePhoneSmsReuseEnabled(state); } @@ -2112,6 +2115,18 @@ }; } + function isPhoneSignupIdentityState(state = {}) { + const signupMethod = String(state?.resolvedSignupMethod || state?.signupMethod || '').trim().toLowerCase(); + const identifierType = String(state?.accountIdentifierType || '').trim().toLowerCase(); + if (signupMethod === 'phone' || identifierType === 'phone') { + return true; + } + return Boolean( + normalizeActivation(state?.signupPhoneActivation) + || normalizeActivation(state?.signupPhoneCompletedActivation) + ); + } + function formatNoSupplySuggestion(context = {}) { const suggestions = []; const minPrice = Number(context?.minPrice); @@ -4677,6 +4692,9 @@ } async function handoffFreeReusablePhone(tabId, state = {}) { + if (isPhoneSignupIdentityState(state)) { + return null; + } if (!normalizeFreePhoneReuseEnabled(state?.freePhoneReuseEnabled)) { return null; } @@ -4837,10 +4855,12 @@ ...state, phoneSmsProvider: normalizePhoneSmsProvider(providerName), }); + const canUseSavedActivationForCurrentFlow = !isPhoneSignupIdentityState(state); const preferredActivation = normalizeActivation(state[PREFERRED_PHONE_ACTIVATION_STATE_KEY]); let failedPreferredActivation = null; const canTryPreferredActivation = ( - !Boolean(options?.skipPreferredActivation) + canUseSavedActivationForCurrentFlow + && !Boolean(options?.skipPreferredActivation) && preferredActivation && (provider === PHONE_SMS_PROVIDER_HERO || provider === PHONE_SMS_PROVIDER_5SIM) && preferredActivation.provider === provider @@ -5021,6 +5041,9 @@ async function markActivationReusableAfterSuccess(state, activation) { const normalizedActivation = normalizeActivation(activation); + if (isPhoneSignupIdentityState(state)) { + return; + } if (!isPhoneSmsReuseEnabled(state)) { await clearReusableActivation(); return; @@ -5059,6 +5082,9 @@ } function shouldPreserveActivationForFreeReuse(state, activation) { + if (isPhoneSignupIdentityState(state)) { + return false; + } if (!normalizeFreePhoneReuseEnabled(state?.freePhoneReuseEnabled)) { return false; } @@ -5072,6 +5098,9 @@ } function shouldSkipTerminalStatusForFreeReuse(state, activation) { + if (isPhoneSignupIdentityState(state)) { + return false; + } const normalizedActivation = normalizeActivation(activation); if (!normalizedActivation || normalizedActivation.provider !== PHONE_SMS_PROVIDER_HERO) { return false; @@ -5102,6 +5131,9 @@ ...(state || {}), ...(typeof getState === 'function' ? await getState() : {}), }; + if (isPhoneSignupIdentityState(latestState)) { + return; + } if (!normalizeFreePhoneReuseEnabled(latestState?.freePhoneReuseEnabled)) { return; } @@ -5141,6 +5173,9 @@ ...(state || {}), ...(typeof getState === 'function' ? await getState() : {}), }; + if (isPhoneSignupIdentityState(latestState)) { + return; + } const savedActivation = normalizeFreeReusablePhoneActivation( latestState[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY] ); @@ -5185,6 +5220,9 @@ ...(state || {}), ...(typeof getState === 'function' ? await getState() : {}), }; + if (isPhoneSignupIdentityState(latestState)) { + return; + } const savedActivation = normalizeFreeReusablePhoneActivation( latestState[FREE_REUSABLE_PHONE_ACTIVATION_STATE_KEY] ); diff --git a/docs/md/手机号注册禁用号码复用开发方案.md b/docs/md/手机号注册禁用号码复用开发方案.md new file mode 100644 index 0000000..7166dd2 --- /dev/null +++ b/docs/md/手机号注册禁用号码复用开发方案.md @@ -0,0 +1,126 @@ +# 手机号注册禁用号码复用开发方案 + +## 1. 需求结论 + +手机号注册流程中,手机号是账号主身份,不应进入普通号码复用、白嫖复用、自动白嫖复用、优先号码等复用链路。邮箱注册流程中的手机接码才是附加验证资源,才允许复用。 + +本次开发目标: + +1. 手机号注册模式下,复用相关功能自动显示为关闭并禁止修改。 +2. 切回邮箱注册模式后,恢复用户原本保存的复用偏好。 +3. 后台运行时必须有硬约束,不能只依赖 UI 禁用。 +4. 手动白嫖保存入口必须阻断手机号注册身份,避免绕过 UI。 +5. 测试覆盖普通复用、白嫖复用、优先号码、设置保存和消息入口。 + +## 2. 设计边界 + +### 2.1 符合要求的部分 + +- 手机号注册时禁用复用符合身份模型:手机号是账号标识,不是后续 add-phone 验证资源。 +- 邮箱注册时恢复复用符合用户预期:用户原本的复用配置不应因为临时切到手机号注册而丢失。 +- 后台硬约束是必要的:自动运行、导入配置、消息入口都可能绕过侧边栏状态。 + +### 2.2 需要避免的缺陷 + +1. 只改 UI 不改运行时:仍会被自动运行或消息入口绕过。 +2. 切到手机号注册时把配置永久写成 false:切回邮箱注册后用户偏好丢失。 +3. 只禁用普通复用,不禁用白嫖复用:白嫖复用优先级更高,仍会污染手机号身份。 +4. 只禁用白嫖开关,不禁用手动记录按钮:用户仍可把手机号注册订单写成白嫖资源。 +5. 忽略优先号码:优先号码本质上也是复用,应在手机号注册模式下不参与。 +6. 清空复用池:这会破坏邮箱注册模式下的历史资源;本次只做有效禁用,不主动清空。 + +## 3. 开发清单 + +### 阶段 1:后台运行时复用边界硬约束 + +目标: + +- 新增统一判断:当前状态是否为手机号注册身份流程。 +- 手机号注册身份下跳过普通复用和优先号码。 +- 手机号注册成功后不写入普通复用池。 +- 手机号注册身份下不触发白嫖复用保存、白嫖终态跳过、自动白嫖 handoff。 + +自检项: + +- `prepareSignupPhoneActivation` 不会复用旧号码。 +- `finalizeSignupPhoneActivationAfterSuccess` 仍会正常完成接码订单,但不保存为复用资源。 +- Step9 边界场景即使状态误带手机号身份,也不会走普通/白嫖复用。 +- 不清空原有邮箱模式复用资源。 +- 无拼写错误、无乱码、无无关重构。 + +### 阶段 2:侧边栏 UI 有效禁用与偏好保留 + +目标: + +- 手机号注册模式下普通号码复用、白嫖复用、自动白嫖复用显示关闭并禁用。 +- 优先号码和白嫖号码运行态入口在手机号注册模式下不可修改。 +- 切回邮箱注册时恢复 `latestState` 中保存的原始偏好。 +- 保存设置时不把手机号模式下的 UI 强制 false 写回持久配置。 + +自检项: + +- 手机号注册切换时控件不会闪回可用。 +- 邮箱注册切回后复用偏好恢复。 +- 自动运行锁和手机号注册锁不互相覆盖。 +- 变更不影响 provider 切换、价格区间、接码展开折叠。 +- 无 UI 文案乱码。 + +### 阶段 3:消息入口与手动白嫖保存拦截 + +目标: + +- `SET_FREE_REUSABLE_PHONE` 在手机号注册身份下返回明确错误。 +- `SAVE_SETTING` 在手机号注册有效模式下忽略复用开关改动,保留历史偏好。 +- 后台约束与 UI 约束一致。 + +自检项: + +- 侧边栏按钮和外部消息都无法在手机号注册身份下保存白嫖号。 +- 邮箱注册模式下原行为保持。 +- 不影响清除白嫖号码能力。 +- 不影响导入旧配置的兼容读取。 + +### 阶段 4:测试覆盖 + +目标: + +- 补充 phone verification flow 测试。 +- 补充 sidepanel 设置保存测试。 +- 补充 message router 保存拦截测试。 + +自检项: + +- 手机号注册不读取普通复用。 +- 手机号注册不读取白嫖复用。 +- 手机号注册不自动保存白嫖资源。 +- 手机号注册保存设置不会覆盖邮箱模式复用偏好。 +- 邮箱注册复用路径不回归。 + +### 阶段 5:全面审查与提交 + +目标: + +- 运行相关测试。 +- `git diff` 审查每个改动点。 +- 检查中文文档和 UI 字符串没有乱码。 +- 检查没有误改无关文件。 +- 使用中文提交信息提交代码。 + +自检项: + +- 相关测试通过。 +- 没有遗留调试代码。 +- 没有破坏当前工作区的其他改动。 +- 提交内容只包含本需求相关文件。 + +## 4. 完成标准 + +本需求只有同时满足以下条件才算完成: + +1. 手机号注册模式下所有复用链路运行时失效。 +2. 手机号注册模式下侧边栏无法修改复用相关控件。 +3. 切回邮箱注册后原复用偏好恢复。 +4. 白嫖复用不能通过手动消息入口绕过。 +5. 单元测试覆盖新增边界。 +6. 全面审查无设计缺陷、边界遗漏、乱码和无关改动。 +7. 最终使用中文提交信息完成 commit。 diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index da927e6..63136f7 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -2475,6 +2475,19 @@ header { white-space: nowrap; } +.hero-sms-price-control.is-disabled, +.hero-sms-settings-cell.is-disabled, +.hero-sms-runtime-cell.is-disabled { + opacity: 0.56; +} + +.hero-sms-price-control.is-disabled .toggle-switch, +.hero-sms-settings-cell.is-disabled .toggle-switch, +.hero-sms-runtime-cell.is-disabled .data-input, +.hero-sms-runtime-cell.is-disabled .btn { + cursor: not-allowed; +} + .data-unit { font-size: 12px; font-weight: 600; diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index f54853f..fbf56fb 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -519,10 +519,12 @@ const SIGNUP_METHOD_EMAIL = 'email'; const SIGNUP_METHOD_PHONE = 'phone'; const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL; const DEFAULT_ACTIVE_FLOW_ID = 'openai'; +const PHONE_SIGNUP_REUSE_LOCK_TITLE = '手机号注册流程不使用号码复用,切回邮箱注册后会恢复原设置'; let latestState = null; let currentPlusModeEnabled = false; let currentPlusPaymentMethod = DEFAULT_PLUS_PAYMENT_METHOD; let currentSignupMethod = DEFAULT_SIGNUP_METHOD; +let phoneSignupReuseUiWasLocked = false; let lastConfirmedOperationDelayEnabled = true; let heroSmsCountrySelectionOrder = []; let phoneSmsProviderOrderSelection = []; @@ -3457,17 +3459,33 @@ function collectSettingsPayload() { const defaultPhoneCodePollMaxRounds = typeof DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS !== 'undefined' ? DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS : 12; - const phoneSmsReuseEnabledValue = typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled + const selectedSignupMethod = typeof getSelectedSignupMethod === 'function' + ? getSelectedSignupMethod() + : ( + (typeof normalizeSignupMethod === 'function' + ? normalizeSignupMethod(latestState?.signupMethod) + : (String(latestState?.signupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email')) + ); + const phoneSignupReuseLocked = typeof isPhoneSignupReuseLocked === 'function' + ? isPhoneSignupReuseLocked(latestState, { signupMethod: selectedSignupMethod }) + : selectedSignupMethod === 'phone'; + const phoneSmsReuseEnabledValue = phoneSignupReuseLocked + ? getStoredPhoneSmsReuseEnabled(latestState) + : typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled ? normalizeHeroSmsReuseEnabledValue(inputHeroSmsReuseEnabled.checked) : normalizeHeroSmsReuseEnabledValue( latestState?.phoneSmsReuseEnabled, latestState?.heroSmsReuseEnabled ); const heroSmsReuseEnabledValue = phoneSmsReuseEnabledValue; - const freePhoneReuseEnabledValue = typeof inputFreePhoneReuseEnabled !== 'undefined' && inputFreePhoneReuseEnabled + const freePhoneReuseEnabledValue = phoneSignupReuseLocked + ? getStoredFreePhoneReuseEnabled(latestState) + : typeof inputFreePhoneReuseEnabled !== 'undefined' && inputFreePhoneReuseEnabled ? Boolean(inputFreePhoneReuseEnabled.checked) : Boolean(latestState?.freePhoneReuseEnabled); - const freePhoneReuseAutoEnabledValue = typeof inputFreePhoneReuseAutoEnabled !== 'undefined' && inputFreePhoneReuseAutoEnabled + const freePhoneReuseAutoEnabledValue = phoneSignupReuseLocked + ? getStoredFreePhoneReuseAutoEnabled(latestState) + : typeof inputFreePhoneReuseAutoEnabled !== 'undefined' && inputFreePhoneReuseAutoEnabled ? Boolean(inputFreePhoneReuseAutoEnabled.checked) : Boolean(latestState?.freePhoneReuseAutoEnabled); const defaultHeroSmsAcquirePriority = typeof DEFAULT_HERO_SMS_ACQUIRE_PRIORITY !== 'undefined' @@ -3556,7 +3574,9 @@ function collectSettingsPayload() { const heroSmsPreferredPriceValue = typeof inputHeroSmsPreferredPrice !== 'undefined' && inputHeroSmsPreferredPrice ? normalizeHeroSmsMaxPriceValue(inputHeroSmsPreferredPrice.value) : normalizeHeroSmsMaxPriceValue(latestState?.heroSmsPreferredPrice || ''); - const phonePreferredActivationValue = typeof getSelectedPhonePreferredActivation === 'function' + const phonePreferredActivationValue = phoneSignupReuseLocked + ? (latestState?.phonePreferredActivation ? { ...latestState.phonePreferredActivation } : null) + : typeof getSelectedPhonePreferredActivation === 'function' ? getSelectedPhonePreferredActivation() : null; const phoneVerificationReplacementLimitValue = typeof inputPhoneReplacementLimit !== 'undefined' && inputPhoneReplacementLimit @@ -3647,13 +3667,6 @@ function collectSettingsPayload() { const currentPayPalAccount = typeof getCurrentPayPalAccount === 'function' ? getCurrentPayPalAccount(latestState) : payPalAccounts.find((account) => account?.id === String(latestState?.currentPayPalAccountId || '').trim()) || null; - const selectedSignupMethod = typeof getSelectedSignupMethod === 'function' - ? getSelectedSignupMethod() - : ( - (typeof normalizeSignupMethod === 'function' - ? normalizeSignupMethod(latestState?.signupMethod) - : (String(latestState?.signupMethod || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email')) - ); const normalizePanelModeSafe = typeof normalizePanelMode === 'function' ? normalizePanelMode : ((value = '') => { @@ -7573,6 +7586,69 @@ function normalizeSignupMethod(value = '') { : 'email'; } +function isPhoneSignupReuseLocked(state = latestState, options = {}) { + const hasOptionMethod = Object.prototype.hasOwnProperty.call(options || {}, 'signupMethod'); + const rawMethod = hasOptionMethod + ? options.signupMethod + : (typeof getSelectedSignupMethod === 'function' + ? getSelectedSignupMethod() + : (state?.signupMethod || DEFAULT_SIGNUP_METHOD)); + const selectedMethod = normalizeSignupMethod(rawMethod); + if (selectedMethod === SIGNUP_METHOD_PHONE) { + return true; + } + + if (!options?.includeRuntimeIdentity) { + return false; + } + + const identifierType = String( + options?.accountIdentifierType + ?? state?.accountIdentifierType + ?? '' + ).trim().toLowerCase(); + return identifierType === 'phone'; +} + +function getStoredPhoneSmsReuseEnabled(state = latestState) { + return normalizeHeroSmsReuseEnabledValue( + state?.phoneSmsReuseEnabled, + state?.heroSmsReuseEnabled + ); +} + +function getStoredFreePhoneReuseEnabled(state = latestState) { + return Boolean(state?.freePhoneReuseEnabled); +} + +function getStoredFreePhoneReuseAutoEnabled(state = latestState) { + return Boolean(state?.freePhoneReuseAutoEnabled); +} + +function restorePhoneReuseControlsFromState(state = latestState) { + if (typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled) { + inputHeroSmsReuseEnabled.checked = getStoredPhoneSmsReuseEnabled(state); + } + if (typeof inputFreePhoneReuseEnabled !== 'undefined' && inputFreePhoneReuseEnabled) { + inputFreePhoneReuseEnabled.checked = getStoredFreePhoneReuseEnabled(state); + } + if (typeof inputFreePhoneReuseAutoEnabled !== 'undefined' && inputFreePhoneReuseAutoEnabled) { + inputFreePhoneReuseAutoEnabled.checked = getStoredFreePhoneReuseAutoEnabled(state); + } +} + +function setElementReuseLockedState(element, locked, title = PHONE_SIGNUP_REUSE_LOCK_TITLE) { + if (!element) { + return; + } + element.classList?.toggle?.('is-disabled', Boolean(locked)); + if (locked) { + element.title = title; + } else if (element.title === title) { + element.title = ''; + } +} + function normalizePanelMode(value = '') { const normalized = String(value || '').trim().toLowerCase(); if (normalized === 'sub2api' || normalized === 'codex2api') { @@ -7904,19 +7980,79 @@ function updatePhoneVerificationSettingsUI() { if (typeof rowFreePhoneReuseAutoEnabled !== 'undefined' && rowFreePhoneReuseAutoEnabled) { rowFreePhoneReuseAutoEnabled.style.display = showSettings ? '' : 'none'; } + const phoneSignupReuseLocked = typeof isPhoneSignupReuseLocked === 'function' + ? isPhoneSignupReuseLocked(latestState, { + signupMethod: typeof getSelectedSignupMethod === 'function' + ? getSelectedSignupMethod() + : latestState?.signupMethod, + }) + : false; + if (!phoneSignupReuseLocked && phoneSignupReuseUiWasLocked) { + restorePhoneReuseControlsFromState(latestState); + } + if (phoneSignupReuseLocked) { + if (typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled) { + inputHeroSmsReuseEnabled.checked = false; + } + if (typeof inputFreePhoneReuseEnabled !== 'undefined' && inputFreePhoneReuseEnabled) { + inputFreePhoneReuseEnabled.checked = false; + } + if (typeof inputFreePhoneReuseAutoEnabled !== 'undefined' && inputFreePhoneReuseAutoEnabled) { + inputFreePhoneReuseAutoEnabled.checked = false; + } + } + phoneSignupReuseUiWasLocked = phoneSignupReuseLocked; + const settingsLocked = isAutoRunLockedPhase() || isAutoRunScheduledPhase(); const freePhoneReuseEnabled = Boolean( - typeof inputFreePhoneReuseEnabled !== 'undefined' && inputFreePhoneReuseEnabled?.checked + !phoneSignupReuseLocked + && typeof inputFreePhoneReuseEnabled !== 'undefined' + && inputFreePhoneReuseEnabled?.checked ); - const freePhoneReuseAutoAvailable = showSettings && freePhoneReuseEnabled; + const freePhoneReuseAutoAvailable = showSettings && !phoneSignupReuseLocked && freePhoneReuseEnabled; + if (typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled) { + inputHeroSmsReuseEnabled.disabled = settingsLocked || phoneSignupReuseLocked; + } if (typeof inputFreePhoneReuseAutoEnabled !== 'undefined' && inputFreePhoneReuseAutoEnabled) { - inputFreePhoneReuseAutoEnabled.disabled = !freePhoneReuseAutoAvailable; + inputFreePhoneReuseAutoEnabled.disabled = settingsLocked || phoneSignupReuseLocked || !freePhoneReuseAutoAvailable; if (!freePhoneReuseAutoAvailable) { inputFreePhoneReuseAutoEnabled.checked = false; } } - setFreePhoneReuseControlsLocked(isAutoRunLockedPhase() || isAutoRunScheduledPhase()); + setFreePhoneReuseControlsLocked(settingsLocked || phoneSignupReuseLocked); + if (typeof selectHeroSmsPreferredActivation !== 'undefined' && selectHeroSmsPreferredActivation) { + selectHeroSmsPreferredActivation.disabled = settingsLocked || phoneSignupReuseLocked; + } + if (typeof inputFreeReusablePhone !== 'undefined' && inputFreeReusablePhone) { + inputFreeReusablePhone.disabled = settingsLocked || phoneSignupReuseLocked; + } + if (typeof btnSaveFreeReusablePhone !== 'undefined' && btnSaveFreeReusablePhone) { + btnSaveFreeReusablePhone.disabled = settingsLocked || phoneSignupReuseLocked; + } + if (typeof btnClearFreeReusablePhone !== 'undefined' && btnClearFreeReusablePhone) { + btnClearFreeReusablePhone.disabled = settingsLocked || phoneSignupReuseLocked; + } + const heroSmsReuseRow = typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled?.closest + ? inputHeroSmsReuseEnabled.closest('.hero-sms-price-control') + : null; + setElementReuseLockedState(heroSmsReuseRow, phoneSignupReuseLocked); + setElementReuseLockedState( + typeof rowFreePhoneReuseEnabled !== 'undefined' ? rowFreePhoneReuseEnabled : null, + phoneSignupReuseLocked + ); + setElementReuseLockedState( + typeof rowFreePhoneReuseAutoEnabled !== 'undefined' ? rowFreePhoneReuseAutoEnabled : null, + phoneSignupReuseLocked + ); + setElementReuseLockedState( + typeof rowHeroSmsPreferredActivation !== 'undefined' ? rowHeroSmsPreferredActivation : null, + phoneSignupReuseLocked + ); + setElementReuseLockedState( + typeof rowFreeReusablePhone !== 'undefined' ? rowFreeReusablePhone : null, + phoneSignupReuseLocked + ); if (typeof rowFreePhoneReuseAutoEnabled !== 'undefined' && rowFreePhoneReuseAutoEnabled) { - rowFreePhoneReuseAutoEnabled.classList.toggle('is-disabled', !freePhoneReuseAutoAvailable); + rowFreePhoneReuseAutoEnabled.classList.toggle('is-disabled', phoneSignupReuseLocked || !freePhoneReuseAutoAvailable); } const runtimeVisible = enabled; [ @@ -14031,11 +14167,24 @@ inputNexSmsServiceCode?.addEventListener('blur', () => { }); inputHeroSmsReuseEnabled?.addEventListener('change', () => { + if (isPhoneSignupReuseLocked(latestState)) { + inputHeroSmsReuseEnabled.checked = false; + updatePhoneVerificationSettingsUI(); + return; + } markSettingsDirty(true); saveSettings({ silent: true }).catch(() => { }); }); inputFreePhoneReuseEnabled?.addEventListener('change', () => { + if (isPhoneSignupReuseLocked(latestState)) { + inputFreePhoneReuseEnabled.checked = false; + if (inputFreePhoneReuseAutoEnabled) { + inputFreePhoneReuseAutoEnabled.checked = false; + } + updatePhoneVerificationSettingsUI(); + return; + } if (!inputFreePhoneReuseEnabled.checked && inputFreePhoneReuseAutoEnabled) { inputFreePhoneReuseAutoEnabled.checked = false; } @@ -14045,12 +14194,22 @@ inputFreePhoneReuseEnabled?.addEventListener('change', () => { }); inputFreePhoneReuseAutoEnabled?.addEventListener('change', () => { + if (isPhoneSignupReuseLocked(latestState)) { + inputFreePhoneReuseAutoEnabled.checked = false; + updatePhoneVerificationSettingsUI(); + return; + } updatePhoneVerificationSettingsUI(); markSettingsDirty(true); saveSettings({ silent: true }).catch(() => { }); }); btnSaveFreeReusablePhone?.addEventListener('click', async () => { + if (isPhoneSignupReuseLocked(latestState)) { + showToast?.('手机号注册流程不能记录白嫖复用号码,请切回邮箱注册后再使用。', 'warn', 2600); + updatePhoneVerificationSettingsUI(); + return; + } const phoneNumber = String(inputFreeReusablePhone?.value || '').trim(); if (!phoneNumber) { showToast?.('请先填写白嫖复用手机号。', 'warn', 2200); @@ -14094,6 +14253,11 @@ selectHeroSmsAcquirePriority?.addEventListener('change', () => { saveSettings({ silent: true }).catch(() => { }); }); selectHeroSmsPreferredActivation?.addEventListener('change', () => { + if (isPhoneSignupReuseLocked(latestState)) { + renderPhonePreferredActivationOptions(latestState); + updatePhoneVerificationSettingsUI(); + return; + } markSettingsDirty(true); saveSettings({ silent: true }).catch(() => { }); }); @@ -14967,6 +15131,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { message.payload.phoneSmsReuseEnabled, message.payload.heroSmsReuseEnabled ); + updatePhoneVerificationSettingsUI(); } if (message.payload.freePhoneReuseEnabled !== undefined && inputFreePhoneReuseEnabled) { inputFreePhoneReuseEnabled.checked = Boolean(message.payload.freePhoneReuseEnabled); diff --git a/tests/background-message-router-module.test.js b/tests/background-message-router-module.test.js index 23fcdb8..f1190ba 100644 --- a/tests/background-message-router-module.test.js +++ b/tests/background-message-router-module.test.js @@ -47,6 +47,20 @@ test('background free reusable phone setter can recover local HeroSMS activation assert.match(setterBlock, /manualOnly:\s*!activationId/); }); +test('background blocks free reusable phone mutations while phone signup owns the identity', () => { + const source = fs.readFileSync('background.js', 'utf8'); + const clearStart = source.indexOf('async function clearFreeReusablePhoneActivation'); + const setterStart = source.indexOf('async function setFreeReusablePhoneActivation'); + const setterEnd = source.indexOf('// ============================================================\n// Tab Registry', setterStart); + const clearBlock = source.slice(clearStart, setterStart); + const setterBlock = source.slice(setterStart, setterEnd); + + assert.match(source, /function isPhoneSignupIdentityStateForReuse\(/); + assert.match(source, /function hasSignupPhoneActivationState\(/); + assert.match(clearBlock, /isPhoneSignupIdentityStateForReuse\(state\)/); + assert.match(setterBlock, /isPhoneSignupIdentityStateForReuse\(state\)/); +}); + test('background HeroSMS phone prefix inference covers built-in major countries', () => { const source = fs.readFileSync('background.js', 'utf8'); const supportedStart = source.indexOf('const HERO_SMS_SUPPORTED_COUNTRY_IDS = ['); @@ -132,6 +146,124 @@ test('SAVE_SETTING broadcasts free phone reuse setting updates for realtime side ); }); +test('SAVE_SETTING preserves phone reuse preferences while phone signup is selected', async () => { + const source = fs.readFileSync('background/message-router.js', 'utf8'); + const globalScope = { console }; + const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope); + const persistedPayloads = []; + let state = { + signupMethod: 'phone', + phoneVerificationEnabled: true, + plusModeEnabled: false, + phoneSmsReuseEnabled: true, + heroSmsReuseEnabled: true, + freePhoneReuseEnabled: true, + freePhoneReuseAutoEnabled: true, + phonePreferredActivation: { + provider: 'hero-sms', + activationId: 'stored', + phoneNumber: '66950001111', + }, + }; + + const router = api.createMessageRouter({ + addLog: async () => {}, + buildLuckmailSessionSettingsPayload: () => ({}), + buildPersistentSettingsPayload: (input = {}) => ({ + signupMethod: String(input.signupMethod || state.signupMethod), + phoneSmsReuseEnabled: Boolean(input.phoneSmsReuseEnabled), + heroSmsReuseEnabled: Boolean(input.heroSmsReuseEnabled), + freePhoneReuseEnabled: Boolean(input.freePhoneReuseEnabled), + freePhoneReuseAutoEnabled: Boolean(input.freePhoneReuseAutoEnabled), + phonePreferredActivation: input.phonePreferredActivation ?? null, + }), + broadcastDataUpdate: () => {}, + getState: async () => ({ ...state }), + setPersistentSettings: async (updates) => { + persistedPayloads.push({ ...updates }); + }, + setState: async (updates) => { + state = { ...state, ...updates }; + }, + }); + + const response = await router.handleMessage({ + type: 'SAVE_SETTING', + payload: { + signupMethod: 'phone', + phoneSmsReuseEnabled: false, + heroSmsReuseEnabled: false, + freePhoneReuseEnabled: false, + freePhoneReuseAutoEnabled: false, + phonePreferredActivation: null, + }, + }); + + assert.equal(response.ok, true); + assert.equal(state.phoneSmsReuseEnabled, true); + assert.equal(state.heroSmsReuseEnabled, true); + assert.equal(state.freePhoneReuseEnabled, true); + assert.equal(state.freePhoneReuseAutoEnabled, true); + assert.deepStrictEqual(state.phonePreferredActivation, { + provider: 'hero-sms', + activationId: 'stored', + phoneNumber: '66950001111', + }); + assert.equal(persistedPayloads[0].phoneSmsReuseEnabled, true); + assert.equal(persistedPayloads[0].freePhoneReuseEnabled, true); +}); + +test('SAVE_SETTING allows phone reuse preferences after switching back to email signup', async () => { + const source = fs.readFileSync('background/message-router.js', 'utf8'); + const globalScope = { console }; + const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope); + let state = { + signupMethod: 'phone', + phoneVerificationEnabled: true, + plusModeEnabled: false, + phoneSmsReuseEnabled: true, + heroSmsReuseEnabled: true, + freePhoneReuseEnabled: true, + freePhoneReuseAutoEnabled: true, + }; + + const router = api.createMessageRouter({ + addLog: async () => {}, + buildLuckmailSessionSettingsPayload: () => ({}), + buildPersistentSettingsPayload: (input = {}) => ({ + signupMethod: String(input.signupMethod || state.signupMethod), + phoneSmsReuseEnabled: Boolean(input.phoneSmsReuseEnabled), + heroSmsReuseEnabled: Boolean(input.heroSmsReuseEnabled), + freePhoneReuseEnabled: Boolean(input.freePhoneReuseEnabled), + freePhoneReuseAutoEnabled: Boolean(input.freePhoneReuseAutoEnabled), + }), + broadcastDataUpdate: () => {}, + getState: async () => ({ ...state }), + setPersistentSettings: async () => {}, + setState: async (updates) => { + state = { ...state, ...updates }; + }, + }); + + const response = await router.handleMessage({ + type: 'SAVE_SETTING', + payload: { + signupMethod: 'email', + phoneSmsReuseEnabled: false, + heroSmsReuseEnabled: false, + freePhoneReuseEnabled: false, + freePhoneReuseAutoEnabled: false, + }, + }); + + assert.equal(response.ok, true); + assert.equal(state.signupMethod, 'email'); + assert.equal(state.phoneSmsReuseEnabled, false); + assert.equal(state.heroSmsReuseEnabled, false); + assert.equal(state.freePhoneReuseEnabled, false); + assert.equal(state.freePhoneReuseAutoEnabled, false); +}); + test('SAVE_SETTING broadcasts operation delay setting without background success log', async () => { const source = fs.readFileSync('background/message-router.js', 'utf8'); const globalScope = { console }; diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js index a5fa3df..d083bfb 100644 --- a/tests/phone-verification-flow.test.js +++ b/tests/phone-verification-flow.test.js @@ -140,6 +140,67 @@ test('signup phone helper persists signup runtime state without touching add-pho assert.ok(!setStateCalls.some((updates) => Object.prototype.hasOwnProperty.call(updates, 'currentPhoneActivation'))); }); +test('signup phone helper buys a fresh number instead of using reuse entries', async () => { + const actions = []; + let currentState = { + heroSmsApiKey: 'demo-key', + signupMethod: 'phone', + phoneSmsReuseEnabled: true, + heroSmsReuseEnabled: true, + phonePreferredActivation: { + activationId: 'preferred-activation', + phoneNumber: '66950002222', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 52, + successfulUses: 0, + maxUses: 3, + }, + reusablePhoneActivation: { + activationId: 'paid-reuse', + phoneNumber: '66950003333', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 52, + successfulUses: 1, + maxUses: 3, + }, + }; + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + const action = parsedUrl.searchParams.get('action'); + actions.push(action); + if (action === 'reactivate') { + throw new Error('phone signup should not reactivate reusable numbers'); + } + if (action === 'getPrices') { + return { ok: true, text: async () => buildHeroSmsPricesPayload() }; + } + if (action === 'getNumber') { + return { ok: true, text: async () => 'ACCESS_NUMBER:signup-fresh:66959916439' }; + } + throw new Error(`Unexpected HeroSMS action: ${action}`); + }, + getState: async () => currentState, + sendToContentScriptResilient: async () => ({}), + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + const activation = await helpers.prepareSignupPhoneActivation(currentState); + + assert.equal(activation.activationId, 'signup-fresh'); + assert.deepStrictEqual(actions, ['getPrices', 'getNumber']); + assert.equal(currentState.signupPhoneNumber, '66959916439'); + assert.equal(currentState.reusablePhoneActivation.activationId, 'paid-reuse'); +}); + test('signup phone helper polls signup SMS code and keeps activation purpose isolated', async () => { const setStateCalls = []; let currentState = { @@ -274,6 +335,82 @@ test('signup phone helper finalizes or cancels signup activation without clearin assert.ok(!setStateCalls.some((updates) => Object.prototype.hasOwnProperty.call(updates, 'currentPhoneActivation'))); }); +test('signup phone helper does not store signup numbers into the reusable pool', async () => { + const setStateCalls = []; + let currentState = { + heroSmsApiKey: 'demo-key', + signupMethod: 'phone', + phoneSmsReuseEnabled: true, + heroSmsReuseEnabled: true, + signupPhoneActivation: { + activationId: 'signup-123', + phoneNumber: '66959916439', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 52, + successfulUses: 0, + maxUses: 3, + }, + reusablePhoneActivation: { + activationId: 'paid-reuse', + phoneNumber: '66950003333', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 52, + successfulUses: 1, + maxUses: 3, + }, + phoneReusableActivationPool: [ + { + activationId: 'pool-reuse', + phoneNumber: '66950004444', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 52, + successfulUses: 1, + maxUses: 3, + }, + ], + }; + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + const action = parsedUrl.searchParams.get('action'); + if (action === 'setStatus') { + return { ok: true, text: async () => 'ACCESS_READY' }; + } + throw new Error(`Unexpected HeroSMS action: ${action}`); + }, + getState: async () => currentState, + sendToContentScriptResilient: async () => ({}), + setState: async (updates) => { + setStateCalls.push(updates); + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + await helpers.finalizeSignupPhoneActivationAfterSuccess(currentState, currentState.signupPhoneActivation); + + assert.equal(currentState.reusablePhoneActivation.activationId, 'paid-reuse'); + assert.deepStrictEqual( + currentState.phoneReusableActivationPool.map((entry) => entry.activationId), + ['pool-reuse'] + ); + assert.equal( + setStateCalls.some((updates) => updates?.reusablePhoneActivation?.activationId === 'signup-123'), + false + ); + assert.equal( + setStateCalls.some((updates) => Array.isArray(updates?.phoneReusableActivationPool) + && updates.phoneReusableActivationPool.some((entry) => entry.activationId === 'signup-123')), + false + ); +}); + test('signup phone helper completes signup SMS verification without touching add-phone activation', async () => { const setStateCalls = []; const contentMessages = []; @@ -4953,6 +5090,104 @@ test('phone verification helper gives automatic free reuse priority over paid re assert.equal(currentState.reusablePhoneActivation.activationId, 'paid-reuse'); }); +test('phone verification helper ignores reuse entries for phone signup identity', async () => { + const requests = []; + let currentState = { + heroSmsApiKey: 'demo-key', + signupMethod: 'phone', + accountIdentifierType: 'phone', + accountIdentifier: '66959916439', + phoneSmsReuseEnabled: true, + heroSmsReuseEnabled: true, + freePhoneReuseEnabled: true, + freePhoneReuseAutoEnabled: true, + verificationResendCount: 0, + currentPhoneActivation: null, + reusablePhoneActivation: { + activationId: 'paid-reuse', + phoneNumber: '66950003333', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 52, + countryLabel: 'Thailand', + successfulUses: 0, + maxUses: 3, + }, + freeReusablePhoneActivation: { + activationId: 'free-priority', + phoneNumber: '66950004444', + provider: 'hero-sms', + serviceCode: 'dr', + countryId: 52, + countryLabel: 'Thailand', + successfulUses: 0, + maxUses: 3, + source: 'free-manual-reuse', + }, + }; + + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url) => { + const parsedUrl = new URL(url); + requests.push(parsedUrl); + const action = parsedUrl.searchParams.get('action'); + const id = parsedUrl.searchParams.get('id'); + if (action === 'reactivate' || id === 'free-priority') { + throw new Error(`phone signup identity should not use reusable activation: ${action}:${id || ''}`); + } + if (action === 'getPrices') { + return { ok: true, text: async () => buildHeroSmsPricesPayload() }; + } + if (action === 'getNumber') { + return { ok: true, text: async () => 'ACCESS_NUMBER:fresh-phone:66950008888' }; + } + if (action === 'getStatus' && id === 'fresh-phone') { + return { ok: true, text: async () => 'STATUS_OK:445566' }; + } + if (action === 'setStatus' && id === 'fresh-phone') { + return { ok: true, text: async () => 'ACCESS_ACTIVATION' }; + } + throw new Error(`Unexpected HeroSMS action: ${action}:${id || ''}`); + }, + getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs, + getState: async () => ({ ...currentState }), + sendToContentScriptResilient: async (_source, message) => { + if (message.type === 'SUBMIT_PHONE_NUMBER') { + return { phoneVerificationPage: true, url: 'https://auth.openai.com/phone-verification' }; + } + if (message.type === 'SUBMIT_PHONE_VERIFICATION_CODE') { + return { success: true, consentReady: true, url: 'https://auth.openai.com/authorize' }; + } + throw new Error(`Unexpected content-script message: ${message.type}`); + }, + setState: async (updates) => { + currentState = { ...currentState, ...updates }; + }, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + const result = await helpers.completePhoneVerificationFlow(1, { + addPhonePage: true, + phoneVerificationPage: false, + url: 'https://auth.openai.com/add-phone', + }); + + assert.deepStrictEqual(result, { + success: true, + consentReady: true, + url: 'https://auth.openai.com/authorize', + }); + assert.deepStrictEqual( + requests.map((requestUrl) => requestUrl.searchParams.get('action')), + ['getPrices', 'getNumber', 'getStatus', 'setStatus'] + ); + assert.equal(currentState.freeReusablePhoneActivation.activationId, 'free-priority'); + assert.equal(currentState.reusablePhoneActivation.activationId, 'paid-reuse'); +}); + test('phone verification helper hands off manual-only free reuse even when automatic free reuse is enabled', async () => { const requests = []; const messages = []; diff --git a/tests/sidepanel-phone-verification-settings.test.js b/tests/sidepanel-phone-verification-settings.test.js index bb5ea1a..e811728 100644 --- a/tests/sidepanel-phone-verification-settings.test.js +++ b/tests/sidepanel-phone-verification-settings.test.js @@ -519,6 +519,28 @@ test('updatePhoneVerificationSettingsUI toggles SMS rows from the sms switch and const api = new Function(` let phoneVerificationSectionExpanded = false; let latestState = {}; +let phoneSignupReuseUiWasLocked = false; +const PHONE_SIGNUP_REUSE_LOCK_TITLE = '手机号注册流程不使用号码复用,切回邮箱注册后会恢复原设置'; +const SIGNUP_METHOD_EMAIL = 'email'; +const SIGNUP_METHOD_PHONE = 'phone'; +const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL; +const DEFAULT_HERO_SMS_REUSE_ENABLED = true; +function createMockClassList() { + const values = new Set(); + return { + toggle(name, force) { + const enabled = force === undefined ? !values.has(name) : Boolean(force); + if (enabled) values.add(name); + else values.delete(name); + }, + contains(name) { + return values.has(name); + }, + }; +} +function createMockRow() { + return { style: { display: 'none' }, classList: createMockClassList(), title: '' }; +} const inputPhoneVerificationEnabled = { checked: false }; const rowPhoneVerificationEnabled = { style: { display: 'none' } }; const rowPhoneVerificationFold = { style: { display: 'none' } }; @@ -583,13 +605,24 @@ const rowHeroSmsCurrentNumber = { style: { display: 'none' } }; const rowHeroSmsCurrentCountdown = { style: { display: 'none' } }; const rowHeroSmsPriceTiers = { style: { display: 'none' } }; const rowHeroSmsCurrentCode = { style: { display: 'none' } }; -const rowHeroSmsPreferredActivation = { style: { display: 'none' } }; +const rowHeroSmsPreferredActivation = createMockRow(); const rowPhoneVerificationResendCount = { style: { display: 'none' } }; const rowPhoneReplacementLimit = { style: { display: 'none' } }; const rowPhoneCodeWaitSeconds = { style: { display: 'none' } }; const rowPhoneCodeTimeoutWindows = { style: { display: 'none' } }; const rowPhoneCodePollIntervalSeconds = { style: { display: 'none' } }; const rowPhoneCodePollMaxRounds = { style: { display: 'none' } }; +const rowFreePhoneReuseEnabled = createMockRow(); +const rowFreePhoneReuseAutoEnabled = createMockRow(); +const rowFreeReusablePhone = createMockRow(); +const heroSmsReuseRow = createMockRow(); +const inputHeroSmsReuseEnabled = { checked: true, disabled: false, closest: () => heroSmsReuseRow }; +const inputFreePhoneReuseEnabled = { checked: true, disabled: false }; +const inputFreePhoneReuseAutoEnabled = { checked: true, disabled: false }; +const selectHeroSmsPreferredActivation = { disabled: false }; +const inputFreeReusablePhone = { disabled: false }; +const btnSaveFreeReusablePhone = { disabled: false }; +const btnClearFreeReusablePhone = { disabled: false }; const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms'; const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim'; const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms'; @@ -602,10 +635,23 @@ function updateSignupMethodUI() { function syncSignupPhoneInputFromState() { rowSignupPhone.style.display = inputPhoneVerificationEnabled.checked && latestState.signupPhoneNumber ? '' : 'none'; } -function setFreePhoneReuseControlsLocked() {} +function setFreePhoneReuseControlsLocked(locked) { + inputFreePhoneReuseEnabled.disabled = Boolean(locked); + inputFreePhoneReuseAutoEnabled.disabled = Boolean(locked) + || !Boolean(inputFreePhoneReuseEnabled.checked) + || !Boolean(inputPhoneVerificationEnabled.checked && phoneVerificationSectionExpanded); +} function isAutoRunLockedPhase() { return false; } function isAutoRunScheduledPhase() { return false; } +${extractFunction('normalizeSignupMethod')} +${extractFunction('normalizeHeroSmsReuseEnabledValue')} +${extractFunction('isPhoneSignupReuseLocked')} +${extractFunction('getStoredPhoneSmsReuseEnabled')} +${extractFunction('getStoredFreePhoneReuseEnabled')} +${extractFunction('getStoredFreePhoneReuseAutoEnabled')} +${extractFunction('restorePhoneReuseControlsFromState')} +${extractFunction('setElementReuseLockedState')} ${extractFunction('updatePhoneVerificationSettingsUI')} return { @@ -642,12 +688,23 @@ return { rowHeroSmsPriceTiers, rowHeroSmsCurrentCode, rowHeroSmsPreferredActivation, + rowFreePhoneReuseEnabled, + rowFreePhoneReuseAutoEnabled, + rowFreeReusablePhone, rowPhoneVerificationResendCount, rowPhoneReplacementLimit, rowPhoneCodeWaitSeconds, rowPhoneCodeTimeoutWindows, rowPhoneCodePollIntervalSeconds, rowPhoneCodePollMaxRounds, + heroSmsReuseRow, + inputHeroSmsReuseEnabled, + inputFreePhoneReuseEnabled, + inputFreePhoneReuseAutoEnabled, + selectHeroSmsPreferredActivation, + inputFreeReusablePhone, + btnSaveFreeReusablePhone, + btnClearFreeReusablePhone, setSelectedPhoneSmsProvider(value) { selectPhoneSmsProvider.value = value; }, updatePhoneVerificationSettingsUI, }; @@ -733,6 +790,46 @@ return { assert.equal(api.rowPhoneCodePollIntervalSeconds.style.display, ''); assert.equal(api.rowPhoneCodePollMaxRounds.style.display, ''); + api.setLatestState({ + signupMethod: 'phone', + signupPhoneNumber: '66959916439', + phoneSmsReuseEnabled: true, + freePhoneReuseEnabled: true, + freePhoneReuseAutoEnabled: true, + }); + api.updatePhoneVerificationSettingsUI(); + assert.equal(api.inputHeroSmsReuseEnabled.checked, false); + assert.equal(api.inputHeroSmsReuseEnabled.disabled, true); + assert.equal(api.inputFreePhoneReuseEnabled.checked, false); + assert.equal(api.inputFreePhoneReuseEnabled.disabled, true); + assert.equal(api.inputFreePhoneReuseAutoEnabled.checked, false); + assert.equal(api.inputFreePhoneReuseAutoEnabled.disabled, true); + assert.equal(api.selectHeroSmsPreferredActivation.disabled, true); + assert.equal(api.inputFreeReusablePhone.disabled, true); + assert.equal(api.btnSaveFreeReusablePhone.disabled, true); + assert.equal(api.btnClearFreeReusablePhone.disabled, true); + assert.equal(api.rowFreePhoneReuseEnabled.classList.contains('is-disabled'), true); + assert.equal(api.rowFreeReusablePhone.classList.contains('is-disabled'), true); + + api.setLatestState({ + signupMethod: 'email', + signupPhoneNumber: '', + phoneSmsReuseEnabled: true, + freePhoneReuseEnabled: true, + freePhoneReuseAutoEnabled: true, + }); + api.updatePhoneVerificationSettingsUI(); + assert.equal(api.inputHeroSmsReuseEnabled.checked, true); + assert.equal(api.inputHeroSmsReuseEnabled.disabled, false); + assert.equal(api.inputFreePhoneReuseEnabled.checked, true); + assert.equal(api.inputFreePhoneReuseEnabled.disabled, false); + assert.equal(api.inputFreePhoneReuseAutoEnabled.checked, true); + assert.equal(api.inputFreePhoneReuseAutoEnabled.disabled, false); + assert.equal(api.selectHeroSmsPreferredActivation.disabled, false); + assert.equal(api.inputFreeReusablePhone.disabled, false); + assert.equal(api.btnSaveFreeReusablePhone.disabled, false); + assert.equal(api.btnClearFreeReusablePhone.disabled, false); + api.setSelectedPhoneSmsProvider('5sim'); api.updatePhoneVerificationSettingsUI(); assert.equal(api.rowFiveSimApiKey.style.display, ''); @@ -759,6 +856,19 @@ let latestState = { fiveSimCountryOrder: ['thailand', 'england'], heroSmsMinPrice: '0.0444', fiveSimMinPrice: '0.3333', + phoneSmsReuseEnabled: false, + heroSmsReuseEnabled: false, + freePhoneReuseEnabled: false, + freePhoneReuseAutoEnabled: false, + phonePreferredActivation: { + provider: 'hero-sms', + activationId: 'stored-activation', + phoneNumber: '66950001111', + countryId: 52, + countryLabel: 'Thailand', + successfulUses: 2, + maxUses: 3, + }, }; let cloudflareDomainEditMode = false; let cloudflareTempEmailDomainEditMode = false; @@ -857,6 +967,9 @@ const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms'; const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim'; const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms'; const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS; +const SIGNUP_METHOD_EMAIL = 'email'; +const SIGNUP_METHOD_PHONE = 'phone'; +const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL; const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam'; const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)'; const DEFAULT_FIVE_SIM_OPERATOR = 'any'; @@ -916,6 +1029,11 @@ ${extractFunction('normalizeHeroSmsAcquirePriority')} ${extractFunction('normalizeHeroSmsCountryId')} ${extractFunction('normalizeHeroSmsCountryLabel')} ${extractFunction('getSelectedHeroSmsCountryOption')} +${extractFunction('normalizeSignupMethod')} +${extractFunction('isPhoneSignupReuseLocked')} +${extractFunction('getStoredPhoneSmsReuseEnabled')} +${extractFunction('getStoredFreePhoneReuseEnabled')} +${extractFunction('getStoredFreePhoneReuseAutoEnabled')} function syncHeroSmsFallbackSelectionOrderFromSelect() { return [{ id: 52, label: 'Thailand' }, { id: 16, label: 'United Kingdom' }]; } @@ -948,21 +1066,21 @@ return { collectSettingsPayload }; assert.equal(payload.nexSmsApiKey, 'nex-key'); assert.deepStrictEqual(payload.nexSmsCountryOrder, [1]); assert.equal(payload.nexSmsServiceCode, 'ot'); - assert.equal(payload.phoneSmsReuseEnabled, true); - assert.equal(payload.heroSmsReuseEnabled, true); - assert.equal(payload.freePhoneReuseEnabled, true); - assert.equal(payload.freePhoneReuseAutoEnabled, true); + assert.equal(payload.phoneSmsReuseEnabled, false); + assert.equal(payload.heroSmsReuseEnabled, false); + assert.equal(payload.freePhoneReuseEnabled, false); + assert.equal(payload.freePhoneReuseAutoEnabled, false); assert.equal(payload.heroSmsAcquirePriority, 'price'); assert.equal(payload.heroSmsMinPrice, '0.03'); assert.equal(payload.heroSmsMaxPrice, '0.12'); assert.equal(payload.heroSmsPreferredPrice, '0.0512'); assert.deepStrictEqual(payload.phonePreferredActivation, { provider: 'hero-sms', - activationId: 'demo-activation', - phoneNumber: '66958889999', + activationId: 'stored-activation', + phoneNumber: '66950001111', countryId: 52, countryLabel: 'Thailand', - successfulUses: 0, + successfulUses: 2, maxUses: 3, }); assert.equal(payload.phoneVerificationReplacementLimit, 5);