From 3aa9b136d32b5ff5e8a9fa9e0882b9a10b36a653 Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Wed, 20 May 2026 07:38:49 +0800
Subject: [PATCH 1/8] Integrate Plus hosted session binding
---
background.js | 85 +++++++++-
background/plus-hosted-checkout-success.js | 153 ++++++++++++++++++
background/steps/create-plus-checkout.js | 53 +++++-
content/plus-checkout.js | 41 ++++-
data/step-definitions.js | 13 +-
shared/flow-capabilities.js | 30 ++--
shared/settings-schema.js | 2 +-
sidepanel/sidepanel.html | 2 +-
sidepanel/sidepanel.js | 29 +---
...ctive-plus-account-access-strategy.test.js | 53 ++++--
.../background-message-router-module.test.js | 68 ++------
tests/flow-capabilities-module.test.js | 37 ++++-
.../hosted-checkout-background-wiring.test.js | 120 ++++++++++++++
tests/plus-account-access-strategy.test.js | 15 +-
tests/plus-checkout-address-input.test.js | 53 +++++-
tests/plus-checkout-create-wait.test.js | 96 +++++++++++
tests/plus-hosted-checkout-success.test.js | 135 ++++++++++++++++
tests/sidepanel-flow-source-registry.test.js | 1 +
tests/sidepanel-plus-payment-method.test.js | 4 +-
tests/step-definitions-module.test.js | 8 +-
20 files changed, 850 insertions(+), 148 deletions(-)
create mode 100644 background/plus-hosted-checkout-success.js
create mode 100644 tests/hosted-checkout-background-wiring.test.js
create mode 100644 tests/plus-hosted-checkout-success.test.js
diff --git a/background.js b/background.js
index d826c65..0e8bd81 100644
--- a/background.js
+++ b/background.js
@@ -41,6 +41,7 @@ importScripts(
'background/message-router.js',
'background/verification-flow.js',
'background/auto-run-controller.js',
+ 'background/plus-hosted-checkout-success.js',
'background/tab-runtime.js',
'background/navigation-utils.js',
'background/logging-status.js',
@@ -857,6 +858,19 @@ function buildResolvedStepDefinitionState(state = {}) {
|| capabilityState?.effectiveSignupMethod
|| requestedSignupMethod
);
+ const resolvedPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy(
+ stepDefinitionOptions.plusAccountAccessStrategy
+ ?? capabilityState?.effectivePlusAccountAccessStrategy
+ ?? state?.plusAccountAccessStrategy
+ );
+ const effectivePlusAccountAccessStrategy = (
+ resolvedActiveFlowId === defaultFlowId
+ && plusModeEnabled
+ && Boolean(state?.accountContributionEnabled)
+ && resolvedSignupMethod === SIGNUP_METHOD_EMAIL
+ )
+ ? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION
+ : resolvedPlusAccountAccessStrategy;
return {
...state,
@@ -868,11 +882,7 @@ function buildResolvedStepDefinitionState(state = {}) {
? plusModeEnabled
: Boolean(stepDefinitionOptions.plusModeEnabled),
plusPaymentMethod,
- plusAccountAccessStrategy: normalizePlusAccountAccessStrategy(
- stepDefinitionOptions.plusAccountAccessStrategy
- ?? capabilityState?.effectivePlusAccountAccessStrategy
- ?? state?.plusAccountAccessStrategy
- ),
+ plusAccountAccessStrategy: effectivePlusAccountAccessStrategy,
signupMethod: resolvedSignupMethod,
resolvedSignupMethod: resolvedSignupMethod,
phoneSignupReloginAfterBindEmailEnabled: Boolean(state?.phoneSignupReloginAfterBindEmailEnabled),
@@ -1150,7 +1160,7 @@ const PERSISTED_SETTING_DEFAULTS = {
customPassword: '',
plusModeEnabled: false,
plusPaymentMethod: DEFAULT_PLUS_PAYMENT_METHOD,
- plusAccountAccessStrategy: 'oauth',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
paypalEmail: '',
paypalPassword: '',
currentPayPalAccountId: '',
@@ -10521,6 +10531,7 @@ let resumeWaiter = null;
const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000;
const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = 5 * 60 * 1000;
const AUTO_RUN_STEP_IDLE_LOG_CHECK_INTERVAL_MS = 5000;
+const HOSTED_CHECKOUT_SUCCESS_SIGNAL_TIMEOUT_MS = 30 * 60 * 1000;
const AUTO_RUN_STEP_IDLE_RESTART_MAX_ATTEMPTS = 3;
const AUTO_RUN_STEP_IDLE_RESTART_ERROR_PREFIX = 'AUTO_RUN_STEP_IDLE_RESTART::';
const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 7, 8, 9]);
@@ -10530,7 +10541,6 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([
'submit-signup-email',
'fetch-signup-code',
'wait-registration-success',
- 'plus-checkout-create',
'plus-checkout-billing',
'paypal-approve',
'plus-checkout-return',
@@ -10558,6 +10568,7 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([
const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([
'fill-password',
'fill-profile',
+ 'plus-checkout-create',
'gopay-subscription-confirm',
'platform-verify',
]);
@@ -10665,7 +10676,34 @@ function getAutoRunPreExecutionDelayMs(step, state = {}) {
return getAutoRunPreExecutionDelayMsForNode(getNodeIdByStepForState(step, state), state);
}
+function isHostedCheckoutSuccessCompletionNode(nodeId, state = {}) {
+ const executionKey = getNodeExecutionKeyForState(nodeId, state);
+ if ((executionKey || nodeId) !== 'plus-checkout-create') {
+ return false;
+ }
+ if (!state?.plusModeEnabled) {
+ return false;
+ }
+ const paymentMethod = String(state?.plusPaymentMethod || '').trim().toLowerCase();
+ if (paymentMethod !== 'paypal') {
+ return false;
+ }
+ const signupMethod = String(state?.resolvedSignupMethod || state?.signupMethod || 'email').trim().toLowerCase();
+ if (signupMethod === 'phone') {
+ return false;
+ }
+ if (Boolean(state?.accountContributionEnabled)) {
+ return true;
+ }
+ const strategy = normalizePlusAccountAccessStrategy(state?.plusAccountAccessStrategy);
+ return strategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION
+ || strategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION;
+}
+
function getNodeCompletionSignalTimeoutMs(nodeId, state = {}) {
+ if (isHostedCheckoutSuccessCompletionNode(nodeId, state)) {
+ return HOSTED_CHECKOUT_SUCCESS_SIGNAL_TIMEOUT_MS;
+ }
const executionKey = getNodeExecutionKeyForState(nodeId, state);
return STEP_COMPLETION_SIGNAL_TIMEOUTS_BY_STEP_KEY.get(executionKey || nodeId) || AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS;
}
@@ -10674,6 +10712,12 @@ function getStepCompletionSignalTimeoutMs(step, state = {}) {
return getNodeCompletionSignalTimeoutMs(getNodeIdByStepForState(step, state), state);
}
+function getAutoRunNodeIdleLogTimeoutMs(nodeId, state = {}) {
+ return isHostedCheckoutSuccessCompletionNode(nodeId, state)
+ ? HOSTED_CHECKOUT_SUCCESS_SIGNAL_TIMEOUT_MS
+ : AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS;
+}
+
function notifyNodeComplete(nodeId, payload) {
const normalizedNodeId = String(nodeId || '').trim();
const waiter = nodeWaiters.get(normalizedNodeId);
@@ -10974,10 +11018,19 @@ async function runAutoNodeActionWithIdleLogWatchdog(nodeId, action, options = {}
}
async function executeNodeAndWaitWithAutoRunIdleLogWatchdog(nodeId, delayAfter = 2000, options = {}) {
+ const executionState = await getState();
+ const idleTimeoutMs = Number(options.idleTimeoutMs) > 0
+ ? Number(options.idleTimeoutMs)
+ : (typeof getAutoRunNodeIdleLogTimeoutMs === 'function'
+ ? getAutoRunNodeIdleLogTimeoutMs(nodeId, executionState)
+ : AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS);
return runAutoNodeActionWithIdleLogWatchdog(
nodeId,
() => executeNodeAndWait(nodeId, delayAfter),
- options
+ {
+ ...options,
+ idleTimeoutMs,
+ }
);
}
@@ -13441,6 +13494,16 @@ const plusReturnConfirmExecutor = self.MultiPageBackgroundPlusReturnConfirm?.cre
sleepWithStop,
waitForTabUrlMatchUntilStopped,
});
+const plusHostedCheckoutSuccessManager = self.MultiPagePlusHostedCheckoutSuccess?.createPlusHostedCheckoutSuccessManager({
+ addLog,
+ completeNodeFromBackground,
+ failNodeFromBackground: async (nodeId, message) => {
+ notifyNodeError(nodeId, message);
+ await finalizeDeferredNodeExecutionError(nodeId, new Error(message));
+ },
+ getState,
+ setState,
+});
const sub2ApiSessionImportExecutor = self.MultiPageBackgroundSub2ApiSessionImport?.createSub2ApiSessionImportExecutor({
addLog,
chrome,
@@ -15541,6 +15604,12 @@ chrome.alarms.onAlarm.addListener((alarm) => {
}
});
+chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
+ plusHostedCheckoutSuccessManager?.handleTabUpdated(tabId, changeInfo, tab).catch((err) => {
+ console.error(LOG_PREFIX, 'Failed to process PayPal hosted checkout success page:', err);
+ });
+});
+
chrome.runtime.onStartup.addListener(() => {
migrateLegacyAccountContributionState().catch((err) => {
console.error(LOG_PREFIX, 'Failed to migrate legacy account contribution state on startup:', err);
diff --git a/background/plus-hosted-checkout-success.js b/background/plus-hosted-checkout-success.js
new file mode 100644
index 0000000..8bdb2e9
--- /dev/null
+++ b/background/plus-hosted-checkout-success.js
@@ -0,0 +1,153 @@
+(function attachPlusHostedCheckoutSuccess(root, factory) {
+ root.MultiPagePlusHostedCheckoutSuccess = factory();
+})(typeof self !== 'undefined' ? self : globalThis, function createPlusHostedCheckoutSuccessModule() {
+ const PAYMENT_SUCCESS_URL_PATTERN = /^https:\/\/(?:chatgpt\.com|www\.chatgpt\.com|chat\.openai\.com)\/(?:backend-api\/)?payments\/success(?:[/?#]|$)/i;
+ const PLUS_CHECKOUT_NODE_ID = 'plus-checkout-create';
+ const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
+
+ function normalizeString(value = '') {
+ return String(value || '').trim();
+ }
+
+ function normalizePaymentMethod(value = '') {
+ return normalizeString(value).toLowerCase();
+ }
+
+ function isPaymentSuccessUrl(url = '') {
+ return PAYMENT_SUCCESS_URL_PATTERN.test(normalizeString(url));
+ }
+
+ function isRunnableNodeStatus(value = '') {
+ const normalized = normalizeString(value).toLowerCase();
+ return !normalized || normalized === 'pending' || normalized === 'running';
+ }
+
+ function isHostedCheckoutSuccessWaitActive(state = {}, tabId = null) {
+ if (!state || typeof state !== 'object') {
+ return false;
+ }
+ if (!state.plusModeEnabled) {
+ return false;
+ }
+ if (normalizePaymentMethod(state.plusPaymentMethod) !== PLUS_PAYMENT_METHOD_PAYPAL) {
+ return false;
+ }
+ if (state.plusHostedCheckoutCompletionPending !== true) {
+ return false;
+ }
+
+ const checkoutTabId = Number(state.plusCheckoutTabId);
+ if (!Number.isInteger(checkoutTabId) || checkoutTabId <= 0) {
+ return false;
+ }
+ if (tabId !== null && checkoutTabId !== Number(tabId)) {
+ return false;
+ }
+
+ return isRunnableNodeStatus(state.nodeStatuses?.[PLUS_CHECKOUT_NODE_ID]);
+ }
+
+ function createPlusHostedCheckoutSuccessManager(deps = {}) {
+ const {
+ addLog: rawAddLog = async () => {},
+ completeNodeFromBackground = null,
+ failNodeFromBackground = null,
+ getState = async () => ({}),
+ setState = async () => {},
+ } = deps;
+
+ const activeTabIds = new Set();
+
+ function addLog(message, level = 'info', options = {}) {
+ return rawAddLog(message, level, {
+ stepKey: PLUS_CHECKOUT_NODE_ID,
+ nodeId: PLUS_CHECKOUT_NODE_ID,
+ ...(options && typeof options === 'object' ? options : {}),
+ });
+ }
+
+ async function completeFromSuccessTab(tabId, successUrl = '') {
+ const numericTabId = Number(tabId);
+ if (!Number.isInteger(numericTabId) || numericTabId <= 0) {
+ return null;
+ }
+ if (activeTabIds.has(numericTabId)) {
+ return null;
+ }
+ activeTabIds.add(numericTabId);
+
+ try {
+ const initialState = await getState();
+ if (!isHostedCheckoutSuccessWaitActive(initialState, numericTabId)) {
+ return null;
+ }
+
+ const latestState = await getState();
+ if (!isHostedCheckoutSuccessWaitActive(latestState, numericTabId)) {
+ return null;
+ }
+
+ const normalizedSuccessUrl = normalizeString(successUrl);
+ await setState({
+ plusReturnUrl: normalizedSuccessUrl,
+ });
+ await addLog('Detected ChatGPT payment success page; continuing Plus session import flow.', 'ok');
+
+ if (typeof completeNodeFromBackground === 'function') {
+ await completeNodeFromBackground(PLUS_CHECKOUT_NODE_ID, {
+ plusReturnUrl: normalizedSuccessUrl,
+ plusHostedCheckoutCompleted: true,
+ });
+ }
+
+ await setState({
+ plusHostedCheckoutCompletionPending: false,
+ plusHostedCheckoutCompleted: true,
+ });
+
+ return {
+ completed: true,
+ plusReturnUrl: normalizedSuccessUrl,
+ };
+ } catch (error) {
+ const message = normalizeString(error?.message) || 'unknown error';
+ await addLog(`Failed to continue after ChatGPT payment success page: ${message}`, 'error');
+ if (typeof failNodeFromBackground === 'function') {
+ await failNodeFromBackground(PLUS_CHECKOUT_NODE_ID, message);
+ return {
+ completed: false,
+ failed: true,
+ message,
+ };
+ }
+ throw error;
+ } finally {
+ activeTabIds.delete(numericTabId);
+ }
+ }
+
+ async function handleTabUpdated(tabId, changeInfo = {}, tab = {}) {
+ if (changeInfo?.status !== 'complete' && tab?.status !== 'complete') {
+ return null;
+ }
+ const nextUrl = normalizeString(changeInfo?.url || tab?.url);
+ if (!isPaymentSuccessUrl(nextUrl)) {
+ return null;
+ }
+ return completeFromSuccessTab(tabId, nextUrl);
+ }
+
+ return {
+ completeFromSuccessTab,
+ handleTabUpdated,
+ isHostedCheckoutSuccessWaitActive,
+ isPaymentSuccessUrl,
+ };
+ }
+
+ return {
+ createPlusHostedCheckoutSuccessManager,
+ isHostedCheckoutSuccessWaitActive,
+ isPaymentSuccessUrl,
+ };
+});
diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js
index 3e5fb30..f4a08fd 100644
--- a/background/steps/create-plus-checkout.js
+++ b/background/steps/create-plus-checkout.js
@@ -7,6 +7,8 @@
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
+ const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
+ const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
@@ -47,6 +49,17 @@
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
}
+ 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 'oauth';
+ }
+
function getCheckoutModeLabel(state = {}) {
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
@@ -63,6 +76,25 @@
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay' : 'PayPal';
}
+ function shouldWaitForHostedCheckoutSuccess(state = {}, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
+ if (!state?.plusModeEnabled) {
+ return false;
+ }
+ if (normalizePlusPaymentMethod(paymentMethod) !== PLUS_PAYMENT_METHOD_PAYPAL) {
+ return false;
+ }
+ const signupMethod = String(state?.resolvedSignupMethod || state?.signupMethod || 'email').trim().toLowerCase();
+ if (signupMethod === 'phone') {
+ return false;
+ }
+ if (Boolean(state?.accountContributionEnabled)) {
+ return true;
+ }
+ const strategy = normalizePlusAccountAccessStrategy(state?.plusAccountAccessStrategy);
+ return strategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION
+ || strategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION;
+ }
+
async function openFreshChatGptTabForCheckoutCreate() {
const tab = typeof createAutomationTab === 'function'
? await createAutomationTab({ url: PLUS_CHECKOUT_ENTRY_URL, active: true })
@@ -439,6 +471,8 @@
plusCheckoutCountry: result.country || 'ID',
plusCheckoutCurrency: result.currency || 'IDR',
plusCheckoutSource: result.checkoutSource,
+ plusHostedCheckoutCompletionPending: false,
+ plusHostedCheckoutCompleted: false,
gopayHelperTaskId: result.taskId,
gopayHelperTaskStatus: result.taskStatus,
gopayHelperStatusText: result.statusText,
@@ -484,10 +518,14 @@
logMessage: '步骤 6:正在等待 ChatGPT 页面完成加载,再继续创建订阅页...',
});
+ const waitForHostedSuccess = shouldWaitForHostedCheckoutSuccess(state, paymentMethod);
const result = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
type: 'CREATE_PLUS_CHECKOUT',
source: 'background',
- payload: { paymentMethod },
+ payload: {
+ paymentMethod,
+ ...(waitForHostedSuccess ? { hostedCheckoutMode: true } : {}),
+ },
});
if (result?.error) {
@@ -496,9 +534,10 @@
if (!result?.checkoutUrl) {
throw new Error(`步骤 6:${checkoutModeLabel}未返回可用的订阅链接。`);
}
+ const checkoutUrl = String(result.checkoutUrl || '').trim();
await addLog(`步骤 6:${checkoutModeLabel}已创建,正在打开订阅页面...`, 'ok');
- await chrome.tabs.update(tabId, { url: result.checkoutUrl, active: true });
+ await chrome.tabs.update(tabId, { url: checkoutUrl, active: true });
await waitForTabCompleteUntilStopped(tabId);
await sleepWithStop(1000);
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
@@ -509,14 +548,22 @@
await setState({
plusCheckoutTabId: tabId,
- plusCheckoutUrl: result.checkoutUrl,
+ plusCheckoutUrl: checkoutUrl,
plusCheckoutCountry: result.country || 'DE',
plusCheckoutCurrency: result.currency || 'EUR',
plusCheckoutSource: '',
+ plusReturnUrl: '',
+ plusHostedCheckoutCompletionPending: waitForHostedSuccess,
+ plusHostedCheckoutCompleted: false,
});
await addLog(`步骤 6:Plus Checkout 页面已就绪(${paymentMethodLabel} / ${result.country || 'DE'} ${result.currency || 'EUR'}),准备继续下一步。`, 'info');
+ if (waitForHostedSuccess) {
+ await addLog('Step 6: PayPal hosted checkout is open; waiting for the ChatGPT payment success page before importing the Plus session.', 'info');
+ return;
+ }
+
await completeNodeFromBackground('plus-checkout-create', {
plusCheckoutCountry: result.country || 'DE',
plusCheckoutCurrency: result.currency || 'EUR',
diff --git a/content/plus-checkout.js b/content/plus-checkout.js
index b3f7f2a..bc5891e 100644
--- a/content/plus-checkout.js
+++ b/content/plus-checkout.js
@@ -616,10 +616,12 @@ function writeGoPayDiagnostics(reason, level = 'info') {
return writePaymentMethodDiagnostics(PLUS_PAYMENT_METHOD_GOPAY, reason, level);
}
-function buildPlusCheckoutPayload(paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
+function buildPlusCheckoutPayload(paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL, options = {}) {
const config = getPaymentMethodConfig(paymentMethod);
+ const hostedCheckoutMode = config.id === PLUS_PAYMENT_METHOD_PAYPAL && Boolean(options.hostedCheckoutMode);
return {
...JSON.parse(JSON.stringify(PLUS_CHECKOUT_PAYLOAD_BASE)),
+ checkout_ui_mode: hostedCheckoutMode ? 'hosted' : 'custom',
billing_details: {
...config.billingDetails,
},
@@ -635,6 +637,31 @@ function buildPlusCheckoutUrl(checkoutSessionId, paymentMethod = PLUS_PAYMENT_ME
return `https://chatgpt.com/checkout/${config.checkoutMerchantPath}/${sessionId}`;
}
+function findHostedCheckoutUrl(payload = {}) {
+ const queue = [payload];
+ const seen = new Set();
+ while (queue.length) {
+ const current = queue.shift();
+ if (!current || typeof current !== 'object' || seen.has(current)) {
+ continue;
+ }
+ seen.add(current);
+
+ const values = Array.isArray(current) ? current : Object.values(current);
+ for (const value of values) {
+ if (typeof value === 'string') {
+ const candidate = value.trim();
+ if (/^https:\/\/(?:pay\.openai\.com|checkout\.stripe\.com)\/c\/pay\//i.test(candidate)) {
+ return candidate;
+ }
+ } else if (value && typeof value === 'object') {
+ queue.push(value);
+ }
+ }
+ }
+ return '';
+}
+
async function createPlusCheckoutSession(options = {}) {
await waitForDocumentComplete();
log('Plus:正在读取 ChatGPT 登录会话...');
@@ -650,7 +677,8 @@ async function createPlusCheckoutSession(options = {}) {
log('Plus:正在创建 checkout 会话...');
const paymentMethod = normalizePlusPaymentMethod(options.paymentMethod);
- const checkoutPayload = buildPlusCheckoutPayload(paymentMethod);
+ const hostedCheckoutMode = Boolean(options.hostedCheckoutMode);
+ const checkoutPayload = buildPlusCheckoutPayload(paymentMethod, { hostedCheckoutMode });
const response = await fetch('https://chatgpt.com/backend-api/payments/checkout', {
method: 'POST',
credentials: 'include',
@@ -666,9 +694,16 @@ async function createPlusCheckoutSession(options = {}) {
const detail = data?.detail || data?.message || `HTTP ${response.status}`;
throw new Error(`创建 Plus Checkout 失败:${detail}`);
}
+ const hostedCheckoutUrl = findHostedCheckoutUrl(data);
+ const fallbackCheckoutUrl = buildPlusCheckoutUrl(data.checkout_session_id, paymentMethod);
+ const checkoutUrl = hostedCheckoutMode && paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL && hostedCheckoutUrl
+ ? hostedCheckoutUrl
+ : fallbackCheckoutUrl;
return {
- checkoutUrl: buildPlusCheckoutUrl(data.checkout_session_id, paymentMethod),
+ checkoutUrl,
+ hostedCheckoutUrl,
+ fallbackCheckoutUrl,
country: checkoutPayload.billing_details.country,
currency: checkoutPayload.billing_details.currency,
};
diff --git a/data/step-definitions.js b/data/step-definitions.js
index 27b0a0a..3db0ac7 100644
--- a/data/step-definitions.js
+++ b/data/step-definitions.js
@@ -32,6 +32,7 @@
{ id: 8, order: 80, key: 'paypal-approve', title: 'PayPal 登录与授权', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-approve' },
{ id: 9, order: 90, key: 'plus-checkout-return', title: '订阅回跳确认', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-return' },
];
+ const PLUS_PAYPAL_SESSION_PREFIX_STEP_DEFINITIONS = PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS.slice(0, 6);
const PLUS_GOPAY_PREFIX_STEP_DEFINITIONS = [
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网', sourceId: 'chatgpt', driverId: null, command: 'open-chatgpt' },
@@ -168,16 +169,16 @@
const NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true });
const PLUS_PAYPAL_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL);
const PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS = createOpenAiSteps(
- PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS,
- 10,
- 100,
+ PLUS_PAYPAL_SESSION_PREFIX_STEP_DEFINITIONS,
+ 7,
+ 70,
SIGNUP_METHOD_EMAIL,
{ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION }
);
const PLUS_PAYPAL_CPA_SESSION_STEP_DEFINITIONS = createOpenAiSteps(
- PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS,
- 10,
- 100,
+ PLUS_PAYPAL_SESSION_PREFIX_STEP_DEFINITIONS,
+ 7,
+ 70,
SIGNUP_METHOD_EMAIL,
{ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION }
);
diff --git a/shared/flow-capabilities.js b/shared/flow-capabilities.js
index d5f430f..9c7eadf 100644
--- a/shared/flow-capabilities.js
+++ b/shared/flow-capabilities.js
@@ -12,6 +12,11 @@
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 OPENAI_PLUS_ACCOUNT_ACCESS_STRATEGIES = Object.freeze([
+ PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH,
+ PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION,
+ PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION,
+ ]);
const VALID_OPENAI_TARGET_IDS = Array.isArray(flowRegistryApi.OPENAI_TARGET_IDS)
? flowRegistryApi.OPENAI_TARGET_IDS.slice()
: ['cpa', 'sub2api', 'codex2api'];
@@ -378,24 +383,27 @@
const requestedPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy(
options?.plusAccountAccessStrategy ?? state?.plusAccountAccessStrategy
);
- const availablePlusAccountAccessStrategies = activeFlowId === 'openai'
+ const canUsePlusAccountAccessStrategies = activeFlowId === 'openai'
&& Boolean(flowState.supportsPlusMode)
&& Boolean(runtimeLocks.plusModeEnabled)
- && effectiveSignupMethod === SIGNUP_METHOD_EMAIL
- ? (
- Array.isArray(targetState.supportedPlusAccountAccessStrategies)
- && targetState.supportedPlusAccountAccessStrategies.length > 0
- ? targetState.supportedPlusAccountAccessStrategies.slice()
- : [PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH]
- )
+ && effectiveSignupMethod === SIGNUP_METHOD_EMAIL;
+ const forceSub2ApiSessionImportForContribution = canUsePlusAccountAccessStrategies
+ && Boolean(runtimeLocks.accountContribution);
+ const availablePlusAccountAccessStrategies = canUsePlusAccountAccessStrategies
+ ? (forceSub2ApiSessionImportForContribution
+ ? [PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION]
+ : OPENAI_PLUS_ACCOUNT_ACCESS_STRATEGIES.slice())
: [PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH];
- const effectivePlusAccountAccessStrategy = availablePlusAccountAccessStrategies.includes(requestedPlusAccountAccessStrategy)
- ? requestedPlusAccountAccessStrategy
- : PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
+ const effectivePlusAccountAccessStrategy = forceSub2ApiSessionImportForContribution
+ ? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION
+ : (availablePlusAccountAccessStrategies.includes(requestedPlusAccountAccessStrategy)
+ ? requestedPlusAccountAccessStrategy
+ : PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH);
const canEditPlusAccountAccessStrategy = activeFlowId === 'openai'
&& Boolean(flowState.supportsPlusMode)
&& Boolean(runtimeLocks.plusModeEnabled)
&& effectiveSignupMethod === SIGNUP_METHOD_EMAIL
+ && !forceSub2ApiSessionImportForContribution
&& availablePlusAccountAccessStrategies.length > 1;
const visibleGroupIds = typeof flowRegistryApi.getVisibleGroupIds === 'function'
&& isRegisteredFlowId(activeFlowId)
diff --git a/shared/settings-schema.js b/shared/settings-schema.js
index 2f39824..d9a7070 100644
--- a/shared/settings-schema.js
+++ b/shared/settings-schema.js
@@ -106,7 +106,7 @@
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
- plusAccountAccessStrategy: 'oauth',
+ plusAccountAccessStrategy: 'sub2api_codex_session',
},
autoRun: {
stepExecutionRange: {
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 87d721b..39c82c2 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -320,7 +320,7 @@
- 当前来源仅支持 OAuth
+ Plus 模式未启用
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index eec0060..0fe3a03 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -555,7 +555,7 @@ const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
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 DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION;
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
@@ -9312,31 +9312,8 @@ function updatePlusModeUI() {
selectPlusAccountAccessStrategy.setAttribute('aria-disabled', String(selectPlusAccountAccessStrategy.disabled));
}
if (typeof plusAccountAccessStrategyCaption !== 'undefined' && plusAccountAccessStrategyCaption) {
- if (!enabled) {
- plusAccountAccessStrategyCaption.textContent = '当前来源仅支持 OAuth';
- } else if (!canEditPlusAccountAccessStrategy) {
- plusAccountAccessStrategyCaption.textContent = '当前来源仅支持 OAuth';
- } else if (effectivePlusAccountAccessStrategy === sub2apiSessionStrategyValue) {
- plusAccountAccessStrategyCaption.textContent = '复用当前 Plus 已登录会话,直接导入到 SUB2API';
- } else if (effectivePlusAccountAccessStrategy === oauthStrategyValue) {
- plusAccountAccessStrategyCaption.textContent = '通过 OAuth 回调创建 SUB2API 账号';
- } else {
- plusAccountAccessStrategyCaption.textContent = '当前来源仅支持 OAuth';
- }
- }
- if (typeof plusAccountAccessStrategyCaption !== 'undefined' && plusAccountAccessStrategyCaption) {
- if (!enabled || !canEditPlusAccountAccessStrategy) {
- plusAccountAccessStrategyCaption.textContent = '当前来源仅支持 OAuth';
- } else {
- plusAccountAccessStrategyCaption.textContent = describePlusAccountAccessStrategy(
- effectivePlusAccountAccessStrategy,
- effectiveTargetId
- );
- }
- }
- if (typeof plusAccountAccessStrategyCaption !== 'undefined' && plusAccountAccessStrategyCaption) {
- plusAccountAccessStrategyCaption.textContent = !enabled || !canEditPlusAccountAccessStrategy
- ? '当前来源仅支持 OAuth'
+ plusAccountAccessStrategyCaption.textContent = !enabled
+ ? 'Plus 模式未启用'
: describePlusAccountAccessStrategy(
effectivePlusAccountAccessStrategy,
effectiveTargetId
diff --git a/tests/background-effective-plus-account-access-strategy.test.js b/tests/background-effective-plus-account-access-strategy.test.js
index aae5b48..2b837e5 100644
--- a/tests/background-effective-plus-account-access-strategy.test.js
+++ b/tests/background-effective-plus-account-access-strategy.test.js
@@ -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');
+});
diff --git a/tests/background-message-router-module.test.js b/tests/background-message-router-module.test.js
index cc0908e..a9938fe 100644
--- a/tests/background-message-router-module.test.js
+++ b/tests/background-message-router-module.test.js
@@ -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: '',
});
});
diff --git a/tests/flow-capabilities-module.test.js b/tests/flow-capabilities-module.test.js
index 0f9befe..3f3b729 100644
--- a/tests/flow-capabilities-module.test.js
+++ b/tests/flow-capabilities-module.test.js
@@ -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');
});
diff --git a/tests/hosted-checkout-background-wiring.test.js b/tests/hosted-checkout-background-wiring.test.js
new file mode 100644
index 0000000..d230a67
--- /dev/null
+++ b/tests/hosted-checkout-background-wiring.test.js
@@ -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/);
+});
diff --git a/tests/plus-account-access-strategy.test.js b/tests/plus-account-access-strategy.test.js
index 3998edc..7d8966f 100644
--- a/tests/plus-account-access-strategy.test.js
+++ b/tests/plus-account-access-strategy.test.js
@@ -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,
},
},
]);
diff --git a/tests/plus-checkout-address-input.test.js b/tests/plus-checkout-address-input.test.js
index 738aa1e..377d71f 100644
--- a/tests/plus-checkout-address-input.test.js
+++ b/tests/plus-checkout-address-input.test.js
@@ -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' });
diff --git a/tests/plus-checkout-create-wait.test.js b/tests/plus-checkout-create-wait.test.js
index 15ee2e0..4bd2ff5 100644
--- a/tests/plus-checkout-create-wait.test.js
+++ b/tests/plus-checkout-create-wait.test.js
@@ -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({
diff --git a/tests/plus-hosted-checkout-success.test.js b/tests/plus-hosted-checkout-success.test.js
new file mode 100644
index 0000000..9a51b6a
--- /dev/null
+++ b/tests/plus-hosted-checkout-success.test.js
@@ -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);
+});
diff --git a/tests/sidepanel-flow-source-registry.test.js b/tests/sidepanel-flow-source-registry.test.js
index 58ab6ad..397d1ae 100644
--- a/tests/sidepanel-flow-source-registry.test.js
+++ b/tests/sidepanel-flow-source-registry.test.js
@@ -123,6 +123,7 @@ return {
plusAccountAccessStrategy: 'oauth',
signupMethod: 'email',
phoneSignupReloginAfterBindEmailEnabled: false,
+ accountContributionEnabled: false,
},
});
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [88] });
diff --git a/tests/sidepanel-plus-payment-method.test.js b/tests/sidepanel-plus-payment-method.test.js
index b6560ee..027902e 100644
--- a/tests/sidepanel-plus-payment-method.test.js
+++ b/tests/sidepanel-plus-payment-method.test.js
@@ -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 },
});
});
diff --git a/tests/step-definitions-module.test.js b/tests/step-definitions-module.test.js
index 9dfb329..1271b8c 100644
--- a/tests/step-definitions-module.test.js
+++ b/tests/step-definitions-module.test.js
@@ -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',
From f36e0cedb31a020b45db182847a075f6d287198b Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Wed, 20 May 2026 08:05:25 +0800
Subject: [PATCH 2/8] Revert "Integrate Plus hosted session binding"
This reverts commit 3aa9b136d32b5ff5e8a9fa9e0882b9a10b36a653.
---
background.js | 85 +---------
background/plus-hosted-checkout-success.js | 153 ------------------
background/steps/create-plus-checkout.js | 53 +-----
content/plus-checkout.js | 41 +----
data/step-definitions.js | 13 +-
shared/flow-capabilities.js | 30 ++--
shared/settings-schema.js | 2 +-
sidepanel/sidepanel.html | 2 +-
sidepanel/sidepanel.js | 29 +++-
...ctive-plus-account-access-strategy.test.js | 53 ++----
.../background-message-router-module.test.js | 68 ++++++--
tests/flow-capabilities-module.test.js | 37 +----
.../hosted-checkout-background-wiring.test.js | 120 --------------
tests/plus-account-access-strategy.test.js | 15 +-
tests/plus-checkout-address-input.test.js | 53 +-----
tests/plus-checkout-create-wait.test.js | 96 -----------
tests/plus-hosted-checkout-success.test.js | 135 ----------------
tests/sidepanel-flow-source-registry.test.js | 1 -
tests/sidepanel-plus-payment-method.test.js | 4 +-
tests/step-definitions-module.test.js | 8 +-
20 files changed, 148 insertions(+), 850 deletions(-)
delete mode 100644 background/plus-hosted-checkout-success.js
delete mode 100644 tests/hosted-checkout-background-wiring.test.js
delete mode 100644 tests/plus-hosted-checkout-success.test.js
diff --git a/background.js b/background.js
index 0e8bd81..d826c65 100644
--- a/background.js
+++ b/background.js
@@ -41,7 +41,6 @@ importScripts(
'background/message-router.js',
'background/verification-flow.js',
'background/auto-run-controller.js',
- 'background/plus-hosted-checkout-success.js',
'background/tab-runtime.js',
'background/navigation-utils.js',
'background/logging-status.js',
@@ -858,19 +857,6 @@ function buildResolvedStepDefinitionState(state = {}) {
|| capabilityState?.effectiveSignupMethod
|| requestedSignupMethod
);
- const resolvedPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy(
- stepDefinitionOptions.plusAccountAccessStrategy
- ?? capabilityState?.effectivePlusAccountAccessStrategy
- ?? state?.plusAccountAccessStrategy
- );
- const effectivePlusAccountAccessStrategy = (
- resolvedActiveFlowId === defaultFlowId
- && plusModeEnabled
- && Boolean(state?.accountContributionEnabled)
- && resolvedSignupMethod === SIGNUP_METHOD_EMAIL
- )
- ? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION
- : resolvedPlusAccountAccessStrategy;
return {
...state,
@@ -882,7 +868,11 @@ function buildResolvedStepDefinitionState(state = {}) {
? plusModeEnabled
: Boolean(stepDefinitionOptions.plusModeEnabled),
plusPaymentMethod,
- plusAccountAccessStrategy: effectivePlusAccountAccessStrategy,
+ plusAccountAccessStrategy: normalizePlusAccountAccessStrategy(
+ stepDefinitionOptions.plusAccountAccessStrategy
+ ?? capabilityState?.effectivePlusAccountAccessStrategy
+ ?? state?.plusAccountAccessStrategy
+ ),
signupMethod: resolvedSignupMethod,
resolvedSignupMethod: resolvedSignupMethod,
phoneSignupReloginAfterBindEmailEnabled: Boolean(state?.phoneSignupReloginAfterBindEmailEnabled),
@@ -1160,7 +1150,7 @@ const PERSISTED_SETTING_DEFAULTS = {
customPassword: '',
plusModeEnabled: false,
plusPaymentMethod: DEFAULT_PLUS_PAYMENT_METHOD,
- plusAccountAccessStrategy: 'sub2api_codex_session',
+ plusAccountAccessStrategy: 'oauth',
paypalEmail: '',
paypalPassword: '',
currentPayPalAccountId: '',
@@ -10531,7 +10521,6 @@ let resumeWaiter = null;
const AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS = 120000;
const AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS = 5 * 60 * 1000;
const AUTO_RUN_STEP_IDLE_LOG_CHECK_INTERVAL_MS = 5000;
-const HOSTED_CHECKOUT_SUCCESS_SIGNAL_TIMEOUT_MS = 30 * 60 * 1000;
const AUTO_RUN_STEP_IDLE_RESTART_MAX_ATTEMPTS = 3;
const AUTO_RUN_STEP_IDLE_RESTART_ERROR_PREFIX = 'AUTO_RUN_STEP_IDLE_RESTART::';
const AUTO_RUN_BACKGROUND_COMPLETED_STEPS = new Set([1, 2, 4, 6, 7, 8, 9]);
@@ -10541,6 +10530,7 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([
'submit-signup-email',
'fetch-signup-code',
'wait-registration-success',
+ 'plus-checkout-create',
'plus-checkout-billing',
'paypal-approve',
'plus-checkout-return',
@@ -10568,7 +10558,6 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([
const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([
'fill-password',
'fill-profile',
- 'plus-checkout-create',
'gopay-subscription-confirm',
'platform-verify',
]);
@@ -10676,34 +10665,7 @@ function getAutoRunPreExecutionDelayMs(step, state = {}) {
return getAutoRunPreExecutionDelayMsForNode(getNodeIdByStepForState(step, state), state);
}
-function isHostedCheckoutSuccessCompletionNode(nodeId, state = {}) {
- const executionKey = getNodeExecutionKeyForState(nodeId, state);
- if ((executionKey || nodeId) !== 'plus-checkout-create') {
- return false;
- }
- if (!state?.plusModeEnabled) {
- return false;
- }
- const paymentMethod = String(state?.plusPaymentMethod || '').trim().toLowerCase();
- if (paymentMethod !== 'paypal') {
- return false;
- }
- const signupMethod = String(state?.resolvedSignupMethod || state?.signupMethod || 'email').trim().toLowerCase();
- if (signupMethod === 'phone') {
- return false;
- }
- if (Boolean(state?.accountContributionEnabled)) {
- return true;
- }
- const strategy = normalizePlusAccountAccessStrategy(state?.plusAccountAccessStrategy);
- return strategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION
- || strategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION;
-}
-
function getNodeCompletionSignalTimeoutMs(nodeId, state = {}) {
- if (isHostedCheckoutSuccessCompletionNode(nodeId, state)) {
- return HOSTED_CHECKOUT_SUCCESS_SIGNAL_TIMEOUT_MS;
- }
const executionKey = getNodeExecutionKeyForState(nodeId, state);
return STEP_COMPLETION_SIGNAL_TIMEOUTS_BY_STEP_KEY.get(executionKey || nodeId) || AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS;
}
@@ -10712,12 +10674,6 @@ function getStepCompletionSignalTimeoutMs(step, state = {}) {
return getNodeCompletionSignalTimeoutMs(getNodeIdByStepForState(step, state), state);
}
-function getAutoRunNodeIdleLogTimeoutMs(nodeId, state = {}) {
- return isHostedCheckoutSuccessCompletionNode(nodeId, state)
- ? HOSTED_CHECKOUT_SUCCESS_SIGNAL_TIMEOUT_MS
- : AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS;
-}
-
function notifyNodeComplete(nodeId, payload) {
const normalizedNodeId = String(nodeId || '').trim();
const waiter = nodeWaiters.get(normalizedNodeId);
@@ -11018,19 +10974,10 @@ async function runAutoNodeActionWithIdleLogWatchdog(nodeId, action, options = {}
}
async function executeNodeAndWaitWithAutoRunIdleLogWatchdog(nodeId, delayAfter = 2000, options = {}) {
- const executionState = await getState();
- const idleTimeoutMs = Number(options.idleTimeoutMs) > 0
- ? Number(options.idleTimeoutMs)
- : (typeof getAutoRunNodeIdleLogTimeoutMs === 'function'
- ? getAutoRunNodeIdleLogTimeoutMs(nodeId, executionState)
- : AUTO_RUN_STEP_IDLE_LOG_TIMEOUT_MS);
return runAutoNodeActionWithIdleLogWatchdog(
nodeId,
() => executeNodeAndWait(nodeId, delayAfter),
- {
- ...options,
- idleTimeoutMs,
- }
+ options
);
}
@@ -13494,16 +13441,6 @@ const plusReturnConfirmExecutor = self.MultiPageBackgroundPlusReturnConfirm?.cre
sleepWithStop,
waitForTabUrlMatchUntilStopped,
});
-const plusHostedCheckoutSuccessManager = self.MultiPagePlusHostedCheckoutSuccess?.createPlusHostedCheckoutSuccessManager({
- addLog,
- completeNodeFromBackground,
- failNodeFromBackground: async (nodeId, message) => {
- notifyNodeError(nodeId, message);
- await finalizeDeferredNodeExecutionError(nodeId, new Error(message));
- },
- getState,
- setState,
-});
const sub2ApiSessionImportExecutor = self.MultiPageBackgroundSub2ApiSessionImport?.createSub2ApiSessionImportExecutor({
addLog,
chrome,
@@ -15604,12 +15541,6 @@ chrome.alarms.onAlarm.addListener((alarm) => {
}
});
-chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
- plusHostedCheckoutSuccessManager?.handleTabUpdated(tabId, changeInfo, tab).catch((err) => {
- console.error(LOG_PREFIX, 'Failed to process PayPal hosted checkout success page:', err);
- });
-});
-
chrome.runtime.onStartup.addListener(() => {
migrateLegacyAccountContributionState().catch((err) => {
console.error(LOG_PREFIX, 'Failed to migrate legacy account contribution state on startup:', err);
diff --git a/background/plus-hosted-checkout-success.js b/background/plus-hosted-checkout-success.js
deleted file mode 100644
index 8bdb2e9..0000000
--- a/background/plus-hosted-checkout-success.js
+++ /dev/null
@@ -1,153 +0,0 @@
-(function attachPlusHostedCheckoutSuccess(root, factory) {
- root.MultiPagePlusHostedCheckoutSuccess = factory();
-})(typeof self !== 'undefined' ? self : globalThis, function createPlusHostedCheckoutSuccessModule() {
- const PAYMENT_SUCCESS_URL_PATTERN = /^https:\/\/(?:chatgpt\.com|www\.chatgpt\.com|chat\.openai\.com)\/(?:backend-api\/)?payments\/success(?:[/?#]|$)/i;
- const PLUS_CHECKOUT_NODE_ID = 'plus-checkout-create';
- const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
-
- function normalizeString(value = '') {
- return String(value || '').trim();
- }
-
- function normalizePaymentMethod(value = '') {
- return normalizeString(value).toLowerCase();
- }
-
- function isPaymentSuccessUrl(url = '') {
- return PAYMENT_SUCCESS_URL_PATTERN.test(normalizeString(url));
- }
-
- function isRunnableNodeStatus(value = '') {
- const normalized = normalizeString(value).toLowerCase();
- return !normalized || normalized === 'pending' || normalized === 'running';
- }
-
- function isHostedCheckoutSuccessWaitActive(state = {}, tabId = null) {
- if (!state || typeof state !== 'object') {
- return false;
- }
- if (!state.plusModeEnabled) {
- return false;
- }
- if (normalizePaymentMethod(state.plusPaymentMethod) !== PLUS_PAYMENT_METHOD_PAYPAL) {
- return false;
- }
- if (state.plusHostedCheckoutCompletionPending !== true) {
- return false;
- }
-
- const checkoutTabId = Number(state.plusCheckoutTabId);
- if (!Number.isInteger(checkoutTabId) || checkoutTabId <= 0) {
- return false;
- }
- if (tabId !== null && checkoutTabId !== Number(tabId)) {
- return false;
- }
-
- return isRunnableNodeStatus(state.nodeStatuses?.[PLUS_CHECKOUT_NODE_ID]);
- }
-
- function createPlusHostedCheckoutSuccessManager(deps = {}) {
- const {
- addLog: rawAddLog = async () => {},
- completeNodeFromBackground = null,
- failNodeFromBackground = null,
- getState = async () => ({}),
- setState = async () => {},
- } = deps;
-
- const activeTabIds = new Set();
-
- function addLog(message, level = 'info', options = {}) {
- return rawAddLog(message, level, {
- stepKey: PLUS_CHECKOUT_NODE_ID,
- nodeId: PLUS_CHECKOUT_NODE_ID,
- ...(options && typeof options === 'object' ? options : {}),
- });
- }
-
- async function completeFromSuccessTab(tabId, successUrl = '') {
- const numericTabId = Number(tabId);
- if (!Number.isInteger(numericTabId) || numericTabId <= 0) {
- return null;
- }
- if (activeTabIds.has(numericTabId)) {
- return null;
- }
- activeTabIds.add(numericTabId);
-
- try {
- const initialState = await getState();
- if (!isHostedCheckoutSuccessWaitActive(initialState, numericTabId)) {
- return null;
- }
-
- const latestState = await getState();
- if (!isHostedCheckoutSuccessWaitActive(latestState, numericTabId)) {
- return null;
- }
-
- const normalizedSuccessUrl = normalizeString(successUrl);
- await setState({
- plusReturnUrl: normalizedSuccessUrl,
- });
- await addLog('Detected ChatGPT payment success page; continuing Plus session import flow.', 'ok');
-
- if (typeof completeNodeFromBackground === 'function') {
- await completeNodeFromBackground(PLUS_CHECKOUT_NODE_ID, {
- plusReturnUrl: normalizedSuccessUrl,
- plusHostedCheckoutCompleted: true,
- });
- }
-
- await setState({
- plusHostedCheckoutCompletionPending: false,
- plusHostedCheckoutCompleted: true,
- });
-
- return {
- completed: true,
- plusReturnUrl: normalizedSuccessUrl,
- };
- } catch (error) {
- const message = normalizeString(error?.message) || 'unknown error';
- await addLog(`Failed to continue after ChatGPT payment success page: ${message}`, 'error');
- if (typeof failNodeFromBackground === 'function') {
- await failNodeFromBackground(PLUS_CHECKOUT_NODE_ID, message);
- return {
- completed: false,
- failed: true,
- message,
- };
- }
- throw error;
- } finally {
- activeTabIds.delete(numericTabId);
- }
- }
-
- async function handleTabUpdated(tabId, changeInfo = {}, tab = {}) {
- if (changeInfo?.status !== 'complete' && tab?.status !== 'complete') {
- return null;
- }
- const nextUrl = normalizeString(changeInfo?.url || tab?.url);
- if (!isPaymentSuccessUrl(nextUrl)) {
- return null;
- }
- return completeFromSuccessTab(tabId, nextUrl);
- }
-
- return {
- completeFromSuccessTab,
- handleTabUpdated,
- isHostedCheckoutSuccessWaitActive,
- isPaymentSuccessUrl,
- };
- }
-
- return {
- createPlusHostedCheckoutSuccessManager,
- isHostedCheckoutSuccessWaitActive,
- isPaymentSuccessUrl,
- };
-});
diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js
index f4a08fd..3e5fb30 100644
--- a/background/steps/create-plus-checkout.js
+++ b/background/steps/create-plus-checkout.js
@@ -7,8 +7,6 @@
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
- const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
- const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
@@ -49,17 +47,6 @@
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
}
- 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 'oauth';
- }
-
function getCheckoutModeLabel(state = {}) {
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
@@ -76,25 +63,6 @@
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay' : 'PayPal';
}
- function shouldWaitForHostedCheckoutSuccess(state = {}, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
- if (!state?.plusModeEnabled) {
- return false;
- }
- if (normalizePlusPaymentMethod(paymentMethod) !== PLUS_PAYMENT_METHOD_PAYPAL) {
- return false;
- }
- const signupMethod = String(state?.resolvedSignupMethod || state?.signupMethod || 'email').trim().toLowerCase();
- if (signupMethod === 'phone') {
- return false;
- }
- if (Boolean(state?.accountContributionEnabled)) {
- return true;
- }
- const strategy = normalizePlusAccountAccessStrategy(state?.plusAccountAccessStrategy);
- return strategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION
- || strategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION;
- }
-
async function openFreshChatGptTabForCheckoutCreate() {
const tab = typeof createAutomationTab === 'function'
? await createAutomationTab({ url: PLUS_CHECKOUT_ENTRY_URL, active: true })
@@ -471,8 +439,6 @@
plusCheckoutCountry: result.country || 'ID',
plusCheckoutCurrency: result.currency || 'IDR',
plusCheckoutSource: result.checkoutSource,
- plusHostedCheckoutCompletionPending: false,
- plusHostedCheckoutCompleted: false,
gopayHelperTaskId: result.taskId,
gopayHelperTaskStatus: result.taskStatus,
gopayHelperStatusText: result.statusText,
@@ -518,14 +484,10 @@
logMessage: '步骤 6:正在等待 ChatGPT 页面完成加载,再继续创建订阅页...',
});
- const waitForHostedSuccess = shouldWaitForHostedCheckoutSuccess(state, paymentMethod);
const result = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
type: 'CREATE_PLUS_CHECKOUT',
source: 'background',
- payload: {
- paymentMethod,
- ...(waitForHostedSuccess ? { hostedCheckoutMode: true } : {}),
- },
+ payload: { paymentMethod },
});
if (result?.error) {
@@ -534,10 +496,9 @@
if (!result?.checkoutUrl) {
throw new Error(`步骤 6:${checkoutModeLabel}未返回可用的订阅链接。`);
}
- const checkoutUrl = String(result.checkoutUrl || '').trim();
await addLog(`步骤 6:${checkoutModeLabel}已创建,正在打开订阅页面...`, 'ok');
- await chrome.tabs.update(tabId, { url: checkoutUrl, active: true });
+ await chrome.tabs.update(tabId, { url: result.checkoutUrl, active: true });
await waitForTabCompleteUntilStopped(tabId);
await sleepWithStop(1000);
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
@@ -548,22 +509,14 @@
await setState({
plusCheckoutTabId: tabId,
- plusCheckoutUrl: checkoutUrl,
+ plusCheckoutUrl: result.checkoutUrl,
plusCheckoutCountry: result.country || 'DE',
plusCheckoutCurrency: result.currency || 'EUR',
plusCheckoutSource: '',
- plusReturnUrl: '',
- plusHostedCheckoutCompletionPending: waitForHostedSuccess,
- plusHostedCheckoutCompleted: false,
});
await addLog(`步骤 6:Plus Checkout 页面已就绪(${paymentMethodLabel} / ${result.country || 'DE'} ${result.currency || 'EUR'}),准备继续下一步。`, 'info');
- if (waitForHostedSuccess) {
- await addLog('Step 6: PayPal hosted checkout is open; waiting for the ChatGPT payment success page before importing the Plus session.', 'info');
- return;
- }
-
await completeNodeFromBackground('plus-checkout-create', {
plusCheckoutCountry: result.country || 'DE',
plusCheckoutCurrency: result.currency || 'EUR',
diff --git a/content/plus-checkout.js b/content/plus-checkout.js
index bc5891e..b3f7f2a 100644
--- a/content/plus-checkout.js
+++ b/content/plus-checkout.js
@@ -616,12 +616,10 @@ function writeGoPayDiagnostics(reason, level = 'info') {
return writePaymentMethodDiagnostics(PLUS_PAYMENT_METHOD_GOPAY, reason, level);
}
-function buildPlusCheckoutPayload(paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL, options = {}) {
+function buildPlusCheckoutPayload(paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
const config = getPaymentMethodConfig(paymentMethod);
- const hostedCheckoutMode = config.id === PLUS_PAYMENT_METHOD_PAYPAL && Boolean(options.hostedCheckoutMode);
return {
...JSON.parse(JSON.stringify(PLUS_CHECKOUT_PAYLOAD_BASE)),
- checkout_ui_mode: hostedCheckoutMode ? 'hosted' : 'custom',
billing_details: {
...config.billingDetails,
},
@@ -637,31 +635,6 @@ function buildPlusCheckoutUrl(checkoutSessionId, paymentMethod = PLUS_PAYMENT_ME
return `https://chatgpt.com/checkout/${config.checkoutMerchantPath}/${sessionId}`;
}
-function findHostedCheckoutUrl(payload = {}) {
- const queue = [payload];
- const seen = new Set();
- while (queue.length) {
- const current = queue.shift();
- if (!current || typeof current !== 'object' || seen.has(current)) {
- continue;
- }
- seen.add(current);
-
- const values = Array.isArray(current) ? current : Object.values(current);
- for (const value of values) {
- if (typeof value === 'string') {
- const candidate = value.trim();
- if (/^https:\/\/(?:pay\.openai\.com|checkout\.stripe\.com)\/c\/pay\//i.test(candidate)) {
- return candidate;
- }
- } else if (value && typeof value === 'object') {
- queue.push(value);
- }
- }
- }
- return '';
-}
-
async function createPlusCheckoutSession(options = {}) {
await waitForDocumentComplete();
log('Plus:正在读取 ChatGPT 登录会话...');
@@ -677,8 +650,7 @@ async function createPlusCheckoutSession(options = {}) {
log('Plus:正在创建 checkout 会话...');
const paymentMethod = normalizePlusPaymentMethod(options.paymentMethod);
- const hostedCheckoutMode = Boolean(options.hostedCheckoutMode);
- const checkoutPayload = buildPlusCheckoutPayload(paymentMethod, { hostedCheckoutMode });
+ const checkoutPayload = buildPlusCheckoutPayload(paymentMethod);
const response = await fetch('https://chatgpt.com/backend-api/payments/checkout', {
method: 'POST',
credentials: 'include',
@@ -694,16 +666,9 @@ async function createPlusCheckoutSession(options = {}) {
const detail = data?.detail || data?.message || `HTTP ${response.status}`;
throw new Error(`创建 Plus Checkout 失败:${detail}`);
}
- const hostedCheckoutUrl = findHostedCheckoutUrl(data);
- const fallbackCheckoutUrl = buildPlusCheckoutUrl(data.checkout_session_id, paymentMethod);
- const checkoutUrl = hostedCheckoutMode && paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL && hostedCheckoutUrl
- ? hostedCheckoutUrl
- : fallbackCheckoutUrl;
return {
- checkoutUrl,
- hostedCheckoutUrl,
- fallbackCheckoutUrl,
+ checkoutUrl: buildPlusCheckoutUrl(data.checkout_session_id, paymentMethod),
country: checkoutPayload.billing_details.country,
currency: checkoutPayload.billing_details.currency,
};
diff --git a/data/step-definitions.js b/data/step-definitions.js
index 3db0ac7..27b0a0a 100644
--- a/data/step-definitions.js
+++ b/data/step-definitions.js
@@ -32,7 +32,6 @@
{ id: 8, order: 80, key: 'paypal-approve', title: 'PayPal 登录与授权', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-approve' },
{ id: 9, order: 90, key: 'plus-checkout-return', title: '订阅回跳确认', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-return' },
];
- const PLUS_PAYPAL_SESSION_PREFIX_STEP_DEFINITIONS = PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS.slice(0, 6);
const PLUS_GOPAY_PREFIX_STEP_DEFINITIONS = [
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网', sourceId: 'chatgpt', driverId: null, command: 'open-chatgpt' },
@@ -169,16 +168,16 @@
const NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true });
const PLUS_PAYPAL_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL);
const PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS = createOpenAiSteps(
- PLUS_PAYPAL_SESSION_PREFIX_STEP_DEFINITIONS,
- 7,
- 70,
+ PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS,
+ 10,
+ 100,
SIGNUP_METHOD_EMAIL,
{ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION }
);
const PLUS_PAYPAL_CPA_SESSION_STEP_DEFINITIONS = createOpenAiSteps(
- PLUS_PAYPAL_SESSION_PREFIX_STEP_DEFINITIONS,
- 7,
- 70,
+ PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS,
+ 10,
+ 100,
SIGNUP_METHOD_EMAIL,
{ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION }
);
diff --git a/shared/flow-capabilities.js b/shared/flow-capabilities.js
index 9c7eadf..d5f430f 100644
--- a/shared/flow-capabilities.js
+++ b/shared/flow-capabilities.js
@@ -12,11 +12,6 @@
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 OPENAI_PLUS_ACCOUNT_ACCESS_STRATEGIES = Object.freeze([
- PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH,
- PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION,
- PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION,
- ]);
const VALID_OPENAI_TARGET_IDS = Array.isArray(flowRegistryApi.OPENAI_TARGET_IDS)
? flowRegistryApi.OPENAI_TARGET_IDS.slice()
: ['cpa', 'sub2api', 'codex2api'];
@@ -383,27 +378,24 @@
const requestedPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy(
options?.plusAccountAccessStrategy ?? state?.plusAccountAccessStrategy
);
- const canUsePlusAccountAccessStrategies = activeFlowId === 'openai'
+ const availablePlusAccountAccessStrategies = activeFlowId === 'openai'
&& Boolean(flowState.supportsPlusMode)
&& Boolean(runtimeLocks.plusModeEnabled)
- && effectiveSignupMethod === SIGNUP_METHOD_EMAIL;
- const forceSub2ApiSessionImportForContribution = canUsePlusAccountAccessStrategies
- && Boolean(runtimeLocks.accountContribution);
- const availablePlusAccountAccessStrategies = canUsePlusAccountAccessStrategies
- ? (forceSub2ApiSessionImportForContribution
- ? [PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION]
- : OPENAI_PLUS_ACCOUNT_ACCESS_STRATEGIES.slice())
+ && effectiveSignupMethod === SIGNUP_METHOD_EMAIL
+ ? (
+ Array.isArray(targetState.supportedPlusAccountAccessStrategies)
+ && targetState.supportedPlusAccountAccessStrategies.length > 0
+ ? targetState.supportedPlusAccountAccessStrategies.slice()
+ : [PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH]
+ )
: [PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH];
- const effectivePlusAccountAccessStrategy = forceSub2ApiSessionImportForContribution
- ? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION
- : (availablePlusAccountAccessStrategies.includes(requestedPlusAccountAccessStrategy)
- ? requestedPlusAccountAccessStrategy
- : PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH);
+ const effectivePlusAccountAccessStrategy = availablePlusAccountAccessStrategies.includes(requestedPlusAccountAccessStrategy)
+ ? requestedPlusAccountAccessStrategy
+ : PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const canEditPlusAccountAccessStrategy = activeFlowId === 'openai'
&& Boolean(flowState.supportsPlusMode)
&& Boolean(runtimeLocks.plusModeEnabled)
&& effectiveSignupMethod === SIGNUP_METHOD_EMAIL
- && !forceSub2ApiSessionImportForContribution
&& availablePlusAccountAccessStrategies.length > 1;
const visibleGroupIds = typeof flowRegistryApi.getVisibleGroupIds === 'function'
&& isRegisteredFlowId(activeFlowId)
diff --git a/shared/settings-schema.js b/shared/settings-schema.js
index d9a7070..2f39824 100644
--- a/shared/settings-schema.js
+++ b/shared/settings-schema.js
@@ -106,7 +106,7 @@
plus: {
plusModeEnabled: false,
plusPaymentMethod: 'paypal',
- plusAccountAccessStrategy: 'sub2api_codex_session',
+ plusAccountAccessStrategy: 'oauth',
},
autoRun: {
stepExecutionRange: {
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 39c82c2..87d721b 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -320,7 +320,7 @@
- Plus 模式未启用
+ 当前来源仅支持 OAuth
diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js
index 0fe3a03..eec0060 100644
--- a/sidepanel/sidepanel.js
+++ b/sidepanel/sidepanel.js
@@ -555,7 +555,7 @@ const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
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 DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION;
+const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
@@ -9312,8 +9312,31 @@ function updatePlusModeUI() {
selectPlusAccountAccessStrategy.setAttribute('aria-disabled', String(selectPlusAccountAccessStrategy.disabled));
}
if (typeof plusAccountAccessStrategyCaption !== 'undefined' && plusAccountAccessStrategyCaption) {
- plusAccountAccessStrategyCaption.textContent = !enabled
- ? 'Plus 模式未启用'
+ if (!enabled) {
+ plusAccountAccessStrategyCaption.textContent = '当前来源仅支持 OAuth';
+ } else if (!canEditPlusAccountAccessStrategy) {
+ plusAccountAccessStrategyCaption.textContent = '当前来源仅支持 OAuth';
+ } else if (effectivePlusAccountAccessStrategy === sub2apiSessionStrategyValue) {
+ plusAccountAccessStrategyCaption.textContent = '复用当前 Plus 已登录会话,直接导入到 SUB2API';
+ } else if (effectivePlusAccountAccessStrategy === oauthStrategyValue) {
+ plusAccountAccessStrategyCaption.textContent = '通过 OAuth 回调创建 SUB2API 账号';
+ } else {
+ plusAccountAccessStrategyCaption.textContent = '当前来源仅支持 OAuth';
+ }
+ }
+ if (typeof plusAccountAccessStrategyCaption !== 'undefined' && plusAccountAccessStrategyCaption) {
+ if (!enabled || !canEditPlusAccountAccessStrategy) {
+ plusAccountAccessStrategyCaption.textContent = '当前来源仅支持 OAuth';
+ } else {
+ plusAccountAccessStrategyCaption.textContent = describePlusAccountAccessStrategy(
+ effectivePlusAccountAccessStrategy,
+ effectiveTargetId
+ );
+ }
+ }
+ if (typeof plusAccountAccessStrategyCaption !== 'undefined' && plusAccountAccessStrategyCaption) {
+ plusAccountAccessStrategyCaption.textContent = !enabled || !canEditPlusAccountAccessStrategy
+ ? '当前来源仅支持 OAuth'
: describePlusAccountAccessStrategy(
effectivePlusAccountAccessStrategy,
effectiveTargetId
diff --git a/tests/background-effective-plus-account-access-strategy.test.js b/tests/background-effective-plus-account-access-strategy.test.js
index 2b837e5..aae5b48 100644
--- a/tests/background-effective-plus-account-access-strategy.test.js
+++ b/tests/background-effective-plus-account-access-strategy.test.js
@@ -55,8 +55,6 @@ 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: {
@@ -122,10 +120,13 @@ 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 = state.accountContributionEnabled && state.plusModeEnabled
- ? 'sub2api_codex_session'
- : requestedStrategy;
+ const effectiveStrategy = normalizedPanelMode === 'sub2api'
+ ? (requestedStrategy === 'sub2api_codex_session' ? 'sub2api_codex_session' : 'oauth')
+ : (normalizedPanelMode === 'cpa'
+ ? (requestedStrategy === 'cpa_codex_session' ? 'cpa_codex_session' : 'oauth')
+ : 'oauth');
return {
effectivePanelMode: options.panelMode,
effectivePlusAccountAccessStrategy: effectiveStrategy,
@@ -152,12 +153,12 @@ return {
`)();
}
-test('background step resolution keeps SUB2API session tail independent from the current panel mode', () => {
+test('background step resolution keeps SUB2API session tail only when the effective Plus target supports it', () => {
const api = createHarness();
const state = {
activeFlowId: 'openai',
flowId: 'openai',
- panelMode: 'cpa',
+ panelMode: 'sub2api',
plusModeEnabled: true,
plusPaymentMethod: 'paypal',
plusAccountAccessStrategy: 'sub2api_codex_session',
@@ -204,7 +205,7 @@ test('background step resolution keeps CPA session tail when the effective Plus
]);
});
-test('background step resolution keeps SUB2API session tail even when the current panel mode is CPA', () => {
+test('background step resolution falls back to OAuth tail when the requested session strategy is not effective for the current panel mode', () => {
const api = createHarness();
const state = {
activeFlowId: 'openai',
@@ -219,34 +220,12 @@ test('background step resolution keeps SUB2API session tail even when the curren
const resolvedState = api.buildResolvedStepDefinitionState(state);
const nodeIds = api.getNodeIdsForState(state);
- 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',
+ 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',
]);
});
-
-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');
-});
diff --git a/tests/background-message-router-module.test.js b/tests/background-message-router-module.test.js
index a9938fe..cc0908e 100644
--- a/tests/background-message-router-module.test.js
+++ b/tests/background-message-router-module.test.js
@@ -453,7 +453,7 @@ test('SAVE_SETTING rebuilds Plus node statuses when the account access strategy
});
});
-test('SAVE_SETTING rebuilds Plus node statuses without forcing the Plus strategy back to OAuth', async () => {
+test('SAVE_SETTING rebuilds Plus node statuses when panel mode forces the effective 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,7 +491,8 @@ test('SAVE_SETTING rebuilds Plus node statuses without forcing the Plus strategy
: {},
broadcastDataUpdate: (payload) => broadcasts.push(payload),
getNodeIdsForState: (nextState = {}) => (
- String(nextState.plusAccountAccessStrategy || '').trim() === 'sub2api_codex_session'
+ String(nextState.panelMode || '').trim() === 'sub2api'
+ && String(nextState.plusAccountAccessStrategy || '').trim() === 'sub2api_codex_session'
? [
'open-chatgpt',
'plus-checkout-create',
@@ -531,24 +532,61 @@ test('SAVE_SETTING rebuilds Plus node statuses without forcing the Plus strategy
assert.equal(response.ok, true);
assert.equal(state.panelMode, 'cpa');
assert.equal(state.plusAccountAccessStrategy, 'sub2api_codex_session');
- 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.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.deepStrictEqual(state.nodeStatuses, {
- 'open-chatgpt': 'completed',
- 'plus-checkout-create': 'completed',
- 'plus-checkout-billing': 'completed',
- 'paypal-approve': 'completed',
- 'plus-checkout-return': 'completed',
- 'sub2api-session-import': 'running',
+ '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',
});
- assert.equal(Object.prototype.hasOwnProperty.call(state.nodeStatuses, 'oauth-login'), false);
+ assert.equal(Object.prototype.hasOwnProperty.call(state.nodeStatuses, 'sub2api-session-import'), 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: '',
});
});
diff --git a/tests/flow-capabilities-module.test.js b/tests/flow-capabilities-module.test.js
index 3f3b729..0f9befe 100644
--- a/tests/flow-capabilities-module.test.js
+++ b/tests/flow-capabilities-module.test.js
@@ -212,7 +212,7 @@ test('flow capability registry exposes editable Plus account access strategies f
assert.deepEqual(
capabilityState.availablePlusAccountAccessStrategies,
- ['oauth', 'cpa_codex_session', 'sub2api_codex_session']
+ ['oauth', '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', 'sub2api_codex_session']
+ ['oauth', 'cpa_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 keeps Plus session strategies independent from the current OpenAI target', () => {
+test('flow capability registry forces OAuth when the current target only supports OAuth', () => {
const api = loadApi();
const registry = api.createFlowCapabilityRegistry();
@@ -254,38 +254,13 @@ test('flow capability registry keeps Plus session strategies independent from th
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, ['sub2api_codex_session']);
+ assert.deepEqual(capabilityState.availablePlusAccountAccessStrategies, ['oauth']);
assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'cpa_codex_session');
- assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'sub2api_codex_session');
+ assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'oauth');
assert.equal(capabilityState.canEditPlusAccountAccessStrategy, false);
- assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'sub2api_codex_session');
+ assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'oauth');
});
diff --git a/tests/hosted-checkout-background-wiring.test.js b/tests/hosted-checkout-background-wiring.test.js
deleted file mode 100644
index d230a67..0000000
--- a/tests/hosted-checkout-background-wiring.test.js
+++ /dev/null
@@ -1,120 +0,0 @@
-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/);
-});
diff --git a/tests/plus-account-access-strategy.test.js b/tests/plus-account-access-strategy.test.js
index 7d8966f..3998edc 100644
--- a/tests/plus-account-access-strategy.test.js
+++ b/tests/plus-account-access-strategy.test.js
@@ -115,14 +115,13 @@ return {
`)();
}
-test('sidepanel keeps requested SUB2API session strategy independent from the selected target', () => {
+test('sidepanel keeps requested Plus account strategy while OAuth-only targets force the effective value', () => {
const api = buildHarness(
`{
canShowPlusSettings: true,
runtimeLocks: { plusModeEnabled: true },
- canEditPlusAccountAccessStrategy: true,
- availablePlusAccountAccessStrategies: ['oauth', 'cpa_codex_session', 'sub2api_codex_session'],
- effectivePlusAccountAccessStrategy: 'sub2api_codex_session',
+ canEditPlusAccountAccessStrategy: false,
+ effectivePlusAccountAccessStrategy: 'oauth',
}`,
`{
activeFlowId: 'openai',
@@ -135,11 +134,11 @@ test('sidepanel keeps requested SUB2API session strategy independent from the se
api.updatePlusModeUI();
assert.equal(api.rowPlusAccountAccessStrategy.style.display, '');
- assert.equal(api.selectPlusAccountAccessStrategy.disabled, false);
+ assert.equal(api.selectPlusAccountAccessStrategy.disabled, true);
assert.equal(api.selectPlusAccountAccessStrategy.dataset.requestedValue, 'sub2api_codex_session');
- assert.equal(api.selectPlusAccountAccessStrategy.value, 'sub2api_codex_session');
+ assert.equal(api.selectPlusAccountAccessStrategy.value, 'oauth');
assert.equal(api.getRequestedPlusAccountAccessStrategy(), 'sub2api_codex_session');
- assert.match(api.plusAccountAccessStrategyCaption.textContent, /SUB2API/);
+ assert.match(api.plusAccountAccessStrategyCaption.textContent, /OAuth/);
});
test('sidepanel enables SUB2API session strategy selection when the current Plus target supports it', () => {
@@ -284,7 +283,6 @@ return {
plusAccountAccessStrategy: 'sub2api_codex_session',
signupMethod: 'email',
phoneSignupReloginAfterBindEmailEnabled: false,
- accountContributionEnabled: false,
},
},
{
@@ -296,7 +294,6 @@ return {
plusAccountAccessStrategy: 'sub2api_codex_session',
signupMethod: 'email',
phoneSignupReloginAfterBindEmailEnabled: false,
- accountContributionEnabled: false,
},
},
]);
diff --git a/tests/plus-checkout-address-input.test.js b/tests/plus-checkout-address-input.test.js
index 377d71f..738aa1e 100644
--- a/tests/plus-checkout-address-input.test.js
+++ b/tests/plus-checkout-address-input.test.js
@@ -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', checkoutResponse = null } = {}) {
+function createPlusCheckoutMessageHarness({ checkoutSessionId = 'cs_test_123' } = {}) {
const attrs = new Map();
let listener = null;
const fetchCalls = [];
@@ -84,7 +84,7 @@ function createPlusCheckoutMessageHarness({ checkoutSessionId = 'cs_test_123', c
return {
ok: true,
status: 200,
- json: async () => checkoutResponse || ({ checkout_session_id: checkoutSessionId }),
+ json: async () => ({ checkout_session_id: checkoutSessionId }),
};
}
throw new Error(`unexpected fetch url: ${url}`);
@@ -124,58 +124,9 @@ 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' });
diff --git a/tests/plus-checkout-create-wait.test.js b/tests/plus-checkout-create-wait.test.js
index 4bd2ff5..15ee2e0 100644
--- a/tests/plus-checkout-create-wait.test.js
+++ b/tests/plus-checkout-create-wait.test.js
@@ -257,102 +257,6 @@ 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({
diff --git a/tests/plus-hosted-checkout-success.test.js b/tests/plus-hosted-checkout-success.test.js
deleted file mode 100644
index 9a51b6a..0000000
--- a/tests/plus-hosted-checkout-success.test.js
+++ /dev/null
@@ -1,135 +0,0 @@
-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);
-});
diff --git a/tests/sidepanel-flow-source-registry.test.js b/tests/sidepanel-flow-source-registry.test.js
index 397d1ae..58ab6ad 100644
--- a/tests/sidepanel-flow-source-registry.test.js
+++ b/tests/sidepanel-flow-source-registry.test.js
@@ -123,7 +123,6 @@ return {
plusAccountAccessStrategy: 'oauth',
signupMethod: 'email',
phoneSignupReloginAfterBindEmailEnabled: false,
- accountContributionEnabled: false,
},
});
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [88] });
diff --git a/tests/sidepanel-plus-payment-method.test.js b/tests/sidepanel-plus-payment-method.test.js
index 027902e..b6560ee 100644
--- a/tests/sidepanel-plus-payment-method.test.js
+++ b/tests/sidepanel-plus-payment-method.test.js
@@ -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, accountContributionEnabled: false },
+ options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: 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, accountContributionEnabled: false },
+ options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
});
});
diff --git a/tests/step-definitions-module.test.js b/tests/step-definitions-module.test.js
index 1271b8c..9dfb329 100644
--- a/tests/step-definitions-module.test.js
+++ b/tests/step-definitions-module.test.js
@@ -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-create',
- expectedStepIds: [1, 2, 3, 4, 5, 6, 7],
+ previousNodeId: 'plus-checkout-return',
+ expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
},
{
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-create',
- expectedStepIds: [1, 2, 3, 4, 5, 6, 7],
+ previousNodeId: 'plus-checkout-return',
+ expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
},
{
label: 'gopay',
From 72c578b9b499bc1117e2f89388a2f8a8a66270aa Mon Sep 17 00:00:00 2001
From: QLHazyCoder <2825305047@qq.com>
Date: Wed, 20 May 2026 09:03:40 +0800
Subject: [PATCH 3/8] Add independent PayPal hosted binding mode
---
background.js | 89 +++-
background/steps/create-plus-checkout.js | 471 +++++++++++++++++-
content/paypal-flow.js | 310 ++++++++++++
content/plus-checkout.js | 208 +++++++-
data/step-definitions.js | 93 ++++
gopay-utils.js | 5 +
shared/flow-capabilities.js | 21 +-
shared/settings-schema.js | 26 +-
sidepanel/sidepanel.html | 17 +
sidepanel/sidepanel.js | 126 ++++-
...ackground-account-history-settings.test.js | 1 +
tests/flow-capabilities-module.test.js | 39 +-
tests/gopay-utils.test.js | 2 +
tests/plus-account-access-strategy.test.js | 20 +-
tests/plus-checkout-address-input.test.js | 38 +-
tests/plus-checkout-create-wait.test.js | 107 ++++
tests/sidepanel-flow-source-registry.test.js | 1 +
tests/sidepanel-plus-payment-method.test.js | 65 ++-
tests/step-definitions-module.test.js | 42 ++
19 files changed, 1621 insertions(+), 60 deletions(-)
diff --git a/background.js b/background.js
index d826c65..dc315cd 100644
--- a/background.js
+++ b/background.js
@@ -128,6 +128,36 @@ const PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = self.MultiPageSte
signupMethod: 'phone',
phoneSignupReloginAfterBindEmailEnabled: true,
}) || PLUS_PAYPAL_PHONE_STEP_DEFINITIONS;
+const PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
+ activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal-hosted',
+}) || PLUS_PAYPAL_STEP_DEFINITIONS;
+const PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
+ activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal-hosted',
+ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION,
+}) || PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS;
+const PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
+ activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal-hosted',
+ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION,
+}) || PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS;
+const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
+ activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal-hosted',
+ signupMethod: 'phone',
+}) || PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS;
+const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
+ activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
+ plusModeEnabled: true,
+ plusPaymentMethod: 'paypal-hosted',
+ signupMethod: 'phone',
+ phoneSignupReloginAfterBindEmailEnabled: true,
+}) || PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS;
const PLUS_GOPAY_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
plusModeEnabled: true,
@@ -214,6 +244,11 @@ const ALL_STEP_DEFINITIONS = (() => {
...PLUS_PAYPAL_CPA_SESSION_STEP_DEFINITIONS,
...PLUS_PAYPAL_PHONE_STEP_DEFINITIONS,
...PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
+ ...PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS,
+ ...PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS,
+ ...PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS,
+ ...PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS,
+ ...PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
...PLUS_GOPAY_STEP_DEFINITIONS,
...PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS,
...PLUS_GOPAY_CPA_SESSION_STEP_DEFINITIONS,
@@ -243,6 +278,10 @@ const PLUS_PAYPAL_STEP_IDS = PLUS_PAYPAL_STEP_DEFINITIONS
.map((definition) => Number(definition?.id))
.filter(Number.isFinite)
.sort((left, right) => left - right);
+const PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_IDS = PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS
+ .map((definition) => Number(definition?.id))
+ .filter(Number.isFinite)
+ .sort((left, right) => left - right);
const PLUS_GOPAY_STEP_IDS = PLUS_GOPAY_STEP_DEFINITIONS
.map((definition) => Number(definition?.id))
.filter(Number.isFinite)
@@ -255,6 +294,7 @@ const PLUS_STEP_IDS = PLUS_PAYPAL_STEP_IDS;
const LAST_STEP_ID = Math.max(
NORMAL_STEP_IDS[NORMAL_STEP_IDS.length - 1] || 10,
PLUS_PAYPAL_STEP_IDS[PLUS_PAYPAL_STEP_IDS.length - 1] || 10,
+ PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_IDS[PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_IDS.length - 1] || 10,
PLUS_GOPAY_STEP_IDS[PLUS_GOPAY_STEP_IDS.length - 1] || 10,
PLUS_GPC_STEP_IDS[PLUS_GPC_STEP_IDS.length - 1] || 10
);
@@ -681,9 +721,11 @@ const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze([
]);
const FIVE_SIM_OPERATOR = DEFAULT_FIVE_SIM_OPERATOR;
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
+const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
-const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
+const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL_HOSTED;
+const DEFAULT_PLUS_HOSTED_CHECKOUT_OAUTH_DELAY_SECONDS = 3;
const DISPLAY_TIMEZONE = 'Asia/Shanghai';
const MICROSOFT_TOKEN_DNR_RULE_ID = 1001;
const PERSISTENT_ALIAS_STATE_KEYS = [
@@ -781,6 +823,12 @@ function isPlusModeState(state = {}) {
function normalizePlusPaymentMethod(value = '') {
const normalized = String(value || '').trim().toLowerCase();
+ const paypalHostedValue = typeof PLUS_PAYMENT_METHOD_PAYPAL_HOSTED !== 'undefined'
+ ? PLUS_PAYMENT_METHOD_PAYPAL_HOSTED
+ : 'paypal-hosted';
+ if (normalized === paypalHostedValue || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
+ return paypalHostedValue;
+ }
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_PAYMENT_METHOD_GPC_HELPER;
}
@@ -937,6 +985,20 @@ function getStepDefinitionsForState(state = {}) {
}
return PLUS_GOPAY_STEP_DEFINITIONS;
}
+ if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) {
+ if (signupMethod === SIGNUP_METHOD_PHONE) {
+ return Boolean(resolvedState?.phoneSignupReloginAfterBindEmailEnabled)
+ ? PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS
+ : PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS;
+ }
+ if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
+ return PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS;
+ }
+ if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) {
+ return PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS;
+ }
+ return PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS;
+ }
if (
signupMethod === SIGNUP_METHOD_EMAIL
&& plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION
@@ -967,6 +1029,9 @@ function getStepIdsForState(state = {}) {
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_GPC_STEP_IDS;
}
+ if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) {
+ return PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_IDS;
+ }
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_IDS : PLUS_PAYPAL_STEP_IDS;
}
@@ -1151,6 +1216,9 @@ const PERSISTED_SETTING_DEFAULTS = {
plusModeEnabled: false,
plusPaymentMethod: DEFAULT_PLUS_PAYMENT_METHOD,
plusAccountAccessStrategy: 'oauth',
+ hostedCheckoutVerificationUrl: '',
+ hostedCheckoutPhoneNumber: '',
+ plusHostedCheckoutOauthDelaySeconds: DEFAULT_PLUS_HOSTED_CHECKOUT_OAUTH_DELAY_SECONDS,
paypalEmail: '',
paypalPassword: '',
currentPayPalAccountId: '',
@@ -1325,6 +1393,9 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([
'plusModeEnabled',
'plusPaymentMethod',
'plusAccountAccessStrategy',
+ 'hostedCheckoutVerificationUrl',
+ 'hostedCheckoutPhoneNumber',
+ 'plusHostedCheckoutOauthDelaySeconds',
'mailProvider',
'ipProxyEnabled',
'ipProxyService',
@@ -1892,6 +1963,12 @@ function normalizePlusPaymentMethod(value = '') {
return rootScope.GoPayUtils.normalizePlusPaymentMethod(value);
}
const normalized = String(value || '').trim().toLowerCase();
+ const paypalHostedValue = typeof PLUS_PAYMENT_METHOD_PAYPAL_HOSTED !== 'undefined'
+ ? PLUS_PAYMENT_METHOD_PAYPAL_HOSTED
+ : 'paypal-hosted';
+ if (normalized === paypalHostedValue || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
+ return paypalHostedValue;
+ }
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_PAYMENT_METHOD_GPC_HELPER;
}
@@ -3084,6 +3161,14 @@ function normalizePersistentSettingValue(key, value) {
return normalizePlusPaymentMethod(value);
case 'plusAccountAccessStrategy':
return normalizePlusAccountAccessStrategy(value);
+ case 'hostedCheckoutVerificationUrl':
+ return String(value || '').trim();
+ case 'hostedCheckoutPhoneNumber':
+ return String(value || '').trim();
+ case 'plusHostedCheckoutOauthDelaySeconds': {
+ const numeric = Number(value);
+ return Math.min(120, Math.max(0, Math.floor(Number.isFinite(numeric) ? numeric : DEFAULT_PLUS_HOSTED_CHECKOUT_OAUTH_DELAY_SECONDS)));
+ }
case 'paypalEmail':
return String(value || '').trim();
case 'paypalPassword':
@@ -13358,6 +13443,7 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c
createAutomationTab,
ensureContentScriptReadyOnTabUntilStopped,
fetch: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
+ getState,
markCurrentRegistrationAccountUsed,
registerTab,
sendTabMessageUntilStopped,
@@ -13365,6 +13451,7 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c
sleepWithStop,
throwIfStopped,
waitForTabCompleteUntilStopped,
+ waitForTabUrlMatchUntilStopped,
});
const plusCheckoutBillingExecutor = self.MultiPageBackgroundPlusCheckoutBilling?.createPlusCheckoutBillingExecutor({
addLog,
diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js
index 3e5fb30..e3b1f71 100644
--- a/background/steps/create-plus-checkout.js
+++ b/background/steps/create-plus-checkout.js
@@ -2,14 +2,24 @@
root.MultiPageBackgroundPlusCheckoutCreate = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPlusCheckoutCreateModule() {
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
+ const PAYPAL_SOURCE = 'paypal-flow';
const PLUS_CHECKOUT_ENTRY_URL = 'https://chatgpt.com/';
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/plus-checkout.js'];
+ const PAYPAL_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/paypal-flow.js'];
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
+ const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
+ const HOSTED_CHECKOUT_ADDRESS_ENDPOINT = 'https://www.meiguodizhi.com/api/v1/dz';
+ const HOSTED_CHECKOUT_SUCCESS_URL_PATTERN = /^https:\/\/(?:chatgpt\.com|www\.chatgpt\.com|chat\.openai\.com)\/(?:backend-api\/)?payments\/success(?:[/?#]|$)/i;
+ const HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS = 120000;
+ const HOSTED_CHECKOUT_PAYPAL_TIMEOUT_MS = 10 * 60 * 1000;
+ const HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS = 12;
+ const HOSTED_CHECKOUT_VERIFICATION_POLL_INTERVAL_MS = 5000;
+ const HOSTED_CHECKOUT_DEFAULT_PHONE = '1234567890';
function createPlusCheckoutCreateExecutor(deps = {}) {
const {
@@ -19,11 +29,13 @@
createAutomationTab = null,
ensureContentScriptReadyOnTabUntilStopped,
fetch: fetchImpl = null,
+ getState = null,
registerTab,
sendTabMessageUntilStopped,
setState,
sleepWithStop,
waitForTabCompleteUntilStopped,
+ waitForTabUrlMatchUntilStopped = null,
throwIfStopped = () => {},
} = deps;
@@ -41,6 +53,9 @@
return rootScope.GoPayUtils.normalizePlusPaymentMethod(value);
}
const normalized = String(value || '').trim().toLowerCase();
+ if (normalized === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
+ return PLUS_PAYMENT_METHOD_PAYPAL_HOSTED;
+ }
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_PAYMENT_METHOD_GPC_HELPER;
}
@@ -52,6 +67,9 @@
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return 'GPC 订阅页';
}
+ if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) {
+ return 'PayPal 无卡直绑';
+ }
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay 订阅页' : 'Plus Checkout';
}
@@ -60,6 +78,9 @@
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return 'GPC';
}
+ if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) {
+ return 'PayPal 无卡直绑';
+ }
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay' : 'PayPal';
}
@@ -77,6 +98,444 @@
return tabId;
}
+ function isPayPalUrl(url = '') {
+ return /paypal\./i.test(String(url || ''));
+ }
+
+ function isHostedCheckoutSuccessUrl(url = '') {
+ return HOSTED_CHECKOUT_SUCCESS_URL_PATTERN.test(String(url || ''));
+ }
+
+ function isHostedOpenAiCheckoutUrl(url = '') {
+ return /^https:\/\/(?:pay\.openai\.com|checkout\.stripe\.com)\/c\/pay(?:\/|$)/i.test(String(url || ''));
+ }
+
+ async function waitForUrlMatch(tabId, matcher, timeoutMs = 30000, retryDelayMs = 500) {
+ const deadline = Date.now() + Math.max(1000, Number(timeoutMs) || 30000);
+ while (Date.now() < deadline) {
+ throwIfStopped();
+ const tab = await chrome?.tabs?.get?.(tabId).catch(() => null);
+ if (!tab) {
+ return null;
+ }
+ if (matcher(tab.url || '', tab)) {
+ return tab;
+ }
+ await sleepWithStop(retryDelayMs);
+ }
+ return null;
+ }
+
+ async function getHostedCheckoutRuntimeConfig(state = {}) {
+ const latestState = typeof getState === 'function' ? await getState().catch(() => ({})) : {};
+ return {
+ verificationUrl: String(
+ state?.hostedCheckoutVerificationUrl
+ || latestState?.hostedCheckoutVerificationUrl
+ || ''
+ ).trim(),
+ phone: String(
+ state?.hostedCheckoutPhoneNumber
+ || latestState?.hostedCheckoutPhoneNumber
+ || HOSTED_CHECKOUT_DEFAULT_PHONE
+ ).trim(),
+ oauthDelaySeconds: normalizeHostedCheckoutDelaySeconds(
+ state?.plusHostedCheckoutOauthDelaySeconds
+ ?? latestState?.plusHostedCheckoutOauthDelaySeconds
+ ),
+ };
+ }
+
+ function normalizeHostedCheckoutDelaySeconds(value) {
+ const numeric = Number(value);
+ if (!Number.isFinite(numeric)) {
+ return 3;
+ }
+ return Math.min(120, Math.max(0, Math.floor(numeric)));
+ }
+
+ async function fetchHostedCheckoutAddress() {
+ const { response, data } = await fetchJsonWithTimeout(HOSTED_CHECKOUT_ADDRESS_ENDPOINT, {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ path: '/', method: 'address' }),
+ }, 30000);
+ if (!response?.ok) {
+ throw new Error(`获取无卡直绑地址失败(HTTP ${response?.status || 0})。`);
+ }
+ const address = data?.address || data || {};
+ return {
+ street: String(address.Address || address.street || '123 Main St').trim(),
+ city: String(address.City || address.city || 'New York').trim(),
+ state: String(address.State_Full || address.State || address.state || 'New York').trim(),
+ zip: String(address.Zip_Code || address.zip || '10001').trim().slice(0, 5) || '10001',
+ };
+ }
+
+ function buildRandomHostedEmail() {
+ const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
+ let localPart = '';
+ for (let index = 0; index < 16; index += 1) {
+ localPart += alphabet[Math.floor(Math.random() * alphabet.length)];
+ }
+ return `${localPart}@gmail.com`;
+ }
+
+ function buildRandomHostedPassword() {
+ const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^';
+ let value = 'Aa1!';
+ while (value.length < 14) {
+ value += alphabet[Math.floor(Math.random() * alphabet.length)];
+ }
+ return value;
+ }
+
+ function buildHostedVisaCard() {
+ const digits = [4, 1, 4, 7];
+ while (digits.length < 15) {
+ digits.push(Math.floor(Math.random() * 10));
+ }
+ const reversed = digits.slice().reverse();
+ let sum = 0;
+ for (let index = 0; index < reversed.length; index += 1) {
+ let digit = reversed[index];
+ if (index % 2 === 0) {
+ digit *= 2;
+ if (digit > 9) digit -= 9;
+ }
+ sum += digit;
+ }
+ digits.push((10 - (sum % 10)) % 10);
+ const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
+ const year = (new Date().getFullYear() % 100) + 3;
+ return {
+ number: digits.join(''),
+ expiry: `${month} / ${year}`,
+ cvv: String(Math.floor(100 + Math.random() * 900)),
+ };
+ }
+
+ function buildHostedGuestProfile(address = {}, config = {}) {
+ const card = buildHostedVisaCard();
+ return {
+ email: buildRandomHostedEmail(),
+ password: buildRandomHostedPassword(),
+ phone: String(config?.phone || HOSTED_CHECKOUT_DEFAULT_PHONE).trim(),
+ firstName: 'James',
+ lastName: 'Smith',
+ cardNumber: card.number,
+ cardExpiry: card.expiry,
+ cardCvv: card.cvv,
+ address,
+ };
+ }
+
+ function extractHostedVerificationCode(payload = '') {
+ const candidates = [payload?.data, payload?.code, payload?.text, payload?.message, payload];
+ for (const candidate of candidates) {
+ const text = typeof candidate === 'object' ? JSON.stringify(candidate) : String(candidate || '');
+ const match = text.match(/\d{6}/);
+ if (match) {
+ return match[0];
+ }
+ }
+ return '';
+ }
+
+ async function fetchHostedVerificationCode(verificationUrl = '') {
+ const url = String(verificationUrl || '').trim();
+ if (!url) {
+ throw new Error('未配置 PayPal 无卡直绑验证码接口。');
+ }
+ const fetcher = typeof fetchImpl === 'function'
+ ? fetchImpl
+ : (typeof fetch === 'function' ? fetch.bind(globalThis) : null);
+ if (typeof fetcher !== 'function') {
+ throw new Error('当前运行环境不支持 fetch,无法获取无卡直绑验证码。');
+ }
+ const separator = url.includes('?') ? '&' : '?';
+ const response = await fetcher(`${url}${separator}t=${Date.now()}`, {
+ method: 'GET',
+ headers: { Accept: 'application/json,text/plain,*/*' },
+ });
+ const text = await response.text().catch(() => '');
+ let payload = text;
+ try {
+ payload = text ? JSON.parse(text) : {};
+ } catch {
+ payload = text;
+ }
+ const code = extractHostedVerificationCode(payload);
+ if (!code) {
+ throw new Error('验证码接口暂未返回有效 6 位验证码。');
+ }
+ return code;
+ }
+
+ async function pollHostedVerificationCode(verificationUrl = '') {
+ let lastError = null;
+ for (let attempt = 1; attempt <= HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS; attempt += 1) {
+ throwIfStopped();
+ try {
+ const code = await fetchHostedVerificationCode(verificationUrl);
+ await addLog(`步骤 6:已获取无卡直绑验证码(${attempt}/${HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS})。`, 'info');
+ return code;
+ } catch (error) {
+ lastError = error;
+ await addLog(`步骤 6:无卡直绑验证码暂不可用(${attempt}/${HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS}):${error?.message || error}`, 'warn');
+ if (attempt < HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS) {
+ await sleepWithStop(HOSTED_CHECKOUT_VERIFICATION_POLL_INTERVAL_MS);
+ }
+ }
+ }
+ throw lastError || new Error('无卡直绑验证码轮询失败。');
+ }
+
+ async function runHostedOpenAiCheckout(tabId, profile, config) {
+ await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
+ inject: PLUS_CHECKOUT_INJECT_FILES,
+ injectSource: PLUS_CHECKOUT_SOURCE,
+ logMessage: '步骤 6:正在等待 OpenAI hosted checkout 脚本就绪...',
+ });
+ const firstResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
+ type: 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP',
+ source: 'background',
+ payload: { address: profile.address },
+ });
+ if (firstResult?.error) {
+ throw new Error(firstResult.error);
+ }
+
+ const deadline = Date.now() + HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS;
+ let verificationSubmitted = false;
+ while (Date.now() < deadline) {
+ throwIfStopped();
+ const tab = await chrome?.tabs?.get?.(tabId).catch(() => null);
+ if (!tab) {
+ throw new Error('步骤 6:无卡直绑 checkout 标签页已关闭。');
+ }
+ const currentUrl = String(tab.url || '').trim();
+ if (isPayPalUrl(currentUrl) || isHostedCheckoutSuccessUrl(currentUrl)) {
+ return currentUrl;
+ }
+ await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
+ inject: PLUS_CHECKOUT_INJECT_FILES,
+ injectSource: PLUS_CHECKOUT_SOURCE,
+ });
+ const pageState = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
+ type: 'PLUS_CHECKOUT_GET_STATE',
+ source: 'background',
+ payload: {},
+ });
+ if (pageState?.error) {
+ throw new Error(pageState.error);
+ }
+ if (pageState?.hostedVerificationVisible && !verificationSubmitted) {
+ const verificationCode = await pollHostedVerificationCode(config.verificationUrl);
+ const verifyResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
+ type: 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP',
+ source: 'background',
+ payload: { verificationCode },
+ });
+ if (verifyResult?.error) {
+ throw new Error(verifyResult.error);
+ }
+ verificationSubmitted = true;
+ }
+ await sleepWithStop(500);
+ }
+ throw new Error('步骤 6:OpenAI hosted checkout 长时间未跳转到 PayPal 或支付成功页。');
+ }
+
+ async function getHostedPayPalState(tabId) {
+ await waitForTabCompleteUntilStopped(tabId);
+ await ensureContentScriptReadyOnTabUntilStopped(PAYPAL_SOURCE, tabId, {
+ inject: PAYPAL_INJECT_FILES,
+ injectSource: PAYPAL_SOURCE,
+ logMessage: '步骤 6:正在等待 PayPal 无卡直绑页面脚本就绪...',
+ });
+ const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
+ type: 'PAYPAL_HOSTED_GET_STATE',
+ source: 'background',
+ payload: {},
+ });
+ if (result?.error) {
+ throw new Error(result.error);
+ }
+ return result || {};
+ }
+
+ async function runHostedPayPalStep(tabId, payload = {}) {
+ await waitForTabCompleteUntilStopped(tabId);
+ await ensureContentScriptReadyOnTabUntilStopped(PAYPAL_SOURCE, tabId, {
+ inject: PAYPAL_INJECT_FILES,
+ injectSource: PAYPAL_SOURCE,
+ logMessage: '步骤 6:正在等待 PayPal 无卡直绑页面脚本就绪...',
+ });
+ const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
+ type: 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP',
+ source: 'background',
+ payload,
+ });
+ if (result?.error) {
+ throw new Error(result.error);
+ }
+ return result || {};
+ }
+
+ async function runHostedPayPalCheckout(tabId, profile, config) {
+ const deadline = Date.now() + HOSTED_CHECKOUT_PAYPAL_TIMEOUT_MS;
+ while (Date.now() < deadline) {
+ throwIfStopped();
+ const tab = await chrome?.tabs?.get?.(tabId).catch(() => null);
+ if (!tab) {
+ throw new Error('步骤 6:PayPal 无卡直绑标签页已关闭。');
+ }
+ const currentUrl = String(tab.url || '').trim();
+ if (isHostedCheckoutSuccessUrl(currentUrl)) {
+ await addLog('步骤 6:PayPal 无卡直绑已回到 ChatGPT 支付成功页。', 'ok');
+ return currentUrl;
+ }
+ if (!isPayPalUrl(currentUrl)) {
+ await addLog(`步骤 6:无卡直绑已离开 PayPal(${currentUrl || '空 URL'}),继续等待支付成功页。`, 'info');
+ await sleepWithStop(1000);
+ continue;
+ }
+
+ const pageState = await getHostedPayPalState(tabId);
+ if (pageState.hostedStage === 'verification' && pageState.verificationInputsVisible) {
+ const verificationCode = await pollHostedVerificationCode(config.verificationUrl);
+ await runHostedPayPalStep(tabId, { verificationCode });
+ await sleepWithStop(1000);
+ continue;
+ }
+ if (pageState.hostedStage === 'pay_login') {
+ await addLog('步骤 6:正在填写 PayPal 无卡直绑登录邮箱。', 'info');
+ await runHostedPayPalStep(tabId, { email: profile.email });
+ await sleepWithStop(1000);
+ continue;
+ }
+ if (pageState.hostedStage === 'guest_checkout') {
+ await addLog('步骤 6:正在填写 PayPal 无卡直绑卡支付资料。', 'info');
+ await runHostedPayPalStep(tabId, {
+ ...profile,
+ phone: config.phone || profile.phone,
+ });
+ await sleepWithStop(1500);
+ continue;
+ }
+ if (pageState.hostedStage === 'review_consent') {
+ await addLog('步骤 6:正在确认 PayPal 无卡直绑账单复核页。', 'info');
+ await runHostedPayPalStep(tabId, {});
+ await sleepWithStop(1000);
+ continue;
+ }
+ if (pageState.hostedStage === 'approval') {
+ throw new Error('步骤 6:无卡直绑流程进入普通 PayPal 授权页,请检查支付方式是否选择正确。');
+ }
+ await sleepWithStop(1000);
+ }
+ throw new Error('步骤 6:PayPal 无卡直绑自动化超时,未等到支付成功页。');
+ }
+
+ async function waitHostedSuccessDelayAndComplete(tabId, state = {}, successUrl = '') {
+ const config = await getHostedCheckoutRuntimeConfig(state);
+ if (config.oauthDelaySeconds > 0) {
+ await addLog(`步骤 6:支付成功后等待 ${config.oauthDelaySeconds} 秒,再继续账号接入。`, 'info');
+ await sleepWithStop(config.oauthDelaySeconds * 1000);
+ }
+ await completeNodeFromBackground('plus-checkout-create', {
+ plusReturnUrl: successUrl,
+ plusHostedCheckoutCompleted: true,
+ plusHostedCheckoutOauthDelaySeconds: config.oauthDelaySeconds,
+ });
+ }
+
+ function resolveCheckoutTargetUrl(result = {}, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
+ if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) {
+ return String(
+ result?.preferredCheckoutUrl
+ || result?.hostedCheckoutUrl
+ || result?.checkoutUrl
+ || ''
+ ).trim();
+ }
+ return String(result?.checkoutUrl || '').trim();
+ }
+
+ async function executeHostedCheckoutCreate(tabId, state = {}, result = {}) {
+ const targetCheckoutUrl = resolveCheckoutTargetUrl(result, PLUS_PAYMENT_METHOD_PAYPAL_HOSTED);
+ if (!targetCheckoutUrl) {
+ throw new Error('步骤 6:PayPal 无卡直绑未返回可用的订阅链接。');
+ }
+
+ await addLog('步骤 6:PayPal 无卡直绑链接已创建,正在打开 hosted checkout 页面...', 'ok');
+ await chrome.tabs.update(tabId, { url: targetCheckoutUrl, active: true });
+ await waitForTabCompleteUntilStopped(tabId);
+
+ const landedTab = await waitForUrlMatch(
+ tabId,
+ (url) => isHostedOpenAiCheckoutUrl(url) || isPayPalUrl(url) || isHostedCheckoutSuccessUrl(url),
+ HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS,
+ 500
+ );
+ const landedUrl = String(landedTab?.url || targetCheckoutUrl || '').trim();
+
+ await setState({
+ plusCheckoutTabId: tabId,
+ plusCheckoutUrl: landedUrl,
+ plusCheckoutCountry: result.country || 'US',
+ plusCheckoutCurrency: result.currency || 'USD',
+ plusCheckoutSource: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED,
+ plusReturnUrl: '',
+ });
+
+ await addLog(`步骤 6:PayPal 无卡直绑页面已就绪(${result.country || 'US'} ${result.currency || 'USD'}),开始自动完成直绑支付链路。`, 'info');
+
+ if (isHostedCheckoutSuccessUrl(landedUrl)) {
+ await waitHostedSuccessDelayAndComplete(tabId, state, landedUrl);
+ return;
+ }
+
+ const config = await getHostedCheckoutRuntimeConfig(state);
+ const address = await fetchHostedCheckoutAddress();
+ const guestProfile = buildHostedGuestProfile(address, config);
+ let transitionUrl = landedUrl;
+
+ if (isHostedOpenAiCheckoutUrl(transitionUrl)) {
+ transitionUrl = await runHostedOpenAiCheckout(tabId, guestProfile, config);
+ }
+
+ if (isHostedCheckoutSuccessUrl(transitionUrl)) {
+ await waitHostedSuccessDelayAndComplete(tabId, state, transitionUrl);
+ return;
+ }
+
+ if (!isPayPalUrl(transitionUrl)) {
+ const transitionTab = await waitForUrlMatch(
+ tabId,
+ (url) => isPayPalUrl(url) || isHostedCheckoutSuccessUrl(url),
+ HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS,
+ 500
+ );
+ transitionUrl = String(transitionTab?.url || '').trim();
+ }
+
+ if (isHostedCheckoutSuccessUrl(transitionUrl)) {
+ await waitHostedSuccessDelayAndComplete(tabId, state, transitionUrl);
+ return;
+ }
+ if (!isPayPalUrl(transitionUrl)) {
+ throw new Error('步骤 6:PayPal 无卡直绑提交后未进入 PayPal 或支付成功页。');
+ }
+
+ const successUrl = await runHostedPayPalCheckout(tabId, guestProfile, config);
+ await waitHostedSuccessDelayAndComplete(tabId, state, successUrl);
+ }
+
function normalizeHelperCountryCode(countryCode = '86') {
const digits = String(countryCode || '').replace(/\D/g, '');
return digits || '86';
@@ -493,12 +952,18 @@
if (result?.error) {
throw new Error(result.error);
}
- if (!result?.checkoutUrl) {
+ const targetCheckoutUrl = resolveCheckoutTargetUrl(result, paymentMethod);
+ if (!targetCheckoutUrl) {
throw new Error(`步骤 6:${checkoutModeLabel}未返回可用的订阅链接。`);
}
+ if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) {
+ await executeHostedCheckoutCreate(tabId, state, result);
+ return;
+ }
+
await addLog(`步骤 6:${checkoutModeLabel}已创建,正在打开订阅页面...`, 'ok');
- await chrome.tabs.update(tabId, { url: result.checkoutUrl, active: true });
+ await chrome.tabs.update(tabId, { url: targetCheckoutUrl, active: true });
await waitForTabCompleteUntilStopped(tabId);
await sleepWithStop(1000);
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
@@ -509,7 +974,7 @@
await setState({
plusCheckoutTabId: tabId,
- plusCheckoutUrl: result.checkoutUrl,
+ plusCheckoutUrl: targetCheckoutUrl,
plusCheckoutCountry: result.country || 'DE',
plusCheckoutCurrency: result.currency || 'EUR',
plusCheckoutSource: '',
diff --git a/content/paypal-flow.js b/content/paypal-flow.js
index 689811e..9fd8782 100644
--- a/content/paypal-flow.js
+++ b/content/paypal-flow.js
@@ -3,6 +3,14 @@
console.log('[MultiPage:paypal-flow] Content script loaded on', location.href);
const PAYPAL_FLOW_LISTENER_SENTINEL = 'data-multipage-paypal-flow-listener';
+const PAYPAL_HOSTED_DEFAULT_PHONE = '1234567890';
+const PAYPAL_HOSTED_STAGE_OUTSIDE = 'outside_paypal';
+const PAYPAL_HOSTED_STAGE_LOGIN = 'pay_login';
+const PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT = 'guest_checkout';
+const PAYPAL_HOSTED_STAGE_VERIFICATION = 'verification';
+const PAYPAL_HOSTED_STAGE_REVIEW = 'review_consent';
+const PAYPAL_HOSTED_STAGE_APPROVAL = 'approval';
+const PAYPAL_HOSTED_STAGE_UNKNOWN = 'unknown';
if (document.documentElement.getAttribute(PAYPAL_FLOW_LISTENER_SENTINEL) !== '1') {
document.documentElement.setAttribute(PAYPAL_FLOW_LISTENER_SENTINEL, '1');
@@ -13,6 +21,8 @@ if (document.documentElement.getAttribute(PAYPAL_FLOW_LISTENER_SENTINEL) !== '1'
|| message.type === 'PAYPAL_SUBMIT_LOGIN'
|| message.type === 'PAYPAL_DISMISS_PROMPTS'
|| message.type === 'PAYPAL_CLICK_APPROVE'
+ || message.type === 'PAYPAL_HOSTED_GET_STATE'
+ || message.type === 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP'
) {
resetStopState();
handlePayPalCommand(message).then((result) => {
@@ -47,6 +57,10 @@ async function handlePayPalCommand(message) {
return dismissPayPalPrompts();
case 'PAYPAL_CLICK_APPROVE':
return clickPayPalApprove();
+ case 'PAYPAL_HOSTED_GET_STATE':
+ return inspectPayPalHostedState();
+ case 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP':
+ return runPayPalHostedCheckoutStep(message.payload || {});
default:
throw new Error(`paypal-flow.js 不处理消息:${message.type}`);
}
@@ -223,6 +237,302 @@ function findApproveButton() {
]);
}
+function getPayPalPathname() {
+ return String(location?.pathname || '').trim();
+}
+
+function findHostedVerificationInputs() {
+ return Array.from({ length: 6 }, (_, index) => document.getElementById(`ci-ciBasic-${index}`))
+ .filter((input) => input && isVisibleElement(input) && isEnabledControl(input));
+}
+
+function hasHostedVerificationInputs() {
+ return findHostedVerificationInputs().length >= 6;
+}
+
+function isHostedLoginPage() {
+ return getPayPalPathname() === '/pay' || Boolean(document.getElementById('email'));
+}
+
+function isHostedGuestCheckoutPage() {
+ return /\/checkoutweb\//i.test(getPayPalPathname())
+ || Boolean(document.getElementById('cardNumber'))
+ || Boolean(document.getElementById('billingLine1'));
+}
+
+function isHostedReviewPage() {
+ return /\/webapps\/hermes/i.test(getPayPalPathname());
+}
+
+function findHostedReviewConsentButton() {
+ const direct = document.getElementById('consentButton')
+ || document.querySelector('button[data-testid="consentButton"]');
+ if (direct && isVisibleElement(direct) && isEnabledControl(direct)) {
+ return direct;
+ }
+ return findClickableByText([
+ /agree\s*(?:and)?\s*continue|accept|continue/i,
+ /同意并继续|同意|继续/i,
+ ]);
+}
+
+function detectPayPalHostedStage() {
+ if (!/paypal\./i.test(String(location?.host || ''))) {
+ return PAYPAL_HOSTED_STAGE_OUTSIDE;
+ }
+ if (hasHostedVerificationInputs()) {
+ return PAYPAL_HOSTED_STAGE_VERIFICATION;
+ }
+ if (isHostedGuestCheckoutPage()) {
+ return PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT;
+ }
+ if (isHostedReviewPage() && findHostedReviewConsentButton()) {
+ return PAYPAL_HOSTED_STAGE_REVIEW;
+ }
+ if (isHostedLoginPage()) {
+ return PAYPAL_HOSTED_STAGE_LOGIN;
+ }
+ return findApproveButton() ? PAYPAL_HOSTED_STAGE_APPROVAL : PAYPAL_HOSTED_STAGE_UNKNOWN;
+}
+
+function fillHostedInputById(id, value) {
+ const input = document.getElementById(String(id || '').trim());
+ if (!input || !isVisibleElement(input) || !isEnabledControl(input)) {
+ return false;
+ }
+ fillInput(input, String(value || ''));
+ return true;
+}
+
+function selectHostedOptionByIdText(id, value) {
+ const select = document.getElementById(String(id || '').trim());
+ const expected = normalizeText(value).toLowerCase();
+ if (!select || !expected) {
+ return false;
+ }
+ const option = Array.from(select.options || []).find((item) => {
+ const optionText = normalizeText(item.textContent || item.label || '').toLowerCase();
+ const optionValue = normalizeText(item.value || '').toLowerCase();
+ return optionText.includes(expected) || optionValue.includes(expected);
+ });
+ if (!option) {
+ return false;
+ }
+ select.value = option.value;
+ select.dispatchEvent(new Event('input', { bubbles: true }));
+ select.dispatchEvent(new Event('change', { bubbles: true }));
+ return true;
+}
+
+function findHostedSubmitButton() {
+ return document.querySelector('button[data-testid="submit-button"]')
+ || document.querySelector('button[data-testid="hosted-payment-submit-button"]')
+ || document.querySelector('button[data-atomic-wait-intent="Submit_Email"]')
+ || document.querySelector('button.SubmitButton--complete')
+ || findEmailNextButton()
+ || findLoginNextButton()
+ || findClickableByText([
+ /pay|continue|next|agree|subscribe/i,
+ /支付|继续|下一步|同意|订阅/i,
+ ]);
+}
+
+async function clickHostedSubmitButton() {
+ const button = await waitUntil(() => {
+ const candidate = findHostedSubmitButton();
+ return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
+ }, {
+ intervalMs: 500,
+ timeoutMs: 15000,
+ timeoutMessage: 'PayPal hosted checkout 未找到可点击的继续/提交按钮。',
+ });
+ await performPayPalOperationWithDelay({ stepKey: 'plus-checkout-create', kind: 'click', label: 'hosted-paypal-submit' }, async () => {
+ simulateClick(button);
+ });
+ await sleep(1000);
+ return {
+ clicked: true,
+ buttonText: getActionText(button),
+ verificationRequired: hasHostedVerificationInputs(),
+ };
+}
+
+function buildHostedRandomEmail() {
+ const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
+ let value = '';
+ for (let index = 0; index < 16; index += 1) {
+ value += alphabet[Math.floor(Math.random() * alphabet.length)];
+ }
+ return `${value}@gmail.com`;
+}
+
+function buildHostedRandomPassword() {
+ const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^';
+ let value = 'Aa1!';
+ while (value.length < 14) {
+ value += alphabet[Math.floor(Math.random() * alphabet.length)];
+ }
+ return value;
+}
+
+function buildHostedVisaCard() {
+ const digits = [4, 1, 4, 7];
+ while (digits.length < 15) {
+ digits.push(Math.floor(Math.random() * 10));
+ }
+ const reversed = digits.slice().reverse();
+ let sum = 0;
+ for (let index = 0; index < reversed.length; index += 1) {
+ let digit = reversed[index];
+ if (index % 2 === 0) {
+ digit *= 2;
+ if (digit > 9) digit -= 9;
+ }
+ sum += digit;
+ }
+ digits.push((10 - (sum % 10)) % 10);
+ const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
+ const year = (new Date().getFullYear() % 100) + 3;
+ return {
+ number: digits.join(''),
+ expiry: `${month} / ${year}`,
+ cvv: String(Math.floor(100 + Math.random() * 900)),
+ };
+}
+
+async function submitHostedLogin(payload = {}) {
+ await waitForDocumentComplete();
+ const email = normalizeText(payload.email || buildHostedRandomEmail());
+ const emailInput = document.getElementById('email') || findEmailInput();
+ if (!emailInput) {
+ throw new Error('PayPal hosted checkout 未找到邮箱输入框。');
+ }
+ refillPayPalEmailInput(emailInput, email);
+ const clickResult = await clickHostedSubmitButton();
+ return {
+ stage: PAYPAL_HOSTED_STAGE_LOGIN,
+ submitted: true,
+ generatedEmail: email,
+ verificationRequired: Boolean(clickResult.verificationRequired),
+ };
+}
+
+async function fillHostedVerificationCode(payload = {}) {
+ await waitForDocumentComplete();
+ const code = String(payload.verificationCode || payload.code || '').replace(/\D+/g, '').slice(0, 6);
+ if (code.length !== 6) {
+ throw new Error('PayPal hosted checkout 验证码无效。');
+ }
+ const inputs = findHostedVerificationInputs();
+ if (inputs.length < 6) {
+ throw new Error('PayPal hosted checkout 当前页面未显示验证码输入框。');
+ }
+ await performPayPalOperationWithDelay({ stepKey: 'plus-checkout-create', kind: 'fill', label: 'hosted-paypal-verification-code' }, async () => {
+ inputs.forEach((input, index) => fillInput(input, code[index] || ''));
+ });
+ return {
+ stage: PAYPAL_HOSTED_STAGE_VERIFICATION,
+ codeSubmitted: true,
+ };
+}
+
+async function fillHostedGuestCheckout(payload = {}) {
+ await waitForDocumentComplete();
+ const countrySelect = document.getElementById('country');
+ if (countrySelect && String(countrySelect.value || '').trim().toUpperCase() !== 'US') {
+ countrySelect.value = 'US';
+ countrySelect.dispatchEvent(new Event('change', { bubbles: true }));
+ await sleep(1000);
+ }
+ const generatedCard = buildHostedVisaCard();
+ const address = payload.address && typeof payload.address === 'object' ? payload.address : {};
+ const values = {
+ email: normalizeText(payload.email || buildHostedRandomEmail()),
+ phone: normalizeText(payload.phone || PAYPAL_HOSTED_DEFAULT_PHONE),
+ cardNumber: String(payload.cardNumber || generatedCard.number).replace(/\s+/g, ''),
+ cardExpiry: normalizeText(payload.cardExpiry || generatedCard.expiry),
+ cardCvv: normalizeText(payload.cardCvv || generatedCard.cvv),
+ password: String(payload.password || buildHostedRandomPassword()),
+ firstName: normalizeText(payload.firstName || 'James'),
+ lastName: normalizeText(payload.lastName || 'Smith'),
+ };
+ fillHostedInputById('email', values.email);
+ fillHostedInputById('phone', values.phone);
+ fillHostedInputById('cardNumber', values.cardNumber);
+ fillHostedInputById('cardExpiry', values.cardExpiry);
+ fillHostedInputById('cardCvv', values.cardCvv);
+ fillHostedInputById('password', values.password);
+ fillHostedInputById('firstName', values.firstName);
+ fillHostedInputById('lastName', values.lastName);
+ fillHostedInputById('billingLine1', address.street || address.address1 || '');
+ fillHostedInputById('billingCity', address.city || '');
+ fillHostedInputById('billingPostalCode', address.zip || address.postalCode || '');
+ selectHostedOptionByIdText('billingState', address.state || address.region || '');
+ const clickResult = await clickHostedSubmitButton();
+ return {
+ stage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT,
+ submitted: true,
+ verificationRequired: Boolean(clickResult.verificationRequired),
+ };
+}
+
+async function clickHostedReviewConsent() {
+ await waitForDocumentComplete();
+ const button = await waitUntil(() => {
+ const candidate = findHostedReviewConsentButton();
+ return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
+ }, {
+ intervalMs: 500,
+ timeoutMs: 30000,
+ timeoutMessage: 'PayPal hosted checkout 未找到账单确认按钮。',
+ });
+ await performPayPalOperationWithDelay({ stepKey: 'plus-checkout-create', kind: 'click', label: 'hosted-paypal-review-consent' }, async () => {
+ simulateClick(button);
+ });
+ return {
+ stage: PAYPAL_HOSTED_STAGE_REVIEW,
+ submitted: true,
+ };
+}
+
+async function runPayPalHostedCheckoutStep(payload = {}) {
+ const stage = detectPayPalHostedStage();
+ if (stage === PAYPAL_HOSTED_STAGE_VERIFICATION) {
+ return payload.verificationCode || payload.code
+ ? fillHostedVerificationCode(payload)
+ : { stage, requiresVerificationCode: true };
+ }
+ if (stage === PAYPAL_HOSTED_STAGE_LOGIN) {
+ return submitHostedLogin(payload);
+ }
+ if (stage === PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT) {
+ return fillHostedGuestCheckout(payload);
+ }
+ if (stage === PAYPAL_HOSTED_STAGE_REVIEW) {
+ return clickHostedReviewConsent();
+ }
+ return {
+ stage,
+ submitted: false,
+ approveReady: Boolean(findApproveButton()),
+ };
+}
+
+function inspectPayPalHostedState() {
+ const stage = detectPayPalHostedStage();
+ return {
+ url: location.href,
+ readyState: document.readyState,
+ hostedStage: stage,
+ verificationInputsVisible: hasHostedVerificationInputs(),
+ hasGuestCardFields: Boolean(document.getElementById('cardNumber')),
+ hasHostedEmailInput: Boolean(document.getElementById('email') || findEmailInput()),
+ reviewConsentReady: Boolean(findHostedReviewConsentButton()),
+ approveReady: Boolean(findApproveButton()),
+ bodyTextPreview: normalizeText(document.body?.innerText || '').slice(0, 240),
+ };
+}
+
function findPasskeyPromptButtons() {
const promptPatterns = [
/passkey|通行密钥|安全密钥|下次登录|faster|save/i,
diff --git a/content/plus-checkout.js b/content/plus-checkout.js
index b3f7f2a..7ca4353 100644
--- a/content/plus-checkout.js
+++ b/content/plus-checkout.js
@@ -34,6 +34,7 @@ const PLUS_CHECKOUT_CONFIGS = {
};
const PAYPAL_DIAGNOSTIC_LOG_INTERVAL_MS = 5000;
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
+const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PAYMENT_METHOD_CONFIGS = {
[PLUS_PAYMENT_METHOD_PAYPAL]: {
@@ -47,6 +48,17 @@ const PAYMENT_METHOD_CONFIGS = {
},
patterns: [/paypal/i],
},
+ [PLUS_PAYMENT_METHOD_PAYPAL_HOSTED]: {
+ id: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED,
+ label: 'PayPal 无卡直绑',
+ diagnosticLabel: 'PayPal hosted',
+ checkoutMerchantPath: 'openai_llc',
+ billingDetails: {
+ country: 'US',
+ currency: 'USD',
+ },
+ patterns: [/paypal/i],
+ },
[PLUS_PAYMENT_METHOD_GOPAY]: {
id: PLUS_PAYMENT_METHOD_GOPAY,
label: 'GoPay',
@@ -80,6 +92,7 @@ if (document.documentElement.getAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL) !== '
|| message.type === 'PLUS_CHECKOUT_SELECT_ADDRESS_SUGGESTION'
|| message.type === 'PLUS_CHECKOUT_ENSURE_BILLING_ADDRESS'
|| message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE'
+ || message.type === 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP'
|| message.type === 'PLUS_CHECKOUT_GET_STATE'
) {
resetStopState();
@@ -119,6 +132,8 @@ async function handlePlusCheckoutCommand(message) {
return ensurePlusStructuredBillingAddress(message.payload || {});
case 'PLUS_CHECKOUT_CLICK_SUBSCRIBE':
return clickPlusSubscribe(message.payload || {});
+ case 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP':
+ return runPayPalHostedOpenAiCheckoutStep(message.payload || {});
case 'PLUS_CHECKOUT_GET_STATE':
return inspectPlusCheckoutState(message.payload || {});
default:
@@ -152,6 +167,153 @@ async function waitForDocumentComplete() {
await sleep(1000);
}
+function isPayPalHostedOpenAiCheckoutPage() {
+ const host = String(location?.host || '').toLowerCase();
+ return host.includes('pay.openai.com') || host.includes('checkout.stripe.com');
+}
+
+function hideHostedAddressAutocomplete() {
+ [
+ '.AddressAutocomplete-results',
+ '[class*="AddressAutocomplete"]',
+ '#billing-address-autocomplete-results',
+ ].forEach((selector) => {
+ document.querySelectorAll(selector).forEach((node) => {
+ try {
+ node.style.setProperty('display', 'none', 'important');
+ node.style.setProperty('visibility', 'hidden', 'important');
+ node.style.setProperty('pointer-events', 'none', 'important');
+ } catch {
+ // Best effort only; checkout still works without this cleanup.
+ }
+ });
+ });
+}
+
+function hasHostedOpenAiVerificationDialog() {
+ return Boolean(document.getElementById('ci-ciBasic-0'));
+}
+
+function fillHostedInput(selector, value) {
+ const input = document.querySelector(selector);
+ if (!input) {
+ return false;
+ }
+ fillInput(input, String(value || ''));
+ return true;
+}
+
+function selectHostedOptionByText(selector, value) {
+ const select = document.querySelector(selector);
+ const expected = normalizeText(value).toLowerCase();
+ if (!select || !expected) {
+ return false;
+ }
+ const option = Array.from(select.options || []).find((item) => {
+ const optionText = normalizeText(item.textContent || item.label || '').toLowerCase();
+ const optionValue = normalizeText(item.value || '').toLowerCase();
+ return optionText.includes(expected) || optionValue.includes(expected);
+ });
+ if (!option) {
+ return false;
+ }
+ select.value = option.value;
+ select.dispatchEvent(new Event('input', { bubbles: true }));
+ select.dispatchEvent(new Event('change', { bubbles: true }));
+ return true;
+}
+
+function findHostedPayPalButton() {
+ return document.querySelector('[data-testid="paypal-accordion-item-button"]')
+ || document.querySelector('.paypal-accordion-item button')
+ || findClickableByText([/paypal/i]);
+}
+
+function findHostedSubmitButton() {
+ return document.querySelector('button[data-testid="submit-button"]')
+ || document.querySelector('button[data-testid="hosted-payment-submit-button"]')
+ || document.querySelector('button[data-atomic-wait-intent="Submit_Email"]')
+ || document.querySelector('button.SubmitButton--complete')
+ || findClickableByText([
+ /next|continue|pay|subscribe|agree/i,
+ /下一页|继续|支付|订阅|同意/i,
+ ]);
+}
+
+async function clickHostedSubmitButton() {
+ const button = await waitUntil(() => {
+ hideHostedAddressAutocomplete();
+ const candidate = findHostedSubmitButton();
+ return candidate && isEnabledControl(candidate) && isVisibleElement(candidate) ? candidate : null;
+ }, {
+ label: 'hosted checkout 提交按钮',
+ intervalMs: 500,
+ timeoutMs: 15000,
+ });
+ document.activeElement?.blur?.();
+ await sleep(300);
+ simulateClick(button);
+ await sleep(1200);
+ return {
+ clicked: true,
+ buttonText: getActionText(button),
+ hostedVerificationVisible: hasHostedOpenAiVerificationDialog(),
+ };
+}
+
+function fillHostedOpenAiVerificationCode(verificationCode = '') {
+ const code = String(verificationCode || '').replace(/\D+/g, '').slice(0, 6);
+ if (code.length !== 6) {
+ throw new Error('hosted checkout OpenAI 验证码无效。');
+ }
+ for (let index = 0; index < 6; index += 1) {
+ const input = document.getElementById(`ci-ciBasic-${index}`);
+ if (!input) {
+ throw new Error('hosted checkout OpenAI 页面未找到完整的验证码输入框。');
+ }
+ fillInput(input, code[index]);
+ }
+ return {
+ verificationCodeFilled: true,
+ hostedVerificationVisible: true,
+ };
+}
+
+async function runPayPalHostedOpenAiCheckoutStep(payload = {}) {
+ await waitForDocumentComplete();
+ if (!isPayPalHostedOpenAiCheckoutPage()) {
+ throw new Error('当前页面不是 PayPal 无卡直绑的 OpenAI hosted checkout 页面。');
+ }
+ if (payload.verificationCode) {
+ return fillHostedOpenAiVerificationCode(payload.verificationCode);
+ }
+
+ hideHostedAddressAutocomplete();
+ const payPalButton = findHostedPayPalButton();
+ if (payPalButton) {
+ simulateClick(payPalButton);
+ await sleep(500);
+ }
+
+ const address = payload.address && typeof payload.address === 'object' ? payload.address : {};
+ fillHostedInput('#billingAddressLine1', address.street || address.address1 || '');
+ fillHostedInput('#billingLocality', address.city || '');
+ fillHostedInput('#billingPostalCode', address.zip || address.postalCode || '');
+ selectHostedOptionByText('#billingAdministrativeArea', address.state || address.region || '');
+
+ const consent = document.getElementById('termsOfServiceConsentCheckbox');
+ if (consent && !consent.checked) {
+ simulateClick(consent);
+ }
+
+ for (let count = 0; count < 5; count += 1) {
+ hideHostedAddressAutocomplete();
+ await sleep(250);
+ }
+
+ return clickHostedSubmitButton();
+}
+
function isVisibleElement(el) {
if (!el) return false;
const style = window.getComputedStyle(el);
@@ -423,7 +585,14 @@ function findPaymentCardAncestor(el, pattern) {
}
function normalizePlusPaymentMethod(value = '') {
- return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY
+ const normalized = String(value || '').trim().toLowerCase();
+ const paypalHostedValue = typeof PLUS_PAYMENT_METHOD_PAYPAL_HOSTED !== 'undefined'
+ ? PLUS_PAYMENT_METHOD_PAYPAL_HOSTED
+ : 'paypal-hosted';
+ if (normalized === paypalHostedValue || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
+ return paypalHostedValue;
+ }
+ return normalized === PLUS_PAYMENT_METHOD_GOPAY
? PLUS_PAYMENT_METHOD_GOPAY
: PLUS_PAYMENT_METHOD_PAYPAL;
}
@@ -620,6 +789,7 @@ function buildPlusCheckoutPayload(paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
const config = getPaymentMethodConfig(paymentMethod);
return {
...JSON.parse(JSON.stringify(PLUS_CHECKOUT_PAYLOAD_BASE)),
+ checkout_ui_mode: config.id === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED ? 'hosted' : 'custom',
billing_details: {
...config.billingDetails,
},
@@ -635,6 +805,29 @@ function buildPlusCheckoutUrl(checkoutSessionId, paymentMethod = PLUS_PAYMENT_ME
return `https://chatgpt.com/checkout/${config.checkoutMerchantPath}/${sessionId}`;
}
+function findHostedCheckoutUrl(payload = {}) {
+ const queue = [payload];
+ while (queue.length) {
+ const current = queue.shift();
+ if (!current || typeof current !== 'object') {
+ continue;
+ }
+ if (Array.isArray(current)) {
+ queue.push(...current);
+ continue;
+ }
+ for (const value of Object.values(current)) {
+ if (typeof value === 'string' && /^https:\/\/(?:pay\.openai\.com|checkout\.stripe\.com)\/c\/pay\//i.test(value.trim())) {
+ return value.trim();
+ }
+ if (value && typeof value === 'object') {
+ queue.push(value);
+ }
+ }
+ }
+ return '';
+}
+
async function createPlusCheckoutSession(options = {}) {
await waitForDocumentComplete();
log('Plus:正在读取 ChatGPT 登录会话...');
@@ -667,8 +860,16 @@ async function createPlusCheckoutSession(options = {}) {
throw new Error(`创建 Plus Checkout 失败:${detail}`);
}
+ const checkoutUrl = buildPlusCheckoutUrl(data.checkout_session_id, paymentMethod);
+ const hostedCheckoutUrl = findHostedCheckoutUrl(data);
+ const preferredCheckoutUrl = paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED
+ ? (hostedCheckoutUrl || checkoutUrl)
+ : checkoutUrl;
+
return {
- checkoutUrl: buildPlusCheckoutUrl(data.checkout_session_id, paymentMethod),
+ checkoutUrl,
+ hostedCheckoutUrl,
+ preferredCheckoutUrl,
country: checkoutPayload.billing_details.country,
currency: checkoutPayload.billing_details.currency,
};
@@ -1550,6 +1751,9 @@ async function inspectPlusCheckoutState(options = {}) {
cardFieldsVisible: hasCreditCardFields(),
billingFieldsVisible: hasBillingAddressFields(),
hasSubscribeButton: Boolean(findSubscribeButton()),
+ hostedOpenAiPage: isPayPalHostedOpenAiCheckoutPage(),
+ hostedVerificationVisible: hasHostedOpenAiVerificationDialog(),
+ hostedPayPalButtonFound: Boolean(findHostedPayPalButton()),
checkoutAmountSummary: getCheckoutAmountSummary(),
addressFieldValues: {
address1: structuredAddress.address1?.value || '',
diff --git a/data/step-definitions.js b/data/step-definitions.js
index 27b0a0a..e7b2a8d 100644
--- a/data/step-definitions.js
+++ b/data/step-definitions.js
@@ -3,6 +3,7 @@
})(typeof self !== 'undefined' ? self : globalThis, function createStepDefinitionsModule() {
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
+ const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
@@ -32,6 +33,7 @@
{ id: 8, order: 80, key: 'paypal-approve', title: 'PayPal 登录与授权', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-approve' },
{ id: 9, order: 90, key: 'plus-checkout-return', title: '订阅回跳确认', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-return' },
];
+ const PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS = PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS.slice(0, 6);
const PLUS_GOPAY_PREFIX_STEP_DEFINITIONS = [
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网', sourceId: 'chatgpt', driverId: null, command: 'open-chatgpt' },
@@ -163,6 +165,53 @@
];
}
+ function createHostedCheckoutAuthTail(startId, startOrder, signupMethod = SIGNUP_METHOD_EMAIL, options = {}) {
+ const id = Number(startId) || 7;
+ const order = Number(startOrder) || id * 10;
+ const commonStart = [
+ { id, order, key: 'oauth-login', title: '刷新 OAuth 并登录', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' },
+ { id: id + 1, order: order + 10, key: 'fetch-login-code', title: '获取登录验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' },
+ ];
+
+ if (signupMethod === SIGNUP_METHOD_PHONE) {
+ if (isPhoneSignupReloginAfterBindEmailEnabled(options)) {
+ return [
+ ...commonStart,
+ { id: id + 2, order: order + 20, key: 'bind-email', title: '绑定邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'bind-email' },
+ { id: id + 3, order: order + 30, key: 'fetch-bind-email-code', title: '获取绑定邮箱验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fetch-bind-email-code', mailRuleId: 'openai-login-code' },
+ { id: id + 4, order: order + 40, key: 'relogin-bound-email', title: '绑定邮箱后刷新 OAuth 并登录(邮箱)', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' },
+ { id: id + 5, order: order + 50, key: 'fetch-bound-email-login-code', title: '获取登录验证码(邮箱)', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' },
+ { id: id + 6, order: order + 60, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' },
+ { id: id + 7, order: order + 70, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' },
+ ];
+ }
+ return [
+ ...commonStart,
+ { id: id + 2, order: order + 20, key: 'bind-email', title: '绑定邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'bind-email' },
+ { id: id + 3, order: order + 30, key: 'fetch-bind-email-code', title: '获取绑定邮箱验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fetch-bind-email-code', mailRuleId: 'openai-login-code' },
+ { id: id + 4, order: order + 40, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' },
+ { id: id + 5, order: order + 50, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' },
+ ];
+ }
+
+ return [
+ ...commonStart,
+ { id: id + 2, order: order + 20, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' },
+ { id: id + 3, order: order + 30, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' },
+ ];
+ }
+
+ function createHostedCheckoutSteps(prefixSteps, startId, startOrder, signupMethod = SIGNUP_METHOD_EMAIL, options = {}) {
+ const sessionTailFactory = resolvePlusSessionImportTail(options, signupMethod);
+ const tailSteps = sessionTailFactory
+ ? sessionTailFactory(startId, startOrder)
+ : createHostedCheckoutAuthTail(startId, startOrder, signupMethod, options);
+ return [
+ ...prefixSteps,
+ ...tailSteps,
+ ];
+ }
+
const NORMAL_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_EMAIL);
const NORMAL_PHONE_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE);
const NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true });
@@ -183,6 +232,23 @@
);
const PLUS_PAYPAL_PHONE_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE);
const PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true });
+ const PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_EMAIL);
+ const PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS = createHostedCheckoutSteps(
+ PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS,
+ 7,
+ 70,
+ SIGNUP_METHOD_EMAIL,
+ { plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION }
+ );
+ const PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS = createHostedCheckoutSteps(
+ PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS,
+ 7,
+ 70,
+ SIGNUP_METHOD_EMAIL,
+ { plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION }
+ );
+ const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE);
+ const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true });
const PLUS_GOPAY_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GOPAY_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL);
const PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS = createOpenAiSteps(
PLUS_GOPAY_PREFIX_STEP_DEFINITIONS,
@@ -313,6 +379,9 @@
function normalizePlusPaymentMethod(value = '') {
const normalized = String(value || '').trim().toLowerCase();
+ if (normalized === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
+ return PLUS_PAYMENT_METHOD_PAYPAL_HOSTED;
+ }
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_PAYMENT_METHOD_GPC_HELPER;
}
@@ -352,6 +421,20 @@
}
const paymentMethod = normalizePlusPaymentMethod(options?.plusPaymentMethod || options?.paymentMethod);
const plusAccountAccessStrategy = normalizePlusAccountAccessStrategy(options?.plusAccountAccessStrategy);
+ if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) {
+ if (signupMethod === SIGNUP_METHOD_PHONE) {
+ return reloginAfterBindEmail
+ ? PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS
+ : PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS;
+ }
+ if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
+ return PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS;
+ }
+ if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) {
+ return PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS;
+ }
+ return PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS;
+ }
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
if (signupMethod === SIGNUP_METHOD_PHONE) {
return reloginAfterBindEmail
@@ -433,6 +516,11 @@
...PLUS_PAYPAL_CPA_SESSION_STEP_DEFINITIONS,
...PLUS_PAYPAL_PHONE_STEP_DEFINITIONS,
...PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
+ ...PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS,
+ ...PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS,
+ ...PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS,
+ ...PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS,
+ ...PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
...PLUS_GOPAY_STEP_DEFINITIONS,
...PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS,
...PLUS_GOPAY_CPA_SESSION_STEP_DEFINITIONS,
@@ -639,6 +727,11 @@
PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS,
PLUS_PAYPAL_PHONE_STEP_DEFINITIONS,
PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
+ PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS,
+ PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS,
+ PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS,
+ PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS,
+ PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
PLUS_GOPAY_STEP_DEFINITIONS,
PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS,
PLUS_GOPAY_PHONE_STEP_DEFINITIONS,
diff --git a/gopay-utils.js b/gopay-utils.js
index 0c25a0a..1f110b4 100644
--- a/gopay-utils.js
+++ b/gopay-utils.js
@@ -2,6 +2,7 @@
root.GoPayUtils = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createGoPayUtils() {
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
+ const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
@@ -11,6 +12,9 @@
function normalizePlusPaymentMethod(value = '') {
const normalized = String(value || '').trim().toLowerCase();
+ if (normalized === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
+ return PLUS_PAYMENT_METHOD_PAYPAL_HOSTED;
+ }
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_PAYMENT_METHOD_GPC_HELPER;
}
@@ -416,6 +420,7 @@
PLUS_PAYMENT_METHOD_GPC_HELPER,
PLUS_PAYMENT_METHOD_GOPAY,
PLUS_PAYMENT_METHOD_PAYPAL,
+ PLUS_PAYMENT_METHOD_PAYPAL_HOSTED,
buildGpcCardBalanceUrl,
buildGpcApiKeyBalanceUrl,
buildGpcApiKeyHeaders,
diff --git a/shared/flow-capabilities.js b/shared/flow-capabilities.js
index d5f430f..4c05fa0 100644
--- a/shared/flow-capabilities.js
+++ b/shared/flow-capabilities.js
@@ -378,24 +378,31 @@
const requestedPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy(
options?.plusAccountAccessStrategy ?? state?.plusAccountAccessStrategy
);
+ const basePlusAccountAccessStrategies = [
+ PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH,
+ PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION,
+ PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION,
+ ];
const availablePlusAccountAccessStrategies = activeFlowId === 'openai'
&& Boolean(flowState.supportsPlusMode)
&& Boolean(runtimeLocks.plusModeEnabled)
&& effectiveSignupMethod === SIGNUP_METHOD_EMAIL
- ? (
- Array.isArray(targetState.supportedPlusAccountAccessStrategies)
- && targetState.supportedPlusAccountAccessStrategies.length > 0
- ? targetState.supportedPlusAccountAccessStrategies.slice()
- : [PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH]
- )
+ ? (runtimeLocks.accountContribution
+ ? [PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION]
+ : basePlusAccountAccessStrategies)
: [PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH];
- const effectivePlusAccountAccessStrategy = availablePlusAccountAccessStrategies.includes(requestedPlusAccountAccessStrategy)
+ const effectivePlusAccountAccessStrategy = runtimeLocks.accountContribution
+ && runtimeLocks.plusModeEnabled
+ && effectiveSignupMethod === SIGNUP_METHOD_EMAIL
+ ? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION
+ : availablePlusAccountAccessStrategies.includes(requestedPlusAccountAccessStrategy)
? requestedPlusAccountAccessStrategy
: PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
const canEditPlusAccountAccessStrategy = activeFlowId === 'openai'
&& Boolean(flowState.supportsPlusMode)
&& Boolean(runtimeLocks.plusModeEnabled)
&& effectiveSignupMethod === SIGNUP_METHOD_EMAIL
+ && !runtimeLocks.accountContribution
&& availablePlusAccountAccessStrategies.length > 1;
const visibleGroupIds = typeof flowRegistryApi.getVisibleGroupIds === 'function'
&& isRegisteredFlowId(activeFlowId)
diff --git a/shared/settings-schema.js b/shared/settings-schema.js
index 2f39824..8b6f7ad 100644
--- a/shared/settings-schema.js
+++ b/shared/settings-schema.js
@@ -105,8 +105,11 @@
},
plus: {
plusModeEnabled: false,
- plusPaymentMethod: 'paypal',
+ plusPaymentMethod: 'paypal-hosted',
plusAccountAccessStrategy: 'oauth',
+ hostedCheckoutVerificationUrl: '',
+ hostedCheckoutPhoneNumber: '',
+ plusHostedCheckoutOauthDelaySeconds: 3,
},
autoRun: {
stepExecutionRange: {
@@ -317,6 +320,24 @@
?? nested?.flows?.openai?.plus?.plusAccountAccessStrategy
?? defaults.flows.openai.plus.plusAccountAccessStrategy
),
+ hostedCheckoutVerificationUrl: String(
+ input?.hostedCheckoutVerificationUrl
+ ?? nested?.flows?.openai?.plus?.hostedCheckoutVerificationUrl
+ ?? defaults.flows.openai.plus.hostedCheckoutVerificationUrl
+ ).trim(),
+ hostedCheckoutPhoneNumber: String(
+ input?.hostedCheckoutPhoneNumber
+ ?? nested?.flows?.openai?.plus?.hostedCheckoutPhoneNumber
+ ?? defaults.flows.openai.plus.hostedCheckoutPhoneNumber
+ ).trim(),
+ plusHostedCheckoutOauthDelaySeconds: (() => {
+ const numeric = Number(
+ input?.plusHostedCheckoutOauthDelaySeconds
+ ?? nested?.flows?.openai?.plus?.plusHostedCheckoutOauthDelaySeconds
+ ?? defaults.flows.openai.plus.plusHostedCheckoutOauthDelaySeconds
+ );
+ return Math.min(120, Math.max(0, Math.floor(Number.isFinite(numeric) ? numeric : defaults.flows.openai.plus.plusHostedCheckoutOauthDelaySeconds)));
+ })(),
},
autoRun: {
stepExecutionRange: normalizeStepExecutionRangeEntry(
@@ -456,6 +477,9 @@
next.plusModeEnabled = openaiState.plus.plusModeEnabled;
next.plusPaymentMethod = openaiState.plus.plusPaymentMethod;
next.plusAccountAccessStrategy = openaiState.plus.plusAccountAccessStrategy;
+ next.hostedCheckoutVerificationUrl = openaiState.plus.hostedCheckoutVerificationUrl;
+ next.hostedCheckoutPhoneNumber = openaiState.plus.hostedCheckoutPhoneNumber;
+ next.plusHostedCheckoutOauthDelaySeconds = openaiState.plus.plusHostedCheckoutOauthDelaySeconds;
next.mailProvider = normalizedState.services.email.provider;
next.ipProxyEnabled = normalizedState.services.proxy.enabled;
next.ipProxyService = normalizedState.services.proxy.provider;
diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html
index 87d721b..1a92415 100644
--- a/sidepanel/sidepanel.html
+++ b/sidepanel/sidepanel.html
@@ -327,6 +327,7 @@
Plus 支付