From bf7d37e4da1085f438bab9f58c15be49d1139e50 Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Thu, 28 May 2026 12:41:57 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DHeroSMS=E8=BF=90?=
=?UTF-8?q?=E8=90=A5=E5=95=86=E4=B8=8E=E6=8E=A5=E7=A0=81=E9=85=8D=E7=BD=AE?=
=?UTF-8?q?=E5=88=B7=E6=96=B0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
background.js | 20 ++
background/phone-verification-flow.js | 129 +++++++++-
sidepanel/sidepanel.html | 9 +
sidepanel/sidepanel.js | 226 ++++++++++++++++++
...ackground-account-history-settings.test.js | 4 +
tests/phone-verification-flow.test.js | 218 +++++++++++++++++
...epanel-phone-verification-settings.test.js | 35 +++
7 files changed, 632 insertions(+), 9 deletions(-)
diff --git a/background.js b/background.js
index 71fd75b..5178673 100644
--- a/background.js
+++ b/background.js
@@ -693,6 +693,7 @@ const HERO_SMS_SERVICE_CODE = 'dr';
const HERO_SMS_SERVICE_LABEL = 'OpenAI';
const HERO_SMS_COUNTRY_ID = 52;
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
+const DEFAULT_HERO_SMS_OPERATOR = 'any';
const PHONE_SMS_PROVIDER_HERO = 'hero-sms';
const PHONE_SMS_PROVIDER_5SIM = '5sim';
const PHONE_SMS_PROVIDER_HERO_SMS = PHONE_SMS_PROVIDER_HERO;
@@ -1457,6 +1458,7 @@ const PERSISTED_SETTING_DEFAULTS = {
heroSmsApiKey: '',
heroSmsReuseEnabled: DEFAULT_HERO_SMS_REUSE_ENABLED,
heroSmsAcquirePriority: DEFAULT_HERO_SMS_ACQUIRE_PRIORITY,
+ heroSmsOperator: DEFAULT_HERO_SMS_OPERATOR,
heroSmsMinPrice: '',
heroSmsMaxPrice: '',
heroSmsPreferredPrice: '',
@@ -1790,6 +1792,21 @@ function normalizeHeroSmsAcquirePriority(value = '') {
return HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
}
+function normalizeHeroSmsOperator(value = '', fallback = DEFAULT_HERO_SMS_OPERATOR) {
+ const normalized = String(value || '')
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9_-]+/g, '');
+ if (normalized) {
+ return normalized;
+ }
+ const fallbackNormalized = String(fallback || '')
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9_-]+/g, '');
+ return fallbackNormalized || DEFAULT_HERO_SMS_OPERATOR;
+}
+
function normalizeHeroSmsCountryFallback(value = []) {
const source = Array.isArray(value)
? value
@@ -3548,6 +3565,8 @@ function normalizePersistentSettingValue(key, value) {
return Boolean(value);
case 'heroSmsAcquirePriority':
return normalizeHeroSmsAcquirePriority(value);
+ case 'heroSmsOperator':
+ return normalizeHeroSmsOperator(value);
case 'heroSmsMinPrice':
case 'heroSmsMaxPrice':
return normalizeHeroSmsMaxPrice(value);
@@ -13690,6 +13709,7 @@ const phoneVerificationHelpers = self.MultiPageBackgroundPhoneVerification?.crea
DEFAULT_NEX_SMS_COUNTRY_ORDER,
DEFAULT_NEX_SMS_SERVICE_CODE,
DEFAULT_HERO_SMS_BASE_URL,
+ DEFAULT_HERO_SMS_OPERATOR,
DEFAULT_HERO_SMS_REUSE_ENABLED,
DEFAULT_PHONE_CODE_WAIT_SECONDS,
DEFAULT_PHONE_CODE_TIMEOUT_WINDOWS,
diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js
index 08141ef..7f10099 100644
--- a/background/phone-verification-flow.js
+++ b/background/phone-verification-flow.js
@@ -23,6 +23,7 @@
DEFAULT_FIVE_SIM_BASE_URL = 'https://5sim.net/v1',
DEFAULT_FIVE_SIM_PRODUCT = 'openai',
DEFAULT_FIVE_SIM_OPERATOR = 'any',
+ DEFAULT_HERO_SMS_OPERATOR = 'any',
DEFAULT_FIVE_SIM_COUNTRY_ORDER = ['thailand'],
DEFAULT_NEX_SMS_BASE_URL = 'https://api.nexsms.net',
DEFAULT_NEX_SMS_COUNTRY_ORDER = [1],
@@ -548,6 +549,21 @@
return String(value || '').trim() || fallback;
}
+ function normalizeHeroSmsOperator(value = '', fallback = DEFAULT_HERO_SMS_OPERATOR) {
+ const normalized = String(value || '')
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9_-]+/g, '');
+ if (normalized) {
+ return normalized;
+ }
+ const fallbackNormalized = String(fallback || '')
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9_-]+/g, '');
+ return fallbackNormalized || DEFAULT_HERO_SMS_OPERATOR;
+ }
+
function inferHeroSmsCountryFromPhoneNumber(phoneNumber = '') {
const digits = String(phoneNumber || '').replace(/\D+/g, '');
if (!digits) {
@@ -927,6 +943,17 @@
return normalizePhoneSmsProvider(activation?.provider || state?.phoneSmsProvider);
}
+ function scopeStateToActivationProvider(state = {}, activation = {}) {
+ const provider = getActivationProviderId(activation, state);
+ if (!provider) {
+ return state || {};
+ }
+ return {
+ ...(state || {}),
+ phoneSmsProvider: provider,
+ };
+ }
+
function getPhoneSmsProviderLabel(providerId) {
const provider = normalizePhoneSmsProvider(providerId);
if (provider === PHONE_SMS_PROVIDER_FIVE_SIM) {
@@ -2100,6 +2127,7 @@
provider,
apiKey,
baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL),
+ operator: normalizeHeroSmsOperator(state.heroSmsOperator, DEFAULT_HERO_SMS_OPERATOR),
countryCandidates: resolveCountryCandidates(state),
};
}
@@ -2113,10 +2141,82 @@
provider: PHONE_SMS_PROVIDER_HERO,
apiKey,
baseUrl: normalizeUrl(state.heroSmsBaseUrl, DEFAULT_HERO_SMS_BASE_URL),
+ operator: normalizeHeroSmsOperator(state.heroSmsOperator, DEFAULT_HERO_SMS_OPERATOR),
countryCandidates: resolveCountryCandidates(state),
};
}
+ const LATEST_PHONE_SETTING_KEYS = Object.freeze([
+ 'phoneSmsProvider',
+ 'phoneSmsProviderOrder',
+ 'phoneSmsReuseEnabled',
+ 'heroSmsApiKey',
+ 'heroSmsBaseUrl',
+ 'heroSmsCountryId',
+ 'heroSmsCountryLabel',
+ 'heroSmsCountryFallback',
+ 'heroSmsAcquirePriority',
+ 'heroSmsOperator',
+ 'heroSmsMinPrice',
+ 'heroSmsMaxPrice',
+ 'heroSmsPreferredPrice',
+ 'heroSmsActivationRetryRounds',
+ 'heroSmsActivationRetryDelayMs',
+ 'fiveSimApiKey',
+ 'fiveSimBaseUrl',
+ 'fiveSimCountryId',
+ 'fiveSimCountryLabel',
+ 'fiveSimCountryFallback',
+ 'fiveSimCountryOrder',
+ 'fiveSimOperator',
+ 'fiveSimProduct',
+ 'fiveSimMinPrice',
+ 'fiveSimMaxPrice',
+ 'nexSmsApiKey',
+ 'nexSmsBaseUrl',
+ 'nexSmsCountryOrder',
+ 'nexSmsServiceCode',
+ 'phoneVerificationReplacementLimit',
+ 'phoneCodeWaitSeconds',
+ 'phoneCodeTimeoutWindows',
+ 'phoneCodePollIntervalSeconds',
+ 'phoneCodePollMaxRounds',
+ 'freePhoneReuseEnabled',
+ 'freePhoneReuseAutoEnabled',
+ ]);
+
+ async function mergeLatestPhoneSettingsState(state = {}, options = {}) {
+ if (typeof getState !== 'function') {
+ return state || {};
+ }
+ try {
+ const latestState = await getState();
+ if (!latestState || typeof latestState !== 'object') {
+ return state || {};
+ }
+ const mergedState = {
+ ...latestState,
+ ...(state || {}),
+ };
+ const preservePhoneSmsProvider = Boolean(options?.preservePhoneSmsProvider);
+ LATEST_PHONE_SETTING_KEYS.forEach((key) => {
+ if (preservePhoneSmsProvider && key === 'phoneSmsProvider') {
+ return;
+ }
+ if (Object.prototype.hasOwnProperty.call(latestState, key)) {
+ mergedState[key] = latestState[key];
+ }
+ });
+ mergedState.heroSmsOperator = normalizeHeroSmsOperator(
+ mergedState.heroSmsOperator,
+ DEFAULT_HERO_SMS_OPERATOR
+ );
+ return mergedState;
+ } catch (_) {
+ return state || {};
+ }
+ }
+
function parseActivationPayload(payload, fallback = null) {
const normalizedFallback = normalizeActivation(fallback) || normalizeActivationFallback(fallback);
const directActivation = normalizeActivation(payload);
@@ -2537,6 +2637,10 @@
service: HERO_SMS_SERVICE_CODE,
country: countryConfig.id,
};
+ const operator = normalizeHeroSmsOperator(config?.operator, DEFAULT_HERO_SMS_OPERATOR);
+ if (operator && operator !== DEFAULT_HERO_SMS_OPERATOR) {
+ query.operator = operator;
+ }
if (options.maxPrice !== null && options.maxPrice !== undefined) {
query.maxPrice = options.maxPrice;
if (options.fixedPrice !== false) {
@@ -3584,6 +3688,9 @@
}
async function requestPhoneActivation(state = {}, options = {}) {
+ state = await mergeLatestPhoneSettingsState(state, {
+ preservePhoneSmsProvider: Boolean(options?.preservePhoneSmsProvider),
+ });
if (normalizePhoneSmsProvider(state?.phoneSmsProvider) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
@@ -3931,11 +4038,12 @@
if (getActivationProviderId(normalizedActivation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
- return provider.reuseActivation(state, normalizedActivation);
+ return provider.reuseActivation(scopeStateToActivationProvider(state, normalizedActivation), normalizedActivation);
}
}
- const config = resolvePhoneConfig(state);
+ const scopedState = scopeStateToActivationProvider(state, normalizedActivation);
+ const config = resolvePhoneConfig(scopedState);
if (config.provider === PHONE_SMS_PROVIDER_5SIM) {
const payload = await fetchFiveSimPayload(
config,
@@ -3980,7 +4088,7 @@
);
return `free reuse setStatus(${normalizedStatus}) skipped`;
}
- const config = resolvePhoneConfig(state);
+ const config = resolvePhoneConfig(scopeStateToActivationProvider(state, normalizedActivation));
if (config.provider === PHONE_SMS_PROVIDER_5SIM) {
const endpoint = normalizedStatus === 6
? `/user/finish/${normalizedActivation.activationId}`
@@ -4029,7 +4137,7 @@
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
- await provider.finishActivation(state, activation);
+ await provider.finishActivation(scopeStateToActivationProvider(state, activation), activation);
return;
}
}
@@ -4050,7 +4158,7 @@
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
- await provider.cancelActivation(state, activation);
+ await provider.cancelActivation(scopeStateToActivationProvider(state, activation), activation);
return;
}
}
@@ -4151,7 +4259,7 @@
if (getActivationProviderId(activation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
- await provider.banActivation(state, activation);
+ await provider.banActivation(scopeStateToActivationProvider(state, activation), activation);
return;
}
}
@@ -4162,7 +4270,7 @@
}
async function requestAdditionalPhoneSms(state = {}, activation) {
- const config = resolvePhoneConfig(state);
+ const config = resolvePhoneConfig(scopeStateToActivationProvider(state, activation));
if (config.provider !== PHONE_SMS_PROVIDER_HERO) {
return;
}
@@ -4342,12 +4450,13 @@
if (getActivationProviderId(normalizedActivation, state) === PHONE_SMS_PROVIDER_FIVE_SIM) {
const provider = getFiveSimProviderForState(state);
if (provider) {
- return provider.pollActivationCode(state, normalizedActivation, options);
+ return provider.pollActivationCode(scopeStateToActivationProvider(state, normalizedActivation), normalizedActivation, options);
}
}
const statusAction = resolveActivationStatusAction(normalizedActivation);
- const config = resolvePhoneConfig(state);
+ const scopedState = scopeStateToActivationProvider(state, normalizedActivation);
+ const config = resolvePhoneConfig(scopedState);
const configuredTimeoutMs = Math.max(1000, Number(options.timeoutMs) || 0);
const timeoutMs = configuredTimeoutMs || (
typeof getOAuthFlowStepTimeoutMs === 'function'
@@ -5101,6 +5210,7 @@
}
async function acquirePhoneActivation(state = {}, options = {}) {
+ state = await mergeLatestPhoneSettingsState(state);
const provider = normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER);
const providerOrder = resolvePhoneProviderOrder(state, provider);
const countryCandidates = resolveCountryCandidatesForProvider(state, provider);
@@ -5261,6 +5371,7 @@
{
blockedCountryIds: useBlockedCountryIds,
countryPriceFloorByCountryId: useCountryPriceFloorByCountryId,
+ preservePhoneSmsProvider: true,
}
);
const providerLabel = getPhoneSmsProviderLabel(providerCandidate);
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 26dcee2..e35dc8d 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -1536,6 +1536,15 @@
+
+
运营商
+
+
+ 按主国家读取 HeroSMS 运营商;不限时不附加 operator。
+
+
接码 API
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index 1211ecd..b20934e 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -460,6 +460,7 @@ const rowHeroSmsPlatform = document.getElementById('row-hero-sms-platform');
const rowHeroSmsCountry = document.getElementById('row-hero-sms-country');
const rowHeroSmsCountryFallback = document.getElementById('row-hero-sms-country-fallback');
const rowHeroSmsAcquirePriority = document.getElementById('row-hero-sms-acquire-priority');
+const rowHeroSmsOperator = document.getElementById('row-hero-sms-operator');
const rowHeroSmsApiKey = document.getElementById('row-hero-sms-api-key');
const rowHeroSmsMaxPrice = document.getElementById('row-hero-sms-max-price');
const rowPhoneSmsProvider = document.getElementById('row-phone-sms-provider');
@@ -514,6 +515,7 @@ const inputFreeReusablePhone = document.getElementById('input-free-reusable-phon
const selectHeroSmsCountry = document.getElementById('select-hero-sms-country');
const selectHeroSmsCountryFallback = document.getElementById('select-hero-sms-country-fallback');
const selectHeroSmsAcquirePriority = document.getElementById('select-hero-sms-acquire-priority');
+const selectHeroSmsOperator = document.getElementById('select-hero-sms-operator');
const selectHeroSmsPreferredActivation = document.getElementById('select-hero-sms-preferred-activation');
const selectFiveSimCountry = document.getElementById('select-five-sim-country');
const heroSmsCountryMenuShell = document.getElementById('hero-sms-country-menu-shell');
@@ -606,6 +608,9 @@ let currentStepDefinitionFlowId = DEFAULT_ACTIVE_FLOW_ID;
let phoneSignupReuseUiWasLocked = false;
let kiroRsConnectionTestStatusText = '未测试';
let heroSmsCountrySelectionOrder = [];
+let heroSmsOperatorsByCountryId = new Map();
+let heroSmsOperatorsLoadedAt = 0;
+let isRenderingHeroSmsOperatorOptions = false;
let phoneSmsProviderOrderSelection = [];
let heroSmsCountryMenuSearchKeyword = '';
const heroSmsCountrySearchTextById = new Map();
@@ -680,6 +685,8 @@ const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
const DEFAULT_NEX_SMS_COUNTRY_ORDER = Object.freeze([1]);
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
const HERO_SMS_COUNTRY_SELECTION_MAX = 3;
+const DEFAULT_HERO_SMS_OPERATOR = 'any';
+const HERO_SMS_OPERATORS_URL = 'https://hero-sms.com/stubs/handler_api.php?action=getOperators';
const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
@@ -4667,6 +4674,9 @@ function collectSettingsPayload() {
? DEFAULT_HERO_SMS_ACQUIRE_PRIORITY
: 'country'
);
+ const heroSmsOperatorValue = typeof selectHeroSmsOperator !== 'undefined' && selectHeroSmsOperator
+ ? normalizeHeroSmsOperatorValue(selectHeroSmsOperator.value || latestState?.heroSmsOperator)
+ : normalizeHeroSmsOperatorValue(latestState?.heroSmsOperator);
const currentPhoneSmsMaxPriceValue = typeof inputHeroSmsMaxPrice !== 'undefined' && inputHeroSmsMaxPrice
? normalizePhoneSmsMaxPriceValue(inputHeroSmsMaxPrice.value, phoneSmsProviderValue)
: '';
@@ -5263,6 +5273,7 @@ function collectSettingsPayload() {
freePhoneReuseEnabled: freePhoneReuseEnabledValue,
freePhoneReuseAutoEnabled: freePhoneReuseAutoEnabledValue,
heroSmsAcquirePriority: heroSmsAcquirePriorityValue,
+ heroSmsOperator: heroSmsOperatorValue,
heroSmsMinPrice: heroSmsMinPriceValue,
heroSmsMaxPrice: heroSmsMaxPriceValue,
heroSmsPreferredPrice: heroSmsPreferredPriceValue,
@@ -6151,6 +6162,21 @@ function normalizeHeroSmsAcquirePriority(value = '') {
return HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
}
+function normalizeHeroSmsOperatorValue(value = '', fallback = DEFAULT_HERO_SMS_OPERATOR) {
+ const normalized = String(value || '')
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9_-]+/g, '');
+ if (normalized) {
+ return normalized;
+ }
+ const fallbackNormalized = String(fallback || '')
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9_-]+/g, '');
+ return fallbackNormalized || DEFAULT_HERO_SMS_OPERATOR;
+}
+
function normalizeHeroSmsCountryFallbackList(value = []) {
const source = Array.isArray(value)
? value
@@ -6323,6 +6349,35 @@ function parseHeroSmsCountryPayload(payload) {
return [];
}
+function parseHeroSmsOperatorsPayload(payload) {
+ const source = payload?.countryOperators || payload?.operators || payload?.data || payload?.result || payload;
+ if (!source || typeof source !== 'object' || Array.isArray(source)) {
+ return new Map();
+ }
+ const result = new Map();
+ Object.entries(source).forEach(([countryId, operators]) => {
+ const normalizedCountryId = Math.floor(Number(countryId));
+ if (!Number.isFinite(normalizedCountryId) || normalizedCountryId <= 0 || !Array.isArray(operators)) {
+ return;
+ }
+ const seen = new Set();
+ const normalizedOperators = operators
+ .map((operator) => normalizeHeroSmsOperatorValue(operator, ''))
+ .filter(Boolean)
+ .filter((operator) => {
+ if (seen.has(operator)) {
+ return false;
+ }
+ seen.add(operator);
+ return true;
+ });
+ if (normalizedOperators.length) {
+ result.set(String(normalizedCountryId), normalizedOperators);
+ }
+ });
+ return result;
+}
+
function normalizeHeroSmsPriceForPreview(value) {
const direct = Number(value);
if (Number.isFinite(direct) && direct >= 0) {
@@ -7693,6 +7748,141 @@ async function loadHeroSmsCountries(options = {}) {
showLimitToast: false,
});
updateHeroSmsPlatformDisplay();
+ setHeroSmsOperatorSelectValue(latestState?.heroSmsOperator);
+ refreshHeroSmsOperatorOptions({ silent: true, selectedOperator: latestState?.heroSmsOperator });
+}
+
+function getHeroSmsOperatorCountryId() {
+ const countrySelect = selectHeroSmsCountry || selectHeroSmsCountryFallback;
+ if (heroSmsCountrySelectionOrder.length) {
+ return normalizeHeroSmsCountryId(heroSmsCountrySelectionOrder[0], DEFAULT_HERO_SMS_COUNTRY_ID);
+ }
+ const selectedOption = countrySelect
+ ? Array.from(countrySelect.options || []).find((option) => option.selected)
+ : null;
+ return normalizeHeroSmsCountryId(
+ selectedOption?.value || latestState?.heroSmsCountryId,
+ DEFAULT_HERO_SMS_COUNTRY_ID
+ );
+}
+
+async function loadHeroSmsOperators(options = {}) {
+ const silent = Boolean(options?.silent);
+ const force = Boolean(options?.force);
+ if (
+ !force
+ && heroSmsOperatorsByCountryId instanceof Map
+ && heroSmsOperatorsByCountryId.size
+ && Date.now() - heroSmsOperatorsLoadedAt < 10 * 60 * 1000
+ ) {
+ return heroSmsOperatorsByCountryId;
+ }
+ try {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), 3500);
+ const response = await fetch(HERO_SMS_OPERATORS_URL, {
+ signal: controller.signal,
+ cache: 'no-store',
+ headers: { Accept: 'application/json' },
+ });
+ clearTimeout(timeoutId);
+ if (!response.ok) {
+ throw new Error(`HTTP ${response.status}`);
+ }
+ const payload = await response.json();
+ const parsed = parseHeroSmsOperatorsPayload(payload);
+ if (!parsed.size) {
+ throw new Error('运营商列表为空');
+ }
+ heroSmsOperatorsByCountryId = parsed;
+ heroSmsOperatorsLoadedAt = Date.now();
+ } catch (error) {
+ if (!(heroSmsOperatorsByCountryId instanceof Map)) {
+ heroSmsOperatorsByCountryId = new Map();
+ }
+ if (!silent && typeof showToast === 'function') {
+ showToast(`HeroSMS 运营商列表加载失败:${normalizeHeroSmsFetchErrorMessage(error)}(已保留“不限”)`, 'warn', 2600);
+ }
+ }
+ return heroSmsOperatorsByCountryId;
+}
+
+function renderHeroSmsOperatorOptions(selectedOperator = null) {
+ if (!selectHeroSmsOperator || isRenderingHeroSmsOperatorOptions) {
+ return;
+ }
+ isRenderingHeroSmsOperatorOptions = true;
+ try {
+ const currentValue = normalizeHeroSmsOperatorValue(
+ selectedOperator !== null && selectedOperator !== undefined
+ ? selectedOperator
+ : (selectHeroSmsOperator.value || latestState?.heroSmsOperator),
+ DEFAULT_HERO_SMS_OPERATOR
+ );
+ const countryId = String(getHeroSmsOperatorCountryId());
+ const operators = heroSmsOperatorsByCountryId instanceof Map
+ ? (heroSmsOperatorsByCountryId.get(countryId) || [])
+ : [];
+ selectHeroSmsOperator.innerHTML = '';
+ const anyOption = document.createElement('option');
+ anyOption.value = DEFAULT_HERO_SMS_OPERATOR;
+ anyOption.textContent = '不限(any)';
+ selectHeroSmsOperator.appendChild(anyOption);
+ operators.forEach((operator) => {
+ const option = document.createElement('option');
+ option.value = operator;
+ option.textContent = operator;
+ selectHeroSmsOperator.appendChild(option);
+ });
+ if (
+ currentValue
+ && currentValue !== DEFAULT_HERO_SMS_OPERATOR
+ && !Array.from(selectHeroSmsOperator.options).some((option) => option.value === currentValue)
+ ) {
+ const selectedOption = document.createElement('option');
+ selectedOption.value = currentValue;
+ selectedOption.textContent = currentValue;
+ selectHeroSmsOperator.appendChild(selectedOption);
+ }
+ selectHeroSmsOperator.value = currentValue || DEFAULT_HERO_SMS_OPERATOR;
+ selectHeroSmsOperator.disabled = operators.length === 0;
+ } finally {
+ isRenderingHeroSmsOperatorOptions = false;
+ }
+}
+
+function setHeroSmsOperatorSelectValue(operator = latestState?.heroSmsOperator) {
+ if (!selectHeroSmsOperator) {
+ return DEFAULT_HERO_SMS_OPERATOR;
+ }
+ const normalized = normalizeHeroSmsOperatorValue(operator, DEFAULT_HERO_SMS_OPERATOR);
+ if (
+ normalized
+ && normalized !== DEFAULT_HERO_SMS_OPERATOR
+ && !Array.from(selectHeroSmsOperator.options || []).some((option) => option.value === normalized)
+ ) {
+ const selectedOption = document.createElement('option');
+ selectedOption.value = normalized;
+ selectedOption.textContent = normalized;
+ selectHeroSmsOperator.appendChild(selectedOption);
+ }
+ selectHeroSmsOperator.value = normalized;
+ return normalized;
+}
+
+function refreshHeroSmsOperatorOptions(options = {}) {
+ if (!selectHeroSmsOperator) {
+ return Promise.resolve(heroSmsOperatorsByCountryId);
+ }
+ return loadHeroSmsOperators(options)
+ .then(() => {
+ renderHeroSmsOperatorOptions(options?.selectedOperator);
+ return heroSmsOperatorsByCountryId;
+ })
+ .catch(() => {
+ renderHeroSmsOperatorOptions(options?.selectedOperator);
+ return heroSmsOperatorsByCountryId;
+ });
}
function getFiveSimCountryLabelByCode(code = '') {
@@ -9655,6 +9845,7 @@ function updatePhoneVerificationSettingsUI() {
typeof rowHeroSmsCountry !== 'undefined' ? rowHeroSmsCountry : null,
typeof rowHeroSmsCountryFallback !== 'undefined' ? rowHeroSmsCountryFallback : null,
typeof rowHeroSmsAcquirePriority !== 'undefined' ? rowHeroSmsAcquirePriority : null,
+ typeof rowHeroSmsOperator !== 'undefined' ? rowHeroSmsOperator : null,
typeof rowHeroSmsApiKey !== 'undefined' ? rowHeroSmsApiKey : null,
typeof rowFiveSimApiKey !== 'undefined' ? rowFiveSimApiKey : null,
typeof rowFiveSimCountry !== 'undefined' ? rowFiveSimCountry : null,
@@ -9687,6 +9878,7 @@ function updatePhoneVerificationSettingsUI() {
if (rowHeroSmsCountry) rowHeroSmsCountry.style.display = showSettings && heroProvider ? '' : 'none';
if (rowHeroSmsCountryFallback) rowHeroSmsCountryFallback.style.display = showSettings && heroProvider ? '' : 'none';
if (rowHeroSmsAcquirePriority) rowHeroSmsAcquirePriority.style.display = showSettings && heroProvider ? '' : 'none';
+ if (rowHeroSmsOperator) rowHeroSmsOperator.style.display = showSettings && heroProvider ? '' : 'none';
if (rowHeroSmsApiKey) rowHeroSmsApiKey.style.display = showSettings && heroProvider ? '' : 'none';
if (rowFiveSimApiKey) rowFiveSimApiKey.style.display = showSettings && fiveSimProvider ? '' : 'none';
if (rowFiveSimCountry) rowFiveSimCountry.style.display = showSettings && fiveSimProvider ? '' : 'none';
@@ -11664,6 +11856,9 @@ function applySettingsState(state) {
if (typeof selectHeroSmsAcquirePriority !== 'undefined' && selectHeroSmsAcquirePriority) {
selectHeroSmsAcquirePriority.value = normalizeHeroSmsAcquirePriority(state?.heroSmsAcquirePriority);
}
+ if (typeof selectHeroSmsOperator !== 'undefined' && selectHeroSmsOperator) {
+ setHeroSmsOperatorSelectValue(state?.heroSmsOperator);
+ }
if (inputHeroSmsMaxPrice) {
inputHeroSmsMaxPrice.value = restoredPhoneSmsProvider === PHONE_SMS_PROVIDER_FIVE_SIM
? normalizeFiveSimMaxPriceValue(state?.fiveSimMaxPrice || '')
@@ -11731,6 +11926,9 @@ function applySettingsState(state) {
{ includePrimary: true }
);
updateHeroSmsPlatformDisplay();
+ if (restoredPhoneSmsProvider === PHONE_SMS_PROVIDER_HERO_SMS) {
+ refreshHeroSmsOperatorOptions({ silent: true, selectedOperator: state?.heroSmsOperator });
+ }
} else if (selectHeroSmsCountry) {
const restoredCountryId = restoredPhoneSmsProvider === PHONE_SMS_PROVIDER_FIVE_SIM
? String(normalizeFiveSimCountryId(state?.fiveSimCountryId))
@@ -16960,6 +17158,7 @@ async function switchPhoneSmsProvider(nextProvider) {
patch.heroSmsApiKey = currentApiKey;
patch.heroSmsMaxPrice = currentMaxPrice;
patch.heroSmsMinPrice = currentMinPrice;
+ patch.heroSmsOperator = normalizeHeroSmsOperatorValue(selectHeroSmsOperator?.value || latestState?.heroSmsOperator);
patch.heroSmsCountryId = currentPrimary.id;
patch.heroSmsCountryLabel = currentPrimary.label;
patch.heroSmsCountryFallback = currentFallback;
@@ -16986,6 +17185,9 @@ async function switchPhoneSmsProvider(nextProvider) {
if (inputFiveSimOperator) {
inputFiveSimOperator.value = normalizeFiveSimOperator(latestState?.fiveSimOperator);
}
+ if (selectHeroSmsOperator) {
+ setHeroSmsOperatorSelectValue(latestState?.heroSmsOperator);
+ }
if (displayHeroSmsPriceTiers) displayHeroSmsPriceTiers.textContent = '未获取';
if (displayPhoneSmsBalance) displayPhoneSmsBalance.textContent = '余额未获取';
if (rowHeroSmsPriceTiers) rowHeroSmsPriceTiers.style.display = 'none';
@@ -17004,6 +17206,9 @@ async function switchPhoneSmsProvider(nextProvider) {
? normalizeFiveSimCountryFallbackList(latestState?.fiveSimCountryFallback || [])
: normalizeHeroSmsCountryFallbackList(latestState?.heroSmsCountryFallback || []);
applyHeroSmsFallbackSelection([restoredPrimary, ...restoredFallback], { includePrimary: true });
+ if (normalizedNextProvider === PHONE_SMS_PROVIDER_HERO_SMS) {
+ refreshHeroSmsOperatorOptions({ silent: true, selectedOperator: latestState?.heroSmsOperator });
+ }
updatePhoneVerificationSettingsUI();
markSettingsDirty(true);
saveSettings({ silent: true }).catch(() => {});
@@ -17112,6 +17317,8 @@ selectPhoneSmsProvider?.addEventListener('change', async () => {
],
{ includePrimary: true }
);
+ setHeroSmsOperatorSelectValue(latestState?.heroSmsOperator);
+ refreshHeroSmsOperatorOptions({ silent: true, selectedOperator: latestState?.heroSmsOperator });
}
updateHeroSmsPlatformDisplay();
updatePhoneVerificationSettingsUI();
@@ -17330,6 +17537,19 @@ inputFiveSimOperator?.addEventListener('blur', () => {
inputFiveSimOperator.value = normalizeFiveSimOperator(inputFiveSimOperator.value);
saveSettings({ silent: true }).catch(() => { });
});
+selectHeroSmsOperator?.addEventListener('change', () => {
+ const nextOperator = normalizeHeroSmsOperatorValue(selectHeroSmsOperator.value);
+ setHeroSmsOperatorSelectValue(nextOperator);
+ syncLatestState({ heroSmsOperator: nextOperator });
+ markSettingsDirty(true);
+ saveSettings({ silent: true }).catch(() => { });
+});
+selectHeroSmsOperator?.addEventListener('focus', () => {
+ refreshHeroSmsOperatorOptions({ silent: true, selectedOperator: selectHeroSmsOperator.value });
+});
+selectHeroSmsOperator?.addEventListener('pointerdown', () => {
+ refreshHeroSmsOperatorOptions({ silent: true, selectedOperator: selectHeroSmsOperator.value });
+});
inputHeroSmsPreferredPrice?.addEventListener('input', () => {
markSettingsDirty(true);
scheduleSettingsAutoSave();
@@ -18259,6 +18479,9 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.payload.heroSmsAcquirePriority !== undefined && selectHeroSmsAcquirePriority) {
selectHeroSmsAcquirePriority.value = normalizeHeroSmsAcquirePriority(message.payload.heroSmsAcquirePriority);
}
+ if (message.payload.heroSmsOperator !== undefined && selectHeroSmsOperator) {
+ setHeroSmsOperatorSelectValue(message.payload.heroSmsOperator);
+ }
if ((message.payload.heroSmsMaxPrice !== undefined || message.payload.fiveSimMaxPrice !== undefined) && inputHeroSmsMaxPrice) {
inputHeroSmsMaxPrice.value = getSelectedPhoneSmsProvider() === PHONE_SMS_PROVIDER_FIVE_SIM
? normalizeFiveSimMaxPriceValue(message.payload.fiveSimMaxPrice !== undefined ? message.payload.fiveSimMaxPrice : latestState?.fiveSimMaxPrice)
@@ -18427,6 +18650,9 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
{ includePrimary: true }
);
updateHeroSmsPlatformDisplay();
+ if (activeProvider === PHONE_SMS_PROVIDER_HERO_SMS) {
+ refreshHeroSmsOperatorOptions({ silent: true, selectedOperator: latestState?.heroSmsOperator });
+ }
}
if (
message.payload.currentPhoneActivation !== undefined
diff --git a/tests/background-account-history-settings.test.js b/tests/background-account-history-settings.test.js
index 82af977..29b9901 100644
--- a/tests/background-account-history-settings.test.js
+++ b/tests/background-account-history-settings.test.js
@@ -73,6 +73,7 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizePhoneCodePollMaxRounds'),
extractFunction('normalizeHeroSmsMaxPrice'),
extractFunction('normalizeHeroSmsCountryFallback'),
+ extractFunction('normalizeHeroSmsOperator'),
extractFunction('normalizePhoneSmsProvider'),
extractFunction('normalizeFiveSimCountryId'),
extractFunction('normalizeFiveSimCountryLabel'),
@@ -114,6 +115,7 @@ const VERIFICATION_RESEND_COUNT_MIN = 0;
const VERIFICATION_RESEND_COUNT_MAX = 20;
const HERO_SMS_COUNTRY_ID = 52;
const HERO_SMS_COUNTRY_LABEL = 'Thailand';
+const DEFAULT_HERO_SMS_OPERATOR = 'any';
const PHONE_SMS_PROVIDER_HERO_SMS = 'hero-sms';
const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const PHONE_SMS_PROVIDER_NEXSMS = 'nexsms';
@@ -280,6 +282,8 @@ return {
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0.123456'), '0.1235');
assert.equal(api.normalizePersistentSettingValue('heroSmsMaxPrice', '0'), '');
assert.equal(api.normalizePersistentSettingValue('heroSmsPreferredPrice', '0.051234'), '0.0512');
+ assert.equal(api.normalizePersistentSettingValue('heroSmsOperator', ' AIS!! '), 'ais');
+ assert.equal(api.normalizePersistentSettingValue('heroSmsOperator', ''), 'any');
assert.equal(api.normalizePersistentSettingValue('signupMethod', 'phone'), 'phone');
assert.equal(api.normalizePersistentSettingValue('signupMethod', 'unknown'), 'email');
assert.equal(api.normalizePersistentSettingValue('activeFlowId', 'codex'), 'openai');
diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js
index 7663ba5..4626fc2 100644
--- a/tests/phone-verification-flow.test.js
+++ b/tests/phone-verification-flow.test.js
@@ -83,6 +83,78 @@ test('phone verification helper requests HeroSMS numbers with fixed OpenAI and T
assert.equal(requests[1].searchParams.get('api_key'), 'demo-key');
});
+test('phone verification helper appends HeroSMS operator when configured', async () => {
+ const requests = [];
+ 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') {
+ return {
+ ok: true,
+ text: async () => 'ACCESS_NUMBER:123456:66959916439',
+ };
+ }
+ throw new Error(`Unexpected HeroSMS action: ${action}`);
+ },
+ getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsOperator: 'ais' }),
+ sendToContentScriptResilient: async () => ({}),
+ setState: async () => {},
+ sleepWithStop: async () => {},
+ throwIfStopped: () => {},
+ });
+
+ await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key', heroSmsOperator: ' AIS!! ' });
+
+ const getNumberRequest = requests.find((requestUrl) => requestUrl.searchParams.get('action') === 'getNumber');
+ assert.equal(getNumberRequest.searchParams.get('operator'), 'ais');
+});
+
+test('phone verification helper reads latest HeroSMS operator from persistent state', async () => {
+ const requests = [];
+ 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') {
+ return {
+ ok: true,
+ text: async () => 'ACCESS_NUMBER:123456:66959916439',
+ };
+ }
+ throw new Error(`Unexpected HeroSMS action: ${action}`);
+ },
+ getState: async () => ({ heroSmsApiKey: 'demo-key', heroSmsOperator: 'dtac' }),
+ sendToContentScriptResilient: async () => ({}),
+ setState: async () => {},
+ sleepWithStop: async () => {},
+ throwIfStopped: () => {},
+ });
+
+ await helpers.requestPhoneActivation({ heroSmsApiKey: 'demo-key' });
+
+ const getNumberRequest = requests.find((requestUrl) => requestUrl.searchParams.get('action') === 'getNumber');
+ assert.equal(getNumberRequest.searchParams.get('operator'), 'dtac');
+});
+
test('signup phone helper persists signup runtime state without touching add-phone activation', async () => {
const setStateCalls = [];
let currentState = {
@@ -2208,6 +2280,152 @@ test('phone verification helper acquires a number from 5sim with fallback countr
assert.equal(requests[3].pathname, '/v1/user/buy/activation/england/any/openai');
});
+test('phone verification helper preserves fallback provider while refreshing latest settings', async () => {
+ const heroRequests = [];
+ const fiveSimRequests = [];
+ const submittedNumbers = [];
+ let currentState = {
+ heroSmsApiKey: 'hero-key',
+ phoneSmsProvider: 'hero-sms',
+ phoneSmsProviderOrder: ['hero-sms', '5sim'],
+ heroSmsCountryId: 52,
+ heroSmsCountryLabel: 'Thailand',
+ fiveSimApiKey: 'five-token',
+ fiveSimCountryOrder: ['vietnam'],
+ fiveSimOperator: 'any',
+ fiveSimProduct: 'openai',
+ phoneSmsReuseEnabled: true,
+ verificationResendCount: 0,
+ phoneVerificationReplacementLimit: 1,
+ phoneCodeWaitSeconds: 15,
+ phoneCodeTimeoutWindows: 1,
+ phoneCodePollIntervalSeconds: 1,
+ phoneCodePollMaxRounds: 1,
+ currentPhoneActivation: null,
+ reusablePhoneActivation: null,
+ };
+
+ const helpers = api.createPhoneVerificationHelpers({
+ addLog: async () => {},
+ ensureStep8SignupPageReady: async () => {},
+ fetchImpl: async (url, options = {}) => {
+ const parsedUrl = new URL(url);
+ const action = parsedUrl.searchParams.get('action');
+ if (parsedUrl.hostname === 'hero-sms.com') {
+ heroRequests.push(parsedUrl);
+ if (action === 'getPrices') {
+ return {
+ ok: true,
+ text: async () => buildHeroSmsPricesPayload({ country: '52', cost: 0.05, count: 5 }),
+ };
+ }
+ if (action === 'getNumber' || action === 'getNumberV2') {
+ return { ok: true, text: async () => 'NO_NUMBERS' };
+ }
+ throw new Error(`Unexpected HeroSMS action: ${action}`);
+ }
+ fiveSimRequests.push({ url: parsedUrl, options });
+ if (parsedUrl.pathname === '/v1/guest/prices') {
+ return {
+ ok: true,
+ status: 200,
+ text: async () => JSON.stringify({
+ openai: {
+ vietnam: {
+ any: {
+ cost: 0.08,
+ count: 3,
+ },
+ },
+ },
+ }),
+ };
+ }
+ if (parsedUrl.pathname === '/v1/user/buy/activation/vietnam/any/openai') {
+ return {
+ ok: true,
+ status: 200,
+ text: async () => JSON.stringify({
+ id: 5020,
+ phone: '+84901122334',
+ country: 'vietnam',
+ country_name: 'Vietnam',
+ product: 'openai',
+ }),
+ };
+ }
+ if (parsedUrl.pathname === '/v1/user/check/5020') {
+ return {
+ ok: true,
+ status: 200,
+ text: async () => JSON.stringify({
+ id: 5020,
+ phone: '+84901122334',
+ status: 'RECEIVED',
+ sms: [{ text: 'OpenAI code 123456' }],
+ }),
+ };
+ }
+ if (parsedUrl.pathname === '/v1/user/finish/5020') {
+ return {
+ ok: true,
+ status: 200,
+ text: async () => JSON.stringify({ status: 'FINISHED' }),
+ };
+ }
+ throw new Error(`Unexpected 5sim request: ${parsedUrl.pathname}`);
+ },
+ getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
+ getState: async () => ({ ...currentState }),
+ sendToContentScriptResilient: async (_source, message) => {
+ if (message.type === 'SUBMIT_PHONE_NUMBER') {
+ submittedNumbers.push(message.payload.phoneNumber);
+ 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(submittedNumbers, ['+84901122334']);
+ assert.equal(currentState.reusablePhoneActivation.provider, '5sim');
+ assert.equal(currentState.reusablePhoneActivation.activationId, '5020');
+ assert.equal(heroRequests.some((requestUrl) => requestUrl.searchParams.get('action') === 'getNumber'), true);
+ assert.equal(heroRequests.some((requestUrl) => requestUrl.searchParams.get('action') === 'getNumberV2'), true);
+ assert.equal(
+ heroRequests.every((requestUrl) => ['getPrices', 'getNumber', 'getNumberV2'].includes(requestUrl.searchParams.get('action'))),
+ true
+ );
+ assert.deepStrictEqual(
+ fiveSimRequests.map((entry) => entry.url.pathname),
+ [
+ '/v1/guest/prices',
+ '/v1/user/buy/activation/vietnam/any/openai',
+ '/v1/user/check/5020',
+ '/v1/user/finish/5020',
+ ]
+ );
+ assert.equal(fiveSimRequests[1].options.headers.Authorization, 'Bearer five-token');
+});
+
test('phone verification helper prefers phoneSmsReuseEnabled over legacy heroSmsReuseEnabled for 5sim acquisition', async () => {
const requests = [];
const helpers = api.createPhoneVerificationHelpers({
diff --git a/tests/sidepanel-phone-verification-settings.test.js b/tests/sidepanel-phone-verification-settings.test.js
index 9548234..fd9dcf5 100644
--- a/tests/sidepanel-phone-verification-settings.test.js
+++ b/tests/sidepanel-phone-verification-settings.test.js
@@ -95,6 +95,8 @@ test('sidepanel html exposes phone verification toggle and multi-provider SMS ro
assert.match(html, /id="row-hero-sms-country-fallback"/);
assert.match(html, /id="row-hero-sms-acquire-priority"/);
assert.match(html, /id="select-hero-sms-acquire-priority"/);
+ assert.match(html, /id="row-hero-sms-operator"/);
+ assert.match(html, /id="select-hero-sms-operator"/);
assert.match(html, /id="select-hero-sms-country"[^>]*multiple/);
assert.doesNotMatch(html, /id="select-hero-sms-country-fallback"/);
assert.match(html, /id="row-hero-sms-api-key"/);
@@ -660,6 +662,7 @@ const rowHeroSmsPlatform = { style: { display: 'none' } };
const rowHeroSmsCountry = { style: { display: 'none' } };
const rowHeroSmsCountryFallback = { style: { display: 'none' } };
const rowHeroSmsAcquirePriority = { style: { display: 'none' } };
+const rowHeroSmsOperator = { style: { display: 'none' } };
const rowHeroSmsApiKey = { style: { display: 'none' } };
const rowHeroSmsMaxPrice = { style: { display: 'none' } };
const rowFiveSimApiKey = { style: { display: 'none' } };
@@ -741,6 +744,7 @@ return {
rowHeroSmsCountry,
rowHeroSmsCountryFallback,
rowHeroSmsAcquirePriority,
+ rowHeroSmsOperator,
rowHeroSmsApiKey,
rowHeroSmsMaxPrice,
rowFiveSimApiKey,
@@ -795,6 +799,7 @@ return {
assert.equal(api.rowHeroSmsCountry.style.display, 'none');
assert.equal(api.rowHeroSmsCountryFallback.style.display, 'none');
assert.equal(api.rowHeroSmsAcquirePriority.style.display, 'none');
+ assert.equal(api.rowHeroSmsOperator.style.display, 'none');
assert.equal(api.rowHeroSmsApiKey.style.display, 'none');
assert.equal(api.rowHeroSmsMaxPrice.style.display, 'none');
assert.equal(api.rowFiveSimOperator.style.display, 'none');
@@ -845,6 +850,7 @@ return {
assert.equal(api.rowHeroSmsCountry.style.display, '');
assert.equal(api.rowHeroSmsCountryFallback.style.display, '');
assert.equal(api.rowHeroSmsAcquirePriority.style.display, '');
+ assert.equal(api.rowHeroSmsOperator.style.display, '');
assert.equal(api.rowHeroSmsApiKey.style.display, '');
assert.equal(api.rowHeroSmsMaxPrice.style.display, '');
assert.equal(api.rowFiveSimOperator.style.display, 'none');
@@ -1001,6 +1007,7 @@ const inputNexSmsApiKey = { value: 'nex-key' };
const inputNexSmsServiceCode = { value: 'ot' };
const inputHeroSmsReuseEnabled = { checked: true };
const selectHeroSmsAcquirePriority = { value: 'price' };
+const selectHeroSmsOperator = { value: 'AIS!!' };
function getSelectedPhonePreferredActivation() {
return {
provider: 'hero-sms',
@@ -1039,6 +1046,7 @@ const DEFAULT_HERO_SMS_REUSE_ENABLED = true;
const HERO_SMS_ACQUIRE_PRIORITY_COUNTRY = 'country';
const HERO_SMS_ACQUIRE_PRIORITY_PRICE = 'price';
const DEFAULT_HERO_SMS_ACQUIRE_PRIORITY = HERO_SMS_ACQUIRE_PRIORITY_COUNTRY;
+const DEFAULT_HERO_SMS_OPERATOR = 'any';
const PHONE_REPLACEMENT_LIMIT_MIN = 1;
const PHONE_REPLACEMENT_LIMIT_MAX = 20;
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
@@ -1101,6 +1109,7 @@ ${extractFunction('normalizeFiveSimCountryFallbackList')}
${extractFunction('normalizePhoneSmsMaxPriceValue')}
${extractFunction('normalizePhoneSmsMinPriceValue')}
${extractFunction('normalizeHeroSmsMaxPriceValue')}
+${extractFunction('normalizeHeroSmsOperatorValue')}
${extractFunction('normalizePhoneVerificationReplacementLimit')}
${extractFunction('normalizePhoneCodeWaitSecondsValue')}
${extractFunction('normalizePhoneCodeTimeoutWindowsValue')}
@@ -1153,6 +1162,7 @@ return { collectSettingsPayload };
assert.equal(payload.freePhoneReuseEnabled, false);
assert.equal(payload.freePhoneReuseAutoEnabled, false);
assert.equal(payload.heroSmsAcquirePriority, 'price');
+ assert.equal(payload.heroSmsOperator, 'ais');
assert.equal(payload.heroSmsMinPrice, '0.03');
assert.equal(payload.heroSmsMaxPrice, '0.12');
assert.equal(payload.heroSmsPreferredPrice, '0.0512');
@@ -1201,6 +1211,7 @@ const PHONE_SMS_PROVIDER_FIVE_SIM = '5sim';
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'vietnam';
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
+const DEFAULT_HERO_SMS_OPERATOR = 'any';
const DEFAULT_HERO_SMS_COUNTRY_ID = 52;
const DEFAULT_HERO_SMS_COUNTRY_LABEL = 'Thailand';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
@@ -1210,6 +1221,7 @@ const inputHeroSmsApiKey = { value: 'hero-live' };
const inputHeroSmsMinPrice = { value: '0.03' };
const inputHeroSmsMaxPrice = { value: '0.22' };
const inputFiveSimOperator = { value: 'any' };
+const selectHeroSmsOperator = { value: 'ais', options: [{ value: 'any' }, { value: 'ais' }] };
const displayHeroSmsPriceTiers = { textContent: '' };
const displayPhoneSmsBalance = { textContent: '' };
const rowHeroSmsPriceTiers = { style: { display: '' } };
@@ -1229,6 +1241,7 @@ ${extractFunction('normalizePhoneSmsMinPriceValue')}
${extractFunction('normalizePhoneSmsMaxPriceValue')}
${extractFunction('normalizeHeroSmsCountryId')}
${extractFunction('normalizeHeroSmsCountryLabel')}
+${extractFunction('normalizeHeroSmsOperatorValue')}
${extractFunction('normalizeHeroSmsCountryFallbackList')}
${extractFunction('normalizeFiveSimCountryFallbackList')}
function getSelectedHeroSmsCountryOption() {
@@ -1244,6 +1257,8 @@ function syncHeroSmsFallbackSelectionOrderFromSelect() {
function syncLatestState(patch) { latestState = { ...latestState, ...patch }; }
function loadHeroSmsCountries() { return Promise.resolve(); }
function applyHeroSmsFallbackSelection() {}
+function setHeroSmsOperatorSelectValue(operator = latestState?.heroSmsOperator) { selectHeroSmsOperator.value = normalizeHeroSmsOperatorValue(operator); }
+function refreshHeroSmsOperatorOptions() { return Promise.resolve(); }
function updatePhoneVerificationSettingsUI() {}
function markSettingsDirty() {}
function saveSettings() { savedPayload = { ...latestState }; return Promise.resolve(); }
@@ -1291,6 +1306,26 @@ return {
assert.equal(api.savedPayload.fiveSimMinPrice, '0.88');
});
+test('HeroSMS operator helpers normalize keyed operator payloads', () => {
+ const api = new Function(`
+const DEFAULT_HERO_SMS_OPERATOR = 'any';
+${extractFunction('normalizeHeroSmsOperatorValue')}
+${extractFunction('parseHeroSmsOperatorsPayload')}
+return { normalizeHeroSmsOperatorValue, parseHeroSmsOperatorsPayload };
+`)();
+
+ assert.equal(api.normalizeHeroSmsOperatorValue(' AIS!! '), 'ais');
+ assert.equal(api.normalizeHeroSmsOperatorValue('', 'dtac'), 'dtac');
+ const parsed = api.parseHeroSmsOperatorsPayload({
+ operators: {
+ 52: [' AIS ', 'dtac', 'AIS'],
+ 6: ['telkomsel'],
+ },
+ });
+ assert.deepStrictEqual(parsed.get('52'), ['ais', 'dtac']);
+ assert.deepStrictEqual(parsed.get('6'), ['telkomsel']);
+});
+
test('formatPhoneSmsPriceEntriesSummary treats HeroSMS physicalCount=0 as out of stock even when count is positive', () => {
const api = new Function(`
${extractFunction('normalizeHeroSmsPriceForPreview')}