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({