feat: add PayPal hosted checkout proxy support
This commit is contained in:
@@ -10,6 +10,11 @@
|
|||||||
const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
|
const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
|
||||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||||
|
const LOCAL_CHECKOUT_PROXY_HEALTH_URL = 'http://127.0.0.1:21988/health';
|
||||||
|
const LOCAL_CHECKOUT_PROXY_URL = 'socks5://127.0.0.1:21987';
|
||||||
|
const LOCAL_CHECKOUT_PROXY_SETTINGS_SCOPE = 'regular';
|
||||||
|
const LOCAL_CHECKOUT_PROXY_TIMEOUT_MS = 1200;
|
||||||
|
const LOCAL_CHECKOUT_PROXY_SETTLE_MS = 350;
|
||||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||||
@@ -24,6 +29,7 @@
|
|||||||
const PAYPAL_HOSTED_STAGE_LOGIN = 'pay_login';
|
const PAYPAL_HOSTED_STAGE_LOGIN = 'pay_login';
|
||||||
const PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT = 'guest_checkout';
|
const PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT = 'guest_checkout';
|
||||||
const PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT = 'create_account';
|
const PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT = 'create_account';
|
||||||
|
const PAYPAL_HOSTED_STAGE_SECURITY_CODE = 'security_code';
|
||||||
const PAYPAL_HOSTED_STAGE_REVIEW = 'review_consent';
|
const PAYPAL_HOSTED_STAGE_REVIEW = 'review_consent';
|
||||||
const PAYPAL_HOSTED_STAGE_APPROVAL = 'approval';
|
const PAYPAL_HOSTED_STAGE_APPROVAL = 'approval';
|
||||||
const PAYPAL_HOSTED_STAGE_UNKNOWN = 'unknown';
|
const PAYPAL_HOSTED_STAGE_UNKNOWN = 'unknown';
|
||||||
@@ -58,6 +64,7 @@
|
|||||||
sleepWithStop,
|
sleepWithStop,
|
||||||
waitForTabCompleteUntilStopped,
|
waitForTabCompleteUntilStopped,
|
||||||
waitForTabUrlMatchUntilStopped = null,
|
waitForTabUrlMatchUntilStopped = null,
|
||||||
|
withCheckoutCreationProxy = null,
|
||||||
throwIfStopped = () => {},
|
throwIfStopped = () => {},
|
||||||
} = deps;
|
} = deps;
|
||||||
|
|
||||||
@@ -78,6 +85,194 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseSocks5Endpoint(proxyUrl = '') {
|
||||||
|
const text = String(proxyUrl || '').trim();
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let parsed = null;
|
||||||
|
try {
|
||||||
|
parsed = new URL(text);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (String(parsed.protocol || '').replace(/:$/, '').toLowerCase() !== 'socks5') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const host = String(parsed.hostname || '').replace(/^\[|\]$/g, '').trim();
|
||||||
|
const port = Number.parseInt(String(parsed.port || ''), 10);
|
||||||
|
if (!host || !Number.isInteger(port) || port <= 0 || port > 65535) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { host, port };
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCheckoutCreationPacScript(endpoint) {
|
||||||
|
const proxyHost = String(endpoint?.host || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
||||||
|
const port = Number.parseInt(String(endpoint?.port || ''), 10);
|
||||||
|
return `
|
||||||
|
function FindProxyForURL(url, host) {
|
||||||
|
host = String(host || '').toLowerCase();
|
||||||
|
if (host === 'chatgpt.com' || dnsDomainIs(host, '.chatgpt.com')) {
|
||||||
|
return "SOCKS5 ${proxyHost}:${port}";
|
||||||
|
}
|
||||||
|
return "DIRECT";
|
||||||
|
}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function callChromeProxySettings(method, details = {}) {
|
||||||
|
const proxySettings = chrome?.proxy?.settings;
|
||||||
|
if (!proxySettings || typeof proxySettings[method] !== 'function') {
|
||||||
|
return Promise.reject(new Error('当前浏览器不支持扩展代理 API'));
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
proxySettings[method](details, (value) => {
|
||||||
|
const lastError = chrome?.runtime?.lastError;
|
||||||
|
if (lastError) {
|
||||||
|
reject(new Error(lastError.message || String(lastError)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function canControlProxySettings(details = {}) {
|
||||||
|
const level = String(details?.levelOfControl || '').trim();
|
||||||
|
return !level || level === 'controlled_by_this_extension' || level === 'controllable_by_this_extension';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readProxySettingsSnapshot() {
|
||||||
|
return callChromeProxySettings('get', { incognito: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function restoreProxySettingsSnapshot(snapshot = null) {
|
||||||
|
const value = snapshot?.value;
|
||||||
|
const level = String(snapshot?.levelOfControl || '').trim();
|
||||||
|
if (level === 'controlled_by_this_extension' && value && typeof value === 'object') {
|
||||||
|
await callChromeProxySettings('set', {
|
||||||
|
value,
|
||||||
|
scope: LOCAL_CHECKOUT_PROXY_SETTINGS_SCOPE,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await callChromeProxySettings('clear', {
|
||||||
|
scope: LOCAL_CHECKOUT_PROXY_SETTINGS_SCOPE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchLocalCheckoutProxyHealth() {
|
||||||
|
if (typeof fetchImpl !== 'function') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const controller = typeof AbortController === 'function' ? new AbortController() : null;
|
||||||
|
let timer = null;
|
||||||
|
try {
|
||||||
|
timer = controller
|
||||||
|
? setTimeout(() => controller.abort(), LOCAL_CHECKOUT_PROXY_TIMEOUT_MS)
|
||||||
|
: null;
|
||||||
|
const response = await fetchImpl(`${LOCAL_CHECKOUT_PROXY_HEALTH_URL}?t=${Date.now()}`, {
|
||||||
|
method: 'GET',
|
||||||
|
cache: 'no-store',
|
||||||
|
headers: { Accept: 'application/json,text/plain,*/*' },
|
||||||
|
...(controller ? { signal: controller.signal } : {}),
|
||||||
|
});
|
||||||
|
if (!response?.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!payload?.ok) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const endpoint = parseSocks5Endpoint(payload.localProxy || LOCAL_CHECKOUT_PROXY_URL);
|
||||||
|
return endpoint ? { endpoint, payload } : null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function applyTemporaryCheckoutProxy(endpoint) {
|
||||||
|
const pacScript = buildCheckoutCreationPacScript(endpoint);
|
||||||
|
await callChromeProxySettings('set', {
|
||||||
|
value: {
|
||||||
|
mode: 'pac_script',
|
||||||
|
pacScript: {
|
||||||
|
data: pacScript,
|
||||||
|
mandatory: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scope: LOCAL_CHECKOUT_PROXY_SETTINGS_SCOPE,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runWithLocalCheckoutCreationProxy(action) {
|
||||||
|
if (typeof withCheckoutCreationProxy === 'function') {
|
||||||
|
return withCheckoutCreationProxy({
|
||||||
|
healthUrl: LOCAL_CHECKOUT_PROXY_HEALTH_URL,
|
||||||
|
localProxyUrl: LOCAL_CHECKOUT_PROXY_URL,
|
||||||
|
}, action);
|
||||||
|
}
|
||||||
|
if (!chrome?.proxy?.settings || typeof fetchImpl !== 'function') {
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
|
||||||
|
const health = await fetchLocalCheckoutProxyHealth();
|
||||||
|
if (!health?.endpoint) {
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
|
||||||
|
let snapshot = null;
|
||||||
|
let applied = false;
|
||||||
|
let result = null;
|
||||||
|
let proxyError = null;
|
||||||
|
let restoreFailed = false;
|
||||||
|
try {
|
||||||
|
try {
|
||||||
|
snapshot = await readProxySettingsSnapshot();
|
||||||
|
} catch (error) {
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
if (!canControlProxySettings(snapshot)) {
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
await applyTemporaryCheckoutProxy(health.endpoint);
|
||||||
|
applied = true;
|
||||||
|
await sleepWithStop(LOCAL_CHECKOUT_PROXY_SETTLE_MS);
|
||||||
|
result = await action();
|
||||||
|
if (result?.error && !result?.stopped) {
|
||||||
|
proxyError = new Error(result.error);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (!applied) {
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
proxyError = error;
|
||||||
|
} finally {
|
||||||
|
if (applied) {
|
||||||
|
try {
|
||||||
|
await restoreProxySettingsSnapshot(snapshot);
|
||||||
|
await sleepWithStop(LOCAL_CHECKOUT_PROXY_SETTLE_MS);
|
||||||
|
} catch (error) {
|
||||||
|
restoreFailed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result && !proxyError) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (proxyError) {
|
||||||
|
if (restoreFailed) {
|
||||||
|
throw proxyError;
|
||||||
|
}
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizePlusPaymentMethod(value = '') {
|
function normalizePlusPaymentMethod(value = '') {
|
||||||
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
const rootScope = typeof self !== 'undefined' ? self : globalThis;
|
||||||
if (rootScope.GoPayUtils?.normalizePlusPaymentMethod) {
|
if (rootScope.GoPayUtils?.normalizePlusPaymentMethod) {
|
||||||
@@ -589,6 +784,16 @@
|
|||||||
return result || {};
|
return result || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitHostedPayPalSecurityCode(tabId, config = {}, stepKey = PAYPAL_HOSTED_STEP_CREATE_ACCOUNT) {
|
||||||
|
const stepNumber = getHostedStepNumber(stepKey);
|
||||||
|
const verificationCode = await pollHostedVerificationCode(config.verificationUrl);
|
||||||
|
await addHostedStepLog(stepKey, `步骤 ${stepNumber}:已获取 PayPal 手机验证码,正在填写。`, 'info');
|
||||||
|
return runHostedPayPalStep(tabId, {
|
||||||
|
expectedStage: PAYPAL_HOSTED_STAGE_SECURITY_CODE,
|
||||||
|
securityCode: verificationCode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getHostedStageOrder(stage = '') {
|
function getHostedStageOrder(stage = '') {
|
||||||
switch (stage) {
|
switch (stage) {
|
||||||
case PAYPAL_HOSTED_STAGE_LOGIN:
|
case PAYPAL_HOSTED_STAGE_LOGIN:
|
||||||
@@ -597,6 +802,8 @@
|
|||||||
return 2;
|
return 2;
|
||||||
case PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT:
|
case PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT:
|
||||||
return 3;
|
return 3;
|
||||||
|
case PAYPAL_HOSTED_STAGE_SECURITY_CODE:
|
||||||
|
return 3.5;
|
||||||
case PAYPAL_HOSTED_STAGE_REVIEW:
|
case PAYPAL_HOSTED_STAGE_REVIEW:
|
||||||
return 4;
|
return 4;
|
||||||
case PAYPAL_HOSTED_STAGE_OUTSIDE:
|
case PAYPAL_HOSTED_STAGE_OUTSIDE:
|
||||||
@@ -634,7 +841,7 @@
|
|||||||
try {
|
try {
|
||||||
const pageState = await getHostedPayPalState(tabId);
|
const pageState = await getHostedPayPalState(tabId);
|
||||||
lastStage = pageState?.hostedStage || lastStage;
|
lastStage = pageState?.hostedStage || lastStage;
|
||||||
if (predicate(pageState)) {
|
if (await predicate(pageState)) {
|
||||||
return pageState;
|
return pageState;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -934,6 +1141,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pageState = await getHostedPayPalState(tabId);
|
const pageState = await getHostedPayPalState(tabId);
|
||||||
|
const config = await getHostedCheckoutRuntimeConfig(state);
|
||||||
|
if (pageState.hostedStage === PAYPAL_HOSTED_STAGE_SECURITY_CODE) {
|
||||||
|
await submitHostedPayPalSecurityCode(tabId, config, stepKey);
|
||||||
|
const nextState = await waitForHostedPayPalStage(
|
||||||
|
tabId,
|
||||||
|
(stateInfo) => stateInfo?.hostedStage && stateInfo.hostedStage !== PAYPAL_HOSTED_STAGE_SECURITY_CODE,
|
||||||
|
{ label: `步骤 ${stepNumber}:等待 PayPal 验证码提交后跳转` }
|
||||||
|
);
|
||||||
|
await completeHostedStep(stepKey, tabId, {
|
||||||
|
plusHostedCheckoutLastStage: nextState.hostedStage || '',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (isHostedStageAtOrAfter(pageState.hostedStage, PAYPAL_HOSTED_STAGE_REVIEW)
|
if (isHostedStageAtOrAfter(pageState.hostedStage, PAYPAL_HOSTED_STAGE_REVIEW)
|
||||||
&& pageState.hostedStage !== PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT) {
|
&& pageState.hostedStage !== PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT) {
|
||||||
await addHostedStepLog(stepKey, `步骤 ${stepNumber}:当前 PayPal 已进入后续页面(${pageState.hostedStage}),创建确认节点直接完成。`, 'info');
|
await addHostedStepLog(stepKey, `步骤 ${stepNumber}:当前 PayPal 已进入后续页面(${pageState.hostedStage}),创建确认节点直接完成。`, 'info');
|
||||||
@@ -952,7 +1172,13 @@
|
|||||||
});
|
});
|
||||||
const nextState = await waitForHostedPayPalStage(
|
const nextState = await waitForHostedPayPalStage(
|
||||||
tabId,
|
tabId,
|
||||||
(stateInfo) => stateInfo?.hostedStage && stateInfo.hostedStage !== PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT,
|
async (stateInfo) => {
|
||||||
|
if (stateInfo?.hostedStage === PAYPAL_HOSTED_STAGE_SECURITY_CODE) {
|
||||||
|
await submitHostedPayPalSecurityCode(tabId, config, stepKey);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return stateInfo?.hostedStage && stateInfo.hostedStage !== PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT;
|
||||||
|
},
|
||||||
{ label: `步骤 ${stepNumber}:等待 PayPal 创建确认页跳转` }
|
{ label: `步骤 ${stepNumber}:等待 PayPal 创建确认页跳转` }
|
||||||
);
|
);
|
||||||
await completeHostedStep(stepKey, tabId, {
|
await completeHostedStep(stepKey, tabId, {
|
||||||
@@ -1392,7 +1618,9 @@
|
|||||||
const paymentMethodLabel = getPlusPaymentMethodLabel(paymentMethod);
|
const paymentMethodLabel = getPlusPaymentMethodLabel(paymentMethod);
|
||||||
const checkoutModeLabel = getCheckoutModeLabel(state);
|
const checkoutModeLabel = getCheckoutModeLabel(state);
|
||||||
await addLog(`步骤 6:正在打开新的 ChatGPT 会话,准备创建${checkoutModeLabel}...`, 'info');
|
await addLog(`步骤 6:正在打开新的 ChatGPT 会话,准备创建${checkoutModeLabel}...`, 'info');
|
||||||
const tabId = await openFreshChatGptTabForCheckoutCreate();
|
let tabId = 0;
|
||||||
|
const createCheckout = async () => {
|
||||||
|
tabId = await openFreshChatGptTabForCheckoutCreate();
|
||||||
|
|
||||||
await waitForTabCompleteUntilStopped(tabId);
|
await waitForTabCompleteUntilStopped(tabId);
|
||||||
await sleepWithStop(1000);
|
await sleepWithStop(1000);
|
||||||
@@ -1402,11 +1630,15 @@
|
|||||||
logMessage: '步骤 6:正在等待 ChatGPT 页面完成加载,再继续创建订阅页...',
|
logMessage: '步骤 6:正在等待 ChatGPT 页面完成加载,再继续创建订阅页...',
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
|
return sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
|
||||||
type: 'CREATE_PLUS_CHECKOUT',
|
type: 'CREATE_PLUS_CHECKOUT',
|
||||||
source: 'background',
|
source: 'background',
|
||||||
payload: { paymentMethod },
|
payload: { paymentMethod },
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
const result = paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED
|
||||||
|
? await runWithLocalCheckoutCreationProxy(createCheckout)
|
||||||
|
: await createCheckout();
|
||||||
|
|
||||||
if (result?.error) {
|
if (result?.error) {
|
||||||
throw new Error(result.error);
|
throw new Error(result.error);
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const PAYPAL_HOSTED_STAGE_OUTSIDE = 'outside_paypal';
|
|||||||
const PAYPAL_HOSTED_STAGE_LOGIN = 'pay_login';
|
const PAYPAL_HOSTED_STAGE_LOGIN = 'pay_login';
|
||||||
const PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT = 'guest_checkout';
|
const PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT = 'guest_checkout';
|
||||||
const PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT = 'create_account';
|
const PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT = 'create_account';
|
||||||
|
const PAYPAL_HOSTED_STAGE_SECURITY_CODE = 'security_code';
|
||||||
const PAYPAL_HOSTED_STAGE_REVIEW = 'review_consent';
|
const PAYPAL_HOSTED_STAGE_REVIEW = 'review_consent';
|
||||||
const PAYPAL_HOSTED_STAGE_APPROVAL = 'approval';
|
const PAYPAL_HOSTED_STAGE_APPROVAL = 'approval';
|
||||||
const PAYPAL_HOSTED_STAGE_UNKNOWN = 'unknown';
|
const PAYPAL_HOSTED_STAGE_UNKNOWN = 'unknown';
|
||||||
@@ -15,6 +16,7 @@ const PAYPAL_HOSTED_STEP_KEYS = {
|
|||||||
[PAYPAL_HOSTED_STAGE_LOGIN]: 'paypal-hosted-email',
|
[PAYPAL_HOSTED_STAGE_LOGIN]: 'paypal-hosted-email',
|
||||||
[PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT]: 'paypal-hosted-card',
|
[PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT]: 'paypal-hosted-card',
|
||||||
[PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT]: 'paypal-hosted-create-account',
|
[PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT]: 'paypal-hosted-create-account',
|
||||||
|
[PAYPAL_HOSTED_STAGE_SECURITY_CODE]: 'paypal-hosted-create-account',
|
||||||
[PAYPAL_HOSTED_STAGE_REVIEW]: 'paypal-hosted-review',
|
[PAYPAL_HOSTED_STAGE_REVIEW]: 'paypal-hosted-review',
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -308,10 +310,63 @@ function findHostedReviewConsentButton() {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getHostedSecurityCodeInputs() {
|
||||||
|
const pageText = normalizeText(document.body?.innerText || document.body?.textContent || '');
|
||||||
|
const pageLooksLikeSecurityCode = /enter\s+(?:your\s+)?code|6[-\s]*digit\s+code|security\s+code|verification\s+code|we\s+sent\s+a\s+6[-\s]*digit\s+code/i.test(pageText);
|
||||||
|
const visibleInputs = getVisibleControls('input')
|
||||||
|
.filter((input) => {
|
||||||
|
const type = String(input.getAttribute?.('type') || input.type || '').trim().toLowerCase();
|
||||||
|
return isEnabledControl(input)
|
||||||
|
&& !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type);
|
||||||
|
});
|
||||||
|
const candidates = visibleInputs
|
||||||
|
.filter((input) => {
|
||||||
|
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
|
||||||
|
const metadata = getActionText(input);
|
||||||
|
return maxLength === 1 || /otp|code|verification|security|one[-\s]*time/i.test(metadata);
|
||||||
|
});
|
||||||
|
if (candidates.length < 6 && pageLooksLikeSecurityCode && visibleInputs.length >= 6) {
|
||||||
|
return visibleInputs.slice(0, 6);
|
||||||
|
}
|
||||||
|
const singleDigitInputs = candidates.filter((input) => {
|
||||||
|
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
|
||||||
|
const valueLength = String(input.value || '').length;
|
||||||
|
return maxLength === 1 || valueLength <= 1;
|
||||||
|
});
|
||||||
|
return singleDigitInputs.length >= 6 ? singleDigitInputs.slice(0, 6) : candidates;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHostedSecurityCodeSingleInput() {
|
||||||
|
return getVisibleControls('input').find((input) => {
|
||||||
|
const type = String(input.getAttribute?.('type') || input.type || '').trim().toLowerCase();
|
||||||
|
const maxLength = Number(input.getAttribute?.('maxlength') || input.maxLength || 0);
|
||||||
|
const metadata = getActionText(input);
|
||||||
|
return isEnabledControl(input)
|
||||||
|
&& !['hidden', 'checkbox', 'radio', 'submit', 'button', 'file'].includes(type)
|
||||||
|
&& maxLength >= 6
|
||||||
|
&& /otp|code|verification|security|one[-\s]*time/i.test(metadata);
|
||||||
|
}) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findHostedSecurityCodeSubmitButton() {
|
||||||
|
return findClickableByText([
|
||||||
|
/submit|continue|next|verify|confirm|done/i,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHostedSecurityCodePage() {
|
||||||
|
const pageText = normalizeText(document.body?.innerText || document.body?.textContent || '');
|
||||||
|
const hasSecurityText = /enter\s+(?:your\s+)?code|6[-\s]*digit\s+code|security\s+code|verification\s+code|we\s+sent\s+a\s+6[-\s]*digit\s+code/i.test(pageText);
|
||||||
|
return hasSecurityText && (getHostedSecurityCodeInputs().length >= 6 || Boolean(getHostedSecurityCodeSingleInput()));
|
||||||
|
}
|
||||||
|
|
||||||
function detectPayPalHostedStage() {
|
function detectPayPalHostedStage() {
|
||||||
if (!/paypal\./i.test(String(location?.host || ''))) {
|
if (!/paypal\./i.test(String(location?.host || ''))) {
|
||||||
return PAYPAL_HOSTED_STAGE_OUTSIDE;
|
return PAYPAL_HOSTED_STAGE_OUTSIDE;
|
||||||
}
|
}
|
||||||
|
if (isHostedSecurityCodePage()) {
|
||||||
|
return PAYPAL_HOSTED_STAGE_SECURITY_CODE;
|
||||||
|
}
|
||||||
if (isHostedGuestCheckoutPage()) {
|
if (isHostedGuestCheckoutPage()) {
|
||||||
return PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT;
|
return PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT;
|
||||||
}
|
}
|
||||||
@@ -465,6 +520,48 @@ function verifyHostedPhoneBeforeSubmit(expectedPhone = '') {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeHostedSecurityCode(value = '') {
|
||||||
|
const code = String(value || '').replace(/\D/g, '').slice(0, 6);
|
||||||
|
return /^\d{6}$/.test(code) ? code : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitHostedSecurityCode(payload = {}) {
|
||||||
|
await waitForDocumentComplete();
|
||||||
|
const code = normalizeHostedSecurityCode(payload.securityCode || payload.verificationCode || payload.code || '');
|
||||||
|
if (!code) {
|
||||||
|
throw new Error('PayPal hosted checkout 验证码为空或不是 6 位数字。');
|
||||||
|
}
|
||||||
|
const digitInputs = getHostedSecurityCodeInputs();
|
||||||
|
const singleInput = getHostedSecurityCodeSingleInput();
|
||||||
|
if (digitInputs.length >= 6) {
|
||||||
|
digitInputs.slice(0, 6).forEach((input, index) => {
|
||||||
|
fillInput(input, code[index]);
|
||||||
|
});
|
||||||
|
} else if (singleInput) {
|
||||||
|
fillInput(singleInput, code);
|
||||||
|
} else {
|
||||||
|
throw new Error('PayPal hosted checkout 未找到验证码输入框。');
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitButton = findHostedSecurityCodeSubmitButton();
|
||||||
|
if (submitButton && isVisibleElement(submitButton) && isEnabledControl(submitButton)) {
|
||||||
|
await performPayPalOperationWithDelay({
|
||||||
|
stepKey: getHostedStepKey(PAYPAL_HOSTED_STAGE_SECURITY_CODE),
|
||||||
|
kind: 'click',
|
||||||
|
label: 'hosted-paypal-security-code-submit',
|
||||||
|
}, async () => {
|
||||||
|
simulateClick(submitButton);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await sleep(1000);
|
||||||
|
return {
|
||||||
|
stage: PAYPAL_HOSTED_STAGE_SECURITY_CODE,
|
||||||
|
securityCodeSubmitted: true,
|
||||||
|
submitted: Boolean(submitButton),
|
||||||
|
inputCount: digitInputs.length >= 6 ? 6 : 1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function clickHostedCreateAccount(payload = {}) {
|
async function clickHostedCreateAccount(payload = {}) {
|
||||||
await waitForDocumentComplete();
|
await waitForDocumentComplete();
|
||||||
const button = await waitUntil(() => {
|
const button = await waitUntil(() => {
|
||||||
@@ -640,6 +737,9 @@ async function runPayPalHostedCheckoutStep(payload = {}) {
|
|||||||
if (stage === PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT) {
|
if (stage === PAYPAL_HOSTED_STAGE_CREATE_ACCOUNT) {
|
||||||
return clickHostedCreateAccount(payload);
|
return clickHostedCreateAccount(payload);
|
||||||
}
|
}
|
||||||
|
if (stage === PAYPAL_HOSTED_STAGE_SECURITY_CODE) {
|
||||||
|
return submitHostedSecurityCode(payload);
|
||||||
|
}
|
||||||
if (stage === PAYPAL_HOSTED_STAGE_REVIEW) {
|
if (stage === PAYPAL_HOSTED_STAGE_REVIEW) {
|
||||||
return clickHostedReviewConsent();
|
return clickHostedReviewConsent();
|
||||||
}
|
}
|
||||||
@@ -659,6 +759,7 @@ function inspectPayPalHostedState() {
|
|||||||
hostedStage: stage,
|
hostedStage: stage,
|
||||||
hasGuestCardFields: Boolean(document.getElementById('cardNumber')),
|
hasGuestCardFields: Boolean(document.getElementById('cardNumber')),
|
||||||
hasHostedEmailInput: Boolean(document.getElementById('email') || findEmailInput()),
|
hasHostedEmailInput: Boolean(document.getElementById('email') || findEmailInput()),
|
||||||
|
securityCodeVisible: stage === PAYPAL_HOSTED_STAGE_SECURITY_CODE,
|
||||||
createAccountReady: Boolean(createAccountButton && isVisibleElement(createAccountButton) && isEnabledControl(createAccountButton)),
|
createAccountReady: Boolean(createAccountButton && isVisibleElement(createAccountButton) && isEnabledControl(createAccountButton)),
|
||||||
reviewConsentReady: Boolean(findHostedReviewConsentButton()),
|
reviewConsentReady: Boolean(findHostedReviewConsentButton()),
|
||||||
approveReady: Boolean(findApproveButton()),
|
approveReady: Boolean(findApproveButton()),
|
||||||
|
|||||||
@@ -413,6 +413,16 @@ function createHostedPayPalHarness(options = {}) {
|
|||||||
id: 'btnNext',
|
id: 'btnNext',
|
||||||
text: '下一页',
|
text: '下一页',
|
||||||
});
|
});
|
||||||
|
const securityCodeInputs = Array.from({ length: 6 }, (_value, index) => createDomElement({
|
||||||
|
tagName: 'INPUT',
|
||||||
|
id: `securityCode${index + 1}`,
|
||||||
|
type: 'text',
|
||||||
|
}));
|
||||||
|
const securityCodeContinueButton = createDomElement({
|
||||||
|
tagName: 'BUTTON',
|
||||||
|
id: 'securityCodeContinue',
|
||||||
|
text: 'Continue',
|
||||||
|
});
|
||||||
|
|
||||||
function setElements(nextElements) {
|
function setElements(nextElements) {
|
||||||
elements = nextElements;
|
elements = nextElements;
|
||||||
@@ -464,6 +474,15 @@ function createHostedPayPalHarness(options = {}) {
|
|||||||
setElements([emailInput, nextButton, createAccountButton]);
|
setElements([emailInput, nextButton, createAccountButton]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function showSecurityCode() {
|
||||||
|
location.href = 'https://www.paypal.com/checkoutweb/security-code';
|
||||||
|
location.host = 'www.paypal.com';
|
||||||
|
location.pathname = '/checkoutweb/security-code';
|
||||||
|
body.innerText = 'Enter your code We sent a 6-digit code to (835) 253-1607 Resend';
|
||||||
|
body.textContent = body.innerText;
|
||||||
|
setElements([...securityCodeInputs, securityCodeContinueButton]);
|
||||||
|
}
|
||||||
|
|
||||||
const context = {
|
const context = {
|
||||||
console: { log() {}, warn() {}, error() {}, info() {} },
|
console: { log() {}, warn() {}, error() {}, info() {} },
|
||||||
location,
|
location,
|
||||||
@@ -558,6 +577,7 @@ function createHostedPayPalHarness(options = {}) {
|
|||||||
showPayEmail,
|
showPayEmail,
|
||||||
showCreateAccount,
|
showCreateAccount,
|
||||||
showGuestCheckout,
|
showGuestCheckout,
|
||||||
|
showSecurityCode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,3 +707,37 @@ test('PayPal hosted create account page is detected and handled as its own step'
|
|||||||
[{ stepKey: 'paypal-hosted-create-account', kind: 'click', label: 'hosted-paypal-create-account' }]
|
[{ stepKey: 'paypal-hosted-create-account', kind: 'click', label: 'hosted-paypal-create-account' }]
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('PayPal hosted security code page fills six digit code inputs', async () => {
|
||||||
|
const harness = createHostedPayPalHarness();
|
||||||
|
harness.showSecurityCode();
|
||||||
|
|
||||||
|
const state = await harness.send({
|
||||||
|
type: 'PAYPAL_HOSTED_GET_STATE',
|
||||||
|
source: 'test',
|
||||||
|
payload: {},
|
||||||
|
});
|
||||||
|
assert.equal(state.ok, true);
|
||||||
|
assert.equal(state.hostedStage, 'security_code');
|
||||||
|
assert.equal(state.securityCodeVisible, true);
|
||||||
|
|
||||||
|
const result = await harness.send({
|
||||||
|
type: 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP',
|
||||||
|
source: 'test',
|
||||||
|
payload: {
|
||||||
|
expectedStage: 'security_code',
|
||||||
|
securityCode: '921714',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.ok, true);
|
||||||
|
assert.equal(result.stage, 'security_code');
|
||||||
|
assert.equal(result.securityCodeSubmitted, true);
|
||||||
|
assert.deepEqual(
|
||||||
|
harness.events
|
||||||
|
.filter((event) => event.type === 'fill' && /^securityCode/.test(event.id))
|
||||||
|
.map((event) => event.value),
|
||||||
|
['9', '2', '1', '7', '1', '4']
|
||||||
|
);
|
||||||
|
assert.equal(harness.events.some((event) => event.type === 'click' && event.id === 'securityCodeContinue'), true);
|
||||||
|
});
|
||||||
|
|||||||
@@ -259,6 +259,7 @@ test('Plus checkout create does not wait 20 seconds after opening checkout page'
|
|||||||
|
|
||||||
test('GoPay plus checkout create forwards gopay payment method to the checkout content script', async () => {
|
test('GoPay plus checkout create forwards gopay payment method to the checkout content script', async () => {
|
||||||
const events = [];
|
const events = [];
|
||||||
|
let proxyCallCount = 0;
|
||||||
const executor = api.createPlusCheckoutCreateExecutor({
|
const executor = api.createPlusCheckoutCreateExecutor({
|
||||||
addLog: async () => {},
|
addLog: async () => {},
|
||||||
chrome: {
|
chrome: {
|
||||||
@@ -281,11 +282,183 @@ test('GoPay plus checkout create forwards gopay payment method to the checkout c
|
|||||||
setState: async () => {},
|
setState: async () => {},
|
||||||
sleepWithStop: async () => {},
|
sleepWithStop: async () => {},
|
||||||
waitForTabCompleteUntilStopped: async () => {},
|
waitForTabCompleteUntilStopped: async () => {},
|
||||||
|
withCheckoutCreationProxy: async () => {
|
||||||
|
proxyCallCount += 1;
|
||||||
|
throw new Error('gopay checkout should not use the checkout proxy wrapper');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await executor.executePlusCheckoutCreate({ plusPaymentMethod: 'gopay' });
|
await executor.executePlusCheckoutCreate({ plusPaymentMethod: 'gopay' });
|
||||||
|
|
||||||
assert.deepStrictEqual(events[0]?.payload, { paymentMethod: 'gopay' });
|
assert.deepStrictEqual(events[0]?.payload, { paymentMethod: 'gopay' });
|
||||||
|
assert.equal(proxyCallCount, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PayPal no-card binding creates checkout inside the local checkout proxy wrapper', async () => {
|
||||||
|
const events = [];
|
||||||
|
const executor = api.createPlusCheckoutCreateExecutor({
|
||||||
|
addLog: async () => {},
|
||||||
|
chrome: {
|
||||||
|
tabs: {
|
||||||
|
create: async () => ({ id: 77, url: 'https://chatgpt.com/', status: 'complete' }),
|
||||||
|
update: async () => {},
|
||||||
|
get: async () => ({ id: 77, url: 'https://www.paypal.com/pay?token=BA-wrapper', status: 'complete' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
completeNodeFromBackground: async () => {},
|
||||||
|
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||||
|
fetch: async () => ({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({
|
||||||
|
address: {
|
||||||
|
Address: '1 Main St',
|
||||||
|
City: 'New York',
|
||||||
|
State: 'New York',
|
||||||
|
Zip_Code: '10001',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
getState: async () => ({
|
||||||
|
hostedCheckoutPhoneNumber: '4155551234',
|
||||||
|
}),
|
||||||
|
registerTab: async () => {},
|
||||||
|
sendTabMessageUntilStopped: async (_tabId, _source, message) => {
|
||||||
|
events.push({ type: 'tab-message', message, inProxy: events.some((event) => event.type === 'proxy-enter') && !events.some((event) => event.type === 'proxy-exit') });
|
||||||
|
if (message.type === 'CREATE_PLUS_CHECKOUT') {
|
||||||
|
return {
|
||||||
|
checkoutUrl: 'https://chatgpt.com/checkout/openai_llc/cs_hosted',
|
||||||
|
preferredCheckoutUrl: 'https://pay.openai.com/c/pay/cs_hosted',
|
||||||
|
hostedCheckoutUrl: 'https://pay.openai.com/c/pay/cs_hosted',
|
||||||
|
country: 'US',
|
||||||
|
currency: 'USD',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (message.type === 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP') {
|
||||||
|
return { clicked: true };
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected message type ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
waitForTabCompleteUntilStopped: async () => {},
|
||||||
|
withCheckoutCreationProxy: async (config, action) => {
|
||||||
|
events.push({ type: 'proxy-enter', config });
|
||||||
|
const result = await action();
|
||||||
|
events.push({ type: 'proxy-exit' });
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await executor.executePlusCheckoutCreate({
|
||||||
|
plusPaymentMethod: 'paypal-hosted',
|
||||||
|
plusHostedCheckoutOauthDelaySeconds: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(events.find((event) => event.type === 'proxy-enter')?.config, {
|
||||||
|
healthUrl: 'http://127.0.0.1:21988/health',
|
||||||
|
localProxyUrl: 'socks5://127.0.0.1:21987',
|
||||||
|
});
|
||||||
|
assert.equal(
|
||||||
|
events.find((event) => event.type === 'tab-message' && event.message.type === 'CREATE_PLUS_CHECKOUT')?.inProxy,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('PayPal no-card binding falls back to direct checkout when local helper proxy fails', async () => {
|
||||||
|
const events = [];
|
||||||
|
let createAttempts = 0;
|
||||||
|
const proxySettings = {
|
||||||
|
get(details, callback) {
|
||||||
|
events.push({ type: 'proxy-get', details });
|
||||||
|
callback({
|
||||||
|
levelOfControl: 'controllable_by_this_extension',
|
||||||
|
value: { mode: 'system' },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
set(details, callback) {
|
||||||
|
events.push({ type: 'proxy-set', details });
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
clear(details, callback) {
|
||||||
|
events.push({ type: 'proxy-clear', details });
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const executor = api.createPlusCheckoutCreateExecutor({
|
||||||
|
addLog: async () => {},
|
||||||
|
chrome: {
|
||||||
|
runtime: {},
|
||||||
|
proxy: {
|
||||||
|
settings: proxySettings,
|
||||||
|
},
|
||||||
|
tabs: {
|
||||||
|
create: async () => ({ id: 78, url: 'https://chatgpt.com/', status: 'complete' }),
|
||||||
|
update: async () => {},
|
||||||
|
get: async () => ({ id: 78, url: 'https://www.paypal.com/pay?token=BA-direct', status: 'complete' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
completeNodeFromBackground: async () => {},
|
||||||
|
ensureContentScriptReadyOnTabUntilStopped: async () => {},
|
||||||
|
fetch: async (url) => {
|
||||||
|
events.push({ type: 'fetch', url });
|
||||||
|
if (String(url).startsWith('http://127.0.0.1:21988/health')) {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({ ok: true, localProxy: 'socks5://127.0.0.1:21987' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({
|
||||||
|
address: {
|
||||||
|
Address: '1 Main St',
|
||||||
|
City: 'New York',
|
||||||
|
State: 'New York',
|
||||||
|
Zip_Code: '10001',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getState: async () => ({
|
||||||
|
hostedCheckoutPhoneNumber: '4155551234',
|
||||||
|
}),
|
||||||
|
registerTab: async () => {},
|
||||||
|
sendTabMessageUntilStopped: async (_tabId, _source, message) => {
|
||||||
|
events.push({ type: 'tab-message', message });
|
||||||
|
if (message.type === 'CREATE_PLUS_CHECKOUT') {
|
||||||
|
createAttempts += 1;
|
||||||
|
if (createAttempts === 1) {
|
||||||
|
return { error: 'proxy connect failed' };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
checkoutUrl: 'https://chatgpt.com/checkout/openai_llc/cs_hosted',
|
||||||
|
preferredCheckoutUrl: 'https://www.paypal.com/pay?token=BA-direct',
|
||||||
|
hostedCheckoutUrl: 'https://www.paypal.com/pay?token=BA-direct',
|
||||||
|
country: 'US',
|
||||||
|
currency: 'USD',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (message.type === 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP') {
|
||||||
|
return { clicked: true };
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected message type ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async () => {},
|
||||||
|
sleepWithStop: async () => {},
|
||||||
|
waitForTabCompleteUntilStopped: async () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
await executor.executePlusCheckoutCreate({
|
||||||
|
plusPaymentMethod: 'paypal-hosted',
|
||||||
|
plusHostedCheckoutOauthDelaySeconds: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(createAttempts, 2);
|
||||||
|
assert.equal(events.some((event) => event.type === 'proxy-set' && event.details?.value?.mode === 'pac_script'), true);
|
||||||
|
assert.equal(events.some((event) => event.type === 'proxy-clear' && event.details?.scope === 'regular'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('PayPal no-card binding create opens and submits hosted OpenAI checkout before completing', async () => {
|
test('PayPal no-card binding create opens and submits hosted OpenAI checkout before completing', async () => {
|
||||||
@@ -554,6 +727,89 @@ test('PayPal hosted email node completes when Next navigation drops the content
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('PayPal hosted create account node submits PayPal security code from SMS text payload', async () => {
|
||||||
|
const events = [];
|
||||||
|
let stage = 'create_account';
|
||||||
|
const executor = api.createPlusCheckoutCreateExecutor({
|
||||||
|
addLog: async (message, level = 'info', options = {}) => events.push({ type: 'log', message, level, options }),
|
||||||
|
chrome: {
|
||||||
|
tabs: {
|
||||||
|
get: async (tabId) => ({ id: tabId, url: 'https://www.paypal.com/checkoutweb/create-account', status: 'complete' }),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
completeNodeFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
|
||||||
|
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options) => events.push({ type: 'ready', source, tabId, options }),
|
||||||
|
fetch: async (url) => {
|
||||||
|
events.push({ type: 'fetch', url });
|
||||||
|
if (url === 'https://www.meiguodizhi.com/api/v1/dz') {
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({
|
||||||
|
address: {
|
||||||
|
Address: '1 Main St',
|
||||||
|
City: 'New York',
|
||||||
|
State: 'New York',
|
||||||
|
Zip_Code: '10001',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
assert.equal(String(url).startsWith('https://otp.example.test/latest?t='), true);
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => JSON.stringify({
|
||||||
|
code: 1,
|
||||||
|
msg: 'ok',
|
||||||
|
data: {
|
||||||
|
code: "PayPal: 921714 is your security code. Don't share it.",
|
||||||
|
code_time: '2026-05-25 01:41:22',
|
||||||
|
expired_date: '2026-08-15 00:00:00',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getState: async () => ({
|
||||||
|
hostedCheckoutVerificationUrl: 'https://otp.example.test/latest',
|
||||||
|
hostedCheckoutPhoneNumber: '8352531607',
|
||||||
|
}),
|
||||||
|
registerTab: async (source, tabId) => events.push({ type: 'register', source, tabId }),
|
||||||
|
sendTabMessageUntilStopped: async (tabId, source, message) => {
|
||||||
|
events.push({ type: 'tab-message', tabId, source, message });
|
||||||
|
if (message.type === 'PAYPAL_HOSTED_GET_STATE') {
|
||||||
|
return { hostedStage: stage };
|
||||||
|
}
|
||||||
|
if (message.type === 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP' && message.payload.expectedStage === 'create_account') {
|
||||||
|
stage = 'security_code';
|
||||||
|
return { clicked: true, submitted: true, stage: 'create_account' };
|
||||||
|
}
|
||||||
|
if (message.type === 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP' && message.payload.expectedStage === 'security_code') {
|
||||||
|
stage = 'review_consent';
|
||||||
|
return { securityCodeSubmitted: true, stage: 'security_code' };
|
||||||
|
}
|
||||||
|
throw new Error(`unexpected message type ${message.type}`);
|
||||||
|
},
|
||||||
|
setState: async (payload) => events.push({ type: 'set-state', payload }),
|
||||||
|
sleepWithStop: async (ms) => events.push({ type: 'sleep', ms }),
|
||||||
|
waitForTabCompleteUntilStopped: async () => events.push({ type: 'tab-complete' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
await executor.executePayPalHostedCreateAccount({
|
||||||
|
plusCheckoutTabId: 55,
|
||||||
|
plusPaymentMethod: 'paypal-hosted',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
events.find((event) => event.type === 'tab-message' && event.message?.payload?.expectedStage === 'security_code')?.message?.payload,
|
||||||
|
{
|
||||||
|
expectedStage: 'security_code',
|
||||||
|
securityCode: '921714',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
assert.equal(events.some((event) => event.type === 'complete' && event.step === 'paypal-hosted-create-account'), true);
|
||||||
|
});
|
||||||
|
|
||||||
test('Plus checkout content routes billing operations through the operation delay gate', async () => {
|
test('Plus checkout content routes billing operations through the operation delay gate', async () => {
|
||||||
const { checkoutEvents, send } = createCheckoutContentHarness();
|
const { checkoutEvents, send } = createCheckoutContentHarness();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user