diff --git a/background/steps/oauth-login.js b/background/steps/oauth-login.js index 07eb041..7c8e0bd 100644 --- a/background/steps/oauth-login.js +++ b/background/steps/oauth-login.js @@ -40,29 +40,64 @@ return /缺少|未配置|请输入|无效|错误|失败|401|认证失败|未授权|unauthorized|invalid/i.test(message); } + function normalizeStep7IdentifierType(value = '') { + const normalized = String(value || '').trim().toLowerCase(); + return normalized === 'phone' || normalized === 'email' ? normalized : ''; + } + + function normalizeStep7SignupMethod(value = '') { + return String(value || '').trim().toLowerCase() === 'phone' ? 'phone' : 'email'; + } + + function canUseConfiguredPhoneSignup(state = {}) { + return normalizeStep7SignupMethod(state?.signupMethod) === 'phone' + && Boolean(state?.phoneVerificationEnabled) + && !Boolean(state?.plusModeEnabled) + && !Boolean(state?.contributionMode); + } + + function resolveStep7LoginIdentifierType(state = {}, fallbackType = '') { + const explicitIdentifierType = normalizeStep7IdentifierType(state?.accountIdentifierType); + if (explicitIdentifierType) { + return explicitIdentifierType; + } + + const frozenSignupMethod = normalizeStep7IdentifierType(state?.resolvedSignupMethod); + if (frozenSignupMethod) { + return frozenSignupMethod; + } + + if (canUseConfiguredPhoneSignup(state)) { + return 'phone'; + } + + return normalizeStep7IdentifierType(fallbackType) || 'email'; + } + async function executeStep7(state) { const visibleStep = Math.floor(Number(state?.visibleStep) || 0); const completionStep = visibleStep > 0 ? visibleStep : 7; - const resolvedIdentifierType = String( - state?.accountIdentifierType - || (state?.signupPhoneNumber ? 'phone' : '') - || '' - ).trim().toLowerCase() === 'phone' - ? 'phone' - : 'email'; - const phoneNumber = String( - state?.signupPhoneNumber - || (resolvedIdentifierType === 'phone' ? state?.accountIdentifier : '') - || state?.signupPhoneCompletedActivation?.phoneNumber - || state?.signupPhoneActivation?.phoneNumber - || '' - ).trim(); - const email = String( - state?.email - || (resolvedIdentifierType === 'email' ? state?.accountIdentifier : '') - || '' - ).trim(); - if (!email && !phoneNumber) { + const resolvedIdentifierType = resolveStep7LoginIdentifierType(state); + const phoneNumber = resolvedIdentifierType === 'phone' + ? String( + state?.signupPhoneNumber + || (normalizeStep7IdentifierType(state?.accountIdentifierType) === 'phone' ? state?.accountIdentifier : '') + || state?.signupPhoneCompletedActivation?.phoneNumber + || state?.signupPhoneActivation?.phoneNumber + || '' + ).trim() + : ''; + const email = resolvedIdentifierType === 'email' + ? String( + state?.email + || (normalizeStep7IdentifierType(state?.accountIdentifierType) === 'email' ? state?.accountIdentifier : '') + || '' + ).trim() + : ''; + if ( + (resolvedIdentifierType === 'phone' && !phoneNumber) + || (resolvedIdentifierType !== 'phone' && !email) + ) { throw new Error('缺少登录账号:请先完成步骤 2,或在侧栏“注册邮箱/注册手机号”中手动填写账号后再执行当前步骤。'); } @@ -75,25 +110,23 @@ try { const currentState = attempt === 1 ? state : await getState(); const password = currentState.password || currentState.customPassword || ''; - const currentIdentifierType = String( - currentState?.accountIdentifierType - || (currentState?.signupPhoneNumber ? 'phone' : '') - || resolvedIdentifierType - ).trim().toLowerCase() === 'phone' - ? 'phone' - : 'email'; - const currentPhoneNumber = String( - currentState?.signupPhoneNumber - || (currentIdentifierType === 'phone' ? currentState?.accountIdentifier : '') - || currentState?.signupPhoneCompletedActivation?.phoneNumber - || currentState?.signupPhoneActivation?.phoneNumber - || phoneNumber - ).trim(); - const currentEmail = String( - currentState?.email - || (currentIdentifierType === 'email' ? currentState?.accountIdentifier : '') - || email - ).trim(); + const currentIdentifierType = resolveStep7LoginIdentifierType(currentState, resolvedIdentifierType); + const currentPhoneNumber = currentIdentifierType === 'phone' + ? String( + currentState?.signupPhoneNumber + || (normalizeStep7IdentifierType(currentState?.accountIdentifierType) === 'phone' ? currentState?.accountIdentifier : '') + || currentState?.signupPhoneCompletedActivation?.phoneNumber + || currentState?.signupPhoneActivation?.phoneNumber + || phoneNumber + ).trim() + : ''; + const currentEmail = currentIdentifierType === 'email' + ? String( + currentState?.email + || (normalizeStep7IdentifierType(currentState?.accountIdentifierType) === 'email' ? currentState?.accountIdentifier : '') + || email + ).trim() + : ''; const accountIdentifier = currentIdentifierType === 'phone' ? currentPhoneNumber : currentEmail; diff --git a/content/signup-page.js b/content/signup-page.js index 9f5e152..d78aea6 100644 --- a/content/signup-page.js +++ b/content/signup-page.js @@ -5202,9 +5202,9 @@ async function step6OpenLoginEntry(payload, snapshot) { const visibleStep = Math.floor(Number(payload?.visibleStep) || 0) || 7; const currentSnapshot = normalizeStep6Snapshot(snapshot || inspectLoginAuthState()); const preferPhoneLogin = String(payload?.loginIdentifierType || '').trim() === 'phone' || (!payload?.email && payload?.phoneNumber); - const trigger = preferPhoneLogin - ? (currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger()) - : (currentSnapshot.loginEntryTrigger || findLoginEntryTrigger()); + const genericEntryTrigger = currentSnapshot.loginEntryTrigger || findLoginEntryTrigger(); + const phoneEntryTrigger = currentSnapshot.phoneEntryTrigger || findLoginPhoneEntryTrigger(); + const trigger = genericEntryTrigger || (preferPhoneLogin ? phoneEntryTrigger : null); if (!trigger || !isActionEnabled(trigger)) { return createStep6RecoverableResult('missing_login_entry_trigger', currentSnapshot, { message: preferPhoneLogin diff --git a/tests/background-step6-retry-limit.test.js b/tests/background-step6-retry-limit.test.js index 09cfaaf..579977b 100644 --- a/tests/background-step6-retry-limit.test.js +++ b/tests/background-step6-retry-limit.test.js @@ -358,6 +358,106 @@ test('step 7 forwards phone login identity payload when account identifier is ph ]); }); +test('step 7 keeps Plus email login even when phone sms runtime exists', async () => { + const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope); + + const events = { + payloads: [], + }; + + const executor = api.createStep7Executor({ + addLog: async () => {}, + completeStepFromBackground: async () => {}, + getErrorMessage: (error) => error?.message || String(error || ''), + getLoginAuthStateLabel: (state) => state || 'unknown', + getState: async () => ({ + plusModeEnabled: true, + phoneVerificationEnabled: true, + signupMethod: 'phone', + email: 'plus.user@example.com', + password: 'secret', + signupPhoneNumber: '+441111111111', + }), + isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable', + isStep6SuccessResult: (result) => result?.step6Outcome === 'success', + refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest', + reuseOrCreateTab: async () => {}, + sendToContentScriptResilient: async (_sourceName, message) => { + events.payloads.push(message.payload); + return { + step6Outcome: 'success', + state: 'verification_page', + loginVerificationRequestedAt: 123456, + }; + }, + STEP6_MAX_ATTEMPTS: 3, + throwIfStopped: () => {}, + }); + + await executor.executeStep7({ + plusModeEnabled: true, + phoneVerificationEnabled: true, + signupMethod: 'phone', + email: 'plus.user@example.com', + password: 'secret', + signupPhoneNumber: '+441111111111', + visibleStep: 10, + }); + + assert.equal(events.payloads[0].loginIdentifierType, 'email'); + assert.equal(events.payloads[0].email, 'plus.user@example.com'); + assert.equal(events.payloads[0].phoneNumber, ''); + assert.equal(events.payloads[0].accountIdentifier, 'plus.user@example.com'); +}); + +test('step 7 can infer phone login from an available phone signup configuration before step 2 finishes', async () => { + const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8'); + const globalScope = {}; + const api = new Function('self', `${source}; return self.MultiPageBackgroundStep7;`)(globalScope); + + const events = { + payloads: [], + }; + + const executor = api.createStep7Executor({ + addLog: async () => {}, + completeStepFromBackground: async () => {}, + getErrorMessage: (error) => error?.message || String(error || ''), + getLoginAuthStateLabel: (state) => state || 'unknown', + getState: async () => ({ + phoneVerificationEnabled: true, + signupMethod: 'phone', + signupPhoneNumber: '+447780579093', + }), + isStep6RecoverableResult: (result) => result?.step6Outcome === 'recoverable', + isStep6SuccessResult: (result) => result?.step6Outcome === 'success', + refreshOAuthUrlBeforeStep6: async () => 'https://oauth.example/latest', + reuseOrCreateTab: async () => {}, + sendToContentScriptResilient: async (_sourceName, message) => { + events.payloads.push(message.payload); + return { + step6Outcome: 'success', + state: 'phone_verification_page', + loginVerificationRequestedAt: 987654, + }; + }, + STEP6_MAX_ATTEMPTS: 3, + throwIfStopped: () => {}, + }); + + await executor.executeStep7({ + phoneVerificationEnabled: true, + signupMethod: 'phone', + signupPhoneNumber: '+447780579093', + }); + + assert.equal(events.payloads[0].loginIdentifierType, 'phone'); + assert.equal(events.payloads[0].phoneNumber, '+447780579093'); + assert.equal(events.payloads[0].email, ''); +}); + test('step 7 can start from a manually filled signup phone without completed step 2 or step 3 state', async () => { const source = fs.readFileSync('background/steps/oauth-login.js', 'utf8'); const globalScope = {}; diff --git a/tests/step7-phone-login-entry.test.js b/tests/step7-phone-login-entry.test.js index 92f45d7..b1e1a0a 100644 --- a/tests/step7-phone-login-entry.test.js +++ b/tests/step7-phone-login-entry.test.js @@ -184,6 +184,50 @@ test('step 7 treats unified OpenAI login page as email input despite phone optio assert.equal(api.getLoginPhoneInput(), null, 'phone option button must not turn email input into phone input'); }); +test('step 7 clicks the generic other-account entry before phone entry', async () => { + const api = new Function(` +const clicks = []; +const genericEntry = { id: 'generic', textContent: '\\u767b\\u5f55\\u81f3\\u53e6\\u4e00\\u4e2a\\u5e10\\u6237' }; +const phoneEntry = { id: 'phone', textContent: '\\u4f7f\\u7528\\u7535\\u8bdd\\u53f7\\u7801\\u7ee7\\u7eed' }; + +function normalizeStep6Snapshot(snapshot) { return snapshot; } +function inspectLoginAuthState() { + return { state: 'entry_page', loginEntryTrigger: genericEntry, phoneEntryTrigger: phoneEntry }; +} +function findLoginEntryTrigger() { return genericEntry; } +function findLoginPhoneEntryTrigger() { return phoneEntry; } +function isActionEnabled() { return true; } +function getActionText(el) { return el.textContent || ''; } +function log() {} +async function humanPause() {} +function simulateClick(el) { clicks.push(el.id); } +async function waitForLoginEntryOpenTransition() { return { state: 'phone_entry_page' }; } +async function switchFromEmailPageToPhoneLogin() { return { routed: 'switch-phone' }; } +async function step6LoginFromEmailPage() { return { routed: 'email' }; } +async function step6LoginFromPasswordPage() { return { routed: 'password' }; } +async function step6LoginFromPhonePage() { return { routed: 'phone' }; } +async function finalizeStep6VerificationReady() { return { routed: 'verification' }; } +function createStep6OAuthConsentSuccessResult() { return { routed: 'oauth' }; } +function createStep6AddEmailSuccessResult() { return { routed: 'add-email' }; } +async function createStep6LoginTimeoutRecoveryTransition() { return { action: 'recoverable', result: { routed: 'recoverable' } }; } +function createStep6RecoverableResult(reason, snapshot, options = {}) { + return { step6Outcome: 'recoverable', reason, state: snapshot?.state, message: options.message || '' }; +} + +${extractFunction('step6OpenLoginEntry')} + +return { clicks, step6OpenLoginEntry }; + `)(); + + const result = await api.step6OpenLoginEntry( + { loginIdentifierType: 'phone', phoneNumber: '+441111111111' }, + { state: 'entry_page', loginEntryTrigger: { id: 'generic', textContent: '\u767b\u5f55\u81f3\u53e6\u4e00\u4e2a\u5e10\u6237' }, phoneEntryTrigger: { id: 'phone', textContent: '\u4f7f\u7528\u7535\u8bdd\u53f7\u7801\u7ee7\u7eed' } } + ); + + assert.deepStrictEqual(api.clicks, ['generic']); + assert.equal(result.routed, 'phone'); +}); + test('step 7 detects username text input when usernameKind is phone_number', () => { const api = createPhoneLoginEntryApi({ phoneUsernameKind: true,