Integrate Plus hosted session binding
This commit is contained in:
@@ -55,6 +55,8 @@ function extractFunction(name) {
|
||||
function createHarness() {
|
||||
return new Function(`
|
||||
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
|
||||
const SIGNUP_METHOD_EMAIL = 'email';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
|
||||
const workflowEngine = null;
|
||||
const self = {
|
||||
MultiPageStepDefinitions: {
|
||||
@@ -120,13 +122,10 @@ function isPlusModeState(state = {}) {
|
||||
return Boolean(state?.plusModeEnabled);
|
||||
}
|
||||
function resolveCurrentFlowCapabilities(state = {}, options = {}) {
|
||||
const normalizedPanelMode = String(options.panelMode || '').trim().toLowerCase();
|
||||
const requestedStrategy = normalizePlusAccountAccessStrategy(state.plusAccountAccessStrategy);
|
||||
const effectiveStrategy = normalizedPanelMode === 'sub2api'
|
||||
? (requestedStrategy === 'sub2api_codex_session' ? 'sub2api_codex_session' : 'oauth')
|
||||
: (normalizedPanelMode === 'cpa'
|
||||
? (requestedStrategy === 'cpa_codex_session' ? 'cpa_codex_session' : 'oauth')
|
||||
: 'oauth');
|
||||
const effectiveStrategy = state.accountContributionEnabled && state.plusModeEnabled
|
||||
? 'sub2api_codex_session'
|
||||
: requestedStrategy;
|
||||
return {
|
||||
effectivePanelMode: options.panelMode,
|
||||
effectivePlusAccountAccessStrategy: effectiveStrategy,
|
||||
@@ -153,12 +152,12 @@ return {
|
||||
`)();
|
||||
}
|
||||
|
||||
test('background step resolution keeps SUB2API session tail only when the effective Plus target supports it', () => {
|
||||
test('background step resolution keeps SUB2API session tail independent from the current panel mode', () => {
|
||||
const api = createHarness();
|
||||
const state = {
|
||||
activeFlowId: 'openai',
|
||||
flowId: 'openai',
|
||||
panelMode: 'sub2api',
|
||||
panelMode: 'cpa',
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal',
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
@@ -205,7 +204,7 @@ test('background step resolution keeps CPA session tail when the effective Plus
|
||||
]);
|
||||
});
|
||||
|
||||
test('background step resolution falls back to OAuth tail when the requested session strategy is not effective for the current panel mode', () => {
|
||||
test('background step resolution keeps SUB2API session tail even when the current panel mode is CPA', () => {
|
||||
const api = createHarness();
|
||||
const state = {
|
||||
activeFlowId: 'openai',
|
||||
@@ -220,12 +219,34 @@ test('background step resolution falls back to OAuth tail when the requested ses
|
||||
const resolvedState = api.buildResolvedStepDefinitionState(state);
|
||||
const nodeIds = api.getNodeIdsForState(state);
|
||||
|
||||
assert.equal(resolvedState.plusAccountAccessStrategy, 'oauth');
|
||||
assert.equal(nodeIds.includes('sub2api-session-import'), false);
|
||||
assert.deepStrictEqual(nodeIds.slice(-4), [
|
||||
'oauth-login',
|
||||
'fetch-login-code',
|
||||
'confirm-oauth',
|
||||
'platform-verify',
|
||||
assert.equal(resolvedState.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
assert.deepStrictEqual(nodeIds, [
|
||||
'open-chatgpt',
|
||||
'plus-checkout-create',
|
||||
'plus-checkout-billing',
|
||||
'paypal-approve',
|
||||
'plus-checkout-return',
|
||||
'sub2api-session-import',
|
||||
]);
|
||||
});
|
||||
|
||||
test('background contribution Plus mode forces SUB2API session import over a CPA session request', () => {
|
||||
const api = createHarness();
|
||||
const state = {
|
||||
activeFlowId: 'openai',
|
||||
flowId: 'openai',
|
||||
panelMode: 'cpa',
|
||||
plusModeEnabled: true,
|
||||
accountContributionEnabled: true,
|
||||
plusPaymentMethod: 'paypal',
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
signupMethod: 'email',
|
||||
};
|
||||
|
||||
const resolvedState = api.buildResolvedStepDefinitionState(state);
|
||||
const nodeIds = api.getNodeIdsForState(state);
|
||||
|
||||
assert.equal(resolvedState.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
assert.equal(nodeIds.includes('cpa-session-import'), false);
|
||||
assert.equal(nodeIds.at(-1), 'sub2api-session-import');
|
||||
});
|
||||
|
||||
@@ -453,7 +453,7 @@ test('SAVE_SETTING rebuilds Plus node statuses when the account access strategy
|
||||
});
|
||||
});
|
||||
|
||||
test('SAVE_SETTING rebuilds Plus node statuses when panel mode forces the effective strategy back to OAuth', async () => {
|
||||
test('SAVE_SETTING rebuilds Plus node statuses without forcing the Plus strategy back to OAuth', async () => {
|
||||
const source = fs.readFileSync('background/message-router.js', 'utf8');
|
||||
const globalScope = { console };
|
||||
const api = new Function('self', `${source}; return self.MultiPageBackgroundMessageRouter;`)(globalScope);
|
||||
@@ -491,8 +491,7 @@ test('SAVE_SETTING rebuilds Plus node statuses when panel mode forces the effect
|
||||
: {},
|
||||
broadcastDataUpdate: (payload) => broadcasts.push(payload),
|
||||
getNodeIdsForState: (nextState = {}) => (
|
||||
String(nextState.panelMode || '').trim() === 'sub2api'
|
||||
&& String(nextState.plusAccountAccessStrategy || '').trim() === 'sub2api_codex_session'
|
||||
String(nextState.plusAccountAccessStrategy || '').trim() === 'sub2api_codex_session'
|
||||
? [
|
||||
'open-chatgpt',
|
||||
'plus-checkout-create',
|
||||
@@ -532,61 +531,24 @@ test('SAVE_SETTING rebuilds Plus node statuses when panel mode forces the effect
|
||||
assert.equal(response.ok, true);
|
||||
assert.equal(state.panelMode, 'cpa');
|
||||
assert.equal(state.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
assert.equal(state.currentNodeId, '');
|
||||
assert.equal(state.oauthUrl, null);
|
||||
assert.equal(state.localhostUrl, null);
|
||||
assert.equal(state.sub2apiSessionId, null);
|
||||
assert.equal(state.plusManualConfirmationPending, false);
|
||||
assert.equal(state.plusManualConfirmationMessage, '');
|
||||
assert.equal(state.currentNodeId, 'sub2api-session-import');
|
||||
assert.equal(state.oauthUrl, 'https://oauth.example/current');
|
||||
assert.equal(state.localhostUrl, 'http://localhost:38080/callback');
|
||||
assert.equal(state.sub2apiSessionId, 'sub-session');
|
||||
assert.equal(state.plusManualConfirmationPending, true);
|
||||
assert.equal(state.plusManualConfirmationMessage, '完成后继续导入当前 ChatGPT 会话到 SUB2API。');
|
||||
assert.deepStrictEqual(state.nodeStatuses, {
|
||||
'open-chatgpt': 'pending',
|
||||
'plus-checkout-create': 'pending',
|
||||
'plus-checkout-billing': 'pending',
|
||||
'paypal-approve': 'pending',
|
||||
'plus-checkout-return': 'pending',
|
||||
'oauth-login': 'pending',
|
||||
'fetch-login-code': 'pending',
|
||||
'post-login-phone-verification': 'pending',
|
||||
'confirm-oauth': 'pending',
|
||||
'platform-verify': 'pending',
|
||||
'open-chatgpt': 'completed',
|
||||
'plus-checkout-create': 'completed',
|
||||
'plus-checkout-billing': 'completed',
|
||||
'paypal-approve': 'completed',
|
||||
'plus-checkout-return': 'completed',
|
||||
'sub2api-session-import': 'running',
|
||||
});
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(state.nodeStatuses, 'sub2api-session-import'), false);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(state.nodeStatuses, 'oauth-login'), false);
|
||||
assert.deepStrictEqual(broadcasts.at(-1), {
|
||||
panelMode: 'cpa',
|
||||
signupMethod: 'email',
|
||||
oauthUrl: null,
|
||||
localhostUrl: null,
|
||||
oauthFlowDeadlineAt: null,
|
||||
oauthFlowDeadlineSourceUrl: null,
|
||||
cpaOAuthState: null,
|
||||
cpaManagementOrigin: null,
|
||||
sub2apiSessionId: null,
|
||||
sub2apiOAuthState: null,
|
||||
sub2apiGroupId: null,
|
||||
sub2apiGroupIds: [],
|
||||
sub2apiDraftName: null,
|
||||
sub2apiProxyId: null,
|
||||
codex2apiSessionId: null,
|
||||
codex2apiOAuthState: null,
|
||||
plusManualConfirmationPending: false,
|
||||
plusManualConfirmationRequestId: '',
|
||||
plusManualConfirmationStep: 0,
|
||||
plusManualConfirmationMethod: '',
|
||||
plusManualConfirmationTitle: '',
|
||||
plusManualConfirmationMessage: '',
|
||||
nodeStatuses: {
|
||||
'open-chatgpt': 'pending',
|
||||
'plus-checkout-create': 'pending',
|
||||
'plus-checkout-billing': 'pending',
|
||||
'paypal-approve': 'pending',
|
||||
'plus-checkout-return': 'pending',
|
||||
'oauth-login': 'pending',
|
||||
'fetch-login-code': 'pending',
|
||||
'post-login-phone-verification': 'pending',
|
||||
'confirm-oauth': 'pending',
|
||||
'platform-verify': 'pending',
|
||||
},
|
||||
currentNodeId: '',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ test('flow capability registry exposes editable Plus account access strategies f
|
||||
|
||||
assert.deepEqual(
|
||||
capabilityState.availablePlusAccountAccessStrategies,
|
||||
['oauth', 'sub2api_codex_session']
|
||||
['oauth', 'cpa_codex_session', 'sub2api_codex_session']
|
||||
);
|
||||
assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
@@ -236,7 +236,7 @@ test('flow capability registry exposes editable Plus account access strategies f
|
||||
|
||||
assert.deepEqual(
|
||||
capabilityState.availablePlusAccountAccessStrategies,
|
||||
['oauth', 'cpa_codex_session']
|
||||
['oauth', 'cpa_codex_session', 'sub2api_codex_session']
|
||||
);
|
||||
assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'cpa_codex_session');
|
||||
assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'cpa_codex_session');
|
||||
@@ -244,7 +244,7 @@ test('flow capability registry exposes editable Plus account access strategies f
|
||||
assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'cpa_codex_session');
|
||||
});
|
||||
|
||||
test('flow capability registry forces OAuth when the current target only supports OAuth', () => {
|
||||
test('flow capability registry keeps Plus session strategies independent from the current OpenAI target', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry();
|
||||
|
||||
@@ -254,13 +254,38 @@ test('flow capability registry forces OAuth when the current target only support
|
||||
openaiIntegrationTargetId: 'codex2api',
|
||||
signupMethod: 'email',
|
||||
plusModeEnabled: true,
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
capabilityState.availablePlusAccountAccessStrategies,
|
||||
['oauth', 'cpa_codex_session', 'sub2api_codex_session']
|
||||
);
|
||||
assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
assert.equal(capabilityState.canEditPlusAccountAccessStrategy, true);
|
||||
assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
});
|
||||
|
||||
test('flow capability registry forces SUB2API Plus session import during contribution mode', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry();
|
||||
|
||||
const capabilityState = registry.resolveSidepanelCapabilities({
|
||||
state: {
|
||||
activeFlowId: 'openai',
|
||||
openaiIntegrationTargetId: 'cpa',
|
||||
signupMethod: 'email',
|
||||
plusModeEnabled: true,
|
||||
accountContributionEnabled: true,
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(capabilityState.availablePlusAccountAccessStrategies, ['oauth']);
|
||||
assert.deepEqual(capabilityState.availablePlusAccountAccessStrategies, ['sub2api_codex_session']);
|
||||
assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'cpa_codex_session');
|
||||
assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'oauth');
|
||||
assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
assert.equal(capabilityState.canEditPlusAccountAccessStrategy, false);
|
||||
assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'oauth');
|
||||
assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
});
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let index = start; index < source.length; index += 1) {
|
||||
const ch = source[index];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) {
|
||||
signatureEnded = true;
|
||||
}
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const ch = source[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('background routes plus-checkout-create through a completion signal for hosted checkout', () => {
|
||||
const backgroundSetStart = source.indexOf('const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([');
|
||||
const signalSetStart = source.indexOf('const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([');
|
||||
const timeoutMapStart = source.indexOf('const STEP_COMPLETION_SIGNAL_TIMEOUTS_BY_STEP_KEY = new Map([');
|
||||
const backgroundSetSource = source.slice(backgroundSetStart, signalSetStart);
|
||||
const signalSetSource = source.slice(signalSetStart, timeoutMapStart);
|
||||
|
||||
assert.doesNotMatch(backgroundSetSource, /'plus-checkout-create'/);
|
||||
assert.match(signalSetSource, /'plus-checkout-create'/);
|
||||
});
|
||||
|
||||
test('background gives PayPal session-import hosted checkout a long completion and idle window', () => {
|
||||
const bundle = `
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000;
|
||||
const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = 5 * 60 * 1000;
|
||||
const HOSTED_CHECKOUT_SUCCESS_SIGNAL_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
const STEP_COMPLETION_SIGNAL_TIMEOUTS_BY_STEP_KEY = new Map([
|
||||
['fill-profile', 150000],
|
||||
['gopay-subscription-confirm', 1800000],
|
||||
]);
|
||||
function normalizePlusAccountAccessStrategy(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) return PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION;
|
||||
if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) return PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION;
|
||||
return PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
|
||||
}
|
||||
function getNodeExecutionKeyForState(nodeId, state = {}) {
|
||||
return String(state?.nodes?.[nodeId]?.executeKey || nodeId || '').trim();
|
||||
}
|
||||
${extractFunction('isHostedCheckoutSuccessCompletionNode')}
|
||||
${extractFunction('getNodeCompletionSignalTimeoutMs')}
|
||||
${extractFunction('getAutoRunNodeIdleLogTimeoutMs')}
|
||||
return { isHostedCheckoutSuccessCompletionNode, getNodeCompletionSignalTimeoutMs, getAutoRunNodeIdleLogTimeoutMs };
|
||||
`;
|
||||
const api = new Function(bundle)();
|
||||
const hostedState = {
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal',
|
||||
signupMethod: 'email',
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
};
|
||||
|
||||
assert.equal(api.isHostedCheckoutSuccessCompletionNode('plus-checkout-create', hostedState), true);
|
||||
assert.equal(api.getNodeCompletionSignalTimeoutMs('plus-checkout-create', hostedState), 30 * 60 * 1000);
|
||||
assert.equal(api.getAutoRunNodeIdleLogTimeoutMs('plus-checkout-create', hostedState), 30 * 60 * 1000);
|
||||
assert.equal(api.isHostedCheckoutSuccessCompletionNode('plus-checkout-create', {
|
||||
...hostedState,
|
||||
accountContributionEnabled: true,
|
||||
plusAccountAccessStrategy: 'oauth',
|
||||
}), true);
|
||||
assert.equal(api.isHostedCheckoutSuccessCompletionNode('plus-checkout-create', { ...hostedState, plusAccountAccessStrategy: 'oauth' }), false);
|
||||
assert.equal(api.isHostedCheckoutSuccessCompletionNode('plus-checkout-create', { ...hostedState, signupMethod: 'phone' }), false);
|
||||
assert.equal(api.isHostedCheckoutSuccessCompletionNode('plus-checkout-create', {
|
||||
...hostedState,
|
||||
accountContributionEnabled: true,
|
||||
signupMethod: 'phone',
|
||||
}), false);
|
||||
assert.equal(api.isHostedCheckoutSuccessCompletionNode('plus-checkout-create', { ...hostedState, plusPaymentMethod: 'gopay' }), false);
|
||||
assert.equal(api.getNodeCompletionSignalTimeoutMs('plus-checkout-create', { ...hostedState, plusAccountAccessStrategy: 'oauth' }), 120000);
|
||||
assert.equal(api.getAutoRunNodeIdleLogTimeoutMs('plus-checkout-create', { ...hostedState, plusAccountAccessStrategy: 'oauth' }), 5 * 60 * 1000);
|
||||
});
|
||||
|
||||
test('background imports and registers the hosted checkout success manager', () => {
|
||||
assert.match(source, /importScripts\([\s\S]*'background\/plus-hosted-checkout-success\.js'/);
|
||||
assert.match(source, /createPlusHostedCheckoutSuccessManager\(\{/);
|
||||
assert.match(source, /chrome\.tabs\.onUpdated\.addListener\(\(tabId, changeInfo, tab\) => \{[\s\S]*plusHostedCheckoutSuccessManager\?\.handleTabUpdated/);
|
||||
});
|
||||
@@ -115,13 +115,14 @@ return {
|
||||
`)();
|
||||
}
|
||||
|
||||
test('sidepanel keeps requested Plus account strategy while OAuth-only targets force the effective value', () => {
|
||||
test('sidepanel keeps requested SUB2API session strategy independent from the selected target', () => {
|
||||
const api = buildHarness(
|
||||
`{
|
||||
canShowPlusSettings: true,
|
||||
runtimeLocks: { plusModeEnabled: true },
|
||||
canEditPlusAccountAccessStrategy: false,
|
||||
effectivePlusAccountAccessStrategy: 'oauth',
|
||||
canEditPlusAccountAccessStrategy: true,
|
||||
availablePlusAccountAccessStrategies: ['oauth', 'cpa_codex_session', 'sub2api_codex_session'],
|
||||
effectivePlusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
}`,
|
||||
`{
|
||||
activeFlowId: 'openai',
|
||||
@@ -134,11 +135,11 @@ test('sidepanel keeps requested Plus account strategy while OAuth-only targets f
|
||||
api.updatePlusModeUI();
|
||||
|
||||
assert.equal(api.rowPlusAccountAccessStrategy.style.display, '');
|
||||
assert.equal(api.selectPlusAccountAccessStrategy.disabled, true);
|
||||
assert.equal(api.selectPlusAccountAccessStrategy.disabled, false);
|
||||
assert.equal(api.selectPlusAccountAccessStrategy.dataset.requestedValue, 'sub2api_codex_session');
|
||||
assert.equal(api.selectPlusAccountAccessStrategy.value, 'oauth');
|
||||
assert.equal(api.selectPlusAccountAccessStrategy.value, 'sub2api_codex_session');
|
||||
assert.equal(api.getRequestedPlusAccountAccessStrategy(), 'sub2api_codex_session');
|
||||
assert.match(api.plusAccountAccessStrategyCaption.textContent, /OAuth/);
|
||||
assert.match(api.plusAccountAccessStrategyCaption.textContent, /SUB2API/);
|
||||
});
|
||||
|
||||
test('sidepanel enables SUB2API session strategy selection when the current Plus target supports it', () => {
|
||||
@@ -283,6 +284,7 @@ return {
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
signupMethod: 'email',
|
||||
phoneSignupReloginAfterBindEmailEnabled: false,
|
||||
accountContributionEnabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -294,6 +296,7 @@ return {
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
signupMethod: 'email',
|
||||
phoneSignupReloginAfterBindEmailEnabled: false,
|
||||
accountContributionEnabled: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -38,7 +38,7 @@ test('plus checkout content script can be injected repeatedly on the same page',
|
||||
assert.equal(context.__MULTIPAGE_PLUS_CHECKOUT_READY__, true);
|
||||
});
|
||||
|
||||
function createPlusCheckoutMessageHarness({ checkoutSessionId = 'cs_test_123' } = {}) {
|
||||
function createPlusCheckoutMessageHarness({ checkoutSessionId = 'cs_test_123', checkoutResponse = null } = {}) {
|
||||
const attrs = new Map();
|
||||
let listener = null;
|
||||
const fetchCalls = [];
|
||||
@@ -84,7 +84,7 @@ function createPlusCheckoutMessageHarness({ checkoutSessionId = 'cs_test_123' }
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ checkout_session_id: checkoutSessionId }),
|
||||
json: async () => checkoutResponse || ({ checkout_session_id: checkoutSessionId }),
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected fetch url: ${url}`);
|
||||
@@ -124,9 +124,58 @@ test('CREATE_PLUS_CHECKOUT keeps PayPal on DE/EUR and openai_ie merchant path by
|
||||
assert.equal(checkoutCall.options.headers.Authorization, 'Bearer test-access-token');
|
||||
const payload = JSON.parse(checkoutCall.options.body);
|
||||
assert.equal(payload.plan_name, 'chatgptplusplan');
|
||||
assert.equal(payload.checkout_ui_mode, 'custom');
|
||||
assert.deepEqual(payload.billing_details, { country: 'DE', currency: 'EUR' });
|
||||
});
|
||||
|
||||
test('CREATE_PLUS_CHECKOUT ignores hosted PayPal URLs outside hosted mode', async () => {
|
||||
const harness = createPlusCheckoutMessageHarness({
|
||||
checkoutResponse: {
|
||||
checkout_session_id: 'cs_paypal_custom',
|
||||
nested: {
|
||||
hosted: 'https://pay.openai.com/c/pay/hosted_cs_paypal_custom',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await harness.send({
|
||||
type: 'CREATE_PLUS_CHECKOUT',
|
||||
source: 'test',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.checkoutUrl, 'https://chatgpt.com/checkout/openai_ie/cs_paypal_custom');
|
||||
assert.equal(result.hostedCheckoutUrl, 'https://pay.openai.com/c/pay/hosted_cs_paypal_custom');
|
||||
assert.equal(result.fallbackCheckoutUrl, 'https://chatgpt.com/checkout/openai_ie/cs_paypal_custom');
|
||||
});
|
||||
|
||||
test('CREATE_PLUS_CHECKOUT prefers the hosted PayPal checkout URL only in hosted mode', async () => {
|
||||
const harness = createPlusCheckoutMessageHarness({
|
||||
checkoutResponse: {
|
||||
checkout_session_id: 'cs_paypal_hosted',
|
||||
nested: {
|
||||
hosted: 'https://pay.openai.com/c/pay/hosted_cs_paypal',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await harness.send({
|
||||
type: 'CREATE_PLUS_CHECKOUT',
|
||||
source: 'test',
|
||||
payload: { hostedCheckoutMode: true },
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.checkoutUrl, 'https://pay.openai.com/c/pay/hosted_cs_paypal');
|
||||
assert.equal(result.hostedCheckoutUrl, 'https://pay.openai.com/c/pay/hosted_cs_paypal');
|
||||
assert.equal(result.fallbackCheckoutUrl, 'https://chatgpt.com/checkout/openai_ie/cs_paypal_hosted');
|
||||
|
||||
const checkoutCall = harness.fetchCalls.find((call) => call.url === 'https://chatgpt.com/backend-api/payments/checkout');
|
||||
const payload = JSON.parse(checkoutCall.options.body);
|
||||
assert.equal(payload.checkout_ui_mode, 'hosted');
|
||||
});
|
||||
|
||||
test('CREATE_PLUS_CHECKOUT uses ID/IDR and openai_llc merchant path for GoPay', async () => {
|
||||
const harness = createPlusCheckoutMessageHarness({ checkoutSessionId: 'cs_gopay' });
|
||||
|
||||
|
||||
@@ -257,6 +257,102 @@ test('Plus checkout create does not wait 20 seconds after opening checkout page'
|
||||
assert.equal(events.some((event) => event.type === 'sleep' && event.ms === 20000), false);
|
||||
});
|
||||
|
||||
test('PayPal session-import checkout waits for the ChatGPT payment success signal', async () => {
|
||||
const events = [];
|
||||
const executor = api.createPlusCheckoutCreateExecutor({
|
||||
addLog: async (message, level = 'info') => events.push({ type: 'log', message, level }),
|
||||
chrome: {
|
||||
tabs: {
|
||||
create: async (payload) => {
|
||||
events.push({ type: 'tab-create', payload });
|
||||
return { id: 43 };
|
||||
},
|
||||
update: async (tabId, payload) => events.push({ type: 'tab-update', tabId, payload }),
|
||||
},
|
||||
},
|
||||
completeNodeFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => events.push({ type: 'ready' }),
|
||||
registerTab: async (source, tabId) => events.push({ type: 'register', source, tabId }),
|
||||
sendTabMessageUntilStopped: async (tabId, source, message) => {
|
||||
events.push({ type: 'tab-message', tabId, source, message });
|
||||
return {
|
||||
checkoutUrl: 'https://pay.openai.com/c/pay/hosted_session',
|
||||
country: 'US',
|
||||
currency: 'USD',
|
||||
};
|
||||
},
|
||||
setState: async (payload) => events.push({ type: 'set-state', payload }),
|
||||
sleepWithStop: async (ms) => events.push({ type: 'sleep', ms }),
|
||||
waitForTabCompleteUntilStopped: async () => events.push({ type: 'tab-complete' }),
|
||||
});
|
||||
|
||||
await executor.executePlusCheckoutCreate({
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal',
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
signupMethod: 'email',
|
||||
});
|
||||
|
||||
assert.equal(events.some((event) => event.type === 'complete'), false);
|
||||
assert.deepEqual(
|
||||
events.find((event) => event.type === 'tab-message')?.message?.payload,
|
||||
{ paymentMethod: 'paypal', hostedCheckoutMode: true }
|
||||
);
|
||||
assert.deepEqual(
|
||||
events.find((event) => event.type === 'tab-update'),
|
||||
{ type: 'tab-update', tabId: 43, payload: { url: 'https://pay.openai.com/c/pay/hosted_session', active: true } }
|
||||
);
|
||||
const statePayload = events.find((event) => event.type === 'set-state')?.payload || {};
|
||||
assert.equal(statePayload.plusCheckoutTabId, 43);
|
||||
assert.equal(statePayload.plusCheckoutUrl, 'https://pay.openai.com/c/pay/hosted_session');
|
||||
assert.equal(statePayload.plusHostedCheckoutCompletionPending, true);
|
||||
assert.equal(statePayload.plusHostedCheckoutCompleted, false);
|
||||
assert.equal(statePayload.plusReturnUrl, '');
|
||||
assert.equal(events.some((event) => event.type === 'log' && /waiting for the ChatGPT payment success page/.test(event.message)), true);
|
||||
});
|
||||
|
||||
test('Contribution Plus checkout uses hosted success waiting even when the requested strategy is OAuth', async () => {
|
||||
const events = [];
|
||||
const executor = api.createPlusCheckoutCreateExecutor({
|
||||
addLog: async () => {},
|
||||
chrome: {
|
||||
tabs: {
|
||||
create: async () => ({ id: 44 }),
|
||||
update: async (tabId, payload) => events.push({ type: 'tab-update', tabId, payload }),
|
||||
},
|
||||
},
|
||||
completeNodeFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
|
||||
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||
registerTab: async () => {},
|
||||
sendTabMessageUntilStopped: async (tabId, source, message) => {
|
||||
events.push({ type: 'tab-message', tabId, source, message });
|
||||
return {
|
||||
checkoutUrl: 'https://pay.openai.com/c/pay/contribution_hosted_session',
|
||||
country: 'US',
|
||||
currency: 'USD',
|
||||
};
|
||||
},
|
||||
setState: async (payload) => events.push({ type: 'set-state', payload }),
|
||||
sleepWithStop: async () => {},
|
||||
waitForTabCompleteUntilStopped: async () => {},
|
||||
});
|
||||
|
||||
await executor.executePlusCheckoutCreate({
|
||||
plusModeEnabled: true,
|
||||
accountContributionEnabled: true,
|
||||
plusPaymentMethod: 'paypal',
|
||||
plusAccountAccessStrategy: 'oauth',
|
||||
signupMethod: 'email',
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
events.find((event) => event.type === 'tab-message')?.message?.payload,
|
||||
{ paymentMethod: 'paypal', hostedCheckoutMode: true }
|
||||
);
|
||||
assert.equal(events.some((event) => event.type === 'complete'), false);
|
||||
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.plusHostedCheckoutCompletionPending, true);
|
||||
});
|
||||
|
||||
test('GoPay plus checkout create forwards gopay payment method to the checkout content script', async () => {
|
||||
const events = [];
|
||||
const executor = api.createPlusCheckoutCreateExecutor({
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background/plus-hosted-checkout-success.js', 'utf8');
|
||||
|
||||
function loadApi() {
|
||||
const scope = {};
|
||||
return new Function('self', `${source}; return self.MultiPagePlusHostedCheckoutSuccess;`)(scope);
|
||||
}
|
||||
|
||||
function createState(overrides = {}) {
|
||||
return {
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal',
|
||||
plusHostedCheckoutCompletionPending: true,
|
||||
plusCheckoutTabId: 77,
|
||||
nodeStatuses: {
|
||||
'plus-checkout-create': 'running',
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test('hosted checkout success manager completes plus-checkout-create on the tracked success tab', async () => {
|
||||
const api = loadApi();
|
||||
const events = [];
|
||||
let state = createState();
|
||||
const manager = api.createPlusHostedCheckoutSuccessManager({
|
||||
addLog: async (message, level, options) => events.push({ type: 'log', message, level, options }),
|
||||
completeNodeFromBackground: async (nodeId, payload) => events.push({ type: 'complete', nodeId, payload }),
|
||||
getState: async () => state,
|
||||
setState: async (payload) => {
|
||||
events.push({ type: 'set-state', payload });
|
||||
state = { ...state, ...payload };
|
||||
},
|
||||
});
|
||||
|
||||
const successUrl = 'https://chatgpt.com/payments/success?session_id=cs_test';
|
||||
const result = await manager.handleTabUpdated(77, { status: 'complete' }, { url: successUrl });
|
||||
|
||||
assert.deepEqual(result, {
|
||||
completed: true,
|
||||
plusReturnUrl: successUrl,
|
||||
});
|
||||
assert.deepEqual(events.find((event) => event.type === 'complete'), {
|
||||
type: 'complete',
|
||||
nodeId: 'plus-checkout-create',
|
||||
payload: {
|
||||
plusReturnUrl: successUrl,
|
||||
plusHostedCheckoutCompleted: true,
|
||||
},
|
||||
});
|
||||
assert.equal(state.plusHostedCheckoutCompletionPending, false);
|
||||
assert.equal(state.plusHostedCheckoutCompleted, true);
|
||||
assert.equal(state.plusReturnUrl, successUrl);
|
||||
assert.equal(events.find((event) => event.type === 'log')?.options?.nodeId, 'plus-checkout-create');
|
||||
});
|
||||
|
||||
test('hosted checkout success manager ignores unrelated updates and inactive states', async () => {
|
||||
const api = loadApi();
|
||||
const events = [];
|
||||
let state = createState();
|
||||
const manager = api.createPlusHostedCheckoutSuccessManager({
|
||||
completeNodeFromBackground: async () => events.push('complete'),
|
||||
getState: async () => state,
|
||||
setState: async () => events.push('set-state'),
|
||||
});
|
||||
|
||||
assert.equal(api.isPaymentSuccessUrl('https://chatgpt.com/payments/success'), true);
|
||||
assert.equal(api.isPaymentSuccessUrl('https://example.com/payments/success'), false);
|
||||
assert.equal(await manager.handleTabUpdated(77, { status: 'loading' }, { url: 'https://chatgpt.com/payments/success' }), null);
|
||||
assert.equal(await manager.handleTabUpdated(77, { status: 'complete' }, { url: 'https://chatgpt.com/' }), null);
|
||||
assert.equal(await manager.handleTabUpdated(88, { status: 'complete' }, { url: 'https://chatgpt.com/payments/success' }), null);
|
||||
|
||||
state = createState({ plusHostedCheckoutCompletionPending: false });
|
||||
assert.equal(await manager.handleTabUpdated(77, { status: 'complete' }, { url: 'https://chatgpt.com/payments/success' }), null);
|
||||
|
||||
state = createState({ plusHostedCheckoutCompletionPending: true, nodeStatuses: { 'plus-checkout-create': 'completed' } });
|
||||
assert.equal(await manager.handleTabUpdated(77, { status: 'complete' }, { url: 'https://chatgpt.com/payments/success' }), null);
|
||||
assert.deepEqual(events, []);
|
||||
});
|
||||
|
||||
test('hosted checkout success manager deduplicates concurrent success events for one tab', async () => {
|
||||
const api = loadApi();
|
||||
const events = [];
|
||||
const state = createState();
|
||||
let releaseComplete;
|
||||
const completeGate = new Promise((resolve) => {
|
||||
releaseComplete = resolve;
|
||||
});
|
||||
const manager = api.createPlusHostedCheckoutSuccessManager({
|
||||
addLog: async () => {},
|
||||
completeNodeFromBackground: async (nodeId) => {
|
||||
events.push({ type: 'complete', nodeId });
|
||||
await completeGate;
|
||||
},
|
||||
getState: async () => state,
|
||||
setState: async (payload) => events.push({ type: 'set-state', payload }),
|
||||
});
|
||||
|
||||
const first = manager.handleTabUpdated(77, { status: 'complete' }, { url: 'https://chatgpt.com/payments/success' });
|
||||
const second = manager.handleTabUpdated(77, { status: 'complete' }, { url: 'https://chatgpt.com/payments/success' });
|
||||
await Promise.resolve();
|
||||
releaseComplete();
|
||||
|
||||
const results = await Promise.all([first, second]);
|
||||
assert.equal(results.filter(Boolean).length, 1);
|
||||
assert.equal(events.filter((event) => event.type === 'complete').length, 1);
|
||||
});
|
||||
|
||||
test('hosted checkout success manager fails the checkout node when continuation throws', async () => {
|
||||
const api = loadApi();
|
||||
const events = [];
|
||||
const manager = api.createPlusHostedCheckoutSuccessManager({
|
||||
addLog: async (message, level) => events.push({ type: 'log', message, level }),
|
||||
completeNodeFromBackground: async () => {
|
||||
throw new Error('boom');
|
||||
},
|
||||
failNodeFromBackground: async (nodeId, message) => events.push({ type: 'fail', nodeId, message }),
|
||||
getState: async () => createState(),
|
||||
setState: async () => {},
|
||||
});
|
||||
|
||||
const result = await manager.handleTabUpdated(77, { status: 'complete' }, { url: 'https://chatgpt.com/payments/success' });
|
||||
|
||||
assert.equal(result.failed, true);
|
||||
assert.equal(result.message, 'boom');
|
||||
assert.deepEqual(events.find((event) => event.type === 'fail'), {
|
||||
type: 'fail',
|
||||
nodeId: 'plus-checkout-create',
|
||||
message: 'boom',
|
||||
});
|
||||
assert.equal(events.some((event) => event.type === 'log' && event.level === 'error'), true);
|
||||
});
|
||||
@@ -123,6 +123,7 @@ return {
|
||||
plusAccountAccessStrategy: 'oauth',
|
||||
signupMethod: 'email',
|
||||
phoneSignupReloginAfterBindEmailEnabled: false,
|
||||
accountContributionEnabled: false,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [88] });
|
||||
|
||||
@@ -113,7 +113,7 @@ return {
|
||||
assert.deepEqual(api.getStepIds(), [7]);
|
||||
assert.deepEqual(api.calls[0], {
|
||||
type: 'getSteps',
|
||||
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
|
||||
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false, accountContributionEnabled: false },
|
||||
});
|
||||
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [7] });
|
||||
});
|
||||
@@ -303,7 +303,7 @@ return {
|
||||
assert.deepEqual(api.getStepIds(), [13]);
|
||||
assert.deepEqual(api.calls[0], {
|
||||
type: 'getSteps',
|
||||
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
|
||||
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false, accountContributionEnabled: false },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -276,8 +276,8 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
|
||||
plusPaymentMethod: 'paypal',
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
},
|
||||
previousNodeId: 'plus-checkout-return',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
previousNodeId: 'plus-checkout-create',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7],
|
||||
},
|
||||
{
|
||||
label: 'gopay',
|
||||
@@ -357,8 +357,8 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
|
||||
plusPaymentMethod: 'paypal',
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
},
|
||||
previousNodeId: 'plus-checkout-return',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
previousNodeId: 'plus-checkout-create',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7],
|
||||
},
|
||||
{
|
||||
label: 'gopay',
|
||||
|
||||
Reference in New Issue
Block a user