diff --git a/background/generated-email-helpers.js b/background/generated-email-helpers.js index 576cb14..55e5566 100644 --- a/background/generated-email-helpers.js +++ b/background/generated-email-helpers.js @@ -284,11 +284,16 @@ } if (generator === 'icloud') { const stateFetchMode = String(mergedState.icloudFetchMode || '').trim().toLowerCase(); - return fetchIcloudHideMyEmail({ + const icloudOptions = { generateNew: Boolean(options.generateNew) || stateFetchMode === 'always_new', - hostPreference: mergedState.icloudHostPreference, - preferredHost: mergedState.preferredIcloudHost, - }); + }; + if (mergedState.icloudHostPreference !== undefined) { + icloudOptions.hostPreference = mergedState.icloudHostPreference; + } + if (mergedState.preferredIcloudHost !== undefined) { + icloudOptions.preferredHost = mergedState.preferredIcloudHost; + } + return fetchIcloudHideMyEmail(icloudOptions); } if (generator === 'cloudflare') { return fetchCloudflareEmail(mergedState, options); diff --git a/docs/错误重试分层策略.md b/docs/错误重试分层策略.md index 4d98495..46c7f59 100644 --- a/docs/错误重试分层策略.md +++ b/docs/错误重试分层策略.md @@ -45,22 +45,22 @@ ## 4. 错误分类优先级(当前实现) -1) 先做当步恢复(能在步骤内闭环就不升级) -2) 当步失败后,再判断是否走小循环(回步骤 7) +1) 先做当步恢复(能在步骤内闭环就不升级) +2) 当步失败后,再判断是否走小循环(回步骤 7) 3) 小循环仍失败,才交给大循环(attempt / round / 线程间隔) ## 5. 已落实的关键规则(与近期问题对应) -- `Failed to fetch / network error / fetch failed`: +- `Failed to fetch / network error / fetch failed`: 步骤 4/8/9 进入有上限的网络重试(当前默认 3 次,冷却 12 秒)。 -- `Step 9: 5sim check activation failed: order not found`: +- `Step 9: 5sim check activation failed: order not found`: 归类为“号码失效”,在步骤 9 内立即换号(当步重试),不应直接触发线程间隔。 -- `Route Error 405`(手机号验证码页): +- `Route Error 405`(手机号验证码页): 优先当步恢复;若落入 route-405 循环,步骤 9 内换号。 -- `add-phone / phone-verification` 页面状态: +- `add-phone / phone-verification` 页面状态: 表示流程已进入手机链路,自动运行层默认不再盲目回步骤 7,避免破坏当前链路。 ## 6. 关于“第几轮尝试 + 暂停”含义 @@ -70,4 +70,3 @@ - 用户手动停止会中断当前计划;恢复时是否回到步骤 7,取决于当前页面状态与回调是否过期: - 回调未过期且仍在可恢复链路:优先当前链路继续; - 回调已过期或链路断裂:回步骤 7 重拉授权。 - diff --git a/sidepanel/sidepanel.js b/sidepanel/sidepanel.js index 60dbdd3..8df2482 100644 --- a/sidepanel/sidepanel.js +++ b/sidepanel/sidepanel.js @@ -162,6 +162,7 @@ const inputCodex2ApiAdminKey = document.getElementById('input-codex2api-admin-ke const rowCustomPassword = document.getElementById('row-custom-password'); const rowPlusMode = document.getElementById('row-plus-mode'); const inputPlusModeEnabled = document.getElementById('input-plus-mode-enabled'); +const selectPlusPaymentMethod = document.getElementById('select-plus-payment-method'); const rowPayPalAccount = document.getElementById('row-paypal-account'); const selectPayPalAccount = document.getElementById('select-paypal-account'); const btnAddPayPalAccount = document.getElementById('btn-add-paypal-account'); @@ -291,6 +292,7 @@ const inputAutoSkipFailuresThreadIntervalMinutes = document.getElementById('inpu const inputAutoDelayEnabled = document.getElementById('input-auto-delay-enabled'); const inputAutoDelayMinutes = document.getElementById('input-auto-delay-minutes'); const inputAutoStepDelaySeconds = document.getElementById('input-auto-step-delay-seconds'); +const inputOAuthFlowTimeoutEnabled = document.getElementById('input-oauth-flow-timeout-enabled'); const inputVerificationResendCount = document.getElementById('input-verification-resend-count'); const rowPhoneVerificationEnabled = document.getElementById('row-phone-verification-enabled'); const btnTogglePhoneVerificationSection = document.getElementById('btn-toggle-phone-verification-section'); @@ -402,6 +404,7 @@ 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 phoneSmsProviderOrderSelection = []; let heroSmsCountryMenuSearchKeyword = ''; @@ -417,7 +420,7 @@ const fiveSimCountrySearchTextByCode = new Map(); let nexSmsCountrySelectionOrder = []; let nexSmsCountryMenuSearchKeyword = ''; const nexSmsCountrySearchTextById = 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); @@ -538,8 +541,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; @@ -548,9 +565,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); @@ -843,6 +861,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; @@ -1279,6 +1299,95 @@ async function openConfirmModalWithOption({ }; } +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'; } @@ -2707,6 +2816,13 @@ function collectSettingsPayload() { const currentPayPalAccount = typeof getCurrentPayPalAccount === 'function' ? getCurrentPayPalAccount(latestState) : payPalAccounts.find((account) => account?.id === String(latestState?.currentPayPalAccountId || '').trim()) || null; + 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, @@ -2744,6 +2860,7 @@ function collectSettingsPayload() { plusModeEnabled: typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled ? Boolean(inputPlusModeEnabled.checked) : Boolean(latestState?.plusModeEnabled), + plusPaymentMethod, paypalEmail: String(currentPayPalAccount?.email || latestState?.paypalEmail || '').trim(), paypalPassword: String(currentPayPalAccount?.password || latestState?.paypalPassword || ''), currentPayPalAccountId: String(latestState?.currentPayPalAccountId || '').trim(), @@ -2797,6 +2914,9 @@ function collectSettingsPayload() { autoRunDelayEnabled: inputAutoDelayEnabled.checked, autoRunDelayMinutes: normalizeAutoDelayMinutes(inputAutoDelayMinutes.value), autoStepDelaySeconds: normalizeAutoStepDelaySeconds(inputAutoStepDelaySeconds.value), + oauthFlowTimeoutEnabled: typeof inputOAuthFlowTimeoutEnabled !== 'undefined' && inputOAuthFlowTimeoutEnabled + ? Boolean(inputOAuthFlowTimeoutEnabled.checked) + : true, phoneVerificationEnabled: Boolean(inputPhoneVerificationEnabled?.checked), phoneSmsProvider: phoneSmsProviderValue, phoneSmsProviderOrder: phoneSmsProviderOrderValue, @@ -6361,13 +6481,18 @@ function updatePlusModeUI() { const enabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled ? Boolean(inputPlusModeEnabled.checked) : false; + const paymentMethod = getSelectedPlusPaymentMethod(); + if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) { + selectPlusPaymentMethod.value = paymentMethod; + selectPlusPaymentMethod.style.display = enabled ? '' : 'none'; + } [ typeof rowPayPalAccount !== 'undefined' ? rowPayPalAccount : null, ].forEach((row) => { if (!row) { return; } - row.style.display = enabled ? '' : 'none'; + row.style.display = enabled && paymentMethod === 'paypal' ? '' : 'none'; }); } @@ -6435,10 +6560,10 @@ function scheduleSettingsAutoSave() { } async function saveSettings(options = {}) { - const { silent = false } = options; + const { silent = false, force = false } = options; clearTimeout(settingsAutoSaveTimer); - if (!settingsDirty && !settingsSaveInFlight && silent) { + if (!force && !settingsDirty && !settingsSaveInFlight && silent) { return; } @@ -6484,6 +6609,12 @@ async function saveSettings(options = {}) { } } +async function persistCurrentSettingsForAction() { + clearTimeout(settingsAutoSaveTimer); + await waitForSettingsSaveIdle(); + await saveSettings({ silent: true, force: true }); +} + function applyAutoRunStatus(payload = currentAutoRun) { syncAutoRunState(payload); const runLabel = getAutoRunLabel(currentAutoRun); @@ -6494,7 +6625,13 @@ function applyAutoRunStatus(payload = currentAutoRun) { setSettingsCardLocked(settingsCardLocked); - inputRunCount.disabled = currentAutoRun.autoRunning || shouldLockRunCountToEmailPool(); + const lockedRunCount = getLockedRunCountFromEmailPool(); + const shouldSyncAutoRunTotalRuns = currentAutoRun.autoRunning + || locked + || paused + || scheduled; + + inputRunCount.disabled = currentAutoRun.autoRunning || lockedRunCount > 0; btnAutoRun.disabled = currentAutoRun.autoRunning; btnFetchEmail.disabled = locked || isCustomMailProvider() @@ -6502,7 +6639,9 @@ function applyAutoRunStatus(payload = currentAutoRun) { inputEmail.disabled = locked; inputAutoSkipFailures.disabled = scheduled; - if (currentAutoRun.totalRuns > 0) { + if (lockedRunCount > 0) { + inputRunCount.value = String(lockedRunCount); + } else if (shouldSyncAutoRunTotalRuns && currentAutoRun.totalRuns > 0) { inputRunCount.value = String(currentAutoRun.totalRuns); } @@ -6603,14 +6742,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(); } @@ -6620,7 +6762,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'; @@ -6677,6 +6819,9 @@ function applySettingsState(state) { if (typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled) { inputPlusModeEnabled.checked = Boolean(state?.plusModeEnabled); } + if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) { + selectPlusPaymentMethod.value = normalizePlusPaymentMethod(state?.plusPaymentMethod); + } inputVpsUrl.value = state?.vpsUrl || ''; inputVpsPassword.value = state?.vpsPassword || ''; setLocalCpaStep9Mode(state?.localCpaStep9Mode); @@ -6853,6 +6998,11 @@ function applySettingsState(state) { inputAutoDelayEnabled.checked = Boolean(state?.autoRunDelayEnabled); inputAutoDelayMinutes.value = String(normalizeAutoDelayMinutes(state?.autoRunDelayMinutes)); inputAutoStepDelaySeconds.value = formatAutoStepDelayInputValue(state?.autoStepDelaySeconds); + if (typeof inputOAuthFlowTimeoutEnabled !== 'undefined' && inputOAuthFlowTimeoutEnabled) { + inputOAuthFlowTimeoutEnabled.checked = state?.oauthFlowTimeoutEnabled !== undefined + ? Boolean(state.oauthFlowTimeoutEnabled) + : true; + } if (inputVerificationResendCount) { const restoredVerificationResendCount = state?.verificationResendCount !== undefined ? state.verificationResendCount @@ -6950,7 +7100,12 @@ function applySettingsState(state) { if (normalized === 'nexsms') return 'nexsms'; return 'hero-sms'; }); - const providerOrderForRestore = resolveNormalizedProviderOrderForRuntime(state || {}); + const providerOrderForRestore = typeof resolveNormalizedProviderOrderForRuntime === 'function' + ? resolveNormalizedProviderOrderForRuntime(state || {}) + : [normalizeProvider( + state?.phoneSmsProvider + || (typeof DEFAULT_PHONE_SMS_PROVIDER !== 'undefined' ? DEFAULT_PHONE_SMS_PROVIDER : 'hero-sms') + )]; const activeProvider = providerOrderForRestore[0] || normalizeProvider( state?.phoneSmsProvider || (typeof DEFAULT_PHONE_SMS_PROVIDER !== 'undefined' ? DEFAULT_PHONE_SMS_PROVIDER : 'hero-sms') @@ -7028,6 +7183,9 @@ function applySettingsState(state) { queueLuckmailPurchaseRefresh(); } updateButtonStates(); + if (typeof syncPlusManualConfirmationDialog === 'function') { + void syncPlusManualConfirmationDialog(); + } } async function restoreState() { @@ -9122,6 +9280,7 @@ stepsList?.addEventListener('click', async (event) => { if (!(await maybeTakeoverAutoRun(`执行步骤 ${step}`))) { return; } + await persistCurrentSettingsForAction(); if (step === 3) { if (inputPassword.value !== (latestState?.customPassword || '')) { await chrome.runtime.sendMessage({ @@ -9363,8 +9522,8 @@ async function startAutoRunFromCurrentSettings() { console.warn('Failed to refresh contribution content hint before auto run:', error); } - if (typeof settingsDirty !== 'undefined' && settingsDirty && typeof saveSettings === 'function') { - await saveSettings({ silent: true }); + if (typeof persistCurrentSettingsForAction === 'function') { + await persistCurrentSettingsForAction(); } const customEmailPoolEnabled = typeof usesCustomEmailPoolGenerator === 'function' @@ -9646,7 +9805,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 = normalizePlusPaymentMethod(selectPlusPaymentMethod.value); + updatePlusModeUI(); + syncStepDefinitionsForMode(Boolean(inputPlusModeEnabled?.checked), selectPlusPaymentMethod.value, { render: true }); markSettingsDirty(true); saveSettings({ silent: true }).catch(() => { }); }); @@ -10504,6 +10671,11 @@ inputAutoDelayMinutes.addEventListener('blur', () => { saveSettings({ silent: true }).catch(() => { }); }); +inputOAuthFlowTimeoutEnabled?.addEventListener('change', () => { + markSettingsDirty(true); + saveSettings({ silent: true }).catch(() => { }); +}); + inputPhoneVerificationEnabled?.addEventListener('change', () => { if (inputPhoneVerificationEnabled.checked) { setPhoneVerificationSectionExpanded(true); @@ -11224,6 +11396,20 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { ) { updateMailProviderUI(); } + if (message.payload.plusModeEnabled !== undefined && inputPlusModeEnabled) { + inputPlusModeEnabled.checked = Boolean(message.payload.plusModeEnabled); + } + if (message.payload.plusPaymentMethod !== undefined && selectPlusPaymentMethod) { + 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) { renderHotmailAccounts(); if (selectMailProvider.value === 'hotmail-api') { @@ -11303,6 +11489,9 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { if (message.payload.autoStepDelaySeconds !== undefined) { inputAutoStepDelaySeconds.value = formatAutoStepDelayInputValue(message.payload.autoStepDelaySeconds); } + if (message.payload.oauthFlowTimeoutEnabled !== undefined && typeof inputOAuthFlowTimeoutEnabled !== 'undefined' && inputOAuthFlowTimeoutEnabled) { + inputOAuthFlowTimeoutEnabled.checked = Boolean(message.payload.oauthFlowTimeoutEnabled); + } if ( ( message.payload.verificationResendCount !== undefined @@ -11509,6 +11698,7 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => { } updateAccountRunHistorySettingsUI(); renderContributionMode(); + void syncPlusManualConfirmationDialog(); break; } diff --git a/tests/sidepanel-plus-payment-method.test.js b/tests/sidepanel-plus-payment-method.test.js new file mode 100644 index 0000000..824a109 --- /dev/null +++ b/tests/sidepanel-plus-payment-method.test.js @@ -0,0 +1,168 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); + +const sidepanelSource = fs.readFileSync('sidepanel/sidepanel.js', 'utf8'); + +function extractFunction(name) { + const asyncStart = sidepanelSource.indexOf(`async function ${name}`); + const normalStart = sidepanelSource.indexOf(`function ${name}`); + const start = asyncStart !== -1 + ? asyncStart + : normalStart; + if (start === -1) { + throw new Error(`Function ${name} not found`); + } + const signatureEnd = sidepanelSource.indexOf(')', start); + const bodyStart = sidepanelSource.indexOf('{', signatureEnd); + let depth = 0; + let end = bodyStart; + for (; end < sidepanelSource.length; end += 1) { + const char = sidepanelSource[end]; + if (char === '{') { + depth += 1; + } else if (char === '}') { + depth -= 1; + if (depth === 0) { + end += 1; + break; + } + } + } + return sidepanelSource.slice(start, end); +} + +test('sidepanel step definitions keep the selected Plus payment method', () => { + const bundle = [ + extractFunction('normalizePlusPaymentMethod'), + extractFunction('getStepDefinitionsForMode'), + extractFunction('rebuildStepDefinitionState'), + extractFunction('syncStepDefinitionsForMode'), + ].join('\n'); + + const api = new Function(` +const calls = []; +const window = { + MultiPageStepDefinitions: { + getSteps(options) { + calls.push({ type: 'getSteps', options }); + return [{ id: options.plusPaymentMethod === 'gopay' ? 7 : 6, order: 1 }]; + }, + }, +}; +let currentPlusModeEnabled = false; +let currentPlusPaymentMethod = 'paypal'; +let stepDefinitions = []; +let STEP_IDS = []; +let STEP_DEFAULT_STATUSES = {}; +let SKIPPABLE_STEPS = new Set(); +function renderStepsList() { + calls.push({ type: 'render', stepIds: [...STEP_IDS] }); +} +${bundle} +return { + calls, + syncStepDefinitionsForMode, + getCurrentPlusPaymentMethod: () => currentPlusPaymentMethod, + getStepIds: () => [...STEP_IDS], +}; +`)(); + + api.syncStepDefinitionsForMode(true, 'gopay', { render: true }); + + assert.equal(api.getCurrentPlusPaymentMethod(), 'gopay'); + assert.deepEqual(api.getStepIds(), [7]); + assert.deepEqual(api.calls[0], { + type: 'getSteps', + options: { plusModeEnabled: true, plusPaymentMethod: 'gopay' }, + }); + assert.deepEqual(api.calls[1], { type: 'render', stepIds: [7] }); +}); + +test('sidepanel Plus UI hides PayPal account selector while GoPay is selected', () => { + const bundle = [ + extractFunction('normalizePlusPaymentMethod'), + extractFunction('getSelectedPlusPaymentMethod'), + extractFunction('updatePlusModeUI'), + ].join('\n'); + + const api = new Function(` +let latestState = { plusPaymentMethod: 'gopay' }; +let currentPlusPaymentMethod = 'paypal'; +const inputPlusModeEnabled = { checked: true }; +const selectPlusPaymentMethod = { value: 'gopay', style: { display: 'none' } }; +const rowPayPalAccount = { style: { display: '' } }; +${bundle} +return { updatePlusModeUI, selectPlusPaymentMethod, rowPayPalAccount }; +`)(); + + api.updatePlusModeUI(); + + assert.equal(api.selectPlusPaymentMethod.style.display, ''); + assert.equal(api.rowPayPalAccount.style.display, 'none'); + + api.selectPlusPaymentMethod.value = 'paypal'; + api.updatePlusModeUI(); + assert.equal(api.rowPayPalAccount.style.display, ''); +}); + +test('sidepanel resolves pending GoPay manual confirmation from DATA_UPDATED state', async () => { + const bundle = [ + extractFunction('openPlusManualConfirmationDialog'), + extractFunction('syncPlusManualConfirmationDialog'), + ].join('\n'); + + const api = new Function(` +const events = []; +let latestState = { + plusManualConfirmationPending: true, + plusManualConfirmationRequestId: 'gopay-request-1', + plusManualConfirmationStep: 7, + plusManualConfirmationMethod: 'gopay', + plusManualConfirmationTitle: 'GoPay 订阅确认', + plusManualConfirmationMessage: '请确认订阅。', +}; +let activePlusManualConfirmationRequestId = ''; +let plusManualConfirmationDialogInFlight = false; +function openActionModal(options) { + events.push({ type: 'modal', options }); + return Promise.resolve('confirm'); +} +function showToast(message, tone) { + events.push({ type: 'toast', message, tone }); +} +const chrome = { + runtime: { + async sendMessage(message) { + events.push({ type: 'send', message }); + latestState = { + ...latestState, + plusManualConfirmationPending: false, + }; + return { ok: true }; + }, + }, +}; +${bundle} +return { events, syncPlusManualConfirmationDialog }; +`)(); + + await api.syncPlusManualConfirmationDialog(); + + assert.equal(api.events[0].type, 'modal'); + assert.equal(api.events[0].options.title, 'GoPay 订阅确认'); + assert.deepEqual(api.events[1], { + type: 'send', + message: { + type: 'RESOLVE_PLUS_MANUAL_CONFIRMATION', + source: 'sidepanel', + payload: { + step: 7, + requestId: 'gopay-request-1', + confirmed: true, + }, + }, + }); + assert.match(api.events[2].message, /GoPay/); + assert.equal(api.events[2].tone, 'info'); +});