新增手机号绑定邮箱后邮箱模式重登流程

This commit is contained in:
QLHazyCoder
2026-05-16 03:23:28 +08:00
parent 727d1523dd
commit efad1f94d7
16 changed files with 715 additions and 35 deletions
@@ -199,6 +199,8 @@ return {
assert.equal(api.normalizePersistentSettingValue('accountRunHistoryTextEnabled', 1), true);
assert.equal(api.normalizePersistentSettingValue('phoneVerificationEnabled', 1), true);
assert.equal(api.normalizePersistentSettingValue('phoneSignupReloginAfterBindEmailEnabled', 1), true);
assert.equal(api.normalizePersistentSettingValue('phoneSignupReloginAfterBindEmailEnabled', 0), false);
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'gopay'), 'gopay');
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'gpc-helper'), 'gpc-helper');
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'paypal'), 'paypal');
@@ -181,6 +181,7 @@ return {
plusModeEnabled: true,
plusPaymentMethod: 'gopay',
signupMethod: 'phone',
phoneSignupReloginAfterBindEmailEnabled: false,
}]);
assert.equal(steps[0].title, '注册并输入手机号');
});
+16 -4
View File
@@ -13,25 +13,37 @@ test('background imports node registry and shared workflow definitions', () => {
assert.match(source, /PLUS_PAYPAL_STEP_DEFINITIONS/);
assert.match(source, /PLUS_GOPAY_STEP_DEFINITIONS/);
assert.match(source, /NORMAL_PHONE_STEP_DEFINITIONS/);
assert.match(source, /NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/);
assert.match(source, /PLUS_PAYPAL_PHONE_STEP_DEFINITIONS/);
assert.match(source, /PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/);
assert.match(source, /PLUS_GOPAY_PHONE_STEP_DEFINITIONS/);
assert.match(source, /PLUS_GOPAY_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/);
assert.match(source, /PLUS_GPC_PHONE_STEP_DEFINITIONS/);
assert.match(source, /PLUS_GPC_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS/);
assert.match(source, /plusPayPalStepRegistry/);
assert.match(source, /plusGoPayStepRegistry/);
assert.match(source, /normalPhoneStepRegistry/);
assert.match(source, /normalPhoneBoundEmailReloginStepRegistry/);
assert.match(source, /plusPayPalPhoneStepRegistry/);
assert.match(source, /plusPayPalPhoneBoundEmailReloginStepRegistry/);
assert.match(source, /plusGoPayPhoneStepRegistry/);
assert.match(source, /plusGoPayPhoneBoundEmailReloginStepRegistry/);
assert.match(source, /plusGpcPhoneStepRegistry/);
assert.match(source, /plusGpcPhoneBoundEmailReloginStepRegistry/);
assert.match(source, /const signupMethod = getSignupMethodForStepDefinitions\(state\);/);
assert.match(source, /signupMethod === SIGNUP_METHOD_PHONE \? normalPhoneStepRegistry : normalStepRegistry/);
assert.match(source, /const useBoundEmailRelogin = signupMethod === SIGNUP_METHOD_PHONE/);
assert.match(source, /useBoundEmailRelogin \? normalPhoneBoundEmailReloginStepRegistry : normalPhoneStepRegistry/);
assert.match(source, /const paymentMethod = normalizePlusPaymentMethod\(state\?\.plusPaymentMethod\);/);
assert.match(source, /paymentMethod === PLUS_PAYMENT_METHOD_GOPAY/);
assert.match(source, /signupMethod === SIGNUP_METHOD_PHONE \? plusGoPayPhoneStepRegistry : plusGoPayStepRegistry/);
assert.match(source, /signupMethod === SIGNUP_METHOD_PHONE \? plusPayPalPhoneStepRegistry : plusPayPalStepRegistry/);
assert.match(source, /signupMethod === SIGNUP_METHOD_PHONE \? plusGpcPhoneStepRegistry : plusGpcStepRegistry/);
assert.match(source, /useBoundEmailRelogin \? plusGoPayPhoneBoundEmailReloginStepRegistry : plusGoPayPhoneStepRegistry/);
assert.match(source, /useBoundEmailRelogin \? plusPayPalPhoneBoundEmailReloginStepRegistry : plusPayPalPhoneStepRegistry/);
assert.match(source, /useBoundEmailRelogin \? plusGpcPhoneBoundEmailReloginStepRegistry : plusGpcPhoneStepRegistry/);
assert.match(source, /activeStepRegistry\.executeNode\(normalizedNodeId,\s*\{/);
assert.match(source, /'bind-email': \(state\) => step8Executor\.executeBindEmail\(state\)/);
assert.match(source, /'fetch-bind-email-code': \(state\) => step8Executor\.executeFetchBindEmailCode\(state\)/);
assert.match(source, /'relogin-bound-email': \(state\) => executeReloginBoundEmail\(state\)/);
assert.match(source, /'fetch-bound-email-login-code': \(state\) => step8Executor\.executeBoundEmailLoginCode\(state\)/);
assert.match(source, /'post-bound-email-phone-verification': \(state\) => step8Executor\.executeBoundEmailPostLoginPhoneVerification\(state\)/);
assert.match(source, /background\/steps\/create-plus-checkout\.js/);
assert.match(source, /background\/steps\/fill-plus-checkout\.js/);
assert.match(source, /background\/steps\/gopay-manual-confirm\.js/);
+224
View File
@@ -797,6 +797,146 @@ test('fetch-bind-email-code rejects unexpected pages after bind-email submitted'
]);
});
test('fetch-bound-email-login-code uses bound email identity and does not allow add-email', async () => {
const calls = {
ensureOptions: null,
resolveState: null,
resolveOptions: null,
setStates: [],
};
const realDateNow = Date.now;
Date.now = () => 333000;
const executor = api.createStep8Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async (options) => {
calls.ensureOptions = options;
return {
state: 'verification_page',
displayedEmail: 'bind.user@example.com',
url: 'https://auth.openai.com/email-verification',
};
},
getOAuthFlowRemainingMs: async () => 9000,
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getMailConfig: () => ({
provider: 'qq',
label: 'QQ 邮箱',
source: 'mail-qq',
url: 'https://mail.qq.com',
navigateOnReuse: false,
}),
getTabId: async () => 1,
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
LUCKMAIL_PROVIDER: 'luckmail-api',
resolveVerificationStep: async (_step, state, _mail, options) => {
calls.resolveState = state;
calls.resolveOptions = options;
},
reuseOrCreateTab: async () => 1,
setState: async (payload) => {
calls.setStates.push(payload);
},
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
throwIfStopped: () => {},
});
try {
await executor.executeBoundEmailLoginCode({
visibleStep: 11,
nodeId: 'fetch-bound-email-login-code',
signupMethod: 'phone',
resolvedSignupMethod: 'phone',
accountIdentifierType: 'phone',
accountIdentifier: '+447780579093',
signupPhoneNumber: '+447780579093',
email: 'bind.user@example.com',
step8VerificationTargetEmail: 'bind.user@example.com',
oauthUrl: 'https://oauth.example/latest',
});
} finally {
Date.now = realDateNow;
}
assert.equal(calls.ensureOptions.allowAddEmailPage, false);
assert.equal(calls.ensureOptions.allowPhoneVerificationPage, true);
assert.equal(calls.resolveState.signupMethod, 'email');
assert.equal(calls.resolveState.resolvedSignupMethod, 'email');
assert.equal(calls.resolveState.accountIdentifierType, 'email');
assert.equal(calls.resolveState.accountIdentifier, 'bind.user@example.com');
assert.equal(calls.resolveOptions.completionStep, 11);
assert.equal(calls.resolveOptions.sessionKey, '11:333000');
assert.equal(calls.resolveOptions.targetEmail, 'bind.user@example.com');
assert.deepStrictEqual(calls.setStates, [
{
step8VerificationTargetEmail: 'bind.user@example.com',
},
]);
});
test('fetch-bound-email-login-code defers phone pages to bound-email phone verification step', async () => {
const completions = [];
const setStates = [];
const executor = api.createStep8Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
completeNodeFromBackground: async (step, payload) => {
completions.push({ step, payload });
},
ensureStep8VerificationPageReady: async () => ({
state: 'add_phone_page',
addPhonePage: true,
url: 'https://auth.openai.com/add-phone',
}),
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs) => defaultTimeoutMs,
getTabId: async () => 1,
reuseOrCreateTab: async () => 1,
setState: async (payload) => {
setStates.push(payload);
},
throwIfStopped: () => {},
});
await executor.executeBoundEmailLoginCode({
visibleStep: 11,
nodeId: 'fetch-bound-email-login-code',
email: 'bind.user@example.com',
step8VerificationTargetEmail: 'bind.user@example.com',
oauthUrl: 'https://oauth.example/latest',
});
assert.deepStrictEqual(setStates, [
{
step8VerificationTargetEmail: '',
loginVerificationRequestedAt: null,
},
]);
assert.deepStrictEqual(completions, [
{
step: 'fetch-bound-email-login-code',
payload: {
loginVerificationRequestedAt: null,
skipLoginVerificationStep: true,
addPhonePage: true,
phoneVerificationPage: false,
},
},
]);
});
test('step 8 does not submit or recover add-email inside fetch-login-code', async () => {
const calls = {
ensureCalls: 0,
@@ -1189,6 +1329,90 @@ test('Plus login-code step reuses step 8 verification logic but completes visibl
assert.deepStrictEqual(remainingStepCalls, [11, 11]);
});
test('bound-email relogin code step points recovery to the relogin step in Plus mode', async () => {
let resolvedStep = null;
let resolvedOptions = null;
let readyOptions = null;
const remainingStepCalls = [];
const executor = api.createStep8Executor({
addLog: async () => {},
chrome: {
tabs: {
update: async () => {},
},
},
CLOUDFLARE_TEMP_EMAIL_PROVIDER: 'cloudflare-temp-email',
confirmCustomVerificationStepBypass: async () => {},
ensureStep8VerificationPageReady: async (options) => {
readyOptions = options;
return { state: 'verification_page', displayedEmail: 'bound.user@example.com' };
},
rerunStep7ForStep8Recovery: async () => {},
getOAuthFlowRemainingMs: async (details) => {
remainingStepCalls.push(details.step);
return 9000;
},
getOAuthFlowStepTimeoutMs: async (defaultTimeoutMs, details) => {
remainingStepCalls.push(details.step);
return Math.min(defaultTimeoutMs, 9000);
},
getMailConfig: () => ({
provider: 'qq',
label: 'QQ 邮箱',
source: 'mail-qq',
url: 'https://mail.qq.com',
navigateOnReuse: false,
}),
getState: async () => ({
email: 'bound.user@example.com',
password: 'secret',
plusModeEnabled: true,
signupMethod: 'phone',
phoneSignupReloginAfterBindEmailEnabled: true,
}),
getTabId: async (sourceName) => (sourceName === 'signup-page' ? 1 : 2),
HOTMAIL_PROVIDER: 'hotmail-api',
isTabAlive: async () => true,
isVerificationMailPollingError: () => false,
LUCKMAIL_PROVIDER: 'luckmail-api',
resolveVerificationStep: async (step, _state, _mail, options) => {
resolvedStep = step;
resolvedOptions = options;
await options.getRemainingTimeMs({ actionLabel: '绑定邮箱后登录验证码流程' });
},
reuseOrCreateTab: async () => {},
setState: async () => {},
shouldUseCustomRegistrationEmail: () => false,
STANDARD_MAIL_VERIFICATION_RESEND_INTERVAL_MS: 25000,
STEP7_MAIL_POLLING_RECOVERY_MAX_ATTEMPTS: 8,
throwIfStopped: () => {},
});
await executor.executeBoundEmailLoginCode({
visibleStep: 14,
plusModeEnabled: true,
signupMethod: 'phone',
phoneSignupReloginAfterBindEmailEnabled: true,
email: 'bound.user@example.com',
password: 'secret',
oauthUrl: 'https://oauth.example/latest',
step8VerificationTargetEmail: 'bound.user@example.com',
});
assert.equal(resolvedStep, 8);
assert.equal(resolvedOptions.completionStep, 14);
assert.equal(resolvedOptions.targetEmail, 'bound.user@example.com');
assert.deepStrictEqual(readyOptions, {
visibleStep: 14,
authLoginStep: 13,
allowPhoneVerificationPage: true,
allowAddEmailPage: false,
timeoutMs: 9000,
});
assert.deepStrictEqual(remainingStepCalls, [14, 14]);
});
test('step 8 completes directly when auth page is already on OAuth consent page', async () => {
const events = {
resolveCalls: 0,
@@ -7,6 +7,7 @@ const {
} = require('../mail-provider-utils');
const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8');
const sidepanelHtml = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
function extractFunction(name) {
const markers = [`async function ${name}(`, `function ${name}(`];
@@ -347,6 +348,14 @@ return {
assert.equal(api.shouldWarnCpaPhoneSignup('phone', 'cpa'), true);
});
test('phone signup relogin-after-bind-email switch is wired into UI and step definitions', () => {
assert.match(sidepanelHtml, /row-phone-signup-relogin-after-bind-email/);
assert.match(sidepanelHtml, /input-phone-signup-relogin-after-bind-email/);
assert.match(sidepanelSource, /phoneSignupReloginAfterBindEmailEnabled: typeof inputPhoneSignupReloginAfterBindEmail !== 'undefined'/);
assert.match(sidepanelSource, /phoneSignupReloginAfterBindEmailEnabled: Boolean\(state\?\.phoneSignupReloginAfterBindEmailEnabled\)/);
assert.match(sidepanelSource, /nextPhoneSignupReloginAfterBindEmailEnabled !== currentPhoneSignupReloginAfterBindEmailEnabled/);
});
test('manual step 3 uses phone identity without requiring registration email', () => {
const api = new Function(`
let latestState = { signupMethod: 'phone', phoneVerificationEnabled: true, signupPhoneNumber: '+441111111111', accountIdentifierType: 'phone', accountIdentifier: '+441111111111' };
+4 -2
View File
@@ -83,6 +83,7 @@ const window = {
let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = 'paypal';
let currentSignupMethod = 'email';
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
const DEFAULT_SIGNUP_METHOD = 'email';
let stepDefinitions = [];
let STEP_IDS = [];
@@ -106,7 +107,7 @@ return {
assert.deepEqual(api.getStepIds(), [7]);
assert.deepEqual(api.calls[0], {
type: 'getSteps',
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', signupMethod: 'email' },
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
});
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [7] });
});
@@ -252,6 +253,7 @@ const window = {
let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = 'paypal';
let currentSignupMethod = 'email';
let currentPhoneSignupReloginAfterBindEmailEnabled = false;
const DEFAULT_SIGNUP_METHOD = 'email';
let stepDefinitions = [];
let STEP_IDS = [];
@@ -275,7 +277,7 @@ return {
assert.deepEqual(api.getStepIds(), [13]);
assert.deepEqual(api.calls[0], {
type: 'getSteps',
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', signupMethod: 'email' },
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
});
});
+45
View File
@@ -9,8 +9,17 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
const api = new Function('self', `${source}; return self.MultiPageStepDefinitions;`)(globalScope);
const steps = api.getSteps();
const phoneSteps = api.getSteps({ signupMethod: 'phone' });
const phoneReloginSteps = api.getSteps({
signupMethod: 'phone',
phoneSignupReloginAfterBindEmailEnabled: true,
});
const plusSteps = api.getSteps({ plusModeEnabled: true });
const plusPhoneSteps = api.getSteps({ plusModeEnabled: true, signupMethod: 'phone' });
const plusPhoneReloginSteps = api.getSteps({
plusModeEnabled: true,
signupMethod: 'phone',
phoneSignupReloginAfterBindEmailEnabled: true,
});
const goPaySteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gopay' });
const gpcSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' });
@@ -58,6 +67,27 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
'platform-verify',
]
);
assert.deepStrictEqual(
phoneReloginSteps.map((step) => step.key),
[
'open-chatgpt',
'submit-signup-email',
'fill-password',
'fetch-signup-code',
'fill-profile',
'wait-registration-success',
'oauth-login',
'fetch-login-code',
'bind-email',
'relogin-bound-email',
'fetch-bound-email-login-code',
'post-bound-email-phone-verification',
'confirm-oauth',
'platform-verify',
]
);
assert.equal(phoneReloginSteps.find((step) => step.key === 'relogin-bound-email')?.title, '绑定邮箱后刷新 OAuth 并登录(邮箱)');
assert.equal(phoneReloginSteps.some((step) => step.key === 'fetch-bind-email-code'), false);
assert.deepStrictEqual(
plusSteps.map((step) => step.key),
@@ -103,6 +133,19 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
'platform-verify',
]
);
assert.deepStrictEqual(
plusPhoneReloginSteps.map((step) => step.key).slice(-8),
[
'oauth-login',
'fetch-login-code',
'bind-email',
'relogin-bound-email',
'fetch-bound-email-login-code',
'post-bound-email-phone-verification',
'confirm-oauth',
'platform-verify',
]
);
assert.equal(goPaySteps.some((step) => step.key === 'paypal-approve'), false);
assert.equal(api.getStepById(8, { plusModeEnabled: true, plusPaymentMethod: 'gopay' }), null);
assert.equal(api.getPlusPaymentStepTitle({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), '');
@@ -110,6 +153,8 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.equal(api.getLastStepId({ plusModeEnabled: true }), 14);
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone' }), 15);
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, signupMethod: 'phone', phoneSignupReloginAfterBindEmailEnabled: true }), 17);
assert.equal(api.hasFlow('openai'), true);
assert.equal(api.hasFlow('site-a'), false);
assert.deepStrictEqual(api.getRegisteredFlowIds(), ['openai']);