限制手机号注册流程使用号码复用
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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]
|
||||
);
|
||||
|
||||
@@ -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。
|
||||
@@ -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;
|
||||
|
||||
+181
-16
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user