feat(gopay): support GoPay Plus checkout flow

This commit is contained in:
朴圣佑
2026-05-01 02:02:33 +08:00
committed by QLHazyCoder
parent 2eb913e00b
commit d851cc4d36
24 changed files with 3798 additions and 162 deletions
+14 -6
View File
@@ -4,6 +4,7 @@
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
const PLUS_CHECKOUT_ENTRY_URL = 'https://chatgpt.com/';
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/plus-checkout.js'];
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
function createPlusCheckoutCreateExecutor(deps = {}) {
const {
@@ -40,9 +41,18 @@
return tabId;
}
function normalizePlusPaymentMethod(value = '') {
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY ? 'gopay' : 'paypal';
}
function getPlusPaymentMethodLabel(method = 'paypal') {
return normalizePlusPaymentMethod(method) === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay' : 'PayPal';
}
async function executePlusCheckoutCreate(state = {}) {
const checkoutModeLabel = getCheckoutModeLabel(state);
await addLog(`步骤 6:正在打开新的 ChatGPT 会话,准备创建${checkoutModeLabel}...`, 'info');
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
const paymentMethodLabel = getPlusPaymentMethodLabel(paymentMethod);
await addLog('步骤 6:正在新打开 ChatGPT 会话页,准备创建 Plus Checkout...', 'info');
const tabId = await openFreshChatGptTabForCheckoutCreate();
await waitForTabCompleteUntilStopped(tabId);
@@ -56,9 +66,7 @@
const result = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
type: 'CREATE_PLUS_CHECKOUT',
source: 'background',
payload: {
paymentMethod: normalizePlusPaymentMethod(state?.plusPaymentMethod),
},
payload: { paymentMethod },
});
if (result?.error) {
@@ -85,7 +93,7 @@
plusCheckoutCurrency: result.currency || 'EUR',
});
await addLog(`步骤 6${checkoutModeLabel}已就绪`, 'info');
await addLog(`步骤 6Plus Checkout 页面已就绪(${paymentMethodLabel} / ${result.country || 'DE'} ${result.currency || 'EUR'}),准备继续下一步`, 'info');
await completeStepFromBackground(6, {
plusCheckoutCountry: result.country || 'DE',
+72 -28
View File
@@ -7,6 +7,22 @@
const PLUS_CHECKOUT_FRAME_READY_DELAY_MS = 500;
const PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS = 3;
const PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS = 20000;
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PAYMENT_METHOD_CONFIGS = {
[PLUS_PAYMENT_METHOD_PAYPAL]: {
id: PLUS_PAYMENT_METHOD_PAYPAL,
label: 'PayPal',
selectMessageType: 'PLUS_CHECKOUT_SELECT_PAYPAL',
redirectPattern: /paypal\./i,
},
[PLUS_PAYMENT_METHOD_GOPAY]: {
id: PLUS_PAYMENT_METHOD_GOPAY,
label: 'GoPay',
selectMessageType: 'PLUS_CHECKOUT_SELECT_GOPAY',
redirectPattern: /gopay|gojek|midtrans|xendit|stripe|checkout/i,
},
};
const MEIGUODIZHI_ADDRESS_ENDPOINT = 'https://www.meiguodizhi.com/api/v1/dz';
const MEIGUODIZHI_COUNTRY_CONFIG = {
AR: { path: '/ar-address', city: 'Buenos Aires', aliases: ['ar', 'argentina', '阿根廷'] },
@@ -18,6 +34,7 @@
FR: { path: '/fr-address', city: 'Paris', aliases: ['fr', 'fra', 'france', '法国'] },
GB: { path: '/uk-address', city: 'London', aliases: ['gb', 'uk', 'united kingdom', 'britain', 'england', '英国'] },
HK: { path: '/hk-address', city: 'Hong Kong', aliases: ['hk', 'hong kong', '香港'] },
ID: { path: '/id-address', city: 'Jakarta', aliases: ['id', 'indonesia', '印度尼西亚', '印尼'] },
IT: { path: '/it-address', city: 'Rome', aliases: ['it', 'ita', 'italy', '意大利'] },
JP: { path: '/jp-address', city: 'Tokyo', aliases: ['jp', 'jpn', 'japan', '日本', '日本国'] },
KR: { path: '/kr-address', city: 'Seoul', aliases: ['kr', 'kor', 'korea', 'south korea', '韩国'] },
@@ -62,6 +79,16 @@
return normalizeText(value).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
}
function normalizePlusPaymentMethod(value = '') {
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY
? PLUS_PAYMENT_METHOD_GOPAY
: PLUS_PAYMENT_METHOD_PAYPAL;
}
function getPaymentMethodConfig(method = PLUS_PAYMENT_METHOD_PAYPAL) {
return PAYMENT_METHOD_CONFIGS[normalizePlusPaymentMethod(method)] || PAYMENT_METHOD_CONFIGS[PLUS_PAYMENT_METHOD_PAYPAL];
}
function resolveMeiguodizhiCountryCode(value = '') {
const normalized = normalizeText(value);
const upper = normalized.toUpperCase();
@@ -73,7 +100,7 @@
compact === code.toLowerCase()
|| (config.aliases || []).some((alias) => {
const compactAlias = compactCountryText(alias);
return compact === compactAlias || compact.includes(compactAlias);
return compact === compactAlias || (compactAlias.length >= 4 && compact.includes(compactAlias));
})
));
return match?.[0] || '';
@@ -170,9 +197,11 @@
};
}
async function resolveBillingAddressSeed(state = {}, countryOverride = '') {
const requestedCountry = normalizeText(countryOverride || state.plusCheckoutCountry || 'DE');
const countryCode = resolveMeiguodizhiCountryCode(requestedCountry) || 'DE';
async function resolveBillingAddressSeed(state = {}, countryOverride = '', options = {}) {
const paymentMethod = normalizePlusPaymentMethod(options.paymentMethod || state?.plusPaymentMethod);
const forcedCountry = paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'ID' : '';
const requestedCountry = normalizeText(forcedCountry || countryOverride || state.plusCheckoutCountry || 'DE');
const countryCode = forcedCountry || resolveMeiguodizhiCountryCode(requestedCountry) || 'DE';
const localSeed = getLocalAddressSeed(countryCode);
const lookupSeed = localSeed || buildMeiguodizhiLookupSeed(countryCode);
if (!lookupSeed) {
@@ -296,21 +325,22 @@
});
}
async function waitForPayPalRedirectAfterSubmit(tabId) {
async function waitForPaymentRedirectAfterSubmit(tabId, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
const paymentConfig = getPaymentMethodConfig(paymentMethod);
const startedAt = Date.now();
while (Date.now() - startedAt < PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS) {
const tab = await chrome.tabs.get(tabId).catch(() => null);
if (!tab) {
throw new Error('步骤 7:checkout 标签页已关闭,无法继续等待 PayPal 跳转。');
throw new Error(`步骤 7:checkout 标签页已关闭,无法继续等待 ${paymentConfig.label} 跳转。`);
}
const url = String(tab.url || '');
if (/paypal\./i.test(url)) {
if (paymentConfig.redirectPattern.test(url) && !isPlusCheckoutUrl(url)) {
await waitForTabCompleteUntilStopped(tabId);
await sleepWithStop(1000);
return true;
}
if (url && !isPlusCheckoutUrl(url)) {
await addLog(`步骤 7:点击订阅后页面跳转到非 PayPal 地址:${url}`, 'warn');
await addLog(`步骤 7:点击订阅后页面跳转到非 ${paymentConfig.label} 识别地址:${url}`, 'warn');
return false;
}
await sleepWithStop(500);
@@ -318,6 +348,10 @@
return false;
}
async function waitForPayPalRedirectAfterSubmit(tabId) {
return waitForPaymentRedirectAfterSubmit(tabId, PLUS_PAYMENT_METHOD_PAYPAL);
}
async function inspectCheckoutFrame(tabId, frame) {
try {
const result = await sendFrameMessage(tabId, frame.frameId, {
@@ -353,6 +387,7 @@
.map((item) => {
const flags = [];
if (item.result?.hasPayPal) flags.push('paypal');
if (item.result?.hasGoPay) flags.push('gopay');
if (item.result?.billingFieldsVisible) flags.push('billing');
if (item.result?.hasSubscribeButton) flags.push('subscribe');
if (!flags.length && item.error) flags.push(item.error);
@@ -372,7 +407,13 @@
return inspections;
}
function pickPaymentFrame(inspections) {
function pickPaymentFrame(inspections, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
const normalizedPaymentMethod = normalizePlusPaymentMethod(paymentMethod);
if (normalizedPaymentMethod === PLUS_PAYMENT_METHOD_GOPAY) {
return inspections.find((item) => item.result?.hasGoPay || item.result?.gopayCandidates?.length)
|| inspections.find((item) => isPaymentFrameUrl(item.frame.url))
|| null;
}
return inspections.find((item) => item.result?.hasPayPal || item.result?.paypalCandidates?.length)
|| inspections.find((item) => isPaymentFrameUrl(item.frame.url))
|| null;
@@ -418,14 +459,14 @@
const amountLabel = amountSummary.rawAmount || (
Number.isFinite(Number(amountSummary.amount)) ? String(amountSummary.amount) : '未知金额'
);
await addLog(`步骤 7${phaseLabel}检测到今日应付金额不是 0${amountLabel}),说明当前账号没有免费试用资格,将跳过 PayPal 提交。`, 'warn');
await addLog(`步骤 7${phaseLabel}检测到今日应付金额不是 0${amountLabel}),说明当前账号没有免费试用资格,将跳过支付提交。`, 'warn');
if (typeof markCurrentRegistrationAccountUsed === 'function') {
await markCurrentRegistrationAccountUsed(state, {
reason: 'plus-checkout-non-free-trial',
logPrefix: 'Plus Checkout:当前账号没有免费试用资格',
});
}
throw new Error(`PLUS_CHECKOUT_NON_FREE_TRIAL::步骤 7:今日应付金额不是 0(${amountLabel}),当前账号没有免费试用资格,已跳过 PayPal 提交。`);
throw new Error(`PLUS_CHECKOUT_NON_FREE_TRIAL::步骤 7:今日应付金额不是 0(${amountLabel}),当前账号没有免费试用资格,已跳过支付提交。`);
}
async function getReadyCheckoutFrames(tabId) {
@@ -445,9 +486,9 @@
};
}
async function resolvePaymentFrame(tabId, frames) {
async function resolvePaymentFrame(tabId, frames, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
const inspections = await inspectCheckoutFrames(tabId, frames);
const picked = pickPaymentFrame(inspections);
const picked = pickPaymentFrame(inspections, paymentMethod);
if (picked) {
return {
frameId: picked.frame.frameId,
@@ -519,6 +560,8 @@
}
async function executePlusCheckoutBilling(state = {}) {
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
const paymentConfig = getPaymentMethodConfig(paymentMethod);
const tabId = await getCheckoutTabId(state);
await addLog('步骤 7:正在等待 Plus Checkout 页面加载完成...', 'info');
await waitForTabCompleteUntilStopped(tabId);
@@ -533,27 +576,27 @@
await ensureFreeTrialAmount(tabId, state, {
phaseLabel: 'Checkout 页面加载后',
});
const paymentFrame = await resolvePaymentFrame(tabId, readyFrames);
const paymentFrame = await resolvePaymentFrame(tabId, readyFrames, paymentMethod);
if (paymentFrame.frameId === null) {
const frameSummary = buildFrameSummary(paymentFrame.inspections);
throw new Error(`步骤 7:未在主页面或 iframe 中发现 PayPal DOM,无法自动切换付款方式。frame 摘要:${frameSummary}`);
throw new Error(`步骤 7:未在主页面或 iframe 中发现 ${paymentConfig.label} DOM,无法自动切换付款方式。frame 摘要:${frameSummary}`);
}
if (!paymentFrame.ready) {
throw new Error(`步骤 7:已定位到 PayPal 所在 iframeframeId=${paymentFrame.frameId}),但账单脚本无法注入该 iframe。请提供该 iframe 的控制台结构或截图。`);
throw new Error(`步骤 7:已定位到 ${paymentConfig.label} 所在 iframeframeId=${paymentFrame.frameId}),但账单脚本无法注入该 iframe。请提供该 iframe 的控制台结构或截图。`);
}
if (paymentFrame.frameId !== 0) {
await addLog(`步骤 7PayPal 位于 checkout iframeframeId=${paymentFrame.frameId}),将改为在该 frame 内操作。`, 'info');
await addLog(`步骤 7${paymentConfig.label} 位于 checkout iframeframeId=${paymentFrame.frameId}),将改为在该 frame 内操作。`, 'info');
}
const randomName = generateRandomName();
const fullName = [randomName.firstName, randomName.lastName].filter(Boolean).join(' ');
await addLog('步骤 7:正在切换 PayPal 付款方式...', 'info');
await addLog(`步骤 7:正在切换 ${paymentConfig.label} 付款方式...`, 'info');
const paymentResult = await sendFrameMessage(tabId, paymentFrame.frameId, {
type: 'PLUS_CHECKOUT_SELECT_PAYPAL',
type: paymentConfig.selectMessageType,
source: 'background',
payload: {},
payload: { paymentMethod },
});
if (paymentResult?.error) {
throw new Error(paymentResult.error);
@@ -567,7 +610,7 @@
await addLog(`步骤 7:账单地址位于 checkout iframeframeId=${billingFrame.frameId}),将改为在该 frame 内填写。`, 'info');
}
const addressSeed = await resolveBillingAddressSeed(state, billingFrame.countryText);
const addressSeed = await resolveBillingAddressSeed(state, billingFrame.countryText, { paymentMethod });
if (!addressSeed) {
throw new Error('步骤 7:未找到可用的本地账单地址种子。');
}
@@ -645,7 +688,7 @@
phaseLabel: '提交订阅前',
});
let redirectedToPayPal = false;
let redirectedToPayment = false;
let lastSubmitError = '';
for (let attempt = 1; attempt <= PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS; attempt += 1) {
await addLog(
@@ -666,6 +709,7 @@
source: 'background',
payload: {
beforeClickDelayMs: attempt === 1 ? 700 : 1200,
paymentMethod,
},
});
if (subscribeResult?.error) {
@@ -674,17 +718,17 @@
continue;
}
await addLog(`步骤 7:账单地址已提交,正在等待跳转到 PayPal${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS}...`, 'info');
redirectedToPayPal = await waitForPayPalRedirectAfterSubmit(tabId);
if (redirectedToPayPal) {
await addLog(`步骤 7:账单地址已提交,正在等待跳转到 ${paymentConfig.label}${attempt}/${PLUS_CHECKOUT_SUBMIT_MAX_ATTEMPTS}...`, 'info');
redirectedToPayment = await waitForPaymentRedirectAfterSubmit(tabId, paymentMethod);
if (redirectedToPayment) {
break;
}
lastSubmitError = `提交后 ${Math.round(PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS / 1000)} 秒内未跳转到 PayPal`;
lastSubmitError = `提交后 ${Math.round(PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS / 1000)} 秒内未跳转到 ${paymentConfig.label}`;
await addLog(`步骤 7${lastSubmitError},将重试提交。`, 'warn');
}
if (!redirectedToPayPal) {
throw new Error(`步骤 7:多次提交账单地址后仍未跳转到 PayPal${lastSubmitError}`);
if (!redirectedToPayment) {
throw new Error(`步骤 7:多次提交账单地址后仍未跳转到 ${paymentConfig.label}${lastSubmitError}`);
}
await completeStepFromBackground(7, {
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -2,6 +2,7 @@
root.MultiPageBackgroundPlusReturnConfirm = factory();
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPlusReturnConfirmModule() {
const PAYPAL_SOURCE = 'paypal-flow';
const GOPAY_SOURCE = 'gopay-flow';
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
const PLUS_RETURN_SETTLE_WAIT_MS = 20000;
@@ -21,6 +22,10 @@
if (paypalTabId && await isTabAlive(PAYPAL_SOURCE)) {
return paypalTabId;
}
const gopayTabId = await getTabId(GOPAY_SOURCE);
if (gopayTabId && await isTabAlive(GOPAY_SOURCE)) {
return gopayTabId;
}
const checkoutTabId = await getTabId(PLUS_CHECKOUT_SOURCE);
if (checkoutTabId) {
return checkoutTabId;
@@ -29,17 +34,17 @@
if (storedTabId) {
return storedTabId;
}
throw new Error('步骤 9:未找到 Plus / PayPal 标签页,无法确认订阅回跳。');
throw new Error('步骤 9:未找到 Plus / PayPal / GoPay 标签页,无法确认订阅回跳。');
}
function isReturnUrl(url = '') {
return /https:\/\/(?:chatgpt\.com|chat\.openai\.com|openai\.com)\//i.test(String(url || ''))
&& !/paypal\./i.test(String(url || ''));
&& !/paypal\.|gopay|gojek|midtrans|xendit|stripe/i.test(String(url || ''));
}
async function executePlusReturnConfirm(state = {}) {
const tabId = await resolveReturnTabId(state);
await addLog('步骤 9:正在等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面...', 'info');
await addLog('步骤 9:正在等待支付授权后回跳到 ChatGPT / OpenAI 页面...', 'info');
const tab = await waitForTabUrlMatchUntilStopped(tabId, isReturnUrl);
await addLog('步骤 9:已检测到订阅回跳页面,固定等待 20 秒让页面完成加载。', 'info');
await sleepWithStop(PLUS_RETURN_SETTLE_WAIT_MS);