diff --git a/background.js b/background.js index c5a6ccd..f802da9 100644 --- a/background.js +++ b/background.js @@ -8924,6 +8924,34 @@ async function handleStepData(step, payload) { return messageRouter.handleStepData(step, payload); } + function shouldPreservePhoneIdentityForStepEmailPayload(state = {}, stepPayload = {}) { + if (String(stepPayload.accountIdentifierType || '').trim().toLowerCase() === 'email') { + return false; + } + return Boolean( + String(state.signupPhoneNumber || '').trim() + || (String(state.accountIdentifierType || '').trim().toLowerCase() === 'phone' + && String(state.accountIdentifier || '').trim()) + || state.signupPhoneActivation + || state.signupPhoneCompletedActivation + ); + } + + async function persistStepEmailPayload(email, stepPayload = {}, source = 'step_identity') { + if (!email) { + return; + } + const currentState = await getState(); + if (shouldPreservePhoneIdentityForStepEmailPayload(currentState, stepPayload)) { + await persistRegistrationEmailState(currentState, email, { + source, + preserveAccountIdentity: true, + }); + return; + } + await setEmailState(email); + } + switch (step) { case 1: { const updates = {}; @@ -8949,8 +8977,8 @@ async function handleStepData(step, payload) { break; } case 2: - if (payload.email) await setEmailState(payload.email); - if (payload.accountIdentifierType || payload.accountIdentifier || payload.signupPhoneNumber || payload.signupPhoneActivation) { + await persistStepEmailPayload(payload.email, payload, 'step2_identity'); + if (!payload.email && (payload.accountIdentifierType || payload.accountIdentifier || payload.signupPhoneNumber || payload.signupPhoneActivation)) { await setState({ accountIdentifierType: payload.accountIdentifierType || null, accountIdentifier: String(payload.accountIdentifier || '').trim(), @@ -8969,7 +8997,7 @@ async function handleStepData(step, payload) { } break; case 3: - if (payload.email) await setEmailState(payload.email); + await persistStepEmailPayload(payload.email, payload, 'step3_identity'); if (payload.signupVerificationRequestedAt) { await setState({ signupVerificationRequestedAt: payload.signupVerificationRequestedAt }); } @@ -11724,6 +11752,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter setContributionMode, setEmailState, setEmailStateSilently, + persistRegistrationEmailState, setFreeReusablePhoneActivation, setSignupPhoneState, setSignupPhoneStateSilently, diff --git a/background/message-router.js b/background/message-router.js index f3127a0..1285de4 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -104,6 +104,7 @@ setContributionMode, setEmailState, setEmailStateSilently, + persistRegistrationEmailState, setFreeReusablePhoneActivation, setSignupPhoneState, setSignupPhoneStateSilently, @@ -210,6 +211,42 @@ : ''; } + function hasPhoneSignupIdentity(state = {}) { + const identifierType = String(state?.accountIdentifierType || '').trim().toLowerCase(); + return Boolean( + String(state?.signupPhoneNumber || '').trim() + || (identifierType === 'phone' && String(state?.accountIdentifier || '').trim()) + || state?.signupPhoneActivation + || state?.signupPhoneCompletedActivation + ); + } + + function shouldPreservePhoneIdentityForEmailPayload(payload = {}, state = {}) { + const identifierType = String(payload?.accountIdentifierType || '').trim().toLowerCase(); + if (identifierType === 'email') { + return false; + } + return hasPhoneSignupIdentity(state); + } + + async function persistEmailIdentityFromStepPayload(email, payload = {}, source = 'step_payload') { + if (!email) { + return; + } + const state = await getState(); + const preserveAccountIdentity = shouldPreservePhoneIdentityForEmailPayload(payload, state); + if (preserveAccountIdentity && typeof persistRegistrationEmailState === 'function') { + await persistRegistrationEmailState(state, email, { + source, + preserveAccountIdentity: true, + }); + return; + } + await setEmailState(email, preserveAccountIdentity + ? { source, preserveAccountIdentity: true } + : { source }); + } + function normalizeAutomationWindowId(value) { if (value === null || value === undefined || value === '') { return null; @@ -262,7 +299,10 @@ const email = resolveEmailIdentityPayload(payload); if (identifierType === 'email' || email) { if (email) { - await setEmailState(email); + await persistEmailIdentityFromStepPayload(email, payload, 'step_identity'); + } + if (email) { + return; } const updates = { phoneNumber: '', diff --git a/tests/auto-run-step6-restart.test.js b/tests/auto-run-step6-restart.test.js index 70ea3f8..a729bc8 100644 --- a/tests/auto-run-step6-restart.test.js +++ b/tests/auto-run-step6-restart.test.js @@ -83,6 +83,65 @@ const defaultStepDefinitions = { 10: { key: 'platform-verify' }, }; +const PHONE_IDENTITY_STATE_KEYS = [ + 'phoneNumber', + 'signupPhoneNumber', + 'signupPhoneActivation', + 'signupPhoneCompletedActivation', + 'signupPhoneVerificationRequestedAt', + 'signupPhoneVerificationPurpose', + 'accountIdentifierType', + 'accountIdentifier', +]; + +function createDownstreamResetHarness(stepKey = '') { + return new Function(` +function getStepExecutionKeyForState() { + return ${JSON.stringify(stepKey)}; +} +${extractFunction('getDownstreamStateResets')} +return { getDownstreamStateResets }; +`)(); +} + +test('downstream restarts after account creation preserve phone signup identity fields', () => { + const numericResetHarness = createDownstreamResetHarness(''); + const stepKeyResetHarnesses = [ + createDownstreamResetHarness('oauth-login'), + createDownstreamResetHarness('fetch-login-code'), + createDownstreamResetHarness('confirm-oauth'), + ]; + const phoneState = { + accountIdentifierType: 'phone', + accountIdentifier: '+447780579093', + signupPhoneNumber: '+447780579093', + signupPhoneActivation: { activationId: 'active', phoneNumber: '+447780579093' }, + signupPhoneCompletedActivation: { activationId: 'done', phoneNumber: '+447780579093' }, + }; + + for (const step of [5, 6, 7, 8, 9]) { + const resets = numericResetHarness.getDownstreamStateResets(step, phoneState); + for (const key of PHONE_IDENTITY_STATE_KEYS) { + assert.equal( + Object.prototype.hasOwnProperty.call(resets, key), + false, + `step ${step} reset must not clear ${key}` + ); + } + } + + for (const harness of stepKeyResetHarnesses) { + const resets = harness.getDownstreamStateResets(10, phoneState); + for (const key of PHONE_IDENTITY_STATE_KEYS) { + assert.equal( + Object.prototype.hasOwnProperty.call(resets, key), + false, + `${key} must not be cleared by step-key reset` + ); + } + } +}); + function createHarness(options = {}) { const { startStep = 7, diff --git a/tests/background-message-router-step2-skip.test.js b/tests/background-message-router-step2-skip.test.js index acf3638..eb12289 100644 --- a/tests/background-message-router-step2-skip.test.js +++ b/tests/background-message-router-step2-skip.test.js @@ -14,6 +14,7 @@ function createRouter(overrides = {}) { broadcasts: [], balanceRefreshes: [], emailStates: [], + persistedRegistrationEmails: [], signupPhoneStates: [], signupPhoneSilentStates: [], finalizePayloads: [], @@ -123,6 +124,9 @@ function createRouter(overrides = {}) { events.emailStates.push(email); }, setEmailStateSilently: async () => {}, + persistRegistrationEmailState: async (state, email, options) => { + events.persistedRegistrationEmails.push({ state, email, options }); + }, setSignupPhoneState: async (phoneNumber) => { events.signupPhoneStates.push(phoneNumber); }, @@ -206,14 +210,38 @@ test('message router clears stale signup phone runtime when step 2 resolves emai }); assert.deepStrictEqual(events.emailStates, ['user@example.com']); - assert.deepStrictEqual(events.signupPhoneSilentStates, [null]); - assert.ok(events.stateUpdates.some((updates) => ( - updates.accountIdentifierType === 'email' - && updates.accountIdentifier === 'user@example.com' - && updates.signupPhoneNumber === '' - && updates.signupPhoneActivation === null - && updates.signupPhoneCompletedActivation === null - ))); + assert.deepStrictEqual(events.signupPhoneSilentStates, []); + assert.deepStrictEqual(events.stateUpdates, []); +}); + +test('message router preserves phone signup identity when step payload only reports registration email', async () => { + const { router, events } = createRouter({ + state: { + stepStatuses: { 3: 'pending' }, + accountIdentifierType: 'phone', + accountIdentifier: '+66959916439', + signupPhoneNumber: '+66959916439', + signupPhoneActivation: { activationId: 'active', phoneNumber: '+66959916439' }, + }, + }); + + await router.handleStepData(3, { + email: 'bound@example.com', + signupVerificationRequestedAt: 123456, + }); + + assert.deepStrictEqual(events.emailStates, []); + assert.equal(events.persistedRegistrationEmails.length, 1); + assert.equal(events.persistedRegistrationEmails[0].email, 'bound@example.com'); + assert.deepStrictEqual(events.persistedRegistrationEmails[0].options, { + source: 'step_identity', + preserveAccountIdentity: true, + }); + assert.equal(events.persistedRegistrationEmails[0].state.signupPhoneNumber, '+66959916439'); + assert.equal(events.persistedRegistrationEmails[0].state.accountIdentifierType, 'phone'); + assert.equal(events.signupPhoneSilentStates.length, 0); + assert.ok(!events.stateUpdates.some((updates) => updates.signupPhoneNumber === '')); + assert.ok(events.stateUpdates.some((updates) => updates.signupVerificationRequestedAt === 123456)); }); test('message router does not overwrite a completed step 3 when step 2 is replayed', async () => {