From 127fd1c653bb893afc89fe67d0e43b0e4b2e6eab Mon Sep 17 00:00:00 2001
From: zhangkun <1184253234@qq.com>
Date: Tue, 28 Apr 2026 02:51:48 +0800
Subject: [PATCH 1/6] =?UTF-8?q?feat:=20=E6=89=8B=E5=8A=A8=E9=85=8D?=
=?UTF-8?q?=E7=BD=AE=20HeroSMS=20maxPrice=20=E5=B9=B6=E8=B0=83=E6=95=B4?=
=?UTF-8?q?=E6=89=8B=E6=9C=BA=E5=8F=B7=E5=A4=8D=E7=94=A8=E8=AE=A1=E6=95=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
background.js | 3 +
background/phone-verification-flow.js | 203 +++------
sidepanel/sidepanel.html | 5 +
sidepanel/sidepanel.js | 35 +-
tests/phone-verification-flow.test.js | 406 +++++++-----------
...epanel-phone-verification-settings.test.js | 8 +
6 files changed, 264 insertions(+), 396 deletions(-)
diff --git a/background.js b/background.js
index 97b85b1..187e9f6 100644
--- a/background.js
+++ b/background.js
@@ -477,6 +477,7 @@ const PERSISTED_SETTING_DEFAULTS = {
hotmailAccounts: [],
mail2925Accounts: [],
heroSmsApiKey: '',
+ heroSmsMaxPrice: '',
heroSmsCountryId: HERO_SMS_COUNTRY_ID,
heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL,
};
@@ -1316,6 +1317,8 @@ function normalizePersistentSettingValue(key, value) {
return normalizeMail2925Accounts(value);
case 'heroSmsApiKey':
return String(value || '');
+ case 'heroSmsMaxPrice':
+ return String(value || '').trim();
case 'heroSmsCountryId':
return Math.max(1, Math.floor(Number(value) || HERO_SMS_COUNTRY_ID));
case 'heroSmsCountryLabel':
diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js
index ca16eae..6b7fc84 100644
--- a/background/phone-verification-flow.js
+++ b/background/phone-verification-flow.js
@@ -27,7 +27,6 @@
const DEFAULT_PHONE_SUBMIT_ATTEMPTS = 3;
const DEFAULT_PHONE_CODE_WAIT_WINDOW_MS = 60000;
const DEFAULT_PHONE_NUMBER_MAX_USES = 3;
- const DEFAULT_PHONE_PRICE_LOOKUP_ATTEMPTS = 3;
const PHONE_CODE_TIMEOUT_ERROR_PREFIX = 'PHONE_CODE_TIMEOUT::';
const PHONE_RESTART_STEP7_ERROR_PREFIX = 'PHONE_RESTART_STEP7::';
@@ -47,6 +46,18 @@
return String(value || '').trim();
}
+ function normalizeManualHeroSmsMaxPrice(value) {
+ const trimmed = String(value ?? '').trim();
+ if (!trimmed) {
+ return null;
+ }
+ const price = Number(trimmed);
+ if (!Number.isFinite(price) || price <= 0) {
+ return null;
+ }
+ return String(price);
+ }
+
function normalizeUseCount(value) {
return Math.max(0, Math.floor(Number(value) || 0));
}
@@ -241,8 +252,13 @@
if (!apiKey) {
throw new Error('HeroSMS API key is missing. Save it in the side panel before running the phone flow.');
}
+ const maxPrice = normalizeManualHeroSmsMaxPrice(state.heroSmsMaxPrice);
+ if (!maxPrice) {
+ throw new Error('HeroSMS maxPrice is missing. Fill it in below the country selector before running the phone flow.');
+ }
return {
apiKey,
+ maxPrice,
baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL),
};
}
@@ -289,91 +305,10 @@
return activation?.statusAction === 'getStatusV2' ? 'getStatusV2' : 'getStatus';
}
- function normalizeHeroSmsPrice(value) {
- const price = Number(value);
- if (!Number.isFinite(price) || price < 0) {
- return null;
- }
- return price;
- }
-
- function collectHeroSmsPriceCandidates(payload, candidates = []) {
- if (Array.isArray(payload)) {
- payload.forEach((entry) => collectHeroSmsPriceCandidates(entry, candidates));
- return candidates;
- }
- if (!payload || typeof payload !== 'object') {
- return candidates;
- }
-
- const cost = normalizeHeroSmsPrice(payload.cost);
- if (cost !== null) {
- const count = Number(payload.count);
- const physicalCount = Number(payload.physicalCount);
- const hasCount = Number.isFinite(count);
- const hasPhysicalCount = Number.isFinite(physicalCount);
- if ((!hasCount && !hasPhysicalCount) || count > 0 || physicalCount > 0) {
- candidates.push(cost);
- }
- }
-
- Object.values(payload).forEach((value) => collectHeroSmsPriceCandidates(value, candidates));
- return candidates;
- }
-
- function findLowestHeroSmsPrice(payload) {
- const candidates = collectHeroSmsPriceCandidates(payload, []);
- if (!candidates.length) {
- return null;
- }
- return Math.min(...candidates);
- }
-
function isHeroSmsNoNumbersPayload(payload) {
return /\bNO_NUMBERS\b/i.test(describeHeroSmsPayload(payload));
}
- function extractHeroSmsWrongMaxPrice(payload) {
- if (payload && typeof payload === 'object') {
- const title = String(payload.title || '').trim();
- const minPrice = normalizeHeroSmsPrice(payload.info?.min);
- if (/^WRONG_MAX_PRICE$/i.test(title) && minPrice !== null) {
- return minPrice;
- }
- }
-
- const text = describeHeroSmsPayload(payload);
- const match = text.match(/\bWRONG_MAX_PRICE:(\d+(?:\.\d+)?)\b/i);
- if (!match) {
- return null;
- }
- return normalizeHeroSmsPrice(match[1]);
- }
-
- function isNetworkFetchFailure(error) {
- const message = String(error?.message || '').trim();
- return /failed to fetch|networkerror|load failed/i.test(message);
- }
-
- async function resolveCheapestPhoneActivationPrice(config, countryConfig) {
- for (let attempt = 1; attempt <= DEFAULT_PHONE_PRICE_LOOKUP_ATTEMPTS; attempt += 1) {
- try {
- const payload = await fetchHeroSmsPayload(config, {
- action: 'getPrices',
- service: HERO_SMS_SERVICE_CODE,
- country: countryConfig.id,
- }, 'HeroSMS getPrices');
- const price = findLowestHeroSmsPrice(payload);
- if (price !== null) {
- return price;
- }
- } catch (_) {
- // Best-effort lookup only.
- }
- }
- return null;
- }
-
async function fetchPhoneActivationPayload(config, countryConfig, action, options = {}) {
const query = {
action,
@@ -387,49 +322,10 @@
return fetchHeroSmsPayload(config, query, `HeroSMS ${action}`);
}
- async function requestPhoneActivationWithPrice(config, countryConfig, action, maxPrice) {
- let nextMaxPrice = maxPrice;
- let retriedWithUpdatedPrice = false;
- let retriedWithoutPrice = false;
-
- while (true) {
- try {
- return await fetchPhoneActivationPayload(config, countryConfig, action, {
- maxPrice: nextMaxPrice,
- });
- } catch (error) {
- const updatedMaxPrice = extractHeroSmsWrongMaxPrice(error?.payload || error?.message);
- if (
- nextMaxPrice !== null
- && nextMaxPrice !== undefined
- && !retriedWithUpdatedPrice
- && updatedMaxPrice !== null
- ) {
- nextMaxPrice = updatedMaxPrice;
- retriedWithUpdatedPrice = true;
- continue;
- }
-
- if (
- nextMaxPrice !== null
- && nextMaxPrice !== undefined
- && !retriedWithoutPrice
- && isNetworkFetchFailure(error)
- ) {
- nextMaxPrice = null;
- retriedWithoutPrice = true;
- continue;
- }
-
- throw error;
- }
- }
- }
-
async function requestPhoneActivation(state = {}) {
const config = resolvePhoneConfig(state);
const countryConfig = resolveCountryConfig(state);
- const maxPrice = await resolveCheapestPhoneActivationPrice(config, countryConfig);
+ const maxPrice = config.maxPrice;
const buildFallbackActivation = (requestAction) => ({
countryId: countryConfig.id,
...(requestAction === 'getNumberV2' ? { statusAction: 'getStatusV2' } : {}),
@@ -438,19 +334,19 @@
let payload;
try {
- payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
+ payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
} catch (error) {
if (!isHeroSmsNoNumbersPayload(error?.payload || error?.message)) {
throw error;
}
requestAction = 'getNumberV2';
- payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
+ payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
}
let activation = parseActivationPayload(payload, buildFallbackActivation(requestAction));
if (!activation && requestAction === 'getNumber' && isHeroSmsNoNumbersPayload(payload)) {
requestAction = 'getNumberV2';
- payload = await requestPhoneActivationWithPrice(config, countryConfig, requestAction, maxPrice);
+ payload = await fetchPhoneActivationPayload(config, countryConfig, requestAction, { maxPrice });
activation = parseActivationPayload(payload, buildFallbackActivation(requestAction));
}
@@ -496,7 +392,7 @@
}
async function completePhoneActivation(state = {}, activation) {
- await setPhoneActivationStatus(state, activation, 6, 'HeroSMS setStatus(6)');
+ await setPhoneActivationStatus(state, activation, 3, 'HeroSMS setStatus(3)');
}
async function cancelPhoneActivation(state = {}, activation) {
@@ -738,6 +634,20 @@
await persistReusableActivation(null);
}
+ async function incrementCurrentActivationUseCount(activation) {
+ const normalizedActivation = normalizeActivation(activation);
+ if (!normalizedActivation) {
+ return null;
+ }
+
+ const nextActivation = {
+ ...normalizedActivation,
+ successfulUses: Math.min(normalizedActivation.successfulUses + 1, normalizedActivation.maxUses),
+ };
+ await persistCurrentActivation(nextActivation);
+ return nextActivation;
+ }
+
async function acquirePhoneActivation(state = {}) {
const countryConfig = resolveCountryConfig(state);
const reusableActivation = normalizeActivation(state[REUSABLE_PHONE_ACTIVATION_STATE_KEY]);
@@ -769,28 +679,24 @@
return activation;
}
- async function markActivationReusableAfterSuccess(activation) {
+ async function syncReusableActivationAfterUse(activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
await clearReusableActivation();
return;
}
- const successfulUses = normalizedActivation.successfulUses + 1;
- if (successfulUses >= normalizedActivation.maxUses) {
+ if (normalizedActivation.successfulUses >= normalizedActivation.maxUses) {
await clearReusableActivation();
return;
}
- await persistReusableActivation({
- ...normalizedActivation,
- successfulUses,
- });
+ await persistReusableActivation(normalizedActivation);
}
async function waitForPhoneCodeOrRotateNumber(tabId, state, activation) {
- const normalizedActivation = normalizeActivation(activation);
- if (!normalizedActivation) {
+ let currentActivation = normalizeActivation(activation);
+ if (!currentActivation) {
throw new Error('Phone activation is missing.');
}
@@ -799,11 +705,11 @@
for (let windowIndex = 1; windowIndex <= 2; windowIndex += 1) {
await addLog(
- `Step 9: waiting up to 60 seconds for SMS on ${normalizedActivation.phoneNumber} (${windowIndex}/2).`,
+ `Step 9: waiting up to 60 seconds for SMS on ${currentActivation.phoneNumber} (${windowIndex}/2).`,
'info'
);
try {
- const code = await pollPhoneActivationCode(state, normalizedActivation, {
+ const code = await pollPhoneActivationCode(state, currentActivation, {
actionLabel: windowIndex === 1
? 'poll phone verification code from HeroSMS'
: 'poll resent phone verification code from HeroSMS',
@@ -820,7 +726,7 @@
lastLoggedStatus = statusText;
lastLoggedPollCount = pollCount;
await addLog(
- `Step 9: HeroSMS status for ${normalizedActivation.phoneNumber}: ${statusText} (${Math.ceil(elapsedMs / 1000)}s elapsed).`,
+ `Step 9: HeroSMS status for ${currentActivation.phoneNumber}: ${statusText} (${Math.ceil(elapsedMs / 1000)}s elapsed).`,
'info'
);
},
@@ -836,10 +742,10 @@
if (windowIndex === 1) {
await addLog(
- `Step 9: no SMS arrived for ${normalizedActivation.phoneNumber} within 60 seconds, requesting another SMS.`,
+ `Step 9: no SMS arrived for ${currentActivation.phoneNumber} within 60 seconds, requesting another SMS.`,
'warn'
);
- await requestAdditionalPhoneSms(state, normalizedActivation);
+ await requestAdditionalPhoneSms(state, currentActivation);
try {
await resendPhoneVerificationCode(tabId);
await addLog('Step 9: clicked "Resend text message" on the phone verification page.', 'info');
@@ -850,10 +756,10 @@
}
await addLog(
- `Step 9: still no SMS for ${normalizedActivation.phoneNumber} 60 seconds after resend, restarting from step 7 with a new number.`,
+ `Step 9: still no SMS for ${currentActivation.phoneNumber} 60 seconds after resend, restarting from step 7 with a new number.`,
'warn'
);
- throw buildPhoneRestartStep7Error(normalizedActivation.phoneNumber);
+ throw buildPhoneRestartStep7Error(currentActivation.phoneNumber);
}
}
@@ -965,8 +871,17 @@
continue;
}
- await completePhoneActivation(state, activation);
- await markActivationReusableAfterSuccess(activation);
+ activation = await incrementCurrentActivationUseCount(activation);
+ try {
+ await completePhoneActivation(state, activation);
+ await syncReusableActivationAfterUse(activation);
+ } catch (activationStatusError) {
+ await clearReusableActivation();
+ await addLog(
+ `Step 9: phone verification succeeded, but HeroSMS setStatus(3) failed. The next flow will request a new number. ${activationStatusError.message}`,
+ 'warn'
+ );
+ }
shouldCancelActivation = false;
await clearCurrentActivation();
await addLog('Step 9: phone verification finished, waiting for OAuth consent.', 'ok');
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 7db0a30..c0b7e0e 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -569,6 +569,11 @@
+
+ 最高价格
+
+
接码 API
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index 5d66c65..3dd2d10 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -292,8 +292,10 @@ const rowPhoneVerificationEnabled = document.getElementById('row-phone-verificat
const inputPhoneVerificationEnabled = document.getElementById('input-phone-verification-enabled');
const rowHeroSmsPlatform = document.getElementById('row-hero-sms-platform');
const rowHeroSmsCountry = document.getElementById('row-hero-sms-country');
+const rowHeroSmsMaxPrice = document.getElementById('row-hero-sms-max-price');
const rowHeroSmsApiKey = document.getElementById('row-hero-sms-api-key');
const inputHeroSmsApiKey = document.getElementById('input-hero-sms-api-key');
+const inputHeroSmsMaxPrice = document.getElementById('input-hero-sms-max-price');
const selectHeroSmsCountry = document.getElementById('select-hero-sms-country');
const displayHeroSmsPlatform = document.getElementById('display-hero-sms-platform');
const rowAccountRunHistoryHelperBaseUrl = document.getElementById('row-account-run-history-helper-base-url');
@@ -2200,6 +2202,9 @@ function collectSettingsPayload() {
const heroSmsApiKeyValue = typeof inputHeroSmsApiKey !== 'undefined' && inputHeroSmsApiKey
? (inputHeroSmsApiKey.value || '')
: '';
+ const heroSmsMaxPriceValue = typeof inputHeroSmsMaxPrice !== 'undefined' && inputHeroSmsMaxPrice
+ ? normalizeHeroSmsMaxPriceValue(inputHeroSmsMaxPrice.value)
+ : '';
const heroSmsCountry = typeof getSelectedHeroSmsCountryOption === 'function'
? getSelectedHeroSmsCountryOption()
: {
@@ -2299,6 +2304,7 @@ function collectSettingsPayload() {
DEFAULT_VERIFICATION_RESEND_COUNT
),
heroSmsApiKey: heroSmsApiKeyValue,
+ heroSmsMaxPrice: heroSmsMaxPriceValue,
heroSmsCountryId: heroSmsCountry.id,
heroSmsCountryLabel: heroSmsCountry.label,
};
@@ -2357,6 +2363,18 @@ function normalizeHeroSmsCountryLabel(value = '') {
return String(value || '').trim() || DEFAULT_HERO_SMS_COUNTRY_LABEL;
}
+function normalizeHeroSmsMaxPriceValue(value, fallback = '') {
+ const trimmed = String(value ?? '').trim();
+ if (!trimmed) {
+ return String(fallback || '').trim();
+ }
+ const numeric = Number(trimmed);
+ if (!Number.isFinite(numeric) || numeric <= 0) {
+ return String(fallback || '').trim();
+ }
+ return String(numeric);
+}
+
function getSelectedHeroSmsCountryOption() {
if (!selectHeroSmsCountry) {
return {
@@ -2472,7 +2490,7 @@ function updateAccountRunHistorySettingsUI() {
function updatePhoneVerificationSettingsUI() {
const enabled = Boolean(inputPhoneVerificationEnabled?.checked);
- [rowHeroSmsPlatform, rowHeroSmsCountry, rowHeroSmsApiKey].forEach((row) => {
+ [rowHeroSmsPlatform, rowHeroSmsCountry, rowHeroSmsMaxPrice, rowHeroSmsApiKey].forEach((row) => {
if (!row) {
return;
}
@@ -2983,6 +3001,9 @@ function applySettingsState(state) {
if (inputHeroSmsApiKey) {
inputHeroSmsApiKey.value = state?.heroSmsApiKey || '';
}
+ if (inputHeroSmsMaxPrice) {
+ inputHeroSmsMaxPrice.value = normalizeHeroSmsMaxPriceValue(state?.heroSmsMaxPrice);
+ }
if (selectHeroSmsCountry) {
const restoredCountryId = String(normalizeHeroSmsCountryId(state?.heroSmsCountryId));
if (Array.from(selectHeroSmsCountry.options).some((option) => option.value === restoredCountryId)) {
@@ -6413,6 +6434,15 @@ inputHeroSmsApiKey?.addEventListener('blur', () => {
saveSettings({ silent: true }).catch(() => { });
});
+inputHeroSmsMaxPrice?.addEventListener('input', () => {
+ markSettingsDirty(true);
+ scheduleSettingsAutoSave();
+});
+inputHeroSmsMaxPrice?.addEventListener('blur', () => {
+ inputHeroSmsMaxPrice.value = normalizeHeroSmsMaxPriceValue(inputHeroSmsMaxPrice.value);
+ saveSettings({ silent: true }).catch(() => { });
+});
+
selectHeroSmsCountry?.addEventListener('change', () => {
updateHeroSmsPlatformDisplay(getSelectedHeroSmsCountryOption().label);
markSettingsDirty(true);
@@ -6811,6 +6841,9 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.payload.heroSmsApiKey !== undefined && inputHeroSmsApiKey) {
inputHeroSmsApiKey.value = message.payload.heroSmsApiKey || '';
}
+ if (message.payload.heroSmsMaxPrice !== undefined && inputHeroSmsMaxPrice) {
+ inputHeroSmsMaxPrice.value = normalizeHeroSmsMaxPriceValue(message.payload.heroSmsMaxPrice);
+ }
if (message.payload.phoneVerificationEnabled !== undefined && inputPhoneVerificationEnabled) {
inputPhoneVerificationEnabled.checked = Boolean(message.payload.phoneVerificationEnabled);
updatePhoneVerificationSettingsUI();
diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js
index 9072848..441d21b 100644
--- a/tests/phone-verification-flow.test.js
+++ b/tests/phone-verification-flow.test.js
@@ -32,7 +32,7 @@ function buildHeroSmsStatusV2Payload({ smsCode = '', smsText = '', callCode = ''
});
}
-test('phone verification helper requests HeroSMS numbers with fixed OpenAI and Thailand parameters', async () => {
+test('phone verification helper requests HeroSMS numbers with manual maxPrice and fixed OpenAI/Thailand parameters', async () => {
const requests = [];
const helpers = api.createPhoneVerificationHelpers({
addLog: async () => {},
@@ -40,26 +40,22 @@ test('phone verification helper requests HeroSMS numbers with fixed OpenAI and T
fetchImpl: async (url) => {
const parsedUrl = new URL(url);
requests.push(parsedUrl);
- const action = parsedUrl.searchParams.get('action');
- if (action === 'getPrices') {
- return {
- ok: true,
- text: async () => buildHeroSmsPricesPayload(),
- };
- }
return {
ok: true,
text: async () => 'ACCESS_NUMBER:123456:66959916439',
};
},
- getState: async () => ({ heroSmsApiKey: 'demo-key' }),
+ getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '0.08' }),
sendToContentScriptResilient: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
- const activation = await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
+ const activation = await helpers.requestPhoneActivation({
+ heroSmsApiKey: 'demo-key',
+ heroSmsMaxPrice: '0.08',
+ });
assert.deepStrictEqual(activation, {
activationId: '123456',
@@ -70,111 +66,33 @@ test('phone verification helper requests HeroSMS numbers with fixed OpenAI and T
successfulUses: 0,
maxUses: 3,
});
- assert.equal(requests.length, 2);
- assert.equal(requests[0].searchParams.get('action'), 'getPrices');
+ assert.equal(requests.length, 1);
+ assert.equal(requests[0].searchParams.get('action'), 'getNumber');
assert.equal(requests[0].searchParams.get('service'), 'dr');
assert.equal(requests[0].searchParams.get('country'), '52');
assert.equal(requests[0].searchParams.get('api_key'), 'demo-key');
- assert.equal(requests[1].searchParams.get('action'), 'getNumber');
- assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
- assert.equal(requests[1].searchParams.get('fixedPrice'), 'true');
- assert.equal(requests[1].searchParams.get('service'), 'dr');
- assert.equal(requests[1].searchParams.get('country'), '52');
- assert.equal(requests[1].searchParams.get('api_key'), 'demo-key');
+ assert.equal(requests[0].searchParams.get('maxPrice'), '0.08');
+ assert.equal(requests[0].searchParams.get('fixedPrice'), 'true');
});
-test('phone verification helper retries HeroSMS getPrices until it receives a usable lowest price', async () => {
- const requests = [];
- let getPricesAttempt = 0;
+test('phone verification helper requires manual HeroSMS maxPrice', async () => {
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');
- if (action === 'getPrices') {
- getPricesAttempt += 1;
- return getPricesAttempt < 3
- ? {
- ok: true,
- text: async () => JSON.stringify({ unavailable: true }),
- }
- : {
- ok: true,
- text: async () => buildHeroSmsPricesPayload({ cost: 0.09 }),
- };
- }
- if (action === 'getNumber') {
- return {
- ok: true,
- text: async () => 'ACCESS_NUMBER:123456:66959916439',
- };
- }
- throw new Error(`Unexpected HeroSMS action: ${action}`);
+ fetchImpl: async () => {
+ throw new Error('should not request HeroSMS without maxPrice');
},
- getState: async () => ({ heroSmsApiKey: 'demo-key' }),
+ getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '' }),
sendToContentScriptResilient: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
- await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
-
- assert.equal(requests.length, 4);
- assert.equal(requests[0].searchParams.get('action'), 'getPrices');
- assert.equal(requests[1].searchParams.get('action'), 'getPrices');
- assert.equal(requests[2].searchParams.get('action'), 'getPrices');
- assert.equal(requests[3].searchParams.get('action'), 'getNumber');
- assert.equal(requests[3].searchParams.get('maxPrice'), '0.09');
- assert.equal(requests[3].searchParams.get('fixedPrice'), 'true');
-});
-
-test('phone verification helper falls back to plain getNumber only after HeroSMS getPrices fails three times', async () => {
- const requests = [];
- let getPricesAttempt = 0;
- 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');
- if (action === 'getPrices') {
- getPricesAttempt += 1;
- return {
- ok: true,
- text: async () => JSON.stringify({ unavailable: getPricesAttempt }),
- };
- }
- if (action === 'getNumber') {
- return {
- ok: true,
- text: async () => 'ACCESS_NUMBER:123456:66959916439',
- };
- }
- throw new Error(`Unexpected HeroSMS action: ${action}`);
- },
- getState: async () => ({ heroSmsApiKey: 'demo-key' }),
- sendToContentScriptResilient: async () => ({}),
- setState: async () => {},
- sleepWithStop: async () => {},
- throwIfStopped: () => {},
- });
-
- await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
-
- assert.equal(requests.length, 4);
- assert.equal(requests[0].searchParams.get('action'), 'getPrices');
- assert.equal(requests[1].searchParams.get('action'), 'getPrices');
- assert.equal(requests[2].searchParams.get('action'), 'getPrices');
- assert.equal(requests[2].searchParams.get('service'), 'dr');
- assert.equal(requests[2].searchParams.get('country'), '52');
- assert.equal(requests[2].searchParams.get('api_key'), 'demo-key');
- assert.equal(requests[3].searchParams.get('action'), 'getNumber');
- assert.equal(requests[3].searchParams.get('maxPrice'), null);
- assert.equal(requests[3].searchParams.get('fixedPrice'), null);
+ await assert.rejects(
+ () => helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '' }),
+ /HeroSMS maxPrice is missing/i
+ );
});
test('phone verification helper retries with HeroSMS getNumberV2 when getNumber reports NO_NUMBERS', async () => {
@@ -186,12 +104,6 @@ test('phone verification helper retries with HeroSMS getNumberV2 when getNumber
const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
- if (action === 'getPrices') {
- return {
- ok: true,
- text: async () => buildHeroSmsPricesPayload({ country: '16' }),
- };
- }
if (action === 'getNumber') {
return {
ok: true,
@@ -209,7 +121,7 @@ test('phone verification helper retries with HeroSMS getNumberV2 when getNumber
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
- getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsCountryId: 16 }),
+ getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '0.08', heroSmsCountryId: 16 }),
sendToContentScriptResilient: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
@@ -218,6 +130,7 @@ test('phone verification helper retries with HeroSMS getNumberV2 when getNumber
const activation = await helpers.requestPhoneActivation({
heroSmsApiKey: 'demo-key',
+ heroSmsMaxPrice: '0.08',
heroSmsCountryId: 16,
});
@@ -231,17 +144,15 @@ test('phone verification helper retries with HeroSMS getNumberV2 when getNumber
maxUses: 3,
statusAction: 'getStatusV2',
});
- assert.equal(requests.length, 3);
- assert.equal(requests[0].searchParams.get('action'), 'getPrices');
+ assert.equal(requests.length, 2);
+ assert.equal(requests[0].searchParams.get('action'), 'getNumber');
assert.equal(requests[0].searchParams.get('country'), '16');
- assert.equal(requests[1].searchParams.get('action'), 'getNumber');
+ assert.equal(requests[0].searchParams.get('maxPrice'), '0.08');
+ assert.equal(requests[0].searchParams.get('fixedPrice'), 'true');
+ assert.equal(requests[1].searchParams.get('action'), 'getNumberV2');
assert.equal(requests[1].searchParams.get('country'), '16');
assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
assert.equal(requests[1].searchParams.get('fixedPrice'), 'true');
- assert.equal(requests[2].searchParams.get('action'), 'getNumberV2');
- assert.equal(requests[2].searchParams.get('country'), '16');
- assert.equal(requests[2].searchParams.get('maxPrice'), '0.08');
- assert.equal(requests[2].searchParams.get('fixedPrice'), 'true');
});
test('phone verification helper uses HeroSMS getStatusV2 after acquiring a number via getNumberV2', async () => {
@@ -249,6 +160,7 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
const stateUpdates = [];
let currentState = {
heroSmsApiKey: 'demo-key',
+ heroSmsMaxPrice: '0.08',
heroSmsCountryId: 16,
heroSmsCountryLabel: 'United Kingdom',
verificationResendCount: 0,
@@ -264,12 +176,6 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
- if (action === 'getPrices') {
- return {
- ok: true,
- text: async () => buildHeroSmsPricesPayload({ country: '16' }),
- };
- }
if (action === 'getNumber') {
return {
ok: true,
@@ -354,6 +260,18 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
statusAction: 'getStatusV2',
},
},
+ {
+ currentPhoneActivation: {
+ activationId: '654321',
+ phoneNumber: '447911123456',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 16,
+ successfulUses: 1,
+ maxUses: 3,
+ statusAction: 'getStatusV2',
+ },
+ },
{
reusablePhoneActivation: {
activationId: '654321',
@@ -372,7 +290,6 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
]);
const actions = requests.map((url) => url.searchParams.get('action'));
assert.deepStrictEqual(actions, [
- 'getPrices',
'getNumber',
'getNumberV2',
'getStatusV2',
@@ -381,117 +298,32 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
]);
});
-test('phone verification helper refreshes maxPrice when HeroSMS returns WRONG_MAX_PRICE', async () => {
- const requests = [];
- let getNumberAttempt = 0;
+test('phone verification helper keeps the user-provided maxPrice and surfaces HeroSMS price errors', async () => {
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');
- if (action === 'getPrices') {
- return {
- ok: true,
- text: async () => buildHeroSmsPricesPayload(),
- };
- }
if (action === 'getNumber') {
- getNumberAttempt += 1;
- return getNumberAttempt === 1
- ? {
- ok: false,
- text: async () => 'WRONG_MAX_PRICE:0.09',
- }
- : {
- ok: true,
- text: async () => 'ACCESS_NUMBER:123456:66959916439',
- };
+ return {
+ ok: false,
+ text: async () => 'WRONG_MAX_PRICE:0.09',
+ };
}
throw new Error(`Unexpected HeroSMS action: ${action}`);
},
- getState: async () => ({ heroSmsApiKey: 'demo-key' }),
+ getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '0.08' }),
sendToContentScriptResilient: async () => ({}),
setState: async () => {},
sleepWithStop: async () => {},
throwIfStopped: () => {},
});
- const activation = await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
-
- assert.deepStrictEqual(activation, {
- activationId: '123456',
- phoneNumber: '66959916439',
- provider: 'hero-sms',
- serviceCode: 'dr',
- countryId: 52,
- successfulUses: 0,
- maxUses: 3,
- });
- assert.equal(requests.length, 3);
- assert.equal(requests[0].searchParams.get('action'), 'getPrices');
- assert.equal(requests[1].searchParams.get('action'), 'getNumber');
- assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
- assert.equal(requests[2].searchParams.get('action'), 'getNumber');
- assert.equal(requests[2].searchParams.get('maxPrice'), '0.09');
- assert.equal(requests[2].searchParams.get('fixedPrice'), 'true');
-});
-
-test('phone verification helper falls back to plain getNumber when priced request fails to fetch', async () => {
- const requests = [];
- let getNumberAttempt = 0;
- 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');
- if (action === 'getPrices') {
- return {
- ok: true,
- text: async () => buildHeroSmsPricesPayload(),
- };
- }
- if (action === 'getNumber') {
- getNumberAttempt += 1;
- if (getNumberAttempt === 1) {
- throw new TypeError('Failed to fetch');
- }
- return {
- ok: true,
- text: async () => 'ACCESS_NUMBER:123456:66959916439',
- };
- }
- throw new Error(`Unexpected HeroSMS action: ${action}`);
- },
- getState: async () => ({ heroSmsApiKey: 'demo-key' }),
- sendToContentScriptResilient: async () => ({}),
- setState: async () => {},
- sleepWithStop: async () => {},
- throwIfStopped: () => {},
- });
-
- const activation = await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
-
- assert.deepStrictEqual(activation, {
- activationId: '123456',
- phoneNumber: '66959916439',
- provider: 'hero-sms',
- serviceCode: 'dr',
- countryId: 52,
- successfulUses: 0,
- maxUses: 3,
- });
- assert.equal(requests.length, 3);
- assert.equal(requests[0].searchParams.get('action'), 'getPrices');
- assert.equal(requests[1].searchParams.get('action'), 'getNumber');
- assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
- assert.equal(requests[1].searchParams.get('fixedPrice'), 'true');
- assert.equal(requests[2].searchParams.get('action'), 'getNumber');
- assert.equal(requests[2].searchParams.get('maxPrice'), null);
- assert.equal(requests[2].searchParams.get('fixedPrice'), null);
+ await assert.rejects(
+ () => helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key', heroSmsMaxPrice: '0.08' }),
+ /WRONG_MAX_PRICE:0.09/
+ );
});
test('phone verification helper completes add-phone flow, clears current activation, and stores reusable number state', async () => {
@@ -499,6 +331,7 @@ test('phone verification helper completes add-phone flow, clears current activat
const stateUpdates = [];
let currentState = {
heroSmsApiKey: 'demo-key',
+ heroSmsMaxPrice: '0.08',
verificationResendCount: 1,
currentPhoneActivation: null,
reusablePhoneActivation: null,
@@ -511,12 +344,6 @@ test('phone verification helper completes add-phone flow, clears current activat
const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
- if (action === 'getPrices') {
- return {
- ok: true,
- text: async () => buildHeroSmsPricesPayload(),
- };
- }
if (action === 'getNumber') {
return {
ok: true,
@@ -586,6 +413,17 @@ test('phone verification helper completes add-phone flow, clears current activat
maxUses: 3,
},
},
+ {
+ currentPhoneActivation: {
+ activationId: '123456',
+ phoneNumber: '66959916439',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 52,
+ successfulUses: 1,
+ maxUses: 3,
+ },
+ },
{
reusablePhoneActivation: {
activationId: '123456',
@@ -603,7 +441,86 @@ test('phone verification helper completes add-phone flow, clears current activat
]);
const actions = requests.map((url) => url.searchParams.get('action'));
- assert.deepStrictEqual(actions, ['getPrices', 'getNumber', 'getStatus', 'setStatus']);
+ assert.deepStrictEqual(actions, ['getNumber', 'getStatus', 'setStatus']);
+});
+
+test('phone verification helper still succeeds when HeroSMS setStatus(3) fails after a successful submit', async () => {
+ const requests = [];
+ let currentState = {
+ heroSmsApiKey: 'demo-key',
+ heroSmsMaxPrice: '0.08',
+ verificationResendCount: 1,
+ currentPhoneActivation: null,
+ reusablePhoneActivation: null,
+ };
+
+ 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');
+ if (action === 'getNumber') {
+ return {
+ ok: true,
+ text: async () => 'ACCESS_NUMBER:123456:66959916439',
+ };
+ }
+ if (action === 'getStatus') {
+ return {
+ ok: true,
+ text: async () => 'STATUS_OK:654321',
+ };
+ }
+ if (action === 'setStatus') {
+ return {
+ ok: false,
+ text: async () => 'TEMPORARY_ERROR',
+ };
+ }
+ throw new Error(`Unexpected HeroSMS action: ${action}`);
+ },
+ 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(currentState.currentPhoneActivation, null);
+ assert.deepStrictEqual(currentState.reusablePhoneActivation, null);
+ const actions = requests.map((url) => url.searchParams.get('action'));
+ assert.deepStrictEqual(actions, ['getNumber', 'getStatus', 'setStatus']);
});
test('phone verification helper uses the configured HeroSMS country for both number acquisition and add-phone submission', async () => {
@@ -611,6 +528,7 @@ test('phone verification helper uses the configured HeroSMS country for both num
const submittedPayloads = [];
let currentState = {
heroSmsApiKey: 'demo-key',
+ heroSmsMaxPrice: '0.08',
heroSmsCountryId: 16,
heroSmsCountryLabel: 'United Kingdom',
verificationResendCount: 0,
@@ -625,12 +543,6 @@ test('phone verification helper uses the configured HeroSMS country for both num
const parsedUrl = new URL(url);
requests.push(parsedUrl);
const action = parsedUrl.searchParams.get('action');
- if (action === 'getPrices') {
- return {
- ok: true,
- text: async () => buildHeroSmsPricesPayload({ country: '16' }),
- };
- }
if (action === 'getNumber') {
return {
ok: true,
@@ -688,12 +600,10 @@ test('phone verification helper uses the configured HeroSMS country for both num
consentReady: true,
url: 'https://auth.openai.com/authorize',
});
- assert.equal(requests[0].searchParams.get('action'), 'getPrices');
+ assert.equal(requests[0].searchParams.get('action'), 'getNumber');
assert.equal(requests[0].searchParams.get('country'), '16');
- assert.equal(requests[1].searchParams.get('action'), 'getNumber');
- assert.equal(requests[1].searchParams.get('country'), '16');
- assert.equal(requests[1].searchParams.get('maxPrice'), '0.08');
- assert.equal(requests[1].searchParams.get('fixedPrice'), 'true');
+ assert.equal(requests[0].searchParams.get('maxPrice'), '0.08');
+ assert.equal(requests[0].searchParams.get('fixedPrice'), 'true');
assert.deepStrictEqual(submittedPayloads, [{
phoneNumber: '447911123456',
countryId: 16,
@@ -704,8 +614,10 @@ test('phone verification helper uses the configured HeroSMS country for both num
test('phone verification helper throws a step-7 restart error after 60 seconds plus one resend window without SMS', async () => {
const requests = [];
const messages = [];
+ const stateUpdates = [];
let currentState = {
heroSmsApiKey: 'demo-key',
+ heroSmsMaxPrice: '0.08',
verificationResendCount: 0,
currentPhoneActivation: null,
reusablePhoneActivation: null,
@@ -725,13 +637,6 @@ test('phone verification helper throws a step-7 restart error after 60 seconds p
const action = parsedUrl.searchParams.get('action');
const id = parsedUrl.searchParams.get('id');
- if (action === 'getPrices') {
- return {
- ok: true,
- text: async () => buildHeroSmsPricesPayload(),
- };
- }
-
if (action === 'getNumber') {
return {
ok: true,
@@ -775,6 +680,7 @@ test('phone verification helper throws a step-7 restart error after 60 seconds p
throw new Error(`Unexpected content-script message: ${message.type}`);
},
setState: async (updates) => {
+ stateUpdates.push(updates);
currentState = { ...currentState, ...updates };
},
sleepWithStop: async () => {
@@ -799,7 +705,6 @@ test('phone verification helper throws a step-7 restart error after 60 seconds p
const actions = requests.map((url) => `${url.searchParams.get('action')}:${url.searchParams.get('id') || ''}`);
assert.deepStrictEqual(actions, [
- 'getPrices:',
'getNumber:',
'getStatus:123456',
'setStatus:123456',
@@ -807,6 +712,11 @@ test('phone verification helper throws a step-7 restart error after 60 seconds p
'setStatus:123456',
]);
assert.equal(currentState.currentPhoneActivation, null);
+ assert.equal(
+ stateUpdates.some((updates) => Number(updates.currentPhoneActivation?.successfulUses) > 0),
+ false,
+ '60 seconds without SMS should not increment reuse count'
+ );
} finally {
Date.now = realDateNow;
}
@@ -817,6 +727,7 @@ test('phone verification helper replaces the number when code submission returns
const messages = [];
let currentState = {
heroSmsApiKey: 'demo-key',
+ heroSmsMaxPrice: '0.08',
verificationResendCount: 1,
currentPhoneActivation: null,
reusablePhoneActivation: null,
@@ -838,13 +749,6 @@ test('phone verification helper replaces the number when code submission returns
const action = parsedUrl.searchParams.get('action');
const id = parsedUrl.searchParams.get('id');
- if (action === 'getPrices') {
- return {
- ok: true,
- text: async () => buildHeroSmsPricesPayload(),
- };
- }
-
if (action === 'getNumber') {
const nextNumber = numbers[numberIndex];
numberIndex += 1;
@@ -925,11 +829,9 @@ test('phone verification helper replaces the number when code submission returns
const actions = requests.map((url) => `${url.searchParams.get('action')}:${url.searchParams.get('id') || ''}`);
assert.deepStrictEqual(actions, [
- 'getPrices:',
'getNumber:',
'getStatus:111111',
'setStatus:111111',
- 'getPrices:',
'getNumber:',
'getStatus:222222',
'setStatus:222222',
@@ -950,6 +852,7 @@ test('phone verification helper reuses the same number up to three successful re
const requests = [];
let currentState = {
heroSmsApiKey: 'demo-key',
+ heroSmsMaxPrice: '0.08',
verificationResendCount: 0,
currentPhoneActivation: null,
reusablePhoneActivation: {
@@ -1038,6 +941,7 @@ test('phone verification helper keeps maxUses behavior for reused V2 activations
const requests = [];
let currentState = {
heroSmsApiKey: 'demo-key',
+ heroSmsMaxPrice: '0.08',
heroSmsCountryId: 16,
heroSmsCountryLabel: 'United Kingdom',
verificationResendCount: 0,
diff --git a/tests/sidepanel-phone-verification-settings.test.js b/tests/sidepanel-phone-verification-settings.test.js
index bc75814..6a517a2 100644
--- a/tests/sidepanel-phone-verification-settings.test.js
+++ b/tests/sidepanel-phone-verification-settings.test.js
@@ -59,6 +59,7 @@ test('sidepanel html exposes phone verification toggle and dedicated HeroSMS row
assert.match(html, /id="input-phone-verification-enabled"/);
assert.match(html, /id="row-hero-sms-platform"/);
assert.match(html, /id="row-hero-sms-country"/);
+ assert.match(html, /id="row-hero-sms-max-price"/);
assert.match(html, /id="row-hero-sms-api-key"/);
assert.doesNotMatch(html, /id="input-account-run-history-text-enabled"/);
});
@@ -68,6 +69,7 @@ test('updatePhoneVerificationSettingsUI toggles HeroSMS rows from the sms switch
const inputPhoneVerificationEnabled = { checked: false };
const rowHeroSmsPlatform = { style: { display: 'none' } };
const rowHeroSmsCountry = { style: { display: 'none' } };
+const rowHeroSmsMaxPrice = { style: { display: 'none' } };
const rowHeroSmsApiKey = { style: { display: 'none' } };
${extractFunction('updatePhoneVerificationSettingsUI')}
@@ -76,6 +78,7 @@ return {
inputPhoneVerificationEnabled,
rowHeroSmsPlatform,
rowHeroSmsCountry,
+ rowHeroSmsMaxPrice,
rowHeroSmsApiKey,
updatePhoneVerificationSettingsUI,
};
@@ -84,12 +87,14 @@ return {
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowHeroSmsPlatform.style.display, 'none');
assert.equal(api.rowHeroSmsCountry.style.display, 'none');
+ assert.equal(api.rowHeroSmsMaxPrice.style.display, 'none');
assert.equal(api.rowHeroSmsApiKey.style.display, 'none');
api.inputPhoneVerificationEnabled.checked = true;
api.updatePhoneVerificationSettingsUI();
assert.equal(api.rowHeroSmsPlatform.style.display, '');
assert.equal(api.rowHeroSmsCountry.style.display, '');
+ assert.equal(api.rowHeroSmsMaxPrice.style.display, '');
assert.equal(api.rowHeroSmsApiKey.style.display, '');
});
@@ -141,6 +146,7 @@ const inputAutoStepDelaySeconds = { value: '' };
const inputPhoneVerificationEnabled = { checked: true };
const inputVerificationResendCount = { value: '4' };
const inputHeroSmsApiKey = { value: 'demo-key' };
+const inputHeroSmsMaxPrice = { value: '0.08' };
const inputAccountRunHistoryHelperBaseUrl = { value: 'http://127.0.0.1:17373' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
@@ -169,6 +175,7 @@ function normalizeAutoStepDelaySeconds(value) { return value === '' ? null : Num
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
${extractFunction('normalizeHeroSmsCountryId')}
${extractFunction('normalizeHeroSmsCountryLabel')}
+${extractFunction('normalizeHeroSmsMaxPriceValue')}
${extractFunction('getSelectedHeroSmsCountryOption')}
${extractFunction('collectSettingsPayload')}
return { collectSettingsPayload };
@@ -180,6 +187,7 @@ return { collectSettingsPayload };
assert.equal(payload.accountRunHistoryTextEnabled, true);
assert.equal(payload.accountRunHistoryHelperBaseUrl, 'http://127.0.0.1:17373');
assert.equal(payload.heroSmsApiKey, 'demo-key');
+ assert.equal(payload.heroSmsMaxPrice, '0.08');
assert.equal(payload.heroSmsCountryId, 52);
assert.equal(payload.heroSmsCountryLabel, 'Thailand');
});
From 5c804fad6e407cb63d00319694cf23985554765f Mon Sep 17 00:00:00 2001
From: zhangkun <1184253234@qq.com>
Date: Tue, 28 Apr 2026 14:15:07 +0800
Subject: [PATCH 2/6] fix: finalize HeroSMS reuse after full flow success
---
background.js | 17 ++
background/auto-run-controller.js | 1 +
background/message-router.js | 4 +
background/phone-verification-flow.js | 45 +++-
tests/auto-run-fresh-attempt-reset.test.js | 16 ++
tests/background-luckmail.test.js | 1 +
...ckground-message-router-step2-skip.test.js | 32 +++
tests/phone-verification-flow.test.js | 213 +++++++++++++++---
tests/step9-localhost-cleanup-scope.test.js | 1 +
9 files changed, 294 insertions(+), 36 deletions(-)
diff --git a/background.js b/background.js
index 187e9f6..5eb6bd1 100644
--- a/background.js
+++ b/background.js
@@ -560,6 +560,7 @@ const DEFAULT_STATE = {
currentLuckmailMailCursor: null,
currentPhoneActivation: null,
reusablePhoneActivation: null,
+ pendingPhoneActivationConfirmation: null,
autoRunning: false, // 当前是否处于自动运行中。
autoRunPhase: 'idle', // 当前自动运行阶段。
autoRunCurrentRun: 0, // 自动运行当前执行到第几轮。
@@ -5031,6 +5032,13 @@ async function finalizeIcloudAliasAfterSuccessfulFlow(state) {
}
}
+async function finalizePhoneActivationAfterSuccessfulFlow(state) {
+ if (typeof phoneVerificationHelpers?.finalizePendingPhoneActivationConfirmation !== 'function') {
+ return null;
+ }
+ return phoneVerificationHelpers.finalizePendingPhoneActivationConfirmation(state);
+}
+
// ============================================================
// Tab Registry
// ============================================================
@@ -5766,6 +5774,7 @@ function getDownstreamStateResets(step, state = {}) {
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
+ pendingPhoneActivationConfirmation: null,
lastSignupCode: null,
lastLoginCode: null,
localhostUrl: null,
@@ -5780,6 +5789,7 @@ function getDownstreamStateResets(step, state = {}) {
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
+ pendingPhoneActivationConfirmation: null,
lastSignupCode: null,
lastLoginCode: null,
localhostUrl: null,
@@ -5793,6 +5803,7 @@ function getDownstreamStateResets(step, state = {}) {
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
+ pendingPhoneActivationConfirmation: null,
lastSignupCode: null,
lastLoginCode: null,
localhostUrl: null,
@@ -5815,11 +5826,13 @@ function getDownstreamStateResets(step, state = {}) {
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
+ pendingPhoneActivationConfirmation: null,
localhostUrl: null,
};
}
if (step === 9) {
return {
+ pendingPhoneActivationConfirmation: null,
plusReturnUrl: '',
localhostUrl: null,
};
@@ -5830,11 +5843,13 @@ function getDownstreamStateResets(step, state = {}) {
loginVerificationRequestedAt: null,
oauthFlowDeadlineAt: null,
oauthFlowDeadlineSourceUrl: null,
+ pendingPhoneActivationConfirmation: null,
localhostUrl: null,
};
}
if (stepKey === 'confirm-oauth') {
return {
+ pendingPhoneActivationConfirmation: null,
localhostUrl: null,
};
}
@@ -6676,6 +6691,7 @@ async function handleStepData(step, payload) {
excludeLocalhostCallbacks: true,
});
}
+ await finalizePhoneActivationAfterSuccessfulFlow(latestState);
await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
const shouldClearCustomPoolEmail = String(latestState?.emailGenerator || '').trim().toLowerCase() === (
typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
@@ -8528,6 +8544,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
executeStepViaCompletionSignal,
exportSettingsBundle,
fetchGeneratedEmail,
+ finalizePhoneActivationAfterSuccessfulFlow,
finalizeStep3Completion: async () => {
const currentState = await getState();
const signupTabId = await getTabId('signup-page');
diff --git a/background/auto-run-controller.js b/background/auto-run-controller.js
index 37705a9..da1d0ea 100644
--- a/background/auto-run-controller.js
+++ b/background/auto-run-controller.js
@@ -391,6 +391,7 @@
inbucketMailbox: prevState.inbucketMailbox,
cloudflareDomain: prevState.cloudflareDomain,
cloudflareDomains: prevState.cloudflareDomains,
+ reusablePhoneActivation: prevState.reusablePhoneActivation,
autoRunRoundSummaries: serializeAutoRunRoundSummaries(totalRuns, roundSummaries),
autoRunSessionId: sessionId,
tabRegistry: {},
diff --git a/background/message-router.js b/background/message-router.js
index 2c5886e..c0dd637 100644
--- a/background/message-router.js
+++ b/background/message-router.js
@@ -32,6 +32,7 @@
executeStepViaCompletionSignal,
exportSettingsBundle,
fetchGeneratedEmail,
+ finalizePhoneActivationAfterSuccessfulFlow,
finalizeStep3Completion,
finalizeIcloudAliasAfterSuccessfulFlow,
findHotmailAccount,
@@ -202,6 +203,9 @@
excludeLocalhostCallbacks: true,
});
}
+ if (typeof finalizePhoneActivationAfterSuccessfulFlow === 'function') {
+ await finalizePhoneActivationAfterSuccessfulFlow(latestState);
+ }
await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
}
diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js
index 6b7fc84..009fbb3 100644
--- a/background/phone-verification-flow.js
+++ b/background/phone-verification-flow.js
@@ -21,6 +21,7 @@
const PHONE_ACTIVATION_STATE_KEY = 'currentPhoneActivation';
const REUSABLE_PHONE_ACTIVATION_STATE_KEY = 'reusablePhoneActivation';
+ const PENDING_PHONE_ACTIVATION_CONFIRMATION_STATE_KEY = 'pendingPhoneActivationConfirmation';
const DEFAULT_PHONE_POLL_INTERVAL_MS = 5000;
const DEFAULT_PHONE_POLL_TIMEOUT_MS = 180000;
const DEFAULT_PHONE_REQUEST_TIMEOUT_MS = 20000;
@@ -634,18 +635,16 @@
await persistReusableActivation(null);
}
- async function incrementCurrentActivationUseCount(activation) {
+ function incrementActivationUseCount(activation) {
const normalizedActivation = normalizeActivation(activation);
if (!normalizedActivation) {
return null;
}
- const nextActivation = {
+ return {
...normalizedActivation,
successfulUses: Math.min(normalizedActivation.successfulUses + 1, normalizedActivation.maxUses),
};
- await persistCurrentActivation(nextActivation);
- return nextActivation;
}
async function acquirePhoneActivation(state = {}) {
@@ -694,6 +693,35 @@
await persistReusableActivation(normalizedActivation);
}
+ async function persistPendingPhoneActivationConfirmation(activation) {
+ await setState({
+ [PENDING_PHONE_ACTIVATION_CONFIRMATION_STATE_KEY]: activation || null,
+ });
+ }
+
+ async function clearPendingPhoneActivationConfirmation() {
+ await persistPendingPhoneActivationConfirmation(null);
+ }
+
+ async function finalizePendingPhoneActivationConfirmation(stateOverride = null) {
+ const state = stateOverride || await getState();
+ const pendingActivation = normalizeActivation(state[PENDING_PHONE_ACTIVATION_CONFIRMATION_STATE_KEY]);
+ if (!pendingActivation) {
+ return null;
+ }
+
+ const committedActivation = incrementActivationUseCount(pendingActivation);
+ if (!committedActivation) {
+ await clearPendingPhoneActivationConfirmation();
+ await clearReusableActivation();
+ return null;
+ }
+
+ await syncReusableActivationAfterUse(committedActivation);
+ await clearPendingPhoneActivationConfirmation();
+ return committedActivation;
+ }
+
async function waitForPhoneCodeOrRotateNumber(tabId, state, activation) {
let currentActivation = normalizeActivation(activation);
if (!currentActivation) {
@@ -781,6 +809,9 @@
}
if (pageState?.addPhonePage) {
+ if (normalizeActivation(state[PENDING_PHONE_ACTIVATION_CONFIRMATION_STATE_KEY])) {
+ await clearPendingPhoneActivationConfirmation();
+ }
if (activation) {
await cancelPhoneActivation(state, activation);
await clearCurrentActivation();
@@ -871,12 +902,13 @@
continue;
}
- activation = await incrementCurrentActivationUseCount(activation);
try {
await completePhoneActivation(state, activation);
- await syncReusableActivationAfterUse(activation);
+ await persistReusableActivation(activation);
+ await persistPendingPhoneActivationConfirmation(activation);
} catch (activationStatusError) {
await clearReusableActivation();
+ await clearPendingPhoneActivationConfirmation();
await addLog(
`Step 9: phone verification succeeded, but HeroSMS setStatus(3) failed. The next flow will request a new number. ${activationStatusError.message}`,
'warn'
@@ -903,6 +935,7 @@
return {
completePhoneVerificationFlow,
+ finalizePendingPhoneActivationConfirmation,
normalizeActivation,
pollPhoneActivationCode,
reactivatePhoneActivation,
diff --git a/tests/auto-run-fresh-attempt-reset.test.js b/tests/auto-run-fresh-attempt-reset.test.js
index 8627a15..91bf331 100644
--- a/tests/auto-run-fresh-attempt-reset.test.js
+++ b/tests/auto-run-fresh-attempt-reset.test.js
@@ -114,6 +114,12 @@ let currentState = {
inbucketMailbox: '',
cloudflareDomain: '',
cloudflareDomains: [],
+ reusablePhoneActivation: {
+ activationId: '123456',
+ phoneNumber: '66959916439',
+ successfulUses: 1,
+ maxUses: 3,
+ },
tabRegistry: {},
sourceLastUrls: {},
};
@@ -329,6 +335,16 @@ return {
assert.strictEqual(snapshot.currentState.autoRunSessionId, 0, 'session id should be cleared after completion');
assert.strictEqual(snapshot.currentState.gmailBaseEmail, 'demo@gmail.com', 'gmail base email should survive fresh-attempt reset');
assert.strictEqual(snapshot.currentState.mail2925BaseEmail, 'demo@2925.com', '2925 base email should survive fresh-attempt reset');
+ assert.deepStrictEqual(
+ snapshot.currentState.reusablePhoneActivation,
+ {
+ activationId: '123456',
+ phoneNumber: '66959916439',
+ successfulUses: 1,
+ maxUses: 3,
+ },
+ 'reusable phone activation should survive fresh-attempt reset'
+ );
console.log('auto-run fresh attempt reset tests passed');
})().catch((error) => {
diff --git a/tests/background-luckmail.test.js b/tests/background-luckmail.test.js
index ee36635..c54efd8 100644
--- a/tests/background-luckmail.test.js
+++ b/tests/background-luckmail.test.js
@@ -713,6 +713,7 @@ function broadcastDataUpdate() {}
function isLocalhostOAuthCallbackUrl() {
return true;
}
+async function finalizePhoneActivationAfterSuccessfulFlow() {}
async function finalizeIcloudAliasAfterSuccessfulFlow() {}
${bundle}
diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js
index 9ca45f6..9d29dba 100644
--- a/tests/background-message-router-step2-skip.test.js
+++ b/tests/background-message-router-step2-skip.test.js
@@ -12,6 +12,7 @@ function createRouter(overrides = {}) {
stepStatuses: [],
emailStates: [],
finalizePayloads: [],
+ phoneFinalizations: [],
notifyCompletions: [],
notifyErrors: [],
securityBlocks: [],
@@ -49,6 +50,9 @@ function createRouter(overrides = {}) {
executeStepViaCompletionSignal: async () => {},
exportSettingsBundle: async () => ({}),
fetchGeneratedEmail: async () => '',
+ finalizePhoneActivationAfterSuccessfulFlow: overrides.finalizePhoneActivationAfterSuccessfulFlow || (async (state) => {
+ events.phoneFinalizations.push(state);
+ }),
finalizeStep3Completion: overrides.finalizeStep3Completion || (async (payload) => {
events.finalizePayloads.push(payload);
}),
@@ -262,6 +266,34 @@ test('message router finalizes step 3 before marking it completed', async () =>
assert.deepStrictEqual(response, { ok: true });
});
+test('message router finalizes pending phone activation on platform verify success', async () => {
+ const state = {
+ stepStatuses: { 10: 'pending' },
+ reusablePhoneActivation: {
+ activationId: '123456',
+ phoneNumber: '66959916439',
+ successfulUses: 0,
+ maxUses: 3,
+ },
+ pendingPhoneActivationConfirmation: {
+ activationId: '123456',
+ phoneNumber: '66959916439',
+ successfulUses: 0,
+ maxUses: 3,
+ },
+ };
+ const { router, events } = createRouter({
+ state,
+ getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'platform-verify' : '' }),
+ });
+
+ await router.handleStepData(10, {
+ localhostUrl: 'http://localhost:1455/auth/callback?code=ok',
+ });
+
+ assert.deepStrictEqual(events.phoneFinalizations, [state]);
+});
+
test('message router marks step 3 failed when post-submit finalize fails', async () => {
const { router, events } = createRouter({
finalizeStep3Completion: async () => {
diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js
index 441d21b..d4674b2 100644
--- a/tests/phone-verification-flow.test.js
+++ b/tests/phone-verification-flow.test.js
@@ -260,18 +260,6 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
statusAction: 'getStatusV2',
},
},
- {
- currentPhoneActivation: {
- activationId: '654321',
- phoneNumber: '447911123456',
- provider: 'hero-sms',
- serviceCode: 'dr',
- countryId: 16,
- successfulUses: 1,
- maxUses: 3,
- statusAction: 'getStatusV2',
- },
- },
{
reusablePhoneActivation: {
activationId: '654321',
@@ -279,7 +267,19 @@ test('phone verification helper uses HeroSMS getStatusV2 after acquiring a numbe
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 16,
- successfulUses: 1,
+ successfulUses: 0,
+ maxUses: 3,
+ statusAction: 'getStatusV2',
+ },
+ },
+ {
+ pendingPhoneActivationConfirmation: {
+ activationId: '654321',
+ phoneNumber: '447911123456',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 16,
+ successfulUses: 0,
maxUses: 3,
statusAction: 'getStatusV2',
},
@@ -413,17 +413,6 @@ test('phone verification helper completes add-phone flow, clears current activat
maxUses: 3,
},
},
- {
- currentPhoneActivation: {
- activationId: '123456',
- phoneNumber: '66959916439',
- provider: 'hero-sms',
- serviceCode: 'dr',
- countryId: 52,
- successfulUses: 1,
- maxUses: 3,
- },
- },
{
reusablePhoneActivation: {
activationId: '123456',
@@ -431,7 +420,18 @@ test('phone verification helper completes add-phone flow, clears current activat
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
- successfulUses: 1,
+ successfulUses: 0,
+ maxUses: 3,
+ },
+ },
+ {
+ pendingPhoneActivationConfirmation: {
+ activationId: '123456',
+ phoneNumber: '66959916439',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 52,
+ successfulUses: 0,
maxUses: 3,
},
},
@@ -519,6 +519,7 @@ test('phone verification helper still succeeds when HeroSMS setStatus(3) fails a
});
assert.deepStrictEqual(currentState.currentPhoneActivation, null);
assert.deepStrictEqual(currentState.reusablePhoneActivation, null);
+ assert.deepStrictEqual(currentState.pendingPhoneActivationConfirmation, null);
const actions = requests.map((url) => url.searchParams.get('action'));
assert.deepStrictEqual(actions, ['getNumber', 'getStatus', 'setStatus']);
});
@@ -843,12 +844,21 @@ test('phone verification helper replaces the number when code submission returns
provider: 'hero-sms',
serviceCode: 'dr',
countryId: 52,
- successfulUses: 1,
+ successfulUses: 0,
+ maxUses: 3,
+ });
+ assert.deepStrictEqual(currentState.pendingPhoneActivationConfirmation, {
+ activationId: '222222',
+ phoneNumber: '66950000002',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 52,
+ successfulUses: 0,
maxUses: 3,
});
});
-test('phone verification helper reuses the same number up to three successful registrations', async () => {
+test('phone verification helper defers maxUses accounting for reused activations until the full flow succeeds', async () => {
const requests = [];
let currentState = {
heroSmsApiKey: 'demo-key',
@@ -934,10 +944,27 @@ test('phone verification helper reuses the same number up to three successful re
});
assert.equal(requests[0].searchParams.get('action'), 'reactivate');
assert.equal(requests[0].searchParams.get('id'), '123456');
- assert.deepStrictEqual(currentState.reusablePhoneActivation, null);
+ assert.deepStrictEqual(currentState.reusablePhoneActivation, {
+ activationId: '222333',
+ phoneNumber: '66959916439',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 52,
+ successfulUses: 2,
+ maxUses: 3,
+ });
+ assert.deepStrictEqual(currentState.pendingPhoneActivationConfirmation, {
+ activationId: '222333',
+ phoneNumber: '66959916439',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 52,
+ successfulUses: 2,
+ maxUses: 3,
+ });
});
-test('phone verification helper keeps maxUses behavior for reused V2 activations', async () => {
+test('phone verification helper defers maxUses accounting for reused V2 activations until the full flow succeeds', async () => {
const requests = [];
let currentState = {
heroSmsApiKey: 'demo-key',
@@ -1026,5 +1053,131 @@ test('phone verification helper keeps maxUses behavior for reused V2 activations
});
const actions = requests.map((url) => url.searchParams.get('action'));
assert.deepStrictEqual(actions, ['reactivate', 'getStatusV2', 'setStatus']);
- assert.deepStrictEqual(currentState.reusablePhoneActivation, null);
+ assert.deepStrictEqual(currentState.reusablePhoneActivation, {
+ activationId: '222333',
+ phoneNumber: '447911123456',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 16,
+ successfulUses: 2,
+ maxUses: 3,
+ statusAction: 'getStatusV2',
+ });
+ assert.deepStrictEqual(currentState.pendingPhoneActivationConfirmation, {
+ activationId: '222333',
+ phoneNumber: '447911123456',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 16,
+ successfulUses: 2,
+ maxUses: 3,
+ statusAction: 'getStatusV2',
+ });
+});
+
+test('phone verification helper finalizes pending phone activation confirmation after the full flow succeeds', async () => {
+ let currentState = {
+ reusablePhoneActivation: {
+ activationId: '123456',
+ phoneNumber: '66959916439',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 52,
+ successfulUses: 0,
+ maxUses: 3,
+ },
+ pendingPhoneActivationConfirmation: {
+ activationId: '123456',
+ phoneNumber: '66959916439',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 52,
+ successfulUses: 0,
+ maxUses: 3,
+ },
+ };
+
+ const helpers = api.createPhoneVerificationHelpers({
+ addLog: async () => {},
+ getState: async () => ({ ...currentState }),
+ sendToContentScriptResilient: async () => ({}),
+ setState: async (updates) => {
+ currentState = { ...currentState, ...updates };
+ },
+ sleepWithStop: async () => {},
+ throwIfStopped: () => {},
+ });
+
+ const committedActivation = await helpers.finalizePendingPhoneActivationConfirmation();
+
+ assert.deepStrictEqual(committedActivation, {
+ activationId: '123456',
+ phoneNumber: '66959916439',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 52,
+ successfulUses: 1,
+ maxUses: 3,
+ });
+ assert.deepStrictEqual(currentState.reusablePhoneActivation, {
+ activationId: '123456',
+ phoneNumber: '66959916439',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 52,
+ successfulUses: 1,
+ maxUses: 3,
+ });
+ assert.equal(currentState.pendingPhoneActivationConfirmation, null);
+});
+
+test('phone verification helper clears reusable activation when final success exhausts maxUses', async () => {
+ let currentState = {
+ reusablePhoneActivation: {
+ activationId: '222333',
+ phoneNumber: '447911123456',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 16,
+ successfulUses: 2,
+ maxUses: 3,
+ statusAction: 'getStatusV2',
+ },
+ pendingPhoneActivationConfirmation: {
+ activationId: '222333',
+ phoneNumber: '447911123456',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 16,
+ successfulUses: 2,
+ maxUses: 3,
+ statusAction: 'getStatusV2',
+ },
+ };
+
+ const helpers = api.createPhoneVerificationHelpers({
+ addLog: async () => {},
+ getState: async () => ({ ...currentState }),
+ sendToContentScriptResilient: async () => ({}),
+ setState: async (updates) => {
+ currentState = { ...currentState, ...updates };
+ },
+ sleepWithStop: async () => {},
+ throwIfStopped: () => {},
+ });
+
+ const committedActivation = await helpers.finalizePendingPhoneActivationConfirmation();
+
+ assert.deepStrictEqual(committedActivation, {
+ activationId: '222333',
+ phoneNumber: '447911123456',
+ provider: 'hero-sms',
+ serviceCode: 'dr',
+ countryId: 16,
+ successfulUses: 3,
+ maxUses: 3,
+ statusAction: 'getStatusV2',
+ });
+ assert.equal(currentState.reusablePhoneActivation, null);
+ assert.equal(currentState.pendingPhoneActivationConfirmation, null);
});
diff --git a/tests/step9-localhost-cleanup-scope.test.js b/tests/step9-localhost-cleanup-scope.test.js
index 41081c9..9abcc1d 100644
--- a/tests/step9-localhost-cleanup-scope.test.js
+++ b/tests/step9-localhost-cleanup-scope.test.js
@@ -114,6 +114,7 @@ function broadcastDataUpdate() {}
async function addLog(message) {
logMessages.push(message);
}
+async function finalizePhoneActivationAfterSuccessfulFlow() {}
async function finalizeIcloudAliasAfterSuccessfulFlow() {}
function matchesSourceUrlFamily() {
return false;
From 59663c4ca308df63666c8ae91143b00d1c129a96 Mon Sep 17 00:00:00 2001
From: zhangkun <1184253234@qq.com>
Date: Tue, 28 Apr 2026 14:20:29 +0800
Subject: [PATCH 3/6] test: cover HeroSMS maxPrice in icloud provider fixture
---
tests/sidepanel-icloud-provider.test.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/tests/sidepanel-icloud-provider.test.js b/tests/sidepanel-icloud-provider.test.js
index 976e2c6..99e6953 100644
--- a/tests/sidepanel-icloud-provider.test.js
+++ b/tests/sidepanel-icloud-provider.test.js
@@ -321,6 +321,7 @@ const inputVerificationResendCount = { value: '' };
const inputPhoneVerificationEnabled = { checked: false };
const DEFAULT_PHONE_VERIFICATION_ENABLED = false;
const inputHeroSmsApiKey = { value: '' };
+const inputHeroSmsMaxPrice = { value: '' };
const selectHeroSmsCountry = { value: '52', options: [{ value: '52' }] };
const inputRunCount = { value: '' };
const DEFAULT_VERIFICATION_RESEND_COUNT = 4;
@@ -347,6 +348,7 @@ function normalizeAutoRunThreadIntervalMinutes(value) { return Number(value) ||
function normalizeAutoDelayMinutes(value) { return Number(value) || 30; }
function formatAutoStepDelayInputValue(value) { return value == null ? '' : String(value); }
function normalizeVerificationResendCount(value, fallback) { return Number(value) || fallback; }
+function normalizeHeroSmsMaxPriceValue(value) { return String(value ?? '').trim(); }
function normalizeHeroSmsCountryId() { return 52; }
function getSelectedHeroSmsCountryOption() { return { label: 'Thailand' }; }
function updateHeroSmsPlatformDisplay() {}
From 51988b4f8d3c8d486f1f1aafbc095b00277d901d Mon Sep 17 00:00:00 2001
From: EmptyDust <1422492074@qq.com>
Date: Tue, 28 Apr 2026 14:26:17 +0800
Subject: [PATCH 4/6] fix: read hotmail codes from full imap body
---
scripts/hotmail_helper.py | 50 +++++++++++++
tests/hotmail_helper_logging_test.py | 104 ++++++++++++++++++++++++++-
2 files changed, 153 insertions(+), 1 deletion(-)
diff --git a/scripts/hotmail_helper.py b/scripts/hotmail_helper.py
index 806ecad..768b480 100644
--- a/scripts/hotmail_helper.py
+++ b/scripts/hotmail_helper.py
@@ -123,6 +123,48 @@ def log_info(message):
print(f"[HotmailHelper] {message}", flush=True)
+def is_openai_sender(address):
+ sender = str(address or "").strip().lower()
+ return "openai" in sender
+
+
+def get_message_body_content(message):
+ body = message.get("body") or {}
+ if not isinstance(body, dict):
+ return ""
+ return str(body.get("content") or "").strip()
+
+
+def log_openai_messages(messages, transport=""):
+ for message in messages or []:
+ sender_info = message.get("from", {}).get("emailAddress", {}) or {}
+ sender = str(sender_info.get("address") or "").strip()
+ if not is_openai_sender(sender):
+ continue
+
+ sender_name = str(sender_info.get("name") or "").strip()
+ mailbox = str(message.get("mailbox") or "").strip() or "INBOX"
+ subject = str(message.get("subject") or "").strip()
+ transport_label = str(transport or "").strip() or "unknown"
+ base = (
+ f"transport={transport_label} mailbox={mailbox} sender={sender} "
+ f"senderName={sender_name or '-'} subject={subject}"
+ )
+
+ log_info(f"openai mail received {base}")
+
+ body_content = get_message_body_content(message)
+ if body_content:
+ log_info(f"openai mail full body start {base}")
+ print(body_content, flush=True)
+ log_info("openai mail full body end")
+ continue
+
+ preview = str(message.get("bodyPreview") or "").strip()
+ if preview:
+ log_info(f"openai mail preview {base} preview={compact_text(preview, 1000)}")
+
+
def get_proxy_debug_context():
names = ["all_proxy", "http_proxy", "https_proxy", "ALL_PROXY", "HTTP_PROXY", "HTTPS_PROXY"]
parts = []
@@ -481,6 +523,9 @@ def normalize_message(message_id, raw_bytes, mailbox):
}
},
"bodyPreview": body[:500],
+ "body": {
+ "content": body,
+ },
"receivedDateTime": to_iso_string(timestamp_ms),
"receivedTimestamp": timestamp_ms,
}
@@ -680,6 +725,7 @@ def collect_messages(email_addr, client_id, refresh_token, mailboxes, top):
try:
log_info(f"message collection start transport={transport_name}")
result = collector(email_addr, client_id, refresh_token, mailboxes, top)
+ log_openai_messages(result.get("messages") or [], transport=transport_name)
log_info(
f"message collection success transport={transport_name} "
f"tokenEndpoint={result['token_payload'].get('token_endpoint', '')}"
@@ -722,6 +768,10 @@ def select_latest_code(messages, sender_filters, subject_filters, exclude_codes,
preview = str(message.get("bodyPreview", ""))
combined = " ".join([sender, subject.lower(), preview.lower()])
code = extract_code(" ".join([subject, preview, sender]))
+ if not code:
+ body_content = get_message_body_content(message)
+ if body_content:
+ code = extract_code(" ".join([subject, body_content, sender]))
if not code or code in excluded:
return None
diff --git a/tests/hotmail_helper_logging_test.py b/tests/hotmail_helper_logging_test.py
index dade9e4..bc926fc 100644
--- a/tests/hotmail_helper_logging_test.py
+++ b/tests/hotmail_helper_logging_test.py
@@ -18,6 +18,107 @@ hotmail_helper = load_hotmail_helper()
class HotmailHelperLoggingTest(unittest.TestCase):
+ def test_select_latest_code_can_use_full_body_when_preview_is_truncated(self):
+ css_prefix = (
+ 'Your temporary ChatGPT verification code '
+ '@font-face { font-family: "Söhne"; src: url(https://cdn.openai.com/common/fonts/soehne/soehne-buch.woff2) format("woff2"); } '
+ '.ExternalClass { width: 100%; } '
+ '#bodyTable { width: 560px; } '
+ 'body { min-width: 100% !important; } '
+ ) * 8
+ full_body = (
+ css_prefix
+ + 'Enter this temporary verification code to continue: 272964 '
+ + 'Please ignore this email if this was not you.'
+ )
+ message = {
+ "id": "imap-1",
+ "mailbox": "INBOX",
+ "subject": "Your temporary ChatGPT verification code",
+ "from": {
+ "emailAddress": {
+ "address": "otp@tm1.openai.com",
+ "name": "OpenAI",
+ }
+ },
+ "bodyPreview": full_body[:500],
+ "body": {
+ "content": full_body,
+ },
+ "receivedTimestamp": 200,
+ }
+
+ result = hotmail_helper.select_latest_code(
+ [message],
+ ['openai', 'noreply', 'verify', 'auth', 'chatgpt', 'duckduckgo', 'forward'],
+ ['verify', 'verification', 'code', '验证码', 'confirm', 'login'],
+ [],
+ 0,
+ )
+
+ self.assertEqual(result["code"], "272964")
+ self.assertEqual(result["message"]["id"], "imap-1")
+
+ def test_log_openai_messages_logs_full_body_when_available(self):
+ messages = [{
+ "mailbox": "INBOX",
+ "subject": "Your verification code",
+ "from": {
+ "emailAddress": {
+ "address": "account-security@openai.com",
+ "name": "OpenAI",
+ }
+ },
+ "bodyPreview": "Use 123456 to continue.",
+ "body": {
+ "content": "Hello there\nUse 123456 to continue.",
+ },
+ }]
+
+ output = io.StringIO()
+ with redirect_stdout(output):
+ hotmail_helper.log_openai_messages(messages, transport="imap")
+
+ rendered = output.getvalue()
+ self.assertIn(
+ "[HotmailHelper] openai mail received transport=imap mailbox=INBOX sender=account-security@openai.com senderName=OpenAI subject=Your verification code",
+ rendered,
+ )
+ self.assertIn(
+ "[HotmailHelper] openai mail full body start transport=imap mailbox=INBOX sender=account-security@openai.com senderName=OpenAI subject=Your verification code",
+ rendered,
+ )
+ self.assertIn("Hello there\nUse 123456 to continue.", rendered)
+ self.assertIn("[HotmailHelper] openai mail full body end", rendered)
+
+ def test_log_openai_messages_falls_back_to_preview_without_full_body(self):
+ messages = [{
+ "mailbox": "Junk",
+ "subject": "Verify your sign in",
+ "from": {
+ "emailAddress": {
+ "address": "noreply@tm.openai.com",
+ "name": "ChatGPT",
+ }
+ },
+ "bodyPreview": "Use 654321 to continue.",
+ }]
+
+ output = io.StringIO()
+ with redirect_stdout(output):
+ hotmail_helper.log_openai_messages(messages, transport="graph")
+
+ rendered = output.getvalue()
+ self.assertIn(
+ "[HotmailHelper] openai mail received transport=graph mailbox=Junk sender=noreply@tm.openai.com senderName=ChatGPT subject=Verify your sign in",
+ rendered,
+ )
+ self.assertIn(
+ "[HotmailHelper] openai mail preview transport=graph mailbox=Junk sender=noreply@tm.openai.com senderName=ChatGPT subject=Verify your sign in preview=Use 654321 to continue.",
+ rendered,
+ )
+ self.assertNotIn("openai mail full body start", rendered)
+
def test_refresh_access_token_logs_invalid_grant_and_direct_connection_refused_separately(self):
failures = [
{
@@ -38,7 +139,8 @@ class HotmailHelperLoggingTest(unittest.TestCase):
},
]
- with mock.patch.object(hotmail_helper, "try_refresh_access_token", side_effect=failures):
+ with mock.patch.object(hotmail_helper, "try_refresh_access_token", side_effect=failures), \
+ mock.patch.object(hotmail_helper, "get_proxy_debug_context", return_value="direct"):
output = io.StringIO()
with redirect_stdout(output):
with self.assertRaises(RuntimeError):
From 75bc68f53a75711d4fa3bfedbdba5577e747a09c Mon Sep 17 00:00:00 2001
From: daniellee2015
Date: Tue, 28 Apr 2026 15:16:07 +0800
Subject: [PATCH 5/6] fix(step8): stop resend retry loop during in-place
recovery
---
background/steps/fetch-login-code.js | 70 ++++++++++++++++++++++--
background/verification-flow.js | 12 ++++-
tests/background-step7-recovery.test.js | 72 +++++++++++++++++++++++++
tests/verification-flow-polling.test.js | 71 ++++++++++++++++++++++++
4 files changed, 221 insertions(+), 4 deletions(-)
diff --git a/background/steps/fetch-login-code.js b/background/steps/fetch-login-code.js
index 0af5cd4..9f59517 100644
--- a/background/steps/fetch-login-code.js
+++ b/background/steps/fetch-login-code.js
@@ -27,6 +27,7 @@
reuseOrCreateTab,
setState,
shouldUseCustomRegistrationEmail,
+ sleepWithStop,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS,
throwIfStopped,
@@ -167,10 +168,26 @@
});
}
- async function runStep8Attempt(state) {
+ function getStep8ResendIntervalMs(state = {}) {
+ const mail = getMailConfig(state);
+ if (mail?.provider === LUCKMAIL_PROVIDER) {
+ return 15000;
+ }
+ if (mail?.provider === HOTMAIL_PROVIDER || mail?.provider === '2925') {
+ return 0;
+ }
+ return Math.max(0, Number(STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS) || 0);
+ }
+
+ async function runStep8Attempt(state, runtime = {}) {
const visibleStep = getVisibleStep(state, 8);
const mail = getMailConfig(state);
if (mail.error) throw new Error(mail.error);
+ const stateLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
+ let latestResendAt = Math.max(0, Number(runtime?.stickyLastResendAt) || 0, stateLastResendAt);
+ const notifyResendRequestedAt = typeof runtime?.onResendRequestedAt === 'function'
+ ? runtime.onResendRequestedAt
+ : null;
const stepStartedAt = Date.now();
const verificationFilterAfterTimestamp = mail.provider === '2925'
@@ -267,6 +284,16 @@
disableTimeBudgetCap: mail.provider === '2925',
getRemainingTimeMs: getStep8RemainingTimeResolver(state?.oauthUrl || '', visibleStep),
requestFreshCodeFirst: false,
+ lastResendAt: latestResendAt,
+ onResendRequestedAt: async (requestedAt) => {
+ const numericRequestedAt = Number(requestedAt) || 0;
+ if (numericRequestedAt > 0) {
+ latestResendAt = Math.max(latestResendAt, numericRequestedAt);
+ }
+ if (notifyResendRequestedAt) {
+ await notifyResendRequestedAt(latestResendAt);
+ }
+ },
targetEmail: fixedTargetEmail,
resendIntervalMs: mail.provider === LUCKMAIL_PROVIDER
? 15000
@@ -274,6 +301,9 @@
? 0
: STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS),
});
+ return {
+ lastResendAt: latestResendAt,
+ };
}
function isStep8RestartStep7Error(error) {
@@ -285,10 +315,22 @@
let currentState = state;
let mailPollingAttempt = 1;
let lastMailPollingError = null;
+ let stickyLastResendAt = Number(state?.loginVerificationRequestedAt) || 0;
while (true) {
try {
- await runStep8Attempt(currentState);
+ const result = await runStep8Attempt(currentState, {
+ stickyLastResendAt,
+ onResendRequestedAt: async (requestedAt) => {
+ const numericRequestedAt = Number(requestedAt) || 0;
+ if (numericRequestedAt > 0) {
+ stickyLastResendAt = Math.max(stickyLastResendAt, numericRequestedAt);
+ }
+ },
+ });
+ if (Number(result?.lastResendAt) > 0) {
+ stickyLastResendAt = Math.max(stickyLastResendAt, Number(result.lastResendAt) || 0);
+ }
return;
} catch (err) {
const visibleStep = getVisibleStep(currentState, 8);
@@ -323,7 +365,29 @@
`步骤 ${visibleStep}:认证页仍保持在验证码页,将在当前链路直接重试(${mailPollingAttempt}/${STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS}),不回到步骤 ${authLoginStep}。`,
'warn'
);
- currentState = await getState();
+ const latestState = await getState();
+ const latestStateResendAt = Number(latestState?.loginVerificationRequestedAt) || 0;
+ if (latestStateResendAt > 0) {
+ stickyLastResendAt = Math.max(stickyLastResendAt, latestStateResendAt);
+ }
+ currentState = latestState;
+ if (stickyLastResendAt > 0 && (!latestStateResendAt || latestStateResendAt < stickyLastResendAt)) {
+ currentState = {
+ ...latestState,
+ loginVerificationRequestedAt: stickyLastResendAt,
+ };
+ }
+ const resendIntervalMs = getStep8ResendIntervalMs(currentState);
+ const remainingBeforeRetryMs = stickyLastResendAt > 0 && resendIntervalMs > 0
+ ? Math.max(0, resendIntervalMs - (Date.now() - stickyLastResendAt))
+ : 0;
+ if (remainingBeforeRetryMs > 0 && typeof sleepWithStop === 'function') {
+ await addLog(
+ `步骤 ${visibleStep}:上轮已触发重发验证码,为避免重复重发,先等待 ${Math.ceil(remainingBeforeRetryMs / 1000)} 秒后继续当前链路重试。`,
+ 'info'
+ );
+ await sleepWithStop(Math.min(remainingBeforeRetryMs, 3000));
+ }
continue;
}
await addLog(
diff --git a/background/verification-flow.js b/background/verification-flow.js
index dcade7a..f41d9fa 100644
--- a/background/verification-flow.js
+++ b/background/verification-flow.js
@@ -920,8 +920,18 @@
const maxSubmitAttempts = mail.provider === LUCKMAIL_PROVIDER ? 3 : 15;
const resendIntervalMs = Math.max(0, Number(options.resendIntervalMs) || 0);
let lastResendAt = Number(options.lastResendAt) || 0;
+ const externalOnResendRequestedAt = typeof options.onResendRequestedAt === 'function'
+ ? options.onResendRequestedAt
+ : null;
- const updateFilterAfterTimestampForVerificationStep = async (_requestedAt) => {
+ const updateFilterAfterTimestampForVerificationStep = async (requestedAt) => {
+ if (externalOnResendRequestedAt) {
+ try {
+ await externalOnResendRequestedAt(requestedAt);
+ } catch (_) {
+ // Keep resend flow best-effort; state sync callback failures should not break verification.
+ }
+ }
return nextFilterAfterTimestamp;
};
diff --git a/tests/background-step7-recovery.test.js b/tests/background-step7-recovery.test.js
index ef92903..30ef3db 100644
--- a/tests/background-step7-recovery.test.js
+++ b/tests/background-step7-recovery.test.js
@@ -310,6 +310,78 @@ test('step 8 retries in-place when polling fails but auth page still stays on ve
assert.equal(events.ensureCalls >= 3, true);
});
+test('step 8 keeps resend cooldown timestamp across in-place retries to avoid repeated resend storms', async () => {
+ const events = {
+ resolveCalls: 0,
+ resolveLastResendAts: [],
+ sleepMs: [],
+ };
+ const realDateNow = Date.now;
+ Date.now = () => 230000;
+
+ const executor = api.createStep8Executor({
+ addLog: async () => {},
+ chrome: {
+ tabs: {
+ update: async () => {},
+ },
+ },
+ CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
+ completeStepFromBackground: async () => {},
+ confirmCustomVerificationStepBypass: async () => {},
+ ensureStep8VerificationPageReady: async () => ({ state: 'verification_page', displayedEmail: 'user@example.com' }),
+ rerunStep7ForStep8Recovery: async () => {},
+ getOAuthFlowRemainingMs: async () => 9000,
+ getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => Math.min(defaultTimeoutMs, 9000),
+ getMailConfig: () => ({
+ provider: 'qq',
+ label: 'QQ 邮箱',
+ source: 'mail-qq',
+ url: 'https://mail.qq.com',
+ navigateOnReuse: false,
+ }),
+ getState: async () => ({ email: 'user@example.com', password: 'secret', loginVerificationRequestedAt: null }),
+ getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
+ HOTMAIL_PROVIDER: 'hotmail-api',
+ isTabAlive: async () => true,
+ isVerificationMailPollingError: (error) => /页面通信异常|did not respond/i.test(String(error?.message || error || '')),
+ LUCKMAIL_PROVIDER: 'luckmail-api',
+ resolveVerificationStep: async (_step, _state, _mail, options) => {
+ events.resolveCalls += 1;
+ events.resolveLastResendAts.push(Number(options?.lastResendAt) || 0);
+ if (events.resolveCalls === 1) {
+ await options.onResendRequestedAt(222000);
+ throw new Error('步骤 8:页面通信异常 did not respond in 1s');
+ }
+ },
+ reuseOrCreateTab: async () => {},
+ setState: async () => {},
+ setStepStatus: async () => {},
+ shouldUseCustomRegistrationEmail: () => false,
+ sleepWithStop: async (ms) => {
+ events.sleepMs.push(ms);
+ },
+ STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
+ STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
+ throwIfStopped: () => {},
+ });
+
+ try {
+ await executor.executeStep8({
+ email: 'user@example.com',
+ password: 'secret',
+ oauthUrl: 'https://oauth.example/latest',
+ });
+ } finally {
+ Date.now = realDateNow;
+ }
+
+ assert.equal(events.resolveCalls, 2);
+ assert.deepStrictEqual(events.resolveLastResendAts, [0, 222000]);
+ assert.equal(events.sleepMs.length >= 1, true);
+ assert.equal(events.sleepMs[0], 3000);
+});
+
test('step 8 completes when polling fails but recovery probe shows oauth consent page', async () => {
const events = {
ensureCalls: 0,
diff --git a/tests/verification-flow-polling.test.js b/tests/verification-flow-polling.test.js
index 2be3c66..7f1b696 100644
--- a/tests/verification-flow-polling.test.js
+++ b/tests/verification-flow-polling.test.js
@@ -1030,6 +1030,77 @@ test('verification flow waits during resend cooldown instead of tight-looping',
assert.ok(sleepCalls[0] >= 1000);
});
+test('verification flow notifies onResendRequestedAt when resend is triggered', async () => {
+ const resendRequestedAtCalls = [];
+ const stateUpdates = [];
+ let pollCalls = 0;
+
+ const helpers = api.createVerificationFlowHelpers({
+ addLog: async () => {},
+ chrome: { tabs: { update: async () => {} } },
+ CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
+ completeStepFromBackground: async () => {},
+ confirmCustomVerificationStepBypassRequest: async () => ({ confirmed: true }),
+ getHotmailVerificationPollConfig: () => ({}),
+ getHotmailVerificationRequestTimestamp: () => 0,
+ getState: async () => ({}),
+ getTabId: async () => 1,
+ HOTMAIL_PROVIDER: 'hotmail-api',
+ isStopError: () => false,
+ LUCKMAIL_PROVIDER: 'luckmail-api',
+ MAIL_2925_VERIFICATION_INTERVAL_MS: 15000,
+ MAIL_2925_VERIFICATION_MAX_ATTEMPTS: 15,
+ pollCloudflareTempEmailVerificationCode: async () => ({}),
+ pollHotmailVerificationCode: async () => ({}),
+ pollLuckmailVerificationCode: async () => ({}),
+ sendToContentScript: async (_source, message) => {
+ if (message.type === 'RESEND_VERIFICATION_CODE') {
+ return { resent: true };
+ }
+ return {};
+ },
+ sendToMailContentScriptResilient: async (_mail, message) => {
+ if (message.type !== 'POLL_EMAIL') {
+ return {};
+ }
+ pollCalls += 1;
+ return pollCalls === 1
+ ? {}
+ : { code: '654321', emailTimestamp: 123 };
+ },
+ setState: async (payload) => {
+ stateUpdates.push(payload);
+ },
+ setStepStatus: async () => {},
+ sleepWithStop: async () => {},
+ throwIfStopped: () => {},
+ VERIFICATION_POLL_MAX_ROUNDS: 5,
+ });
+
+ await helpers.resolveVerificationStep(
+ 8,
+ {
+ email: 'user@example.com',
+ lastLoginCode: null,
+ },
+ { provider: 'qq', label: 'QQ 邮箱' },
+ {
+ maxResendRequests: 1,
+ resendIntervalMs: 25000,
+ onResendRequestedAt: async (requestedAt) => {
+ resendRequestedAtCalls.push(Number(requestedAt) || 0);
+ },
+ }
+ );
+
+ assert.equal(resendRequestedAtCalls.length >= 1, true);
+ assert.equal(resendRequestedAtCalls[0] > 0, true);
+ assert.equal(
+ stateUpdates.some((payload) => Number(payload?.loginVerificationRequestedAt) > 0),
+ true
+ );
+});
+
test('verification flow uses resilient signup-page transport when submitting verification code', async () => {
const resilientCalls = [];
From b8d8847ce5ba8be3141a9f7e28650ce8d17e4431 Mon Sep 17 00:00:00 2001
From: zhangkun <1184253234@qq.com>
Date: Tue, 28 Apr 2026 21:49:58 +0800
Subject: [PATCH 6/6] fix: defer HeroSMS reuse confirmation until final cleanup
---
background.js | 2 +-
background/message-router.js | 2 +-
background/phone-verification-flow.js | 9 ++--
...ckground-message-router-step2-skip.test.js | 36 +++++++++++++-
tests/phone-verification-flow.test.js | 47 +++++++++++++++++++
5 files changed, 89 insertions(+), 7 deletions(-)
diff --git a/background.js b/background.js
index 5eb6bd1..bad2c22 100644
--- a/background.js
+++ b/background.js
@@ -6691,7 +6691,6 @@ async function handleStepData(step, payload) {
excludeLocalhostCallbacks: true,
});
}
- await finalizePhoneActivationAfterSuccessfulFlow(latestState);
await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
const shouldClearCustomPoolEmail = String(latestState?.emailGenerator || '').trim().toLowerCase() === (
typeof CUSTOM_EMAIL_POOL_GENERATOR === 'string'
@@ -6701,6 +6700,7 @@ async function handleStepData(step, payload) {
if ((shouldUseCustomRegistrationEmail(latestState) || shouldClearCustomPoolEmail) && latestState.email) {
await setEmailStateSilently(null);
}
+ await finalizePhoneActivationAfterSuccessfulFlow(latestState);
break;
}
}
diff --git a/background/message-router.js b/background/message-router.js
index c0dd637..4160e38 100644
--- a/background/message-router.js
+++ b/background/message-router.js
@@ -203,10 +203,10 @@
excludeLocalhostCallbacks: true,
});
}
+ await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
if (typeof finalizePhoneActivationAfterSuccessfulFlow === 'function') {
await finalizePhoneActivationAfterSuccessfulFlow(latestState);
}
- await finalizeIcloudAliasAfterSuccessfulFlow(latestState);
}
async function handleStepData(step, payload) {
diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js
index 009fbb3..e8b7eb8 100644
--- a/background/phone-verification-flow.js
+++ b/background/phone-verification-flow.js
@@ -248,18 +248,19 @@
}
}
- function resolvePhoneConfig(state = {}) {
+ function resolvePhoneConfig(state = {}, options = {}) {
const apiKey = normalizeApiKey(state.heroSmsApiKey);
if (!apiKey) {
throw new Error('HeroSMS API key is missing. Save it in the side panel before running the phone flow.');
}
+ const requireMaxPrice = Boolean(options.requireMaxPrice);
const maxPrice = normalizeManualHeroSmsMaxPrice(state.heroSmsMaxPrice);
- if (!maxPrice) {
+ if (requireMaxPrice && !maxPrice) {
throw new Error('HeroSMS maxPrice is missing. Fill it in below the country selector before running the phone flow.');
}
return {
apiKey,
- maxPrice,
+ ...(maxPrice ? { maxPrice } : {}),
baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL),
};
}
@@ -324,7 +325,7 @@
}
async function requestPhoneActivation(state = {}) {
- const config = resolvePhoneConfig(state);
+ const config = resolvePhoneConfig(state, { requireMaxPrice: true });
const countryConfig = resolveCountryConfig(state);
const maxPrice = config.maxPrice;
const buildFallbackActivation = (requestAction) => ({
diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js
index 9d29dba..7b72c22 100644
--- a/tests/background-message-router-step2-skip.test.js
+++ b/tests/background-message-router-step2-skip.test.js
@@ -56,7 +56,7 @@ function createRouter(overrides = {}) {
finalizeStep3Completion: overrides.finalizeStep3Completion || (async (payload) => {
events.finalizePayloads.push(payload);
}),
- finalizeIcloudAliasAfterSuccessfulFlow: async () => {},
+ finalizeIcloudAliasAfterSuccessfulFlow: overrides.finalizeIcloudAliasAfterSuccessfulFlow || (async () => {}),
findHotmailAccount: async () => null,
flushCommand: async () => {},
getCurrentLuckmailPurchase: () => null,
@@ -294,6 +294,40 @@ test('message router finalizes pending phone activation on platform verify succe
assert.deepStrictEqual(events.phoneFinalizations, [state]);
});
+test('message router does not finalize pending phone activation when icloud finalization fails', async () => {
+ const state = {
+ stepStatuses: { 10: 'pending' },
+ reusablePhoneActivation: {
+ activationId: '123456',
+ phoneNumber: '66959916439',
+ successfulUses: 0,
+ maxUses: 3,
+ },
+ pendingPhoneActivationConfirmation: {
+ activationId: '123456',
+ phoneNumber: '66959916439',
+ successfulUses: 0,
+ maxUses: 3,
+ },
+ };
+ const { router, events } = createRouter({
+ state,
+ getStepDefinitionForState: (step) => ({ id: step, key: step === 10 ? 'platform-verify' : '' }),
+ finalizeIcloudAliasAfterSuccessfulFlow: async () => {
+ throw new Error('icloud finalize failed');
+ },
+ });
+
+ await assert.rejects(
+ () => router.handleStepData(10, {
+ localhostUrl: 'http://localhost:1455/auth/callback?code=ok',
+ }),
+ /icloud finalize failed/
+ );
+
+ assert.deepStrictEqual(events.phoneFinalizations, []);
+});
+
test('message router marks step 3 failed when post-submit finalize fails', async () => {
const { router, events } = createRouter({
finalizeStep3Completion: async () => {
diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js
index d4674b2..28b877b 100644
--- a/tests/phone-verification-flow.test.js
+++ b/tests/phone-verification-flow.test.js
@@ -95,6 +95,53 @@ test('phone verification helper requires manual HeroSMS maxPrice', async () => {
);
});
+test('phone verification helper still clears existing activation when maxPrice is missing', async () => {
+ const requests = [];
+ let currentState = {
+ heroSmsApiKey: 'demo-key',
+ heroSmsMaxPrice: '',
+ currentPhoneActivation: {
+ activationId: '123456',
+ phoneNumber: '66959916439',
+ successfulUses: 0,
+ maxUses: 3,
+ },
+ reusablePhoneActivation: null,
+ pendingPhoneActivationConfirmation: null,
+ };
+ const helpers = api.createPhoneVerificationHelpers({
+ addLog: async () => {},
+ ensureStep8SignupPageReady: async () => {},
+ fetchImpl: async (url) => {
+ const parsedUrl = new URL(url);
+ requests.push(parsedUrl);
+ return {
+ ok: true,
+ text: async () => 'ACCESS_CANCEL',
+ };
+ },
+ getState: async () => currentState,
+ sendToContentScriptResilient: async () => ({}),
+ setState: async (updates) => {
+ currentState = { ...currentState, ...updates };
+ },
+ sleepWithStop: async () => {},
+ throwIfStopped: () => {},
+ });
+
+ await assert.rejects(
+ () => helpers.completePhoneVerificationFlow(1, { addPhonePage: true }),
+ /HeroSMS maxPrice is missing/i
+ );
+
+ assert.equal(requests.length, 1);
+ assert.equal(requests[0].searchParams.get('action'), 'setStatus');
+ assert.equal(requests[0].searchParams.get('id'), '123456');
+ assert.equal(requests[0].searchParams.get('status'), '8');
+ assert.equal(requests[0].searchParams.has('maxPrice'), false);
+ assert.equal(currentState.currentPhoneActivation, null);
+});
+
test('phone verification helper retries with HeroSMS getNumberV2 when getNumber reports NO_NUMBERS', async () => {
const requests = [];
const helpers = api.createPhoneVerificationHelpers({