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 @@ +