From 4bb734fd1d1243d6ffa32d392d9d5ae2c564c403 Mon Sep 17 00:00:00 2001 From: QLHazyCoder <2825305047@qq.com> Date: Thu, 30 Apr 2026 23:43:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0Gopay=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background.js | 113 ++++++++++-- background/message-router.js | 65 ++++++- background/steps/create-plus-checkout.js | 31 +++- background/steps/gopay-manual-confirm.js | 94 ++++++++++ content/plus-checkout.js | 59 ++++-- data/step-definitions.js | 38 +++- sidepanel/sidepanel.js | 169 +++++++++++++++--- ...ackground-account-history-settings.test.js | 3 + tests/gopay-manual-confirm.test.js | 48 +++++ tests/plus-checkout-create-wait.test.js | 51 +++++- tests/step-definitions-module.test.js | 27 +++ 11 files changed, 631 insertions(+), 67 deletions(-) create mode 100644 background/steps/gopay-manual-confirm.js create mode 100644 tests/gopay-manual-confirm.test.js diff --git a/background.js b/background.js index 4d25881..271edeb 100644 --- a/background.js +++ b/background.js @@ -31,6 +31,7 @@ importScripts( 'background/steps/clear-login-cookies.js', 'background/steps/create-plus-checkout.js', 'background/steps/fill-plus-checkout.js', + 'background/steps/gopay-manual-confirm.js', 'background/steps/paypal-approve.js', 'background/steps/plus-return-confirm.js', 'background/steps/oauth-login.js', @@ -48,10 +49,19 @@ importScripts( ); const NORMAL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled: false }) || []; -const PLUS_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled: true }) || NORMAL_STEP_DEFINITIONS; +const PLUS_PAYPAL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ + plusModeEnabled: true, + plusPaymentMethod: 'paypal', +}) || NORMAL_STEP_DEFINITIONS; +const PLUS_GOPAY_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({ + plusModeEnabled: true, + plusPaymentMethod: 'gopay', +}) || PLUS_PAYPAL_STEP_DEFINITIONS; +const PLUS_STEP_DEFINITIONS = PLUS_PAYPAL_STEP_DEFINITIONS; const ALL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getAllSteps?.() || [ ...NORMAL_STEP_DEFINITIONS, - ...PLUS_STEP_DEFINITIONS, + ...PLUS_PAYPAL_STEP_DEFINITIONS, + ...PLUS_GOPAY_STEP_DEFINITIONS, ]; const STEP_IDS = Array.from(new Set(ALL_STEP_DEFINITIONS .map((definition) => Number(definition?.id)) @@ -61,13 +71,18 @@ const NORMAL_STEP_IDS = NORMAL_STEP_DEFINITIONS .map((definition) => Number(definition?.id)) .filter(Number.isFinite) .sort((left, right) => left - right); -const PLUS_STEP_IDS = PLUS_STEP_DEFINITIONS +const PLUS_PAYPAL_STEP_IDS = PLUS_PAYPAL_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) .sort((left, right) => left - right); const LAST_STEP_ID = Math.max( NORMAL_STEP_IDS[NORMAL_STEP_IDS.length - 1] || 10, - PLUS_STEP_IDS[PLUS_STEP_IDS.length - 1] || 10 + PLUS_PAYPAL_STEP_IDS[PLUS_PAYPAL_STEP_IDS.length - 1] || 10, + PLUS_GOPAY_STEP_IDS[PLUS_GOPAY_STEP_IDS.length - 1] || 10 ); const FINAL_OAUTH_CHAIN_START_STEP = 7; @@ -337,6 +352,10 @@ function isPlusModeState(state = {}) { return Boolean(state?.plusModeEnabled); } +function normalizePlusPaymentMethod(value = '') { + return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal'; +} + function normalizeContributionModeSource(value = '') { const normalized = String(value || '').trim().toLowerCase(); return normalized === CONTRIBUTION_SOURCE_SUB2API @@ -372,11 +391,21 @@ function resolveContributionModeRoutingState(state = {}) { } function getStepDefinitionsForState(state = {}) { - return isPlusModeState(state) ? PLUS_STEP_DEFINITIONS : NORMAL_STEP_DEFINITIONS; + if (!isPlusModeState(state)) { + return NORMAL_STEP_DEFINITIONS; + } + return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay' + ? PLUS_GOPAY_STEP_DEFINITIONS + : PLUS_PAYPAL_STEP_DEFINITIONS; } function getStepIdsForState(state = {}) { - return isPlusModeState(state) ? PLUS_STEP_IDS : NORMAL_STEP_IDS; + if (!isPlusModeState(state)) { + return NORMAL_STEP_IDS; + } + return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay' + ? PLUS_GOPAY_STEP_IDS + : PLUS_PAYPAL_STEP_IDS; } function getLastStepIdForState(state = {}) { @@ -573,6 +602,12 @@ const DEFAULT_STATE = { plusBillingAddress: null, plusPaypalApprovedAt: null, plusReturnUrl: '', + plusManualConfirmationPending: false, + plusManualConfirmationRequestId: '', + plusManualConfirmationStep: 0, + plusManualConfirmationMethod: '', + plusManualConfirmationTitle: '', + plusManualConfirmationMessage: '', flowStartTime: null, // 当前流程开始时间。 tabRegistry: {}, // 程序维护的标签页注册表。 sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。 @@ -6244,6 +6279,12 @@ function getDownstreamStateResets(step, state = {}) { plusBillingAddress: null, plusPaypalApprovedAt: null, plusReturnUrl: '', + plusManualConfirmationPending: false, + plusManualConfirmationRequestId: '', + plusManualConfirmationStep: 0, + plusManualConfirmationMethod: '', + plusManualConfirmationTitle: '', + plusManualConfirmationMessage: '', }; if (step <= 1) { @@ -6311,6 +6352,12 @@ function getDownstreamStateResets(step, state = {}) { plusBillingAddress: null, plusPaypalApprovedAt: null, plusReturnUrl: '', + plusManualConfirmationPending: false, + plusManualConfirmationRequestId: '', + plusManualConfirmationStep: 0, + plusManualConfirmationMethod: '', + plusManualConfirmationTitle: '', + plusManualConfirmationMessage: '', } : {}), ...(step === 8 ? { plusPaypalApprovedAt: null, @@ -7231,8 +7278,12 @@ const AUTO_RUN_BACKGROUND_COMPLETED_STEP_KEYS = new Set([ const STEP_COMPLETION_SIGNAL_STEP_KEYS = new Set([ 'fill-password', 'fill-profile', + 'gopay-subscription-confirm', 'platform-verify', ]); +const STEP_COMPLETION_SIGNAL_TIMEOUTS_BY_STEP_KEY = new Map([ + ['gopay-subscription-confirm', 1800000], +]); const AUTO_RUN_PRE_EXECUTION_DELAYS_BY_STEP_KEY = new Map([ ['plus-checkout-create', 20000], ]); @@ -7313,6 +7364,14 @@ function getAutoRunPreExecutionDelayMs(step, state = {}) { return 0; } +function getStepCompletionSignalTimeoutMs(step, state = {}) { + const stepKey = getStepExecutionKeyForState(step, state); + if (stepKey) { + return STEP_COMPLETION_SIGNAL_TIMEOUTS_BY_STEP_KEY.get(stepKey) || AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS; + } + return AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS; +} + function notifyStepComplete(step, payload) { const waiter = stepWaiters.get(step); console.log(LOG_PREFIX, `[notifyStepComplete] step ${step}, hasWaiter=${Boolean(waiter)}`); @@ -7393,8 +7452,12 @@ async function finalizeDeferredStepExecutionError(step, error) { await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, latestState, getErrorMessage(error)); } -async function executeStepViaCompletionSignal(step, timeoutMs = AUTO_RUN_SIGNAL_COMPLETION_TIMEOUT_MS) { - const completionResultPromise = waitForStepComplete(step, timeoutMs).then( +async function executeStepViaCompletionSignal(step, timeoutMs = 0) { + const executionState = await getState(); + const resolvedTimeoutMs = Number(timeoutMs) > 0 + ? timeoutMs + : getStepCompletionSignalTimeoutMs(step, executionState); + const completionResultPromise = waitForStepComplete(step, resolvedTimeoutMs).then( payload => ({ ok: true, payload }), error => ({ ok: false, error }), ); @@ -7592,6 +7655,19 @@ async function requestStop(options = {}) { } stepWaiters.clear(); + if (state.plusManualConfirmationPending) { + const clearManualConfirmationState = { + plusManualConfirmationPending: false, + plusManualConfirmationRequestId: '', + plusManualConfirmationStep: 0, + plusManualConfirmationMethod: '', + plusManualConfirmationTitle: '', + plusManualConfirmationMessage: '', + }; + await setState(clearManualConfirmationState); + broadcastDataUpdate(clearManualConfirmationState); + } + if (resumeWaiter) { resumeWaiter.reject(new Error(STOP_ERROR_MESSAGE)); resumeWaiter = null; @@ -8995,6 +9071,15 @@ const plusCheckoutBillingExecutor = self.MultiPageBackgroundPlusCheckoutBilling? waitForTabCompleteUntilStopped, waitForTabUrlMatchUntilStopped, }); +const goPayManualConfirmExecutor = self.MultiPageBackgroundGoPayManualConfirm?.createGoPayManualConfirmExecutor({ + addLog, + broadcastDataUpdate, + chrome, + getTabId, + isTabAlive, + registerTab, + setState, +}); const payPalApproveExecutor = self.MultiPageBackgroundPayPalApprove?.createPayPalApproveExecutor({ addLog, chrome, @@ -9045,6 +9130,7 @@ const stepExecutorsByKey = { 'clear-login-cookies': () => step6Executor.executeStep6(), 'plus-checkout-create': (state) => plusCheckoutCreateExecutor.executePlusCheckoutCreate(state), 'plus-checkout-billing': (state) => plusCheckoutBillingExecutor.executePlusCheckoutBilling(state), + 'gopay-subscription-confirm': (state) => goPayManualConfirmExecutor.executeGoPayManualConfirm(state), 'paypal-approve': (state) => payPalApproveExecutor.executePayPalApprove(state), 'plus-checkout-return': (state) => plusReturnConfirmExecutor.executePlusReturnConfirm(state), 'oauth-login': (state) => step7Executor.executeStep7(state), @@ -9070,6 +9156,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter clearStopRequest, closeLocalhostCallbackTabs, closeTabsByUrlPrefix, + completeStepFromBackground, deleteHotmailAccount, deleteHotmailAccounts, deleteIcloudAlias, @@ -9180,10 +9267,16 @@ function buildStepRegistry(definitions = []) { } const normalStepRegistry = buildStepRegistry(NORMAL_STEP_DEFINITIONS); -const plusStepRegistry = buildStepRegistry(PLUS_STEP_DEFINITIONS); +const plusPayPalStepRegistry = buildStepRegistry(PLUS_PAYPAL_STEP_DEFINITIONS); +const plusGoPayStepRegistry = buildStepRegistry(PLUS_GOPAY_STEP_DEFINITIONS); function getStepRegistryForState(state = {}) { - return isPlusModeState(state) ? plusStepRegistry : normalStepRegistry; + if (!isPlusModeState(state)) { + return normalStepRegistry; + } + return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay' + ? plusGoPayStepRegistry + : plusPayPalStepRegistry; } async function requestOAuthUrlFromPanel(state, options = {}) { diff --git a/background/message-router.js b/background/message-router.js index 9ce5526..21f1c6b 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -20,6 +20,7 @@ clearStopRequest, closeLocalhostCallbackTabs, closeTabsByUrlPrefix, + completeStepFromBackground, deleteHotmailAccount, deleteHotmailAccounts, deleteIcloudAlias, @@ -418,6 +419,54 @@ return { ok: true }; } + case 'RESOLVE_PLUS_MANUAL_CONFIRMATION': { + const currentState = await getState(); + const step = Number(message.payload?.step) || Number(currentState?.plusManualConfirmationStep) || 0; + const confirmed = Boolean(message.payload?.confirmed); + const requestId = String(message.payload?.requestId || '').trim(); + const currentRequestId = String(currentState?.plusManualConfirmationRequestId || '').trim(); + if (!currentState?.plusManualConfirmationPending) { + return { ok: true, ignored: true }; + } + if (requestId && currentRequestId && requestId !== currentRequestId) { + return { ok: true, ignored: true }; + } + + const clearManualConfirmationState = { + plusManualConfirmationPending: false, + plusManualConfirmationRequestId: '', + plusManualConfirmationStep: 0, + plusManualConfirmationMethod: '', + plusManualConfirmationTitle: '', + plusManualConfirmationMessage: '', + }; + await setState(clearManualConfirmationState); + if (typeof broadcastDataUpdate === 'function') { + broadcastDataUpdate(clearManualConfirmationState); + } + + if (confirmed) { + const methodLabel = String(currentState?.plusManualConfirmationMethod || '').trim().toLowerCase() === 'gopay' + ? 'GoPay' + : '手动'; + await addLog(`步骤 ${step}:已确认${methodLabel}订阅完成,准备继续下一步。`, 'ok'); + await completeStepFromBackground(step, { + plusManualConfirmationMethod: currentState?.plusManualConfirmationMethod || '', + plusManualConfirmedAt: Date.now(), + }); + return { ok: true }; + } + + const cancelMessage = String(currentState?.plusManualConfirmationMethod || '').trim().toLowerCase() === 'gopay' + ? '已取消 GoPay 订阅确认' + : '已取消当前手动确认'; + await setStepStatus(step, 'failed'); + await addLog(`步骤 ${step}:${cancelMessage}。`, 'warn'); + await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, null, cancelMessage); + notifyStepError(step, cancelMessage); + return { ok: true }; + } + case 'GET_STATE': { return await getState(); } @@ -649,12 +698,19 @@ const sessionUpdates = buildLuckmailSessionSettingsPayload(message.payload || {}); const modeChanged = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled') && Boolean(currentState?.plusModeEnabled) !== Boolean(updates.plusModeEnabled); + const plusPaymentChanged = Object.prototype.hasOwnProperty.call(updates, 'plusPaymentMethod') + && String(currentState?.plusPaymentMethod || 'paypal').trim().toLowerCase() + !== String(updates.plusPaymentMethod || 'paypal').trim().toLowerCase(); + const nextPlusModeEnabled = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled') + ? Boolean(updates.plusModeEnabled) + : Boolean(currentState?.plusModeEnabled); + const stepModeChanged = modeChanged || (nextPlusModeEnabled && plusPaymentChanged); await setPersistentSettings(updates); const stateUpdates = { ...updates, ...sessionUpdates, }; - if (modeChanged && typeof getStepIdsForState === 'function') { + if (stepModeChanged && typeof getStepIdsForState === 'function') { const nextStateForSteps = { ...currentState, ...stateUpdates }; stateUpdates.stepStatuses = Object.fromEntries( getStepIdsForState(nextStateForSteps).map((stepId) => [stepId, 'pending']) @@ -705,6 +761,13 @@ : 'Plus 模式已关闭,已恢复普通注册授权步骤。', 'info' ); + } else if (plusPaymentChanged && nextPlusModeEnabled) { + const selectedPlusPaymentMethod = String( + stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal' + ).trim().toLowerCase() === 'gopay' + ? 'GoPay' + : 'PayPal'; + await addLog(`Plus 鏀粯鏂瑰紡宸插垏鎹负 ${selectedPlusPaymentMethod}锛屽凡鏇存柊瀵瑰簲鐨?Plus 姝ラ銆?`, 'info'); } return { ok: true, state: await getState(), proxyRouting }; } diff --git a/background/steps/create-plus-checkout.js b/background/steps/create-plus-checkout.js index cc319e4..2af4366 100644 --- a/background/steps/create-plus-checkout.js +++ b/background/steps/create-plus-checkout.js @@ -18,11 +18,21 @@ waitForTabCompleteUntilStopped, } = deps; + function normalizePlusPaymentMethod(value = '') { + return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal'; + } + + function getCheckoutModeLabel(state = {}) { + return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay' + ? 'GoPay 订阅页' + : 'Plus Checkout'; + } + async function openFreshChatGptTabForCheckoutCreate() { const tab = await chrome.tabs.create({ url: PLUS_CHECKOUT_ENTRY_URL, active: true }); const tabId = Number(tab?.id); if (!Number.isInteger(tabId)) { - throw new Error('步骤 6:打开 ChatGPT 页面失败,未获取到有效标签页 ID。'); + throw new Error('步骤 6:打开 ChatGPT 页面失败,无法创建订阅页。'); } if (typeof registerTab === 'function') { await registerTab(PLUS_CHECKOUT_SOURCE, tabId); @@ -30,8 +40,9 @@ return tabId; } - async function executePlusCheckoutCreate() { - await addLog('步骤 6:正在新打开 ChatGPT 会话页,准备创建 Plus Checkout...', 'info'); + async function executePlusCheckoutCreate(state = {}) { + const checkoutModeLabel = getCheckoutModeLabel(state); + await addLog(`步骤 6:正在打开新的 ChatGPT 会话,准备创建${checkoutModeLabel}...`, 'info'); const tabId = await openFreshChatGptTabForCheckoutCreate(); await waitForTabCompleteUntilStopped(tabId); @@ -39,30 +50,32 @@ await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, { inject: PLUS_CHECKOUT_INJECT_FILES, injectSource: PLUS_CHECKOUT_SOURCE, - logMessage: '步骤 6:ChatGPT 页面仍在加载,等待 Plus Checkout 脚本就绪...', + logMessage: '步骤 6:正在等待 ChatGPT 页面完成加载,再继续创建订阅页...', }); const result = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, { type: 'CREATE_PLUS_CHECKOUT', source: 'background', - payload: {}, + payload: { + paymentMethod: normalizePlusPaymentMethod(state?.plusPaymentMethod), + }, }); if (result?.error) { throw new Error(result.error); } if (!result?.checkoutUrl) { - throw new Error('步骤 6:Plus Checkout 创建后未返回支付链接。'); + throw new Error(`步骤 6:${checkoutModeLabel}未返回可用的订阅链接。`); } - await addLog('步骤 6:Plus Checkout 已创建,正在打开支付页面...', 'ok'); + await addLog(`步骤 6:${checkoutModeLabel}已创建,正在打开订阅页面...`, 'ok'); await chrome.tabs.update(tabId, { url: result.checkoutUrl, active: true }); await waitForTabCompleteUntilStopped(tabId); await sleepWithStop(1000); await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, { inject: PLUS_CHECKOUT_INJECT_FILES, injectSource: PLUS_CHECKOUT_SOURCE, - logMessage: '步骤 6:Checkout 页面仍在加载,等待页面脚本就绪...', + logMessage: '步骤 6:正在等待订阅页面完成加载...', }); await setState({ @@ -72,7 +85,7 @@ plusCheckoutCurrency: result.currency || 'EUR', }); - await addLog('步骤 6:Plus Checkout 页面已就绪,准备继续下一步。', 'info'); + await addLog(`步骤 6:${checkoutModeLabel}已就绪。`, 'info'); await completeStepFromBackground(6, { plusCheckoutCountry: result.country || 'DE', diff --git a/background/steps/gopay-manual-confirm.js b/background/steps/gopay-manual-confirm.js new file mode 100644 index 0000000..f33724c --- /dev/null +++ b/background/steps/gopay-manual-confirm.js @@ -0,0 +1,94 @@ +(function attachBackgroundGoPayManualConfirm(root, factory) { + root.MultiPageBackgroundGoPayManualConfirm = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundGoPayManualConfirmModule() { + const PLUS_CHECKOUT_SOURCE = 'plus-checkout'; + const DEFAULT_CONFIRM_TITLE = 'GoPay 订阅确认'; + const DEFAULT_CONFIRM_MESSAGE = 'GoPay 订阅页已打开。请先手动完成订阅,完成后确认继续 OAuth 登录。'; + + function createGoPayManualConfirmExecutor(deps = {}) { + const { + addLog, + broadcastDataUpdate, + chrome, + getTabId, + isTabAlive, + registerTab, + setState, + } = deps; + + function buildRequestId() { + return `gopay-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + } + + async function resolveCheckoutTabId(state = {}) { + const registeredTabId = typeof getTabId === 'function' + ? await getTabId(PLUS_CHECKOUT_SOURCE) + : null; + if (registeredTabId && typeof isTabAlive === 'function' && await isTabAlive(PLUS_CHECKOUT_SOURCE)) { + return Number(registeredTabId) || 0; + } + + const storedTabId = Number(state?.plusCheckoutTabId) || 0; + if (storedTabId && chrome?.tabs?.get) { + const tab = await chrome.tabs.get(storedTabId).catch(() => null); + if (tab?.id) { + if (typeof registerTab === 'function') { + await registerTab(PLUS_CHECKOUT_SOURCE, tab.id); + } + return tab.id; + } + } + + const checkoutUrl = String(state?.plusCheckoutUrl || '').trim(); + if (!checkoutUrl) { + throw new Error('步骤 7:未检测到 GoPay 订阅页,请先执行步骤 6。'); + } + + if (!chrome?.tabs?.create) { + throw new Error('步骤 7:无法自动重新打开 GoPay 订阅页。'); + } + + const tab = await chrome.tabs.create({ url: checkoutUrl, active: true }); + const tabId = Number(tab?.id) || 0; + if (!tabId) { + throw new Error('步骤 7:重新打开 GoPay 订阅页失败。'); + } + if (typeof registerTab === 'function') { + await registerTab(PLUS_CHECKOUT_SOURCE, tabId); + } + return tabId; + } + + async function executeGoPayManualConfirm(state = {}) { + const tabId = await resolveCheckoutTabId(state); + if (chrome?.tabs?.update && tabId) { + await chrome.tabs.update(tabId, { active: true }).catch(() => {}); + } + + const payload = { + plusCheckoutTabId: tabId, + plusManualConfirmationPending: true, + plusManualConfirmationRequestId: buildRequestId(), + plusManualConfirmationStep: 7, + plusManualConfirmationMethod: 'gopay', + plusManualConfirmationTitle: DEFAULT_CONFIRM_TITLE, + plusManualConfirmationMessage: DEFAULT_CONFIRM_MESSAGE, + }; + + await setState(payload); + if (typeof broadcastDataUpdate === 'function') { + broadcastDataUpdate(payload); + } + + await addLog('步骤 7:正在等待手动完成 GoPay 订阅,确认后继续 OAuth 登录。', 'info'); + } + + return { + executeGoPayManualConfirm, + }; + } + + return { + createGoPayManualConfirmExecutor, + }; +}); diff --git a/content/plus-checkout.js b/content/plus-checkout.js index 216b194..e409849 100644 --- a/content/plus-checkout.js +++ b/content/plus-checkout.js @@ -5,19 +5,33 @@ console.log('[MultiPage:plus-checkout] Content script loaded on', location.href) window.__MULTIPAGE_PLUS_CHECKOUT_READY__ = true; const PLUS_CHECKOUT_LISTENER_SENTINEL = 'data-multipage-plus-checkout-listener'; -const PLUS_CHECKOUT_PAYLOAD = { +const PLUS_CHECKOUT_BASE_PAYLOAD = { entry_point: 'all_plans_pricing_modal', plan_name: 'chatgptplusplan', - billing_details: { - country: 'DE', - currency: 'EUR', - }, checkout_ui_mode: 'custom', promo_campaign: { promo_campaign_id: 'plus-1-month-free', is_coupon_from_query_param: false, }, }; +const PLUS_CHECKOUT_CONFIGS = { + paypal: { + billing_details: { + country: 'DE', + currency: 'EUR', + }, + checkoutUrlPrefix: 'https://chatgpt.com/checkout/openai_ie/', + paymentLabel: 'PayPal', + }, + gopay: { + billing_details: { + country: 'ID', + currency: 'IDR', + }, + checkoutUrlPrefix: 'https://chatgpt.com/checkout/openai_llc/', + paymentLabel: 'GoPay', + }, +}; const PAYPAL_DIAGNOSTIC_LOG_INTERVAL_MS = 5000; if (document.documentElement.getAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL) !== '1') { @@ -55,7 +69,7 @@ if (document.documentElement.getAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL) !== ' async function handlePlusCheckoutCommand(message) { switch (message.type) { case 'CREATE_PLUS_CHECKOUT': - return createPlusCheckoutSession(); + return createPlusCheckoutSession(message.payload || {}); case 'FILL_PLUS_BILLING_AND_SUBMIT': return fillPlusBillingAndSubmit(message.payload || {}); case 'PLUS_CHECKOUT_SELECT_PAYPAL': @@ -112,6 +126,27 @@ function normalizeText(text = '') { return String(text || '').replace(/\s+/g, ' ').trim(); } +function normalizePlusPaymentMethod(value = '') { + return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal'; +} + +function buildPlusCheckoutConfig(payload = {}) { + const paymentMethod = normalizePlusPaymentMethod(payload.paymentMethod); + const config = PLUS_CHECKOUT_CONFIGS[paymentMethod] || PLUS_CHECKOUT_CONFIGS.paypal; + return { + paymentMethod, + paymentLabel: config.paymentLabel, + checkoutUrlPrefix: config.checkoutUrlPrefix, + checkoutPayload: { + ...PLUS_CHECKOUT_BASE_PAYLOAD, + billing_details: { + country: config.billing_details.country, + currency: config.billing_details.currency, + }, + }, + }; +} + function getActionText(el) { return normalizeText([ el?.textContent, @@ -400,8 +435,9 @@ function writePayPalDiagnostics(reason, level = 'info') { return diagnostics; } -async function createPlusCheckoutSession() { +async function createPlusCheckoutSession(payload = {}) { await waitForDocumentComplete(); + const checkoutConfig = buildPlusCheckoutConfig(payload); log('Plus:正在读取 ChatGPT 登录会话...'); const sessionResponse = await fetch('/api/auth/session', { @@ -421,7 +457,7 @@ async function createPlusCheckoutSession() { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, - body: JSON.stringify(PLUS_CHECKOUT_PAYLOAD), + body: JSON.stringify(checkoutConfig.checkoutPayload), }); const data = await response.json().catch(() => ({})); @@ -431,9 +467,10 @@ async function createPlusCheckoutSession() { } return { - checkoutUrl: `https://chatgpt.com/checkout/openai_ie/${data.checkout_session_id}`, - country: PLUS_CHECKOUT_PAYLOAD.billing_details.country, - currency: PLUS_CHECKOUT_PAYLOAD.billing_details.currency, + checkoutUrl: `${checkoutConfig.checkoutUrlPrefix}${data.checkout_session_id}`, + country: checkoutConfig.checkoutPayload.billing_details.country, + currency: checkoutConfig.checkoutPayload.billing_details.currency, + paymentMethod: checkoutConfig.paymentMethod, }; } diff --git a/data/step-definitions.js b/data/step-definitions.js index 3852332..eaa765a 100644 --- a/data/step-definitions.js +++ b/data/step-definitions.js @@ -14,7 +14,7 @@ { id: 10, order: 100, key: 'platform-verify', title: '平台回调验证' }, ]; - const PLUS_STEP_DEFINITIONS = [ + const PLUS_PAYPAL_STEP_DEFINITIONS = [ { id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网' }, { id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱' }, { id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' }, @@ -30,12 +30,35 @@ { id: 13, order: 130, key: 'platform-verify', title: '平台回调验证' }, ]; + const PLUS_GOPAY_STEP_DEFINITIONS = [ + { id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网' }, + { id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱' }, + { id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' }, + { id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码' }, + { id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日' }, + { id: 6, order: 60, key: 'plus-checkout-create', title: '打开 GoPay 订阅页' }, + { id: 7, order: 70, key: 'gopay-subscription-confirm', title: '等待 GoPay 订阅确认' }, + { id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录' }, + { id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码' }, + { id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth' }, + { id: 13, order: 130, key: 'platform-verify', title: '平台回调验证' }, + ]; + function isPlusModeEnabled(options = {}) { return Boolean(options?.plusModeEnabled || options?.plusMode); } + function normalizePlusPaymentMethod(value = '') { + return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal'; + } + function getModeStepDefinitions(options = {}) { - return isPlusModeEnabled(options) ? PLUS_STEP_DEFINITIONS : NORMAL_STEP_DEFINITIONS; + if (!isPlusModeEnabled(options)) { + return NORMAL_STEP_DEFINITIONS; + } + return normalizePlusPaymentMethod(options?.plusPaymentMethod || options?.paymentMethod) === 'gopay' + ? PLUS_GOPAY_STEP_DEFINITIONS + : PLUS_PAYPAL_STEP_DEFINITIONS; } function cloneSteps(steps = []) { @@ -48,7 +71,11 @@ function getAllSteps() { const keyed = new Map(); - for (const step of [...NORMAL_STEP_DEFINITIONS, ...PLUS_STEP_DEFINITIONS]) { + for (const step of [ + ...NORMAL_STEP_DEFINITIONS, + ...PLUS_PAYPAL_STEP_DEFINITIONS, + ...PLUS_GOPAY_STEP_DEFINITIONS, + ]) { keyed.set(`${step.id}:${step.key}`, step); } return cloneSteps(Array.from(keyed.values()).sort((left, right) => { @@ -80,12 +107,15 @@ return { STEP_DEFINITIONS: NORMAL_STEP_DEFINITIONS, NORMAL_STEP_DEFINITIONS, - PLUS_STEP_DEFINITIONS, + PLUS_STEP_DEFINITIONS: PLUS_PAYPAL_STEP_DEFINITIONS, + PLUS_PAYPAL_STEP_DEFINITIONS, + PLUS_GOPAY_STEP_DEFINITIONS, getAllSteps, getLastStepId, getStepById, getStepIds, getSteps, isPlusModeEnabled, + normalizePlusPaymentMethod, }; }); diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 8383489..36615b6 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -356,10 +356,11 @@ const btnAutoStartContinue = document.getElementById('btn-auto-start-continue'); const autoHintText = document.querySelector('.auto-hint'); const stepsList = document.querySelector('.steps-list'); let currentPlusModeEnabled = false; +let currentPlusPaymentMethod = 'paypal'; let heroSmsCountrySelectionOrder = []; let heroSmsCountryMenuSearchKeyword = ''; const heroSmsCountrySearchTextById = new Map(); -let stepDefinitions = getStepDefinitionsForMode(false); +let stepDefinitions = getStepDefinitionsForMode(false, currentPlusPaymentMethod); let STEP_IDS = stepDefinitions.map((step) => Number(step.id)).filter(Number.isFinite); let STEP_DEFAULT_STATUSES = Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending'])); let SKIPPABLE_STEPS = new Set(STEP_IDS); @@ -453,8 +454,22 @@ const AUTO_RUN_PLUS_RISK_PROMPT_DISMISSED_STORAGE_KEY = 'multipage-auto-run-plus const PLUS_CONTRIBUTION_PROMPT_LEDGER_STORAGE_KEY = 'multipage-plus-contribution-prompt-ledger'; const PHONE_VERIFICATION_SECTION_EXPANDED_STORAGE_KEY = 'multipage-phone-verification-section-expanded'; -function getStepDefinitionsForMode(plusModeEnabled = false) { - return (window.MultiPageStepDefinitions?.getSteps?.({ plusModeEnabled }) || []) +function normalizePlusPaymentMethod(value = '') { + return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal'; +} + +function getSelectedPlusPaymentMethod() { + if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) { + return normalizePlusPaymentMethod(selectPlusPaymentMethod.value); + } + return normalizePlusPaymentMethod(latestState?.plusPaymentMethod || currentPlusPaymentMethod); +} + +function getStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethod = 'paypal') { + return (window.MultiPageStepDefinitions?.getSteps?.({ + plusModeEnabled, + plusPaymentMethod: normalizePlusPaymentMethod(plusPaymentMethod), + }) || []) .sort((left, right) => { const leftOrder = Number.isFinite(left.order) ? left.order : left.id; const rightOrder = Number.isFinite(right.order) ? right.order : right.id; @@ -463,9 +478,10 @@ function getStepDefinitionsForMode(plusModeEnabled = false) { }); } -function rebuildStepDefinitionState(plusModeEnabled = false) { +function rebuildStepDefinitionState(plusModeEnabled = false, plusPaymentMethod = 'paypal') { currentPlusModeEnabled = Boolean(plusModeEnabled); - stepDefinitions = getStepDefinitionsForMode(currentPlusModeEnabled); + currentPlusPaymentMethod = normalizePlusPaymentMethod(plusPaymentMethod); + stepDefinitions = getStepDefinitionsForMode(currentPlusModeEnabled, currentPlusPaymentMethod); STEP_IDS = stepDefinitions.map((step) => Number(step.id)).filter(Number.isFinite); STEP_DEFAULT_STATUSES = Object.fromEntries(STEP_IDS.map((stepId) => [stepId, 'pending'])); SKIPPABLE_STEPS = new Set(STEP_IDS); @@ -758,6 +774,8 @@ let cloudflareTempEmailDomainEditMode = false; let modalChoiceResolver = null; let currentModalActions = []; let modalResultBuilder = null; +let activePlusManualConfirmationRequestId = ''; +let plusManualConfirmationDialogInFlight = false; let scheduledCountdownTimer = null; let configMenuOpen = false; let configActionInFlight = false; @@ -1194,6 +1212,98 @@ async function openConfirmModalWithOption({ }; } + +// Override the initial GoPay confirmation helpers so the dialog can +// recover after an accidental close instead of silently leaving step 7 hanging. +async function openPlusManualConfirmationDialog(options = {}) { + const method = String(options.method || '').trim().toLowerCase(); + const title = String(options.title || '').trim() || (method === 'gopay' ? 'GoPay 订阅确认' : '手动确认'); + const message = String(options.message || '').trim() + || (method === 'gopay' + ? '请在当前订阅页中手动完成 GoPay 订阅,完成后点击“我已完成订阅”继续。' + : '请先在页面中完成当前手动操作,完成后点击确认继续。'); + return openActionModal({ + title, + message, + actions: [ + { id: 'cancel', label: '取消等待', variant: 'btn-ghost' }, + { id: 'confirm', label: '我已完成订阅', variant: 'btn-primary' }, + ], + alert: method === 'gopay' + ? { text: '确认后流程会直接继续到 Plus 模式第 10 步 OAuth 登录。', tone: 'info' } + : null, + }); +} + +async function syncPlusManualConfirmationDialog() { + const requestId = String(latestState?.plusManualConfirmationRequestId || '').trim(); + const pending = Boolean(latestState?.plusManualConfirmationPending); + if (!pending || !requestId || plusManualConfirmationDialogInFlight || activePlusManualConfirmationRequestId === requestId) { + return; + } + + const step = Number(latestState?.plusManualConfirmationStep) || 0; + const method = String(latestState?.plusManualConfirmationMethod || '').trim().toLowerCase(); + const title = latestState?.plusManualConfirmationTitle; + const message = latestState?.plusManualConfirmationMessage; + activePlusManualConfirmationRequestId = requestId; + plusManualConfirmationDialogInFlight = true; + let shouldReopenDialog = false; + + try { + const choice = await openPlusManualConfirmationDialog({ + method, + title, + message, + }); + const currentRequestId = String(latestState?.plusManualConfirmationRequestId || '').trim(); + const stillPending = Boolean(latestState?.plusManualConfirmationPending); + if (!stillPending || currentRequestId !== requestId) { + return; + } + if (choice == null) { + shouldReopenDialog = true; + showToast('当前订阅确认仍在等待中,将重新弹出确认窗口。', 'info', 1800); + return; + } + + const confirmed = choice === 'confirm'; + const response = await chrome.runtime.sendMessage({ + type: 'RESOLVE_PLUS_MANUAL_CONFIRMATION', + source: 'sidepanel', + payload: { + step, + requestId, + confirmed, + }, + }); + if (response?.error) { + throw new Error(response.error); + } + if (confirmed) { + showToast(method === 'gopay' ? 'GoPay 订阅已确认,正在继续 OAuth 登录...' : '已确认,流程继续执行中...', 'info', 2200); + } else { + showToast(method === 'gopay' ? '已取消 GoPay 订阅等待。' : '已取消当前手动确认。', 'warn', 2200); + } + } catch (error) { + showToast(error?.message || String(error || '未知错误'), 'error'); + } finally { + if (activePlusManualConfirmationRequestId === requestId) { + activePlusManualConfirmationRequestId = ''; + } + plusManualConfirmationDialogInFlight = false; + if ( + shouldReopenDialog + && latestState?.plusManualConfirmationPending + && String(latestState?.plusManualConfirmationRequestId || '').trim() === requestId + ) { + setTimeout(() => { + void syncPlusManualConfirmationDialog(); + }, 0); + } + } +} + function isPromptDismissed(storageKey) { return localStorage.getItem(storageKey) === '1'; } @@ -2447,9 +2557,13 @@ function collectSettingsPayload() { const currentPayPalAccount = typeof getCurrentPayPalAccount === 'function' ? getCurrentPayPalAccount(latestState) : payPalAccounts.find((account) => account?.id === String(latestState?.currentPayPalAccountId || '').trim()) || null; - const plusPaymentMethod = typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod - ? (String(selectPlusPaymentMethod.value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal') - : (String(latestState?.plusPaymentMethod || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal'); + const plusPaymentMethod = typeof getSelectedPlusPaymentMethod === 'function' + ? getSelectedPlusPaymentMethod() + : (String( + (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod + ? selectPlusPaymentMethod.value + : latestState?.plusPaymentMethod) || '' + ).trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal'); return { ...(contributionModeEnabled ? {} : { panelMode: selectPanelMode.value, @@ -3593,9 +3707,7 @@ function updatePlusModeUI() { const enabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled ? Boolean(inputPlusModeEnabled.checked) : false; - const paymentMethod = typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod - ? (String(selectPlusPaymentMethod.value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal') - : (String(latestState?.plusPaymentMethod || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal'); + const paymentMethod = getSelectedPlusPaymentMethod(); if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) { selectPlusPaymentMethod.value = paymentMethod; selectPlusPaymentMethod.style.display = enabled ? '' : 'none'; @@ -3842,14 +3954,17 @@ function renderStepsList() { updateButtonStates(); } -function syncStepDefinitionsForMode(plusModeEnabled = false, options = {}) { +function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethod = 'paypal', options = {}) { const nextPlusModeEnabled = Boolean(plusModeEnabled); - const shouldRender = Boolean(options.render) || nextPlusModeEnabled !== currentPlusModeEnabled; + const nextPlusPaymentMethod = normalizePlusPaymentMethod(plusPaymentMethod); + const shouldRender = Boolean(options.render) + || nextPlusModeEnabled !== currentPlusModeEnabled + || nextPlusPaymentMethod !== currentPlusPaymentMethod; if (!shouldRender) { return; } - rebuildStepDefinitionState(nextPlusModeEnabled); + rebuildStepDefinitionState(nextPlusModeEnabled, nextPlusPaymentMethod); renderStepsList(); } @@ -3859,7 +3974,7 @@ function syncStepDefinitionsForMode(plusModeEnabled = false, options = {}) { function applySettingsState(state) { if (typeof syncStepDefinitionsForMode === 'function') { - syncStepDefinitionsForMode(Boolean(state?.plusModeEnabled)); + syncStepDefinitionsForMode(Boolean(state?.plusModeEnabled), state?.plusPaymentMethod); } const fallbackIpProxyService = '711proxy'; const fallbackIpProxyMode = 'account'; @@ -3909,9 +4024,7 @@ function applySettingsState(state) { inputPlusModeEnabled.checked = Boolean(state?.plusModeEnabled); } if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) { - selectPlusPaymentMethod.value = String(state?.plusPaymentMethod || '').trim().toLowerCase() === 'gopay' - ? 'gopay' - : 'paypal'; + selectPlusPaymentMethod.value = normalizePlusPaymentMethod(state?.plusPaymentMethod); } inputVpsUrl.value = state?.vpsUrl || ''; inputVpsPassword.value = state?.vpsPassword || ''; @@ -4182,6 +4295,9 @@ function applySettingsState(state) { queueLuckmailPurchaseRefresh(); } updateButtonStates(); + if (typeof syncPlusManualConfirmationDialog === 'function') { + void syncPlusManualConfirmationDialog(); + } } async function restoreState() { @@ -6801,16 +6917,15 @@ inputPassword.addEventListener('blur', () => { inputPlusModeEnabled?.addEventListener('change', () => { updatePlusModeUI(); - syncStepDefinitionsForMode(Boolean(inputPlusModeEnabled.checked), { render: true }); + syncStepDefinitionsForMode(Boolean(inputPlusModeEnabled.checked), getSelectedPlusPaymentMethod(), { render: true }); markSettingsDirty(true); saveSettings({ silent: true }).catch(() => { }); }); selectPlusPaymentMethod?.addEventListener('change', () => { - selectPlusPaymentMethod.value = String(selectPlusPaymentMethod.value || '').trim().toLowerCase() === 'gopay' - ? 'gopay' - : 'paypal'; + selectPlusPaymentMethod.value = normalizePlusPaymentMethod(selectPlusPaymentMethod.value); updatePlusModeUI(); + syncStepDefinitionsForMode(Boolean(inputPlusModeEnabled?.checked), selectPlusPaymentMethod.value, { render: true }); markSettingsDirty(true); saveSettings({ silent: true }).catch(() => { }); }); @@ -8149,11 +8264,14 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { inputPlusModeEnabled.checked = Boolean(message.payload.plusModeEnabled); } if (message.payload.plusPaymentMethod !== undefined && selectPlusPaymentMethod) { - selectPlusPaymentMethod.value = String(message.payload.plusPaymentMethod || '').trim().toLowerCase() === 'gopay' - ? 'gopay' - : 'paypal'; + selectPlusPaymentMethod.value = normalizePlusPaymentMethod(message.payload.plusPaymentMethod); } if (message.payload.plusModeEnabled !== undefined || message.payload.plusPaymentMethod !== undefined) { + syncStepDefinitionsForMode( + Boolean(latestState?.plusModeEnabled), + latestState?.plusPaymentMethod, + { render: true } + ); updatePlusModeUI(); } if (message.payload.currentHotmailAccountId !== undefined || message.payload.hotmailAccounts !== undefined) { @@ -8343,6 +8461,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { } updateAccountRunHistorySettingsUI(); renderContributionMode(); + void syncPlusManualConfirmationDialog(); break; } diff --git a/tests/background-account-history-settings.test.js b/tests/background-account-history-settings.test.js index 40838ab..8734d4a 100644 --- a/tests/background-account-history-settings.test.js +++ b/tests/background-account-history-settings.test.js @@ -123,6 +123,9 @@ return { assert.equal(api.normalizePersistentSettingValue('accountRunHistoryTextEnabled', 1), true); assert.equal(api.normalizePersistentSettingValue('phoneVerificationEnabled', 1), true); + assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'gopay'), 'gopay'); + assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'paypal'), 'paypal'); + assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'unknown'), 'paypal'); assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '7'), 7); assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '-1'), 0); assert.equal(api.normalizePersistentSettingValue('phoneVerificationReplacementLimit', '9'), 9); diff --git a/tests/gopay-manual-confirm.test.js b/tests/gopay-manual-confirm.test.js new file mode 100644 index 0000000..6bd7368 --- /dev/null +++ b/tests/gopay-manual-confirm.test.js @@ -0,0 +1,48 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const source = fs.readFileSync('background/steps/gopay-manual-confirm.js', 'utf8'); +const globalScope = {}; +const api = new Function('self', `${source}; return self.MultiPageBackgroundGoPayManualConfirm;`)(globalScope); + +test('GoPay manual confirm executor publishes pending manual confirmation state for step 7', async () => { + const stateUpdates = []; + const broadcasts = []; + const events = []; + const executor = api.createGoPayManualConfirmExecutor({ + addLog: async (message, level = 'info') => { + events.push({ type: 'log', message, level }); + }, + broadcastDataUpdate: (payload) => { + broadcasts.push(payload); + }, + chrome: { + tabs: { + update: async (tabId, payload) => { + events.push({ type: 'tab-update', tabId, payload }); + }, + }, + }, + getTabId: async () => 42, + isTabAlive: async () => true, + registerTab: async () => {}, + setState: async (payload) => { + stateUpdates.push(payload); + }, + }); + + await executor.executeGoPayManualConfirm({ plusCheckoutTabId: 42, plusCheckoutUrl: 'https://chatgpt.com/checkout/openai_llc/session' }); + + assert.equal(stateUpdates.length, 1); + assert.equal(stateUpdates[0].plusManualConfirmationPending, true); + assert.equal(stateUpdates[0].plusManualConfirmationMethod, 'gopay'); + assert.equal(stateUpdates[0].plusManualConfirmationStep, 7); + assert.match(String(stateUpdates[0].plusManualConfirmationRequestId || ''), /^gopay-/); + assert.equal(broadcasts.length, 1); + assert.equal(broadcasts[0].plusManualConfirmationPending, true); + assert.deepStrictEqual( + events.find((event) => event.type === 'tab-update'), + { type: 'tab-update', tabId: 42, payload: { active: true } } + ); +}); diff --git a/tests/plus-checkout-create-wait.test.js b/tests/plus-checkout-create-wait.test.js index 6f606ac..48d12a1 100644 --- a/tests/plus-checkout-create-wait.test.js +++ b/tests/plus-checkout-create-wait.test.js @@ -32,11 +32,14 @@ test('Plus checkout create does not wait 20 seconds after opening checkout page' registerTab: async (source, tabId) => { events.push({ type: 'register', source, tabId }); }, - sendTabMessageUntilStopped: async () => ({ - checkoutUrl: 'https://checkout.stripe.com/c/pay/session', - country: 'US', - currency: 'USD', - }), + sendTabMessageUntilStopped: async (tabId, source, message) => { + events.push({ type: 'tab-message', tabId, source, message }); + return { + checkoutUrl: 'https://checkout.stripe.com/c/pay/session', + country: 'US', + currency: 'USD', + }; + }, setState: async (payload) => { events.push({ type: 'set-state', payload }); }, @@ -61,11 +64,45 @@ test('Plus checkout create does not wait 20 seconds after opening checkout page' const sleepEvents = events.filter((event) => event.type === 'sleep'); assert.deepStrictEqual(sleepEvents.map((event) => event.ms), [1000, 1000]); + assert.deepStrictEqual( + events.find((event) => event.type === 'tab-message')?.message?.payload, + { paymentMethod: 'paypal' } + ); const completeIndex = events.findIndex((event) => event.type === 'complete'); - const readyLogIndex = events.findIndex((event) => event.type === 'log' && /Plus Checkout 页面已就绪/.test(event.message)); + const readyLogIndex = events.findIndex((event) => event.type === 'log' && /已就绪/.test(event.message)); assert.ok(readyLogIndex > -1); assert.ok(completeIndex > readyLogIndex); assert.equal(events.some((event) => event.type === 'sleep' && event.ms === 20000), false); - assert.equal(events.some((event) => event.type === 'log' && /固定等待 20 秒后继续下一步/.test(event.message)), false); +}); + +test('GoPay plus checkout create forwards gopay payment method to the checkout content script', async () => { + const events = []; + const executor = api.createPlusCheckoutCreateExecutor({ + addLog: async () => {}, + chrome: { + tabs: { + create: async () => ({ id: 99 }), + update: async () => {}, + }, + }, + completeStepFromBackground: async () => {}, + ensureContentScriptReadyOnTabUntilStopped: async () => {}, + registerTab: async () => {}, + sendTabMessageUntilStopped: async (_tabId, _source, message) => { + events.push(message); + return { + checkoutUrl: 'https://chatgpt.com/checkout/openai_llc/test-session', + country: 'ID', + currency: 'IDR', + }; + }, + setState: async () => {}, + sleepWithStop: async () => {}, + waitForTabCompleteUntilStopped: async () => {}, + }); + + await executor.executePlusCheckoutCreate({ plusPaymentMethod: 'gopay' }); + + assert.deepStrictEqual(events[0]?.payload, { paymentMethod: 'gopay' }); }); diff --git a/tests/step-definitions-module.test.js b/tests/step-definitions-module.test.js index 9c08fd1..a3b7769 100644 --- a/tests/step-definitions-module.test.js +++ b/tests/step-definitions-module.test.js @@ -9,6 +9,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', () const api = new Function('self', `${source}; return self.MultiPageStepDefinitions;`)(globalScope); const steps = api.getSteps(); const plusSteps = api.getSteps({ plusModeEnabled: true }); + const goPaySteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }); assert.equal(Array.isArray(steps), true); assert.equal(steps.length, 10); @@ -31,6 +32,9 @@ test('step definitions module exposes ordered normal and Plus step metadata', () 'platform-verify', ] ); + assert.equal(steps[0].title, '打开 ChatGPT 官网'); + assert.equal(steps[5].title, '清理登录 Cookies'); + assert.deepStrictEqual( plusSteps.map((step) => step.key), [ @@ -53,6 +57,29 @@ test('step definitions module exposes ordered normal and Plus step metadata', () assert.equal(plusSteps.some((step) => step.key === 'fetch-login-code'), true); assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]); assert.equal(api.getLastStepId({ plusModeEnabled: true }), 13); + assert.equal(plusSteps[5].title, '创建 Plus Checkout'); + assert.equal(plusSteps[7].title, 'PayPal 登录与授权'); + + assert.deepStrictEqual( + goPaySteps.map((step) => step.key), + [ + 'open-chatgpt', + 'submit-signup-email', + 'fill-password', + 'fetch-signup-code', + 'fill-profile', + 'plus-checkout-create', + 'gopay-subscription-confirm', + 'oauth-login', + 'fetch-login-code', + 'confirm-oauth', + 'platform-verify', + ] + ); + assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), [1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13]); + assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), 13); + assert.equal(goPaySteps[5].title, '打开 GoPay 订阅页'); + assert.equal(goPaySteps[6].title, '等待 GoPay 订阅确认'); }); test('sidepanel html loads shared step definitions before sidepanel bootstrap', () => {