feat(gopay): support GoPay Plus checkout flow
This commit is contained in:
+87
-5
@@ -4,6 +4,7 @@ importScripts(
|
||||
'managed-alias-utils.js',
|
||||
'mail2925-utils.js',
|
||||
'paypal-utils.js',
|
||||
'gopay-utils.js',
|
||||
'phone-sms/providers/hero-sms.js',
|
||||
'phone-sms/providers/five-sim.js',
|
||||
'phone-sms/providers/registry.js',
|
||||
@@ -36,6 +37,7 @@ importScripts(
|
||||
'background/steps/fill-plus-checkout.js',
|
||||
'background/steps/gopay-manual-confirm.js',
|
||||
'background/steps/paypal-approve.js',
|
||||
'background/steps/gopay-approve.js',
|
||||
'background/steps/plus-return-confirm.js',
|
||||
'background/steps/oauth-login.js',
|
||||
'background/steps/fetch-login-code.js',
|
||||
@@ -347,6 +349,9 @@ const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const FIVE_SIM_OPERATOR = 'any';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
const DISPLAY_TIMEZONE = 'Asia/Shanghai';
|
||||
const MICROSOFT_TOKEN_DNR_RULE_ID = 1001;
|
||||
const PERSISTENT_ALIAS_STATE_KEYS = [
|
||||
@@ -516,10 +521,14 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
codex2apiAdminKey: '',
|
||||
customPassword: '',
|
||||
plusModeEnabled: false,
|
||||
plusPaymentMethod: 'paypal',
|
||||
plusPaymentMethod: DEFAULT_PLUS_PAYMENT_METHOD,
|
||||
paypalEmail: '',
|
||||
paypalPassword: '',
|
||||
currentPayPalAccountId: '',
|
||||
gopayCountryCode: '+86',
|
||||
gopayPhone: '',
|
||||
gopayOtp: '',
|
||||
gopayPin: '',
|
||||
autoRunSkipFailures: false,
|
||||
autoRunFallbackThreadIntervalMinutes: 0,
|
||||
oauthFlowTimeoutEnabled: true,
|
||||
@@ -645,6 +654,7 @@ const DEFAULT_STATE = {
|
||||
plusBillingCountryText: '',
|
||||
plusBillingAddress: null,
|
||||
plusPaypalApprovedAt: null,
|
||||
plusGoPayApprovedAt: null,
|
||||
plusReturnUrl: '',
|
||||
plusManualConfirmationPending: false,
|
||||
plusManualConfirmationRequestId: '',
|
||||
@@ -962,6 +972,16 @@ function normalizePhoneSmsProvider(value = '') {
|
||||
: PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
}
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.GoPayUtils?.normalizePlusPaymentMethod) {
|
||||
return rootScope.GoPayUtils.normalizePlusPaymentMethod(value);
|
||||
}
|
||||
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY
|
||||
? PLUS_PAYMENT_METHOD_GOPAY
|
||||
: PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
}
|
||||
|
||||
function normalizeFiveSimCountryId(value, fallback = FIVE_SIM_COUNTRY_ID) {
|
||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||
if (rootScope.PhoneSmsFiveSimProvider?.normalizeFiveSimCountryId) {
|
||||
@@ -1803,14 +1823,30 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return String(value || '').trim();
|
||||
case 'customPassword':
|
||||
return String(value || '');
|
||||
case 'plusPaymentMethod':
|
||||
return normalizePlusPaymentMethod(value);
|
||||
case 'paypalEmail':
|
||||
return String(value || '').trim();
|
||||
case 'paypalPassword':
|
||||
return String(value || '');
|
||||
case 'currentPayPalAccountId':
|
||||
return String(value || '').trim();
|
||||
case 'plusPaymentMethod':
|
||||
return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal';
|
||||
case 'gopayCountryCode':
|
||||
return self.GoPayUtils?.normalizeGoPayCountryCode
|
||||
? self.GoPayUtils.normalizeGoPayCountryCode(value)
|
||||
: String(value || '+86').trim();
|
||||
case 'gopayPhone':
|
||||
return self.GoPayUtils?.normalizeGoPayPhone
|
||||
? self.GoPayUtils.normalizeGoPayPhone(value)
|
||||
: String(value || '').trim();
|
||||
case 'gopayOtp':
|
||||
return self.GoPayUtils?.normalizeGoPayOtp
|
||||
? self.GoPayUtils.normalizeGoPayOtp(value)
|
||||
: String(value || '').trim().replace(/[^\d]/g, '');
|
||||
case 'gopayPin':
|
||||
return self.GoPayUtils?.normalizeGoPayPin
|
||||
? self.GoPayUtils.normalizeGoPayPin(value)
|
||||
: String(value || '');
|
||||
case 'autoRunSkipFailures':
|
||||
case 'oauthFlowTimeoutEnabled':
|
||||
case 'autoRunDelayEnabled':
|
||||
@@ -6697,6 +6733,11 @@ function isPlusCheckoutNonFreeTrialFailure(error) {
|
||||
return /PLUS_CHECKOUT_NON_FREE_TRIAL::|今日应付金额不是\s*0|没有免费试用资格/i.test(message);
|
||||
}
|
||||
|
||||
function isGoPayCheckoutRestartRequiredFailure(error) {
|
||||
const message = getErrorMessage(error);
|
||||
return /GOPAY_RESTART_FROM_STEP6::|GOPAY_RETRY_REQUIRED::/i.test(message);
|
||||
}
|
||||
|
||||
function isStep9RecoverableAuthError(error) {
|
||||
const message = String(typeof error === 'string' ? error : error?.message || '');
|
||||
return /STEP9_OAUTH_RETRY::/i.test(message)
|
||||
@@ -6746,6 +6787,7 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
plusBillingCountryText: '',
|
||||
plusBillingAddress: null,
|
||||
plusPaypalApprovedAt: null,
|
||||
plusGoPayApprovedAt: null,
|
||||
plusReturnUrl: '',
|
||||
plusManualConfirmationPending: false,
|
||||
plusManualConfirmationRequestId: '',
|
||||
@@ -6828,6 +6870,7 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
plusBillingCountryText: '',
|
||||
plusBillingAddress: null,
|
||||
plusPaypalApprovedAt: null,
|
||||
plusGoPayApprovedAt: null,
|
||||
plusReturnUrl: '',
|
||||
plusManualConfirmationPending: false,
|
||||
plusManualConfirmationRequestId: '',
|
||||
@@ -6838,6 +6881,7 @@ function getDownstreamStateResets(step, state = {}) {
|
||||
} : {}),
|
||||
...(step === 8 ? {
|
||||
plusPaypalApprovedAt: null,
|
||||
plusGoPayApprovedAt: null,
|
||||
plusReturnUrl: '',
|
||||
} : {}),
|
||||
lastLoginCode: null,
|
||||
@@ -9202,6 +9246,7 @@ async function ensureAutoEmailReady(targetRun, totalRuns, attemptRuns) {
|
||||
async function runAutoSequenceFromStep(startStep, context = {}) {
|
||||
const { targetRun, totalRuns, attemptRuns, continued = false } = context;
|
||||
let postStep7RestartCount = 0;
|
||||
let goPayCheckoutRestartCount = 0;
|
||||
let step4RestartCount = 0;
|
||||
let currentStartStep = startStep;
|
||||
let continueCurrentAttempt = continued;
|
||||
@@ -9270,6 +9315,23 @@ async function runAutoSequenceFromStep(startStep, context = {}) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (step === 8 && isGoPayCheckoutRestartRequiredFailure(err)) {
|
||||
goPayCheckoutRestartCount += 1;
|
||||
if (goPayCheckoutRestartCount > 3) {
|
||||
await addLog(`步骤 8:GoPay Checkout 已连续重建 ${goPayCheckoutRestartCount - 1} 次仍失败,停止自动重试。原因:${getErrorMessage(err)}`, 'error');
|
||||
throw err;
|
||||
}
|
||||
await addLog(
|
||||
`步骤 8:检测到 GoPay 支付页失效/卡死,准备关闭旧页并回到步骤 6 重新创建 Checkout(第 ${goPayCheckoutRestartCount}/3 次)。原因:${getErrorMessage(err)}`,
|
||||
'warn'
|
||||
);
|
||||
await invalidateDownstreamAfterStepRestart(5, {
|
||||
logLabel: `步骤 8 GoPay 支付页失效后准备回到步骤 6 重试(第 ${goPayCheckoutRestartCount}/3 次)`,
|
||||
});
|
||||
step = 6;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (step === 4) {
|
||||
if (isSignupUserAlreadyExistsFailure(err)) {
|
||||
throw err;
|
||||
@@ -9690,6 +9752,7 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c
|
||||
sendTabMessageUntilStopped,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
throwIfStopped,
|
||||
waitForTabCompleteUntilStopped,
|
||||
});
|
||||
const plusCheckoutBillingExecutor = self.MultiPageBackgroundPlusCheckoutBilling?.createPlusCheckoutBillingExecutor({
|
||||
@@ -9731,6 +9794,24 @@ const payPalApproveExecutor = self.MultiPageBackgroundPayPalApprove?.createPayPa
|
||||
waitForTabCompleteUntilStopped,
|
||||
waitForTabUrlMatchUntilStopped,
|
||||
});
|
||||
const goPayApproveExecutor = self.MultiPageBackgroundGoPayApprove?.createGoPayApproveExecutor({
|
||||
addLog,
|
||||
chrome,
|
||||
completeStepFromBackground,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
getTabId,
|
||||
isTabAlive,
|
||||
registerTab,
|
||||
sendTabMessageUntilStopped,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
waitForTabCompleteUntilStopped,
|
||||
clickWithDebugger,
|
||||
requestGoPayOtpInput: (payload = {}) => chrome.runtime.sendMessage({
|
||||
type: 'REQUEST_GOPAY_OTP_INPUT',
|
||||
payload,
|
||||
}),
|
||||
});
|
||||
const plusReturnConfirmExecutor = self.MultiPageBackgroundPlusReturnConfirm?.createPlusReturnConfirmExecutor({
|
||||
addLog,
|
||||
completeStepFromBackground,
|
||||
@@ -9768,8 +9849,9 @@ 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),
|
||||
'paypal-approve': (state) => normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY
|
||||
? goPayApproveExecutor.executeGoPayApprove(state)
|
||||
: payPalApproveExecutor.executePayPalApprove(state),
|
||||
'plus-checkout-return': (state) => plusReturnConfirmExecutor.executePlusReturnConfirm(state),
|
||||
'oauth-login': (state) => step7Executor.executeStep7(state),
|
||||
'fetch-login-code': (state) => step8Executor.executeStep8(state),
|
||||
|
||||
@@ -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(`步骤 6:Plus Checkout 页面已就绪(${paymentMethodLabel} / ${result.country || 'DE'} ${result.currency || 'EUR'}),准备继续下一步。`, 'info');
|
||||
|
||||
await completeStepFromBackground(6, {
|
||||
plusCheckoutCountry: result.country || 'DE',
|
||||
|
||||
@@ -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 所在 iframe(frameId=${paymentFrame.frameId}),但账单脚本无法注入该 iframe。请提供该 iframe 的控制台结构或截图。`);
|
||||
throw new Error(`步骤 7:已定位到 ${paymentConfig.label} 所在 iframe(frameId=${paymentFrame.frameId}),但账单脚本无法注入该 iframe。请提供该 iframe 的控制台结构或截图。`);
|
||||
}
|
||||
|
||||
if (paymentFrame.frameId !== 0) {
|
||||
await addLog(`步骤 7:PayPal 位于 checkout iframe(frameId=${paymentFrame.frameId}),将改为在该 frame 内操作。`, 'info');
|
||||
await addLog(`步骤 7:${paymentConfig.label} 位于 checkout iframe(frameId=${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 iframe(frameId=${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
@@ -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);
|
||||
|
||||
@@ -0,0 +1,725 @@
|
||||
// content/gopay-flow.js — GoPay authorization helper.
|
||||
|
||||
console.log('[MultiPage:gopay-flow] Content script loaded on', location.href);
|
||||
|
||||
const GOPAY_FLOW_LISTENER_SENTINEL = 'data-multipage-gopay-flow-listener';
|
||||
|
||||
if (document.documentElement.getAttribute(GOPAY_FLOW_LISTENER_SENTINEL) !== '1') {
|
||||
document.documentElement.setAttribute(GOPAY_FLOW_LISTENER_SENTINEL, '1');
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (
|
||||
message.type === 'GOPAY_GET_STATE'
|
||||
|| message.type === 'GOPAY_SUBMIT_PHONE'
|
||||
|| message.type === 'GOPAY_SUBMIT_OTP'
|
||||
|| message.type === 'GOPAY_SUBMIT_PIN'
|
||||
|| message.type === 'GOPAY_CLICK_CONTINUE'
|
||||
|| message.type === 'GOPAY_GET_CONTINUE_TARGET'
|
||||
|| message.type === 'GOPAY_CLICK_PAY_NOW'
|
||||
|| message.type === 'GOPAY_GET_PAY_NOW_TARGET'
|
||||
) {
|
||||
resetStopState();
|
||||
handleGoPayCommand(message).then((result) => {
|
||||
sendResponse({ ok: true, ...(result || {}) });
|
||||
}).catch((err) => {
|
||||
if (isStopError(err)) {
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('[MultiPage:gopay-flow] 消息监听已存在,跳过重复注册');
|
||||
}
|
||||
|
||||
async function handleGoPayCommand(message) {
|
||||
switch (message.type) {
|
||||
case 'GOPAY_GET_STATE':
|
||||
return inspectGoPayState();
|
||||
case 'GOPAY_SUBMIT_PHONE':
|
||||
return submitGoPayPhone(message.payload || {});
|
||||
case 'GOPAY_SUBMIT_OTP':
|
||||
return submitGoPayOtp(message.payload || {});
|
||||
case 'GOPAY_SUBMIT_PIN':
|
||||
return submitGoPayPin(message.payload || {});
|
||||
case 'GOPAY_CLICK_CONTINUE':
|
||||
return clickGoPayContinue();
|
||||
case 'GOPAY_GET_CONTINUE_TARGET':
|
||||
return getGoPayContinueTarget();
|
||||
case 'GOPAY_CLICK_PAY_NOW':
|
||||
return clickGoPayPayNow();
|
||||
case 'GOPAY_GET_PAY_NOW_TARGET':
|
||||
return getGoPayPayNowTarget();
|
||||
default:
|
||||
throw new Error(`gopay-flow.js 不处理消息:${message.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitUntil(predicate, options = {}) {
|
||||
const intervalMs = Math.max(50, Math.floor(Number(options.intervalMs) || 250));
|
||||
const timeoutMs = Math.max(0, Math.floor(Number(options.timeoutMs) || 0));
|
||||
const startedAt = Date.now();
|
||||
while (true) {
|
||||
throwIfStopped();
|
||||
const value = await predicate();
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) {
|
||||
throw new Error(options.timeoutMessage || `${options.label || 'GoPay 页面状态'}等待超时`);
|
||||
}
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
async function waitForDocumentComplete() {
|
||||
await waitUntil(() => document.readyState === 'complete', { intervalMs: 200 });
|
||||
await sleep(800);
|
||||
}
|
||||
|
||||
function isVisibleElement(el) {
|
||||
if (!el) return false;
|
||||
let node = el;
|
||||
while (node && node.nodeType === 1) {
|
||||
if (node.hidden || node.getAttribute?.('aria-hidden') === 'true' || node.getAttribute?.('inert') !== null) {
|
||||
return false;
|
||||
}
|
||||
const nodeStyle = window.getComputedStyle(node);
|
||||
if (
|
||||
nodeStyle.display === 'none'
|
||||
|| nodeStyle.visibility === 'hidden'
|
||||
|| nodeStyle.visibility === 'collapse'
|
||||
|| Number(nodeStyle.opacity) === 0
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
const style = window.getComputedStyle(el);
|
||||
const rect = el.getBoundingClientRect();
|
||||
return style.display !== 'none'
|
||||
&& style.visibility !== 'hidden'
|
||||
&& Number(rect.width) > 0
|
||||
&& Number(rect.height) > 0;
|
||||
}
|
||||
|
||||
function normalizeText(text = '') {
|
||||
return String(text || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function getActionText(el) {
|
||||
return normalizeText([
|
||||
el?.textContent,
|
||||
el?.value,
|
||||
el?.getAttribute?.('aria-label'),
|
||||
el?.getAttribute?.('title'),
|
||||
el?.getAttribute?.('placeholder'),
|
||||
el?.getAttribute?.('name'),
|
||||
el?.id,
|
||||
].filter(Boolean).join(' '));
|
||||
}
|
||||
|
||||
function getVisibleControls(selector) {
|
||||
return Array.from(document.querySelectorAll(selector)).filter(isVisibleElement);
|
||||
}
|
||||
|
||||
function isEnabledControl(el) {
|
||||
return Boolean(el)
|
||||
&& !el.disabled
|
||||
&& el.getAttribute?.('aria-disabled') !== 'true';
|
||||
}
|
||||
|
||||
function getVisibleTextInputs() {
|
||||
return getVisibleControls('input, textarea')
|
||||
.filter((input) => {
|
||||
const type = String(input.getAttribute('type') || input.type || '').trim().toLowerCase();
|
||||
return isEnabledControl(input) && !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
|
||||
});
|
||||
}
|
||||
|
||||
function findInputByPatterns(patterns) {
|
||||
return getVisibleTextInputs().find((input) => {
|
||||
const text = getActionText(input);
|
||||
return patterns.some((pattern) => pattern.test(text));
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function findPhoneInput() {
|
||||
const input = findInputByPatterns([
|
||||
/gopay|go\s*pay|phone|mobile|whatsapp|wa|nomor|ponsel|telepon|hp/i,
|
||||
/手机|手机号|电话号码|电话/i,
|
||||
]);
|
||||
if (input && !isCountrySearchInput(input)) {
|
||||
return input;
|
||||
}
|
||||
return getVisibleControls('input[type="tel"]').find((candidate) => isEnabledControl(candidate) && !isCountrySearchInput(candidate)) || null;
|
||||
}
|
||||
|
||||
function isCountrySearchInput(input) {
|
||||
const text = getActionText(input);
|
||||
return /country|country\s*name|country\s*code|国家|地区|区号/i.test(text);
|
||||
}
|
||||
|
||||
function getPageBodyText() {
|
||||
return normalizeText(document.body?.innerText || document.body?.textContent || '');
|
||||
}
|
||||
|
||||
function isGoPayOtpPageText() {
|
||||
if (isGoPayPinPageText()) {
|
||||
return false;
|
||||
}
|
||||
const text = getPageBodyText();
|
||||
return /otp|one[-\s]*time|kode|verification|whatsapp|验证码|短信/i.test(text);
|
||||
}
|
||||
|
||||
function isGoPayPinPageText() {
|
||||
const text = getPageBodyText();
|
||||
return /pin|password|passcode|security|sandi|6\s*digit/i.test(text)
|
||||
|| /pin-web-client\.gopayapi\.com|\/auth\/pin/i.test(location.href || '');
|
||||
}
|
||||
|
||||
function detectGoPayTerminalError(text = getPageBodyText()) {
|
||||
const normalizedText = normalizeText(text);
|
||||
if (!normalizedText) return null;
|
||||
|
||||
if (/waktunya\s+habis|ulang(?:i)?\s+prosesnya\s+dari\s+awal|time(?:'s|\s+is)?\s+(?:out|expired)|session\s+expired|expired|kedaluwarsa/i.test(normalizedText)) {
|
||||
return {
|
||||
code: 'expired',
|
||||
message: 'GoPay 支付会话已超时,需要重新创建 Plus Checkout。',
|
||||
rawText: normalizedText.slice(0, 240),
|
||||
};
|
||||
}
|
||||
|
||||
if (/technical\s+error|don[’']t\s+worry|try\s+again|terjadi\s+kesalahan|error\s+teknis/i.test(normalizedText)) {
|
||||
return {
|
||||
code: 'technical-error',
|
||||
message: 'GoPay 页面显示技术错误,需要重新发起支付授权。',
|
||||
rawText: normalizedText.slice(0, 240),
|
||||
};
|
||||
}
|
||||
|
||||
if (/payment\s+failed|pembayaran\s+gagal|transaksi\s+gagal|ditolak|declined|failed/i.test(normalizedText)) {
|
||||
return {
|
||||
code: 'failed',
|
||||
message: 'GoPay 页面显示支付失败,需要重新发起支付授权。',
|
||||
rawText: normalizedText.slice(0, 240),
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function findOtpInput() {
|
||||
const input = findInputByPatterns([
|
||||
/otp|one[-\s]*time|verification|verify|code|kode|whatsapp|wa|pin-input-field/i,
|
||||
/验证码|短信|代码/i,
|
||||
]);
|
||||
if (input && !isCountrySearchInput(input)) {
|
||||
return input;
|
||||
}
|
||||
if (isGoPayOtpPageText()) {
|
||||
return getVisibleTextInputs().find((candidate) => !isCountrySearchInput(candidate)) || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getGoPayPinInputs() {
|
||||
return getVisibleTextInputs().filter((candidate) => {
|
||||
if (isCountrySearchInput(candidate)) return false;
|
||||
const text = getCombinedElementText(candidate);
|
||||
const maxLength = Number(candidate.getAttribute?.('maxlength') || candidate.maxLength || 0);
|
||||
return /pin|password|passcode|security|sandi|pin-input/i.test(text)
|
||||
|| candidate.getAttribute?.('data-testid')?.startsWith?.('pin-input')
|
||||
|| (isGoPayPinPageText() && maxLength === 1);
|
||||
});
|
||||
}
|
||||
|
||||
function findPinInput() {
|
||||
if (isGoPayOtpPageText()) {
|
||||
return null;
|
||||
}
|
||||
const input = findInputByPatterns([
|
||||
/pin|password|passcode|security|sandi|pin-input/i,
|
||||
/密码|支付密码/i,
|
||||
]);
|
||||
if (input && !isCountrySearchInput(input)) {
|
||||
return input;
|
||||
}
|
||||
return getGoPayPinInputs()[0]
|
||||
|| getVisibleControls('input[type="password"]').find((candidate) => isEnabledControl(candidate) && !isCountrySearchInput(candidate))
|
||||
|| null;
|
||||
}
|
||||
|
||||
function findClickableByText(patterns) {
|
||||
const normalizedPatterns = (Array.isArray(patterns) ? patterns : [patterns]).filter(Boolean);
|
||||
const candidates = getVisibleControls('button, a, [role="button"], input[type="button"], input[type="submit"]');
|
||||
return candidates.find((el) => {
|
||||
if (!isEnabledControl(el)) return false;
|
||||
const text = getActionText(el);
|
||||
return normalizedPatterns.some((pattern) => pattern.test(text));
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function findPayNowButton() {
|
||||
return findClickableByText([
|
||||
/^\s*pay\s+now\s*$/i,
|
||||
/^\s*bayar(?:\s+sekarang)?(?:\s*rp[\s\S]*)?\s*$/i,
|
||||
/^\s*支付\s*$/i,
|
||||
/^\s*立即支付\s*$/i,
|
||||
]);
|
||||
}
|
||||
|
||||
function findContinueButton() {
|
||||
return findClickableByText([
|
||||
/continue|next|submit|verify|confirm|pay|authorize|allow|lanjut|berikut|kirim|bayar|konfirmasi|link/i,
|
||||
/继续|下一步|提交|验证|确认|支付|授权|绑定|关联/i,
|
||||
]);
|
||||
}
|
||||
|
||||
function describeElement(el) {
|
||||
if (!el) return '';
|
||||
const rect = el.getBoundingClientRect?.();
|
||||
const parts = [String(el.tagName || '').toUpperCase()];
|
||||
if (el.id) parts.push(`#${el.id}`);
|
||||
const className = typeof el.className === 'string' ? el.className : el.getAttribute?.('class');
|
||||
if (className) parts.push(`.${String(className).trim().replace(/\s+/g, '.')}`);
|
||||
const text = getActionText(el) || normalizeText(el.innerText || el.textContent || '');
|
||||
if (text) parts.push(`"${text.slice(0, 60)}"`);
|
||||
if (rect) parts.push(`@${Math.round(rect.left)},${Math.round(rect.top)} ${Math.round(rect.width)}x${Math.round(rect.height)}`);
|
||||
return parts.filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
function dispatchPointerMouseSequence(target) {
|
||||
const rect = target.getBoundingClientRect?.();
|
||||
const clientX = rect ? Math.round(rect.left + rect.width / 2) : 0;
|
||||
const clientY = rect ? Math.round(rect.top + rect.height / 2) : 0;
|
||||
const eventInit = {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
composed: true,
|
||||
view: window,
|
||||
detail: 1,
|
||||
button: 0,
|
||||
buttons: 1,
|
||||
clientX,
|
||||
clientY,
|
||||
screenX: window.screenX + clientX,
|
||||
screenY: window.screenY + clientY,
|
||||
};
|
||||
if (typeof PointerEvent === 'function') {
|
||||
target.dispatchEvent(new PointerEvent('pointerover', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
|
||||
target.dispatchEvent(new PointerEvent('pointerenter', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true, bubbles: false }));
|
||||
target.dispatchEvent(new PointerEvent('pointermove', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
|
||||
target.dispatchEvent(new PointerEvent('pointerdown', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
|
||||
}
|
||||
target.dispatchEvent(new MouseEvent('mouseover', eventInit));
|
||||
target.dispatchEvent(new MouseEvent('mouseenter', { ...eventInit, bubbles: false }));
|
||||
target.dispatchEvent(new MouseEvent('mousemove', eventInit));
|
||||
target.dispatchEvent(new MouseEvent('mousedown', eventInit));
|
||||
if (typeof PointerEvent === 'function') {
|
||||
target.dispatchEvent(new PointerEvent('pointerup', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true, buttons: 0 }));
|
||||
}
|
||||
target.dispatchEvent(new MouseEvent('mouseup', { ...eventInit, buttons: 0 }));
|
||||
target.dispatchEvent(new MouseEvent('click', { ...eventInit, buttons: 0 }));
|
||||
}
|
||||
|
||||
async function humanClickElement(el, options = {}) {
|
||||
if (!el) {
|
||||
throw new Error('GoPay 页面未找到可点击元素。');
|
||||
}
|
||||
el.scrollIntoView?.({ block: 'center', inline: 'center' });
|
||||
await sleep(Math.max(0, Number(options.beforeMs) || 120));
|
||||
try {
|
||||
el.focus?.({ preventScroll: true });
|
||||
} catch (_) {
|
||||
try { el.focus?.(); } catch (__) {}
|
||||
}
|
||||
dispatchPointerMouseSequence(el);
|
||||
await sleep(Math.max(0, Number(options.afterDispatchMs) || 120));
|
||||
if (typeof el.click === 'function') {
|
||||
el.click();
|
||||
}
|
||||
await sleep(Math.max(0, Number(options.afterMs) || 1000));
|
||||
}
|
||||
|
||||
async function clickContinueIfPresent(options = {}) {
|
||||
const button = findContinueButton();
|
||||
if (!button) {
|
||||
return { clicked: false, target: '' };
|
||||
}
|
||||
await humanClickElement(button, options);
|
||||
return { clicked: true, target: describeElement(button) };
|
||||
}
|
||||
|
||||
function normalizePhoneNumber(value = '') {
|
||||
return String(value || '').trim().replace(/[^\d+]/g, '');
|
||||
}
|
||||
|
||||
function normalizeGoPayCountryCode(value = '') {
|
||||
const normalized = String(value || '').trim().replace(/[^\d+]/g, '');
|
||||
const digits = normalized.replace(/\D/g, '');
|
||||
return digits ? `+${digits}` : '+86';
|
||||
}
|
||||
|
||||
function getCountryCodeDigits(value = '') {
|
||||
return normalizeGoPayCountryCode(value).replace(/\D/g, '');
|
||||
}
|
||||
|
||||
function normalizeGoPayNationalPhone(value = '', countryCode = '+86') {
|
||||
const countryDigits = getCountryCodeDigits(countryCode);
|
||||
let digits = normalizePhoneNumber(value).replace(/\D/g, '');
|
||||
if (countryDigits && digits.startsWith(countryDigits)) {
|
||||
digits = digits.slice(countryDigits.length);
|
||||
}
|
||||
return digits;
|
||||
}
|
||||
|
||||
function getCombinedElementText(el) {
|
||||
return normalizeText([
|
||||
getActionText(el),
|
||||
el?.innerText,
|
||||
el?.textContent,
|
||||
typeof el?.className === 'string' ? el.className : el?.getAttribute?.('class'),
|
||||
].filter(Boolean).join(' '));
|
||||
}
|
||||
|
||||
function robustClick(el) {
|
||||
if (!el) return;
|
||||
el.scrollIntoView?.({ block: 'center', inline: 'center' });
|
||||
try {
|
||||
el.focus?.();
|
||||
} catch (_) {}
|
||||
const eventInit = { bubbles: true, cancelable: true, view: window };
|
||||
if (typeof PointerEvent === 'function') {
|
||||
el.dispatchEvent(new PointerEvent('pointerdown', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
|
||||
}
|
||||
el.dispatchEvent(new MouseEvent('mousedown', eventInit));
|
||||
if (typeof PointerEvent === 'function') {
|
||||
el.dispatchEvent(new PointerEvent('pointerup', { ...eventInit, pointerId: 1, pointerType: 'mouse', isPrimary: true }));
|
||||
}
|
||||
el.dispatchEvent(new MouseEvent('mouseup', eventInit));
|
||||
el.dispatchEvent(new MouseEvent('click', eventInit));
|
||||
if (typeof el.click === 'function') {
|
||||
el.click();
|
||||
}
|
||||
}
|
||||
|
||||
function readSelectedCountryCodeText() {
|
||||
const candidates = getVisibleControls('.phone-code, .phone-code-wrapper, [class*="phone-code"], button, [role="button"], [tabindex]');
|
||||
for (const candidate of candidates) {
|
||||
const match = getCombinedElementText(candidate).match(/\+\d{1,4}/);
|
||||
if (match) return normalizeGoPayCountryCode(match[0]);
|
||||
}
|
||||
const bodyMatch = normalizeText(document.body?.innerText || document.body?.textContent || '').match(/Phone number:\s*(\+\d{1,4})/i);
|
||||
return bodyMatch ? normalizeGoPayCountryCode(bodyMatch[1]) : '';
|
||||
}
|
||||
|
||||
function findCountryCodeToggle() {
|
||||
const preferred = getVisibleControls('.phone-code-wrapper, [class*="phone-code-wrapper"], .phone-code, [class*="phone-code"]')
|
||||
.find((el) => /\+\d{1,4}/.test(getCombinedElementText(el)));
|
||||
if (preferred) return preferred;
|
||||
return getVisibleControls('button, [role="button"], [tabindex], div, span')
|
||||
.find((el) => /\+\d{1,4}/.test(getCombinedElementText(el)) && /phone|code|country|\+\d{1,4}/i.test(getCombinedElementText(el))) || null;
|
||||
}
|
||||
|
||||
function findCountryCodeOption(countryCode = '+86') {
|
||||
const normalized = normalizeGoPayCountryCode(countryCode);
|
||||
const digits = getCountryCodeDigits(normalized);
|
||||
const countryAliases = {
|
||||
'1': [/United States|USA|Canada|美国|加拿大/i],
|
||||
'60': [/Malaysia|马来西亚/i],
|
||||
'62': [/Indonesia|印尼|印度尼西亚/i],
|
||||
'63': [/Philippines|菲律宾/i],
|
||||
'65': [/Singapore|新加坡/i],
|
||||
'66': [/Thailand|泰国/i],
|
||||
'84': [/Vietnam|越南/i],
|
||||
'86': [/China|中国|Mainland/i],
|
||||
'91': [/India|印度/i],
|
||||
'852': [/Hong Kong|香港/i],
|
||||
'853': [/Macau|Macao|澳门/i],
|
||||
'886': [/Taiwan|台湾/i],
|
||||
}[digits] || [];
|
||||
const controls = getVisibleControls('li.country-item, .country-item, [class*="country-item"], [role="option"], li, button, [role="button"], a, [tabindex], div, span')
|
||||
.map((el) => el.closest?.('.country-item') || el)
|
||||
.filter((el, index, list) => el && list.indexOf(el) === index)
|
||||
.filter((el) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const text = getCombinedElementText(el);
|
||||
const className = typeof el.className === 'string' ? el.className : el.getAttribute?.('class') || '';
|
||||
return text.includes(normalized)
|
||||
&& rect.width > 20
|
||||
&& rect.height > 8
|
||||
&& rect.width < Math.max(480, window.innerWidth * 0.8)
|
||||
&& rect.height < Math.max(120, window.innerHeight * 0.35)
|
||||
&& !/phone-number-input-wrapper|gopay-tokenization-content|asphalt-theme|search-country|country-list/i.test(className);
|
||||
});
|
||||
const matchesAlias = (el) => {
|
||||
const text = getCombinedElementText(el);
|
||||
return countryAliases.some((pattern) => pattern.test(text));
|
||||
};
|
||||
return controls.find((el) => /country-item/i.test(typeof el.className === 'string' ? el.className : el.getAttribute?.('class') || '') && matchesAlias(el))
|
||||
|| controls.find((el) => String(el.tagName || '').toUpperCase() === 'LI' && matchesAlias(el))
|
||||
|| controls.find((el) => el.getAttribute?.('role') === 'option' && matchesAlias(el))
|
||||
|| controls.find(matchesAlias)
|
||||
|| null;
|
||||
}
|
||||
|
||||
async function ensureGoPayCountryCode(countryCode = '+86') {
|
||||
const normalized = normalizeGoPayCountryCode(countryCode);
|
||||
const selected = readSelectedCountryCodeText();
|
||||
if (selected === normalized) {
|
||||
return { changed: false, countryCode: normalized, selected };
|
||||
}
|
||||
|
||||
const toggle = findCountryCodeToggle();
|
||||
if (!toggle) {
|
||||
throw new Error(`GoPay 页面未找到国家区号切换控件,当前识别区号:${selected || '未知'},目标区号:${normalized}`);
|
||||
}
|
||||
robustClick(toggle);
|
||||
await sleep(500);
|
||||
|
||||
const countryDropdown = document.querySelector('.search-country');
|
||||
if (countryDropdown && window.getComputedStyle(countryDropdown).display === 'none') {
|
||||
countryDropdown.style.display = 'block';
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
const countrySearchInput = getVisibleTextInputs().find(isCountrySearchInput);
|
||||
if (countrySearchInput) {
|
||||
fillInput(countrySearchInput, normalized);
|
||||
await sleep(300);
|
||||
}
|
||||
|
||||
const option = await waitUntil(() => findCountryCodeOption(normalized), {
|
||||
label: `GoPay 国家区号 ${normalized}`,
|
||||
intervalMs: 250,
|
||||
timeoutMs: 8000,
|
||||
});
|
||||
robustClick(option);
|
||||
await sleep(500);
|
||||
|
||||
const nextSelected = readSelectedCountryCodeText();
|
||||
if (nextSelected === normalized && countryDropdown) {
|
||||
countryDropdown.style.display = 'none';
|
||||
}
|
||||
if (nextSelected !== normalized) {
|
||||
throw new Error(`GoPay 国家区号切换失败:目标 ${normalized},当前 ${nextSelected || '未知'}`);
|
||||
}
|
||||
return {
|
||||
changed: true,
|
||||
countryCode: normalized,
|
||||
selected: nextSelected,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeOtp(value = '') {
|
||||
return String(value || '').trim().replace(/[^\d]/g, '');
|
||||
}
|
||||
|
||||
|
||||
function fillDigitInputs(inputs = [], code = '') {
|
||||
const normalizedCode = normalizeOtp(code);
|
||||
if (!normalizedCode || !inputs.length) return false;
|
||||
normalizedCode.split('').forEach((digit, index) => {
|
||||
const input = inputs[index];
|
||||
if (!input) return;
|
||||
try {
|
||||
input.focus?.();
|
||||
} catch (_) {}
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', { key: digit, code: `Digit${digit}`, bubbles: true, cancelable: true }));
|
||||
fillInput(input, digit);
|
||||
input.dispatchEvent(new KeyboardEvent('keyup', { key: digit, code: `Digit${digit}`, bubbles: true, cancelable: true }));
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
function fillVisiblePinInputs(pin = '') {
|
||||
const normalizedPin = normalizeOtp(pin);
|
||||
if (!normalizedPin) return false;
|
||||
const pinInputs = getGoPayPinInputs();
|
||||
const digitInputs = pinInputs.filter((input) => {
|
||||
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
|
||||
return maxLength > 0 && maxLength <= 1;
|
||||
});
|
||||
if (digitInputs.length >= Math.min(4, normalizedPin.length)) {
|
||||
return fillDigitInputs(digitInputs, normalizedPin);
|
||||
}
|
||||
const input = findPinInput() || pinInputs[0];
|
||||
if (!input) return false;
|
||||
fillInput(input, normalizedPin);
|
||||
return true;
|
||||
}
|
||||
|
||||
function fillVisibleOtpInputs(code = '') {
|
||||
const normalizedCode = normalizeOtp(code);
|
||||
if (!normalizedCode) return false;
|
||||
|
||||
const otpInputs = getVisibleTextInputs()
|
||||
.filter((input) => {
|
||||
const text = getActionText(input);
|
||||
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
|
||||
return /otp|code|kode|verification|验证码|短信/i.test(text)
|
||||
|| (maxLength > 0 && maxLength <= 1)
|
||||
|| (maxLength > 1 && maxLength <= 8);
|
||||
});
|
||||
|
||||
const digitInputs = otpInputs.filter((input) => {
|
||||
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
|
||||
return maxLength > 0 && maxLength <= 1;
|
||||
});
|
||||
if (digitInputs.length >= Math.min(4, normalizedCode.length)) {
|
||||
return fillDigitInputs(digitInputs, normalizedCode);
|
||||
}
|
||||
|
||||
const input = findOtpInput() || otpInputs[0];
|
||||
if (!input) return false;
|
||||
fillInput(input, normalizedCode);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function submitGoPayPhone(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const countryCode = normalizeGoPayCountryCode(payload.countryCode || payload.gopayCountryCode || '+86');
|
||||
const phone = normalizeGoPayNationalPhone(payload.phone || payload.gopayPhone || '', countryCode);
|
||||
if (!phone) {
|
||||
throw new Error('GoPay 手机号为空,请先在侧边栏配置。');
|
||||
}
|
||||
const countryResult = await ensureGoPayCountryCode(countryCode);
|
||||
const input = await waitUntil(() => findPhoneInput(), {
|
||||
label: 'GoPay 手机号输入框',
|
||||
intervalMs: 250,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
fillInput(input, phone);
|
||||
const clickResult = await clickContinueIfPresent();
|
||||
return {
|
||||
phoneSubmitted: true,
|
||||
countryCode,
|
||||
countryChanged: Boolean(countryResult.changed),
|
||||
clicked: Boolean(clickResult.clicked),
|
||||
clickTarget: clickResult.target || '',
|
||||
phase: 'phone_submitted',
|
||||
};
|
||||
}
|
||||
|
||||
async function submitGoPayOtp(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const code = normalizeOtp(payload.code || payload.otp || '');
|
||||
if (!code) {
|
||||
throw new Error('GoPay WhatsApp 验证码为空。');
|
||||
}
|
||||
const filled = await waitUntil(() => fillVisibleOtpInputs(code), {
|
||||
label: 'GoPay 验证码输入框',
|
||||
intervalMs: 250,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
const clickResult = await clickContinueIfPresent();
|
||||
return {
|
||||
otpSubmitted: Boolean(filled),
|
||||
clicked: Boolean(clickResult.clicked),
|
||||
clickTarget: clickResult.target || '',
|
||||
phase: 'otp_submitted',
|
||||
};
|
||||
}
|
||||
|
||||
async function submitGoPayPin(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const pin = normalizeOtp(payload.pin || payload.gopayPin || '');
|
||||
if (!pin) {
|
||||
throw new Error('GoPay PIN 为空,请先在侧边栏配置。');
|
||||
}
|
||||
const filled = await waitUntil(() => fillVisiblePinInputs(pin), {
|
||||
label: 'GoPay PIN 输入框',
|
||||
intervalMs: 250,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
const clickResult = await clickContinueIfPresent();
|
||||
return {
|
||||
pinSubmitted: Boolean(filled),
|
||||
clicked: Boolean(clickResult.clicked),
|
||||
clickTarget: clickResult.target || '',
|
||||
phase: 'pin_submitted',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function getElementClickRect(el) {
|
||||
if (!el) return null;
|
||||
const rect = el.getBoundingClientRect?.();
|
||||
if (!rect || !Number.isFinite(rect.left) || !Number.isFinite(rect.top)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
centerX: rect.left + rect.width / 2,
|
||||
centerY: rect.top + rect.height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
function getGoPayContinueTarget() {
|
||||
const button = findContinueButton();
|
||||
return {
|
||||
found: Boolean(button),
|
||||
target: describeElement(button),
|
||||
rect: getElementClickRect(button),
|
||||
};
|
||||
}
|
||||
|
||||
function getGoPayPayNowTarget() {
|
||||
const button = findPayNowButton();
|
||||
return {
|
||||
found: Boolean(button),
|
||||
target: describeElement(button),
|
||||
rect: getElementClickRect(button),
|
||||
};
|
||||
}
|
||||
|
||||
async function clickGoPayContinue() {
|
||||
await waitForDocumentComplete();
|
||||
const clickResult = await clickContinueIfPresent({ afterMs: 1200 });
|
||||
return { clicked: Boolean(clickResult.clicked), clickTarget: clickResult.target || '' };
|
||||
}
|
||||
|
||||
async function clickGoPayPayNow() {
|
||||
await waitForDocumentComplete();
|
||||
const button = findPayNowButton();
|
||||
if (!button) {
|
||||
return { clicked: false, clickTarget: '' };
|
||||
}
|
||||
await humanClickElement(button, { afterMs: 1500 });
|
||||
return { clicked: true, clickTarget: describeElement(button) };
|
||||
}
|
||||
|
||||
function inspectGoPayState() {
|
||||
const bodyText = normalizeText(document.body?.innerText || document.body?.textContent || '');
|
||||
const phoneInput = findPhoneInput();
|
||||
const otpInput = findOtpInput();
|
||||
const pinInput = findPinInput();
|
||||
const payNowButton = findPayNowButton();
|
||||
const continueButton = findContinueButton();
|
||||
const terminalError = detectGoPayTerminalError(bodyText);
|
||||
const successTextMatched = /success|successful|completed|selesai|berhasil|approved|authorized|支付成功|绑定成功|已授权/i.test(bodyText);
|
||||
const completed = !phoneInput && !otpInput && !pinInput && successTextMatched;
|
||||
const selectedCountryCode = readSelectedCountryCodeText();
|
||||
return {
|
||||
url: location.href,
|
||||
readyState: document.readyState,
|
||||
selectedCountryCode,
|
||||
hasPhoneInput: Boolean(phoneInput),
|
||||
hasOtpInput: Boolean(otpInput),
|
||||
hasPinInput: Boolean(pinInput),
|
||||
hasPayNowButton: Boolean(payNowButton),
|
||||
hasContinueButton: Boolean(continueButton),
|
||||
hasTerminalError: Boolean(terminalError),
|
||||
terminalError,
|
||||
completed,
|
||||
textPreview: bodyText.slice(0, 500),
|
||||
inputHints: getVisibleTextInputs().map((input) => getActionText(input).slice(0, 120)).filter(Boolean).slice(0, 12),
|
||||
};
|
||||
}
|
||||
+211
-51
@@ -5,7 +5,7 @@ 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_BASE_PAYLOAD = {
|
||||
const PLUS_CHECKOUT_PAYLOAD_BASE = {
|
||||
entry_point: 'all_plans_pricing_modal',
|
||||
plan_name: 'chatgptplusplan',
|
||||
checkout_ui_mode: 'custom',
|
||||
@@ -33,6 +33,32 @@ const PLUS_CHECKOUT_CONFIGS = {
|
||||
},
|
||||
};
|
||||
const PAYPAL_DIAGNOSTIC_LOG_INTERVAL_MS = 5000;
|
||||
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',
|
||||
diagnosticLabel: 'PayPal',
|
||||
checkoutMerchantPath: 'openai_ie',
|
||||
billingDetails: {
|
||||
country: 'DE',
|
||||
currency: 'EUR',
|
||||
},
|
||||
patterns: [/paypal/i],
|
||||
},
|
||||
[PLUS_PAYMENT_METHOD_GOPAY]: {
|
||||
id: PLUS_PAYMENT_METHOD_GOPAY,
|
||||
label: 'GoPay',
|
||||
diagnosticLabel: 'GoPay',
|
||||
checkoutMerchantPath: 'openai_llc',
|
||||
billingDetails: {
|
||||
country: 'ID',
|
||||
currency: 'IDR',
|
||||
},
|
||||
patterns: [/gopay|go\s*pay/i],
|
||||
},
|
||||
};
|
||||
|
||||
if (document.documentElement.getAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL) !== '1') {
|
||||
document.documentElement.setAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL, '1');
|
||||
@@ -42,6 +68,7 @@ if (document.documentElement.getAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL) !== '
|
||||
message.type === 'CREATE_PLUS_CHECKOUT'
|
||||
|| message.type === 'FILL_PLUS_BILLING_AND_SUBMIT'
|
||||
|| message.type === 'PLUS_CHECKOUT_SELECT_PAYPAL'
|
||||
|| message.type === 'PLUS_CHECKOUT_SELECT_GOPAY'
|
||||
|| message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS'
|
||||
|| message.type === 'PLUS_CHECKOUT_FILL_ADDRESS_QUERY'
|
||||
|| message.type === 'PLUS_CHECKOUT_SELECT_ADDRESS_SUGGESTION'
|
||||
@@ -73,7 +100,9 @@ async function handlePlusCheckoutCommand(message) {
|
||||
case 'FILL_PLUS_BILLING_AND_SUBMIT':
|
||||
return fillPlusBillingAndSubmit(message.payload || {});
|
||||
case 'PLUS_CHECKOUT_SELECT_PAYPAL':
|
||||
return selectPlusPayPalPaymentMethod();
|
||||
return selectPlusPayPalPaymentMethod(message.payload || {});
|
||||
case 'PLUS_CHECKOUT_SELECT_GOPAY':
|
||||
return selectPlusGoPayPaymentMethod(message.payload || {});
|
||||
case 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS':
|
||||
return fillPlusBillingAddress(message.payload || {});
|
||||
case 'PLUS_CHECKOUT_FILL_ADDRESS_QUERY':
|
||||
@@ -291,9 +320,9 @@ function getVisibleControls(selector) {
|
||||
function findClickableByText(patterns) {
|
||||
const normalizedPatterns = (Array.isArray(patterns) ? patterns : [patterns])
|
||||
.filter(Boolean);
|
||||
const candidates = getVisibleControls('button, a, [role="button"], input[type="button"], input[type="submit"], [tabindex]');
|
||||
const candidates = getVisibleControls('button, a, [role="button"], [role="tab"], input[type="button"], input[type="submit"], [tabindex]');
|
||||
return candidates.find((el) => {
|
||||
const text = getActionText(el);
|
||||
const text = getCombinedSearchText(el);
|
||||
return normalizedPatterns.some((pattern) => pattern.test(text));
|
||||
}) || null;
|
||||
}
|
||||
@@ -367,7 +396,7 @@ function findInteractiveAncestor(el) {
|
||||
let current = el;
|
||||
for (let depth = 0; current && depth < 8; depth += 1, current = current.parentElement) {
|
||||
if (!isVisibleElement(current) || isDocumentLevelContainer(current)) continue;
|
||||
if (current.matches?.('button, a, label, [role="button"], [role="radio"], input[type="radio"], [tabindex]')) {
|
||||
if (current.matches?.('button, a, label, [role="button"], [role="radio"], [role="tab"], input[type="radio"], [tabindex]')) {
|
||||
return current;
|
||||
}
|
||||
}
|
||||
@@ -387,6 +416,16 @@ function findPaymentCardAncestor(el, pattern) {
|
||||
return null;
|
||||
}
|
||||
|
||||
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 getAncestorChainSummary(el, limit = 6) {
|
||||
const chain = [];
|
||||
let current = el;
|
||||
@@ -409,13 +448,15 @@ function getAncestorChainSummary(el, limit = 6) {
|
||||
return chain;
|
||||
}
|
||||
|
||||
function getPayPalSearchCandidates() {
|
||||
function getPaymentMethodSearchCandidates(method = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const config = getPaymentMethodConfig(method);
|
||||
const selector = [
|
||||
'button',
|
||||
'a',
|
||||
'label',
|
||||
'[role="button"]',
|
||||
'[role="radio"]',
|
||||
'[role="tab"]',
|
||||
'input[type="radio"]',
|
||||
'[tabindex]',
|
||||
'[data-testid]',
|
||||
@@ -428,7 +469,10 @@ function getPayPalSearchCandidates() {
|
||||
].join(', ');
|
||||
|
||||
return getVisibleControls(selector)
|
||||
.filter((el) => /paypal/i.test(getCombinedSearchText(el)))
|
||||
.filter((el) => {
|
||||
const text = getCombinedSearchText(el);
|
||||
return config.patterns.some((pattern) => pattern.test(text));
|
||||
})
|
||||
.sort((left, right) => {
|
||||
const leftRect = left.getBoundingClientRect();
|
||||
const rightRect = right.getBoundingClientRect();
|
||||
@@ -436,26 +480,36 @@ function getPayPalSearchCandidates() {
|
||||
});
|
||||
}
|
||||
|
||||
function findPayPalPaymentMethodTarget() {
|
||||
const paypalPattern = /paypal/i;
|
||||
const directClickable = findClickableByText([paypalPattern]);
|
||||
function getPayPalSearchCandidates() {
|
||||
return getPaymentMethodSearchCandidates(PLUS_PAYMENT_METHOD_PAYPAL);
|
||||
}
|
||||
|
||||
function getGoPaySearchCandidates() {
|
||||
return getPaymentMethodSearchCandidates(PLUS_PAYMENT_METHOD_GOPAY);
|
||||
}
|
||||
|
||||
function findPaymentMethodTarget(method = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const config = getPaymentMethodConfig(method);
|
||||
const directClickable = findClickableByText(config.patterns);
|
||||
if (directClickable) {
|
||||
return directClickable;
|
||||
}
|
||||
|
||||
const radios = getVisibleControls('input[type="radio"], [role="radio"]');
|
||||
const paypalRadio = radios.find((el) => paypalPattern.test(getCombinedSearchText(el)));
|
||||
if (paypalRadio) {
|
||||
return paypalRadio;
|
||||
const matchedRadio = radios.find((el) => config.patterns.some((pattern) => pattern.test(getCombinedSearchText(el))));
|
||||
if (matchedRadio) {
|
||||
return matchedRadio;
|
||||
}
|
||||
|
||||
const candidates = getPayPalSearchCandidates();
|
||||
const candidates = getPaymentMethodSearchCandidates(method);
|
||||
for (const candidate of candidates) {
|
||||
const interactive = findInteractiveAncestor(candidate);
|
||||
if (interactive && paypalPattern.test(getCombinedSearchText(interactive))) {
|
||||
if (interactive && config.patterns.some((pattern) => pattern.test(getCombinedSearchText(interactive)))) {
|
||||
return interactive;
|
||||
}
|
||||
const card = findPaymentCardAncestor(candidate, paypalPattern);
|
||||
const card = config.patterns
|
||||
.map((pattern) => findPaymentCardAncestor(candidate, pattern))
|
||||
.find(Boolean);
|
||||
if (card) {
|
||||
return card;
|
||||
}
|
||||
@@ -464,6 +518,14 @@ function findPayPalPaymentMethodTarget() {
|
||||
return null;
|
||||
}
|
||||
|
||||
function findPayPalPaymentMethodTarget() {
|
||||
return findPaymentMethodTarget(PLUS_PAYMENT_METHOD_PAYPAL);
|
||||
}
|
||||
|
||||
function findGoPayPaymentMethodTarget() {
|
||||
return findPaymentMethodTarget(PLUS_PAYMENT_METHOD_GOPAY);
|
||||
}
|
||||
|
||||
function summarizeElementForDebug(el) {
|
||||
if (!el) return null;
|
||||
const rect = el.getBoundingClientRect();
|
||||
@@ -476,16 +538,24 @@ function summarizeElementForDebug(el) {
|
||||
};
|
||||
}
|
||||
|
||||
function getPayPalCandidateSummaries(limit = 6) {
|
||||
return getPayPalSearchCandidates()
|
||||
function getPaymentMethodCandidateSummaries(method = PLUS_PAYMENT_METHOD_PAYPAL, limit = 6) {
|
||||
return getPaymentMethodSearchCandidates(method)
|
||||
.map(summarizeElementForDebug)
|
||||
.filter(Boolean)
|
||||
.slice(0, limit);
|
||||
}
|
||||
|
||||
function getPayPalCandidateSummaries(limit = 6) {
|
||||
return getPaymentMethodCandidateSummaries(PLUS_PAYMENT_METHOD_PAYPAL, limit);
|
||||
}
|
||||
|
||||
function getGoPayCandidateSummaries(limit = 6) {
|
||||
return getPaymentMethodCandidateSummaries(PLUS_PAYMENT_METHOD_GOPAY, limit);
|
||||
}
|
||||
|
||||
function getPaymentTextPreview(limit = 10) {
|
||||
const seen = new Set();
|
||||
const pattern = /paypal|card|payment|billing|subscribe|pay|银行卡|付款|支付|账单|订阅/i;
|
||||
const pattern = /gopay|go\s*pay|paypal|card|payment|billing|subscribe|pay|银行卡|付款|支付|账单|订阅/i;
|
||||
return getVisibleControls('button, a, label, [role="button"], [role="radio"], input[type="radio"], input[type="button"], input[type="submit"], [data-testid]')
|
||||
.map((el) => getCombinedSearchText(el))
|
||||
.filter((text) => text && pattern.test(text))
|
||||
@@ -499,26 +569,67 @@ function getPaymentTextPreview(limit = 10) {
|
||||
}
|
||||
|
||||
function getPayPalDiagnostics(reason = '') {
|
||||
return getPaymentMethodDiagnostics(PLUS_PAYMENT_METHOD_PAYPAL, reason);
|
||||
}
|
||||
|
||||
function getGoPayDiagnostics(reason = '') {
|
||||
return getPaymentMethodDiagnostics(PLUS_PAYMENT_METHOD_GOPAY, reason);
|
||||
}
|
||||
|
||||
function getPaymentMethodDiagnostics(method = PLUS_PAYMENT_METHOD_PAYPAL, reason = '') {
|
||||
const config = getPaymentMethodConfig(method);
|
||||
return {
|
||||
reason,
|
||||
url: location.href,
|
||||
readyState: document.readyState,
|
||||
paymentMethod: config.id,
|
||||
paymentMethodLabel: config.label,
|
||||
paymentCandidates: getPaymentMethodCandidateSummaries(config.id),
|
||||
paypalCandidates: getPayPalCandidateSummaries(),
|
||||
gopayCandidates: getGoPayCandidateSummaries(),
|
||||
paymentTextPreview: getPaymentTextPreview(),
|
||||
cardFieldsVisible: hasCreditCardFields(),
|
||||
billingFieldsVisible: hasBillingAddressFields(),
|
||||
};
|
||||
}
|
||||
|
||||
function writePayPalDiagnostics(reason, level = 'info') {
|
||||
const diagnostics = getPayPalDiagnostics(reason);
|
||||
function writePaymentMethodDiagnostics(method = PLUS_PAYMENT_METHOD_PAYPAL, reason = '', level = 'info') {
|
||||
const config = getPaymentMethodConfig(method);
|
||||
const diagnostics = getPaymentMethodDiagnostics(config.id, reason);
|
||||
const writer = typeof console[level] === 'function' ? console[level] : console.info;
|
||||
writer.call(console, '[MultiPage:plus-checkout] PayPal diagnostics', diagnostics);
|
||||
log(`Plus Checkout:${reason}。PayPal 候选 ${diagnostics.paypalCandidates.length} 个,银行卡字段${diagnostics.cardFieldsVisible ? '仍可见' : '不可见'}。`, level === 'error' ? 'error' : 'warn');
|
||||
writer.call(console, `[MultiPage:plus-checkout] ${config.diagnosticLabel} diagnostics`, diagnostics);
|
||||
log(`Plus Checkout:${reason}。${config.label} 候选 ${diagnostics.paymentCandidates.length} 个,银行卡字段${diagnostics.cardFieldsVisible ? '仍可见' : '不可见'}。`, level === 'error' ? 'error' : 'warn');
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
async function createPlusCheckoutSession(payload = {}) {
|
||||
function writePayPalDiagnostics(reason, level = 'info') {
|
||||
return writePaymentMethodDiagnostics(PLUS_PAYMENT_METHOD_PAYPAL, reason, level);
|
||||
}
|
||||
|
||||
function writeGoPayDiagnostics(reason, level = 'info') {
|
||||
return writePaymentMethodDiagnostics(PLUS_PAYMENT_METHOD_GOPAY, reason, level);
|
||||
}
|
||||
|
||||
function buildPlusCheckoutPayload(paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const config = getPaymentMethodConfig(paymentMethod);
|
||||
return {
|
||||
...JSON.parse(JSON.stringify(PLUS_CHECKOUT_PAYLOAD_BASE)),
|
||||
billing_details: {
|
||||
...config.billingDetails,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function buildPlusCheckoutUrl(checkoutSessionId, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const sessionId = String(checkoutSessionId || '').trim();
|
||||
if (!sessionId) {
|
||||
throw new Error('创建 Plus Checkout 失败:未返回 checkout_session_id。');
|
||||
}
|
||||
const config = getPaymentMethodConfig(paymentMethod);
|
||||
return `https://chatgpt.com/checkout/${config.checkoutMerchantPath}/${sessionId}`;
|
||||
}
|
||||
|
||||
async function createPlusCheckoutSession(options = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const checkoutConfig = buildPlusCheckoutConfig(payload);
|
||||
log('Plus:正在读取 ChatGPT 登录会话...');
|
||||
@@ -533,6 +644,8 @@ async function createPlusCheckoutSession(payload = {}) {
|
||||
}
|
||||
|
||||
log('Plus:正在创建 checkout 会话...');
|
||||
const paymentMethod = normalizePlusPaymentMethod(options.paymentMethod);
|
||||
const checkoutPayload = buildPlusCheckoutPayload(paymentMethod);
|
||||
const response = await fetch('https://chatgpt.com/backend-api/payments/checkout', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
@@ -540,7 +653,7 @@ async function createPlusCheckoutSession(payload = {}) {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(checkoutConfig.checkoutPayload),
|
||||
body: JSON.stringify(checkoutPayload),
|
||||
});
|
||||
|
||||
const data = await response.json().catch(() => ({}));
|
||||
@@ -550,17 +663,17 @@ async function createPlusCheckoutSession(payload = {}) {
|
||||
}
|
||||
|
||||
return {
|
||||
checkoutUrl: `${checkoutConfig.checkoutUrlPrefix}${data.checkout_session_id}`,
|
||||
country: checkoutConfig.checkoutPayload.billing_details.country,
|
||||
currency: checkoutConfig.checkoutPayload.billing_details.currency,
|
||||
paymentMethod: checkoutConfig.paymentMethod,
|
||||
checkoutUrl: buildPlusCheckoutUrl(data.checkout_session_id, paymentMethod),
|
||||
country: checkoutPayload.billing_details.country,
|
||||
currency: checkoutPayload.billing_details.currency,
|
||||
};
|
||||
}
|
||||
|
||||
async function selectPayPalPaymentMethod() {
|
||||
async function selectPaymentMethod(method = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const config = getPaymentMethodConfig(method);
|
||||
let lastDiagnosticsAt = 0;
|
||||
const target = await waitUntil(() => {
|
||||
const currentTarget = findPayPalPaymentMethodTarget();
|
||||
const currentTarget = findPaymentMethodTarget(config.id);
|
||||
if (currentTarget) {
|
||||
return currentTarget;
|
||||
}
|
||||
@@ -568,31 +681,49 @@ async function selectPayPalPaymentMethod() {
|
||||
const now = Date.now();
|
||||
if (!lastDiagnosticsAt || now - lastDiagnosticsAt >= PAYPAL_DIAGNOSTIC_LOG_INTERVAL_MS) {
|
||||
lastDiagnosticsAt = now;
|
||||
writePayPalDiagnostics('正在等待可点击的 PayPal 付款方式', 'warn');
|
||||
writePaymentMethodDiagnostics(config.id, `正在等待可点击的 ${config.label} 付款方式`, 'warn');
|
||||
}
|
||||
return null;
|
||||
}, {
|
||||
label: 'PayPal 付款方式',
|
||||
label: `${config.label} 付款方式`,
|
||||
intervalMs: 250,
|
||||
});
|
||||
console.info('[MultiPage:plus-checkout] PayPal target selected', summarizeElementForDebug(target));
|
||||
console.info(`[MultiPage:plus-checkout] ${config.label} target selected`, summarizeElementForDebug(target));
|
||||
simulateClick(target);
|
||||
log('Plus Checkout:已点击 PayPal 付款方式,正在确认选中状态。');
|
||||
log(`Plus Checkout:已点击 ${config.label} 付款方式,正在确认选中状态。`);
|
||||
|
||||
if (!await waitForPayPalPaymentMethodActive()) {
|
||||
const diagnostics = writePayPalDiagnostics('点击 PayPal 后页面仍未进入 PayPal 账单表单', 'error');
|
||||
throw new Error(`Plus Checkout:已尝试点击 PayPal,但页面未切换到 PayPal 表单。请提供控制台 PayPal diagnostics 结构。候选数量:${diagnostics.paypalCandidates.length},银行卡字段仍可见:${diagnostics.cardFieldsVisible ? '是' : '否'}。`);
|
||||
if (!await waitForPaymentMethodActive(config.id)) {
|
||||
const diagnostics = writePaymentMethodDiagnostics(config.id, `点击 ${config.label} 后页面仍未进入 ${config.label} 账单表单`, 'error');
|
||||
throw new Error(`Plus Checkout:已尝试点击 ${config.label},但页面未切换到 ${config.label} 表单。请提供控制台 ${config.label} diagnostics 结构。候选数量:${diagnostics.paymentCandidates.length},银行卡字段仍可见:${diagnostics.cardFieldsVisible ? '是' : '否'}。`);
|
||||
}
|
||||
|
||||
log('Plus Checkout:已确认 PayPal 付款方式生效。');
|
||||
log(`Plus Checkout:已确认 ${config.label} 付款方式生效。`);
|
||||
return true;
|
||||
}
|
||||
|
||||
async function selectPayPalPaymentMethod() {
|
||||
return selectPaymentMethod(PLUS_PAYMENT_METHOD_PAYPAL);
|
||||
}
|
||||
|
||||
async function selectGoPayPaymentMethod() {
|
||||
return selectPaymentMethod(PLUS_PAYMENT_METHOD_GOPAY);
|
||||
}
|
||||
|
||||
async function selectPlusPayPalPaymentMethod() {
|
||||
await waitForDocumentComplete();
|
||||
await selectPayPalPaymentMethod();
|
||||
await selectPaymentMethod(PLUS_PAYMENT_METHOD_PAYPAL);
|
||||
return {
|
||||
paymentSelected: true,
|
||||
paymentMethod: PLUS_PAYMENT_METHOD_PAYPAL,
|
||||
};
|
||||
}
|
||||
|
||||
async function selectPlusGoPayPaymentMethod() {
|
||||
await waitForDocumentComplete();
|
||||
await selectPaymentMethod(PLUS_PAYMENT_METHOD_GOPAY);
|
||||
return {
|
||||
paymentSelected: true,
|
||||
paymentMethod: PLUS_PAYMENT_METHOD_GOPAY,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -660,14 +791,14 @@ function hasBillingAddressFields() {
|
||||
});
|
||||
}
|
||||
|
||||
function hasSelectedPayPalControl() {
|
||||
const paypalPattern = /paypal/i;
|
||||
const candidates = getPayPalSearchCandidates();
|
||||
function hasSelectedPaymentMethodControl(method = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const config = getPaymentMethodConfig(method);
|
||||
const candidates = getPaymentMethodSearchCandidates(config.id);
|
||||
return candidates.some((candidate) => {
|
||||
let current = candidate;
|
||||
for (let depth = 0; current && depth < 6; depth += 1, current = current.parentElement) {
|
||||
if (isDocumentLevelContainer(current)) break;
|
||||
if (!paypalPattern.test(getCombinedSearchText(current))) continue;
|
||||
if (!config.patterns.some((pattern) => pattern.test(getCombinedSearchText(current)))) continue;
|
||||
const className = typeof current.className === 'string' ? current.className : current.getAttribute?.('class') || '';
|
||||
if (
|
||||
current.checked === true
|
||||
@@ -684,15 +815,31 @@ function hasSelectedPayPalControl() {
|
||||
});
|
||||
}
|
||||
|
||||
function isPayPalPaymentMethodActive() {
|
||||
return hasSelectedPayPalControl();
|
||||
function hasSelectedPayPalControl() {
|
||||
return hasSelectedPaymentMethodControl(PLUS_PAYMENT_METHOD_PAYPAL);
|
||||
}
|
||||
|
||||
async function waitForPayPalPaymentMethodActive(timeoutMs = 5000) {
|
||||
function hasSelectedGoPayControl() {
|
||||
return hasSelectedPaymentMethodControl(PLUS_PAYMENT_METHOD_GOPAY);
|
||||
}
|
||||
|
||||
function isPaymentMethodActive(method = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
return hasSelectedPaymentMethodControl(method);
|
||||
}
|
||||
|
||||
function isPayPalPaymentMethodActive() {
|
||||
return isPaymentMethodActive(PLUS_PAYMENT_METHOD_PAYPAL);
|
||||
}
|
||||
|
||||
function isGoPayPaymentMethodActive() {
|
||||
return isPaymentMethodActive(PLUS_PAYMENT_METHOD_GOPAY);
|
||||
}
|
||||
|
||||
async function waitForPaymentMethodActive(method = PLUS_PAYMENT_METHOD_PAYPAL, timeoutMs = 5000) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
throwIfStopped();
|
||||
if (isPayPalPaymentMethodActive()) {
|
||||
if (isPaymentMethodActive(method)) {
|
||||
return true;
|
||||
}
|
||||
await sleep(250);
|
||||
@@ -700,6 +847,10 @@ async function waitForPayPalPaymentMethodActive(timeoutMs = 5000) {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function waitForPayPalPaymentMethodActive(timeoutMs = 5000) {
|
||||
return waitForPaymentMethodActive(PLUS_PAYMENT_METHOD_PAYPAL, timeoutMs);
|
||||
}
|
||||
|
||||
async function findAddressSearchInput() {
|
||||
return waitUntil(() => {
|
||||
const direct = findInputByFieldText([
|
||||
@@ -820,6 +971,7 @@ function getCountryCandidates(value = '') {
|
||||
FR: ['France', '法国'],
|
||||
GB: ['United Kingdom', 'UK', 'Britain', 'England', '英国'],
|
||||
HK: ['Hong Kong', '香港'],
|
||||
ID: ['Indonesia', '印度尼西亚', '印尼'],
|
||||
IT: ['Italy', '意大利'],
|
||||
JP: ['Japan', '日本', '日本国'],
|
||||
KR: ['Korea', 'South Korea', '韩国'],
|
||||
@@ -834,6 +986,10 @@ function getCountryCandidates(value = '') {
|
||||
US: ['United States', 'United States of America', 'USA', '美国'],
|
||||
VN: ['Vietnam', '越南'],
|
||||
};
|
||||
const indonesiaCandidates = aliases.ID || [];
|
||||
if (compact === 'id' || compact === 'indonesia' || compact === '印度尼西亚' || compact === '印尼') {
|
||||
return Array.from(new Set([raw, 'ID', ...indonesiaCandidates].filter(Boolean)));
|
||||
}
|
||||
const direct = aliases[String(raw || '').trim().toUpperCase()] || [];
|
||||
const matched = Object.entries(aliases).find(([code, names]) => {
|
||||
if (String(code).toLowerCase() === compact) return true;
|
||||
@@ -1230,7 +1386,8 @@ async function humanLikeClick(el) {
|
||||
|
||||
async function fillPlusBillingAndSubmit(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
await selectPayPalPaymentMethod();
|
||||
const paymentMethod = normalizePlusPaymentMethod(payload.paymentMethod);
|
||||
await selectPaymentMethod(paymentMethod);
|
||||
const billingResult = await fillPlusBillingAddress(payload);
|
||||
|
||||
if (payload.skipSubmit) {
|
||||
@@ -1311,8 +1468,9 @@ async function ensurePlusStructuredBillingAddress(payload = {}) {
|
||||
}
|
||||
|
||||
async function clickPlusSubscribe(payload = {}) {
|
||||
if (payload.ensurePayPalActive && !isPayPalPaymentMethodActive()) {
|
||||
await selectPayPalPaymentMethod();
|
||||
const paymentMethod = normalizePlusPaymentMethod(payload.paymentMethod);
|
||||
if ((payload.ensurePayPalActive || payload.ensurePaymentActive) && !isPaymentMethodActive(paymentMethod)) {
|
||||
await selectPaymentMethod(paymentMethod);
|
||||
}
|
||||
|
||||
const subscribeButton = await waitUntil(() => {
|
||||
@@ -1338,7 +1496,9 @@ function inspectPlusCheckoutState() {
|
||||
readyState: document.readyState,
|
||||
countryText: readCountryText(),
|
||||
hasPayPal: Boolean(findPayPalPaymentMethodTarget()),
|
||||
hasGoPay: Boolean(findGoPayPaymentMethodTarget()),
|
||||
paypalCandidates: getPayPalCandidateSummaries(),
|
||||
gopayCandidates: getGoPayCandidateSummaries(),
|
||||
paymentTextPreview: getPaymentTextPreview(),
|
||||
cardFieldsVisible: hasCreditCardFields(),
|
||||
billingFieldsVisible: hasBillingAddressFields(),
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
// content/whatsapp-flow.js — WhatsApp Web code reader for GoPay.
|
||||
|
||||
console.log('[MultiPage:whatsapp-flow] Content script loaded on', location.href);
|
||||
|
||||
const WHATSAPP_FLOW_LISTENER_SENTINEL = 'data-multipage-whatsapp-flow-listener';
|
||||
|
||||
if (document.documentElement.getAttribute(WHATSAPP_FLOW_LISTENER_SENTINEL) !== '1') {
|
||||
document.documentElement.setAttribute(WHATSAPP_FLOW_LISTENER_SENTINEL, '1');
|
||||
|
||||
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (
|
||||
message.type === 'WHATSAPP_GET_STATE'
|
||||
|| message.type === 'WHATSAPP_FIND_CODE'
|
||||
) {
|
||||
resetStopState();
|
||||
handleWhatsAppCommand(message).then((result) => {
|
||||
sendResponse({ ok: true, ...(result || {}) });
|
||||
}).catch((err) => {
|
||||
if (isStopError(err)) {
|
||||
sendResponse({ stopped: true, error: err.message });
|
||||
return;
|
||||
}
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('[MultiPage:whatsapp-flow] 消息监听已存在,跳过重复注册');
|
||||
}
|
||||
|
||||
async function handleWhatsAppCommand(message) {
|
||||
switch (message.type) {
|
||||
case 'WHATSAPP_GET_STATE':
|
||||
return inspectWhatsAppState();
|
||||
case 'WHATSAPP_FIND_CODE':
|
||||
return findWhatsAppCode(message.payload || {});
|
||||
default:
|
||||
throw new Error(`whatsapp-flow.js 不处理消息:${message.type}`);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeText(text = '') {
|
||||
return String(text || '').replace(/\s+/g, ' ').trim();
|
||||
}
|
||||
|
||||
function getBodyText() {
|
||||
return normalizeText(document.body?.innerText || document.body?.textContent || '');
|
||||
}
|
||||
|
||||
function extractVerificationCode(text = '') {
|
||||
const normalized = normalizeText(text);
|
||||
const preferredPatterns = [
|
||||
/(?:gopay|gojek|openai|chatgpt|kode|code|otp|verification|verifikasi|whatsapp|验证码|驗證碼|代码|代碼)[^\d]{0,40}(\d{4,8})/i,
|
||||
/(\d{4,8})[^\d]{0,40}(?:gopay|gojek|openai|chatgpt|kode|code|otp|verification|verifikasi|验证码|驗證碼|代码|代碼)/i,
|
||||
];
|
||||
for (const pattern of preferredPatterns) {
|
||||
const match = normalized.match(pattern);
|
||||
if (match?.[1]) {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
const genericMatch = normalized.match(/(?<!\d)(\d{4,8})(?!\d)/);
|
||||
return genericMatch?.[1] || '';
|
||||
}
|
||||
|
||||
function getMessageCandidates() {
|
||||
const selectors = [
|
||||
'[data-testid*="msg" i]',
|
||||
'[data-pre-plain-text]',
|
||||
'.message-in',
|
||||
'[role="row"]',
|
||||
'div',
|
||||
'span',
|
||||
];
|
||||
const seen = new Set();
|
||||
const messages = [];
|
||||
for (const selector of selectors) {
|
||||
for (const el of Array.from(document.querySelectorAll(selector))) {
|
||||
const text = normalizeText(el.innerText || el.textContent || '');
|
||||
if (!text || text.length < 4 || seen.has(text)) continue;
|
||||
seen.add(text);
|
||||
messages.push(text);
|
||||
}
|
||||
}
|
||||
return messages.slice(-80);
|
||||
}
|
||||
|
||||
async function waitUntil(predicate, options = {}) {
|
||||
const intervalMs = Math.max(50, Math.floor(Number(options.intervalMs) || 1000));
|
||||
const timeoutMs = Math.max(0, Math.floor(Number(options.timeoutMs) || 0));
|
||||
const startedAt = Date.now();
|
||||
while (true) {
|
||||
throwIfStopped();
|
||||
const value = await predicate();
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
if (timeoutMs > 0 && Date.now() - startedAt >= timeoutMs) {
|
||||
return null;
|
||||
}
|
||||
await sleep(intervalMs);
|
||||
}
|
||||
}
|
||||
|
||||
async function findWhatsAppCode(payload = {}) {
|
||||
const timeoutMs = Math.max(0, Math.floor(Number(payload.timeoutMs) || 0));
|
||||
const result = await waitUntil(() => {
|
||||
const messages = getMessageCandidates();
|
||||
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
||||
const text = messages[index];
|
||||
const code = extractVerificationCode(text);
|
||||
if (code) {
|
||||
return {
|
||||
code,
|
||||
messageText: text.slice(0, 500),
|
||||
};
|
||||
}
|
||||
}
|
||||
const code = extractVerificationCode(getBodyText());
|
||||
return code ? { code, messageText: getBodyText().slice(0, 500) } : null;
|
||||
}, {
|
||||
intervalMs: 1000,
|
||||
timeoutMs,
|
||||
});
|
||||
|
||||
return result || {
|
||||
code: '',
|
||||
messageText: '',
|
||||
};
|
||||
}
|
||||
|
||||
function inspectWhatsAppState() {
|
||||
const bodyText = getBodyText();
|
||||
const code = extractVerificationCode(bodyText);
|
||||
return {
|
||||
url: location.href,
|
||||
readyState: document.readyState,
|
||||
loggedIn: !/use whatsapp on your computer|link a device|scan|qr|使用 WhatsApp|扫码|掃碼/i.test(bodyText),
|
||||
code,
|
||||
textPreview: bodyText.slice(0, 500),
|
||||
messagePreview: getMessageCandidates().slice(-8),
|
||||
};
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
AU: ['au', 'aus', 'australia', '澳大利亚'],
|
||||
DE: ['de', 'deu', 'germany', 'deutschland', '德国'],
|
||||
FR: ['fr', 'fra', 'france', '法国'],
|
||||
ID: ['id', 'indonesia', '印度尼西亚', '印尼'],
|
||||
JP: ['jp', 'jpn', 'japan', '日本', '日本国'],
|
||||
US: ['us', 'usa', 'united states', 'united states of america', 'america', '美国'],
|
||||
};
|
||||
@@ -76,6 +77,28 @@
|
||||
},
|
||||
},
|
||||
],
|
||||
ID: [
|
||||
{
|
||||
query: 'Jakarta Indonesia',
|
||||
suggestionIndex: 1,
|
||||
fallback: {
|
||||
address1: 'Jalan M.H. Thamrin No. 1',
|
||||
city: 'Jakarta',
|
||||
region: 'DKI Jakarta',
|
||||
postalCode: '10310',
|
||||
},
|
||||
},
|
||||
{
|
||||
query: 'Jakarta Selatan',
|
||||
suggestionIndex: 1,
|
||||
fallback: {
|
||||
address1: 'Jalan Jenderal Sudirman Kav. 52-53',
|
||||
city: 'Jakarta',
|
||||
region: 'DKI Jakarta',
|
||||
postalCode: '12190',
|
||||
},
|
||||
},
|
||||
],
|
||||
JP: [
|
||||
{
|
||||
query: 'Tokyo Marunouchi',
|
||||
|
||||
+22
-18
@@ -30,26 +30,23 @@
|
||||
{ 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: '平台回调验证' },
|
||||
];
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_STEP_KEY = 'paypal-approve';
|
||||
|
||||
function isPlusModeEnabled(options = {}) {
|
||||
return Boolean(options?.plusModeEnabled || options?.plusMode);
|
||||
}
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal';
|
||||
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY
|
||||
? PLUS_PAYMENT_METHOD_GOPAY
|
||||
: 'paypal';
|
||||
}
|
||||
|
||||
function getPlusPaymentStepTitle(options = {}) {
|
||||
return normalizePlusPaymentMethod(options?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY
|
||||
? 'GoPay 手机验证与授权'
|
||||
: 'PayPal 登录与授权';
|
||||
}
|
||||
|
||||
function getModeStepDefinitions(options = {}) {
|
||||
@@ -61,12 +58,18 @@
|
||||
: PLUS_PAYPAL_STEP_DEFINITIONS;
|
||||
}
|
||||
|
||||
function cloneSteps(steps = []) {
|
||||
return steps.map((step) => ({ ...step }));
|
||||
function cloneSteps(steps = [], options = {}) {
|
||||
const plusModeEnabled = isPlusModeEnabled(options);
|
||||
return steps.map((step) => ({
|
||||
...step,
|
||||
title: plusModeEnabled && step.key === PLUS_PAYMENT_STEP_KEY
|
||||
? getPlusPaymentStepTitle(options)
|
||||
: step.title,
|
||||
}));
|
||||
}
|
||||
|
||||
function getSteps(options = {}) {
|
||||
return cloneSteps(getModeStepDefinitions(options));
|
||||
return cloneSteps(getModeStepDefinitions(options), options);
|
||||
}
|
||||
|
||||
function getAllSteps() {
|
||||
@@ -101,7 +104,7 @@
|
||||
function getStepById(id, options = {}) {
|
||||
const numericId = Number(id);
|
||||
const match = getModeStepDefinitions(options).find((step) => step.id === numericId);
|
||||
return match ? { ...match } : null;
|
||||
return match ? cloneSteps([match], options)[0] : null;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -112,6 +115,7 @@
|
||||
PLUS_GOPAY_STEP_DEFINITIONS,
|
||||
getAllSteps,
|
||||
getLastStepId,
|
||||
getPlusPaymentStepTitle,
|
||||
getStepById,
|
||||
getStepIds,
|
||||
getSteps,
|
||||
|
||||
@@ -15,7 +15,8 @@
|
||||
5. SUB2API 多分组创建账号。
|
||||
6. OAuth 登录使用全新标签页。
|
||||
7. 接码平台多平台适配:HeroSMS + 5sim。
|
||||
8. 本地导出配置 `/config.json` 忽略规则。
|
||||
8. Plus 支付方式适配:PayPal + GoPay。
|
||||
9. 本地导出配置 `/config.json` 忽略规则。
|
||||
|
||||
推荐每次同步后执行:
|
||||
|
||||
@@ -225,7 +226,7 @@ node --test \
|
||||
|
||||
Plus checkout 页面加载后尽早判断是否有免费试用资格:
|
||||
|
||||
- 如果“今日应付金额”不是 0,直接跳过 PayPal 填写/提交。
|
||||
- 如果“今日应付金额”不是 0,直接跳过支付填写/提交。
|
||||
- 被跳过的账号也要标记为“已用”,避免下次重复拿到。
|
||||
- 提交前再检查一次金额,防止页面中途变化。
|
||||
|
||||
@@ -256,11 +257,39 @@ Plus checkout 页面加载后尽早判断是否有免费试用资格:
|
||||
|
||||
### 维护注意
|
||||
|
||||
1. 不要把免费资格判断放到地址/PayPal 填写之后。
|
||||
1. 不要把免费资格判断放到账单地址或支付方式填写之后。
|
||||
2. 不要把 `PLUS_CHECKOUT_NON_FREE_TRIAL::` 当作普通可重试错误。
|
||||
3. 标记账号已用时要读取 fresh state,避免 step 局部传入的 state 过期。
|
||||
|
||||
## 5. SUB2API 多分组创建账号
|
||||
## 5. Plus 支付方式适配:PayPal + GoPay
|
||||
|
||||
### 目标
|
||||
|
||||
Plus 模式新增 `plusPaymentMethod`:
|
||||
|
||||
- `paypal`:保留现有 PayPal 账号池、登录与授权。
|
||||
- `gopay`:创建 checkout 时使用印尼账单国家 `ID / IDR`,第 7 步选择 GoPay,第 8 步填写 GoPay 手机号;验证码优先使用侧边栏已填值,否则执行时弹出手动输入框;提交验证码后继续填写 GoPay PIN。
|
||||
|
||||
### 关键文件
|
||||
|
||||
| 文件 | 作用 |
|
||||
| --- | --- |
|
||||
| `sidepanel/sidepanel.html` | Plus 支付下拉、GoPay 手机号、GoPay 验证码、GoPay PIN 输入。 |
|
||||
| `sidepanel/sidepanel.js` | 保存 `plusPaymentMethod / gopayCountryCode / gopayPhone / gopayOtp / gopayPin`,按支付方式切换配置行,第 8 步标题按 PayPal / GoPay 动态匹配,并提供 GoPay 验证码手动输入弹窗。 |
|
||||
| `gopay-utils.js` | GoPay 支付方式、手机号、验证码、PIN 规范化工具。 |
|
||||
| `content/plus-checkout.js` | 创建 GoPay checkout payload,识别/选择 GoPay 付款方式。 |
|
||||
| `background/steps/fill-plus-checkout.js` | 第 7 步按支付方式选择 PayPal 或 GoPay,并为 GoPay 使用印尼地址。 |
|
||||
| `background/steps/gopay-approve.js` | 第 8 步 GoPay 手机号、手动验证码、PIN 自动化骨架。 |
|
||||
| `content/gopay-flow.js` | GoPay 页面手机号/验证码/PIN 输入脚本,包含国家区号切换、贴近人工点击的确认按钮事件序列,并在必要时交给后台 debugger 真实鼠标事件兜底。 |
|
||||
| `content/whatsapp-flow.js` | WhatsApp Web 验证码读取脚本。 |
|
||||
|
||||
### 维护注意
|
||||
|
||||
1. GoPay 验证码和 PIN 只能通过侧边栏输入或运行时弹窗临时填写,不要写入代码、测试或文档。
|
||||
2. PayPal 和 GoPay 共享 Plus 可见步骤,第 8 步仍使用 `paypal-approve` 这个 step key,但展示标题会按 `plusPaymentMethod` 变为 PayPal 或 GoPay 文案,后台也会按该字段分发到 PayPal 或 GoPay executor。
|
||||
3. GoPay 页面和 WhatsApp Web 的真实 DOM 可能变化;后续联调时优先在每个页面抓 `GOPAY_GET_STATE / WHATSAPP_GET_STATE` 输出,再补精确选择器。
|
||||
|
||||
## 6. SUB2API 多分组创建账号
|
||||
|
||||
### 目标
|
||||
|
||||
@@ -290,7 +319,7 @@ Plus checkout 页面加载后尽早判断是否有免费试用资格:
|
||||
|
||||
上游如果只按单个 `sub2apiGroupId` 写回,需要保留本地 `sub2apiGroupIds` 数组逻辑。
|
||||
|
||||
## 6. OAuth 登录使用全新标签页
|
||||
## 7. OAuth 登录使用全新标签页
|
||||
|
||||
### 目标
|
||||
|
||||
@@ -307,7 +336,7 @@ OAuth 登录流程打开新标签页,避免复用旧页面导致状态污染
|
||||
|
||||
上游如果改 OAuth tab 打开方式,确认仍保留“全新标签页”语义,不要退回到复用已有 tab。
|
||||
|
||||
## 7. 接码平台多平台适配:HeroSMS + 5sim
|
||||
## 8. 接码平台多平台适配:HeroSMS + 5sim
|
||||
|
||||
### 目标
|
||||
|
||||
@@ -371,7 +400,7 @@ node --check sidepanel/sidepanel.js
|
||||
node --test tests/five-sim-provider.test.js tests/phone-verification-flow.test.js tests/sidepanel-phone-verification-settings.test.js
|
||||
```
|
||||
|
||||
## 8. 本地配置文件忽略
|
||||
## 9. 本地配置文件忽略
|
||||
|
||||
### 目标
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
(function attachGoPayUtils(root, factory) {
|
||||
root.GoPayUtils = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createGoPayUtils() {
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY
|
||||
? PLUS_PAYMENT_METHOD_GOPAY
|
||||
: PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
}
|
||||
|
||||
const DEFAULT_GOPAY_COUNTRY_CODE = '+86';
|
||||
|
||||
function normalizeGoPayCountryCode(value = '') {
|
||||
const normalized = String(value || '').trim().replace(/[^\d+]/g, '');
|
||||
const digits = normalized.replace(/\D/g, '');
|
||||
return digits ? `+${digits}` : DEFAULT_GOPAY_COUNTRY_CODE;
|
||||
}
|
||||
|
||||
function normalizeGoPayPhone(value = '') {
|
||||
return String(value || '').trim().replace(/[^\d+]/g, '');
|
||||
}
|
||||
|
||||
function normalizeGoPayPhoneForCountry(value = '', countryCode = DEFAULT_GOPAY_COUNTRY_CODE) {
|
||||
const normalizedPhone = normalizeGoPayPhone(value);
|
||||
const normalizedCountryCode = normalizeGoPayCountryCode(countryCode);
|
||||
const countryDigits = normalizedCountryCode.replace(/\D/g, '');
|
||||
let nationalNumber = normalizedPhone.replace(/\D/g, '');
|
||||
|
||||
if (countryDigits && nationalNumber.startsWith(countryDigits)) {
|
||||
nationalNumber = nationalNumber.slice(countryDigits.length);
|
||||
}
|
||||
return nationalNumber;
|
||||
}
|
||||
|
||||
function normalizeGoPayPin(value = '') {
|
||||
return String(value || '').trim().replace(/[^\d]/g, '');
|
||||
}
|
||||
|
||||
function normalizeGoPayOtp(value = '') {
|
||||
return String(value || '').trim().replace(/[^\d]/g, '');
|
||||
}
|
||||
|
||||
return {
|
||||
DEFAULT_GOPAY_COUNTRY_CODE,
|
||||
PLUS_PAYMENT_METHOD_GOPAY,
|
||||
PLUS_PAYMENT_METHOD_PAYPAL,
|
||||
normalizeGoPayCountryCode,
|
||||
normalizeGoPayPhone,
|
||||
normalizeGoPayPhoneForCountry,
|
||||
normalizeGoPayOtp,
|
||||
normalizeGoPayPin,
|
||||
normalizePlusPaymentMethod,
|
||||
};
|
||||
});
|
||||
@@ -228,20 +228,25 @@
|
||||
<span class="data-label">Plus 模式</span>
|
||||
<div class="data-inline setting-pair">
|
||||
<div class="setting-group setting-group-primary">
|
||||
<label class="toggle-switch" for="input-plus-mode-enabled" title="开启后使用 Plus Checkout 订阅流程">
|
||||
<label class="toggle-switch" for="input-plus-mode-enabled" title="开启后使用 Plus Checkout 支付授权流程">
|
||||
<input type="checkbox" id="input-plus-mode-enabled" />
|
||||
<span class="toggle-switch-track" aria-hidden="true">
|
||||
<span class="toggle-switch-thumb"></span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="setting-group setting-group-secondary">
|
||||
<select id="select-plus-payment-method" class="data-select plus-payment-method-select" style="display:none;" aria-label="Plus 支付方式">
|
||||
<option value="paypal">PayPal 支付</option>
|
||||
<option value="gopay">GoPay 支付</option>
|
||||
</select>
|
||||
<span class="setting-caption">Plus 订阅链路</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-plus-payment-method" style="display:none;">
|
||||
<span class="data-label">Plus 支付</span>
|
||||
<div class="data-inline">
|
||||
<select id="select-plus-payment-method" class="data-select">
|
||||
<option value="paypal">PayPal</option>
|
||||
<option value="gopay">GoPay</option>
|
||||
</select>
|
||||
<span class="setting-caption" id="plus-payment-method-caption">PayPal 订阅链路</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-paypal-account" style="display:none;">
|
||||
<span class="data-label">PayPal 账号</span>
|
||||
@@ -252,6 +257,44 @@
|
||||
<button id="btn-add-paypal-account" class="btn btn-outline btn-sm data-inline-btn" type="button">添加</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row" id="row-gopay-country-code" style="display:none;">
|
||||
<span class="data-label">GoPay 区号</span>
|
||||
<select id="select-gopay-country-code" class="data-select">
|
||||
<option value="+86">中国 +86</option>
|
||||
<option value="+62">印度尼西亚 +62</option>
|
||||
<option value="+1">美国/加拿大 +1</option>
|
||||
<option value="+852">香港 +852</option>
|
||||
<option value="+853">澳门 +853</option>
|
||||
<option value="+886">台湾 +886</option>
|
||||
<option value="+60">马来西亚 +60</option>
|
||||
<option value="+65">新加坡 +65</option>
|
||||
<option value="+66">泰国 +66</option>
|
||||
<option value="+84">越南 +84</option>
|
||||
<option value="+63">菲律宾 +63</option>
|
||||
<option value="+91">印度 +91</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="data-row" id="row-gopay-phone" style="display:none;">
|
||||
<span class="data-label">GoPay 手机</span>
|
||||
<input type="text" id="input-gopay-phone" class="data-input"
|
||||
placeholder="请输入不含区号的 GoPay / WhatsApp 手机号" autocomplete="tel" />
|
||||
</div>
|
||||
<div class="data-row" id="row-gopay-otp" style="display:none;">
|
||||
<span class="data-label">GoPay 验证码</span>
|
||||
<input type="text" id="input-gopay-otp" class="data-input"
|
||||
placeholder="可选;留空时执行中会弹窗手动输入" inputmode="numeric" autocomplete="one-time-code" />
|
||||
</div>
|
||||
<div class="data-row" id="row-gopay-pin" style="display:none;">
|
||||
<span class="data-label">GoPay PIN</span>
|
||||
<div class="input-with-icon">
|
||||
<input type="password" id="input-gopay-pin" class="data-input data-input-with-icon"
|
||||
placeholder="请输入 GoPay PIN" autocomplete="off" />
|
||||
<button id="btn-toggle-gopay-pin" class="input-icon-btn" type="button"
|
||||
data-password-toggle="input-gopay-pin" data-show-label="显示 GoPay PIN"
|
||||
data-hide-label="隐藏 GoPay PIN" aria-label="显示 GoPay PIN"
|
||||
title="显示 GoPay PIN"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-row module-divider-start" id="row-mail-provider">
|
||||
<span class="data-label">邮箱服务</span>
|
||||
<div class="data-inline">
|
||||
@@ -1783,6 +1826,7 @@
|
||||
<script src="../managed-alias-utils.js"></script>
|
||||
<script src="../mail2925-utils.js"></script>
|
||||
<script src="../paypal-utils.js"></script>
|
||||
<script src="../gopay-utils.js"></script>
|
||||
<script src="../phone-sms/providers/hero-sms.js"></script>
|
||||
<script src="../phone-sms/providers/five-sim.js"></script>
|
||||
<script src="../phone-sms/providers/registry.js"></script>
|
||||
|
||||
+198
-25
@@ -162,10 +162,20 @@ 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 rowPlusPaymentMethod = document.getElementById('row-plus-payment-method');
|
||||
const selectPlusPaymentMethod = document.getElementById('select-plus-payment-method');
|
||||
const plusPaymentMethodCaption = document.getElementById('plus-payment-method-caption');
|
||||
const rowPayPalAccount = document.getElementById('row-paypal-account');
|
||||
const selectPayPalAccount = document.getElementById('select-paypal-account');
|
||||
const btnAddPayPalAccount = document.getElementById('btn-add-paypal-account');
|
||||
const rowGoPayCountryCode = document.getElementById('row-gopay-country-code');
|
||||
const selectGoPayCountryCode = document.getElementById('select-gopay-country-code');
|
||||
const rowGoPayPhone = document.getElementById('row-gopay-phone');
|
||||
const inputGoPayPhone = document.getElementById('input-gopay-phone');
|
||||
const rowGoPayOtp = document.getElementById('row-gopay-otp');
|
||||
const inputGoPayOtp = document.getElementById('input-gopay-otp');
|
||||
const rowGoPayPin = document.getElementById('row-gopay-pin');
|
||||
const inputGoPayPin = document.getElementById('input-gopay-pin');
|
||||
const selectMailProvider = document.getElementById('select-mail-provider');
|
||||
const btnMailLogin = document.getElementById('btn-mail-login');
|
||||
const rowCustomMailProviderPool = document.getElementById('row-custom-mail-provider-pool');
|
||||
@@ -634,21 +644,10 @@ 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 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') {
|
||||
function getStepDefinitionsForMode(plusModeEnabled = false, options = {}) {
|
||||
return (window.MultiPageStepDefinitions?.getSteps?.({
|
||||
plusModeEnabled,
|
||||
plusPaymentMethod: normalizePlusPaymentMethod(plusPaymentMethod),
|
||||
plusPaymentMethod: options.plusPaymentMethod || selectPlusPaymentMethod?.value || 'paypal',
|
||||
}) || [])
|
||||
.sort((left, right) => {
|
||||
const leftOrder = Number.isFinite(left.order) ? left.order : left.id;
|
||||
@@ -658,10 +657,9 @@ function getStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethod =
|
||||
});
|
||||
}
|
||||
|
||||
function rebuildStepDefinitionState(plusModeEnabled = false, plusPaymentMethod = 'paypal') {
|
||||
function rebuildStepDefinitionState(plusModeEnabled = false, options = {}) {
|
||||
currentPlusModeEnabled = Boolean(plusModeEnabled);
|
||||
currentPlusPaymentMethod = normalizePlusPaymentMethod(plusPaymentMethod);
|
||||
stepDefinitions = getStepDefinitionsForMode(currentPlusModeEnabled, currentPlusPaymentMethod);
|
||||
stepDefinitions = getStepDefinitionsForMode(currentPlusModeEnabled, options);
|
||||
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);
|
||||
@@ -693,6 +691,9 @@ const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_ID = 'england';
|
||||
const DEFAULT_FIVE_SIM_COUNTRY_LABEL = '英国 (England)';
|
||||
const DEFAULT_FIVE_SIM_OPERATOR = 'any';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
const DEFAULT_IP_PROXY_SERVICE = '711proxy';
|
||||
const SUPPORTED_IP_PROXY_SERVICES = ['711proxy', 'lumiproxy', 'iproyal', 'omegaproxy'];
|
||||
const IP_PROXY_ENABLED_SERVICES = ['711proxy'];
|
||||
@@ -1895,6 +1896,22 @@ function syncLatestState(nextState) {
|
||||
renderAccountRecords(latestState);
|
||||
}
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
if (window.GoPayUtils?.normalizePlusPaymentMethod) {
|
||||
return window.GoPayUtils.normalizePlusPaymentMethod(value);
|
||||
}
|
||||
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY
|
||||
? PLUS_PAYMENT_METHOD_GOPAY
|
||||
: PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
}
|
||||
|
||||
function getSelectedPlusPaymentMethod(state = latestState) {
|
||||
if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod?.value) {
|
||||
return normalizePlusPaymentMethod(selectPlusPaymentMethod.value);
|
||||
}
|
||||
return normalizePlusPaymentMethod(state?.plusPaymentMethod || DEFAULT_PLUS_PAYMENT_METHOD);
|
||||
}
|
||||
|
||||
function hasOwnStateValue(source, key) {
|
||||
return Object.prototype.hasOwnProperty.call(source, key);
|
||||
}
|
||||
@@ -2845,6 +2862,7 @@ function collectSettingsPayload() {
|
||||
const currentPayPalAccount = typeof getCurrentPayPalAccount === 'function'
|
||||
? getCurrentPayPalAccount(latestState)
|
||||
: payPalAccounts.find((account) => account?.id === String(latestState?.currentPayPalAccountId || '').trim()) || null;
|
||||
const plusPaymentMethod = getSelectedPlusPaymentMethod();
|
||||
return {
|
||||
...(contributionModeEnabled ? {} : {
|
||||
panelMode: selectPanelMode.value,
|
||||
@@ -2887,6 +2905,26 @@ function collectSettingsPayload() {
|
||||
paypalPassword: String(currentPayPalAccount?.password || latestState?.paypalPassword || ''),
|
||||
currentPayPalAccountId: String(latestState?.currentPayPalAccountId || '').trim(),
|
||||
paypalAccounts: payPalAccounts,
|
||||
gopayCountryCode: window.GoPayUtils?.normalizeGoPayCountryCode
|
||||
? window.GoPayUtils.normalizeGoPayCountryCode(typeof selectGoPayCountryCode !== 'undefined' && selectGoPayCountryCode ? selectGoPayCountryCode.value : latestState?.gopayCountryCode)
|
||||
: (typeof selectGoPayCountryCode !== 'undefined' && selectGoPayCountryCode
|
||||
? String(selectGoPayCountryCode.value || '+86').trim()
|
||||
: String(latestState?.gopayCountryCode || '+86').trim()),
|
||||
gopayPhone: window.GoPayUtils?.normalizeGoPayPhone
|
||||
? window.GoPayUtils.normalizeGoPayPhone(typeof inputGoPayPhone !== 'undefined' && inputGoPayPhone ? inputGoPayPhone.value : latestState?.gopayPhone)
|
||||
: (typeof inputGoPayPhone !== 'undefined' && inputGoPayPhone
|
||||
? String(inputGoPayPhone.value || '').trim()
|
||||
: String(latestState?.gopayPhone || '').trim()),
|
||||
gopayOtp: window.GoPayUtils?.normalizeGoPayOtp
|
||||
? window.GoPayUtils.normalizeGoPayOtp(typeof inputGoPayOtp !== 'undefined' && inputGoPayOtp ? inputGoPayOtp.value : latestState?.gopayOtp)
|
||||
: (typeof inputGoPayOtp !== 'undefined' && inputGoPayOtp
|
||||
? String(inputGoPayOtp.value || '').trim().replace(/[^\d]/g, '')
|
||||
: String(latestState?.gopayOtp || '').trim().replace(/[^\d]/g, '')),
|
||||
gopayPin: window.GoPayUtils?.normalizeGoPayPin
|
||||
? window.GoPayUtils.normalizeGoPayPin(typeof inputGoPayPin !== 'undefined' && inputGoPayPin ? inputGoPayPin.value : latestState?.gopayPin)
|
||||
: (typeof inputGoPayPin !== 'undefined' && inputGoPayPin
|
||||
? String(inputGoPayPin.value || '')
|
||||
: String(latestState?.gopayPin || '')),
|
||||
...(contributionModeEnabled ? {} : {
|
||||
customPassword: inputPassword.value,
|
||||
}),
|
||||
@@ -6014,18 +6052,41 @@ function updatePlusModeUI() {
|
||||
const enabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
|
||||
? Boolean(inputPlusModeEnabled.checked)
|
||||
: false;
|
||||
const paymentMethod = getSelectedPlusPaymentMethod();
|
||||
const method = enabled ? getSelectedPlusPaymentMethod() : DEFAULT_PLUS_PAYMENT_METHOD;
|
||||
if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) {
|
||||
selectPlusPaymentMethod.value = paymentMethod;
|
||||
selectPlusPaymentMethod.style.display = enabled ? '' : 'none';
|
||||
selectPlusPaymentMethod.value = method;
|
||||
}
|
||||
if (typeof plusPaymentMethodCaption !== 'undefined' && plusPaymentMethodCaption) {
|
||||
plusPaymentMethodCaption.textContent = method === PLUS_PAYMENT_METHOD_GOPAY
|
||||
? 'GoPay 印尼订阅链路'
|
||||
: 'PayPal 订阅链路';
|
||||
}
|
||||
[
|
||||
typeof rowPlusPaymentMethod !== 'undefined' ? rowPlusPaymentMethod : null,
|
||||
].forEach((row) => {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
row.style.display = enabled ? '' : 'none';
|
||||
});
|
||||
[
|
||||
typeof rowPayPalAccount !== 'undefined' ? rowPayPalAccount : null,
|
||||
].forEach((row) => {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
row.style.display = enabled && paymentMethod === 'paypal' ? '' : 'none';
|
||||
row.style.display = enabled && method === PLUS_PAYMENT_METHOD_PAYPAL ? '' : 'none';
|
||||
});
|
||||
[
|
||||
typeof rowGoPayCountryCode !== 'undefined' ? rowGoPayCountryCode : null,
|
||||
typeof rowGoPayPhone !== 'undefined' ? rowGoPayPhone : null,
|
||||
typeof rowGoPayOtp !== 'undefined' ? rowGoPayOtp : null,
|
||||
typeof rowGoPayPin !== 'undefined' ? rowGoPayPin : null,
|
||||
].forEach((row) => {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
row.style.display = enabled && method === PLUS_PAYMENT_METHOD_GOPAY ? '' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6281,15 +6342,19 @@ function renderStepsList() {
|
||||
|
||||
function syncStepDefinitionsForMode(plusModeEnabled = false, plusPaymentMethod = 'paypal', options = {}) {
|
||||
const nextPlusModeEnabled = Boolean(plusModeEnabled);
|
||||
const nextPlusPaymentMethod = normalizePlusPaymentMethod(plusPaymentMethod);
|
||||
const shouldRender = Boolean(options.render)
|
||||
|| nextPlusModeEnabled !== currentPlusModeEnabled
|
||||
|| nextPlusPaymentMethod !== currentPlusPaymentMethod;
|
||||
const nextPaymentMethod = normalizePlusPaymentMethod(options.plusPaymentMethod || getSelectedPlusPaymentMethod(latestState));
|
||||
const currentPaymentStep = stepDefinitions.find((step) => step.key === 'paypal-approve');
|
||||
const nextPaymentTitle = window.MultiPageStepDefinitions?.getPlusPaymentStepTitle?.({
|
||||
plusModeEnabled: nextPlusModeEnabled,
|
||||
plusPaymentMethod: nextPaymentMethod,
|
||||
});
|
||||
const paymentTitleChanged = Boolean(nextPlusModeEnabled && currentPaymentStep && nextPaymentTitle && currentPaymentStep.title !== nextPaymentTitle);
|
||||
const shouldRender = Boolean(options.render) || nextPlusModeEnabled !== currentPlusModeEnabled || paymentTitleChanged;
|
||||
if (!shouldRender) {
|
||||
return;
|
||||
}
|
||||
|
||||
rebuildStepDefinitionState(nextPlusModeEnabled, nextPlusPaymentMethod);
|
||||
rebuildStepDefinitionState(nextPlusModeEnabled, { plusPaymentMethod: nextPaymentMethod });
|
||||
renderStepsList();
|
||||
}
|
||||
|
||||
@@ -6359,6 +6424,29 @@ function applySettingsState(state) {
|
||||
if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) {
|
||||
selectPlusPaymentMethod.value = normalizePlusPaymentMethod(state?.plusPaymentMethod);
|
||||
}
|
||||
if (typeof selectGoPayCountryCode !== 'undefined' && selectGoPayCountryCode) {
|
||||
const normalizedGoPayCountryCode = window.GoPayUtils?.normalizeGoPayCountryCode
|
||||
? window.GoPayUtils.normalizeGoPayCountryCode(state?.gopayCountryCode)
|
||||
: String(state?.gopayCountryCode || '+86').trim();
|
||||
const hasOption = Array.from(selectGoPayCountryCode.options || [])
|
||||
.some((option) => option.value === normalizedGoPayCountryCode);
|
||||
if (!hasOption && normalizedGoPayCountryCode) {
|
||||
const option = document.createElement('option');
|
||||
option.value = normalizedGoPayCountryCode;
|
||||
option.textContent = `自定义 ${normalizedGoPayCountryCode}`;
|
||||
selectGoPayCountryCode.appendChild(option);
|
||||
}
|
||||
selectGoPayCountryCode.value = normalizedGoPayCountryCode || '+86';
|
||||
}
|
||||
if (typeof inputGoPayPhone !== 'undefined' && inputGoPayPhone) {
|
||||
inputGoPayPhone.value = state?.gopayPhone || '';
|
||||
}
|
||||
if (typeof inputGoPayOtp !== 'undefined' && inputGoPayOtp) {
|
||||
inputGoPayOtp.value = state?.gopayOtp || '';
|
||||
}
|
||||
if (typeof inputGoPayPin !== 'undefined' && inputGoPayPin) {
|
||||
inputGoPayPin.value = state?.gopayPin || '';
|
||||
}
|
||||
inputVpsUrl.value = state?.vpsUrl || '';
|
||||
inputVpsPassword.value = state?.vpsPassword || '';
|
||||
setLocalCpaStep9Mode(state?.localCpaStep9Mode);
|
||||
@@ -7293,6 +7381,56 @@ function getCustomVerificationPromptCopy(step) {
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeGoPayOtpInputValue(value = '') {
|
||||
return window.GoPayUtils?.normalizeGoPayOtp
|
||||
? window.GoPayUtils.normalizeGoPayOtp(value)
|
||||
: String(value || '').trim().replace(/[^\d]/g, '');
|
||||
}
|
||||
|
||||
async function openGoPayOtpInputDialog(payload = {}) {
|
||||
if (!sharedFormDialog?.open) {
|
||||
throw new Error('验证码输入弹窗未加载,请刷新扩展后重试。');
|
||||
}
|
||||
|
||||
const initialCode = normalizeGoPayOtpInputValue(payload.code || inputGoPayOtp?.value || latestState?.gopayOtp || '');
|
||||
const result = await sharedFormDialog.open({
|
||||
title: '输入 GoPay 验证码',
|
||||
message: '请把当前 GoPay 页面收到的验证码填到这里,确认后插件会继续填写验证码并进入 PIN 步骤。',
|
||||
confirmLabel: '提交验证码',
|
||||
confirmVariant: 'btn-primary',
|
||||
fields: [
|
||||
{
|
||||
key: 'code',
|
||||
label: '验证码',
|
||||
type: 'text',
|
||||
required: true,
|
||||
requiredMessage: '请输入 GoPay 验证码。',
|
||||
placeholder: '请输入数字验证码',
|
||||
inputMode: 'numeric',
|
||||
autocomplete: 'one-time-code',
|
||||
value: initialCode,
|
||||
validate: (value) => {
|
||||
const normalized = normalizeGoPayOtpInputValue(value);
|
||||
if (!normalized) return '请输入 GoPay 验证码。';
|
||||
if (normalized.length < 4) return 'GoPay 验证码长度过短,请检查。';
|
||||
return '';
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
const code = normalizeGoPayOtpInputValue(result?.code || '');
|
||||
if (!code) {
|
||||
return { cancelled: true, code: '' };
|
||||
}
|
||||
if (inputGoPayOtp) {
|
||||
inputGoPayOtp.value = code;
|
||||
}
|
||||
syncLatestState({ gopayOtp: code });
|
||||
markSettingsDirty(true);
|
||||
saveSettings({ silent: true }).catch(() => {});
|
||||
return { code };
|
||||
}
|
||||
|
||||
async function openCustomVerificationConfirmDialog(step) {
|
||||
const promptCopy = getCustomVerificationPromptCopy(step);
|
||||
if (step === 8 || step === 11) {
|
||||
@@ -9401,6 +9539,31 @@ selectPlusPaymentMethod?.addEventListener('change', () => {
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
selectPlusPaymentMethod?.addEventListener('change', () => {
|
||||
updatePlusModeUI();
|
||||
syncStepDefinitionsForMode(Boolean(inputPlusModeEnabled?.checked), {
|
||||
render: true,
|
||||
plusPaymentMethod: selectPlusPaymentMethod.value,
|
||||
});
|
||||
markSettingsDirty(true);
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
[
|
||||
selectGoPayCountryCode,
|
||||
inputGoPayPhone,
|
||||
inputGoPayOtp,
|
||||
inputGoPayPin,
|
||||
].forEach((input) => {
|
||||
input?.addEventListener('input', () => {
|
||||
markSettingsDirty(true);
|
||||
scheduleSettingsAutoSave();
|
||||
});
|
||||
input?.addEventListener('blur', () => {
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
});
|
||||
|
||||
selectMailProvider.addEventListener('change', async () => {
|
||||
const previousProvider = latestState?.mailProvider || '';
|
||||
const previousMail2925Mode = latestState?.mail2925Mode;
|
||||
@@ -10781,6 +10944,16 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'REQUEST_GOPAY_OTP_INPUT': {
|
||||
(async () => {
|
||||
const result = await openGoPayOtpInputDialog(message.payload || {});
|
||||
sendResponse(result || { cancelled: true, code: '' });
|
||||
})().catch((err) => {
|
||||
sendResponse({ error: err.message });
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'SECURITY_BLOCKED_ALERT': {
|
||||
openConfirmModal({
|
||||
title: message.payload?.title || '流程已完全停止',
|
||||
|
||||
@@ -9,6 +9,7 @@ test('address sources normalize supported countries and return local seeds', ()
|
||||
|
||||
assert.equal(api.normalizeCountryCode('Deutschland'), 'DE');
|
||||
assert.equal(api.normalizeCountryCode('澳大利亚'), 'AU');
|
||||
assert.equal(api.normalizeCountryCode('印尼'), 'ID');
|
||||
assert.equal(api.normalizeCountryCode('日本'), 'JP');
|
||||
assert.equal(api.normalizeCountryCode('unknown'), '');
|
||||
|
||||
@@ -22,6 +23,10 @@ test('address sources normalize supported countries and return local seeds', ()
|
||||
assert.equal(fallbackSeed.countryCode, 'AU');
|
||||
assert.equal(fallbackSeed.fallback.region, 'New South Wales');
|
||||
|
||||
const idSeed = api.getAddressSeedForCountry('Indonesia');
|
||||
assert.equal(idSeed.countryCode, 'ID');
|
||||
assert.equal(idSeed.fallback.region, 'DKI Jakarta');
|
||||
|
||||
const jpSeed = api.getAddressSeedForCountry('日本');
|
||||
assert.equal(jpSeed.countryCode, 'JP');
|
||||
assert.equal(jpSeed.fallback.region, 'Tokyo');
|
||||
|
||||
@@ -12,5 +12,13 @@ test('background imports step registry and shared step definitions', () => {
|
||||
assert.match(source, /background\/steps\/create-plus-checkout\.js/);
|
||||
assert.match(source, /background\/steps\/fill-plus-checkout\.js/);
|
||||
assert.match(source, /background\/steps\/paypal-approve\.js/);
|
||||
assert.match(source, /background\/steps\/gopay-approve\.js/);
|
||||
assert.match(source, /background\/steps\/plus-return-confirm\.js/);
|
||||
});
|
||||
|
||||
|
||||
test('GoPay approve executor receives debugger click and manual OTP helpers', () => {
|
||||
const source = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(source, /createGoPayApproveExecutor\(\{[\s\S]*clickWithDebugger[\s\S]*requestGoPayOtpInput[\s\S]*\}\)/);
|
||||
assert.match(source, /REQUEST_GOPAY_OTP_INPUT/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('background/steps/gopay-approve.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) throw new Error(`missing function ${name}`);
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let index = start; index < source.length; index += 1) {
|
||||
const ch = source[index];
|
||||
if (ch === '(') parenDepth += 1;
|
||||
if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) signatureEnded = true;
|
||||
}
|
||||
if (ch === '{' && signatureEnded) {
|
||||
braceStart = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const ch = source[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('GoPay OTP always requests manual confirmation even when a previous code exists', () => {
|
||||
const body = extractFunction('requestManualGoPayOtp');
|
||||
assert.doesNotMatch(body, /if\s*\(existingCode\)\s*\{\s*return existingCode;\s*\}/);
|
||||
assert.match(body, /requestGoPayOtpInput\(\{ code: existingCode \}\)/);
|
||||
assert.match(body, /检测到上次保存的 GoPay 验证码/);
|
||||
});
|
||||
|
||||
|
||||
test('GoPay approve handles final payment details iframe as an action frame', () => {
|
||||
assert.match(source, /GOPAY_PAYMENT_FRAME_URL_PATTERN/);
|
||||
assert.match(source, /payment\\\/details/);
|
||||
assert.match(source, /app\\\/challenge/);
|
||||
assert.match(source, /inspectGoPayFramesByDom/);
|
||||
assert.match(source, /getGoPayDomFramePriority/);
|
||||
assert.match(source, /paymentFrames/);
|
||||
assert.match(source, /frameState\?\.hasPayNowButton/);
|
||||
assert.match(source, /getGoPayDomFrameKind/);
|
||||
assert.match(source, /return 'payment'/);
|
||||
assert.match(source, /sendGoPayFrameCommand\(tabId, actionFrameId, 'GOPAY_CLICK_PAY_NOW'/);
|
||||
assert.match(source, /getGoPayDebuggerTargets/);
|
||||
assert.match(source, /chrome\.debugger\.getTargets/);
|
||||
assert.match(source, /targetId: picked\.targetId/);
|
||||
assert.match(source, /sendGoPayDebuggerTargetCommand\(actionTargetId, 'GOPAY_CLICK_PAY_NOW'/);
|
||||
assert.match(source, /sendGoPayDebuggerTargetCommand\(actionTargetId, 'GOPAY_SUBMIT_PIN'/);
|
||||
assert.match(source, /Input\.insertText/);
|
||||
assert.match(source, /最终 Bayar 确认/);
|
||||
});
|
||||
|
||||
test('GoPay approve treats merchant validate-pin iframe as PIN entry frame', () => {
|
||||
assert.match(source, /GOPAY_PIN_FRAME_URL_PATTERN/);
|
||||
assert.match(source, /payment\\\/validate-pin/);
|
||||
assert.match(source, /kind: 'pin'/);
|
||||
assert.match(source, /GOPAY_SUBMIT_PIN/);
|
||||
});
|
||||
|
||||
|
||||
test('GoPay approve closes terminal checkout but does not restart on top-level Pay now alone', () => {
|
||||
assert.match(source, /GOPAY_RESTART_FROM_STEP6::/);
|
||||
assert.match(source, /restartGoPayCheckoutFromStep6/);
|
||||
assert.match(source, /chrome\?\.tabs\?\.remove/);
|
||||
assert.match(source, /handleGoPayTerminalError\(pageState, tabId\)/);
|
||||
assert.match(source, /nextState\.hasTerminalError/);
|
||||
assert.doesNotMatch(source, /GoPay 顶层 Pay now 兜底点击后仍未进入下一步,当前支付会话需要重新创建/);
|
||||
});
|
||||
|
||||
test('GoPay approve falls back to clicking Bayar inside any iframe before top-level Pay now retry', () => {
|
||||
assert.match(source, /clickGoPayPayButtonInAnyFrame/);
|
||||
assert.match(source, /data-testid/);
|
||||
assert.match(source, /pay-button/);
|
||||
assert.match(source, /已在 GoPay iframe 中点击 Bayar 按钮/);
|
||||
assert.match(source, /不再自动回退步骤 6/);
|
||||
});
|
||||
|
||||
test('GoPay approve does not treat phone linking page as debugger iframe action', () => {
|
||||
assert.match(source, /type === 'tel'/);
|
||||
assert.match(source, /const hasContinueButton = !hasPayNowButton && !hasPhoneInput/);
|
||||
assert.match(source, /filter\(\(target\) => target\.type === 'iframe'\)/);
|
||||
});
|
||||
|
||||
|
||||
test('background auto-run routes GoPay restart sentinel back to step 6', () => {
|
||||
const backgroundSource = fs.readFileSync('background.js', 'utf8');
|
||||
assert.match(backgroundSource, /isGoPayCheckoutRestartRequiredFailure/);
|
||||
assert.match(backgroundSource, /GOPAY_RESTART_FROM_STEP6::/);
|
||||
assert.match(backgroundSource, /step === 8 && isGoPayCheckoutRestartRequiredFailure\(err\)/);
|
||||
assert.match(backgroundSource, /step = 6/);
|
||||
assert.match(backgroundSource, /invalidateDownstreamAfterStepRestart\(5/);
|
||||
});
|
||||
@@ -0,0 +1,341 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
const source = fs.readFileSync('content/gopay-flow.js', 'utf8');
|
||||
|
||||
function extractFunction(name) {
|
||||
const markers = [`async function ${name}(`, `function ${name}(`];
|
||||
const start = markers
|
||||
.map((marker) => source.indexOf(marker))
|
||||
.find((index) => index >= 0);
|
||||
if (start < 0) {
|
||||
throw new Error(`missing function ${name}`);
|
||||
}
|
||||
|
||||
let parenDepth = 0;
|
||||
let signatureEnded = false;
|
||||
let braceStart = -1;
|
||||
for (let index = start; index < source.length; index += 1) {
|
||||
const ch = source[index];
|
||||
if (ch === '(') {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ')') {
|
||||
parenDepth -= 1;
|
||||
if (parenDepth === 0) signatureEnded = true;
|
||||
} else if (ch === '{' && signatureEnded) {
|
||||
braceStart = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (braceStart < 0) {
|
||||
throw new Error(`missing body for ${name}`);
|
||||
}
|
||||
|
||||
let depth = 0;
|
||||
let end = braceStart;
|
||||
for (; end < source.length; end += 1) {
|
||||
const ch = source[end];
|
||||
if (ch === '{') depth += 1;
|
||||
if (ch === '}') {
|
||||
depth -= 1;
|
||||
if (depth === 0) {
|
||||
end += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return source.slice(start, end);
|
||||
}
|
||||
|
||||
test('GoPay human click helper dispatches pointer and mouse sequence before native click', async () => {
|
||||
const bundle = [
|
||||
extractFunction('dispatchPointerMouseSequence'),
|
||||
extractFunction('humanClickElement'),
|
||||
].join('\n');
|
||||
const events = [];
|
||||
const button = {
|
||||
tagName: 'BUTTON',
|
||||
scrollIntoView() { events.push('scroll'); },
|
||||
focus() { events.push('focus'); },
|
||||
click() { events.push('native-click'); },
|
||||
getBoundingClientRect() { return { left: 10, top: 20, width: 100, height: 40 }; },
|
||||
dispatchEvent(event) {
|
||||
events.push(event.type);
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
const api = new Function('button', 'events', `
|
||||
const window = { screenX: 0, screenY: 0 };
|
||||
class MouseEvent { constructor(type, init = {}) { this.type = type; this.init = init; } }
|
||||
class PointerEvent extends MouseEvent {}
|
||||
async function sleep() { events.push('sleep'); }
|
||||
${bundle}
|
||||
return { humanClickElement };
|
||||
`)(button, events);
|
||||
|
||||
await api.humanClickElement(button, { beforeMs: 1, afterDispatchMs: 1, afterMs: 1 });
|
||||
|
||||
assert.deepEqual(events.slice(0, 3), ['scroll', 'sleep', 'focus']);
|
||||
assert.ok(events.includes('pointerdown'));
|
||||
assert.ok(events.includes('mousedown'));
|
||||
assert.ok(events.includes('mouseup'));
|
||||
assert.ok(events.includes('click'));
|
||||
assert.equal(events.at(-2), 'native-click');
|
||||
});
|
||||
|
||||
|
||||
test('GoPay continue target exposes a debugger-clickable rect', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getActionText'),
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('isEnabledControl'),
|
||||
extractFunction('findClickableByText'),
|
||||
extractFunction('findContinueButton'),
|
||||
extractFunction('describeElement'),
|
||||
extractFunction('getElementClickRect'),
|
||||
extractFunction('getGoPayContinueTarget'),
|
||||
].join('\n');
|
||||
const button = {
|
||||
tagName: 'BUTTON',
|
||||
id: 'link-and-pay',
|
||||
className: 'btn primary',
|
||||
textContent: 'Link and pay',
|
||||
innerText: 'Link and pay',
|
||||
value: '',
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
parentElement: null,
|
||||
getAttribute(name) {
|
||||
if (name === 'class') return this.className;
|
||||
return '';
|
||||
},
|
||||
getBoundingClientRect() { return { left: 20, top: 30, width: 160, height: 44 }; },
|
||||
};
|
||||
const api = new Function('button', `
|
||||
const window = {
|
||||
getComputedStyle() { return { display: 'block', visibility: 'visible', opacity: '1' }; },
|
||||
innerWidth: 390,
|
||||
innerHeight: 844,
|
||||
};
|
||||
const document = {
|
||||
querySelectorAll(selector) {
|
||||
return selector.includes('button') || selector.includes('[role="button"]') ? [button] : [];
|
||||
},
|
||||
};
|
||||
${bundle}
|
||||
return { getGoPayContinueTarget };
|
||||
`)(button);
|
||||
|
||||
const target = api.getGoPayContinueTarget();
|
||||
assert.equal(target.found, true);
|
||||
assert.equal(target.rect.centerX, 100);
|
||||
assert.equal(target.rect.centerY, 52);
|
||||
assert.match(target.target, /Link and pay/);
|
||||
});
|
||||
|
||||
|
||||
test('GoPay PIN page detection wins over generic pin-input OTP attributes', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getActionText'),
|
||||
extractFunction('getPageBodyText'),
|
||||
extractFunction('isGoPayOtpPageText'),
|
||||
extractFunction('isGoPayPinPageText'),
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('isEnabledControl'),
|
||||
extractFunction('getVisibleTextInputs'),
|
||||
extractFunction('isCountrySearchInput'),
|
||||
extractFunction('getCombinedElementText'),
|
||||
extractFunction('findInputByPatterns'),
|
||||
extractFunction('findOtpInput'),
|
||||
extractFunction('getGoPayPinInputs'),
|
||||
extractFunction('findPinInput'),
|
||||
].join('\n');
|
||||
const pinInputs = Array.from({ length: 6 }, (_, index) => ({
|
||||
tagName: 'INPUT',
|
||||
type: 'text',
|
||||
id: '',
|
||||
className: 'pin-input password',
|
||||
textContent: '',
|
||||
value: '',
|
||||
placeholder: '○',
|
||||
maxLength: 1,
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
parentElement: null,
|
||||
getAttribute(name) {
|
||||
if (name === 'maxlength') return '1';
|
||||
if (name === 'data-testid') return `pin-input-${index}`;
|
||||
if (name === 'class') return this.className;
|
||||
if (name === 'placeholder') return this.placeholder;
|
||||
return '';
|
||||
},
|
||||
getBoundingClientRect() { return { width: 40, height: 40 }; },
|
||||
}));
|
||||
const api = new Function('pinInputs', `
|
||||
const location = { href: 'https://pin-web-client.gopayapi.com/auth/pin/verify' };
|
||||
const window = { getComputedStyle() { return { display: 'block', visibility: 'visible', opacity: '1' }; } };
|
||||
const document = {
|
||||
body: { innerText: 'Silakan ketik 6 digit PIN kamu buat lanjut. Lupa PIN', textContent: 'Silakan ketik 6 digit PIN kamu buat lanjut. Lupa PIN' },
|
||||
querySelectorAll(selector) { return selector.includes('input') ? pinInputs : []; },
|
||||
};
|
||||
${bundle}
|
||||
return { isGoPayOtpPageText, isGoPayPinPageText, findOtpInput, findPinInput, getGoPayPinInputs };
|
||||
`)(pinInputs);
|
||||
|
||||
assert.equal(api.isGoPayPinPageText(), true);
|
||||
assert.equal(api.isGoPayOtpPageText(), false);
|
||||
assert.equal(api.findOtpInput(), null);
|
||||
assert.equal(api.findPinInput(), pinInputs[0]);
|
||||
assert.equal(api.getGoPayPinInputs().length, 6);
|
||||
});
|
||||
|
||||
|
||||
test('GoPay Pay now button is detected separately from generic continue actions', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getActionText'),
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('isEnabledControl'),
|
||||
extractFunction('findClickableByText'),
|
||||
extractFunction('findPayNowButton'),
|
||||
extractFunction('describeElement'),
|
||||
extractFunction('getElementClickRect'),
|
||||
extractFunction('getGoPayPayNowTarget'),
|
||||
].join('\n');
|
||||
const payButton = {
|
||||
tagName: 'BUTTON',
|
||||
id: '',
|
||||
className: 'btn full primary btn-theme',
|
||||
textContent: 'Pay now',
|
||||
innerText: 'Pay now',
|
||||
value: '',
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
parentElement: null,
|
||||
getAttribute(name) { return name === 'class' ? this.className : ''; },
|
||||
getBoundingClientRect() { return { left: 411, top: 689, width: 388, height: 38 }; },
|
||||
};
|
||||
const refreshButton = {
|
||||
...payButton,
|
||||
className: 'refresh-button',
|
||||
textContent: 'Refresh',
|
||||
innerText: 'Refresh',
|
||||
getBoundingClientRect() { return { left: 705, top: 346, width: 90, height: 30 }; },
|
||||
};
|
||||
const api = new Function('payButton', 'refreshButton', `
|
||||
const window = {
|
||||
getComputedStyle() { return { display: 'block', visibility: 'visible', opacity: '1' }; },
|
||||
innerWidth: 1280,
|
||||
innerHeight: 800,
|
||||
};
|
||||
const document = {
|
||||
querySelectorAll(selector) {
|
||||
return selector.includes('button') || selector.includes('[role="button"]') ? [refreshButton, payButton] : [];
|
||||
},
|
||||
};
|
||||
${bundle}
|
||||
return { findPayNowButton, getGoPayPayNowTarget };
|
||||
`)(payButton, refreshButton);
|
||||
|
||||
assert.equal(api.findPayNowButton(), payButton);
|
||||
assert.equal(api.getGoPayPayNowTarget().found, true);
|
||||
assert.match(api.getGoPayPayNowTarget().target, /Pay now/);
|
||||
});
|
||||
|
||||
|
||||
test('GoPay final Bayar amount button is detected without matching terms link', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getActionText'),
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('isEnabledControl'),
|
||||
extractFunction('findClickableByText'),
|
||||
extractFunction('findPayNowButton'),
|
||||
].join('\n');
|
||||
const bayarButton = {
|
||||
tagName: 'BUTTON',
|
||||
className: 'bg-brand text-white',
|
||||
textContent: 'Bayar\nRp 1',
|
||||
innerText: 'Bayar\nRp 1',
|
||||
value: '',
|
||||
disabled: false,
|
||||
hidden: false,
|
||||
parentElement: null,
|
||||
getAttribute(name) { return name === 'class' ? this.className : ''; },
|
||||
getBoundingClientRect() { return { left: 16, top: 556, width: 388, height: 44 }; },
|
||||
};
|
||||
const termsLink = {
|
||||
...bayarButton,
|
||||
tagName: 'A',
|
||||
className: 'font-semibold text-brand cursor-pointer',
|
||||
textContent: 'Syarat & Ketentuan',
|
||||
innerText: 'Syarat & Ketentuan',
|
||||
getBoundingClientRect() { return { left: 224, top: 608, width: 104, height: 16 }; },
|
||||
};
|
||||
const api = new Function('bayarButton', 'termsLink', `
|
||||
const window = {
|
||||
getComputedStyle() { return { display: 'block', visibility: 'visible', opacity: '1' }; },
|
||||
};
|
||||
const document = {
|
||||
querySelectorAll(selector) {
|
||||
return selector.includes('button') || selector.includes('a') || selector.includes('[role="button"]') ? [termsLink, bayarButton] : [];
|
||||
},
|
||||
};
|
||||
${bundle}
|
||||
return { findPayNowButton };
|
||||
`)(bayarButton, termsLink);
|
||||
|
||||
assert.equal(api.findPayNowButton(), bayarButton);
|
||||
});
|
||||
|
||||
|
||||
test('GoPay terminal timeout page is reported as retry-required state', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getActionText'),
|
||||
extractFunction('getPageBodyText'),
|
||||
extractFunction('isGoPayPinPageText'),
|
||||
extractFunction('detectGoPayTerminalError'),
|
||||
extractFunction('isGoPayOtpPageText'),
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('isEnabledControl'),
|
||||
extractFunction('getVisibleTextInputs'),
|
||||
extractFunction('findInputByPatterns'),
|
||||
extractFunction('findPhoneInput'),
|
||||
extractFunction('isCountrySearchInput'),
|
||||
extractFunction('findOtpInput'),
|
||||
extractFunction('getCombinedElementText'),
|
||||
extractFunction('getGoPayPinInputs'),
|
||||
extractFunction('findPinInput'),
|
||||
extractFunction('findClickableByText'),
|
||||
extractFunction('findPayNowButton'),
|
||||
extractFunction('findContinueButton'),
|
||||
extractFunction('readSelectedCountryCodeText'),
|
||||
extractFunction('inspectGoPayState'),
|
||||
].join('\n');
|
||||
const api = new Function(`
|
||||
const location = { href: 'https://merchants-gws-app.gopayapi.com/app/challenge?reference=test' };
|
||||
const window = { getComputedStyle() { return { display: 'none', visibility: 'hidden', opacity: '0' }; } };
|
||||
const document = {
|
||||
body: { innerText: 'Yah, waktunya habis\\nKalau kamu mau coba lagi, tutup halaman ini dan ulangi prosesnya dari awal, ya.', textContent: '' },
|
||||
readyState: 'complete',
|
||||
querySelectorAll() { return []; },
|
||||
};
|
||||
${bundle}
|
||||
return { inspectGoPayState, detectGoPayTerminalError };
|
||||
`)();
|
||||
|
||||
const state = api.inspectGoPayState();
|
||||
assert.equal(state.hasTerminalError, true);
|
||||
assert.equal(state.terminalError.code, 'expired');
|
||||
assert.match(state.terminalError.message, /重新创建 Plus Checkout|超时/);
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
|
||||
function loadGoPayUtils() {
|
||||
const source = fs.readFileSync('gopay-utils.js', 'utf8');
|
||||
const globalScope = {};
|
||||
return new Function('self', `${source}; return self.GoPayUtils;`)(globalScope);
|
||||
}
|
||||
|
||||
test('GoPay utils normalize manual OTP input', () => {
|
||||
const api = loadGoPayUtils();
|
||||
assert.equal(api.normalizeGoPayOtp(' 12-34 56 '), '123456');
|
||||
assert.equal(api.normalizeGoPayOtp('abc'), '');
|
||||
});
|
||||
@@ -38,6 +38,121 @@ test('plus checkout content script can be injected repeatedly on the same page',
|
||||
assert.equal(context.__MULTIPAGE_PLUS_CHECKOUT_READY__, true);
|
||||
});
|
||||
|
||||
function createPlusCheckoutMessageHarness({ checkoutSessionId = 'cs_test_123' } = {}) {
|
||||
const attrs = new Map();
|
||||
let listener = null;
|
||||
const fetchCalls = [];
|
||||
const context = {
|
||||
console: { log() {}, warn() {}, error() {}, info() {} },
|
||||
location: { href: 'https://chatgpt.com/' },
|
||||
window: {},
|
||||
document: {
|
||||
readyState: 'complete',
|
||||
documentElement: {
|
||||
getAttribute(name) {
|
||||
return attrs.get(name) || null;
|
||||
},
|
||||
setAttribute(name, value) {
|
||||
attrs.set(name, String(value));
|
||||
},
|
||||
},
|
||||
},
|
||||
chrome: {
|
||||
runtime: {
|
||||
onMessage: {
|
||||
addListener(fn) {
|
||||
listener = fn;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
resetStopState() {},
|
||||
isStopError() { return false; },
|
||||
throwIfStopped() {},
|
||||
sleep() { return Promise.resolve(); },
|
||||
log() {},
|
||||
fetch: async (url, options = {}) => {
|
||||
fetchCalls.push({ url, options });
|
||||
if (url === '/api/auth/session') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ accessToken: 'test-access-token' }),
|
||||
};
|
||||
}
|
||||
if (url === 'https://chatgpt.com/backend-api/payments/checkout') {
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ checkout_session_id: checkoutSessionId }),
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected fetch url: ${url}`);
|
||||
},
|
||||
};
|
||||
context.window = context;
|
||||
vm.createContext(context);
|
||||
vm.runInContext(source, context);
|
||||
assert.equal(typeof listener, 'function');
|
||||
|
||||
async function send(message) {
|
||||
return await new Promise((resolve) => {
|
||||
listener(message, {}, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
return { send, fetchCalls };
|
||||
}
|
||||
|
||||
test('CREATE_PLUS_CHECKOUT keeps PayPal on DE/EUR and openai_ie merchant path by default', async () => {
|
||||
const harness = createPlusCheckoutMessageHarness({ checkoutSessionId: 'cs_paypal' });
|
||||
|
||||
const result = await harness.send({
|
||||
type: 'CREATE_PLUS_CHECKOUT',
|
||||
source: 'test',
|
||||
payload: {},
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.checkoutUrl, 'https://chatgpt.com/checkout/openai_ie/cs_paypal');
|
||||
assert.equal(result.country, 'DE');
|
||||
assert.equal(result.currency, 'EUR');
|
||||
|
||||
const checkoutCall = harness.fetchCalls.find((call) => call.url === 'https://chatgpt.com/backend-api/payments/checkout');
|
||||
assert.ok(checkoutCall);
|
||||
assert.equal(checkoutCall.options.method, 'POST');
|
||||
assert.equal(checkoutCall.options.headers.Authorization, 'Bearer test-access-token');
|
||||
const payload = JSON.parse(checkoutCall.options.body);
|
||||
assert.equal(payload.plan_name, 'chatgptplusplan');
|
||||
assert.deepEqual(payload.billing_details, { country: 'DE', currency: 'EUR' });
|
||||
});
|
||||
|
||||
test('CREATE_PLUS_CHECKOUT uses ID/IDR and openai_llc merchant path for GoPay', async () => {
|
||||
const harness = createPlusCheckoutMessageHarness({ checkoutSessionId: 'cs_gopay' });
|
||||
|
||||
const result = await harness.send({
|
||||
type: 'CREATE_PLUS_CHECKOUT',
|
||||
source: 'test',
|
||||
payload: { paymentMethod: 'gopay' },
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.checkoutUrl, 'https://chatgpt.com/checkout/openai_llc/cs_gopay');
|
||||
assert.equal(result.country, 'ID');
|
||||
assert.equal(result.currency, 'IDR');
|
||||
|
||||
const checkoutCall = harness.fetchCalls.find((call) => call.url === 'https://chatgpt.com/backend-api/payments/checkout');
|
||||
assert.ok(checkoutCall);
|
||||
const payload = JSON.parse(checkoutCall.options.body);
|
||||
assert.equal(payload.entry_point, 'all_plans_pricing_modal');
|
||||
assert.equal(payload.checkout_ui_mode, 'custom');
|
||||
assert.deepEqual(payload.billing_details, { country: 'ID', currency: 'IDR' });
|
||||
assert.deepEqual(payload.promo_campaign, {
|
||||
promo_campaign_id: 'plus-1-month-free',
|
||||
is_coupon_from_query_param: false,
|
||||
});
|
||||
});
|
||||
|
||||
function extractFunction(name) {
|
||||
const plainStart = source.indexOf(`function ${name}(`);
|
||||
const asyncStart = source.indexOf(`async function ${name}(`);
|
||||
@@ -251,6 +366,9 @@ test('getCheckoutAmountSummary accepts zero today due amount', () => {
|
||||
|
||||
test('isPayPalPaymentMethodActive requires a selected PayPal control', () => {
|
||||
const bundle = [
|
||||
"const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';",
|
||||
"const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';",
|
||||
"const PAYMENT_METHOD_CONFIGS = { paypal: { id: 'paypal', label: 'PayPal', patterns: [/paypal/i] }, gopay: { id: 'gopay', label: 'GoPay', patterns: [/gopay|go\\\\s*pay/i] } };",
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getActionText'),
|
||||
@@ -260,9 +378,14 @@ test('isPayPalPaymentMethodActive requires a selected PayPal control', () => {
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('getVisibleTextInputs'),
|
||||
extractFunction('isDocumentLevelContainer'),
|
||||
extractFunction('normalizePlusPaymentMethod'),
|
||||
extractFunction('getPaymentMethodConfig'),
|
||||
extractFunction('getPaymentMethodSearchCandidates'),
|
||||
extractFunction('getPayPalSearchCandidates'),
|
||||
extractFunction('hasCreditCardFields'),
|
||||
extractFunction('hasSelectedPaymentMethodControl'),
|
||||
extractFunction('hasSelectedPayPalControl'),
|
||||
extractFunction('isPaymentMethodActive'),
|
||||
extractFunction('isPayPalPaymentMethodActive'),
|
||||
].join('\n');
|
||||
|
||||
@@ -579,6 +702,78 @@ return { findCountryDropdown, findRegionDropdown, matchesCountryOption, matchesR
|
||||
assert.equal(api.matchesRegionOption('東京都', 'Tokyo'), true);
|
||||
});
|
||||
|
||||
test('payment method helpers can find and confirm selected GoPay controls', () => {
|
||||
const bundle = [
|
||||
"const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';",
|
||||
"const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';",
|
||||
"const PAYMENT_METHOD_CONFIGS = { paypal: { id: 'paypal', label: 'PayPal', patterns: [/paypal/i] }, gopay: { id: 'gopay', label: 'GoPay', patterns: [/gopay|go\\\\s*pay/i] } };",
|
||||
extractFunction('isVisibleElement'),
|
||||
extractFunction('normalizeText'),
|
||||
extractFunction('getActionText'),
|
||||
extractFunction('getSearchText'),
|
||||
extractFunction('getFieldText'),
|
||||
extractFunction('getCombinedSearchText'),
|
||||
extractFunction('getVisibleControls'),
|
||||
extractFunction('isEnabledControl'),
|
||||
extractFunction('isDocumentLevelContainer'),
|
||||
extractFunction('isPaymentCardSized'),
|
||||
extractFunction('findInteractiveAncestor'),
|
||||
extractFunction('findPaymentCardAncestor'),
|
||||
extractFunction('normalizePlusPaymentMethod'),
|
||||
extractFunction('getPaymentMethodConfig'),
|
||||
extractFunction('getPaymentMethodSearchCandidates'),
|
||||
extractFunction('getGoPaySearchCandidates'),
|
||||
extractFunction('findPaymentMethodTarget'),
|
||||
extractFunction('findGoPayPaymentMethodTarget'),
|
||||
extractFunction('hasSelectedPaymentMethodControl'),
|
||||
extractFunction('hasSelectedGoPayControl'),
|
||||
extractFunction('isPaymentMethodActive'),
|
||||
extractFunction('isGoPayPaymentMethodActive'),
|
||||
].join('\n');
|
||||
|
||||
const gopayButton = createElement({
|
||||
text: 'GoPay',
|
||||
attrs: {
|
||||
id: 'gopay-tab',
|
||||
role: 'tab',
|
||||
'data-testid': 'gopay',
|
||||
'aria-selected': 'true',
|
||||
value: 'gopay',
|
||||
},
|
||||
});
|
||||
const elements = [gopayButton];
|
||||
const documentMock = {
|
||||
documentElement: {},
|
||||
body: {},
|
||||
querySelectorAll: (selector) => {
|
||||
if (String(selector || '').includes('label[for=')) return [];
|
||||
return elements;
|
||||
},
|
||||
};
|
||||
const windowMock = {
|
||||
innerWidth: 1200,
|
||||
innerHeight: 900,
|
||||
getComputedStyle: () => ({ display: 'block', visibility: 'visible' }),
|
||||
};
|
||||
const cssMock = {
|
||||
escape: (value) => String(value),
|
||||
};
|
||||
|
||||
const api = new Function('window', 'document', 'CSS', `
|
||||
function findClickableByText(patterns) {
|
||||
return elements.find((el) => patterns.some((pattern) => pattern.test(getCombinedSearchText(el)))) || null;
|
||||
}
|
||||
const elements = document.querySelectorAll('*');
|
||||
${bundle}
|
||||
return { findGoPayPaymentMethodTarget, getGoPaySearchCandidates, hasSelectedGoPayControl, isGoPayPaymentMethodActive };
|
||||
`)(windowMock, documentMock, cssMock);
|
||||
|
||||
assert.equal(api.findGoPayPaymentMethodTarget(), gopayButton);
|
||||
assert.equal(api.getGoPaySearchCandidates()[0], gopayButton);
|
||||
assert.equal(api.hasSelectedGoPayControl(), true);
|
||||
assert.equal(api.isGoPayPaymentMethodActive(), true);
|
||||
});
|
||||
|
||||
test('fillIfEmpty can overwrite invalid structured address values in the dropdown branch', () => {
|
||||
const bundle = [
|
||||
extractFunction('fillIfEmpty'),
|
||||
|
||||
@@ -36,6 +36,20 @@ function createAuAddressSeed() {
|
||||
};
|
||||
}
|
||||
|
||||
function createIdAddressSeed() {
|
||||
return {
|
||||
countryCode: 'ID',
|
||||
query: 'Jakarta Indonesia',
|
||||
suggestionIndex: 1,
|
||||
fallback: {
|
||||
address1: 'Jalan M.H. Thamrin No. 1',
|
||||
city: 'Jakarta',
|
||||
region: 'DKI Jakarta',
|
||||
postalCode: '10310',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createSuccessfulBillingResult() {
|
||||
return {
|
||||
countryText: 'Germany',
|
||||
@@ -54,6 +68,7 @@ function createExecutorHarness({
|
||||
fetchImpl = null,
|
||||
getAddressSeedForCountry = () => createAddressSeed(),
|
||||
markCurrentRegistrationAccountUsed = async () => {},
|
||||
submitRedirectUrl = 'https://www.paypal.com/checkoutnow',
|
||||
}) {
|
||||
const api = loadPlusCheckoutBillingModule();
|
||||
const events = {
|
||||
@@ -102,7 +117,7 @@ function createExecutorHarness({
|
||||
return stateByFrame[frameId] || { hasPayPal: false, paypalCandidates: [] };
|
||||
}
|
||||
if (message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE') {
|
||||
checkoutTab.url = 'https://www.paypal.com/checkoutnow';
|
||||
checkoutTab.url = submitRedirectUrl;
|
||||
}
|
||||
return createSuccessfulBillingResult();
|
||||
},
|
||||
@@ -131,8 +146,8 @@ function createExecutorHarness({
|
||||
waitForTabCompleteUntilStopped: async () => checkoutTab,
|
||||
waitForTabUrlMatchUntilStopped: async (tabId, matcher) => {
|
||||
events.waitedUrls.push({ tabId });
|
||||
assert.equal(matcher('https://www.paypal.com/checkoutnow'), true);
|
||||
return { id: tabId, url: 'https://www.paypal.com/checkoutnow' };
|
||||
assert.equal(matcher(submitRedirectUrl), true);
|
||||
return { id: tabId, url: submitRedirectUrl };
|
||||
},
|
||||
});
|
||||
|
||||
@@ -225,6 +240,106 @@ test('Plus checkout billing sends the billing command to the iframe that contain
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
});
|
||||
|
||||
test('Plus checkout billing forces Indonesia address for GoPay even when page country differs', async () => {
|
||||
const requestedCountries = [];
|
||||
const fetchRequests = [];
|
||||
const { events, executor } = createExecutorHarness({
|
||||
frames: [
|
||||
{ frameId: 0, url: 'https://chatgpt.com/checkout/openai_llc/cs_test' },
|
||||
{ frameId: 7, url: 'https://js.stripe.com/v3/elements-inner-payment.html' },
|
||||
{ frameId: 8, url: 'https://js.stripe.com/v3/elements-inner-address.html' },
|
||||
],
|
||||
stateByFrame: {
|
||||
0: { hasPayPal: false, hasGoPay: false, paypalCandidates: [], gopayCandidates: [], hasSubscribeButton: true },
|
||||
7: { hasPayPal: false, hasGoPay: true, gopayCandidates: [{ tag: 'button', text: 'GoPay' }] },
|
||||
8: {
|
||||
hasPayPal: false,
|
||||
hasGoPay: false,
|
||||
paypalCandidates: [],
|
||||
gopayCandidates: [],
|
||||
billingFieldsVisible: true,
|
||||
countryText: 'United States',
|
||||
},
|
||||
},
|
||||
getAddressSeedForCountry: (countryValue) => {
|
||||
requestedCountries.push(countryValue);
|
||||
return countryValue === 'ID' ? createIdAddressSeed() : createAddressSeed();
|
||||
},
|
||||
fetchImpl: async (url, init) => {
|
||||
fetchRequests.push({ url, init });
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
status: 'ok',
|
||||
address: {
|
||||
Address: 'Jl. M.H. Thamrin No. 10',
|
||||
City: 'Jakarta',
|
||||
State: 'DKI Jakarta',
|
||||
Zip_Code: '10310',
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
submitRedirectUrl: 'https://app.midtrans.com/snap/v4/redirection/session#/gopay-tokenization/linking',
|
||||
});
|
||||
|
||||
await executor.executePlusCheckoutBilling({ plusPaymentMethod: 'gopay', plusCheckoutCountry: 'US' });
|
||||
|
||||
const fillMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS');
|
||||
assert.equal(requestedCountries[0], 'ID');
|
||||
assert.equal(fillMessage.message.payload.addressSeed.countryCode, 'ID');
|
||||
assert.equal(fillMessage.message.payload.addressSeed.source, 'meiguodizhi');
|
||||
assert.deepEqual(JSON.parse(fetchRequests[0].init.body), {
|
||||
city: 'Jakarta',
|
||||
path: '/id-address',
|
||||
method: 'refresh',
|
||||
});
|
||||
});
|
||||
|
||||
test('Plus checkout billing selects GoPay and waits for a GoPay redirect', async () => {
|
||||
const { checkoutTab, events, executor } = createExecutorHarness({
|
||||
frames: [
|
||||
{ frameId: 0, url: 'https://chatgpt.com/checkout/openai_ie/cs_test' },
|
||||
{ frameId: 7, url: 'https://js.stripe.com/v3/elements-inner-payment.html' },
|
||||
{ frameId: 8, url: 'https://js.stripe.com/v3/elements-inner-address.html' },
|
||||
],
|
||||
stateByFrame: {
|
||||
0: { hasPayPal: false, hasGoPay: false, paypalCandidates: [], gopayCandidates: [], hasSubscribeButton: true },
|
||||
7: { hasPayPal: false, hasGoPay: true, gopayCandidates: [{ tag: 'button', text: 'GoPay' }] },
|
||||
8: {
|
||||
hasPayPal: false,
|
||||
hasGoPay: false,
|
||||
paypalCandidates: [],
|
||||
gopayCandidates: [],
|
||||
billingFieldsVisible: true,
|
||||
countryText: 'Indonesia',
|
||||
},
|
||||
},
|
||||
getAddressSeedForCountry: () => createIdAddressSeed(),
|
||||
fetchImpl: async () => ({
|
||||
ok: false,
|
||||
status: 404,
|
||||
json: async () => ({ status: 'error' }),
|
||||
}),
|
||||
submitRedirectUrl: 'https://gopay.co.id/payment/session',
|
||||
});
|
||||
|
||||
await executor.executePlusCheckoutBilling({ plusPaymentMethod: 'gopay' });
|
||||
|
||||
const selectMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_SELECT_GOPAY');
|
||||
const paypalSelectMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_SELECT_PAYPAL');
|
||||
const fillMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_FILL_BILLING_ADDRESS');
|
||||
const subscribeMessage = events.messages.find((entry) => entry.message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE');
|
||||
assert.equal(selectMessage.frameId, 7);
|
||||
assert.equal(selectMessage.message.payload.paymentMethod, 'gopay');
|
||||
assert.equal(paypalSelectMessage, undefined);
|
||||
assert.equal(fillMessage.message.payload.addressSeed.countryCode, 'ID');
|
||||
assert.equal(subscribeMessage.message.payload.paymentMethod, 'gopay');
|
||||
assert.equal(checkoutTab.url, 'https://gopay.co.id/payment/session');
|
||||
assert.equal(events.completed[0].step, 7);
|
||||
});
|
||||
|
||||
test('Plus checkout billing still inspects a frame when ping readiness is stale', async () => {
|
||||
const { events, executor } = createExecutorHarness({
|
||||
frames: [
|
||||
|
||||
@@ -15,12 +15,20 @@ test('sidepanel loads reusable form dialog and paypal manager before sidepanel b
|
||||
assert.ok(managerIndex < sidepanelIndex);
|
||||
});
|
||||
|
||||
test('sidepanel html contains paypal select and add button controls', () => {
|
||||
test('sidepanel html contains paypal select and GoPay controls', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
|
||||
assert.match(html, /id="row-plus-payment-method"/);
|
||||
assert.match(html, /id="select-plus-payment-method"/);
|
||||
assert.match(html, /id="row-paypal-account"/);
|
||||
assert.match(html, /id="select-paypal-account"/);
|
||||
assert.match(html, /id="btn-add-paypal-account"/);
|
||||
assert.match(html, /id="row-gopay-phone"/);
|
||||
assert.match(html, /id="input-gopay-phone"/);
|
||||
assert.match(html, /id="row-gopay-otp"/);
|
||||
assert.match(html, /id="input-gopay-otp"/);
|
||||
assert.match(html, /id="row-gopay-pin"/);
|
||||
assert.match(html, /id="input-gopay-pin"/);
|
||||
assert.match(html, /id="shared-form-modal"/);
|
||||
});
|
||||
|
||||
|
||||
@@ -55,6 +55,10 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
);
|
||||
assert.equal(plusSteps.some((step) => step.key === 'clear-login-cookies'), false);
|
||||
assert.equal(plusSteps.some((step) => step.key === 'fetch-login-code'), true);
|
||||
assert.equal(plusSteps.find((step) => step.key === 'paypal-approve')?.title, 'PayPal 登录与授权');
|
||||
const goPaySteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gopay' });
|
||||
assert.equal(goPaySteps.find((step) => step.key === 'paypal-approve')?.title, 'GoPay 手机验证与授权');
|
||||
assert.equal(api.getStepById(8, { plusModeEnabled: true, plusPaymentMethod: 'gopay' })?.title, 'GoPay 手机验证与授权');
|
||||
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');
|
||||
@@ -92,13 +96,14 @@ test('sidepanel html loads shared step definitions before sidepanel bootstrap',
|
||||
assert.ok(definitionsIndex < sidepanelIndex);
|
||||
});
|
||||
|
||||
test('sidepanel html exposes Plus mode payment controls and PayPal settings', () => {
|
||||
test('sidepanel html exposes Plus mode, PayPal, and GoPay settings', () => {
|
||||
const html = fs.readFileSync('sidepanel/sidepanel.html', 'utf8');
|
||||
assert.match(html, /id="input-plus-mode-enabled"/);
|
||||
assert.match(html, /id="select-plus-payment-method"/);
|
||||
assert.match(html, /<option value="paypal">PayPal 支付<\/option>/);
|
||||
assert.match(html, /<option value="gopay">GoPay 支付<\/option>/);
|
||||
assert.match(html, /id="select-paypal-account"/);
|
||||
assert.match(html, /id="btn-add-paypal-account"/);
|
||||
assert.match(html, /id="input-gopay-phone"/);
|
||||
assert.match(html, /id="input-gopay-otp"/);
|
||||
assert.match(html, /id="input-gopay-pin"/);
|
||||
assert.match(html, /id="shared-form-modal"/);
|
||||
});
|
||||
|
||||
+6
-5
@@ -44,7 +44,7 @@
|
||||
- 在日志区通过“记录”按钮打开独立的邮箱记录覆盖层,并展示成功/失败/停止/重试统计与分页列表
|
||||
- 查询 GitHub Releases 并展示更新卡片;当前更新服务会区分 `Ultra`、历史 `Pro` 与 legacy `v` 三个版本族,排序时固定以 `Ultra` 为最高正式系列,同时会在读取缓存后重新排序,避免历史 `Pro` 或 `v` 版本误显示为比 `Ultra` 更新
|
||||
- 展示一个单独的“接码”开关与“接码平台”下拉;接码平台当前支持 HeroSMS / 5sim,开启后才展示所选平台的国家、API Key、价格上限等设置,用于 OAuth 登录链路命中手机号验证页时直接续跑手机验证
|
||||
- 展示 `Plus 模式` 开关与 PayPal 账号池配置;开启后 PayPal 配置行会切换为“账号下拉框 + 添加按钮”,添加按钮复用公共表单弹窗录入账号和密码;步骤列表切换为 Plus 模式 13 步定义,普通模式的 Cookie 清理步骤不再显示或执行,登录验证码步骤会移动到 Plus 可见第 11 步
|
||||
- 展示 `Plus 模式` 开关与 Plus 支付方式配置;支付方式支持 PayPal / GoPay,PayPal 展示账号池下拉与添加按钮,GoPay 展示手机号和 PIN;步骤列表切换为 Plus 模式 13 步定义,普通模式的 Cookie 清理步骤不再显示或执行,登录验证码步骤会移动到 Plus 可见第 11 步
|
||||
- 为 Hotmail / 2925 账号池复用同一套“添加账号 / 取消添加 / 批量导入 / 收起列表”表单交互;共享的显隐控制放在 `sidepanel/account-pool-ui.js`,各自 manager 只保留 provider 相关字段校验与业务操作
|
||||
|
||||
### 2.2 Background Service Worker
|
||||
@@ -160,6 +160,7 @@
|
||||
- Codex2API 配置
|
||||
- IP 代理持久配置:`ipProxyEnabled`、服务商、模式、API 地址、服务商配置快照、账号列表、固定 Host / Port / Protocol / Username / Password、地区参数、session 与自动切换阈值
|
||||
- Plus 模式开关 `plusModeEnabled`
|
||||
- Plus 支付方式 `plusPaymentMethod`,GoPay 配置 `gopayPhone / gopayPin`
|
||||
- PayPal 账号池配置 `paypalAccounts / currentPayPalAccountId`,以及供后台步骤兼容读取的 `paypalEmail / paypalPassword`
|
||||
- 邮箱 provider 配置
|
||||
- Hotmail 账号池
|
||||
@@ -499,10 +500,10 @@ Plus 模式通过 `plusModeEnabled` 开启,目标是在普通注册资料完
|
||||
Plus 模式可见步骤:
|
||||
|
||||
1. 第 1~5 步:沿用普通注册入口、邮箱、密码、注册验证码、资料填写链路。
|
||||
2. 第 6 步 `创建 Plus Checkout`:打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session,并打开 `https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`。
|
||||
3. 第 7 步 `填写账单并提交订阅`:选择 PayPal,生成账单全名,从 `data/address-sources.js` 读取同国家 seed query,触发 checkout 内置 Google 地址推荐,选择推荐项并校验地址第 1 行、城市、州/省、邮编等结构化字段,再点击“订阅”。运行时会按 Stripe iframe 拆分执行:付款方式在 `elements-inner-payment` frame,账单地址在 `elements-inner-address` frame,Google 推荐在 `elements-inner-autocompl` frame 时单独点击推荐项。
|
||||
4. 第 8 步 `PayPal 登录与授权`:后台优先读取侧边栏当前选中的 PayPal 账号;为兼容旧链路,也会把该账号同步回 `paypalEmail / paypalPassword`。当页面处于账号输入阶段时,内容脚本会固定对邮箱输入框执行 `focus -> clear -> fill -> blur -> click next`,即使文本框里已经预填了同一个账号,也会重新触发输入事件,避免 PayPal 因跳过重填而停在邮箱页;登录前固定等待 1 秒,关闭可见通行密钥提示,点击“同意并继续”。
|
||||
5. 第 9 步 `订阅回跳确认`:等待 PayPal 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。
|
||||
2. 第 6 步 `创建 Plus Checkout`:打开已登录 ChatGPT 页面,通过 `/api/auth/session` 读取 accessToken,再请求 `https://chatgpt.com/backend-api/payments/checkout` 创建 `chatgptplusplan` 的 checkout session。PayPal 使用 `DE / EUR` 与 `https://chatgpt.com/checkout/openai_ie/{checkout_session_id}`;GoPay 使用 `ID / IDR` 与 `https://chatgpt.com/checkout/openai_llc/{checkout_session_id}`。
|
||||
3. 第 7 步 `填写账单并提交订阅`:按 `plusPaymentMethod` 选择 PayPal 或 GoPay,生成账单全名,从 `data/address-sources.js` 读取地址 seed query,触发 checkout 内置 Google 地址推荐,选择推荐项并校验地址第 1 行、城市、州/省、邮编等结构化字段,再点击“订阅”。GoPay 模式固定使用印尼地址,不根据 IP 国家推断。运行时会按 Stripe iframe 拆分执行:付款方式在 `elements-inner-payment` frame,账单地址在 `elements-inner-address` frame,Google 推荐在 `elements-inner-autocompl` frame 时单独点击推荐项。
|
||||
4. 第 8 步按当前支付方式显示为 `PayPal 登录与授权` 或 `GoPay 手机验证与授权`,底层 step key 仍为 `paypal-approve`。PayPal 模式后台优先读取侧边栏当前选中的 PayPal 账号;GoPay 模式读取侧边栏的国家区号、手机号、可选验证码和 PIN,先在 GoPay 页面填写手机号;验证码优先用侧边栏已填值,否则弹出插件输入框让用户手动填写,提交后继续填写 PIN。
|
||||
5. 第 9 步 `订阅回跳确认`:等待 PayPal / GoPay 授权后回跳到 ChatGPT / OpenAI 页面,页面加载完成后固定等待 1 秒。
|
||||
6. 第 10 步:复用原 Step 7 OAuth 登录执行器,但状态和日志按 Plus 可见第 10 步记录。
|
||||
7. 第 11 步:复用原 Step 8 登录验证码执行器,但状态和日志按 Plus 可见第 11 步记录。
|
||||
8. 第 12 步:复用原 Step 9 OAuth 同意页点击和 localhost callback 捕获执行器,但状态和日志按 Plus 可见第 12 步记录。
|
||||
|
||||
Reference in New Issue
Block a user