diff --git a/background.js b/background.js index 8e9e345..4c24a89 100644 --- a/background.js +++ b/background.js @@ -3,10 +3,12 @@ importScripts( 'managed-alias-utils.js', 'mail2925-utils.js', + 'paypal-utils.js', 'background/phone-verification-flow.js', 'background/account-run-history.js', 'background/contribution-oauth.js', 'background/mail-2925-session.js', + 'background/paypal-account-store.js', 'background/ip-proxy-provider-711proxy.js', 'background/ip-proxy-core.js', 'background/panel-bridge.js', @@ -436,6 +438,7 @@ const PERSISTED_SETTING_DEFAULTS = { plusModeEnabled: false, paypalEmail: '', paypalPassword: '', + currentPayPalAccountId: '', autoRunSkipFailures: false, autoRunFallbackThreadIntervalMinutes: 0, autoRunDelayEnabled: false, @@ -476,6 +479,7 @@ const PERSISTED_SETTING_DEFAULTS = { cloudflareTempEmailDomains: [], hotmailAccounts: [], mail2925Accounts: [], + paypalAccounts: [], heroSmsApiKey: '', heroSmsCountryId: HERO_SMS_COUNTRY_ID, heroSmsCountryLabel: HERO_SMS_COUNTRY_LABEL, @@ -575,6 +579,7 @@ const DEFAULT_STATE = { loginVerificationRequestedAt: null, oauthFlowDeadlineAt: null, oauthFlowDeadlineSourceUrl: null, + currentPayPalAccountId: null, currentHotmailAccountId: null, currentMail2925AccountId: null, preferredIcloudHost: '', @@ -1242,6 +1247,8 @@ function normalizePersistentSettingValue(key, value) { return String(value || '').trim(); case 'paypalPassword': return String(value || ''); + case 'currentPayPalAccountId': + return String(value || '').trim(); case 'autoRunSkipFailures': case 'autoRunDelayEnabled': case 'phoneVerificationEnabled': @@ -1314,6 +1321,8 @@ function normalizePersistentSettingValue(key, value) { return normalizeHotmailAccounts(value); case 'mail2925Accounts': return normalizeMail2925Accounts(value); + case 'paypalAccounts': + return normalizePayPalAccounts(value); case 'heroSmsApiKey': return String(value || ''); case 'heroSmsCountryId': @@ -1929,6 +1938,50 @@ function generatePassword() { return pw.split('').sort(() => Math.random() - 0.5).join(''); } +function normalizePayPalAccount(account = {}) { + if (self.PayPalUtils?.normalizePayPalAccount) { + return self.PayPalUtils.normalizePayPalAccount(account); + } + return { + id: String(account.id || crypto.randomUUID()), + email: String(account.email || '').trim().toLowerCase(), + password: String(account.password || ''), + createdAt: Number.isFinite(Number(account.createdAt)) ? Number(account.createdAt) : Date.now(), + updatedAt: Number.isFinite(Number(account.updatedAt)) ? Number(account.updatedAt) : Date.now(), + lastUsedAt: Number.isFinite(Number(account.lastUsedAt)) ? Number(account.lastUsedAt) : 0, + }; +} + +function normalizePayPalAccounts(accounts) { + if (self.PayPalUtils?.normalizePayPalAccounts) { + return self.PayPalUtils.normalizePayPalAccounts(accounts); + } + return Array.isArray(accounts) ? accounts.map((account) => normalizePayPalAccount(account)) : []; +} + +function findPayPalAccount(accounts, accountId) { + if (self.PayPalUtils?.findPayPalAccount) { + return self.PayPalUtils.findPayPalAccount(accounts, accountId); + } + const normalizedId = String(accountId || '').trim(); + if (!normalizedId) return null; + return normalizePayPalAccounts(accounts).find((account) => account.id === normalizedId) || null; +} + +function upsertPayPalAccountInList(accounts, nextAccount) { + if (self.PayPalUtils?.upsertPayPalAccountInList) { + return self.PayPalUtils.upsertPayPalAccountInList(accounts, nextAccount); + } + const normalizedNext = normalizePayPalAccount(nextAccount); + const list = normalizePayPalAccounts(accounts); + const existingIndex = list.findIndex((account) => account.id === normalizedNext.id); + if (existingIndex >= 0) { + list[existingIndex] = normalizedNext; + return list; + } + return [...list, normalizedNext]; +} + function normalizeHotmailAccount(account = {}) { const normalizedLastAuthAt = Number.isFinite(Number(account.lastAuthAt)) ? Number(account.lastAuthAt) : 0; const normalizedStatus = String( @@ -7544,6 +7597,39 @@ function isMail2925PoolExhaustedPauseError(error) { return /^MAIL2925_POOL_EXHAUSTED_PAUSE::/.test(message); } +const payPalAccountStore = self.MultiPageBackgroundPayPalAccountStore?.createPayPalAccountStore({ + broadcastDataUpdate, + findPayPalAccount, + getState, + normalizePayPalAccount, + normalizePayPalAccounts, + setPersistentSettings, + setState, + upsertPayPalAccountInList, +}); + +async function syncPayPalAccounts(accounts) { + return payPalAccountStore?.syncPayPalAccounts?.(accounts) || []; +} + +async function upsertPayPalAccount(input = {}) { + if (!payPalAccountStore?.upsertPayPalAccount) { + throw new Error('PayPal 账号存储能力尚未接入。'); + } + return payPalAccountStore.upsertPayPalAccount(input); +} + +async function setCurrentPayPalAccount(accountId) { + if (!payPalAccountStore?.setCurrentPayPalAccount) { + throw new Error('PayPal 账号选择能力尚未接入。'); + } + return payPalAccountStore.setCurrentPayPalAccount(accountId); +} + +function getCurrentPayPalAccount(state = null) { + return payPalAccountStore?.getCurrentPayPalAccount?.(state || {}) || null; +} + const generatedEmailHelpers = self.MultiPageGeneratedEmailHelpers?.createGeneratedEmailHelpers({ addLog, buildGeneratedAliasEmail, @@ -8729,6 +8815,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter deleteHotmailAccounts, deleteIcloudAlias, deleteUsedIcloudAliases, + findPayPalAccount, disableUsedLuckmailPurchases, doesStepUseCompletionSignal, ensureMail2925MailboxSession, @@ -8773,9 +8860,11 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter listIcloudAliases, listLuckmailPurchasesForManagement, refreshIpProxyPool, + getCurrentPayPalAccount, getCurrentMail2925Account, normalizeHotmailAccounts, normalizeMail2925Accounts, + normalizePayPalAccounts, normalizeRunCount, AUTO_RUN_TIMER_KIND_SCHEDULED_START, notifyStepComplete, @@ -8791,6 +8880,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter selectLuckmailPurchase, switchIpProxy, changeIpProxyExit, + setCurrentPayPalAccount, setCurrentHotmailAccount, setCurrentMail2925Account, setContributionMode, @@ -8810,9 +8900,11 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter startAutoRunLoop, pollContributionStatus: (...args) => contributionOAuthManager?.pollContributionStatus?.(...args), syncHotmailAccounts, + syncPayPalAccounts, deleteMail2925Account, deleteMail2925Accounts, testHotmailAccountMailAccess, + upsertPayPalAccount, upsertMail2925Account, upsertHotmailAccount, verifyHotmailAccount, diff --git a/background/message-router.js b/background/message-router.js index 2c5886e..0944a3d 100644 --- a/background/message-router.js +++ b/background/message-router.js @@ -35,8 +35,10 @@ finalizeStep3Completion, finalizeIcloudAliasAfterSuccessfulFlow, findHotmailAccount, + findPayPalAccount, flushCommand, getCurrentLuckmailPurchase, + getCurrentPayPalAccount, getCurrentMail2925Account, getPendingAutoRunTimerPlan, getSourceLabel, @@ -62,6 +64,7 @@ refreshIpProxyPool, normalizeHotmailAccounts, normalizeMail2925Accounts, + normalizePayPalAccounts, normalizeRunCount, AUTO_RUN_TIMER_KIND_SCHEDULED_START, notifyStepComplete, @@ -79,6 +82,7 @@ selectLuckmailPurchase, switchIpProxy, changeIpProxyExit, + setCurrentPayPalAccount, setCurrentMail2925Account, setCurrentHotmailAccount, setContributionMode, @@ -99,7 +103,9 @@ deleteMail2925Account, deleteMail2925Accounts, syncHotmailAccounts, + syncPayPalAccounts, testHotmailAccountMailAccess, + upsertPayPalAccount, upsertMail2925Account, upsertHotmailAccount, verifyHotmailAccount, @@ -779,6 +785,16 @@ return { ok: true, account }; } + case 'UPSERT_PAYPAL_ACCOUNT': { + const account = await upsertPayPalAccount(message.payload || {}); + return { ok: true, account }; + } + + case 'SELECT_PAYPAL_ACCOUNT': { + const account = await setCurrentPayPalAccount(String(message.payload?.accountId || '')); + return { ok: true, account }; + } + case 'DELETE_HOTMAIL_ACCOUNT': { await deleteHotmailAccount(String(message.payload?.accountId || '')); return { ok: true }; diff --git a/background/paypal-account-store.js b/background/paypal-account-store.js new file mode 100644 index 0000000..6d6c889 --- /dev/null +++ b/background/paypal-account-store.js @@ -0,0 +1,110 @@ +(function attachBackgroundPayPalAccountStore(root, factory) { + root.MultiPageBackgroundPayPalAccountStore = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPayPalAccountStoreModule() { + function createPayPalAccountStore(deps = {}) { + const { + broadcastDataUpdate, + findPayPalAccount, + getState, + normalizePayPalAccount, + normalizePayPalAccounts, + setPersistentSettings, + setState, + upsertPayPalAccountInList, + } = deps; + + async function syncSelectedPayPalAccountState(account = null) { + const updates = account + ? { + currentPayPalAccountId: account.id, + paypalEmail: String(account.email || '').trim(), + paypalPassword: String(account.password || ''), + } + : { + currentPayPalAccountId: '', + paypalEmail: '', + paypalPassword: '', + }; + + await setPersistentSettings(updates); + await setState({ + currentPayPalAccountId: updates.currentPayPalAccountId || null, + paypalEmail: updates.paypalEmail, + paypalPassword: updates.paypalPassword, + }); + broadcastDataUpdate({ + currentPayPalAccountId: updates.currentPayPalAccountId || null, + paypalEmail: updates.paypalEmail, + paypalPassword: updates.paypalPassword, + }); + } + + async function syncPayPalAccounts(accounts) { + const normalized = normalizePayPalAccounts(accounts); + await setPersistentSettings({ paypalAccounts: normalized }); + await setState({ paypalAccounts: normalized }); + broadcastDataUpdate({ paypalAccounts: normalized }); + + const state = await getState(); + if (state.currentPayPalAccountId && !findPayPalAccount(normalized, state.currentPayPalAccountId)) { + await syncSelectedPayPalAccountState(null); + } + return normalized; + } + + function getCurrentPayPalAccount(state = {}) { + return findPayPalAccount(state.paypalAccounts, state.currentPayPalAccountId) || null; + } + + async function upsertPayPalAccount(input = {}) { + const state = await getState(); + const accounts = normalizePayPalAccounts(state.paypalAccounts); + const normalizedEmail = String(input?.email || '').trim().toLowerCase(); + const existing = input?.id + ? findPayPalAccount(accounts, input.id) + : accounts.find((account) => account.email === normalizedEmail) || null; + const normalized = normalizePayPalAccount({ + ...(existing || {}), + ...input, + id: input?.id || existing?.id || crypto.randomUUID(), + createdAt: existing?.createdAt || Date.now(), + updatedAt: Date.now(), + }); + + const nextAccounts = typeof upsertPayPalAccountInList === 'function' + ? upsertPayPalAccountInList(accounts, normalized) + : accounts.concat(normalized); + + await syncPayPalAccounts(nextAccounts); + + if (state.currentPayPalAccountId === normalized.id) { + await syncSelectedPayPalAccountState(normalized); + } + + return normalized; + } + + async function setCurrentPayPalAccount(accountId) { + const state = await getState(); + const accounts = normalizePayPalAccounts(state.paypalAccounts); + const account = findPayPalAccount(accounts, accountId); + if (!account) { + throw new Error('未找到对应的 PayPal 账号。'); + } + + await syncSelectedPayPalAccountState(account); + return account; + } + + return { + getCurrentPayPalAccount, + setCurrentPayPalAccount, + syncPayPalAccounts, + upsertPayPalAccount, + }; + } + + return { + createPayPalAccountStore, + }; +}); diff --git a/background/steps/paypal-approve.js b/background/steps/paypal-approve.js index d69540d..3d1e390 100644 --- a/background/steps/paypal-approve.js +++ b/background/steps/paypal-approve.js @@ -99,17 +99,30 @@ return result || {}; } + function resolvePayPalCredentials(state = {}) { + const currentId = String(state?.currentPayPalAccountId || '').trim(); + const accounts = Array.isArray(state?.paypalAccounts) ? state.paypalAccounts : []; + const selectedAccount = currentId + ? accounts.find((account) => String(account?.id || '').trim() === currentId) || null + : null; + return { + email: String(selectedAccount?.email || state?.paypalEmail || '').trim(), + password: String(selectedAccount?.password || state?.paypalPassword || ''), + }; + } + async function submitLogin(tabId, state = {}) { - if (!state.paypalPassword) { - throw new Error('步骤 8:未配置 PayPal 密码,请先在侧边栏填写。'); + const credentials = resolvePayPalCredentials(state); + if (!credentials.password) { + throw new Error('步骤 8:未配置可用的 PayPal 账号,请先在侧边栏添加并选择账号。'); } await addLog('步骤 8:正在填写 PayPal 登录信息并提交...', 'info'); const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, { type: 'PAYPAL_SUBMIT_LOGIN', source: 'background', payload: { - email: state.paypalEmail || '', - password: state.paypalPassword || '', + email: credentials.email, + password: credentials.password, }, }); if (result?.error) { diff --git a/paypal-utils.js b/paypal-utils.js new file mode 100644 index 0000000..b949bc7 --- /dev/null +++ b/paypal-utils.js @@ -0,0 +1,56 @@ +(function attachPayPalUtils(root, factory) { + root.PayPalUtils = factory(); +})(typeof self !== 'undefined' ? self : globalThis, function createPayPalUtils() { + function normalizePayPalAccount(account = {}) { + const normalizedEmail = String(account.email || '').trim().toLowerCase(); + const now = Date.now(); + return { + id: String(account.id || crypto.randomUUID()), + email: normalizedEmail, + password: String(account.password || ''), + createdAt: Number.isFinite(Number(account.createdAt)) ? Number(account.createdAt) : now, + updatedAt: Number.isFinite(Number(account.updatedAt)) ? Number(account.updatedAt) : now, + lastUsedAt: Number.isFinite(Number(account.lastUsedAt)) ? Number(account.lastUsedAt) : 0, + }; + } + + function normalizePayPalAccounts(accounts) { + if (!Array.isArray(accounts)) return []; + + const deduped = new Map(); + for (const account of accounts) { + const normalized = normalizePayPalAccount(account); + if (!normalized.email) continue; + deduped.set(normalized.id, normalized); + } + return [...deduped.values()]; + } + + function findPayPalAccount(accounts = [], accountId = '') { + const normalizedId = String(accountId || '').trim(); + if (!normalizedId) return null; + return normalizePayPalAccounts(accounts).find((account) => account.id === normalizedId) || null; + } + + function upsertPayPalAccountInList(accounts = [], nextAccount = null) { + if (!nextAccount) { + return normalizePayPalAccounts(accounts); + } + + const normalizedNext = normalizePayPalAccount(nextAccount); + const list = normalizePayPalAccounts(accounts); + const existingIndex = list.findIndex((account) => account.id === normalizedNext.id); + if (existingIndex >= 0) { + list[existingIndex] = normalizedNext; + return list; + } + return [...list, normalizedNext]; + } + + return { + findPayPalAccount, + normalizePayPalAccount, + normalizePayPalAccounts, + upsertPayPalAccountInList, + }; +}); diff --git a/sidepanel/form-dialog.js b/sidepanel/form-dialog.js new file mode 100644 index 0000000..cf3db7c --- /dev/null +++ b/sidepanel/form-dialog.js @@ -0,0 +1,233 @@ +(function attachSidepanelFormDialog(globalScope) { + function createFormDialog(context = {}) { + const { + overlay = null, + titleNode = null, + closeButton = null, + messageNode = null, + alertNode = null, + fieldsContainer = null, + cancelButton = null, + confirmButton = null, + documentRef = globalScope.document, + } = context; + + let resolver = null; + let currentConfig = null; + let currentInputs = []; + + function setHidden(node, hidden) { + if (!node) return; + node.hidden = Boolean(hidden); + } + + function resetAlert() { + if (!alertNode) return; + alertNode.textContent = ''; + alertNode.className = 'modal-alert modal-form-alert'; + alertNode.hidden = true; + } + + function setAlert(message = '', tone = 'danger') { + if (!alertNode) return; + const text = String(message || '').trim(); + if (!text) { + resetAlert(); + return; + } + alertNode.textContent = text; + alertNode.className = `modal-alert modal-form-alert${tone === 'danger' ? ' is-danger' : ''}`; + alertNode.hidden = false; + } + + function close(result = null) { + if (resolver) { + resolver(result); + resolver = null; + } + currentConfig = null; + currentInputs = []; + resetAlert(); + if (fieldsContainer) { + fieldsContainer.innerHTML = ''; + } + if (overlay) { + overlay.hidden = true; + } + } + + function buildFieldNode(field, values) { + const wrapper = documentRef.createElement('div'); + wrapper.className = 'modal-form-row'; + + const label = documentRef.createElement('label'); + label.className = 'modal-form-label'; + label.textContent = String(field.label || field.key || '').trim(); + wrapper.appendChild(label); + + let input = null; + if (field.type === 'textarea') { + input = documentRef.createElement('textarea'); + input.className = 'data-textarea'; + } else if (field.type === 'select') { + input = documentRef.createElement('select'); + input.className = 'data-select'; + const options = Array.isArray(field.options) ? field.options : []; + options.forEach((option) => { + const optionNode = documentRef.createElement('option'); + optionNode.value = String(option?.value || ''); + optionNode.textContent = String(option?.label || option?.value || ''); + input.appendChild(optionNode); + }); + } else { + input = documentRef.createElement('input'); + input.type = field.type === 'password' ? 'password' : 'text'; + input.className = 'data-input'; + } + + const normalizedValue = Object.prototype.hasOwnProperty.call(values, field.key) + ? values[field.key] + : field.value; + if (normalizedValue !== undefined && normalizedValue !== null) { + input.value = String(normalizedValue); + } + if (field.placeholder) { + input.placeholder = String(field.placeholder); + } + if (field.autocomplete) { + input.autocomplete = String(field.autocomplete); + } + if (field.inputMode) { + input.inputMode = String(field.inputMode); + } + if (field.rows && field.type === 'textarea') { + input.rows = Number(field.rows) || 3; + } + input.dataset.fieldKey = String(field.key || ''); + label.htmlFor = field.key; + input.id = field.key; + wrapper.appendChild(input); + + if (field.type !== 'textarea') { + input.addEventListener('keydown', (event) => { + if (event.key !== 'Enter') { + return; + } + event.preventDefault(); + void handleConfirm(); + }); + } + + currentInputs.push({ field, input }); + return wrapper; + } + + function collectValues() { + return currentInputs.reduce((result, item) => { + result[item.field.key] = item.input.value; + return result; + }, {}); + } + + async function handleConfirm() { + if (!currentConfig) { + close(null); + return; + } + + const values = collectValues(); + resetAlert(); + + for (const item of currentInputs) { + const { field, input } = item; + const rawValue = values[field.key]; + const textValue = String(rawValue || '').trim(); + if (field.required && !textValue) { + setAlert(field.requiredMessage || `${field.label || field.key}不能为空。`); + input.focus?.(); + return; + } + if (typeof field.validate === 'function') { + const validationMessage = await field.validate(rawValue, values); + if (validationMessage) { + setAlert(validationMessage); + input.focus?.(); + return; + } + } + } + + close(values); + } + + function bindEvents() { + overlay?.addEventListener('click', (event) => { + if (event.target === overlay) { + close(null); + } + }); + closeButton?.addEventListener('click', () => close(null)); + cancelButton?.addEventListener('click', () => close(null)); + confirmButton?.addEventListener('click', () => { + void handleConfirm(); + }); + } + + async function open(config = {}) { + if (!overlay || !titleNode || !fieldsContainer || !confirmButton) { + return null; + } + if (resolver) { + close(null); + } + + currentConfig = config || {}; + currentInputs = []; + titleNode.textContent = String(currentConfig.title || '填写表单'); + if (messageNode) { + const message = String(currentConfig.message || '').trim(); + messageNode.textContent = message; + setHidden(messageNode, !message); + } + resetAlert(); + if (currentConfig.alert?.text) { + setAlert(currentConfig.alert.text, currentConfig.alert.tone || 'danger'); + } + + confirmButton.textContent = String(currentConfig.confirmLabel || '确认'); + confirmButton.className = `btn ${currentConfig.confirmVariant || 'btn-primary'} btn-sm`; + fieldsContainer.innerHTML = ''; + + const initialValues = currentConfig.initialValues && typeof currentConfig.initialValues === 'object' + ? currentConfig.initialValues + : {}; + const fields = Array.isArray(currentConfig.fields) ? currentConfig.fields : []; + fields.forEach((field) => { + fieldsContainer.appendChild(buildFieldNode(field, initialValues)); + }); + + overlay.hidden = false; + const firstInput = currentInputs[0]?.input || null; + if (firstInput && typeof globalScope.requestAnimationFrame === 'function') { + globalScope.requestAnimationFrame(() => firstInput.focus?.()); + } else { + firstInput?.focus?.(); + } + + return new Promise((resolve) => { + resolver = resolve; + }); + } + + bindEvents(); + + return { + close, + open, + }; + } + + globalScope.SidepanelFormDialog = { + createFormDialog, + }; +})(window); diff --git a/sidepanel/paypal-manager.js b/sidepanel/paypal-manager.js new file mode 100644 index 0000000..97c1be0 --- /dev/null +++ b/sidepanel/paypal-manager.js @@ -0,0 +1,191 @@ +(function attachSidepanelPayPalManager(globalScope) { + function createPayPalManager(context = {}) { + const { + state, + dom, + helpers, + runtime, + paypalUtils = {}, + } = context; + + let actionInFlight = false; + + function getPayPalAccounts(currentState = state.getLatestState()) { + return helpers.getPayPalAccounts(currentState); + } + + function getCurrentPayPalAccountId(currentState = state.getLatestState()) { + return String(currentState?.currentPayPalAccountId || '').trim(); + } + + function buildSelectOptions(accounts = []) { + if (!accounts.length) { + return ''; + } + return accounts.map((account) => ( + `` + )).join(''); + } + + function applyPayPalAccountMutation(account) { + if (!account?.id) return; + const latestState = state.getLatestState(); + const nextAccounts = typeof paypalUtils.upsertPayPalAccountInList === 'function' + ? paypalUtils.upsertPayPalAccountInList(getPayPalAccounts(latestState), account) + : [...getPayPalAccounts(latestState), account]; + state.syncLatestState({ paypalAccounts: nextAccounts }); + renderPayPalAccounts(); + } + + function renderPayPalAccounts() { + if (!dom.selectPayPalAccount) return; + + const latestState = state.getLatestState(); + const accounts = getPayPalAccounts(latestState); + const currentId = getCurrentPayPalAccountId(latestState); + + dom.selectPayPalAccount.innerHTML = buildSelectOptions(accounts); + dom.selectPayPalAccount.disabled = accounts.length === 0; + dom.selectPayPalAccount.value = accounts.some((account) => account.id === currentId) ? currentId : ''; + } + + async function syncSelectedPayPalAccount(options = {}) { + const { silent = false } = options; + const accountId = String(dom.selectPayPalAccount?.value || '').trim(); + if (!accountId) { + state.syncLatestState({ + currentPayPalAccountId: null, + paypalEmail: '', + paypalPassword: '', + }); + renderPayPalAccounts(); + return null; + } + + const response = await runtime.sendMessage({ + type: 'SELECT_PAYPAL_ACCOUNT', + source: 'sidepanel', + payload: { accountId }, + }); + if (response?.error) { + throw new Error(response.error); + } + + state.syncLatestState({ + currentPayPalAccountId: response.account?.id || accountId, + paypalEmail: String(response.account?.email || '').trim(), + paypalPassword: String(response.account?.password || ''), + }); + renderPayPalAccounts(); + if (!silent) { + helpers.showToast(`已切换当前 PayPal 账号为 ${response.account?.email || accountId}`, 'success', 1800); + } + return response.account || null; + } + + async function openPayPalAccountDialog() { + if (typeof helpers.openFormDialog !== 'function') { + throw new Error('表单弹窗能力未加载,请刷新扩展后重试。'); + } + return helpers.openFormDialog({ + title: '添加 PayPal 账号', + confirmLabel: '保存账号', + confirmVariant: 'btn-primary', + fields: [ + { + key: 'email', + label: 'PayPal 账号', + type: 'text', + placeholder: '请输入 PayPal 登录邮箱', + autocomplete: 'username', + required: true, + requiredMessage: '请先填写 PayPal 账号。', + validate: (value) => { + const normalized = String(value || '').trim(); + if (!normalized.includes('@')) { + return 'PayPal 账号需填写邮箱格式。'; + } + return ''; + }, + }, + { + key: 'password', + label: 'PayPal 密码', + type: 'password', + placeholder: '请输入 PayPal 登录密码', + autocomplete: 'current-password', + required: true, + requiredMessage: '请先填写 PayPal 密码。', + }, + ], + }); + } + + async function handleAddPayPalAccount() { + if (actionInFlight) return; + + const formValues = await openPayPalAccountDialog(); + if (!formValues) { + return; + } + + actionInFlight = true; + if (dom.btnAddPayPalAccount) { + dom.btnAddPayPalAccount.disabled = true; + } + + try { + const response = await runtime.sendMessage({ + type: 'UPSERT_PAYPAL_ACCOUNT', + source: 'sidepanel', + payload: { + email: String(formValues.email || '').trim(), + password: String(formValues.password || ''), + }, + }); + if (response?.error) { + throw new Error(response.error); + } + + applyPayPalAccountMutation(response.account); + if (response.account?.id) { + state.syncLatestState({ currentPayPalAccountId: response.account.id }); + renderPayPalAccounts(); + dom.selectPayPalAccount.value = response.account.id; + await syncSelectedPayPalAccount({ silent: true }); + } + helpers.showToast(`已保存 PayPal 账号 ${response.account?.email || ''}`, 'success', 2200); + } catch (error) { + helpers.showToast(`保存 PayPal 账号失败:${error.message}`, 'error'); + throw error; + } finally { + actionInFlight = false; + if (dom.btnAddPayPalAccount) { + dom.btnAddPayPalAccount.disabled = false; + } + } + } + + function bindPayPalEvents() { + dom.btnAddPayPalAccount?.addEventListener('click', () => { + void handleAddPayPalAccount(); + }); + dom.selectPayPalAccount?.addEventListener('change', () => { + void syncSelectedPayPalAccount().catch((error) => { + helpers.showToast(error.message, 'error'); + renderPayPalAccounts(); + }); + }); + } + + return { + bindPayPalEvents, + renderPayPalAccounts, + syncSelectedPayPalAccount, + }; + } + + globalScope.SidepanelPayPalManager = { + createPayPalManager, + }; +})(window); diff --git a/sidepanel/sidepanel.css b/sidepanel/sidepanel.css index df0dce6..f3f3d94 100644 --- a/sidepanel/sidepanel.css +++ b/sidepanel/sidepanel.css @@ -2722,6 +2722,36 @@ header { cursor: pointer; } +.modal-form-fields { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 14px; +} + +.modal-form-row { + display: flex; + flex-direction: column; + gap: 6px; +} + +.modal-form-label { + font-size: 12px; + font-weight: 600; + color: var(--text-secondary); +} + +.modal-form-row .data-input, +.modal-form-row .data-select, +.modal-form-row .data-textarea { + width: 100%; +} + +.modal-form-alert { + margin-top: -4px; + margin-bottom: 14px; +} + .modal-actions { display: flex; justify-content: flex-end; diff --git a/sidepanel/sidepanel.html b/sidepanel/sidepanel.html index 40e9a1a..5f7d6ab 100644 --- a/sidepanel/sidepanel.html +++ b/sidepanel/sidepanel.html @@ -383,13 +383,14 @@ PayPal 订阅链路 -
+ +