diff --git a/background.js b/background.js index 94530fd..6aedfc6 100644 --- a/background.js +++ b/background.js @@ -809,6 +809,7 @@ const PERSISTED_SETTING_DEFAULTS = { autoStepDelaySeconds: null, step6CookieCleanupEnabled: false, phoneVerificationEnabled: false, + phoneSmsReuseEnabled: DEFAULT_HERO_SMS_REUSE_ENABLED, freePhoneReuseEnabled: true, freePhoneReuseAutoEnabled: true, signupMethod: DEFAULT_SIGNUP_METHOD, @@ -1329,12 +1330,15 @@ function normalizePhoneSmsProvider(value = '') { } return PHONE_SMS_PROVIDER_HERO_SMS; } - function normalizePhoneSmsProviderOrder(value = [], fallbackOrder = []) { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.PhoneSmsProviderRegistry?.normalizeProviderOrder) { + return rootScope.PhoneSmsProviderRegistry.normalizeProviderOrder(value, fallbackOrder); + } const source = Array.isArray(value) ? value : String(value || '') - .split(/[\r\n,,;;]+/) + .split(/[\r\n,]+/) .map((entry) => String(entry || '').trim()) .filter(Boolean); const normalized = []; @@ -1373,7 +1377,6 @@ function normalizePhoneSmsProviderOrder(value = [], fallbackOrder = []) { return normalized.slice(0, DEFAULT_PHONE_SMS_PROVIDER_ORDER.length); } - function normalizeSignupMethod(value = '') { return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' @@ -2668,6 +2671,7 @@ function normalizePersistentSettingValue(key, value) { return typeof value === 'boolean' ? value : true; case 'step6CookieCleanupEnabled': case 'phoneVerificationEnabled': + case 'phoneSmsReuseEnabled': case 'freePhoneReuseEnabled': case 'freePhoneReuseAutoEnabled': case 'plusModeEnabled': @@ -2842,6 +2846,13 @@ function buildPersistentSettingsPayload(input = {}, options = {}) { throw new Error('\u914d\u7f6e\u5185\u5bb9\u683c\u5f0f\u65e0\u6548\u3002'); } + const persistedSettingDefaults = typeof PERSISTED_SETTING_DEFAULTS !== 'undefined' && PERSISTED_SETTING_DEFAULTS + ? PERSISTED_SETTING_DEFAULTS + : {}; + const persistedSettingKeys = Array.isArray(typeof PERSISTED_SETTING_KEYS !== 'undefined' ? PERSISTED_SETTING_KEYS : null) + ? PERSISTED_SETTING_KEYS + : Object.keys(persistedSettingDefaults); + const normalizedInput = { ...input }; if (normalizedInput.autoStepDelaySeconds === undefined) { const legacyAutoStepDelaySeconds = resolveLegacyAutoStepDelaySeconds(normalizedInput); @@ -2860,15 +2871,29 @@ function buildPersistentSettingsPayload(input = {}, options = {}) { const payload = {}; let matchedKeyCount = 0; - for (const key of PERSISTED_SETTING_KEYS) { + for (const key of persistedSettingKeys) { if (normalizedInput[key] !== undefined) { payload[key] = normalizePersistentSettingValue(key, normalizedInput[key]); matchedKeyCount += 1; } else if (fillDefaults) { - payload[key] = normalizePersistentSettingValue(key, PERSISTED_SETTING_DEFAULTS[key]); + payload[key] = normalizePersistentSettingValue(key, persistedSettingDefaults[key]); } } + const hasPhoneSmsReuseEnabled = Object.prototype.hasOwnProperty.call(normalizedInput, 'phoneSmsReuseEnabled'); + const hasHeroSmsReuseEnabled = Object.prototype.hasOwnProperty.call(normalizedInput, 'heroSmsReuseEnabled'); + const hasFiveSimReuseEnabled = Object.prototype.hasOwnProperty.call(normalizedInput, 'fiveSimReuseEnabled'); + if (hasPhoneSmsReuseEnabled || hasHeroSmsReuseEnabled || hasFiveSimReuseEnabled) { + const reuseSource = hasPhoneSmsReuseEnabled + ? normalizedInput.phoneSmsReuseEnabled + : (hasHeroSmsReuseEnabled + ? normalizedInput.heroSmsReuseEnabled + : normalizedInput.fiveSimReuseEnabled); + const normalizedReuseEnabled = normalizePersistentSettingValue('phoneSmsReuseEnabled', reuseSource); + payload.phoneSmsReuseEnabled = normalizedReuseEnabled; + payload.heroSmsReuseEnabled = normalizedReuseEnabled; + } + if (requireKnownKeys && matchedKeyCount === 0) { throw new Error('\u914d\u7f6e\u6587\u4ef6\u4e2d\u6ca1\u6709\u53ef\u8bc6\u522b\u7684\u914d\u7f6e\u5185\u5bb9\u3002'); } diff --git a/background/phone-verification-flow.js b/background/phone-verification-flow.js index 6da2f70..ff61791 100644 --- a/background/phone-verification-flow.js +++ b/background/phone-verification-flow.js @@ -180,6 +180,10 @@ } function normalizePhoneSmsProvider(value = '') { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.PhoneSmsProviderRegistry?.normalizeProviderId) { + return rootScope.PhoneSmsProviderRegistry.normalizeProviderId(value); + } const normalized = String(value || '').trim().toLowerCase(); if (normalized === PHONE_SMS_PROVIDER_5SIM) { return PHONE_SMS_PROVIDER_5SIM; @@ -189,7 +193,6 @@ } return PHONE_SMS_PROVIDER_HERO; } - function isFiveSimProvider(state = {}) { return normalizePhoneSmsProvider(state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER) === PHONE_SMS_PROVIDER_5SIM; } @@ -515,6 +518,13 @@ return Boolean(value); } + function normalizePhoneSmsReuseEnabled(state = {}) { + if (Object.prototype.hasOwnProperty.call(state, 'phoneSmsReuseEnabled')) { + return Boolean(state.phoneSmsReuseEnabled); + } + return normalizeHeroSmsReuseEnabled(state?.heroSmsReuseEnabled); + } + function normalizeFreePhoneReuseEnabled(value) { return Boolean(value); } @@ -536,10 +546,14 @@ } function normalizePhoneSmsProviderOrder(value = [], fallbackOrder = []) { + const rootScope = typeof self !== 'undefined' ? self : globalThis; + if (rootScope.PhoneSmsProviderRegistry?.normalizeProviderOrder) { + return rootScope.PhoneSmsProviderRegistry.normalizeProviderOrder(value, fallbackOrder); + } const source = Array.isArray(value) ? value : String(value || '') - .split(/[\r\n,,;;|/]+/) + .split(/[\r\n,]+/) .map((entry) => String(entry || '').trim()) .filter(Boolean); const normalized = []; @@ -573,7 +587,6 @@ return fallbackNormalized.slice(0, 3); } - function resolvePhoneProviderOrder(state = {}, preferredProvider = '') { const currentProvider = normalizePhoneSmsProvider( preferredProvider || state?.phoneSmsProvider || DEFAULT_PHONE_SMS_PROVIDER @@ -856,10 +869,7 @@ } function isPhoneSmsReuseEnabled(state = {}) { - if (normalizePhoneSmsProvider(state?.phoneSmsProvider) === PHONE_SMS_PROVIDER_FIVE_SIM) { - return state?.fiveSimReuseEnabled !== false; - } - return normalizeHeroSmsReuseEnabled(state?.heroSmsReuseEnabled); + return normalizePhoneSmsReuseEnabled(state); } function createResolvedFiveSimProvider() { @@ -2658,7 +2668,7 @@ { query: { ...(candidatePrice !== null && candidatePrice !== undefined ? { maxPrice: candidatePrice } : {}), - ...(normalizeHeroSmsReuseEnabled(state.heroSmsReuseEnabled) ? { reuse: 1 } : {}), + ...(normalizePhoneSmsReuseEnabled(state) ? { reuse: 1 } : {}), }, } ); @@ -4850,7 +4860,7 @@ delete nextReusableActivation.phoneCodeReceived; delete nextReusableActivation.phoneCodeReceivedAt; await upsertReusableActivationPool(nextReusableActivation, { state }); - if (!normalizeHeroSmsReuseEnabled(state?.heroSmsReuseEnabled)) { + if (!normalizePhoneSmsReuseEnabled(state)) { await clearReusableActivation(); return; } diff --git a/phone-sms/providers/five-sim.js b/phone-sms/providers/five-sim.js index a4f81bf..dab3b6b 100644 --- a/phone-sms/providers/five-sim.js +++ b/phone-sms/providers/five-sim.js @@ -574,22 +574,32 @@ return /not\s+enough\s+(?:user\s+)?balance|not\s+enough\s+rating|unauthorized|invalid\s+token|banned|bad\s+(?:country|operator)|no\s+product|server\s+offline/i.test(text); } - function assertMaxPriceCompatibleWithOperator(state = {}) { - const maxPrice = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice); - const operator = normalizeFiveSimOperator(state.fiveSimOperator); - if (maxPrice && operator !== DEFAULT_OPERATOR) { - throw new Error('5sim maxPrice only works when operator is "any"; clear the price limit or switch operator to any before buying a number.'); - } - } - - async function buyActivationWithPrice(state = {}, countryConfig, maxPrice, deps = {}) { - const config = resolveConfig(state, deps); - const operator = normalizeFiveSimOperator(state.fiveSimOperator); + function assertMaxPriceCompatibleWithOperator(state = {}) { + const maxPrice = normalizeFiveSimMaxPrice(state.fiveSimMaxPrice); + const operator = normalizeFiveSimOperator(state.fiveSimOperator); + if (maxPrice && operator !== DEFAULT_OPERATOR) { + throw new Error('5sim maxPrice only works when operator is "any"; clear the price limit or switch operator to any before buying a number.'); + } + } + + function normalizeReuseEnabled(state = {}) { + if (Object.prototype.hasOwnProperty.call(state, 'phoneSmsReuseEnabled')) { + return state.phoneSmsReuseEnabled !== false; + } + if (Object.prototype.hasOwnProperty.call(state, 'heroSmsReuseEnabled')) { + return state.heroSmsReuseEnabled !== false; + } + return true; + } + + async function buyActivationWithPrice(state = {}, countryConfig, maxPrice, deps = {}) { + const config = resolveConfig(state, deps); + const operator = normalizeFiveSimOperator(state.fiveSimOperator); const query = {}; if (maxPrice !== null && maxPrice !== undefined) { query.maxPrice = maxPrice; } - if (state.fiveSimReuseEnabled !== false) { + if (normalizeReuseEnabled(state)) { query.reuse = 1; } const payload = await fetchJson( diff --git a/phone-sms/providers/registry.js b/phone-sms/providers/registry.js index 5ee7ced..068c2d5 100644 --- a/phone-sms/providers/registry.js +++ b/phone-sms/providers/registry.js @@ -1,41 +1,137 @@ -// phone-sms/providers/registry.js — 接码平台注册表 +// phone-sms/providers/registry.js - phone SMS provider registry (function attachPhoneSmsProviderRegistry(root, factory) { root.PhoneSmsProviderRegistry = factory(root); })(typeof self !== 'undefined' ? self : globalThis, function createPhoneSmsProviderRegistry(root) { const PROVIDER_HERO_SMS = 'hero-sms'; const PROVIDER_FIVE_SIM = '5sim'; + const PROVIDER_NEXSMS = 'nexsms'; const DEFAULT_PROVIDER = PROVIDER_HERO_SMS; + const DEFAULT_PROVIDER_ORDER = Object.freeze([ + PROVIDER_HERO_SMS, + PROVIDER_FIVE_SIM, + PROVIDER_NEXSMS, + ]); + const PROVIDER_DEFINITIONS = Object.freeze({ + [PROVIDER_HERO_SMS]: Object.freeze({ + id: PROVIDER_HERO_SMS, + label: 'HeroSMS', + moduleKey: 'PhoneSmsHeroSmsProvider', + }), + [PROVIDER_FIVE_SIM]: Object.freeze({ + id: PROVIDER_FIVE_SIM, + label: '5sim', + moduleKey: 'PhoneSmsFiveSimProvider', + }), + [PROVIDER_NEXSMS]: Object.freeze({ + id: PROVIDER_NEXSMS, + label: 'NexSMS', + moduleKey: 'PhoneSmsNexSmsProvider', + }), + }); - function normalizeProviderId(value = '') { - const normalized = String(value || '').trim().toLowerCase(); - return normalized === PROVIDER_FIVE_SIM ? PROVIDER_FIVE_SIM : PROVIDER_HERO_SMS; + function resolveProviderKey(value = '') { + if (value && typeof value === 'object' && !Array.isArray(value)) { + return String(value.provider || value.id || value.value || '').trim().toLowerCase(); + } + return String(value || '').trim().toLowerCase(); + } + + function isKnownProviderId(providerId = '') { + return Boolean(PROVIDER_DEFINITIONS[providerId]); + } + + function normalizeProviderId(value = '', fallback = DEFAULT_PROVIDER) { + const normalized = resolveProviderKey(value); + if (isKnownProviderId(normalized)) { + return normalized; + } + const fallbackNormalized = resolveProviderKey(fallback); + if (isKnownProviderId(fallbackNormalized)) { + return fallbackNormalized; + } + return DEFAULT_PROVIDER; + } + + function normalizeProviderOrder(value = [], fallbackOrder = DEFAULT_PROVIDER_ORDER) { + const source = Array.isArray(value) + ? value + : String(value || '') + .split(/[\r\n,,;;]+/) + .map((entry) => String(entry || '').trim()) + .filter(Boolean); + const normalized = []; + const seen = new Set(); + + const pushProvider = (entry) => { + const provider = normalizeProviderId(entry, DEFAULT_PROVIDER); + if (!provider || seen.has(provider)) { + return; + } + seen.add(provider); + normalized.push(provider); + }; + + source.forEach((entry) => { + pushProvider(entry && typeof entry === 'object' && !Array.isArray(entry) + ? (entry.provider || entry.id || entry.value || '') + : entry); + }); + + if (normalized.length) { + return normalized.slice(0, DEFAULT_PROVIDER_ORDER.length); + } + + const fallback = Array.isArray(fallbackOrder) ? fallbackOrder : []; + fallback.forEach((entry) => { + pushProvider(entry && typeof entry === 'object' && !Array.isArray(entry) + ? (entry.provider || entry.id || entry.value || '') + : entry); + }); + + return normalized.slice(0, DEFAULT_PROVIDER_ORDER.length); + } + + function getProviderIds() { + return DEFAULT_PROVIDER_ORDER.slice(); + } + + function getProviderDefinition(providerId = DEFAULT_PROVIDER) { + return PROVIDER_DEFINITIONS[normalizeProviderId(providerId)] || PROVIDER_DEFINITIONS[DEFAULT_PROVIDER]; } function getProviderModule(providerId = DEFAULT_PROVIDER) { - const normalized = normalizeProviderId(providerId); - if (normalized === PROVIDER_FIVE_SIM) { - return root.PhoneSmsFiveSimProvider || null; + const definition = getProviderDefinition(providerId); + const moduleKey = definition?.moduleKey; + if (!moduleKey) { + return null; } - return root.PhoneSmsHeroSmsProvider || null; + return root?.[moduleKey] || null; } function createProvider(providerId = DEFAULT_PROVIDER, deps = {}) { + const definition = getProviderDefinition(providerId); const module = getProviderModule(providerId); if (!module || typeof module.createProvider !== 'function') { - throw new Error(`Phone SMS provider is not loaded: ${normalizeProviderId(providerId)}`); + throw new Error(`Phone SMS provider module is not loaded: ${definition.id}`); } return module.createProvider(deps); } function getProviderLabel(providerId = DEFAULT_PROVIDER) { - return normalizeProviderId(providerId) === PROVIDER_FIVE_SIM ? '5sim' : 'HeroSMS'; + return getProviderDefinition(providerId)?.label || 'HeroSMS'; } return { PROVIDER_HERO_SMS, PROVIDER_FIVE_SIM, + PROVIDER_NEXSMS, DEFAULT_PROVIDER, + DEFAULT_PROVIDER_ORDER, + PROVIDER_DEFINITIONS, normalizeProviderId, + normalizeProviderOrder, + getProviderIds, + getProviderDefinition, getProviderModule, createProvider, getProviderLabel, diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 9061f0b..90bc22a 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -3333,9 +3333,13 @@ function collectSettingsPayload() { const defaultPhoneCodePollMaxRounds = typeof DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS !== 'undefined' ? DEFAULT_PHONE_CODE_POLL_MAX_ROUNDS : 12; - const heroSmsReuseEnabledValue = typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled + const phoneSmsReuseEnabledValue = typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled ? normalizeHeroSmsReuseEnabledValue(inputHeroSmsReuseEnabled.checked) - : defaultHeroSmsReuseEnabled; + : normalizeHeroSmsReuseEnabledValue( + latestState?.phoneSmsReuseEnabled, + latestState?.heroSmsReuseEnabled + ); + const heroSmsReuseEnabledValue = phoneSmsReuseEnabledValue; const freePhoneReuseEnabledValue = typeof inputFreePhoneReuseEnabled !== 'undefined' && inputFreePhoneReuseEnabled ? Boolean(inputFreePhoneReuseEnabled.checked) : Boolean(latestState?.freePhoneReuseEnabled); @@ -3782,6 +3786,7 @@ function collectSettingsPayload() { nexSmsApiKey: nexSmsApiKeyValue, nexSmsCountryOrder: nexSmsCountryOrderValue, nexSmsServiceCode: nexSmsServiceCodeValue, + phoneSmsReuseEnabled: phoneSmsReuseEnabledValue, heroSmsReuseEnabled: heroSmsReuseEnabledValue, freePhoneReuseEnabled: freePhoneReuseEnabledValue, freePhoneReuseAutoEnabled: freePhoneReuseAutoEnabledValue, @@ -3851,15 +3856,22 @@ function normalizeAccountRunHistoryHelperBaseUrlValue(value = '') { function normalizePhoneSmsProvider(value = '') { - if (typeof window !== 'undefined' && window.PhoneSmsProviderRegistry?.normalizeProviderId) { - return window.PhoneSmsProviderRegistry.normalizeProviderId(value); + const rootScope = typeof window !== 'undefined' ? window : globalThis; + if (rootScope.PhoneSmsProviderRegistry?.normalizeProviderId) { + return rootScope.PhoneSmsProviderRegistry.normalizeProviderId(value); } + const nexSmsProvider = typeof PHONE_SMS_PROVIDER_NEXSMS !== 'undefined' + ? PHONE_SMS_PROVIDER_NEXSMS + : 'nexsms'; const normalized = String(value || '').trim().toLowerCase(); - return normalized === PHONE_SMS_PROVIDER_FIVE_SIM - ? PHONE_SMS_PROVIDER_FIVE_SIM - : PHONE_SMS_PROVIDER_HERO_SMS; + if (normalized === PHONE_SMS_PROVIDER_FIVE_SIM) { + return PHONE_SMS_PROVIDER_FIVE_SIM; + } + if (normalized === nexSmsProvider) { + return nexSmsProvider; + } + return PHONE_SMS_PROVIDER_HERO_SMS; } - function setPhoneSmsProviderSelectValue(provider) { const normalizedProvider = normalizePhoneSmsProvider(provider); if (selectPhoneSmsProvider) { @@ -4042,21 +4054,21 @@ function normalizeHeroSmsMaxPriceValue(value = '') { } function normalizePhoneSmsProviderValue(value = '') { - const normalized = String(value || '').trim().toLowerCase(); - if (normalized === PHONE_SMS_PROVIDER_FIVE_SIM) { - return PHONE_SMS_PROVIDER_FIVE_SIM; + const rootScope = typeof window !== 'undefined' ? window : globalThis; + if (rootScope.PhoneSmsProviderRegistry?.normalizeProviderId) { + return rootScope.PhoneSmsProviderRegistry.normalizeProviderId(value); } - if (normalized === PHONE_SMS_PROVIDER_NEXSMS) { - return PHONE_SMS_PROVIDER_NEXSMS; - } - return PHONE_SMS_PROVIDER_HERO; + return normalizePhoneSmsProvider(value); } - function normalizePhoneSmsProviderOrderValue(value = [], fallbackOrder = []) { + const rootScope = typeof window !== 'undefined' ? window : globalThis; + if (rootScope.PhoneSmsProviderRegistry?.normalizeProviderOrder) { + return rootScope.PhoneSmsProviderRegistry.normalizeProviderOrder(value, fallbackOrder); + } const source = Array.isArray(value) ? value : String(value || '') - .split(/[\r\n,,;;|/]+/) + .split(/[\r\n,]+/) .map((entry) => String(entry || '').trim()) .filter(Boolean); const normalized = []; @@ -4089,7 +4101,6 @@ function normalizePhoneSmsProviderOrderValue(value = [], fallbackOrder = []) { }); return fallbackNormalized.slice(0, 3); } - function formatPhoneSmsProviderOrderSummary(order = []) { const normalized = normalizePhoneSmsProviderOrderValue(order, []); if (!normalized.length) { @@ -4593,8 +4604,11 @@ function normalizePhoneCodePollMaxRoundsValue(value, fallback = DEFAULT_PHONE_CO return Math.max(PHONE_CODE_POLL_MAX_ROUNDS_MIN, Math.min(PHONE_CODE_POLL_MAX_ROUNDS_MAX, parsed)); } -function normalizeHeroSmsReuseEnabledValue(value) { +function normalizeHeroSmsReuseEnabledValue(value, fallbackValue = undefined) { if (value === undefined || value === null) { + if (fallbackValue !== undefined && fallbackValue !== null) { + return Boolean(fallbackValue); + } return DEFAULT_HERO_SMS_REUSE_ENABLED; } return Boolean(value); @@ -8983,7 +8997,10 @@ function applySettingsState(state) { : String(state?.nexSmsServiceCode || defaultNexSmsServiceCode).trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '') || defaultNexSmsServiceCode; } if (typeof inputHeroSmsReuseEnabled !== 'undefined' && inputHeroSmsReuseEnabled) { - inputHeroSmsReuseEnabled.checked = normalizeHeroSmsReuseEnabledValue(state?.heroSmsReuseEnabled); + inputHeroSmsReuseEnabled.checked = normalizeHeroSmsReuseEnabledValue( + state?.phoneSmsReuseEnabled, + state?.heroSmsReuseEnabled + ); } if (typeof inputFreePhoneReuseEnabled !== 'undefined' && inputFreePhoneReuseEnabled) { inputFreePhoneReuseEnabled.checked = Boolean(state?.freePhoneReuseEnabled); @@ -14582,8 +14599,15 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message.payload.nexSmsServiceCode !== undefined && inputNexSmsServiceCode) { inputNexSmsServiceCode.value = normalizeNexSmsServiceCodeValue(message.payload.nexSmsServiceCode); } - if (message.payload.heroSmsReuseEnabled !== undefined && inputHeroSmsReuseEnabled) { - inputHeroSmsReuseEnabled.checked = normalizeHeroSmsReuseEnabledValue(message.payload.heroSmsReuseEnabled); + if ( + (message.payload.phoneSmsReuseEnabled !== undefined + || message.payload.heroSmsReuseEnabled !== undefined) + && inputHeroSmsReuseEnabled + ) { + inputHeroSmsReuseEnabled.checked = normalizeHeroSmsReuseEnabledValue( + message.payload.phoneSmsReuseEnabled, + message.payload.heroSmsReuseEnabled + ); } if (message.payload.freePhoneReuseEnabled !== undefined && inputFreePhoneReuseEnabled) { inputFreePhoneReuseEnabled.checked = Boolean(message.payload.freePhoneReuseEnabled); diff --git a/tests/background-account-history-settings.test.js b/tests/background-account-history-settings.test.js index fb76bc8..8cf7c08 100644 --- a/tests/background-account-history-settings.test.js +++ b/tests/background-account-history-settings.test.js @@ -81,6 +81,7 @@ test('background account history settings are normalized independently from hotm extractFunction('normalizeSub2ApiGroupNames'), extractFunction('normalizeBoundedIntegerSetting'), extractFunction('normalizeLocalHttpBaseUrl'), + extractFunction('buildPersistentSettingsPayload'), extractFunction('normalizePersistentSettingValue'), ].join('\n'); @@ -181,10 +182,16 @@ function normalizeCloudflareTempEmailReceiveMailbox(value) { return String(value function normalizeCloudflareTempEmailDomain(value) { return String(value || '').trim(); } function normalizeCloudflareTempEmailDomains(value) { return Array.isArray(value) ? value : []; } function normalizeHotmailAccounts(value) { return Array.isArray(value) ? value : []; } +function resolveLegacyAutoStepDelaySeconds(value) { + return value && Object.prototype.hasOwnProperty.call(value, 'autoStepDelaySeconds') + ? value.autoStepDelaySeconds + : undefined; +} ${bundle} return { normalizeAccountRunHistoryHelperBaseUrl, normalizePersistentSettingValue, + buildPersistentSettingsPayload, }; `)(); @@ -243,6 +250,8 @@ return { assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'NEXSMS'), 'nexsms'); assert.equal(api.normalizePersistentSettingValue('phoneSmsProvider', 'unknown'), 'hero-sms'); assert.deepStrictEqual(api.normalizePersistentSettingValue('phoneSmsProviderOrder', ['nexsms', '5sim', 'nexsms']), ['nexsms', '5sim']); + assert.equal(api.normalizePersistentSettingValue('phoneSmsReuseEnabled', false), false); + assert.equal(api.normalizePersistentSettingValue('phoneSmsReuseEnabled', true), true); assert.equal(api.normalizePersistentSettingValue('fiveSimApiKey', ' demo-five '), ' demo-five '); assert.equal(api.normalizePersistentSettingValue('fiveSimProduct', ' OpenAI! '), 'openai'); assert.equal(api.normalizePersistentSettingValue('fiveSimCountryId', ' England! '), 'england'); @@ -335,4 +344,26 @@ return { countryLabel: '', } ); + const legacyReusePayload = api.buildPersistentSettingsPayload({ + heroSmsReuseEnabled: false, + }); + assert.equal(legacyReusePayload.phoneSmsReuseEnabled, false); + assert.equal(legacyReusePayload.heroSmsReuseEnabled, false); + const fiveSimReusePayload = api.buildPersistentSettingsPayload({ + fiveSimReuseEnabled: false, + }); + assert.equal(fiveSimReusePayload.phoneSmsReuseEnabled, false); + assert.equal(fiveSimReusePayload.heroSmsReuseEnabled, false); + const conflictingReusePayload = api.buildPersistentSettingsPayload({ + heroSmsReuseEnabled: true, + fiveSimReuseEnabled: false, + }); + assert.equal(conflictingReusePayload.phoneSmsReuseEnabled, true); + assert.equal(conflictingReusePayload.heroSmsReuseEnabled, true); + const newReusePayload = api.buildPersistentSettingsPayload({ + phoneSmsReuseEnabled: false, + heroSmsReuseEnabled: true, + }); + assert.equal(newReusePayload.phoneSmsReuseEnabled, false); + assert.equal(newReusePayload.heroSmsReuseEnabled, false); }); diff --git a/tests/background-message-router-module.test.js b/tests/background-message-router-module.test.js index 46f2e0c..23fcdb8 100644 --- a/tests/background-message-router-module.test.js +++ b/tests/background-message-router-module.test.js @@ -15,6 +15,7 @@ test('background defaults enable free phone reuse switches', () => { assert.match(defaultsBlock, /freePhoneReuseEnabled:\s*true/); assert.match(defaultsBlock, /freePhoneReuseAutoEnabled:\s*true/); + assert.match(defaultsBlock, /phoneSmsReuseEnabled:\s*DEFAULT_HERO_SMS_REUSE_ENABLED/); }); test('background free reusable phone setter does not depend on module-scoped phone flow constants', () => { diff --git a/tests/phone-sms-provider-registry.test.js b/tests/phone-sms-provider-registry.test.js new file mode 100644 index 0000000..fdfce9e --- /dev/null +++ b/tests/phone-sms-provider-registry.test.js @@ -0,0 +1,44 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('phone-sms/providers/registry.js', 'utf8'); + +function loadRegistry(root = {}) { + return new Function('self', `${source}; return self.PhoneSmsProviderRegistry;`)(root); +} + +test('phone sms provider registry normalizes ids, order and labels consistently', () => { + const registry = loadRegistry({ + PhoneSmsHeroSmsProvider: { + createProvider: (deps = {}) => ({ provider: 'hero-sms', deps }), + }, + PhoneSmsFiveSimProvider: { + createProvider: (deps = {}) => ({ provider: '5sim', deps }), + }, + }); + + assert.deepStrictEqual(registry.getProviderIds(), ['hero-sms', '5sim', 'nexsms']); + assert.equal(registry.normalizeProviderId(' NEXSMS '), 'nexsms'); + assert.equal(registry.normalizeProviderId('unknown-provider'), 'hero-sms'); + assert.equal(registry.getProviderLabel('nexsms'), 'NexSMS'); + assert.equal(registry.getProviderDefinition('nexsms').moduleKey, 'PhoneSmsNexSmsProvider'); + assert.deepStrictEqual( + registry.normalizeProviderOrder([ + { provider: 'nexsms' }, + { id: '5sim' }, + { value: 'hero-sms' }, + 'NEXSMS', + ]), + ['nexsms', '5sim', 'hero-sms'] + ); + assert.deepStrictEqual( + registry.normalizeProviderOrder([], ['nexsms', '5sim', 'nexsms']), + ['nexsms', '5sim'] + ); + assert.deepStrictEqual( + registry.createProvider('5sim', { foo: 1 }), + { provider: '5sim', deps: { foo: 1 } } + ); + assert.throws(() => registry.createProvider('nexsms'), /Phone SMS provider module is not loaded: nexsms/); +}); diff --git a/tests/phone-verification-flow.test.js b/tests/phone-verification-flow.test.js index 2e3a7f5..9ce1ada 100644 --- a/tests/phone-verification-flow.test.js +++ b/tests/phone-verification-flow.test.js @@ -1948,9 +1948,181 @@ 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 rejects 5sim maxPrice with custom operator before buying', async () => { - const requests = []; - const helpers = api.createPhoneVerificationHelpers({ +test('phone verification helper prefers phoneSmsReuseEnabled over legacy heroSmsReuseEnabled for 5sim acquisition', async () => { + const requests = []; + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url, options = {}) => { + const parsedUrl = new URL(url); + requests.push({ + pathname: parsedUrl.pathname, + search: parsedUrl.searchParams, + headers: options?.headers || {}, + }); + if (parsedUrl.pathname === '/v1/guest/prices') { + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ + openai: { + thailand: { + any: { + cost: 0.08, + count: 12, + }, + }, + }, + }), + }; + } + if (parsedUrl.pathname === '/v1/user/buy/activation/thailand/any/openai') { + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ + id: 1234567, + phone: '+66880000000', + country: 'thailand', + country_name: 'Thailand', + product: 'openai', + }), + }; + } + throw new Error(`Unexpected 5sim request: ${parsedUrl.pathname}`); + }, + getState: async () => ({ + phoneSmsProvider: '5sim', + fiveSimApiKey: 'five-token', + fiveSimCountryOrder: ['thailand'], + fiveSimOperator: 'any', + fiveSimProduct: 'openai', + phoneSmsReuseEnabled: false, + heroSmsReuseEnabled: true, + heroSmsActivationRetryRounds: 1, + }), + sendToContentScriptResilient: async () => ({}), + setState: async () => {}, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + const activation = await helpers.requestPhoneActivation({ + phoneSmsProvider: '5sim', + fiveSimApiKey: 'five-token', + fiveSimCountryOrder: ['thailand'], + fiveSimOperator: 'any', + fiveSimProduct: 'openai', + phoneSmsReuseEnabled: false, + heroSmsReuseEnabled: true, + heroSmsActivationRetryRounds: 1, + }); + + assert.deepStrictEqual(activation, { + activationId: '1234567', + phoneNumber: '+66880000000', + provider: '5sim', + serviceCode: 'openai', + countryId: 'thailand', + countryCode: 'thailand', + countryLabel: 'Thailand', + successfulUses: 0, + maxUses: 3, + }); + assert.equal(requests[0].pathname, '/v1/guest/prices'); + assert.equal(requests[1].pathname, '/v1/user/buy/activation/thailand/any/openai'); + assert.equal(requests[1].search.get('reuse'), null); +}); + +test('phone verification helper treats fiveSimReuseEnabled as legacy-only when phoneSmsReuseEnabled is absent', async () => { + const requests = []; + const helpers = api.createPhoneVerificationHelpers({ + addLog: async () => {}, + ensureStep8SignupPageReady: async () => {}, + fetchImpl: async (url, options = {}) => { + const parsedUrl = new URL(url); + requests.push({ + pathname: parsedUrl.pathname, + search: parsedUrl.searchParams, + headers: options?.headers || {}, + }); + if (parsedUrl.pathname === '/v1/guest/prices') { + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ + openai: { + thailand: { + any: { + cost: 0.08, + count: 12, + }, + }, + }, + }), + }; + } + if (parsedUrl.pathname === '/v1/user/buy/activation/thailand/any/openai') { + return { + ok: true, + status: 200, + text: async () => JSON.stringify({ + id: 1234568, + phone: '+66880000001', + country: 'thailand', + country_name: 'Thailand', + product: 'openai', + }), + }; + } + throw new Error(`Unexpected 5sim request: ${parsedUrl.pathname}`); + }, + getState: async () => ({ + phoneSmsProvider: '5sim', + fiveSimApiKey: 'five-token', + fiveSimCountryOrder: ['thailand'], + fiveSimOperator: 'any', + fiveSimProduct: 'openai', + heroSmsReuseEnabled: true, + fiveSimReuseEnabled: false, + heroSmsActivationRetryRounds: 1, + }), + sendToContentScriptResilient: async () => ({}), + setState: async () => {}, + sleepWithStop: async () => {}, + throwIfStopped: () => {}, + }); + + const activation = await helpers.requestPhoneActivation({ + phoneSmsProvider: '5sim', + fiveSimApiKey: 'five-token', + fiveSimCountryOrder: ['thailand'], + fiveSimOperator: 'any', + fiveSimProduct: 'openai', + heroSmsReuseEnabled: true, + fiveSimReuseEnabled: false, + heroSmsActivationRetryRounds: 1, + }); + + assert.deepStrictEqual(activation, { + activationId: '1234568', + phoneNumber: '+66880000001', + provider: '5sim', + serviceCode: 'openai', + countryId: 'thailand', + countryCode: 'thailand', + countryLabel: 'Thailand', + successfulUses: 0, + maxUses: 3, + }); + assert.equal(requests[0].pathname, '/v1/guest/prices'); + assert.equal(requests[1].pathname, '/v1/user/buy/activation/thailand/any/openai'); + assert.equal(requests[1].search.get('reuse'), '1'); +}); + +test('phone verification helper rejects 5sim maxPrice with custom operator before buying', async () => { + const requests = []; + const helpers = api.createPhoneVerificationHelpers({ addLog: async () => {}, ensureStep8SignupPageReady: async () => {}, fetchImpl: async (url) => { diff --git a/tests/sidepanel-phone-verification-settings.test.js b/tests/sidepanel-phone-verification-settings.test.js index 774983e..6396b1d 100644 --- a/tests/sidepanel-phone-verification-settings.test.js +++ b/tests/sidepanel-phone-verification-settings.test.js @@ -943,6 +943,7 @@ return { collectSettingsPayload }; assert.equal(payload.nexSmsApiKey, 'nex-key'); assert.deepStrictEqual(payload.nexSmsCountryOrder, [1]); assert.equal(payload.nexSmsServiceCode, 'ot'); + assert.equal(payload.phoneSmsReuseEnabled, true); assert.equal(payload.heroSmsReuseEnabled, true); assert.equal(payload.freePhoneReuseEnabled, true); assert.equal(payload.freePhoneReuseAutoEnabled, true);