Add independent PayPal hosted binding mode
This commit is contained in:
+88
-1
@@ -128,6 +128,36 @@ const PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = self.MultiPageSte
|
||||
signupMethod: 'phone',
|
||||
phoneSignupReloginAfterBindEmailEnabled: true,
|
||||
}) || PLUS_PAYPAL_PHONE_STEP_DEFINITIONS;
|
||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal-hosted',
|
||||
}) || PLUS_PAYPAL_STEP_DEFINITIONS;
|
||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal-hosted',
|
||||
plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION,
|
||||
}) || PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS;
|
||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal-hosted',
|
||||
plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION,
|
||||
}) || PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS;
|
||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal-hosted',
|
||||
signupMethod: 'phone',
|
||||
}) || PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS;
|
||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal-hosted',
|
||||
signupMethod: 'phone',
|
||||
phoneSignupReloginAfterBindEmailEnabled: true,
|
||||
}) || PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS;
|
||||
const PLUS_GOPAY_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
|
||||
activeFlowId: DEFAULT_ACTIVE_FLOW_ID,
|
||||
plusModeEnabled: true,
|
||||
@@ -214,6 +244,11 @@ const ALL_STEP_DEFINITIONS = (() => {
|
||||
...PLUS_PAYPAL_CPA_SESSION_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_PHONE_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
|
||||
...PLUS_GOPAY_STEP_DEFINITIONS,
|
||||
...PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS,
|
||||
...PLUS_GOPAY_CPA_SESSION_STEP_DEFINITIONS,
|
||||
@@ -243,6 +278,10 @@ const PLUS_PAYPAL_STEP_IDS = PLUS_PAYPAL_STEP_DEFINITIONS
|
||||
.map((definition) => Number(definition?.id))
|
||||
.filter(Number.isFinite)
|
||||
.sort((left, right) => left - right);
|
||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_IDS = PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS
|
||||
.map((definition) => Number(definition?.id))
|
||||
.filter(Number.isFinite)
|
||||
.sort((left, right) => left - right);
|
||||
const PLUS_GOPAY_STEP_IDS = PLUS_GOPAY_STEP_DEFINITIONS
|
||||
.map((definition) => Number(definition?.id))
|
||||
.filter(Number.isFinite)
|
||||
@@ -255,6 +294,7 @@ const PLUS_STEP_IDS = PLUS_PAYPAL_STEP_IDS;
|
||||
const LAST_STEP_ID = Math.max(
|
||||
NORMAL_STEP_IDS[NORMAL_STEP_IDS.length - 1] || 10,
|
||||
PLUS_PAYPAL_STEP_IDS[PLUS_PAYPAL_STEP_IDS.length - 1] || 10,
|
||||
PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_IDS[PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_IDS.length - 1] || 10,
|
||||
PLUS_GOPAY_STEP_IDS[PLUS_GOPAY_STEP_IDS.length - 1] || 10,
|
||||
PLUS_GPC_STEP_IDS[PLUS_GPC_STEP_IDS.length - 1] || 10
|
||||
);
|
||||
@@ -681,9 +721,11 @@ const HERO_SMS_COUNTRY_BY_PHONE_PREFIX = Object.freeze([
|
||||
]);
|
||||
const FIVE_SIM_OPERATOR = DEFAULT_FIVE_SIM_OPERATOR;
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL_HOSTED;
|
||||
const DEFAULT_PLUS_HOSTED_CHECKOUT_OAUTH_DELAY_SECONDS = 3;
|
||||
const DISPLAY_TIMEZONE = 'Asia/Shanghai';
|
||||
const MICROSOFT_TOKEN_DNR_RULE_ID = 1001;
|
||||
const PERSISTENT_ALIAS_STATE_KEYS = [
|
||||
@@ -781,6 +823,12 @@ function isPlusModeState(state = {}) {
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
const paypalHostedValue = typeof PLUS_PAYMENT_METHOD_PAYPAL_HOSTED !== 'undefined'
|
||||
? PLUS_PAYMENT_METHOD_PAYPAL_HOSTED
|
||||
: 'paypal-hosted';
|
||||
if (normalized === paypalHostedValue || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
|
||||
return paypalHostedValue;
|
||||
}
|
||||
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||||
return PLUS_PAYMENT_METHOD_GPC_HELPER;
|
||||
}
|
||||
@@ -937,6 +985,20 @@ function getStepDefinitionsForState(state = {}) {
|
||||
}
|
||||
return PLUS_GOPAY_STEP_DEFINITIONS;
|
||||
}
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) {
|
||||
if (signupMethod === SIGNUP_METHOD_PHONE) {
|
||||
return Boolean(resolvedState?.phoneSignupReloginAfterBindEmailEnabled)
|
||||
? PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS
|
||||
: PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS;
|
||||
}
|
||||
if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
|
||||
return PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS;
|
||||
}
|
||||
if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) {
|
||||
return PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS;
|
||||
}
|
||||
return PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS;
|
||||
}
|
||||
if (
|
||||
signupMethod === SIGNUP_METHOD_EMAIL
|
||||
&& plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION
|
||||
@@ -967,6 +1029,9 @@ function getStepIdsForState(state = {}) {
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||||
return PLUS_GPC_STEP_IDS;
|
||||
}
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) {
|
||||
return PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_IDS;
|
||||
}
|
||||
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_IDS : PLUS_PAYPAL_STEP_IDS;
|
||||
}
|
||||
|
||||
@@ -1151,6 +1216,9 @@ const PERSISTED_SETTING_DEFAULTS = {
|
||||
plusModeEnabled: false,
|
||||
plusPaymentMethod: DEFAULT_PLUS_PAYMENT_METHOD,
|
||||
plusAccountAccessStrategy: 'oauth',
|
||||
hostedCheckoutVerificationUrl: '',
|
||||
hostedCheckoutPhoneNumber: '',
|
||||
plusHostedCheckoutOauthDelaySeconds: DEFAULT_PLUS_HOSTED_CHECKOUT_OAUTH_DELAY_SECONDS,
|
||||
paypalEmail: '',
|
||||
paypalPassword: '',
|
||||
currentPayPalAccountId: '',
|
||||
@@ -1325,6 +1393,9 @@ const SETTINGS_SCHEMA_VIEW_KEYS = Object.freeze([
|
||||
'plusModeEnabled',
|
||||
'plusPaymentMethod',
|
||||
'plusAccountAccessStrategy',
|
||||
'hostedCheckoutVerificationUrl',
|
||||
'hostedCheckoutPhoneNumber',
|
||||
'plusHostedCheckoutOauthDelaySeconds',
|
||||
'mailProvider',
|
||||
'ipProxyEnabled',
|
||||
'ipProxyService',
|
||||
@@ -1892,6 +1963,12 @@ function normalizePlusPaymentMethod(value = '') {
|
||||
return rootScope.GoPayUtils.normalizePlusPaymentMethod(value);
|
||||
}
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
const paypalHostedValue = typeof PLUS_PAYMENT_METHOD_PAYPAL_HOSTED !== 'undefined'
|
||||
? PLUS_PAYMENT_METHOD_PAYPAL_HOSTED
|
||||
: 'paypal-hosted';
|
||||
if (normalized === paypalHostedValue || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
|
||||
return paypalHostedValue;
|
||||
}
|
||||
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||||
return PLUS_PAYMENT_METHOD_GPC_HELPER;
|
||||
}
|
||||
@@ -3084,6 +3161,14 @@ function normalizePersistentSettingValue(key, value) {
|
||||
return normalizePlusPaymentMethod(value);
|
||||
case 'plusAccountAccessStrategy':
|
||||
return normalizePlusAccountAccessStrategy(value);
|
||||
case 'hostedCheckoutVerificationUrl':
|
||||
return String(value || '').trim();
|
||||
case 'hostedCheckoutPhoneNumber':
|
||||
return String(value || '').trim();
|
||||
case 'plusHostedCheckoutOauthDelaySeconds': {
|
||||
const numeric = Number(value);
|
||||
return Math.min(120, Math.max(0, Math.floor(Number.isFinite(numeric) ? numeric : DEFAULT_PLUS_HOSTED_CHECKOUT_OAUTH_DELAY_SECONDS)));
|
||||
}
|
||||
case 'paypalEmail':
|
||||
return String(value || '').trim();
|
||||
case 'paypalPassword':
|
||||
@@ -13358,6 +13443,7 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c
|
||||
createAutomationTab,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
fetch: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
|
||||
getState,
|
||||
markCurrentRegistrationAccountUsed,
|
||||
registerTab,
|
||||
sendTabMessageUntilStopped,
|
||||
@@ -13365,6 +13451,7 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c
|
||||
sleepWithStop,
|
||||
throwIfStopped,
|
||||
waitForTabCompleteUntilStopped,
|
||||
waitForTabUrlMatchUntilStopped,
|
||||
});
|
||||
const plusCheckoutBillingExecutor = self.MultiPageBackgroundPlusCheckoutBilling?.createPlusCheckoutBillingExecutor({
|
||||
addLog,
|
||||
|
||||
@@ -2,14 +2,24 @@
|
||||
root.MultiPageBackgroundPlusCheckoutCreate = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createBackgroundPlusCheckoutCreateModule() {
|
||||
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
|
||||
const PAYPAL_SOURCE = 'paypal-flow';
|
||||
const PLUS_CHECKOUT_ENTRY_URL = 'https://chatgpt.com/';
|
||||
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/plus-checkout.js'];
|
||||
const PAYPAL_INJECT_FILES = ['content/utils.js', 'content/operation-delay.js', 'content/paypal-flow.js'];
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
const HOSTED_CHECKOUT_ADDRESS_ENDPOINT = 'https://www.meiguodizhi.com/api/v1/dz';
|
||||
const HOSTED_CHECKOUT_SUCCESS_URL_PATTERN = /^https:\/\/(?:chatgpt\.com|www\.chatgpt\.com|chat\.openai\.com)\/(?:backend-api\/)?payments\/success(?:[/?#]|$)/i;
|
||||
const HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS = 120000;
|
||||
const HOSTED_CHECKOUT_PAYPAL_TIMEOUT_MS = 10 * 60 * 1000;
|
||||
const HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS = 12;
|
||||
const HOSTED_CHECKOUT_VERIFICATION_POLL_INTERVAL_MS = 5000;
|
||||
const HOSTED_CHECKOUT_DEFAULT_PHONE = '1234567890';
|
||||
|
||||
function createPlusCheckoutCreateExecutor(deps = {}) {
|
||||
const {
|
||||
@@ -19,11 +29,13 @@
|
||||
createAutomationTab = null,
|
||||
ensureContentScriptReadyOnTabUntilStopped,
|
||||
fetch: fetchImpl = null,
|
||||
getState = null,
|
||||
registerTab,
|
||||
sendTabMessageUntilStopped,
|
||||
setState,
|
||||
sleepWithStop,
|
||||
waitForTabCompleteUntilStopped,
|
||||
waitForTabUrlMatchUntilStopped = null,
|
||||
throwIfStopped = () => {},
|
||||
} = deps;
|
||||
|
||||
@@ -41,6 +53,9 @@
|
||||
return rootScope.GoPayUtils.normalizePlusPaymentMethod(value);
|
||||
}
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
|
||||
return PLUS_PAYMENT_METHOD_PAYPAL_HOSTED;
|
||||
}
|
||||
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||||
return PLUS_PAYMENT_METHOD_GPC_HELPER;
|
||||
}
|
||||
@@ -52,6 +67,9 @@
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||||
return 'GPC 订阅页';
|
||||
}
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) {
|
||||
return 'PayPal 无卡直绑';
|
||||
}
|
||||
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay 订阅页' : 'Plus Checkout';
|
||||
}
|
||||
|
||||
@@ -60,6 +78,9 @@
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||||
return 'GPC';
|
||||
}
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) {
|
||||
return 'PayPal 无卡直绑';
|
||||
}
|
||||
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay' : 'PayPal';
|
||||
}
|
||||
|
||||
@@ -77,6 +98,444 @@
|
||||
return tabId;
|
||||
}
|
||||
|
||||
function isPayPalUrl(url = '') {
|
||||
return /paypal\./i.test(String(url || ''));
|
||||
}
|
||||
|
||||
function isHostedCheckoutSuccessUrl(url = '') {
|
||||
return HOSTED_CHECKOUT_SUCCESS_URL_PATTERN.test(String(url || ''));
|
||||
}
|
||||
|
||||
function isHostedOpenAiCheckoutUrl(url = '') {
|
||||
return /^https:\/\/(?:pay\.openai\.com|checkout\.stripe\.com)\/c\/pay(?:\/|$)/i.test(String(url || ''));
|
||||
}
|
||||
|
||||
async function waitForUrlMatch(tabId, matcher, timeoutMs = 30000, retryDelayMs = 500) {
|
||||
const deadline = Date.now() + Math.max(1000, Number(timeoutMs) || 30000);
|
||||
while (Date.now() < deadline) {
|
||||
throwIfStopped();
|
||||
const tab = await chrome?.tabs?.get?.(tabId).catch(() => null);
|
||||
if (!tab) {
|
||||
return null;
|
||||
}
|
||||
if (matcher(tab.url || '', tab)) {
|
||||
return tab;
|
||||
}
|
||||
await sleepWithStop(retryDelayMs);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function getHostedCheckoutRuntimeConfig(state = {}) {
|
||||
const latestState = typeof getState === 'function' ? await getState().catch(() => ({})) : {};
|
||||
return {
|
||||
verificationUrl: String(
|
||||
state?.hostedCheckoutVerificationUrl
|
||||
|| latestState?.hostedCheckoutVerificationUrl
|
||||
|| ''
|
||||
).trim(),
|
||||
phone: String(
|
||||
state?.hostedCheckoutPhoneNumber
|
||||
|| latestState?.hostedCheckoutPhoneNumber
|
||||
|| HOSTED_CHECKOUT_DEFAULT_PHONE
|
||||
).trim(),
|
||||
oauthDelaySeconds: normalizeHostedCheckoutDelaySeconds(
|
||||
state?.plusHostedCheckoutOauthDelaySeconds
|
||||
?? latestState?.plusHostedCheckoutOauthDelaySeconds
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHostedCheckoutDelaySeconds(value) {
|
||||
const numeric = Number(value);
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return 3;
|
||||
}
|
||||
return Math.min(120, Math.max(0, Math.floor(numeric)));
|
||||
}
|
||||
|
||||
async function fetchHostedCheckoutAddress() {
|
||||
const { response, data } = await fetchJsonWithTimeout(HOSTED_CHECKOUT_ADDRESS_ENDPOINT, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ path: '/', method: 'address' }),
|
||||
}, 30000);
|
||||
if (!response?.ok) {
|
||||
throw new Error(`获取无卡直绑地址失败(HTTP ${response?.status || 0})。`);
|
||||
}
|
||||
const address = data?.address || data || {};
|
||||
return {
|
||||
street: String(address.Address || address.street || '123 Main St').trim(),
|
||||
city: String(address.City || address.city || 'New York').trim(),
|
||||
state: String(address.State_Full || address.State || address.state || 'New York').trim(),
|
||||
zip: String(address.Zip_Code || address.zip || '10001').trim().slice(0, 5) || '10001',
|
||||
};
|
||||
}
|
||||
|
||||
function buildRandomHostedEmail() {
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let localPart = '';
|
||||
for (let index = 0; index < 16; index += 1) {
|
||||
localPart += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
return `${localPart}@gmail.com`;
|
||||
}
|
||||
|
||||
function buildRandomHostedPassword() {
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^';
|
||||
let value = 'Aa1!';
|
||||
while (value.length < 14) {
|
||||
value += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function buildHostedVisaCard() {
|
||||
const digits = [4, 1, 4, 7];
|
||||
while (digits.length < 15) {
|
||||
digits.push(Math.floor(Math.random() * 10));
|
||||
}
|
||||
const reversed = digits.slice().reverse();
|
||||
let sum = 0;
|
||||
for (let index = 0; index < reversed.length; index += 1) {
|
||||
let digit = reversed[index];
|
||||
if (index % 2 === 0) {
|
||||
digit *= 2;
|
||||
if (digit > 9) digit -= 9;
|
||||
}
|
||||
sum += digit;
|
||||
}
|
||||
digits.push((10 - (sum % 10)) % 10);
|
||||
const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
|
||||
const year = (new Date().getFullYear() % 100) + 3;
|
||||
return {
|
||||
number: digits.join(''),
|
||||
expiry: `${month} / ${year}`,
|
||||
cvv: String(Math.floor(100 + Math.random() * 900)),
|
||||
};
|
||||
}
|
||||
|
||||
function buildHostedGuestProfile(address = {}, config = {}) {
|
||||
const card = buildHostedVisaCard();
|
||||
return {
|
||||
email: buildRandomHostedEmail(),
|
||||
password: buildRandomHostedPassword(),
|
||||
phone: String(config?.phone || HOSTED_CHECKOUT_DEFAULT_PHONE).trim(),
|
||||
firstName: 'James',
|
||||
lastName: 'Smith',
|
||||
cardNumber: card.number,
|
||||
cardExpiry: card.expiry,
|
||||
cardCvv: card.cvv,
|
||||
address,
|
||||
};
|
||||
}
|
||||
|
||||
function extractHostedVerificationCode(payload = '') {
|
||||
const candidates = [payload?.data, payload?.code, payload?.text, payload?.message, payload];
|
||||
for (const candidate of candidates) {
|
||||
const text = typeof candidate === 'object' ? JSON.stringify(candidate) : String(candidate || '');
|
||||
const match = text.match(/\d{6}/);
|
||||
if (match) {
|
||||
return match[0];
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function fetchHostedVerificationCode(verificationUrl = '') {
|
||||
const url = String(verificationUrl || '').trim();
|
||||
if (!url) {
|
||||
throw new Error('未配置 PayPal 无卡直绑验证码接口。');
|
||||
}
|
||||
const fetcher = typeof fetchImpl === 'function'
|
||||
? fetchImpl
|
||||
: (typeof fetch === 'function' ? fetch.bind(globalThis) : null);
|
||||
if (typeof fetcher !== 'function') {
|
||||
throw new Error('当前运行环境不支持 fetch,无法获取无卡直绑验证码。');
|
||||
}
|
||||
const separator = url.includes('?') ? '&' : '?';
|
||||
const response = await fetcher(`${url}${separator}t=${Date.now()}`, {
|
||||
method: 'GET',
|
||||
headers: { Accept: 'application/json,text/plain,*/*' },
|
||||
});
|
||||
const text = await response.text().catch(() => '');
|
||||
let payload = text;
|
||||
try {
|
||||
payload = text ? JSON.parse(text) : {};
|
||||
} catch {
|
||||
payload = text;
|
||||
}
|
||||
const code = extractHostedVerificationCode(payload);
|
||||
if (!code) {
|
||||
throw new Error('验证码接口暂未返回有效 6 位验证码。');
|
||||
}
|
||||
return code;
|
||||
}
|
||||
|
||||
async function pollHostedVerificationCode(verificationUrl = '') {
|
||||
let lastError = null;
|
||||
for (let attempt = 1; attempt <= HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS; attempt += 1) {
|
||||
throwIfStopped();
|
||||
try {
|
||||
const code = await fetchHostedVerificationCode(verificationUrl);
|
||||
await addLog(`步骤 6:已获取无卡直绑验证码(${attempt}/${HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS})。`, 'info');
|
||||
return code;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
await addLog(`步骤 6:无卡直绑验证码暂不可用(${attempt}/${HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS}):${error?.message || error}`, 'warn');
|
||||
if (attempt < HOSTED_CHECKOUT_VERIFICATION_POLL_ATTEMPTS) {
|
||||
await sleepWithStop(HOSTED_CHECKOUT_VERIFICATION_POLL_INTERVAL_MS);
|
||||
}
|
||||
}
|
||||
}
|
||||
throw lastError || new Error('无卡直绑验证码轮询失败。');
|
||||
}
|
||||
|
||||
async function runHostedOpenAiCheckout(tabId, profile, config) {
|
||||
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
|
||||
inject: PLUS_CHECKOUT_INJECT_FILES,
|
||||
injectSource: PLUS_CHECKOUT_SOURCE,
|
||||
logMessage: '步骤 6:正在等待 OpenAI hosted checkout 脚本就绪...',
|
||||
});
|
||||
const firstResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
|
||||
type: 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP',
|
||||
source: 'background',
|
||||
payload: { address: profile.address },
|
||||
});
|
||||
if (firstResult?.error) {
|
||||
throw new Error(firstResult.error);
|
||||
}
|
||||
|
||||
const deadline = Date.now() + HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS;
|
||||
let verificationSubmitted = false;
|
||||
while (Date.now() < deadline) {
|
||||
throwIfStopped();
|
||||
const tab = await chrome?.tabs?.get?.(tabId).catch(() => null);
|
||||
if (!tab) {
|
||||
throw new Error('步骤 6:无卡直绑 checkout 标签页已关闭。');
|
||||
}
|
||||
const currentUrl = String(tab.url || '').trim();
|
||||
if (isPayPalUrl(currentUrl) || isHostedCheckoutSuccessUrl(currentUrl)) {
|
||||
return currentUrl;
|
||||
}
|
||||
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
|
||||
inject: PLUS_CHECKOUT_INJECT_FILES,
|
||||
injectSource: PLUS_CHECKOUT_SOURCE,
|
||||
});
|
||||
const pageState = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
|
||||
type: 'PLUS_CHECKOUT_GET_STATE',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
if (pageState?.error) {
|
||||
throw new Error(pageState.error);
|
||||
}
|
||||
if (pageState?.hostedVerificationVisible && !verificationSubmitted) {
|
||||
const verificationCode = await pollHostedVerificationCode(config.verificationUrl);
|
||||
const verifyResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
|
||||
type: 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP',
|
||||
source: 'background',
|
||||
payload: { verificationCode },
|
||||
});
|
||||
if (verifyResult?.error) {
|
||||
throw new Error(verifyResult.error);
|
||||
}
|
||||
verificationSubmitted = true;
|
||||
}
|
||||
await sleepWithStop(500);
|
||||
}
|
||||
throw new Error('步骤 6:OpenAI hosted checkout 长时间未跳转到 PayPal 或支付成功页。');
|
||||
}
|
||||
|
||||
async function getHostedPayPalState(tabId) {
|
||||
await waitForTabCompleteUntilStopped(tabId);
|
||||
await ensureContentScriptReadyOnTabUntilStopped(PAYPAL_SOURCE, tabId, {
|
||||
inject: PAYPAL_INJECT_FILES,
|
||||
injectSource: PAYPAL_SOURCE,
|
||||
logMessage: '步骤 6:正在等待 PayPal 无卡直绑页面脚本就绪...',
|
||||
});
|
||||
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||
type: 'PAYPAL_HOSTED_GET_STATE',
|
||||
source: 'background',
|
||||
payload: {},
|
||||
});
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function runHostedPayPalStep(tabId, payload = {}) {
|
||||
await waitForTabCompleteUntilStopped(tabId);
|
||||
await ensureContentScriptReadyOnTabUntilStopped(PAYPAL_SOURCE, tabId, {
|
||||
inject: PAYPAL_INJECT_FILES,
|
||||
injectSource: PAYPAL_SOURCE,
|
||||
logMessage: '步骤 6:正在等待 PayPal 无卡直绑页面脚本就绪...',
|
||||
});
|
||||
const result = await sendTabMessageUntilStopped(tabId, PAYPAL_SOURCE, {
|
||||
type: 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP',
|
||||
source: 'background',
|
||||
payload,
|
||||
});
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
return result || {};
|
||||
}
|
||||
|
||||
async function runHostedPayPalCheckout(tabId, profile, config) {
|
||||
const deadline = Date.now() + HOSTED_CHECKOUT_PAYPAL_TIMEOUT_MS;
|
||||
while (Date.now() < deadline) {
|
||||
throwIfStopped();
|
||||
const tab = await chrome?.tabs?.get?.(tabId).catch(() => null);
|
||||
if (!tab) {
|
||||
throw new Error('步骤 6:PayPal 无卡直绑标签页已关闭。');
|
||||
}
|
||||
const currentUrl = String(tab.url || '').trim();
|
||||
if (isHostedCheckoutSuccessUrl(currentUrl)) {
|
||||
await addLog('步骤 6:PayPal 无卡直绑已回到 ChatGPT 支付成功页。', 'ok');
|
||||
return currentUrl;
|
||||
}
|
||||
if (!isPayPalUrl(currentUrl)) {
|
||||
await addLog(`步骤 6:无卡直绑已离开 PayPal(${currentUrl || '空 URL'}),继续等待支付成功页。`, 'info');
|
||||
await sleepWithStop(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
const pageState = await getHostedPayPalState(tabId);
|
||||
if (pageState.hostedStage === 'verification' && pageState.verificationInputsVisible) {
|
||||
const verificationCode = await pollHostedVerificationCode(config.verificationUrl);
|
||||
await runHostedPayPalStep(tabId, { verificationCode });
|
||||
await sleepWithStop(1000);
|
||||
continue;
|
||||
}
|
||||
if (pageState.hostedStage === 'pay_login') {
|
||||
await addLog('步骤 6:正在填写 PayPal 无卡直绑登录邮箱。', 'info');
|
||||
await runHostedPayPalStep(tabId, { email: profile.email });
|
||||
await sleepWithStop(1000);
|
||||
continue;
|
||||
}
|
||||
if (pageState.hostedStage === 'guest_checkout') {
|
||||
await addLog('步骤 6:正在填写 PayPal 无卡直绑卡支付资料。', 'info');
|
||||
await runHostedPayPalStep(tabId, {
|
||||
...profile,
|
||||
phone: config.phone || profile.phone,
|
||||
});
|
||||
await sleepWithStop(1500);
|
||||
continue;
|
||||
}
|
||||
if (pageState.hostedStage === 'review_consent') {
|
||||
await addLog('步骤 6:正在确认 PayPal 无卡直绑账单复核页。', 'info');
|
||||
await runHostedPayPalStep(tabId, {});
|
||||
await sleepWithStop(1000);
|
||||
continue;
|
||||
}
|
||||
if (pageState.hostedStage === 'approval') {
|
||||
throw new Error('步骤 6:无卡直绑流程进入普通 PayPal 授权页,请检查支付方式是否选择正确。');
|
||||
}
|
||||
await sleepWithStop(1000);
|
||||
}
|
||||
throw new Error('步骤 6:PayPal 无卡直绑自动化超时,未等到支付成功页。');
|
||||
}
|
||||
|
||||
async function waitHostedSuccessDelayAndComplete(tabId, state = {}, successUrl = '') {
|
||||
const config = await getHostedCheckoutRuntimeConfig(state);
|
||||
if (config.oauthDelaySeconds > 0) {
|
||||
await addLog(`步骤 6:支付成功后等待 ${config.oauthDelaySeconds} 秒,再继续账号接入。`, 'info');
|
||||
await sleepWithStop(config.oauthDelaySeconds * 1000);
|
||||
}
|
||||
await completeNodeFromBackground('plus-checkout-create', {
|
||||
plusReturnUrl: successUrl,
|
||||
plusHostedCheckoutCompleted: true,
|
||||
plusHostedCheckoutOauthDelaySeconds: config.oauthDelaySeconds,
|
||||
});
|
||||
}
|
||||
|
||||
function resolveCheckoutTargetUrl(result = {}, paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) {
|
||||
return String(
|
||||
result?.preferredCheckoutUrl
|
||||
|| result?.hostedCheckoutUrl
|
||||
|| result?.checkoutUrl
|
||||
|| ''
|
||||
).trim();
|
||||
}
|
||||
return String(result?.checkoutUrl || '').trim();
|
||||
}
|
||||
|
||||
async function executeHostedCheckoutCreate(tabId, state = {}, result = {}) {
|
||||
const targetCheckoutUrl = resolveCheckoutTargetUrl(result, PLUS_PAYMENT_METHOD_PAYPAL_HOSTED);
|
||||
if (!targetCheckoutUrl) {
|
||||
throw new Error('步骤 6:PayPal 无卡直绑未返回可用的订阅链接。');
|
||||
}
|
||||
|
||||
await addLog('步骤 6:PayPal 无卡直绑链接已创建,正在打开 hosted checkout 页面...', 'ok');
|
||||
await chrome.tabs.update(tabId, { url: targetCheckoutUrl, active: true });
|
||||
await waitForTabCompleteUntilStopped(tabId);
|
||||
|
||||
const landedTab = await waitForUrlMatch(
|
||||
tabId,
|
||||
(url) => isHostedOpenAiCheckoutUrl(url) || isPayPalUrl(url) || isHostedCheckoutSuccessUrl(url),
|
||||
HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS,
|
||||
500
|
||||
);
|
||||
const landedUrl = String(landedTab?.url || targetCheckoutUrl || '').trim();
|
||||
|
||||
await setState({
|
||||
plusCheckoutTabId: tabId,
|
||||
plusCheckoutUrl: landedUrl,
|
||||
plusCheckoutCountry: result.country || 'US',
|
||||
plusCheckoutCurrency: result.currency || 'USD',
|
||||
plusCheckoutSource: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED,
|
||||
plusReturnUrl: '',
|
||||
});
|
||||
|
||||
await addLog(`步骤 6:PayPal 无卡直绑页面已就绪(${result.country || 'US'} ${result.currency || 'USD'}),开始自动完成直绑支付链路。`, 'info');
|
||||
|
||||
if (isHostedCheckoutSuccessUrl(landedUrl)) {
|
||||
await waitHostedSuccessDelayAndComplete(tabId, state, landedUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
const config = await getHostedCheckoutRuntimeConfig(state);
|
||||
const address = await fetchHostedCheckoutAddress();
|
||||
const guestProfile = buildHostedGuestProfile(address, config);
|
||||
let transitionUrl = landedUrl;
|
||||
|
||||
if (isHostedOpenAiCheckoutUrl(transitionUrl)) {
|
||||
transitionUrl = await runHostedOpenAiCheckout(tabId, guestProfile, config);
|
||||
}
|
||||
|
||||
if (isHostedCheckoutSuccessUrl(transitionUrl)) {
|
||||
await waitHostedSuccessDelayAndComplete(tabId, state, transitionUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPayPalUrl(transitionUrl)) {
|
||||
const transitionTab = await waitForUrlMatch(
|
||||
tabId,
|
||||
(url) => isPayPalUrl(url) || isHostedCheckoutSuccessUrl(url),
|
||||
HOSTED_CHECKOUT_TRANSITION_TIMEOUT_MS,
|
||||
500
|
||||
);
|
||||
transitionUrl = String(transitionTab?.url || '').trim();
|
||||
}
|
||||
|
||||
if (isHostedCheckoutSuccessUrl(transitionUrl)) {
|
||||
await waitHostedSuccessDelayAndComplete(tabId, state, transitionUrl);
|
||||
return;
|
||||
}
|
||||
if (!isPayPalUrl(transitionUrl)) {
|
||||
throw new Error('步骤 6:PayPal 无卡直绑提交后未进入 PayPal 或支付成功页。');
|
||||
}
|
||||
|
||||
const successUrl = await runHostedPayPalCheckout(tabId, guestProfile, config);
|
||||
await waitHostedSuccessDelayAndComplete(tabId, state, successUrl);
|
||||
}
|
||||
|
||||
function normalizeHelperCountryCode(countryCode = '86') {
|
||||
const digits = String(countryCode || '').replace(/\D/g, '');
|
||||
return digits || '86';
|
||||
@@ -493,12 +952,18 @@
|
||||
if (result?.error) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
if (!result?.checkoutUrl) {
|
||||
const targetCheckoutUrl = resolveCheckoutTargetUrl(result, paymentMethod);
|
||||
if (!targetCheckoutUrl) {
|
||||
throw new Error(`步骤 6:${checkoutModeLabel}未返回可用的订阅链接。`);
|
||||
}
|
||||
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) {
|
||||
await executeHostedCheckoutCreate(tabId, state, result);
|
||||
return;
|
||||
}
|
||||
|
||||
await addLog(`步骤 6:${checkoutModeLabel}已创建,正在打开订阅页面...`, 'ok');
|
||||
await chrome.tabs.update(tabId, { url: result.checkoutUrl, active: true });
|
||||
await chrome.tabs.update(tabId, { url: targetCheckoutUrl, active: true });
|
||||
await waitForTabCompleteUntilStopped(tabId);
|
||||
await sleepWithStop(1000);
|
||||
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
|
||||
@@ -509,7 +974,7 @@
|
||||
|
||||
await setState({
|
||||
plusCheckoutTabId: tabId,
|
||||
plusCheckoutUrl: result.checkoutUrl,
|
||||
plusCheckoutUrl: targetCheckoutUrl,
|
||||
plusCheckoutCountry: result.country || 'DE',
|
||||
plusCheckoutCurrency: result.currency || 'EUR',
|
||||
plusCheckoutSource: '',
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
console.log('[MultiPage:paypal-flow] Content script loaded on', location.href);
|
||||
|
||||
const PAYPAL_FLOW_LISTENER_SENTINEL = 'data-multipage-paypal-flow-listener';
|
||||
const PAYPAL_HOSTED_DEFAULT_PHONE = '1234567890';
|
||||
const PAYPAL_HOSTED_STAGE_OUTSIDE = 'outside_paypal';
|
||||
const PAYPAL_HOSTED_STAGE_LOGIN = 'pay_login';
|
||||
const PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT = 'guest_checkout';
|
||||
const PAYPAL_HOSTED_STAGE_VERIFICATION = 'verification';
|
||||
const PAYPAL_HOSTED_STAGE_REVIEW = 'review_consent';
|
||||
const PAYPAL_HOSTED_STAGE_APPROVAL = 'approval';
|
||||
const PAYPAL_HOSTED_STAGE_UNKNOWN = 'unknown';
|
||||
|
||||
if (document.documentElement.getAttribute(PAYPAL_FLOW_LISTENER_SENTINEL) !== '1') {
|
||||
document.documentElement.setAttribute(PAYPAL_FLOW_LISTENER_SENTINEL, '1');
|
||||
@@ -13,6 +21,8 @@ if (document.documentElement.getAttribute(PAYPAL_FLOW_LISTENER_SENTINEL) !== '1'
|
||||
|| message.type === 'PAYPAL_SUBMIT_LOGIN'
|
||||
|| message.type === 'PAYPAL_DISMISS_PROMPTS'
|
||||
|| message.type === 'PAYPAL_CLICK_APPROVE'
|
||||
|| message.type === 'PAYPAL_HOSTED_GET_STATE'
|
||||
|| message.type === 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP'
|
||||
) {
|
||||
resetStopState();
|
||||
handlePayPalCommand(message).then((result) => {
|
||||
@@ -47,6 +57,10 @@ async function handlePayPalCommand(message) {
|
||||
return dismissPayPalPrompts();
|
||||
case 'PAYPAL_CLICK_APPROVE':
|
||||
return clickPayPalApprove();
|
||||
case 'PAYPAL_HOSTED_GET_STATE':
|
||||
return inspectPayPalHostedState();
|
||||
case 'PAYPAL_RUN_HOSTED_CHECKOUT_STEP':
|
||||
return runPayPalHostedCheckoutStep(message.payload || {});
|
||||
default:
|
||||
throw new Error(`paypal-flow.js 不处理消息:${message.type}`);
|
||||
}
|
||||
@@ -223,6 +237,302 @@ function findApproveButton() {
|
||||
]);
|
||||
}
|
||||
|
||||
function getPayPalPathname() {
|
||||
return String(location?.pathname || '').trim();
|
||||
}
|
||||
|
||||
function findHostedVerificationInputs() {
|
||||
return Array.from({ length: 6 }, (_, index) => document.getElementById(`ci-ciBasic-${index}`))
|
||||
.filter((input) => input && isVisibleElement(input) && isEnabledControl(input));
|
||||
}
|
||||
|
||||
function hasHostedVerificationInputs() {
|
||||
return findHostedVerificationInputs().length >= 6;
|
||||
}
|
||||
|
||||
function isHostedLoginPage() {
|
||||
return getPayPalPathname() === '/pay' || Boolean(document.getElementById('email'));
|
||||
}
|
||||
|
||||
function isHostedGuestCheckoutPage() {
|
||||
return /\/checkoutweb\//i.test(getPayPalPathname())
|
||||
|| Boolean(document.getElementById('cardNumber'))
|
||||
|| Boolean(document.getElementById('billingLine1'));
|
||||
}
|
||||
|
||||
function isHostedReviewPage() {
|
||||
return /\/webapps\/hermes/i.test(getPayPalPathname());
|
||||
}
|
||||
|
||||
function findHostedReviewConsentButton() {
|
||||
const direct = document.getElementById('consentButton')
|
||||
|| document.querySelector('button[data-testid="consentButton"]');
|
||||
if (direct && isVisibleElement(direct) && isEnabledControl(direct)) {
|
||||
return direct;
|
||||
}
|
||||
return findClickableByText([
|
||||
/agree\s*(?:and)?\s*continue|accept|continue/i,
|
||||
/同意并继续|同意|继续/i,
|
||||
]);
|
||||
}
|
||||
|
||||
function detectPayPalHostedStage() {
|
||||
if (!/paypal\./i.test(String(location?.host || ''))) {
|
||||
return PAYPAL_HOSTED_STAGE_OUTSIDE;
|
||||
}
|
||||
if (hasHostedVerificationInputs()) {
|
||||
return PAYPAL_HOSTED_STAGE_VERIFICATION;
|
||||
}
|
||||
if (isHostedGuestCheckoutPage()) {
|
||||
return PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT;
|
||||
}
|
||||
if (isHostedReviewPage() && findHostedReviewConsentButton()) {
|
||||
return PAYPAL_HOSTED_STAGE_REVIEW;
|
||||
}
|
||||
if (isHostedLoginPage()) {
|
||||
return PAYPAL_HOSTED_STAGE_LOGIN;
|
||||
}
|
||||
return findApproveButton() ? PAYPAL_HOSTED_STAGE_APPROVAL : PAYPAL_HOSTED_STAGE_UNKNOWN;
|
||||
}
|
||||
|
||||
function fillHostedInputById(id, value) {
|
||||
const input = document.getElementById(String(id || '').trim());
|
||||
if (!input || !isVisibleElement(input) || !isEnabledControl(input)) {
|
||||
return false;
|
||||
}
|
||||
fillInput(input, String(value || ''));
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectHostedOptionByIdText(id, value) {
|
||||
const select = document.getElementById(String(id || '').trim());
|
||||
const expected = normalizeText(value).toLowerCase();
|
||||
if (!select || !expected) {
|
||||
return false;
|
||||
}
|
||||
const option = Array.from(select.options || []).find((item) => {
|
||||
const optionText = normalizeText(item.textContent || item.label || '').toLowerCase();
|
||||
const optionValue = normalizeText(item.value || '').toLowerCase();
|
||||
return optionText.includes(expected) || optionValue.includes(expected);
|
||||
});
|
||||
if (!option) {
|
||||
return false;
|
||||
}
|
||||
select.value = option.value;
|
||||
select.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
function findHostedSubmitButton() {
|
||||
return document.querySelector('button[data-testid="submit-button"]')
|
||||
|| document.querySelector('button[data-testid="hosted-payment-submit-button"]')
|
||||
|| document.querySelector('button[data-atomic-wait-intent="Submit_Email"]')
|
||||
|| document.querySelector('button.SubmitButton--complete')
|
||||
|| findEmailNextButton()
|
||||
|| findLoginNextButton()
|
||||
|| findClickableByText([
|
||||
/pay|continue|next|agree|subscribe/i,
|
||||
/支付|继续|下一步|同意|订阅/i,
|
||||
]);
|
||||
}
|
||||
|
||||
async function clickHostedSubmitButton() {
|
||||
const button = await waitUntil(() => {
|
||||
const candidate = findHostedSubmitButton();
|
||||
return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
|
||||
}, {
|
||||
intervalMs: 500,
|
||||
timeoutMs: 15000,
|
||||
timeoutMessage: 'PayPal hosted checkout 未找到可点击的继续/提交按钮。',
|
||||
});
|
||||
await performPayPalOperationWithDelay({ stepKey: 'plus-checkout-create', kind: 'click', label: 'hosted-paypal-submit' }, async () => {
|
||||
simulateClick(button);
|
||||
});
|
||||
await sleep(1000);
|
||||
return {
|
||||
clicked: true,
|
||||
buttonText: getActionText(button),
|
||||
verificationRequired: hasHostedVerificationInputs(),
|
||||
};
|
||||
}
|
||||
|
||||
function buildHostedRandomEmail() {
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let value = '';
|
||||
for (let index = 0; index < 16; index += 1) {
|
||||
value += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
return `${value}@gmail.com`;
|
||||
}
|
||||
|
||||
function buildHostedRandomPassword() {
|
||||
const alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^';
|
||||
let value = 'Aa1!';
|
||||
while (value.length < 14) {
|
||||
value += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function buildHostedVisaCard() {
|
||||
const digits = [4, 1, 4, 7];
|
||||
while (digits.length < 15) {
|
||||
digits.push(Math.floor(Math.random() * 10));
|
||||
}
|
||||
const reversed = digits.slice().reverse();
|
||||
let sum = 0;
|
||||
for (let index = 0; index < reversed.length; index += 1) {
|
||||
let digit = reversed[index];
|
||||
if (index % 2 === 0) {
|
||||
digit *= 2;
|
||||
if (digit > 9) digit -= 9;
|
||||
}
|
||||
sum += digit;
|
||||
}
|
||||
digits.push((10 - (sum % 10)) % 10);
|
||||
const month = String(Math.floor(Math.random() * 12) + 1).padStart(2, '0');
|
||||
const year = (new Date().getFullYear() % 100) + 3;
|
||||
return {
|
||||
number: digits.join(''),
|
||||
expiry: `${month} / ${year}`,
|
||||
cvv: String(Math.floor(100 + Math.random() * 900)),
|
||||
};
|
||||
}
|
||||
|
||||
async function submitHostedLogin(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const email = normalizeText(payload.email || buildHostedRandomEmail());
|
||||
const emailInput = document.getElementById('email') || findEmailInput();
|
||||
if (!emailInput) {
|
||||
throw new Error('PayPal hosted checkout 未找到邮箱输入框。');
|
||||
}
|
||||
refillPayPalEmailInput(emailInput, email);
|
||||
const clickResult = await clickHostedSubmitButton();
|
||||
return {
|
||||
stage: PAYPAL_HOSTED_STAGE_LOGIN,
|
||||
submitted: true,
|
||||
generatedEmail: email,
|
||||
verificationRequired: Boolean(clickResult.verificationRequired),
|
||||
};
|
||||
}
|
||||
|
||||
async function fillHostedVerificationCode(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const code = String(payload.verificationCode || payload.code || '').replace(/\D+/g, '').slice(0, 6);
|
||||
if (code.length !== 6) {
|
||||
throw new Error('PayPal hosted checkout 验证码无效。');
|
||||
}
|
||||
const inputs = findHostedVerificationInputs();
|
||||
if (inputs.length < 6) {
|
||||
throw new Error('PayPal hosted checkout 当前页面未显示验证码输入框。');
|
||||
}
|
||||
await performPayPalOperationWithDelay({ stepKey: 'plus-checkout-create', kind: 'fill', label: 'hosted-paypal-verification-code' }, async () => {
|
||||
inputs.forEach((input, index) => fillInput(input, code[index] || ''));
|
||||
});
|
||||
return {
|
||||
stage: PAYPAL_HOSTED_STAGE_VERIFICATION,
|
||||
codeSubmitted: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function fillHostedGuestCheckout(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
const countrySelect = document.getElementById('country');
|
||||
if (countrySelect && String(countrySelect.value || '').trim().toUpperCase() !== 'US') {
|
||||
countrySelect.value = 'US';
|
||||
countrySelect.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
await sleep(1000);
|
||||
}
|
||||
const generatedCard = buildHostedVisaCard();
|
||||
const address = payload.address && typeof payload.address === 'object' ? payload.address : {};
|
||||
const values = {
|
||||
email: normalizeText(payload.email || buildHostedRandomEmail()),
|
||||
phone: normalizeText(payload.phone || PAYPAL_HOSTED_DEFAULT_PHONE),
|
||||
cardNumber: String(payload.cardNumber || generatedCard.number).replace(/\s+/g, ''),
|
||||
cardExpiry: normalizeText(payload.cardExpiry || generatedCard.expiry),
|
||||
cardCvv: normalizeText(payload.cardCvv || generatedCard.cvv),
|
||||
password: String(payload.password || buildHostedRandomPassword()),
|
||||
firstName: normalizeText(payload.firstName || 'James'),
|
||||
lastName: normalizeText(payload.lastName || 'Smith'),
|
||||
};
|
||||
fillHostedInputById('email', values.email);
|
||||
fillHostedInputById('phone', values.phone);
|
||||
fillHostedInputById('cardNumber', values.cardNumber);
|
||||
fillHostedInputById('cardExpiry', values.cardExpiry);
|
||||
fillHostedInputById('cardCvv', values.cardCvv);
|
||||
fillHostedInputById('password', values.password);
|
||||
fillHostedInputById('firstName', values.firstName);
|
||||
fillHostedInputById('lastName', values.lastName);
|
||||
fillHostedInputById('billingLine1', address.street || address.address1 || '');
|
||||
fillHostedInputById('billingCity', address.city || '');
|
||||
fillHostedInputById('billingPostalCode', address.zip || address.postalCode || '');
|
||||
selectHostedOptionByIdText('billingState', address.state || address.region || '');
|
||||
const clickResult = await clickHostedSubmitButton();
|
||||
return {
|
||||
stage: PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT,
|
||||
submitted: true,
|
||||
verificationRequired: Boolean(clickResult.verificationRequired),
|
||||
};
|
||||
}
|
||||
|
||||
async function clickHostedReviewConsent() {
|
||||
await waitForDocumentComplete();
|
||||
const button = await waitUntil(() => {
|
||||
const candidate = findHostedReviewConsentButton();
|
||||
return candidate && isVisibleElement(candidate) && isEnabledControl(candidate) ? candidate : null;
|
||||
}, {
|
||||
intervalMs: 500,
|
||||
timeoutMs: 30000,
|
||||
timeoutMessage: 'PayPal hosted checkout 未找到账单确认按钮。',
|
||||
});
|
||||
await performPayPalOperationWithDelay({ stepKey: 'plus-checkout-create', kind: 'click', label: 'hosted-paypal-review-consent' }, async () => {
|
||||
simulateClick(button);
|
||||
});
|
||||
return {
|
||||
stage: PAYPAL_HOSTED_STAGE_REVIEW,
|
||||
submitted: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function runPayPalHostedCheckoutStep(payload = {}) {
|
||||
const stage = detectPayPalHostedStage();
|
||||
if (stage === PAYPAL_HOSTED_STAGE_VERIFICATION) {
|
||||
return payload.verificationCode || payload.code
|
||||
? fillHostedVerificationCode(payload)
|
||||
: { stage, requiresVerificationCode: true };
|
||||
}
|
||||
if (stage === PAYPAL_HOSTED_STAGE_LOGIN) {
|
||||
return submitHostedLogin(payload);
|
||||
}
|
||||
if (stage === PAYPAL_HOSTED_STAGE_GUEST_CHECKOUT) {
|
||||
return fillHostedGuestCheckout(payload);
|
||||
}
|
||||
if (stage === PAYPAL_HOSTED_STAGE_REVIEW) {
|
||||
return clickHostedReviewConsent();
|
||||
}
|
||||
return {
|
||||
stage,
|
||||
submitted: false,
|
||||
approveReady: Boolean(findApproveButton()),
|
||||
};
|
||||
}
|
||||
|
||||
function inspectPayPalHostedState() {
|
||||
const stage = detectPayPalHostedStage();
|
||||
return {
|
||||
url: location.href,
|
||||
readyState: document.readyState,
|
||||
hostedStage: stage,
|
||||
verificationInputsVisible: hasHostedVerificationInputs(),
|
||||
hasGuestCardFields: Boolean(document.getElementById('cardNumber')),
|
||||
hasHostedEmailInput: Boolean(document.getElementById('email') || findEmailInput()),
|
||||
reviewConsentReady: Boolean(findHostedReviewConsentButton()),
|
||||
approveReady: Boolean(findApproveButton()),
|
||||
bodyTextPreview: normalizeText(document.body?.innerText || '').slice(0, 240),
|
||||
};
|
||||
}
|
||||
|
||||
function findPasskeyPromptButtons() {
|
||||
const promptPatterns = [
|
||||
/passkey|通行密钥|安全密钥|下次登录|faster|save/i,
|
||||
|
||||
+206
-2
@@ -34,6 +34,7 @@ const PLUS_CHECKOUT_CONFIGS = {
|
||||
};
|
||||
const PAYPAL_DIAGNOSTIC_LOG_INTERVAL_MS = 5000;
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PAYMENT_METHOD_CONFIGS = {
|
||||
[PLUS_PAYMENT_METHOD_PAYPAL]: {
|
||||
@@ -47,6 +48,17 @@ const PAYMENT_METHOD_CONFIGS = {
|
||||
},
|
||||
patterns: [/paypal/i],
|
||||
},
|
||||
[PLUS_PAYMENT_METHOD_PAYPAL_HOSTED]: {
|
||||
id: PLUS_PAYMENT_METHOD_PAYPAL_HOSTED,
|
||||
label: 'PayPal 无卡直绑',
|
||||
diagnosticLabel: 'PayPal hosted',
|
||||
checkoutMerchantPath: 'openai_llc',
|
||||
billingDetails: {
|
||||
country: 'US',
|
||||
currency: 'USD',
|
||||
},
|
||||
patterns: [/paypal/i],
|
||||
},
|
||||
[PLUS_PAYMENT_METHOD_GOPAY]: {
|
||||
id: PLUS_PAYMENT_METHOD_GOPAY,
|
||||
label: 'GoPay',
|
||||
@@ -80,6 +92,7 @@ if (document.documentElement.getAttribute(PLUS_CHECKOUT_LISTENER_SENTINEL) !== '
|
||||
|| message.type === 'PLUS_CHECKOUT_SELECT_ADDRESS_SUGGESTION'
|
||||
|| message.type === 'PLUS_CHECKOUT_ENSURE_BILLING_ADDRESS'
|
||||
|| message.type === 'PLUS_CHECKOUT_CLICK_SUBSCRIBE'
|
||||
|| message.type === 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP'
|
||||
|| message.type === 'PLUS_CHECKOUT_GET_STATE'
|
||||
) {
|
||||
resetStopState();
|
||||
@@ -119,6 +132,8 @@ async function handlePlusCheckoutCommand(message) {
|
||||
return ensurePlusStructuredBillingAddress(message.payload || {});
|
||||
case 'PLUS_CHECKOUT_CLICK_SUBSCRIBE':
|
||||
return clickPlusSubscribe(message.payload || {});
|
||||
case 'RUN_PAYPAL_HOSTED_OPENAI_CHECKOUT_STEP':
|
||||
return runPayPalHostedOpenAiCheckoutStep(message.payload || {});
|
||||
case 'PLUS_CHECKOUT_GET_STATE':
|
||||
return inspectPlusCheckoutState(message.payload || {});
|
||||
default:
|
||||
@@ -152,6 +167,153 @@ async function waitForDocumentComplete() {
|
||||
await sleep(1000);
|
||||
}
|
||||
|
||||
function isPayPalHostedOpenAiCheckoutPage() {
|
||||
const host = String(location?.host || '').toLowerCase();
|
||||
return host.includes('pay.openai.com') || host.includes('checkout.stripe.com');
|
||||
}
|
||||
|
||||
function hideHostedAddressAutocomplete() {
|
||||
[
|
||||
'.AddressAutocomplete-results',
|
||||
'[class*="AddressAutocomplete"]',
|
||||
'#billing-address-autocomplete-results',
|
||||
].forEach((selector) => {
|
||||
document.querySelectorAll(selector).forEach((node) => {
|
||||
try {
|
||||
node.style.setProperty('display', 'none', 'important');
|
||||
node.style.setProperty('visibility', 'hidden', 'important');
|
||||
node.style.setProperty('pointer-events', 'none', 'important');
|
||||
} catch {
|
||||
// Best effort only; checkout still works without this cleanup.
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function hasHostedOpenAiVerificationDialog() {
|
||||
return Boolean(document.getElementById('ci-ciBasic-0'));
|
||||
}
|
||||
|
||||
function fillHostedInput(selector, value) {
|
||||
const input = document.querySelector(selector);
|
||||
if (!input) {
|
||||
return false;
|
||||
}
|
||||
fillInput(input, String(value || ''));
|
||||
return true;
|
||||
}
|
||||
|
||||
function selectHostedOptionByText(selector, value) {
|
||||
const select = document.querySelector(selector);
|
||||
const expected = normalizeText(value).toLowerCase();
|
||||
if (!select || !expected) {
|
||||
return false;
|
||||
}
|
||||
const option = Array.from(select.options || []).find((item) => {
|
||||
const optionText = normalizeText(item.textContent || item.label || '').toLowerCase();
|
||||
const optionValue = normalizeText(item.value || '').toLowerCase();
|
||||
return optionText.includes(expected) || optionValue.includes(expected);
|
||||
});
|
||||
if (!option) {
|
||||
return false;
|
||||
}
|
||||
select.value = option.value;
|
||||
select.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return true;
|
||||
}
|
||||
|
||||
function findHostedPayPalButton() {
|
||||
return document.querySelector('[data-testid="paypal-accordion-item-button"]')
|
||||
|| document.querySelector('.paypal-accordion-item button')
|
||||
|| findClickableByText([/paypal/i]);
|
||||
}
|
||||
|
||||
function findHostedSubmitButton() {
|
||||
return document.querySelector('button[data-testid="submit-button"]')
|
||||
|| document.querySelector('button[data-testid="hosted-payment-submit-button"]')
|
||||
|| document.querySelector('button[data-atomic-wait-intent="Submit_Email"]')
|
||||
|| document.querySelector('button.SubmitButton--complete')
|
||||
|| findClickableByText([
|
||||
/next|continue|pay|subscribe|agree/i,
|
||||
/下一页|继续|支付|订阅|同意/i,
|
||||
]);
|
||||
}
|
||||
|
||||
async function clickHostedSubmitButton() {
|
||||
const button = await waitUntil(() => {
|
||||
hideHostedAddressAutocomplete();
|
||||
const candidate = findHostedSubmitButton();
|
||||
return candidate && isEnabledControl(candidate) && isVisibleElement(candidate) ? candidate : null;
|
||||
}, {
|
||||
label: 'hosted checkout 提交按钮',
|
||||
intervalMs: 500,
|
||||
timeoutMs: 15000,
|
||||
});
|
||||
document.activeElement?.blur?.();
|
||||
await sleep(300);
|
||||
simulateClick(button);
|
||||
await sleep(1200);
|
||||
return {
|
||||
clicked: true,
|
||||
buttonText: getActionText(button),
|
||||
hostedVerificationVisible: hasHostedOpenAiVerificationDialog(),
|
||||
};
|
||||
}
|
||||
|
||||
function fillHostedOpenAiVerificationCode(verificationCode = '') {
|
||||
const code = String(verificationCode || '').replace(/\D+/g, '').slice(0, 6);
|
||||
if (code.length !== 6) {
|
||||
throw new Error('hosted checkout OpenAI 验证码无效。');
|
||||
}
|
||||
for (let index = 0; index < 6; index += 1) {
|
||||
const input = document.getElementById(`ci-ciBasic-${index}`);
|
||||
if (!input) {
|
||||
throw new Error('hosted checkout OpenAI 页面未找到完整的验证码输入框。');
|
||||
}
|
||||
fillInput(input, code[index]);
|
||||
}
|
||||
return {
|
||||
verificationCodeFilled: true,
|
||||
hostedVerificationVisible: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function runPayPalHostedOpenAiCheckoutStep(payload = {}) {
|
||||
await waitForDocumentComplete();
|
||||
if (!isPayPalHostedOpenAiCheckoutPage()) {
|
||||
throw new Error('当前页面不是 PayPal 无卡直绑的 OpenAI hosted checkout 页面。');
|
||||
}
|
||||
if (payload.verificationCode) {
|
||||
return fillHostedOpenAiVerificationCode(payload.verificationCode);
|
||||
}
|
||||
|
||||
hideHostedAddressAutocomplete();
|
||||
const payPalButton = findHostedPayPalButton();
|
||||
if (payPalButton) {
|
||||
simulateClick(payPalButton);
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
const address = payload.address && typeof payload.address === 'object' ? payload.address : {};
|
||||
fillHostedInput('#billingAddressLine1', address.street || address.address1 || '');
|
||||
fillHostedInput('#billingLocality', address.city || '');
|
||||
fillHostedInput('#billingPostalCode', address.zip || address.postalCode || '');
|
||||
selectHostedOptionByText('#billingAdministrativeArea', address.state || address.region || '');
|
||||
|
||||
const consent = document.getElementById('termsOfServiceConsentCheckbox');
|
||||
if (consent && !consent.checked) {
|
||||
simulateClick(consent);
|
||||
}
|
||||
|
||||
for (let count = 0; count < 5; count += 1) {
|
||||
hideHostedAddressAutocomplete();
|
||||
await sleep(250);
|
||||
}
|
||||
|
||||
return clickHostedSubmitButton();
|
||||
}
|
||||
|
||||
function isVisibleElement(el) {
|
||||
if (!el) return false;
|
||||
const style = window.getComputedStyle(el);
|
||||
@@ -423,7 +585,14 @@ function findPaymentCardAncestor(el, pattern) {
|
||||
}
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
const paypalHostedValue = typeof PLUS_PAYMENT_METHOD_PAYPAL_HOSTED !== 'undefined'
|
||||
? PLUS_PAYMENT_METHOD_PAYPAL_HOSTED
|
||||
: 'paypal-hosted';
|
||||
if (normalized === paypalHostedValue || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
|
||||
return paypalHostedValue;
|
||||
}
|
||||
return normalized === PLUS_PAYMENT_METHOD_GOPAY
|
||||
? PLUS_PAYMENT_METHOD_GOPAY
|
||||
: PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
}
|
||||
@@ -620,6 +789,7 @@ function buildPlusCheckoutPayload(paymentMethod = PLUS_PAYMENT_METHOD_PAYPAL) {
|
||||
const config = getPaymentMethodConfig(paymentMethod);
|
||||
return {
|
||||
...JSON.parse(JSON.stringify(PLUS_CHECKOUT_PAYLOAD_BASE)),
|
||||
checkout_ui_mode: config.id === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED ? 'hosted' : 'custom',
|
||||
billing_details: {
|
||||
...config.billingDetails,
|
||||
},
|
||||
@@ -635,6 +805,29 @@ function buildPlusCheckoutUrl(checkoutSessionId, paymentMethod = PLUS_PAYMENT_ME
|
||||
return `https://chatgpt.com/checkout/${config.checkoutMerchantPath}/${sessionId}`;
|
||||
}
|
||||
|
||||
function findHostedCheckoutUrl(payload = {}) {
|
||||
const queue = [payload];
|
||||
while (queue.length) {
|
||||
const current = queue.shift();
|
||||
if (!current || typeof current !== 'object') {
|
||||
continue;
|
||||
}
|
||||
if (Array.isArray(current)) {
|
||||
queue.push(...current);
|
||||
continue;
|
||||
}
|
||||
for (const value of Object.values(current)) {
|
||||
if (typeof value === 'string' && /^https:\/\/(?:pay\.openai\.com|checkout\.stripe\.com)\/c\/pay\//i.test(value.trim())) {
|
||||
return value.trim();
|
||||
}
|
||||
if (value && typeof value === 'object') {
|
||||
queue.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
async function createPlusCheckoutSession(options = {}) {
|
||||
await waitForDocumentComplete();
|
||||
log('Plus:正在读取 ChatGPT 登录会话...');
|
||||
@@ -667,8 +860,16 @@ async function createPlusCheckoutSession(options = {}) {
|
||||
throw new Error(`创建 Plus Checkout 失败:${detail}`);
|
||||
}
|
||||
|
||||
const checkoutUrl = buildPlusCheckoutUrl(data.checkout_session_id, paymentMethod);
|
||||
const hostedCheckoutUrl = findHostedCheckoutUrl(data);
|
||||
const preferredCheckoutUrl = paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED
|
||||
? (hostedCheckoutUrl || checkoutUrl)
|
||||
: checkoutUrl;
|
||||
|
||||
return {
|
||||
checkoutUrl: buildPlusCheckoutUrl(data.checkout_session_id, paymentMethod),
|
||||
checkoutUrl,
|
||||
hostedCheckoutUrl,
|
||||
preferredCheckoutUrl,
|
||||
country: checkoutPayload.billing_details.country,
|
||||
currency: checkoutPayload.billing_details.currency,
|
||||
};
|
||||
@@ -1550,6 +1751,9 @@ async function inspectPlusCheckoutState(options = {}) {
|
||||
cardFieldsVisible: hasCreditCardFields(),
|
||||
billingFieldsVisible: hasBillingAddressFields(),
|
||||
hasSubscribeButton: Boolean(findSubscribeButton()),
|
||||
hostedOpenAiPage: isPayPalHostedOpenAiCheckoutPage(),
|
||||
hostedVerificationVisible: hasHostedOpenAiVerificationDialog(),
|
||||
hostedPayPalButtonFound: Boolean(findHostedPayPalButton()),
|
||||
checkoutAmountSummary: getCheckoutAmountSummary(),
|
||||
addressFieldValues: {
|
||||
address1: structuredAddress.address1?.value || '',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createStepDefinitionsModule() {
|
||||
const DEFAULT_ACTIVE_FLOW_ID = 'openai';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
|
||||
@@ -32,6 +33,7 @@
|
||||
{ id: 8, order: 80, key: 'paypal-approve', title: 'PayPal 登录与授权', sourceId: 'paypal-flow', driverId: 'content/paypal-flow', command: 'paypal-approve' },
|
||||
{ id: 9, order: 90, key: 'plus-checkout-return', title: '订阅回跳确认', sourceId: 'plus-checkout', driverId: 'content/plus-checkout', command: 'plus-checkout-return' },
|
||||
];
|
||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS = PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS.slice(0, 6);
|
||||
|
||||
const PLUS_GOPAY_PREFIX_STEP_DEFINITIONS = [
|
||||
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网', sourceId: 'chatgpt', driverId: null, command: 'open-chatgpt' },
|
||||
@@ -163,6 +165,53 @@
|
||||
];
|
||||
}
|
||||
|
||||
function createHostedCheckoutAuthTail(startId, startOrder, signupMethod = SIGNUP_METHOD_EMAIL, options = {}) {
|
||||
const id = Number(startId) || 7;
|
||||
const order = Number(startOrder) || id * 10;
|
||||
const commonStart = [
|
||||
{ id, order, key: 'oauth-login', title: '刷新 OAuth 并登录', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' },
|
||||
{ id: id + 1, order: order + 10, key: 'fetch-login-code', title: '获取登录验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' },
|
||||
];
|
||||
|
||||
if (signupMethod === SIGNUP_METHOD_PHONE) {
|
||||
if (isPhoneSignupReloginAfterBindEmailEnabled(options)) {
|
||||
return [
|
||||
...commonStart,
|
||||
{ id: id + 2, order: order + 20, key: 'bind-email', title: '绑定邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'bind-email' },
|
||||
{ id: id + 3, order: order + 30, key: 'fetch-bind-email-code', title: '获取绑定邮箱验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fetch-bind-email-code', mailRuleId: 'openai-login-code' },
|
||||
{ id: id + 4, order: order + 40, key: 'relogin-bound-email', title: '绑定邮箱后刷新 OAuth 并登录(邮箱)', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'oauth-login' },
|
||||
{ id: id + 5, order: order + 50, key: 'fetch-bound-email-login-code', title: '获取登录验证码(邮箱)', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'submit-verification-code', mailRuleId: 'openai-login-code' },
|
||||
{ id: id + 6, order: order + 60, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' },
|
||||
{ id: id + 7, order: order + 70, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' },
|
||||
];
|
||||
}
|
||||
return [
|
||||
...commonStart,
|
||||
{ id: id + 2, order: order + 20, key: 'bind-email', title: '绑定邮箱', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'bind-email' },
|
||||
{ id: id + 3, order: order + 30, key: 'fetch-bind-email-code', title: '获取绑定邮箱验证码', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'fetch-bind-email-code', mailRuleId: 'openai-login-code' },
|
||||
{ id: id + 4, order: order + 40, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' },
|
||||
{ id: id + 5, order: order + 50, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' },
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
...commonStart,
|
||||
{ id: id + 2, order: order + 20, key: 'confirm-oauth', title: '自动确认 OAuth', sourceId: 'openai-auth', driverId: 'content/signup-page', command: 'confirm-oauth' },
|
||||
{ id: id + 3, order: order + 30, key: 'platform-verify', title: '平台回调验证', sourceId: 'platform-panel', driverId: 'content/platform-panel', command: 'platform-verify' },
|
||||
];
|
||||
}
|
||||
|
||||
function createHostedCheckoutSteps(prefixSteps, startId, startOrder, signupMethod = SIGNUP_METHOD_EMAIL, options = {}) {
|
||||
const sessionTailFactory = resolvePlusSessionImportTail(options, signupMethod);
|
||||
const tailSteps = sessionTailFactory
|
||||
? sessionTailFactory(startId, startOrder)
|
||||
: createHostedCheckoutAuthTail(startId, startOrder, signupMethod, options);
|
||||
return [
|
||||
...prefixSteps,
|
||||
...tailSteps,
|
||||
];
|
||||
}
|
||||
|
||||
const NORMAL_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_EMAIL);
|
||||
const NORMAL_PHONE_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE);
|
||||
const NORMAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createOpenAiSteps(NORMAL_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true });
|
||||
@@ -183,6 +232,23 @@
|
||||
);
|
||||
const PLUS_PAYPAL_PHONE_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE);
|
||||
const PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createOpenAiSteps(PLUS_PAYPAL_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true });
|
||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_EMAIL);
|
||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS = createHostedCheckoutSteps(
|
||||
PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS,
|
||||
7,
|
||||
70,
|
||||
SIGNUP_METHOD_EMAIL,
|
||||
{ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION }
|
||||
);
|
||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS = createHostedCheckoutSteps(
|
||||
PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS,
|
||||
7,
|
||||
70,
|
||||
SIGNUP_METHOD_EMAIL,
|
||||
{ plusAccountAccessStrategy: PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION }
|
||||
);
|
||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE);
|
||||
const PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS = createHostedCheckoutSteps(PLUS_PAYPAL_HOSTED_CHECKOUT_PREFIX_STEP_DEFINITIONS, 7, 70, SIGNUP_METHOD_PHONE, { phoneSignupReloginAfterBindEmailEnabled: true });
|
||||
const PLUS_GOPAY_STEP_DEFINITIONS = createOpenAiSteps(PLUS_GOPAY_PREFIX_STEP_DEFINITIONS, 10, 100, SIGNUP_METHOD_EMAIL);
|
||||
const PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS = createOpenAiSteps(
|
||||
PLUS_GOPAY_PREFIX_STEP_DEFINITIONS,
|
||||
@@ -313,6 +379,9 @@
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
|
||||
return PLUS_PAYMENT_METHOD_PAYPAL_HOSTED;
|
||||
}
|
||||
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||||
return PLUS_PAYMENT_METHOD_GPC_HELPER;
|
||||
}
|
||||
@@ -352,6 +421,20 @@
|
||||
}
|
||||
const paymentMethod = normalizePlusPaymentMethod(options?.plusPaymentMethod || options?.paymentMethod);
|
||||
const plusAccountAccessStrategy = normalizePlusAccountAccessStrategy(options?.plusAccountAccessStrategy);
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED) {
|
||||
if (signupMethod === SIGNUP_METHOD_PHONE) {
|
||||
return reloginAfterBindEmail
|
||||
? PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS
|
||||
: PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS;
|
||||
}
|
||||
if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
|
||||
return PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS;
|
||||
}
|
||||
if (plusAccountAccessStrategy === PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION) {
|
||||
return PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS;
|
||||
}
|
||||
return PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS;
|
||||
}
|
||||
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||||
if (signupMethod === SIGNUP_METHOD_PHONE) {
|
||||
return reloginAfterBindEmail
|
||||
@@ -433,6 +516,11 @@
|
||||
...PLUS_PAYPAL_CPA_SESSION_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_PHONE_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS,
|
||||
...PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
|
||||
...PLUS_GOPAY_STEP_DEFINITIONS,
|
||||
...PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS,
|
||||
...PLUS_GOPAY_CPA_SESSION_STEP_DEFINITIONS,
|
||||
@@ -639,6 +727,11 @@
|
||||
PLUS_PAYPAL_SUB2API_SESSION_STEP_DEFINITIONS,
|
||||
PLUS_PAYPAL_PHONE_STEP_DEFINITIONS,
|
||||
PLUS_PAYPAL_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
|
||||
PLUS_PAYPAL_HOSTED_CHECKOUT_STEP_DEFINITIONS,
|
||||
PLUS_PAYPAL_HOSTED_CHECKOUT_SUB2API_SESSION_STEP_DEFINITIONS,
|
||||
PLUS_PAYPAL_HOSTED_CHECKOUT_CPA_SESSION_STEP_DEFINITIONS,
|
||||
PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_STEP_DEFINITIONS,
|
||||
PLUS_PAYPAL_HOSTED_CHECKOUT_PHONE_BOUND_EMAIL_RELOGIN_STEP_DEFINITIONS,
|
||||
PLUS_GOPAY_STEP_DEFINITIONS,
|
||||
PLUS_GOPAY_SUB2API_SESSION_STEP_DEFINITIONS,
|
||||
PLUS_GOPAY_PHONE_STEP_DEFINITIONS,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
root.GoPayUtils = factory();
|
||||
})(typeof self !== 'undefined' ? self : globalThis, function createGoPayUtils() {
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
@@ -11,6 +12,9 @@
|
||||
|
||||
function normalizePlusPaymentMethod(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === PLUS_PAYMENT_METHOD_PAYPAL_HOSTED || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
|
||||
return PLUS_PAYMENT_METHOD_PAYPAL_HOSTED;
|
||||
}
|
||||
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
|
||||
return PLUS_PAYMENT_METHOD_GPC_HELPER;
|
||||
}
|
||||
@@ -416,6 +420,7 @@
|
||||
PLUS_PAYMENT_METHOD_GPC_HELPER,
|
||||
PLUS_PAYMENT_METHOD_GOPAY,
|
||||
PLUS_PAYMENT_METHOD_PAYPAL,
|
||||
PLUS_PAYMENT_METHOD_PAYPAL_HOSTED,
|
||||
buildGpcCardBalanceUrl,
|
||||
buildGpcApiKeyBalanceUrl,
|
||||
buildGpcApiKeyHeaders,
|
||||
|
||||
@@ -378,24 +378,31 @@
|
||||
const requestedPlusAccountAccessStrategy = normalizePlusAccountAccessStrategy(
|
||||
options?.plusAccountAccessStrategy ?? state?.plusAccountAccessStrategy
|
||||
);
|
||||
const basePlusAccountAccessStrategies = [
|
||||
PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH,
|
||||
PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION,
|
||||
PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION,
|
||||
];
|
||||
const availablePlusAccountAccessStrategies = activeFlowId === 'openai'
|
||||
&& Boolean(flowState.supportsPlusMode)
|
||||
&& Boolean(runtimeLocks.plusModeEnabled)
|
||||
&& effectiveSignupMethod === SIGNUP_METHOD_EMAIL
|
||||
? (
|
||||
Array.isArray(targetState.supportedPlusAccountAccessStrategies)
|
||||
&& targetState.supportedPlusAccountAccessStrategies.length > 0
|
||||
? targetState.supportedPlusAccountAccessStrategies.slice()
|
||||
: [PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH]
|
||||
)
|
||||
? (runtimeLocks.accountContribution
|
||||
? [PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION]
|
||||
: basePlusAccountAccessStrategies)
|
||||
: [PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH];
|
||||
const effectivePlusAccountAccessStrategy = availablePlusAccountAccessStrategies.includes(requestedPlusAccountAccessStrategy)
|
||||
const effectivePlusAccountAccessStrategy = runtimeLocks.accountContribution
|
||||
&& runtimeLocks.plusModeEnabled
|
||||
&& effectiveSignupMethod === SIGNUP_METHOD_EMAIL
|
||||
? PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION
|
||||
: availablePlusAccountAccessStrategies.includes(requestedPlusAccountAccessStrategy)
|
||||
? requestedPlusAccountAccessStrategy
|
||||
: PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
|
||||
const canEditPlusAccountAccessStrategy = activeFlowId === 'openai'
|
||||
&& Boolean(flowState.supportsPlusMode)
|
||||
&& Boolean(runtimeLocks.plusModeEnabled)
|
||||
&& effectiveSignupMethod === SIGNUP_METHOD_EMAIL
|
||||
&& !runtimeLocks.accountContribution
|
||||
&& availablePlusAccountAccessStrategies.length > 1;
|
||||
const visibleGroupIds = typeof flowRegistryApi.getVisibleGroupIds === 'function'
|
||||
&& isRegisteredFlowId(activeFlowId)
|
||||
|
||||
@@ -105,8 +105,11 @@
|
||||
},
|
||||
plus: {
|
||||
plusModeEnabled: false,
|
||||
plusPaymentMethod: 'paypal',
|
||||
plusPaymentMethod: 'paypal-hosted',
|
||||
plusAccountAccessStrategy: 'oauth',
|
||||
hostedCheckoutVerificationUrl: '',
|
||||
hostedCheckoutPhoneNumber: '',
|
||||
plusHostedCheckoutOauthDelaySeconds: 3,
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: {
|
||||
@@ -317,6 +320,24 @@
|
||||
?? nested?.flows?.openai?.plus?.plusAccountAccessStrategy
|
||||
?? defaults.flows.openai.plus.plusAccountAccessStrategy
|
||||
),
|
||||
hostedCheckoutVerificationUrl: String(
|
||||
input?.hostedCheckoutVerificationUrl
|
||||
?? nested?.flows?.openai?.plus?.hostedCheckoutVerificationUrl
|
||||
?? defaults.flows.openai.plus.hostedCheckoutVerificationUrl
|
||||
).trim(),
|
||||
hostedCheckoutPhoneNumber: String(
|
||||
input?.hostedCheckoutPhoneNumber
|
||||
?? nested?.flows?.openai?.plus?.hostedCheckoutPhoneNumber
|
||||
?? defaults.flows.openai.plus.hostedCheckoutPhoneNumber
|
||||
).trim(),
|
||||
plusHostedCheckoutOauthDelaySeconds: (() => {
|
||||
const numeric = Number(
|
||||
input?.plusHostedCheckoutOauthDelaySeconds
|
||||
?? nested?.flows?.openai?.plus?.plusHostedCheckoutOauthDelaySeconds
|
||||
?? defaults.flows.openai.plus.plusHostedCheckoutOauthDelaySeconds
|
||||
);
|
||||
return Math.min(120, Math.max(0, Math.floor(Number.isFinite(numeric) ? numeric : defaults.flows.openai.plus.plusHostedCheckoutOauthDelaySeconds)));
|
||||
})(),
|
||||
},
|
||||
autoRun: {
|
||||
stepExecutionRange: normalizeStepExecutionRangeEntry(
|
||||
@@ -456,6 +477,9 @@
|
||||
next.plusModeEnabled = openaiState.plus.plusModeEnabled;
|
||||
next.plusPaymentMethod = openaiState.plus.plusPaymentMethod;
|
||||
next.plusAccountAccessStrategy = openaiState.plus.plusAccountAccessStrategy;
|
||||
next.hostedCheckoutVerificationUrl = openaiState.plus.hostedCheckoutVerificationUrl;
|
||||
next.hostedCheckoutPhoneNumber = openaiState.plus.hostedCheckoutPhoneNumber;
|
||||
next.plusHostedCheckoutOauthDelaySeconds = openaiState.plus.plusHostedCheckoutOauthDelaySeconds;
|
||||
next.mailProvider = normalizedState.services.email.provider;
|
||||
next.ipProxyEnabled = normalizedState.services.proxy.enabled;
|
||||
next.ipProxyService = normalizedState.services.proxy.provider;
|
||||
|
||||
@@ -327,6 +327,7 @@
|
||||
<span class="data-label">Plus 支付</span>
|
||||
<div class="data-inline">
|
||||
<select id="select-plus-payment-method" class="data-select">
|
||||
<option value="paypal-hosted">PayPal 无卡直绑</option>
|
||||
<option value="paypal">PayPal</option>
|
||||
<!--
|
||||
<option value="gopay">GoPay</option>
|
||||
@@ -352,6 +353,22 @@
|
||||
<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-hosted-checkout-verification-url" style="display:none;">
|
||||
<span class="data-label">直绑验证码</span>
|
||||
<input type="text" id="input-hosted-checkout-verification-url" class="data-input"
|
||||
placeholder="用于读取 PayPal / OpenAI 验证码的接口地址" autocomplete="off" />
|
||||
</div>
|
||||
<div class="data-row" id="row-hosted-checkout-phone" style="display:none;">
|
||||
<span class="data-label">直绑电话</span>
|
||||
<input type="text" id="input-hosted-checkout-phone" class="data-input"
|
||||
placeholder="PayPal guest checkout 使用的电话" autocomplete="tel" />
|
||||
</div>
|
||||
<div class="data-row" id="row-plus-hosted-checkout-oauth-delay" style="display:none;">
|
||||
<span class="data-label">成功等待</span>
|
||||
<input type="number" id="input-plus-hosted-checkout-oauth-delay-seconds" class="data-input auto-delay-input"
|
||||
value="3" min="0" max="120" step="1" />
|
||||
<span class="setting-caption">支付成功页出现后再继续账号接入</span>
|
||||
</div>
|
||||
<div class="data-row" id="row-gpc-helper-api" style="display:none;">
|
||||
<span class="data-label">GPC API</span>
|
||||
<div class="data-inline">
|
||||
|
||||
+99
-27
@@ -209,6 +209,12 @@ const btnPayPalAccountMenu = document.getElementById('btn-paypal-account-menu');
|
||||
const payPalAccountCurrent = document.getElementById('paypal-account-current');
|
||||
const payPalAccountMenu = document.getElementById('paypal-account-menu');
|
||||
const btnAddPayPalAccount = document.getElementById('btn-add-paypal-account');
|
||||
const rowHostedCheckoutVerificationUrl = document.getElementById('row-hosted-checkout-verification-url');
|
||||
const inputHostedCheckoutVerificationUrl = document.getElementById('input-hosted-checkout-verification-url');
|
||||
const rowHostedCheckoutPhone = document.getElementById('row-hosted-checkout-phone');
|
||||
const inputHostedCheckoutPhone = document.getElementById('input-hosted-checkout-phone');
|
||||
const rowPlusHostedCheckoutOauthDelay = document.getElementById('row-plus-hosted-checkout-oauth-delay');
|
||||
const inputPlusHostedCheckoutOauthDelaySeconds = document.getElementById('input-plus-hosted-checkout-oauth-delay-seconds');
|
||||
const rowGpcHelperApi = document.getElementById('row-gpc-helper-api');
|
||||
const inputGpcHelperApi = document.getElementById('input-gpc-helper-api');
|
||||
const btnGpcHelperConvertApiKey = document.getElementById('btn-gpc-helper-convert-api-key');
|
||||
@@ -545,13 +551,15 @@ const btnAutoStartContinue = document.getElementById('btn-auto-start-continue');
|
||||
const autoHintText = document.querySelector('.auto-hint');
|
||||
const stepsList = document.querySelector('.steps-list');
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
|
||||
const PLUS_PAYMENT_METHOD_PAYPAL_HOSTED = 'paypal-hosted';
|
||||
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
|
||||
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
|
||||
const DEFAULT_GPC_HELPER_API_URL = 'https://gpc.qlhazycoder.top';
|
||||
const GPC_HELPER_PORTAL_URL = 'https://gpc.qlhazycoder.top/';
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
|
||||
const DEFAULT_PLUS_HOSTED_CHECKOUT_OAUTH_DELAY_SECONDS = 3;
|
||||
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL_HOSTED;
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
@@ -2939,13 +2947,25 @@ function normalizePlusPaymentMethod(value = '') {
|
||||
const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay';
|
||||
const gpcValue = typeof PLUS_PAYMENT_METHOD_GPC_HELPER !== 'undefined' ? PLUS_PAYMENT_METHOD_GPC_HELPER : 'gpc-helper';
|
||||
const paypalValue = typeof PLUS_PAYMENT_METHOD_PAYPAL !== 'undefined' ? PLUS_PAYMENT_METHOD_PAYPAL : 'paypal';
|
||||
const paypalHostedValue = typeof PLUS_PAYMENT_METHOD_PAYPAL_HOSTED !== 'undefined' ? PLUS_PAYMENT_METHOD_PAYPAL_HOSTED : 'paypal-hosted';
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === paypalHostedValue || normalized === 'paypal_direct' || normalized === 'paypal-direct') {
|
||||
return paypalHostedValue;
|
||||
}
|
||||
if (normalized === gpcValue) {
|
||||
return gpcValue;
|
||||
}
|
||||
return normalized === gopayValue ? gopayValue : paypalValue;
|
||||
}
|
||||
|
||||
function normalizePlusHostedCheckoutOauthDelaySeconds(value) {
|
||||
const numeric = Number(String(value ?? '').trim());
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return DEFAULT_PLUS_HOSTED_CHECKOUT_OAUTH_DELAY_SECONDS;
|
||||
}
|
||||
return Math.min(120, Math.max(0, Math.floor(numeric)));
|
||||
}
|
||||
|
||||
function normalizePlusAccountAccessStrategy(value = '') {
|
||||
const normalized = String(value || '').trim().toLowerCase();
|
||||
if (normalized === PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION) {
|
||||
@@ -4505,6 +4525,15 @@ function collectSettingsPayload() {
|
||||
const currentKiroRsKeyValue = typeof inputKiroRsKey !== 'undefined' && inputKiroRsKey
|
||||
? String(inputKiroRsKey.value ?? '').trim()
|
||||
: null;
|
||||
const normalizeHostedCheckoutDelaySecondsSafe = typeof normalizePlusHostedCheckoutOauthDelaySeconds === 'function'
|
||||
? normalizePlusHostedCheckoutOauthDelaySeconds
|
||||
: ((value) => {
|
||||
const numeric = Number(String(value ?? '').trim());
|
||||
if (!Number.isFinite(numeric)) {
|
||||
return 3;
|
||||
}
|
||||
return Math.min(120, Math.max(0, Math.floor(numeric)));
|
||||
});
|
||||
return {
|
||||
activeFlowId,
|
||||
...(accountContributionEnabled ? {} : {
|
||||
@@ -4562,6 +4591,17 @@ function collectSettingsPayload() {
|
||||
plusModeEnabled: effectivePlusModeEnabled,
|
||||
plusPaymentMethod,
|
||||
plusAccountAccessStrategy: requestedPlusAccountAccessStrategy,
|
||||
hostedCheckoutVerificationUrl: typeof inputHostedCheckoutVerificationUrl !== 'undefined' && inputHostedCheckoutVerificationUrl
|
||||
? String(inputHostedCheckoutVerificationUrl.value || '').trim()
|
||||
: String(latestState?.hostedCheckoutVerificationUrl || '').trim(),
|
||||
hostedCheckoutPhoneNumber: typeof inputHostedCheckoutPhone !== 'undefined' && inputHostedCheckoutPhone
|
||||
? String(inputHostedCheckoutPhone.value || '').trim()
|
||||
: String(latestState?.hostedCheckoutPhoneNumber || '').trim(),
|
||||
plusHostedCheckoutOauthDelaySeconds: normalizeHostedCheckoutDelaySecondsSafe(
|
||||
typeof inputPlusHostedCheckoutOauthDelaySeconds !== 'undefined' && inputPlusHostedCheckoutOauthDelaySeconds
|
||||
? inputPlusHostedCheckoutOauthDelaySeconds.value
|
||||
: latestState?.plusHostedCheckoutOauthDelaySeconds
|
||||
),
|
||||
paypalEmail: String(currentPayPalAccount?.email || latestState?.paypalEmail || '').trim(),
|
||||
paypalPassword: String(currentPayPalAccount?.password || latestState?.paypalPassword || ''),
|
||||
currentPayPalAccountId: String(latestState?.currentPayPalAccountId || '').trim(),
|
||||
@@ -9143,6 +9183,7 @@ function updatePhoneVerificationSettingsUI() {
|
||||
|
||||
function updatePlusModeUI() {
|
||||
const paypalValue = typeof PLUS_PAYMENT_METHOD_PAYPAL !== 'undefined' ? PLUS_PAYMENT_METHOD_PAYPAL : 'paypal';
|
||||
const paypalHostedValue = typeof PLUS_PAYMENT_METHOD_PAYPAL_HOSTED !== 'undefined' ? PLUS_PAYMENT_METHOD_PAYPAL_HOSTED : 'paypal-hosted';
|
||||
const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay';
|
||||
const gpcValue = typeof PLUS_PAYMENT_METHOD_GPC_HELPER !== 'undefined' ? PLUS_PAYMENT_METHOD_GPC_HELPER : 'gpc-helper';
|
||||
const oauthStrategyValue = typeof PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH !== 'undefined'
|
||||
@@ -9259,6 +9300,7 @@ function updatePlusModeUI() {
|
||||
const selectedMethod = typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod?.value
|
||||
? normalizePlusPaymentMethod(selectPlusPaymentMethod.value)
|
||||
: method;
|
||||
const hostedRowsVisible = enabled && selectedMethod === paypalHostedValue;
|
||||
const gpcRowsVisible = enabled && selectedMethod === gpcValue;
|
||||
const canShowGpcModeSelector = gpcRowsVisible;
|
||||
const localSmsControlsVisible = gpcRowsVisible && !isGpcAutoMode;
|
||||
@@ -9277,6 +9319,8 @@ function updatePlusModeUI() {
|
||||
? `GPC ${isGpcAutoMode ? '自动' : '手动'}订阅链路`
|
||||
: method === gopayValue
|
||||
? 'GoPay 印尼订阅链路'
|
||||
: method === paypalHostedValue
|
||||
? 'PayPal 无卡直绑链路'
|
||||
: 'PayPal 订阅链路';
|
||||
}
|
||||
if (typeof plusPaymentMethodCaption !== 'undefined' && plusPaymentMethodCaption && method === gpcValue && gpcAutoModeBlocked) {
|
||||
@@ -9335,12 +9379,35 @@ function updatePlusModeUI() {
|
||||
}
|
||||
}
|
||||
if (typeof plusAccountAccessStrategyCaption !== 'undefined' && plusAccountAccessStrategyCaption) {
|
||||
plusAccountAccessStrategyCaption.textContent = !enabled || !canEditPlusAccountAccessStrategy
|
||||
plusAccountAccessStrategyCaption.textContent = !enabled
|
||||
? '当前来源仅支持 OAuth'
|
||||
: describePlusAccountAccessStrategy(
|
||||
: ((effectivePlusAccountAccessStrategy !== oauthStrategyValue || canEditPlusAccountAccessStrategy)
|
||||
? describePlusAccountAccessStrategy(
|
||||
effectivePlusAccountAccessStrategy,
|
||||
effectiveTargetId
|
||||
);
|
||||
)
|
||||
: '当前来源仅支持 OAuth');
|
||||
}
|
||||
if (enabled && effectivePlusAccountAccessStrategy === sub2apiSessionStrategyValue) {
|
||||
[
|
||||
typeof rowSub2ApiUrl !== 'undefined' ? rowSub2ApiUrl : null,
|
||||
typeof rowSub2ApiEmail !== 'undefined' ? rowSub2ApiEmail : null,
|
||||
typeof rowSub2ApiPassword !== 'undefined' ? rowSub2ApiPassword : null,
|
||||
typeof rowSub2ApiGroup !== 'undefined' ? rowSub2ApiGroup : null,
|
||||
typeof rowSub2ApiAccountPriority !== 'undefined' ? rowSub2ApiAccountPriority : null,
|
||||
typeof rowSub2ApiDefaultProxy !== 'undefined' ? rowSub2ApiDefaultProxy : null,
|
||||
].forEach((row) => {
|
||||
if (row) row.style.display = '';
|
||||
});
|
||||
}
|
||||
if (enabled && effectivePlusAccountAccessStrategy === cpaSessionStrategyValue) {
|
||||
[
|
||||
typeof rowVpsUrl !== 'undefined' ? rowVpsUrl : null,
|
||||
typeof rowVpsPassword !== 'undefined' ? rowVpsPassword : null,
|
||||
typeof rowLocalCpaStep9Mode !== 'undefined' ? rowLocalCpaStep9Mode : null,
|
||||
].forEach((row) => {
|
||||
if (row) row.style.display = '';
|
||||
});
|
||||
}
|
||||
[
|
||||
typeof rowPayPalAccount !== 'undefined' ? rowPayPalAccount : null,
|
||||
@@ -9350,6 +9417,16 @@ function updatePlusModeUI() {
|
||||
}
|
||||
row.style.display = enabled && selectedMethod === paypalValue ? '' : 'none';
|
||||
});
|
||||
[
|
||||
typeof rowHostedCheckoutVerificationUrl !== 'undefined' ? rowHostedCheckoutVerificationUrl : null,
|
||||
typeof rowHostedCheckoutPhone !== 'undefined' ? rowHostedCheckoutPhone : null,
|
||||
typeof rowPlusHostedCheckoutOauthDelay !== 'undefined' ? rowPlusHostedCheckoutOauthDelay : null,
|
||||
].forEach((row) => {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
row.style.display = hostedRowsVisible ? '' : 'none';
|
||||
});
|
||||
[
|
||||
typeof rowGpcHelperApi !== 'undefined' ? rowGpcHelperApi : null,
|
||||
typeof rowGpcHelperCardKey !== 'undefined' ? rowGpcHelperCardKey : null,
|
||||
@@ -10507,6 +10584,17 @@ function applySettingsState(state) {
|
||||
if (typeof inputGoPayPin !== 'undefined' && inputGoPayPin) {
|
||||
inputGoPayPin.value = state?.gopayPin || '';
|
||||
}
|
||||
if (typeof inputHostedCheckoutVerificationUrl !== 'undefined' && inputHostedCheckoutVerificationUrl) {
|
||||
inputHostedCheckoutVerificationUrl.value = String(state?.hostedCheckoutVerificationUrl || '').trim();
|
||||
}
|
||||
if (typeof inputHostedCheckoutPhone !== 'undefined' && inputHostedCheckoutPhone) {
|
||||
inputHostedCheckoutPhone.value = String(state?.hostedCheckoutPhoneNumber || '').trim();
|
||||
}
|
||||
if (typeof inputPlusHostedCheckoutOauthDelaySeconds !== 'undefined' && inputPlusHostedCheckoutOauthDelaySeconds) {
|
||||
inputPlusHostedCheckoutOauthDelaySeconds.value = String(
|
||||
normalizePlusHostedCheckoutOauthDelaySeconds(state?.plusHostedCheckoutOauthDelaySeconds)
|
||||
);
|
||||
}
|
||||
inputVpsUrl.value = state?.vpsUrl || '';
|
||||
inputVpsPassword.value = state?.vpsPassword || '';
|
||||
setLocalCpaStep9Mode(state?.localCpaStep9Mode);
|
||||
@@ -14660,6 +14748,9 @@ inputVpsPassword.addEventListener('blur', () => {
|
||||
scheduleSettingsAutoSave();
|
||||
});
|
||||
input?.addEventListener('blur', () => {
|
||||
if (input === inputPlusHostedCheckoutOauthDelaySeconds) {
|
||||
input.value = String(normalizePlusHostedCheckoutOauthDelaySeconds(input.value));
|
||||
}
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
});
|
||||
@@ -14721,29 +14812,6 @@ inputPlusModeEnabled?.addEventListener('change', () => {
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
selectPlusPaymentMethod?.addEventListener('change', () => {
|
||||
selectPlusPaymentMethod.value = normalizePlusPaymentMethod(selectPlusPaymentMethod.value);
|
||||
updatePlusModeUI();
|
||||
const stepDefinitionState = typeof resolveStepDefinitionCapabilityState === 'function'
|
||||
? resolveStepDefinitionCapabilityState({
|
||||
...(latestState || {}),
|
||||
plusModeEnabled: Boolean(inputPlusModeEnabled?.checked),
|
||||
signupMethod: getSelectedSignupMethod(),
|
||||
}, {
|
||||
signupMethod: getSelectedSignupMethod(),
|
||||
})
|
||||
: {
|
||||
plusModeEnabled: Boolean(inputPlusModeEnabled?.checked),
|
||||
signupMethod: getSelectedSignupMethod(),
|
||||
};
|
||||
syncStepDefinitionsForMode(stepDefinitionState.plusModeEnabled, selectPlusPaymentMethod.value, {
|
||||
render: true,
|
||||
signupMethod: stepDefinitionState.signupMethod,
|
||||
});
|
||||
markSettingsDirty(true);
|
||||
saveSettings({ silent: true }).catch(() => { });
|
||||
});
|
||||
|
||||
btnGpcCardKeyPurchase?.addEventListener('click', () => {
|
||||
openExternalUrl('https://pay.ldxp.cn/shop/gpc');
|
||||
});
|
||||
@@ -14842,6 +14910,7 @@ btnTestKiroRs?.addEventListener('click', async () => {
|
||||
});
|
||||
|
||||
selectPlusPaymentMethod?.addEventListener('change', () => {
|
||||
selectPlusPaymentMethod.value = normalizePlusPaymentMethod(selectPlusPaymentMethod.value);
|
||||
updatePlusModeUI();
|
||||
const stepDefinitionState = typeof resolveStepDefinitionCapabilityState === 'function'
|
||||
? resolveStepDefinitionCapabilityState({
|
||||
@@ -14878,6 +14947,9 @@ selectPlusPaymentMethod?.addEventListener('change', () => {
|
||||
inputGoPayPhone,
|
||||
inputGoPayOtp,
|
||||
inputGoPayPin,
|
||||
inputHostedCheckoutVerificationUrl,
|
||||
inputHostedCheckoutPhone,
|
||||
inputPlusHostedCheckoutOauthDelaySeconds,
|
||||
].forEach((input) => {
|
||||
input?.addEventListener('input', () => {
|
||||
markSettingsDirty(true);
|
||||
|
||||
@@ -230,6 +230,7 @@ return {
|
||||
assert.equal(api.normalizePersistentSettingValue('phoneSignupReloginAfterBindEmailEnabled', 0), false);
|
||||
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'gopay'), 'gopay');
|
||||
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'gpc-helper'), 'gpc-helper');
|
||||
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'paypal-hosted'), 'paypal-hosted');
|
||||
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'paypal'), 'paypal');
|
||||
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'unknown'), 'paypal');
|
||||
assert.equal(api.normalizePersistentSettingValue('plusAccountAccessStrategy', 'sub2api_codex_session'), 'sub2api_codex_session');
|
||||
|
||||
@@ -212,7 +212,7 @@ test('flow capability registry exposes editable Plus account access strategies f
|
||||
|
||||
assert.deepEqual(
|
||||
capabilityState.availablePlusAccountAccessStrategies,
|
||||
['oauth', 'sub2api_codex_session']
|
||||
['oauth', 'cpa_codex_session', 'sub2api_codex_session']
|
||||
);
|
||||
assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
@@ -236,7 +236,7 @@ test('flow capability registry exposes editable Plus account access strategies f
|
||||
|
||||
assert.deepEqual(
|
||||
capabilityState.availablePlusAccountAccessStrategies,
|
||||
['oauth', 'cpa_codex_session']
|
||||
['oauth', 'cpa_codex_session', 'sub2api_codex_session']
|
||||
);
|
||||
assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'cpa_codex_session');
|
||||
assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'cpa_codex_session');
|
||||
@@ -244,7 +244,7 @@ test('flow capability registry exposes editable Plus account access strategies f
|
||||
assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'cpa_codex_session');
|
||||
});
|
||||
|
||||
test('flow capability registry forces OAuth when the current target only supports OAuth', () => {
|
||||
test('flow capability registry keeps Plus account access strategies independent from the current target', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry();
|
||||
|
||||
@@ -258,9 +258,34 @@ test('flow capability registry forces OAuth when the current target only support
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(capabilityState.availablePlusAccountAccessStrategies, ['oauth']);
|
||||
assert.deepEqual(
|
||||
capabilityState.availablePlusAccountAccessStrategies,
|
||||
['oauth', 'cpa_codex_session', 'sub2api_codex_session']
|
||||
);
|
||||
assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'cpa_codex_session');
|
||||
assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'oauth');
|
||||
assert.equal(capabilityState.canEditPlusAccountAccessStrategy, false);
|
||||
assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'oauth');
|
||||
assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'cpa_codex_session');
|
||||
assert.equal(capabilityState.canEditPlusAccountAccessStrategy, true);
|
||||
assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'cpa_codex_session');
|
||||
});
|
||||
|
||||
test('flow capability registry forces SUB2API session import only for contribution mode Plus runs', () => {
|
||||
const api = loadApi();
|
||||
const registry = api.createFlowCapabilityRegistry();
|
||||
|
||||
const capabilityState = registry.resolveSidepanelCapabilities({
|
||||
state: {
|
||||
activeFlowId: 'openai',
|
||||
openaiIntegrationTargetId: 'cpa',
|
||||
signupMethod: 'email',
|
||||
plusModeEnabled: true,
|
||||
accountContributionEnabled: true,
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
},
|
||||
});
|
||||
|
||||
assert.deepEqual(capabilityState.availablePlusAccountAccessStrategies, ['sub2api_codex_session']);
|
||||
assert.equal(capabilityState.requestedPlusAccountAccessStrategy, 'cpa_codex_session');
|
||||
assert.equal(capabilityState.effectivePlusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
assert.equal(capabilityState.canEditPlusAccountAccessStrategy, false);
|
||||
assert.equal(capabilityState.stepDefinitionOptions.plusAccountAccessStrategy, 'sub2api_codex_session');
|
||||
});
|
||||
|
||||
@@ -23,6 +23,8 @@ test('GoPay utils normalize manual OTP input', () => {
|
||||
|
||||
test('GoPay utils keeps GPC helper payment method distinct', () => {
|
||||
const api = loadGoPayUtils();
|
||||
assert.equal(api.normalizePlusPaymentMethod('paypal-hosted'), 'paypal-hosted');
|
||||
assert.equal(api.normalizePlusPaymentMethod('paypal_direct'), 'paypal-hosted');
|
||||
assert.equal(api.normalizePlusPaymentMethod('gpc-helper'), 'gpc-helper');
|
||||
assert.equal(api.normalizePlusPaymentMethod('gopay'), 'gopay');
|
||||
assert.equal(api.normalizePlusPaymentMethod('unknown'), 'paypal');
|
||||
|
||||
@@ -115,19 +115,21 @@ return {
|
||||
`)();
|
||||
}
|
||||
|
||||
test('sidepanel keeps requested Plus account strategy while OAuth-only targets force the effective value', () => {
|
||||
test('sidepanel keeps requested Plus account strategy while contribution mode forces SUB2API', () => {
|
||||
const api = buildHarness(
|
||||
`{
|
||||
canShowPlusSettings: true,
|
||||
runtimeLocks: { plusModeEnabled: true },
|
||||
runtimeLocks: { plusModeEnabled: true, accountContribution: true },
|
||||
canEditPlusAccountAccessStrategy: false,
|
||||
effectivePlusAccountAccessStrategy: 'oauth',
|
||||
availablePlusAccountAccessStrategies: ['sub2api_codex_session'],
|
||||
effectivePlusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
}`,
|
||||
`{
|
||||
activeFlowId: 'openai',
|
||||
panelMode: 'cpa',
|
||||
plusPaymentMethod: 'paypal',
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
accountContributionEnabled: true,
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
}`
|
||||
);
|
||||
|
||||
@@ -135,10 +137,10 @@ test('sidepanel keeps requested Plus account strategy while OAuth-only targets f
|
||||
|
||||
assert.equal(api.rowPlusAccountAccessStrategy.style.display, '');
|
||||
assert.equal(api.selectPlusAccountAccessStrategy.disabled, true);
|
||||
assert.equal(api.selectPlusAccountAccessStrategy.dataset.requestedValue, 'sub2api_codex_session');
|
||||
assert.equal(api.selectPlusAccountAccessStrategy.value, 'oauth');
|
||||
assert.equal(api.getRequestedPlusAccountAccessStrategy(), 'sub2api_codex_session');
|
||||
assert.match(api.plusAccountAccessStrategyCaption.textContent, /OAuth/);
|
||||
assert.equal(api.selectPlusAccountAccessStrategy.dataset.requestedValue, 'cpa_codex_session');
|
||||
assert.equal(api.selectPlusAccountAccessStrategy.value, 'sub2api_codex_session');
|
||||
assert.equal(api.getRequestedPlusAccountAccessStrategy(), 'cpa_codex_session');
|
||||
assert.match(api.plusAccountAccessStrategyCaption.textContent, /SUB2API/);
|
||||
});
|
||||
|
||||
test('sidepanel enables SUB2API session strategy selection when the current Plus target supports it', () => {
|
||||
@@ -283,6 +285,7 @@ return {
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
signupMethod: 'email',
|
||||
phoneSignupReloginAfterBindEmailEnabled: false,
|
||||
accountContributionEnabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -294,6 +297,7 @@ return {
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
signupMethod: 'email',
|
||||
phoneSignupReloginAfterBindEmailEnabled: false,
|
||||
accountContributionEnabled: false,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -38,7 +38,7 @@ 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' } = {}) {
|
||||
function createPlusCheckoutMessageHarness({ checkoutSessionId = 'cs_test_123', checkoutResponse = null } = {}) {
|
||||
const attrs = new Map();
|
||||
let listener = null;
|
||||
const fetchCalls = [];
|
||||
@@ -84,7 +84,7 @@ function createPlusCheckoutMessageHarness({ checkoutSessionId = 'cs_test_123' }
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ checkout_session_id: checkoutSessionId }),
|
||||
json: async () => (checkoutResponse || { checkout_session_id: checkoutSessionId }),
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected fetch url: ${url}`);
|
||||
@@ -153,6 +153,40 @@ test('CREATE_PLUS_CHECKOUT uses ID/IDR and openai_llc merchant path for GoPay',
|
||||
});
|
||||
});
|
||||
|
||||
test('CREATE_PLUS_CHECKOUT uses hosted US/USD checkout for PayPal no-card binding', async () => {
|
||||
const hostedUrl = 'https://pay.openai.com/c/pay/cs_hosted_123';
|
||||
const harness = createPlusCheckoutMessageHarness({
|
||||
checkoutSessionId: 'cs_hosted',
|
||||
checkoutResponse: {
|
||||
checkout_session_id: 'cs_hosted',
|
||||
next_action: {
|
||||
redirect_to_url: {
|
||||
url: hostedUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await harness.send({
|
||||
type: 'CREATE_PLUS_CHECKOUT',
|
||||
source: 'test',
|
||||
payload: { paymentMethod: 'paypal-hosted' },
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.checkoutUrl, 'https://chatgpt.com/checkout/openai_llc/cs_hosted');
|
||||
assert.equal(result.hostedCheckoutUrl, hostedUrl);
|
||||
assert.equal(result.preferredCheckoutUrl, hostedUrl);
|
||||
assert.equal(result.country, 'US');
|
||||
assert.equal(result.currency, 'USD');
|
||||
|
||||
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.checkout_ui_mode, 'hosted');
|
||||
assert.deepEqual(payload.billing_details, { country: 'US', currency: 'USD' });
|
||||
});
|
||||
|
||||
function extractFunction(name) {
|
||||
const plainStart = source.indexOf(`function ${name}(`);
|
||||
const asyncStart = source.indexOf(`async function ${name}(`);
|
||||
|
||||
@@ -288,6 +288,113 @@ test('GoPay plus checkout create forwards gopay payment method to the checkout c
|
||||
assert.deepStrictEqual(events[0]?.payload, { paymentMethod: 'gopay' });
|
||||
});
|
||||
|
||||
test('PayPal no-card binding create waits for hosted success and does not use the old PayPal tail', async () => {
|
||||
const events = [];
|
||||
let currentUrl = 'https://chatgpt.com/';
|
||||
const executor = api.createPlusCheckoutCreateExecutor({
|
||||
addLog: async (message, level = 'info') => {
|
||||
events.push({ type: 'log', message, level });
|
||||
},
|
||||
chrome: {
|
||||
tabs: {
|
||||
create: async (payload) => {
|
||||
events.push({ type: 'tab-create', payload });
|
||||
return { id: 55, url: payload.url, status: 'complete' };
|
||||
},
|
||||
update: async (tabId, payload) => {
|
||||
currentUrl = payload.url;
|
||||
events.push({ type: 'tab-update', tabId, payload });
|
||||
return { id: tabId, url: currentUrl, status: 'complete' };
|
||||
},
|
||||
get: async (tabId) => ({ id: tabId, url: currentUrl, 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 });
|
||||
assert.equal(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',
|
||||
},
|
||||
}),
|
||||
};
|
||||
},
|
||||
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 === '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') {
|
||||
currentUrl = 'https://chatgpt.com/backend-api/payments/success?session_id=cs_hosted';
|
||||
return { clicked: true };
|
||||
}
|
||||
if (message.type === 'PLUS_CHECKOUT_GET_STATE') {
|
||||
return { hostedVerificationVisible: false };
|
||||
}
|
||||
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.executePlusCheckoutCreate({
|
||||
plusPaymentMethod: 'paypal-hosted',
|
||||
plusHostedCheckoutOauthDelaySeconds: 0,
|
||||
});
|
||||
|
||||
assert.deepStrictEqual(
|
||||
events.find((event) => event.type === 'tab-message' && event.message.type === 'CREATE_PLUS_CHECKOUT')?.message?.payload,
|
||||
{ paymentMethod: 'paypal-hosted' }
|
||||
);
|
||||
assert.equal(
|
||||
events.find((event) => event.type === 'tab-update')?.payload?.url,
|
||||
'https://pay.openai.com/c/pay/cs_hosted'
|
||||
);
|
||||
const statePayload = events.find((event) => event.type === 'set-state')?.payload || {};
|
||||
assert.equal(statePayload.plusCheckoutSource, 'paypal-hosted');
|
||||
assert.equal(statePayload.plusCheckoutCountry, 'US');
|
||||
assert.equal(statePayload.plusCheckoutCurrency, 'USD');
|
||||
assert.equal(statePayload.plusReturnUrl, '');
|
||||
assert.equal(events.some((event) => event.type === 'tab-message' && event.message.type === 'FILL_PLUS_BILLING_AND_SUBMIT'), false);
|
||||
assert.deepStrictEqual(events.find((event) => event.type === 'complete'), {
|
||||
type: 'complete',
|
||||
step: 'plus-checkout-create',
|
||||
payload: {
|
||||
plusReturnUrl: 'https://chatgpt.com/backend-api/payments/success?session_id=cs_hosted',
|
||||
plusHostedCheckoutCompleted: true,
|
||||
plusHostedCheckoutOauthDelaySeconds: 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Plus checkout content routes billing operations through the operation delay gate', async () => {
|
||||
const { checkoutEvents, send } = createCheckoutContentHarness();
|
||||
|
||||
|
||||
@@ -123,6 +123,7 @@ return {
|
||||
plusAccountAccessStrategy: 'oauth',
|
||||
signupMethod: 'email',
|
||||
phoneSignupReloginAfterBindEmailEnabled: false,
|
||||
accountContributionEnabled: false,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [88] });
|
||||
|
||||
@@ -113,7 +113,7 @@ return {
|
||||
assert.deepEqual(api.getStepIds(), [7]);
|
||||
assert.deepEqual(api.calls[0], {
|
||||
type: 'getSteps',
|
||||
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
|
||||
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gopay', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false, accountContributionEnabled: false },
|
||||
});
|
||||
assert.deepEqual(api.calls[1], { type: 'render', stepIds: [7] });
|
||||
});
|
||||
@@ -188,6 +188,67 @@ return { updatePlusModeUI, selectPlusPaymentMethod, rowPayPalAccount };
|
||||
assert.equal(api.rowPayPalAccount.style.display, '');
|
||||
});
|
||||
|
||||
test('sidepanel Plus UI separates PayPal account mode from PayPal no-card binding mode', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizePlusPaymentMethod'),
|
||||
extractFunction('normalizePlusAccountAccessStrategy'),
|
||||
extractFunction('getSelectedPlusPaymentMethod'),
|
||||
extractFunction('getRequestedPlusAccountAccessStrategy'),
|
||||
extractFunction('normalizeGpcHelperPhoneModeValue'),
|
||||
extractFunction('getGpcHelperAutoModeEnabled'),
|
||||
extractFunction('normalizeGpcAutoModePermissionValue'),
|
||||
extractFunction('getGpcAutoModePermissionFromPayload'),
|
||||
extractFunction('shouldPreserveSelectedGpcAutoMode'),
|
||||
extractFunction('hasGpcAutoModePermissionField'),
|
||||
extractFunction('isGpcAutoModePermissionDenied'),
|
||||
extractFunction('normalizeGpcOtpChannelValue'),
|
||||
extractFunction('updatePlusModeUI'),
|
||||
].join('\n');
|
||||
|
||||
const api = new Function(`
|
||||
let latestState = { plusPaymentMethod: 'paypal-hosted' };
|
||||
let currentPlusPaymentMethod = 'paypal-hosted';
|
||||
let currentPlusAccountAccessStrategy = 'oauth';
|
||||
const inputPlusModeEnabled = { checked: true };
|
||||
const selectPlusPaymentMethod = { value: 'paypal-hosted', style: { display: 'none' } };
|
||||
const GPC_HELPER_PHONE_MODE_AUTO = 'auto';
|
||||
const GPC_HELPER_PHONE_MODE_MANUAL = 'manual';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH = 'oauth';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_SUB2API_CODEX_SESSION = 'sub2api_codex_session';
|
||||
const PLUS_ACCOUNT_ACCESS_STRATEGY_CPA_CODEX_SESSION = 'cpa_codex_session';
|
||||
const DEFAULT_PLUS_ACCOUNT_ACCESS_STRATEGY = PLUS_ACCOUNT_ACCESS_STRATEGY_OAUTH;
|
||||
const plusPaymentMethodCaption = { textContent: '' };
|
||||
const rowPayPalAccount = { style: { display: '' } };
|
||||
const rowHostedCheckoutVerificationUrl = { style: { display: 'none' } };
|
||||
const rowHostedCheckoutPhone = { style: { display: 'none' } };
|
||||
const rowPlusHostedCheckoutOauthDelay = { style: { display: 'none' } };
|
||||
${bundle}
|
||||
return {
|
||||
updatePlusModeUI,
|
||||
selectPlusPaymentMethod,
|
||||
rowPayPalAccount,
|
||||
plusPaymentMethodCaption,
|
||||
rows: { rowHostedCheckoutVerificationUrl, rowHostedCheckoutPhone, rowPlusHostedCheckoutOauthDelay },
|
||||
};
|
||||
`)();
|
||||
|
||||
api.updatePlusModeUI();
|
||||
|
||||
assert.equal(api.rowPayPalAccount.style.display, 'none');
|
||||
assert.equal(api.rows.rowHostedCheckoutVerificationUrl.style.display, '');
|
||||
assert.equal(api.rows.rowHostedCheckoutPhone.style.display, '');
|
||||
assert.equal(api.rows.rowPlusHostedCheckoutOauthDelay.style.display, '');
|
||||
assert.match(api.plusPaymentMethodCaption.textContent, /无卡直绑/);
|
||||
|
||||
api.selectPlusPaymentMethod.value = 'paypal';
|
||||
api.updatePlusModeUI();
|
||||
|
||||
assert.equal(api.rowPayPalAccount.style.display, '');
|
||||
assert.equal(api.rows.rowHostedCheckoutVerificationUrl.style.display, 'none');
|
||||
assert.equal(api.rows.rowHostedCheckoutPhone.style.display, 'none');
|
||||
assert.equal(api.rows.rowPlusHostedCheckoutOauthDelay.style.display, 'none');
|
||||
});
|
||||
|
||||
test('sidepanel Plus UI can hide Plus controls when the shared flow capability registry disables them', () => {
|
||||
const bundle = [
|
||||
extractFunction('normalizePlusPaymentMethod'),
|
||||
@@ -303,7 +364,7 @@ return {
|
||||
assert.deepEqual(api.getStepIds(), [13]);
|
||||
assert.deepEqual(api.calls[0], {
|
||||
type: 'getSteps',
|
||||
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false },
|
||||
options: { activeFlowId: 'openai', plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', plusAccountAccessStrategy: 'oauth', signupMethod: 'email', phoneSignupReloginAfterBindEmailEnabled: false, accountContributionEnabled: false },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
phoneSignupReloginAfterBindEmailEnabled: true,
|
||||
});
|
||||
const plusSteps = api.getSteps({ plusModeEnabled: true });
|
||||
const hostedSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' });
|
||||
const plusPhoneSteps = api.getSteps({ plusModeEnabled: true, signupMethod: 'phone' });
|
||||
const plusPhoneReloginSteps = api.getSteps({
|
||||
plusModeEnabled: true,
|
||||
@@ -211,6 +212,27 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
|
||||
assert.equal(plusSteps[5].title, '创建 Plus Checkout');
|
||||
assert.equal(plusSteps[7].title, 'PayPal 登录与授权');
|
||||
|
||||
assert.deepStrictEqual(
|
||||
hostedSteps.map((step) => step.key),
|
||||
[
|
||||
'open-chatgpt',
|
||||
'submit-signup-email',
|
||||
'fill-password',
|
||||
'fetch-signup-code',
|
||||
'fill-profile',
|
||||
'plus-checkout-create',
|
||||
'oauth-login',
|
||||
'fetch-login-code',
|
||||
'confirm-oauth',
|
||||
'platform-verify',
|
||||
]
|
||||
);
|
||||
assert.equal(hostedSteps.some((step) => step.key === 'plus-checkout-billing'), false);
|
||||
assert.equal(hostedSteps.some((step) => step.key === 'paypal-approve'), false);
|
||||
assert.equal(hostedSteps.some((step) => step.key === 'plus-checkout-return'), false);
|
||||
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'paypal-hosted' }), 10);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
goPaySteps.map((step) => step.key),
|
||||
[
|
||||
@@ -279,6 +301,16 @@ test('Plus session strategy swaps the OAuth tail for a single SUB2API import nod
|
||||
previousNodeId: 'plus-checkout-return',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
},
|
||||
{
|
||||
label: 'paypal-hosted',
|
||||
options: {
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal-hosted',
|
||||
plusAccountAccessStrategy: 'sub2api_codex_session',
|
||||
},
|
||||
previousNodeId: 'plus-checkout-create',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7],
|
||||
},
|
||||
{
|
||||
label: 'gopay',
|
||||
options: {
|
||||
@@ -360,6 +392,16 @@ test('Plus session strategy swaps the OAuth tail for a single CPA import node',
|
||||
previousNodeId: 'plus-checkout-return',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
||||
},
|
||||
{
|
||||
label: 'paypal-hosted',
|
||||
options: {
|
||||
plusModeEnabled: true,
|
||||
plusPaymentMethod: 'paypal-hosted',
|
||||
plusAccountAccessStrategy: 'cpa_codex_session',
|
||||
},
|
||||
previousNodeId: 'plus-checkout-create',
|
||||
expectedStepIds: [1, 2, 3, 4, 5, 6, 7],
|
||||
},
|
||||
{
|
||||
label: 'gopay',
|
||||
options: {
|
||||
|
||||
Reference in New Issue
Block a user