Plus中添加GPC模式

This commit is contained in:
QLHazyCoder
2026-05-04 18:02:00 +08:00
parent 6160f4640d
commit b595f5366a
17 changed files with 2151 additions and 64 deletions
+212 -14
View File
@@ -62,11 +62,16 @@ const PLUS_GOPAY_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
plusModeEnabled: true,
plusPaymentMethod: 'gopay',
}) || PLUS_PAYPAL_STEP_DEFINITIONS;
const PLUS_GPC_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getSteps?.({
plusModeEnabled: true,
plusPaymentMethod: 'gpc-helper',
}) || PLUS_GOPAY_STEP_DEFINITIONS;
const PLUS_STEP_DEFINITIONS = PLUS_PAYPAL_STEP_DEFINITIONS;
const ALL_STEP_DEFINITIONS = self.MultiPageStepDefinitions?.getAllSteps?.() || [
...NORMAL_STEP_DEFINITIONS,
...PLUS_PAYPAL_STEP_DEFINITIONS,
...PLUS_GOPAY_STEP_DEFINITIONS,
...PLUS_GPC_STEP_DEFINITIONS,
];
const STEP_IDS = Array.from(new Set(ALL_STEP_DEFINITIONS
.map((definition) => Number(definition?.id))
@@ -84,11 +89,16 @@ const PLUS_GOPAY_STEP_IDS = PLUS_GOPAY_STEP_DEFINITIONS
.map((definition) => Number(definition?.id))
.filter(Number.isFinite)
.sort((left, right) => left - right);
const PLUS_GPC_STEP_IDS = PLUS_GPC_STEP_DEFINITIONS
.map((definition) => Number(definition?.id))
.filter(Number.isFinite)
.sort((left, right) => left - right);
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_GOPAY_STEP_IDS[PLUS_GOPAY_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
);
const FINAL_OAUTH_CHAIN_START_STEP = 7;
@@ -367,6 +377,7 @@ const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(HERO_SMS_SUPPORTED_COUNTRY_IDS
const FIVE_SIM_OPERATOR = DEFAULT_FIVE_SIM_OPERATOR;
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
const DISPLAY_TIMEZONE = 'Asia/Shanghai';
const MICROSOFT_TOKEN_DNR_RULE_ID = 1001;
@@ -407,7 +418,11 @@ function isPlusModeState(state = {}) {
}
function normalizePlusPaymentMethod(value = '') {
return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal';
const normalized = String(value || '').trim().toLowerCase();
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_PAYMENT_METHOD_GPC_HELPER;
}
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
}
function normalizeContributionModeSource(value = '') {
@@ -460,9 +475,11 @@ function getStepDefinitionsForState(state = {}) {
if (!isPlusModeState(state)) {
return NORMAL_STEP_DEFINITIONS;
}
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY
? PLUS_GOPAY_STEP_DEFINITIONS
: PLUS_PAYPAL_STEP_DEFINITIONS;
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_GPC_STEP_DEFINITIONS;
}
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_DEFINITIONS : PLUS_PAYPAL_STEP_DEFINITIONS;
}
function getStepIdsForState(state = {}) {
@@ -476,9 +493,11 @@ function getStepIdsForState(state = {}) {
if (!isPlusModeState(state)) {
return NORMAL_STEP_IDS;
}
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY
? PLUS_GOPAY_STEP_IDS
: PLUS_PAYPAL_STEP_IDS;
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_GPC_STEP_IDS;
}
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_IDS : PLUS_PAYPAL_STEP_IDS;
}
function getLastStepIdForState(state = {}) {
@@ -579,6 +598,22 @@ const PERSISTED_SETTING_DEFAULTS = {
gopayPhone: '',
gopayOtp: '',
gopayPin: '',
gopayHelperApiUrl: '',
gopayHelperCardKey: '',
gopayHelperPhoneNumber: '',
gopayHelperCountryCode: '+86',
gopayHelperPin: '',
gopayHelperReferenceId: '',
gopayHelperGoPayGuid: '',
gopayHelperRedirectUrl: '',
gopayHelperNextAction: '',
gopayHelperFlowId: '',
gopayHelperChallengeId: '',
gopayHelperStartPayload: null,
gopayHelperBalance: '',
gopayHelperBalancePayload: null,
gopayHelperBalanceUpdatedAt: 0,
gopayHelperBalanceError: '',
autoRunSkipFailures: false,
autoRunFallbackThreadIntervalMinutes: 0,
oauthFlowTimeoutEnabled: true,
@@ -714,6 +749,7 @@ const DEFAULT_STATE = {
plusCheckoutUrl: null, // Plus checkout 运行时短链,不写入持久配置。
plusCheckoutCountry: 'DE',
plusCheckoutCurrency: 'EUR',
plusCheckoutSource: '',
plusBillingCountryText: '',
plusBillingAddress: null,
plusPaypalApprovedAt: null,
@@ -725,6 +761,17 @@ const DEFAULT_STATE = {
plusManualConfirmationMethod: '',
plusManualConfirmationTitle: '',
plusManualConfirmationMessage: '',
gopayHelperReferenceId: '',
gopayHelperGoPayGuid: '',
gopayHelperRedirectUrl: '',
gopayHelperNextAction: '',
gopayHelperFlowId: '',
gopayHelperChallengeId: '',
gopayHelperStartPayload: null,
gopayHelperPinPayload: null,
gopayHelperResolvedOtp: '',
gopayHelperOtpRequestId: '',
gopayHelperOtpReferenceId: '',
flowStartTime: null, // 当前流程开始时间。
tabRegistry: {}, // 程序维护的标签页注册表。
sourceLastUrls: {}, // 各来源页面最近一次打开的地址记录。
@@ -1136,9 +1183,11 @@ function normalizePlusPaymentMethod(value = '') {
if (rootScope.GoPayUtils?.normalizePlusPaymentMethod) {
return rootScope.GoPayUtils.normalizePlusPaymentMethod(value);
}
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY
? PLUS_PAYMENT_METHOD_GOPAY
: PLUS_PAYMENT_METHOD_PAYPAL;
const normalized = String(value || '').trim().toLowerCase();
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_PAYMENT_METHOD_GPC_HELPER;
}
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
}
function normalizeFiveSimCountryId(value, fallback = FIVE_SIM_COUNTRY_ID) {
@@ -2125,6 +2174,37 @@ function normalizePersistentSettingValue(key, value) {
return self.GoPayUtils?.normalizeGoPayPin
? self.GoPayUtils.normalizeGoPayPin(value)
: String(value || '');
case 'gopayHelperPhoneNumber':
return self.GoPayUtils?.normalizeGoPayPhone
? self.GoPayUtils.normalizeGoPayPhone(value)
: String(value || '').trim();
case 'gopayHelperPin':
return self.GoPayUtils?.normalizeGoPayPin
? self.GoPayUtils.normalizeGoPayPin(value)
: String(value || '');
case 'gopayHelperCountryCode':
return self.GoPayUtils?.normalizeGoPayCountryCode
? self.GoPayUtils.normalizeGoPayCountryCode(value)
: String(value || '+86').trim();
case 'gopayHelperApiUrl':
return self.GoPayUtils?.normalizeGpcHelperBaseUrl
? self.GoPayUtils.normalizeGpcHelperBaseUrl(value)
: String(value || '').trim().replace(/\/+$/g, '');
case 'gopayHelperCardKey':
case 'gopayHelperReferenceId':
case 'gopayHelperGoPayGuid':
case 'gopayHelperRedirectUrl':
case 'gopayHelperNextAction':
case 'gopayHelperFlowId':
case 'gopayHelperChallengeId':
case 'gopayHelperBalance':
case 'gopayHelperBalanceError':
return String(value || '').trim();
case 'gopayHelperBalancePayload':
case 'gopayHelperStartPayload':
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
case 'gopayHelperBalanceUpdatedAt':
return Math.max(0, Number(value) || 0);
case 'autoRunSkipFailures':
case 'oauthFlowTimeoutEnabled':
case 'autoRunDelayEnabled':
@@ -7143,6 +7223,7 @@ function getDownstreamStateResets(step, state = {}) {
plusCheckoutUrl: null,
plusCheckoutCountry: 'DE',
plusCheckoutCurrency: 'EUR',
plusCheckoutSource: '',
plusBillingCountryText: '',
plusBillingAddress: null,
plusPaypalApprovedAt: null,
@@ -7154,6 +7235,17 @@ function getDownstreamStateResets(step, state = {}) {
plusManualConfirmationMethod: '',
plusManualConfirmationTitle: '',
plusManualConfirmationMessage: '',
gopayHelperReferenceId: '',
gopayHelperGoPayGuid: '',
gopayHelperRedirectUrl: '',
gopayHelperNextAction: '',
gopayHelperFlowId: '',
gopayHelperChallengeId: '',
gopayHelperStartPayload: null,
gopayHelperPinPayload: null,
gopayHelperResolvedOtp: '',
gopayHelperOtpRequestId: '',
gopayHelperOtpReferenceId: '',
};
if (step <= 1) {
@@ -7237,6 +7329,9 @@ function getDownstreamStateResets(step, state = {}) {
plusManualConfirmationMethod: '',
plusManualConfirmationTitle: '',
plusManualConfirmationMessage: '',
gopayHelperResolvedOtp: '',
gopayHelperOtpRequestId: '',
gopayHelperOtpReferenceId: '',
} : {}),
...(step === 8 ? {
plusPaypalApprovedAt: null,
@@ -9188,6 +9283,100 @@ async function maybeSwitchIpProxyAfterAutoRunRoundSuccess(payload = {}) {
return switchResult;
}
function resolveGpcHelperBaseUrl(apiUrl = '') {
if (self.GoPayUtils?.normalizeGpcHelperBaseUrl) {
return self.GoPayUtils.normalizeGpcHelperBaseUrl(apiUrl);
}
let normalized = String(apiUrl || '').trim().replace(/\/+$/g, '');
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, '');
normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, '');
return normalized;
}
function buildGpcCardBalanceRequestUrl(apiUrl = '', cardKey = '') {
if (self.GoPayUtils?.buildGpcCardBalanceUrl) {
return self.GoPayUtils.buildGpcCardBalanceUrl(apiUrl, cardKey);
}
const baseUrl = resolveGpcHelperBaseUrl(apiUrl);
if (!baseUrl) {
return '';
}
return `${baseUrl}/api/card/balance?card_key=${encodeURIComponent(String(cardKey || '').trim())}`;
}
function formatGpcCardBalancePayload(payload = {}) {
if (self.GoPayUtils?.formatGpcBalancePayload) {
return self.GoPayUtils.formatGpcBalancePayload(payload);
}
if (!payload || typeof payload !== 'object') {
return '';
}
const remaining = payload.remaining_uses ?? payload.remainingUses ?? payload.balance ?? payload.remaining;
const status = String(payload.card_status || payload.cardStatus || payload.status || '').trim();
return [
remaining !== undefined && remaining !== null && String(remaining).trim() !== '' ? `余额 ${remaining}` : '',
status ? `状态 ${status}` : '',
].filter(Boolean).join('');
}
async function refreshGpcCardBalance(state = {}, options = {}) {
const apiUrl = String(state?.gopayHelperApiUrl || '').trim();
const cardKey = String(state?.gopayHelperCardKey || state?.gpcCardKey || state?.cardKey || '').trim();
if (!apiUrl) {
throw new Error('缺少 GPC API 地址。');
}
if (!cardKey) {
throw new Error('缺少 GPC 卡密。');
}
const requestUrl = buildGpcCardBalanceRequestUrl(apiUrl, cardKey);
if (!requestUrl) {
throw new Error('缺少 GPC API 地址。');
}
const response = await fetch(requestUrl, {
method: 'GET',
headers: { Accept: 'application/json' },
});
const rawText = await response.text();
let payload = {};
try {
payload = rawText ? JSON.parse(rawText) : {};
} catch {
payload = { raw: rawText };
}
const balanceText = formatGpcCardBalancePayload(payload) || rawText || '未知';
const updates = {
gopayHelperBalance: balanceText,
gopayHelperBalancePayload: payload && typeof payload === 'object' && !Array.isArray(payload) ? payload : { raw: String(payload || '') },
gopayHelperBalanceUpdatedAt: Date.now(),
gopayHelperBalanceError: '',
};
const flowId = String(payload?.flow_id || payload?.flowId || '').trim();
if (flowId) {
updates.gopayHelperFlowId = flowId;
}
if (!response.ok || payload?.ok === false) {
const detail = payload?.error || payload?.message || payload?.detail || `HTTP ${response.status}`;
const errorUpdates = { ...updates, gopayHelperBalanceError: String(detail || '余额查询失败') };
await setPersistentSettings(errorUpdates);
broadcastDataUpdate(errorUpdates);
throw new Error(String(detail || '余额查询失败'));
}
await setPersistentSettings(updates);
broadcastDataUpdate(updates);
const reason = String(options?.reason || '').trim();
await addLog(
reason === 'round_success'
? `GPC 余额已更新:${balanceText}`
: `GPC 余额查询成功:${balanceText}`,
'info'
);
return { balance: balanceText, payload, updatedAt: updates.gopayHelperBalanceUpdatedAt };
}
const autoRunController = self.MultiPageBackgroundAutoRunController?.createAutoRunController({
addLog,
appendAccountRunRecord: (...args) => appendAndBroadcastAccountRunRecord(...args),
@@ -10109,6 +10298,8 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c
chrome,
completeStepFromBackground,
ensureContentScriptReadyOnTabUntilStopped,
fetch: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
markCurrentRegistrationAccountUsed,
registerTab,
sendTabMessageUntilStopped,
setState,
@@ -10118,18 +10309,21 @@ const plusCheckoutCreateExecutor = self.MultiPageBackgroundPlusCheckoutCreate?.c
});
const plusCheckoutBillingExecutor = self.MultiPageBackgroundPlusCheckoutBilling?.createPlusCheckoutBillingExecutor({
addLog,
broadcastDataUpdate,
chrome,
completeStepFromBackground,
ensureContentScriptReadyOnTabUntilStopped,
fetch: typeof fetch === 'function' ? fetch.bind(globalThis) : null,
generateRandomName,
getAddressSeedForCountry: self.MultiPageAddressSources?.getAddressSeedForCountry,
getState,
getTabId,
isTabAlive,
markCurrentRegistrationAccountUsed,
sendTabMessageUntilStopped,
setState,
sleepWithStop,
throwIfStopped,
waitForTabCompleteUntilStopped,
waitForTabUrlMatchUntilStopped,
probeIpProxyExit,
@@ -10253,6 +10447,7 @@ const messageRouter = self.MultiPageBackgroundMessageRouter?.createMessageRouter
executeStepViaCompletionSignal,
exportSettingsBundle,
fetchGeneratedEmail,
refreshGpcCardBalance,
finalizePhoneActivationAfterSuccessfulFlow,
finalizeStep3Completion: async () => {
const currentState = await getState();
@@ -10360,14 +10555,17 @@ function buildStepRegistry(definitions = []) {
const normalStepRegistry = buildStepRegistry(NORMAL_STEP_DEFINITIONS);
const plusPayPalStepRegistry = buildStepRegistry(PLUS_PAYPAL_STEP_DEFINITIONS);
const plusGoPayStepRegistry = buildStepRegistry(PLUS_GOPAY_STEP_DEFINITIONS);
const plusGpcStepRegistry = buildStepRegistry(PLUS_GPC_STEP_DEFINITIONS);
function getStepRegistryForState(state = {}) {
if (!isPlusModeState(state)) {
return normalStepRegistry;
}
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GOPAY
? plusGoPayStepRegistry
: plusPayPalStepRegistry;
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return plusGpcStepRegistry;
}
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? plusGoPayStepRegistry : plusPayPalStepRegistry;
}
async function requestOAuthUrlFromPanel(state, options = {}) {
+61 -16
View File
@@ -33,6 +33,7 @@
executeStepViaCompletionSignal,
exportSettingsBundle,
fetchGeneratedEmail,
refreshGpcCardBalance,
finalizePhoneActivationAfterSuccessfulFlow,
finalizeStep3Completion,
finalizeIcloudAliasAfterSuccessfulFlow,
@@ -203,6 +204,22 @@
}) || null;
}
function normalizePlusPaymentMethodForDisplay(value = '') {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'gpc-helper') {
return 'gpc-helper';
}
return normalized === 'gopay' ? 'gopay' : 'paypal';
}
function getPlusPaymentMethodLabel(value = '') {
const method = normalizePlusPaymentMethodForDisplay(value);
if (method === 'gpc-helper') {
return 'GPC';
}
return method === 'gopay' ? 'GoPay' : 'PayPal';
}
async function handlePlatformVerifyStepData(payload) {
if (payload.localhostUrl) {
await closeLocalhostCallbackTabs(payload.localhostUrl);
@@ -550,6 +567,8 @@
const confirmed = Boolean(message.payload?.confirmed);
const requestId = String(message.payload?.requestId || '').trim();
const currentRequestId = String(currentState?.plusManualConfirmationRequestId || '').trim();
const method = String(currentState?.plusManualConfirmationMethod || '').trim().toLowerCase();
const isGpcOtp = method === 'gopay-otp';
if (!currentState?.plusManualConfirmationPending) {
return { ok: true, ignored: true };
}
@@ -565,15 +584,31 @@
plusManualConfirmationTitle: '',
plusManualConfirmationMessage: '',
};
if (isGpcOtp && confirmed) {
const otp = String(message.payload?.otp || message.payload?.code || '').trim().replace(/[^\d]/g, '');
if (!otp) {
throw new Error('请输入 GPC OTP 验证码。');
}
const otpUpdates = {
...clearManualConfirmationState,
gopayHelperResolvedOtp: otp,
};
await setState(otpUpdates);
if (typeof broadcastDataUpdate === 'function') {
broadcastDataUpdate(otpUpdates);
}
await addLog(`步骤 ${step}:已收到 GPC OTP,准备提交验证。`, 'ok');
return { ok: true };
}
await setState(clearManualConfirmationState);
if (typeof broadcastDataUpdate === 'function') {
broadcastDataUpdate(clearManualConfirmationState);
}
if (confirmed) {
const methodLabel = String(currentState?.plusManualConfirmationMethod || '').trim().toLowerCase() === 'gopay'
? 'GoPay'
: '手动';
const methodLabel = method === 'gopay' ? 'GoPay' : '手动';
await addLog(`步骤 ${step}:已确认${methodLabel}订阅完成,准备继续下一步。`, 'ok');
await completeStepFromBackground(step, {
plusManualConfirmationMethod: currentState?.plusManualConfirmationMethod || '',
@@ -582,9 +617,9 @@
return { ok: true };
}
const cancelMessage = String(currentState?.plusManualConfirmationMethod || '').trim().toLowerCase() === 'gopay'
const cancelMessage = method === 'gopay'
? '已取消 GoPay 订阅确认'
: '已取消当前手动确认';
: (isGpcOtp ? '已取消 GPC OTP 输入' : '已取消当前手动确认');
await setStepStatus(step, 'failed');
await addLog(`步骤 ${step}${cancelMessage}`, 'warn');
await appendManualAccountRunRecordIfNeeded(`step${step}_failed`, null, cancelMessage);
@@ -836,8 +871,8 @@
const modeChanged = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
&& Boolean(currentState?.plusModeEnabled) !== Boolean(updates.plusModeEnabled);
const plusPaymentChanged = Object.prototype.hasOwnProperty.call(updates, 'plusPaymentMethod')
&& String(currentState?.plusPaymentMethod || 'paypal').trim().toLowerCase()
!== String(updates.plusPaymentMethod || 'paypal').trim().toLowerCase();
&& normalizePlusPaymentMethodForDisplay(currentState?.plusPaymentMethod || 'paypal')
!== normalizePlusPaymentMethodForDisplay(updates.plusPaymentMethod || 'paypal');
const nextPlusModeEnabled = Object.prototype.hasOwnProperty.call(updates, 'plusModeEnabled')
? Boolean(updates.plusModeEnabled)
: Boolean(currentState?.plusModeEnabled);
@@ -912,11 +947,9 @@
await setContributionMode(true);
}
if (modeChanged) {
const selectedPlusPaymentMethod = String(
(stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal')
).trim().toLowerCase() === 'gopay'
? 'GoPay'
: 'PayPal';
const selectedPlusPaymentMethod = getPlusPaymentMethodLabel(
stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal'
);
await addLog(
Boolean(updates.plusModeEnabled)
? `Plus 模式已开启,已切换为 Plus Checkout 步骤,当前支付方式:${selectedPlusPaymentMethod}`
@@ -924,16 +957,28 @@
'info'
);
} else if (plusPaymentChanged && nextPlusModeEnabled) {
const selectedPlusPaymentMethod = String(
const selectedPlusPaymentMethod = getPlusPaymentMethodLabel(
stateUpdates.plusPaymentMethod ?? currentState?.plusPaymentMethod ?? 'paypal'
).trim().toLowerCase() === 'gopay'
? 'GoPay'
: 'PayPal';
);
await addLog(`Plus 支付方式已切换为 ${selectedPlusPaymentMethod},已更新对应的 Plus 步骤。`, 'info');
}
return { ok: true, state: await getState(), proxyRouting };
}
case 'REFRESH_GPC_CARD_BALANCE': {
if (typeof refreshGpcCardBalance !== 'function') {
throw new Error('GPC 卡密余额查询能力尚未接入。');
}
const state = await getState();
const result = await refreshGpcCardBalance({
...(state || {}),
...(message.payload || {}),
}, {
reason: message.payload?.reason,
});
return { ok: true, ...result };
}
case 'RUN_IP_PROXY_AUTO_SYNC_NOW': {
if (typeof runIpProxyAutoSync !== 'function') {
throw new Error('IP 代理自动同步能力尚未接入。');
+359 -9
View File
@@ -4,7 +4,9 @@
const PLUS_CHECKOUT_SOURCE = 'plus-checkout';
const PLUS_CHECKOUT_ENTRY_URL = 'https://chatgpt.com/';
const PLUS_CHECKOUT_INJECT_FILES = ['content/utils.js', 'content/plus-checkout.js'];
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
function createPlusCheckoutCreateExecutor(deps = {}) {
const {
@@ -12,21 +14,42 @@
chrome,
completeStepFromBackground,
ensureContentScriptReadyOnTabUntilStopped,
fetch: fetchImpl = null,
markCurrentRegistrationAccountUsed = null,
registerTab,
sendTabMessageUntilStopped,
setState,
sleepWithStop,
waitForTabCompleteUntilStopped,
throwIfStopped = () => {},
} = deps;
function normalizePlusPaymentMethod(value = '') {
return String(value || '').trim().toLowerCase() === 'gopay' ? 'gopay' : 'paypal';
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.normalizePlusPaymentMethod) {
return rootScope.GoPayUtils.normalizePlusPaymentMethod(value);
}
const normalized = String(value || '').trim().toLowerCase();
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_PAYMENT_METHOD_GPC_HELPER;
}
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
}
function getCheckoutModeLabel(state = {}) {
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === 'gopay'
? 'GoPay 订阅页'
: 'Plus Checkout';
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return 'GPC 订阅页';
}
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay 订阅页' : 'Plus Checkout';
}
function getPlusPaymentMethodLabel(method = PLUS_PAYMENT_METHOD_PAYPAL) {
const paymentMethod = normalizePlusPaymentMethod(method);
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return 'GPC';
}
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay' : 'PayPal';
}
async function openFreshChatGptTabForCheckoutCreate() {
@@ -41,19 +64,345 @@
return tabId;
}
function normalizePlusPaymentMethod(value = '') {
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY ? 'gopay' : 'paypal';
function normalizeHelperCountryCode(countryCode = '86') {
const digits = String(countryCode || '').replace(/\D/g, '');
return digits || '86';
}
function getPlusPaymentMethodLabel(method = 'paypal') {
return normalizePlusPaymentMethod(method) === PLUS_PAYMENT_METHOD_GOPAY ? 'GoPay' : 'PayPal';
function normalizeHelperPhoneNumber(phone = '', countryCode = '86') {
const cleaned = String(phone || '').replace(/\D/g, '');
const countryDigits = normalizeHelperCountryCode(countryCode);
if (countryDigits && cleaned.startsWith(countryDigits) && cleaned.length > countryDigits.length) {
return cleaned.slice(countryDigits.length);
}
return cleaned;
}
function resolveGpcHelperCardKey(state = {}) {
const cardKey = String(state?.gopayHelperCardKey || state?.gpcCardKey || state?.cardKey || '').trim();
if (!cardKey) {
throw new Error('创建 GPC 订单失败:缺少卡密。');
}
return cardKey;
}
function resolveGpcHelperCustomerEmail(state = {}) {
const email = String(
state?.email
|| state?.currentEmail
|| state?.registrationEmail
|| state?.accountEmail
|| state?.mailboxEmail
|| ''
).trim().toLowerCase();
if (!email) {
throw new Error('创建 GPC 订单失败:缺少当前轮邮箱。');
}
return email;
}
function parseGpcAmount(value) {
if (typeof value === 'number') {
return Number.isFinite(value) ? { amount: value, raw: String(value) } : null;
}
if (typeof value !== 'string') {
return null;
}
const raw = String(value || '').trim();
if (!raw || !/\d/.test(raw)) {
return null;
}
const match = raw.match(/([+-]?\d{1,3}(?:[.,]\d{3})*(?:[.,]\d{1,2})|[+-]?\d+(?:[.,]\d{1,2})?)/);
if (!match) {
return null;
}
let numericText = String(match[1] || '').trim();
const lastComma = numericText.lastIndexOf(',');
const lastDot = numericText.lastIndexOf('.');
if (lastComma > -1 && lastDot > -1) {
const decimalSeparator = lastComma > lastDot ? ',' : '.';
const thousandsSeparator = decimalSeparator === ',' ? '.' : ',';
numericText = numericText
.replace(new RegExp(`\\${thousandsSeparator}`, 'g'), '')
.replace(decimalSeparator, '.');
} else if (lastComma > -1) {
numericText = numericText.replace(',', '.');
}
const amount = Number(numericText.replace(/[^\d.+-]/g, ''));
return Number.isFinite(amount) ? { amount, raw } : null;
}
function isGpcAmountKey(key = '') {
const normalized = String(key || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '_');
if (!normalized) {
return false;
}
if (/(?:^|_)(?:id|guid|uuid|phone|country|postal|zip|code|count|status|time|timestamp|created|updated|expires|challenge|client|reference|currency|state)(?:_|$)/i.test(normalized)) {
return false;
}
return /(?:amount|balance|total|due|payable|gross|subtotal|price|charge)/i.test(normalized);
}
function findGpcNonZeroAmount(payload = {}) {
const seen = new Set();
function visit(value, path = [], depth = 0) {
if (value == null || depth > 10) {
return null;
}
const key = path[path.length - 1] || '';
if (isGpcAmountKey(key)) {
const parsed = parseGpcAmount(value);
if (parsed && Math.abs(parsed.amount) >= 0.005) {
return { ...parsed, path: path.join('.') };
}
}
if (typeof value !== 'object') {
return null;
}
if (seen.has(value)) {
return null;
}
seen.add(value);
if (Array.isArray(value)) {
for (let index = 0; index < value.length; index += 1) {
const found = visit(value[index], [...path, String(index)], depth + 1);
if (found) return found;
}
return null;
}
for (const [childKey, childValue] of Object.entries(value)) {
const found = visit(childValue, [...path, childKey], depth + 1);
if (found) return found;
}
return null;
}
return visit(payload);
}
async function abortGpcNonFreeTrialIfNeeded(data = {}, state = {}) {
const nonZeroAmount = findGpcNonZeroAmount(data);
if (!nonZeroAmount) {
return;
}
const amountLabel = nonZeroAmount.raw || String(nonZeroAmount.amount);
await addLog(`步骤 6GPC 接口返回余额非 0${amountLabel}),当前账号没有免费试用资格,将跳过当前账号。`, 'warn');
if (typeof markCurrentRegistrationAccountUsed === 'function') {
await markCurrentRegistrationAccountUsed(state, {
reason: 'plus-checkout-non-free-trial',
logPrefix: 'GPC:当前账号没有免费试用资格',
});
}
throw new Error(`PLUS_CHECKOUT_NON_FREE_TRIAL::步骤 6GPC 接口返回余额非 0(${amountLabel}),当前账号没有免费试用资格,已跳过支付提交。`);
}
function normalizeGpcHelperBaseUrl(apiUrl = '') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.normalizeGpcHelperBaseUrl) {
return rootScope.GoPayUtils.normalizeGpcHelperBaseUrl(apiUrl);
}
let normalized = String(apiUrl || '').trim().replace(/\/+$/g, '');
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, '');
normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, '');
return normalized;
}
function buildGpcHelperApiUrl(apiUrl = '', path = '') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.buildGpcHelperApiUrl) {
return rootScope.GoPayUtils.buildGpcHelperApiUrl(apiUrl, path);
}
const baseUrl = normalizeGpcHelperBaseUrl(apiUrl);
if (!baseUrl) {
return '';
}
const normalizedPath = String(path || '').startsWith('/') ? String(path || '') : `/${String(path || '')}`;
return `${baseUrl}${normalizedPath}`;
}
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) {
const fetcher = typeof fetchImpl === 'function'
? fetchImpl
: (typeof fetch === 'function' ? fetch.bind(globalThis) : null);
if (typeof fetcher !== 'function') {
throw new Error('当前运行环境不支持 fetch,无法调用 GPC API。');
}
const controller = typeof AbortController === 'function' ? new AbortController() : null;
const timer = controller ? setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || 30000)) : null;
try {
const response = await fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) });
const data = await response.json().catch(() => ({}));
return { response, data };
} finally {
if (timer) clearTimeout(timer);
}
}
async function readAccessTokenFromChatGptSessionTab(tabId) {
await waitForTabCompleteUntilStopped(tabId);
await sleepWithStop(1000);
await ensureContentScriptReadyOnTabUntilStopped(PLUS_CHECKOUT_SOURCE, tabId, {
inject: PLUS_CHECKOUT_INJECT_FILES,
injectSource: PLUS_CHECKOUT_SOURCE,
logMessage: '步骤 6:正在等待 ChatGPT 页面完成加载,再继续获取 accessToken...',
});
const sessionResult = await sendTabMessageUntilStopped(tabId, PLUS_CHECKOUT_SOURCE, {
type: 'PLUS_CHECKOUT_GET_STATE',
source: 'background',
payload: {
includeSession: true,
includeAccessToken: true,
},
});
if (sessionResult?.error) {
throw new Error(sessionResult.error);
}
return String(sessionResult?.accessToken || sessionResult?.session?.accessToken || '').trim();
}
async function generateGpcCheckoutFromApi(accessToken = '', state = {}) {
const token = String(accessToken || '').trim();
if (!token) {
throw new Error('创建 GPC 订单失败:缺少 accessToken。');
}
const apiUrl = buildGpcHelperApiUrl(state?.gopayHelperApiUrl, '/api/checkout/start');
if (!apiUrl) {
throw new Error('创建 GPC 订单失败:缺少 API 地址。');
}
const phoneNumber = String(state?.gopayHelperPhoneNumber || '').trim();
const countryCode = normalizeHelperCountryCode(state?.gopayHelperCountryCode || '86');
const pin = String(state?.gopayHelperPin || '').trim();
const cardKey = resolveGpcHelperCardKey(state);
if (!phoneNumber) {
throw new Error('创建 GPC 订单失败:缺少手机号。');
}
if (!pin) {
throw new Error('创建 GPC 订单失败:缺少 PIN。');
}
throwIfStopped();
const payload = {
token,
entry_point: 'all_plans_pricing_modal',
plan_name: 'chatgptplusplan',
billing_details: { country: 'ID', currency: 'IDR' },
promo_campaign: {
promo_campaign_id: 'plus-1-month-free',
is_coupon_from_query_param: false,
},
checkout_ui_mode: 'custom',
proxy: { type: 'direct', url: '' },
tax_region: {
country: 'US',
line1: '1208 Oakdale Street',
city: 'Jonesboro',
postal_code: '72401',
state: 'AR',
},
customer_email: resolveGpcHelperCustomerEmail(state),
card_key: cardKey,
gopay_link: {
type: 'gopay',
country_code: countryCode,
phone_number: normalizeHelperPhoneNumber(phoneNumber, countryCode),
},
};
const { response, data } = await fetchJsonWithTimeout(apiUrl, {
method: 'POST',
headers: {
Accept: '*/*',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
}, 30000);
const referenceId = String(data?.reference_id || data?.referenceId || '').trim();
const gopayGuid = String(data?.gopay_guid || data?.gopayGuid || '').trim();
const redirectUrl = String(data?.redirect_url || data?.redirectUrl || '').trim();
const nextAction = String(data?.next_action || data?.nextAction || '').trim();
const flowId = String(data?.flow_id || data?.flowId || '').trim();
const challengeId = String(data?.challenge_id || data?.challengeId || '').trim();
if (response?.ok) {
await abortGpcNonFreeTrialIfNeeded(data, state);
}
if (!response?.ok || !referenceId) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
const detail = rootScope.GoPayUtils?.extractGpcResponseErrorDetail
? rootScope.GoPayUtils.extractGpcResponseErrorDetail(data, response?.status || 0)
: (data?.detail || data?.message || data?.error || `HTTP ${response?.status || 0}`);
throw new Error(`创建 GPC 订单失败:${detail}`);
}
return {
referenceId,
gopayGuid,
redirectUrl,
nextAction,
flowId,
challengeId,
responsePayload: data && typeof data === 'object' && !Array.isArray(data) ? data : null,
country: 'ID',
currency: 'IDR',
checkoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
};
}
async function executeGpcCheckoutCreate(state = {}) {
let accessToken = String(state?.contributionAccessToken || state?.accessToken || state?.chatgptAccessToken || '').trim();
if (!accessToken) {
await addLog('步骤 6:正在获取 accessToken...', 'info');
const tokenTabId = await openFreshChatGptTabForCheckoutCreate();
try {
accessToken = await readAccessTokenFromChatGptSessionTab(tokenTabId);
} finally {
if (chrome?.tabs?.remove && Number.isInteger(tokenTabId)) {
await chrome.tabs.remove(tokenTabId).catch(() => {});
}
}
}
if (!accessToken) {
throw new Error('步骤 6GPC 模式获取 accessToken 失败。');
}
await addLog('步骤 6:正在调用 GPC 接口创建订单...', 'info');
const result = await generateGpcCheckoutFromApi(accessToken, state);
await setState({
plusCheckoutTabId: null,
plusCheckoutUrl: '',
plusCheckoutCountry: result.country || 'ID',
plusCheckoutCurrency: result.currency || 'IDR',
plusCheckoutSource: result.checkoutSource,
gopayHelperReferenceId: result.referenceId,
gopayHelperGoPayGuid: result.gopayGuid,
gopayHelperRedirectUrl: result.redirectUrl,
gopayHelperNextAction: result.nextAction,
gopayHelperFlowId: result.flowId,
gopayHelperChallengeId: result.challengeId,
gopayHelperStartPayload: result.responsePayload,
});
await addLog('步骤 6:GPC 订单已创建,准备继续下一步。', 'info');
await completeStepFromBackground(6, {
plusCheckoutCountry: result.country || 'ID',
plusCheckoutCurrency: result.currency || 'IDR',
plusCheckoutSource: result.checkoutSource,
});
}
async function executePlusCheckoutCreate(state = {}) {
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
await executeGpcCheckoutCreate(state);
return;
}
const paymentMethodLabel = getPlusPaymentMethodLabel(paymentMethod);
const checkoutModeLabel = getCheckoutModeLabel(state);
await addLog('步骤 6:正在打开 ChatGPT 会话,准备创建 Plus Checkout...', 'info');
await addLog(`步骤 6:正在打开新的 ChatGPT 会话,准备创建${checkoutModeLabel}...`, 'info');
const tabId = await openFreshChatGptTabForCheckoutCreate();
await waitForTabCompleteUntilStopped(tabId);
@@ -92,6 +441,7 @@
plusCheckoutUrl: result.checkoutUrl,
plusCheckoutCountry: result.country || 'DE',
plusCheckoutCurrency: result.currency || 'EUR',
plusCheckoutSource: '',
});
await addLog(`步骤 6Plus Checkout 页面已就绪(${paymentMethodLabel} / ${result.country || 'DE'} ${result.currency || 'EUR'}),准备继续下一步。`, 'info');
+298 -3
View File
@@ -9,6 +9,7 @@
const PLUS_CHECKOUT_PAYPAL_REDIRECT_TIMEOUT_MS = 20000;
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const PAYMENT_METHOD_CONFIGS = {
[PLUS_PAYMENT_METHOD_PAYPAL]: {
id: PLUS_PAYMENT_METHOD_PAYPAL,
@@ -53,12 +54,14 @@
function createPlusCheckoutBillingExecutor(deps = {}) {
const {
addLog,
broadcastDataUpdate,
chrome,
completeStepFromBackground,
ensureContentScriptReadyOnTabUntilStopped,
fetch: fetchImpl = null,
generateRandomName,
getAddressSeedForCountry,
getState,
getTabId,
isTabAlive,
markCurrentRegistrationAccountUsed,
@@ -66,6 +69,7 @@
sleepWithStop,
waitForTabCompleteUntilStopped,
probeIpProxyExit = null,
throwIfStopped = () => {},
} = deps;
function isPlusCheckoutUrl(url = '') {
@@ -76,20 +80,307 @@
return String(value || '').replace(/\s+/g, ' ').trim();
}
function isGpcHelperCheckout(state = {}) {
return normalizePlusPaymentMethod(state?.plusPaymentMethod) === PLUS_PAYMENT_METHOD_GPC_HELPER
|| (normalizeText(state?.plusCheckoutSource) === PLUS_PAYMENT_METHOD_GPC_HELPER
&& Boolean(state?.gopayHelperReferenceId));
}
function compactCountryText(value = '') {
return normalizeText(value).toLowerCase().replace(/[^a-z0-9\u4e00-\u9fff]/g, '');
}
function normalizePlusPaymentMethod(value = '') {
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY
? PLUS_PAYMENT_METHOD_GOPAY
: PLUS_PAYMENT_METHOD_PAYPAL;
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.normalizePlusPaymentMethod) {
return rootScope.GoPayUtils.normalizePlusPaymentMethod(value);
}
const normalized = String(value || '').trim().toLowerCase();
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_PAYMENT_METHOD_GPC_HELPER;
}
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
}
function getPaymentMethodConfig(method = PLUS_PAYMENT_METHOD_PAYPAL) {
return PAYMENT_METHOD_CONFIGS[normalizePlusPaymentMethod(method)] || PAYMENT_METHOD_CONFIGS[PLUS_PAYMENT_METHOD_PAYPAL];
}
function normalizeGpcHelperBaseUrl(apiUrl = '') {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.normalizeGpcHelperBaseUrl) {
return rootScope.GoPayUtils.normalizeGpcHelperBaseUrl(apiUrl);
}
let normalized = String(apiUrl || '').trim().replace(/\/+$/g, '');
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, '');
normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, '');
return normalized;
}
async function fetchJsonWithTimeout(url, options = {}, timeoutMs = 30000) {
const fetcher = typeof fetchImpl === 'function'
? fetchImpl
: (typeof fetch === 'function' ? fetch.bind(globalThis) : null);
if (typeof fetcher !== 'function') {
throw new Error('当前运行环境不支持 fetch,无法调用 GPC API。');
}
const controller = typeof AbortController === 'function' ? new AbortController() : null;
const timer = controller ? setTimeout(() => controller.abort(), Math.max(1000, Number(timeoutMs) || 30000)) : null;
try {
const response = await fetcher(url, { ...options, ...(controller ? { signal: controller.signal } : {}) });
const data = await response.json().catch(() => ({}));
return { response, data };
} finally {
if (timer) clearTimeout(timer);
}
}
function buildGpcOtpPayload(input = {}) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.buildGpcOtpPayload) {
return rootScope.GoPayUtils.buildGpcOtpPayload(input);
}
const payload = {
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
otp: String(input.otp ?? input.code ?? '').trim().replace(/[^\d]/g, ''),
card_key: String(input.card_key ?? input.cardKey ?? '').trim(),
};
const gopayGuid = String(input.gopay_guid ?? input.gopayGuid ?? '').trim();
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
if (flowId) payload.flow_id = flowId;
if (gopayGuid) payload.gopay_guid = gopayGuid;
if (redirectUrl) payload.redirect_url = redirectUrl;
return payload;
}
function buildGpcOtpRetryPayload(input = {}) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.buildGpcOtpRetryPayload) {
return rootScope.GoPayUtils.buildGpcOtpRetryPayload(input);
}
const basePayload = buildGpcOtpPayload(input);
return { ...basePayload, code: basePayload.otp };
}
function buildGpcPinPayload(input = {}) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.buildGpcPinPayload) {
return rootScope.GoPayUtils.buildGpcPinPayload(input);
}
const payload = {
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
challenge_id: String(input.challenge_id ?? input.challengeId ?? '').trim(),
gopay_guid: String(input.gopay_guid ?? input.gopayGuid ?? '').trim(),
pin: String(input.pin ?? '').trim().replace(/[^\d]/g, ''),
card_key: String(input.card_key ?? input.cardKey ?? '').trim(),
};
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
if (flowId) payload.flow_id = flowId;
if (redirectUrl) payload.redirect_url = redirectUrl;
return payload;
}
function buildGpcPinRetryPayload(input = {}) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.buildGpcPinRetryPayload) {
return rootScope.GoPayUtils.buildGpcPinRetryPayload(input);
}
const basePayload = buildGpcPinPayload(input);
return { ...basePayload, challengeId: basePayload.challenge_id };
}
function getGpcResponseErrorDetail(payload = {}, status = 0) {
const rootScope = typeof self !== 'undefined' ? self : globalThis;
if (rootScope.GoPayUtils?.extractGpcResponseErrorDetail) {
return rootScope.GoPayUtils.extractGpcResponseErrorDetail(payload, status);
}
if (payload && typeof payload === 'object') {
return payload.detail || payload.message || payload.error || payload.error_description || payload.reason || `HTTP ${status || 0}`;
}
return `HTTP ${status || 0}`;
}
async function postGpcJsonWithFallback(apiUrl, endpointPath, primaryPayload, fallbackPayload, timeoutMs = 30000) {
const requestUrl = `${apiUrl}${endpointPath}`;
const send = (payload) => fetchJsonWithTimeout(requestUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}, timeoutMs);
const firstResponse = await send(primaryPayload);
if (firstResponse?.response?.ok || !fallbackPayload) {
return { ...firstResponse, retried: false, payload: primaryPayload };
}
const status = Number(firstResponse?.response?.status || 0);
if (status !== 400 && status !== 422) {
return { ...firstResponse, retried: false, payload: primaryPayload };
}
const firstDetail = getGpcResponseErrorDetail(firstResponse?.data, status);
await addLog(`步骤 7GPC 接口返回 ${status}${firstDetail}),使用兼容字段重试。`, 'warn');
const secondResponse = await send(fallbackPayload);
return {
...secondResponse,
retried: true,
payload: fallbackPayload,
firstError: firstDetail,
firstStatus: status,
};
}
function getStateInternal() {
if (typeof getState === 'function') {
return getState();
}
return Promise.resolve({});
}
async function requestGpcOtpInput({ title = '', message = '', referenceId = '' }) {
const requestId = `otp-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const payload = {
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: requestId,
plusManualConfirmationStep: 7,
plusManualConfirmationMethod: 'gopay-otp',
plusManualConfirmationTitle: title || 'GPC OTP 验证',
plusManualConfirmationMessage: message || '请输入 OTP 验证码',
gopayHelperOtpRequestId: requestId,
gopayHelperOtpReferenceId: referenceId,
gopayHelperResolvedOtp: '',
};
await setState(payload);
if (typeof broadcastDataUpdate === 'function') {
broadcastDataUpdate(payload);
}
return new Promise((resolve, reject) => {
const checkInterval = setInterval(async () => {
try {
throwIfStopped();
const currentState = await getStateInternal();
if (!currentState?.plusManualConfirmationPending || currentState?.plusManualConfirmationRequestId !== requestId) {
clearInterval(checkInterval);
const resolvedOtp = String(currentState?.gopayHelperResolvedOtp || '').trim().replace(/[^\d]/g, '');
if (resolvedOtp) {
resolve(resolvedOtp);
} else {
reject(new Error('OTP 输入已取消'));
}
}
} catch (error) {
clearInterval(checkInterval);
reject(error);
}
}, 500);
});
}
async function executeGpcHelperBilling(state = {}) {
const referenceId = String(state?.gopayHelperReferenceId || '').trim();
const apiUrl = normalizeGpcHelperBaseUrl(state?.gopayHelperApiUrl || '');
const cardKey = String(state?.gopayHelperCardKey || state?.gpcCardKey || state?.cardKey || '').trim();
if (!referenceId) {
throw new Error('步骤 7GPC 模式缺少 reference_id,请先执行步骤 6。');
}
if (!apiUrl) {
throw new Error('步骤 7GPC 模式缺少 API 地址。');
}
if (!cardKey) {
throw new Error('步骤 7GPC 模式缺少卡密。');
}
await addLog(`步骤 7:GPC 模式开始 OTP 验证(reference_id: ${referenceId}...`, 'info');
await addLog('步骤 7:等待用户输入 OTP...', 'info');
const otp = await requestGpcOtpInput({
title: 'GPC OTP 验证',
message: `请输入收到的 OTP 验证码(reference_id: ${referenceId}`,
referenceId,
});
const flowId = state?.gopayHelperFlowId
|| state?.gopayHelperStartPayload?.flow_id
|| state?.gopayHelperStartPayload?.flowId
|| state?.gopayHelperBalancePayload?.flow_id
|| state?.gopayHelperBalancePayload?.flowId
|| '';
const baseInput = {
reference_id: referenceId,
otp,
card_key: cardKey,
gopay_guid: state?.gopayHelperGoPayGuid || '',
redirect_url: state?.gopayHelperRedirectUrl || '',
flow_id: flowId,
};
await addLog('步骤 7:正在提交 OTP...', 'info');
const otpResponse = await postGpcJsonWithFallback(
apiUrl,
'/api/gopay/otp',
buildGpcOtpPayload(baseInput),
buildGpcOtpRetryPayload(baseInput),
30000
);
if (!otpResponse?.response?.ok) {
throw new Error(`步骤 7OTP 验证失败:${getGpcResponseErrorDetail(otpResponse?.data, otpResponse?.response?.status || 0)}`);
}
const otpData = otpResponse?.data || {};
const challengeId = String(
otpData?.challenge_id
|| otpData?.challengeId
|| state?.gopayHelperChallengeId
|| state?.gopayHelperStartPayload?.challenge_id
|| state?.gopayHelperStartPayload?.challengeId
|| ''
).trim();
const nextFlowId = String(otpData?.flow_id || otpData?.flowId || baseInput.flow_id || '').trim();
const gopayGuid = String(otpData?.gopay_guid || otpData?.gopayGuid || state?.gopayHelperGoPayGuid || '').trim();
const redirectUrl = String(otpData?.redirect_url || otpData?.redirectUrl || state?.gopayHelperRedirectUrl || '').trim();
if (!challengeId) {
throw new Error('步骤 7GPC OTP 验证后未返回 challenge_id。');
}
const pin = String(state?.gopayHelperPin || '').trim().replace(/[^\d]/g, '');
if (!pin) {
throw new Error('步骤 7GPC 模式缺少 PIN 配置。');
}
await setState({
gopayHelperChallengeId: challengeId,
gopayHelperFlowId: nextFlowId,
gopayHelperGoPayGuid: gopayGuid,
gopayHelperRedirectUrl: redirectUrl,
});
await addLog('步骤 7:正在提交 PIN...', 'info');
const pinInput = {
reference_id: referenceId,
challenge_id: challengeId,
gopay_guid: gopayGuid,
redirect_url: redirectUrl,
flow_id: nextFlowId,
pin,
card_key: cardKey,
};
const pinResponse = await postGpcJsonWithFallback(
apiUrl,
'/api/gopay/pin',
buildGpcPinPayload(pinInput),
buildGpcPinRetryPayload(pinInput),
30000
);
if (!pinResponse?.response?.ok) {
throw new Error(`步骤 7PIN 验证失败:${getGpcResponseErrorDetail(pinResponse?.data, pinResponse?.response?.status || 0)}`);
}
await setState({
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
gopayHelperPinPayload: pinResponse?.data || null,
});
await addLog('步骤 7:GPC 支付完成,准备继续下一步。', 'ok');
await completeStepFromBackground(7, {
plusCheckoutSource: PLUS_PAYMENT_METHOD_GPC_HELPER,
});
}
function resolveMeiguodizhiCountryCode(value = '') {
const normalized = normalizeText(value);
const upper = normalized.toUpperCase();
@@ -617,6 +908,10 @@
}
async function executePlusCheckoutBilling(state = {}) {
if (isGpcHelperCheckout(state)) {
await executeGpcHelperBilling(state);
return;
}
const paymentMethod = normalizePlusPaymentMethod(state?.plusPaymentMethod);
const paymentConfig = getPaymentMethodConfig(paymentMethod);
const tabId = await getCheckoutTabId(state);
+20 -3
View File
@@ -114,7 +114,7 @@ async function handlePlusCheckoutCommand(message) {
case 'PLUS_CHECKOUT_CLICK_SUBSCRIBE':
return clickPlusSubscribe(message.payload || {});
case 'PLUS_CHECKOUT_GET_STATE':
return inspectPlusCheckoutState();
return inspectPlusCheckoutState(message.payload || {});
default:
throw new Error(`plus-checkout.js 不处理消息:${message.type}`);
}
@@ -1500,9 +1500,20 @@ async function clickPlusSubscribe(payload = {}) {
};
}
function inspectPlusCheckoutState() {
const structuredAddress = getStructuredAddressFields();
async function readChatGptSessionAccessToken() {
const sessionResponse = await fetch('/api/auth/session', {
credentials: 'include',
});
const session = await sessionResponse.json().catch(() => ({}));
return {
session,
accessToken: String(session?.accessToken || '').trim(),
};
}
async function inspectPlusCheckoutState(options = {}) {
const structuredAddress = getStructuredAddressFields();
const state = {
url: location.href,
readyState: document.readyState,
countryText: readCountryText(),
@@ -1522,5 +1533,11 @@ function inspectPlusCheckoutState() {
postalCode: structuredAddress.postalCode?.value || '',
},
};
if (options.includeSession || options.includeAccessToken) {
const sessionState = await readChatGptSessionAccessToken();
state.session = sessionState.session;
state.accessToken = sessionState.accessToken;
}
return state;
}
})();
+27 -6
View File
@@ -3,6 +3,7 @@
})(typeof self !== 'undefined' ? self : globalThis, function createStepDefinitionsModule() {
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const PLUS_PAYMENT_STEP_KEY = 'paypal-approve';
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
@@ -50,6 +51,20 @@
{ id: 13, order: 130, key: 'platform-verify', title: '平台回调验证' },
];
const PLUS_GPC_STEP_DEFINITIONS = [
{ id: 1, order: 10, key: 'open-chatgpt', title: '打开 ChatGPT 官网' },
{ id: 2, order: 20, key: 'submit-signup-email', title: '注册并输入邮箱' },
{ id: 3, order: 30, key: 'fill-password', title: '填写密码并继续' },
{ id: 4, order: 40, key: 'fetch-signup-code', title: '获取注册验证码' },
{ id: 5, order: 50, key: 'fill-profile', title: '填写姓名和生日' },
{ id: 6, order: 60, key: 'plus-checkout-create', title: '创建 GPC 订单' },
{ id: 7, order: 70, key: 'plus-checkout-billing', title: 'GPC OTP/PIN 验证' },
{ id: 10, order: 100, key: 'oauth-login', title: '刷新 OAuth 并登录' },
{ id: 11, order: 110, key: 'fetch-login-code', title: '获取登录验证码' },
{ id: 12, order: 120, key: 'confirm-oauth', title: '自动确认 OAuth' },
{ id: 13, order: 130, key: 'platform-verify', title: '平台回调验证' },
];
const PHONE_SIGNUP_TITLE_OVERRIDES = Object.freeze({
'submit-signup-email': '注册并输入手机号',
'fetch-signup-code': '获取手机验证码',
@@ -60,9 +75,11 @@
}
function normalizePlusPaymentMethod(value = '') {
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY
? PLUS_PAYMENT_METHOD_GOPAY
: PLUS_PAYMENT_METHOD_PAYPAL;
const normalized = String(value || '').trim().toLowerCase();
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_PAYMENT_METHOD_GPC_HELPER;
}
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
}
function normalizeSignupMethod(value = '') {
@@ -79,9 +96,11 @@
if (!isPlusModeEnabled(options)) {
return NORMAL_STEP_DEFINITIONS;
}
return normalizePlusPaymentMethod(options?.plusPaymentMethod || options?.paymentMethod) === PLUS_PAYMENT_METHOD_GOPAY
? PLUS_GOPAY_STEP_DEFINITIONS
: PLUS_PAYPAL_STEP_DEFINITIONS;
const paymentMethod = normalizePlusPaymentMethod(options?.plusPaymentMethod || options?.paymentMethod);
if (paymentMethod === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_GPC_STEP_DEFINITIONS;
}
return paymentMethod === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_GOPAY_STEP_DEFINITIONS : PLUS_PAYPAL_STEP_DEFINITIONS;
}
function getPlusPaymentStepTitle(options = {}) {
@@ -123,6 +142,7 @@
...NORMAL_STEP_DEFINITIONS,
...PLUS_PAYPAL_STEP_DEFINITIONS,
...PLUS_GOPAY_STEP_DEFINITIONS,
...PLUS_GPC_STEP_DEFINITIONS,
]) {
keyed.set(`${step.id}:${step.key}`, step);
}
@@ -158,6 +178,7 @@
PLUS_STEP_DEFINITIONS: PLUS_PAYPAL_STEP_DEFINITIONS,
PLUS_PAYPAL_STEP_DEFINITIONS,
PLUS_GOPAY_STEP_DEFINITIONS,
PLUS_GPC_STEP_DEFINITIONS,
SIGNUP_METHOD_EMAIL,
SIGNUP_METHOD_PHONE,
getAllSteps,
+163 -3
View File
@@ -3,11 +3,14 @@
})(typeof self !== 'undefined' ? self : globalThis, function createGoPayUtils() {
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
function normalizePlusPaymentMethod(value = '') {
return String(value || '').trim().toLowerCase() === PLUS_PAYMENT_METHOD_GOPAY
? PLUS_PAYMENT_METHOD_GOPAY
: PLUS_PAYMENT_METHOD_PAYPAL;
const normalized = String(value || '').trim().toLowerCase();
if (normalized === PLUS_PAYMENT_METHOD_GPC_HELPER) {
return PLUS_PAYMENT_METHOD_GPC_HELPER;
}
return normalized === PLUS_PAYMENT_METHOD_GOPAY ? PLUS_PAYMENT_METHOD_GOPAY : PLUS_PAYMENT_METHOD_PAYPAL;
}
const DEFAULT_GOPAY_COUNTRY_CODE = '+86';
@@ -42,10 +45,167 @@
return String(value || '').trim().replace(/[^\d]/g, '');
}
function normalizeGpcHelperBaseUrl(apiUrl = '') {
let normalized = String(apiUrl || '').trim();
if (!normalized) {
return '';
}
normalized = normalized.replace(/\/+$/g, '');
normalized = normalized.replace(/\/api\/checkout\/start$/i, '');
normalized = normalized.replace(/\/api\/gopay\/(?:otp|pin)$/i, '');
normalized = normalized.replace(/\/api\/card\/balance(?:\?.*)?$/i, '');
return normalized;
}
function buildGpcHelperApiUrl(apiUrl = '', path = '') {
const baseUrl = normalizeGpcHelperBaseUrl(apiUrl);
if (!baseUrl) {
return '';
}
const normalizedPath = String(path || '').startsWith('/') ? String(path || '') : `/${String(path || '')}`;
return `${baseUrl}${normalizedPath}`;
}
function buildGpcCardBalanceUrl(apiUrl = '', cardKey = '') {
const endpoint = buildGpcHelperApiUrl(apiUrl, '/api/card/balance');
if (!endpoint) {
return '';
}
return `${endpoint}?card_key=${encodeURIComponent(String(cardKey || '').trim())}`;
}
function extractGpcResponseErrorDetail(payload = {}, status = 0) {
if (!payload || typeof payload !== 'object') {
return status ? `HTTP ${status}` : '未知错误';
}
const payloadText = JSON.stringify(payload).toLowerCase();
if (/account\s+already\s+linked/i.test(payloadText)) {
return 'GOPAY已经绑了订阅,需要手动解绑';
}
const direct = payload.detail
?? payload.message
?? payload.error
?? payload.error_description
?? payload.reason;
if (direct !== undefined && direct !== null && String(direct).trim()) {
const directText = String(direct).trim();
return /account\s+already\s+linked/i.test(directText)
? 'GOPAY已经绑了订阅,需要手动解绑'
: directText;
}
const errorMessages = payload.error_messages ?? payload.errorMessages;
if (Array.isArray(errorMessages) && errorMessages.length > 0) {
const firstMessage = String(errorMessages[0] || '').trim();
if (/account\s+already\s+linked/i.test(firstMessage)) {
return 'GOPAY已经绑了订阅,需要手动解绑';
}
if (firstMessage) {
return firstMessage;
}
}
const errors = payload.errors;
if (Array.isArray(errors) && errors.length > 0) {
const first = errors[0];
if (typeof first === 'string') {
return first.trim() || (status ? `HTTP ${status}` : '未知错误');
}
if (first && typeof first === 'object') {
const field = Array.isArray(first.loc) ? first.loc.join('.') : String(first.field || first.path || '').trim();
const message = String(first.msg || first.message || first.error || '').trim();
return [field, message].filter(Boolean).join(': ') || JSON.stringify(first);
}
}
return status ? `HTTP ${status}` : '未知错误';
}
function buildGpcOtpPayload(input = {}) {
const payload = {
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
otp: normalizeGoPayOtp(input.otp ?? input.code ?? ''),
card_key: String(input.card_key ?? input.cardKey ?? '').trim(),
};
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
const gopayGuid = String(input.gopay_guid ?? input.gopayGuid ?? '').trim();
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
if (flowId) payload.flow_id = flowId;
if (gopayGuid) payload.gopay_guid = gopayGuid;
if (redirectUrl) payload.redirect_url = redirectUrl;
return payload;
}
function buildGpcOtpRetryPayload(input = {}) {
const payload = buildGpcOtpPayload(input);
return { ...payload, code: payload.otp };
}
function buildGpcPinPayload(input = {}) {
const payload = {
reference_id: String(input.reference_id ?? input.referenceId ?? '').trim(),
challenge_id: String(input.challenge_id ?? input.challengeId ?? '').trim(),
gopay_guid: String(input.gopay_guid ?? input.gopayGuid ?? '').trim(),
pin: normalizeGoPayPin(input.pin ?? ''),
card_key: String(input.card_key ?? input.cardKey ?? '').trim(),
};
const flowId = String(input.flow_id ?? input.flowId ?? '').trim();
const redirectUrl = String(input.redirect_url ?? input.redirectUrl ?? '').trim();
if (flowId) payload.flow_id = flowId;
if (redirectUrl) payload.redirect_url = redirectUrl;
return payload;
}
function buildGpcPinRetryPayload(input = {}) {
const payload = buildGpcPinPayload(input);
return { ...payload, challengeId: payload.challenge_id };
}
function formatGpcBalancePayload(payload = {}) {
if (!payload || typeof payload !== 'object') {
return '';
}
const candidates = [
payload.remaining_uses,
payload.remainingUses,
payload.balance,
payload.remaining,
payload.uses,
payload.available_uses,
payload.availableUses,
];
const firstValue = candidates.find((value) => value !== undefined && value !== null && String(value).trim() !== '');
const status = String(payload.card_status || payload.cardStatus || payload.status || '').trim();
const flowId = String(payload.flow_id || payload.flowId || '').trim();
const parts = [];
if (firstValue !== undefined) {
parts.push(`余额 ${firstValue}`);
}
if (status) {
parts.push(`状态 ${status}`);
}
if (flowId) {
parts.push(`flow_id ${flowId}`);
}
return parts.join('');
}
return {
DEFAULT_GOPAY_COUNTRY_CODE,
PLUS_PAYMENT_METHOD_GPC_HELPER,
PLUS_PAYMENT_METHOD_GOPAY,
PLUS_PAYMENT_METHOD_PAYPAL,
buildGpcCardBalanceUrl,
buildGpcHelperApiUrl,
buildGpcOtpPayload,
buildGpcOtpRetryPayload,
buildGpcPinPayload,
buildGpcPinRetryPayload,
extractGpcResponseErrorDetail,
formatGpcBalancePayload,
normalizeGpcHelperBaseUrl,
normalizeGoPayCountryCode,
normalizeGoPayPhone,
normalizeGoPayPhoneForCountry,
+53
View File
@@ -247,7 +247,9 @@
<select id="select-plus-payment-method" class="data-select">
<option value="paypal">PayPal</option>
<option value="gopay">GoPay</option>
<option value="gpc-helper">GPC</option>
</select>
<button id="btn-gpc-card-key-purchase" class="btn btn-outline btn-sm data-inline-btn" type="button" style="display:none;">购买卡密</button>
<span class="setting-caption" id="plus-payment-method-caption">PayPal 订阅链路</span>
</div>
</div>
@@ -260,6 +262,57 @@
<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-gpc-helper-api" style="display:none;">
<span class="data-label">GPC API</span>
<input type="text" id="input-gpc-helper-api" class="data-input" placeholder="https://gopay.hwork.pro" />
</div>
<div class="data-row" id="row-gpc-helper-card-key" style="display:none;">
<span class="data-label">GPC 卡密</span>
<div class="data-inline">
<div class="input-with-icon">
<input type="password" id="input-gpc-helper-card-key" class="data-input data-input-with-icon"
placeholder="请输入购买的 GPC 卡密" autocomplete="off" />
<button id="btn-toggle-gpc-helper-card-key" class="input-icon-btn" type="button"
data-password-toggle="input-gpc-helper-card-key" data-show-label="显示 GPC 卡密"
data-hide-label="隐藏 GPC 卡密" aria-label="显示 GPC 卡密" title="显示 GPC 卡密"></button>
</div>
<button id="btn-gpc-helper-balance" class="btn btn-ghost btn-xs data-inline-btn" type="button">查余额</button>
<span id="display-gpc-helper-balance" class="data-value mono">余额未获取</span>
</div>
</div>
<div class="data-row" id="row-gpc-helper-country-code" style="display:none;">
<span class="data-label">GPC 区号</span>
<select id="select-gpc-helper-country-code" class="data-select">
<option value="+86">中国 +86</option>
<option value="+62">印度尼西亚 +62</option>
<option value="+1">美国/加拿大 +1</option>
<option value="+852">香港 +852</option>
<option value="+853">澳门 +853</option>
<option value="+886">台湾 +886</option>
<option value="+60">马来西亚 +60</option>
<option value="+65">新加坡 +65</option>
<option value="+66">泰国 +66</option>
<option value="+84">越南 +84</option>
<option value="+63">菲律宾 +63</option>
<option value="+91">印度 +91</option>
</select>
</div>
<div class="data-row" id="row-gpc-helper-phone" style="display:none;">
<span class="data-label">GPC 手机</span>
<input type="text" id="input-gpc-helper-phone" class="data-input"
placeholder="GPC 专用手机号,不读取 GoPay 手机字段" autocomplete="tel" />
</div>
<div class="data-row" id="row-gpc-helper-pin" style="display:none;">
<span class="data-label">GPC PIN</span>
<div class="input-with-icon">
<input type="password" id="input-gpc-helper-pin" class="data-input data-input-with-icon"
placeholder="请输入 GoPay PIN" autocomplete="off" />
<button id="btn-toggle-gpc-helper-pin" class="input-icon-btn" type="button"
data-password-toggle="input-gpc-helper-pin" data-show-label="显示 GPC PIN"
data-hide-label="隐藏 GPC PIN" aria-label="显示 GPC PIN"
title="显示 GPC PIN"></button>
</div>
</div>
<div class="data-row" id="row-gopay-country-code" style="display:none;">
<span class="data-label">GoPay 区号</span>
<select id="select-gopay-country-code" class="data-select">
+180 -7
View File
@@ -169,10 +169,25 @@ const rowPlusMode = document.getElementById('row-plus-mode');
const inputPlusModeEnabled = document.getElementById('input-plus-mode-enabled');
const rowPlusPaymentMethod = document.getElementById('row-plus-payment-method');
const selectPlusPaymentMethod = document.getElementById('select-plus-payment-method');
const btnGpcCardKeyPurchase = document.getElementById('btn-gpc-card-key-purchase');
const plusPaymentMethodCaption = document.getElementById('plus-payment-method-caption');
const rowPayPalAccount = document.getElementById('row-paypal-account');
const selectPayPalAccount = document.getElementById('select-paypal-account');
const btnAddPayPalAccount = document.getElementById('btn-add-paypal-account');
const rowGpcHelperApi = document.getElementById('row-gpc-helper-api');
const inputGpcHelperApi = document.getElementById('input-gpc-helper-api');
const rowGpcHelperCardKey = document.getElementById('row-gpc-helper-card-key');
const inputGpcHelperCardKey = document.getElementById('input-gpc-helper-card-key');
const btnToggleGpcHelperCardKey = document.getElementById('btn-toggle-gpc-helper-card-key');
const btnGpcHelperBalance = document.getElementById('btn-gpc-helper-balance');
const displayGpcHelperBalance = document.getElementById('display-gpc-helper-balance');
const rowGpcHelperCountryCode = document.getElementById('row-gpc-helper-country-code');
const selectGpcHelperCountryCode = document.getElementById('select-gpc-helper-country-code');
const rowGpcHelperPhone = document.getElementById('row-gpc-helper-phone');
const inputGpcHelperPhone = document.getElementById('input-gpc-helper-phone');
const rowGpcHelperPin = document.getElementById('row-gpc-helper-pin');
const inputGpcHelperPin = document.getElementById('input-gpc-helper-pin');
const btnToggleGpcHelperPin = document.getElementById('btn-toggle-gpc-helper-pin');
const rowGoPayCountryCode = document.getElementById('row-gopay-country-code');
const selectGoPayCountryCode = document.getElementById('select-gopay-country-code');
const rowGoPayPhone = document.getElementById('row-gopay-phone');
@@ -445,6 +460,7 @@ const autoHintText = document.querySelector('.auto-hint');
const stepsList = document.querySelector('.steps-list');
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const DEFAULT_PLUS_PAYMENT_METHOD = PLUS_PAYMENT_METHOD_PAYPAL;
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
@@ -2006,10 +2022,13 @@ function normalizePlusPaymentMethod(value = '') {
return rootScope.GoPayUtils.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';
return String(value || '').trim().toLowerCase() === gopayValue
? gopayValue
: paypalValue;
const normalized = String(value || '').trim().toLowerCase();
if (normalized === gpcValue) {
return gpcValue;
}
return normalized === gopayValue ? gopayValue : paypalValue;
}
function getSelectedPlusPaymentMethod(state = latestState) {
@@ -3158,6 +3177,29 @@ function collectSettingsPayload() {
: (typeof inputGoPayPin !== 'undefined' && inputGoPayPin
? String(inputGoPayPin.value || '')
: String(latestState?.gopayPin || '')),
gopayHelperApiUrl: window.GoPayUtils?.normalizeGpcHelperBaseUrl
? window.GoPayUtils.normalizeGpcHelperBaseUrl(typeof inputGpcHelperApi !== 'undefined' && inputGpcHelperApi ? inputGpcHelperApi.value : latestState?.gopayHelperApiUrl)
: (typeof inputGpcHelperApi !== 'undefined' && inputGpcHelperApi
? String(inputGpcHelperApi.value || '').trim().replace(/\/+$/g, '')
: String(latestState?.gopayHelperApiUrl || '').trim()),
gopayHelperCardKey: typeof inputGpcHelperCardKey !== 'undefined' && inputGpcHelperCardKey
? String(inputGpcHelperCardKey.value || '').trim()
: String(latestState?.gopayHelperCardKey || '').trim(),
gopayHelperCountryCode: window.GoPayUtils?.normalizeGoPayCountryCode
? window.GoPayUtils.normalizeGoPayCountryCode(typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode ? selectGpcHelperCountryCode.value : latestState?.gopayHelperCountryCode)
: (typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode
? String(selectGpcHelperCountryCode.value || '+86').trim()
: String(latestState?.gopayHelperCountryCode || '+86').trim()),
gopayHelperPhoneNumber: window.GoPayUtils?.normalizeGoPayPhone
? window.GoPayUtils.normalizeGoPayPhone(typeof inputGpcHelperPhone !== 'undefined' && inputGpcHelperPhone ? inputGpcHelperPhone.value : latestState?.gopayHelperPhoneNumber)
: (typeof inputGpcHelperPhone !== 'undefined' && inputGpcHelperPhone
? String(inputGpcHelperPhone.value || '').trim()
: String(latestState?.gopayHelperPhoneNumber || '').trim()),
gopayHelperPin: window.GoPayUtils?.normalizeGoPayPin
? window.GoPayUtils.normalizeGoPayPin(typeof inputGpcHelperPin !== 'undefined' && inputGpcHelperPin ? inputGpcHelperPin.value : latestState?.gopayHelperPin)
: (typeof inputGpcHelperPin !== 'undefined' && inputGpcHelperPin
? String(inputGpcHelperPin.value || '')
: String(latestState?.gopayHelperPin || '')),
...(contributionModeEnabled ? {} : {
customPassword: inputPassword.value,
}),
@@ -6852,6 +6894,7 @@ function updatePhoneVerificationSettingsUI() {
function updatePlusModeUI() {
const paypalValue = typeof PLUS_PAYMENT_METHOD_PAYPAL !== 'undefined' ? PLUS_PAYMENT_METHOD_PAYPAL : 'paypal';
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 defaultMethod = typeof DEFAULT_PLUS_PAYMENT_METHOD !== 'undefined' ? DEFAULT_PLUS_PAYMENT_METHOD : paypalValue;
const enabled = typeof inputPlusModeEnabled !== 'undefined' && inputPlusModeEnabled
? Boolean(inputPlusModeEnabled.checked)
@@ -6864,7 +6907,9 @@ function updatePlusModeUI() {
}
}
if (typeof plusPaymentMethodCaption !== 'undefined' && plusPaymentMethodCaption) {
plusPaymentMethodCaption.textContent = method === gopayValue
plusPaymentMethodCaption.textContent = method === gpcValue
? 'GPC API 订阅链路'
: method === gopayValue
? 'GoPay 印尼订阅链路'
: 'PayPal 订阅链路';
}
@@ -6887,6 +6932,24 @@ function updatePlusModeUI() {
: method;
row.style.display = enabled && selectedMethod === paypalValue ? '' : 'none';
});
[
typeof rowGpcHelperApi !== 'undefined' ? rowGpcHelperApi : null,
typeof rowGpcHelperCardKey !== 'undefined' ? rowGpcHelperCardKey : null,
typeof rowGpcHelperCountryCode !== 'undefined' ? rowGpcHelperCountryCode : null,
typeof rowGpcHelperPhone !== 'undefined' ? rowGpcHelperPhone : null,
typeof rowGpcHelperPin !== 'undefined' ? rowGpcHelperPin : null,
].forEach((row) => {
if (!row) {
return;
}
const selectedMethod = typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod?.value
? normalizePlusPaymentMethod(selectPlusPaymentMethod.value)
: method;
row.style.display = enabled && selectedMethod === gpcValue ? '' : 'none';
});
if (typeof btnGpcCardKeyPurchase !== 'undefined' && btnGpcCardKeyPurchase) {
btnGpcCardKeyPurchase.style.display = enabled && method === gpcValue ? '' : 'none';
}
[
typeof rowGoPayCountryCode !== 'undefined' ? rowGoPayCountryCode : null,
typeof rowGoPayPhone !== 'undefined' ? rowGoPayPhone : null,
@@ -6975,6 +7038,36 @@ async function setRuntimeSignupPhoneState(phoneNumber) {
async function openPlusManualConfirmationDialog(options = {}) {
const method = String(options.method || '').trim().toLowerCase();
const gopayValue = typeof PLUS_PAYMENT_METHOD_GOPAY !== 'undefined' ? PLUS_PAYMENT_METHOD_GOPAY : 'gopay';
if (method === 'gopay-otp') {
if (!sharedFormDialog?.open) {
return null;
}
const result = await sharedFormDialog.open({
title: String(options.title || '').trim() || 'GPC OTP 验证',
message: String(options.message || '').trim() || '请输入收到的 OTP 验证码。',
fields: [
{
key: 'otp',
label: 'OTP',
type: 'text',
placeholder: '请输入 OTP 验证码',
inputMode: 'numeric',
autocomplete: 'one-time-code',
required: true,
requiredMessage: '请输入 OTP 验证码。',
normalize: (value) => String(value || '').trim().replace(/[^\d]/g, ''),
validate: (value) => {
const normalized = String(value || '').trim().replace(/[^\d]/g, '');
if (!normalized) return '请输入 OTP 验证码。';
if (normalized.length < 4) return 'OTP 验证码长度过短,请检查。';
return '';
},
},
],
confirmLabel: '提交 OTP',
});
return result ? { action: 'confirm', otp: String(result.otp || '').trim().replace(/[^\d]/g, '') } : { action: 'cancel' };
}
const title = String(options.title || '').trim() || (method === gopayValue ? 'GoPay 订阅确认' : '手动确认');
const message = String(options.message || '').trim()
|| (method === gopayValue
@@ -7026,7 +7119,7 @@ async function syncPlusManualConfirmationDialog() {
return;
}
const confirmed = choice === 'confirm';
const confirmed = choice === 'confirm' || choice?.action === 'confirm';
const response = await chrome.runtime.sendMessage({
type: 'RESOLVE_PLUS_MANUAL_CONFIRMATION',
source: 'sidepanel',
@@ -7034,15 +7127,28 @@ async function syncPlusManualConfirmationDialog() {
step,
requestId,
confirmed,
...(choice?.otp ? { otp: choice.otp } : {}),
},
});
if (response?.error) {
throw new Error(response.error);
}
if (confirmed) {
showToast(method === gopayValue ? 'GoPay 订阅已确认,正在继续 OAuth 登录...' : '已确认,流程继续执行中...', 'info', 2200);
showToast(
method === 'gopay-otp'
? 'GPC OTP 已提交,正在继续验证...'
: (method === gopayValue ? 'GoPay 订阅已确认,正在继续 OAuth 登录...' : '已确认,流程继续执行中...'),
'info',
2200
);
} else {
showToast(method === gopayValue ? '已取消 GoPay 订阅等待。' : '已取消当前手动确认。', 'warn', 2200);
showToast(
method === 'gopay-otp'
? '已取消 GPC OTP 输入。'
: (method === gopayValue ? '已取消 GoPay 订阅等待。' : '已取消当前手动确认。'),
'warn',
2200
);
}
} catch (error) {
showToast(error?.message || String(error || '未知错误'), 'error');
@@ -7436,6 +7542,40 @@ function applySettingsState(state) {
if (typeof selectPlusPaymentMethod !== 'undefined' && selectPlusPaymentMethod) {
selectPlusPaymentMethod.value = normalizePlusPaymentMethod(state?.plusPaymentMethod);
}
if (typeof inputGpcHelperApi !== 'undefined' && inputGpcHelperApi) {
inputGpcHelperApi.value = state?.gopayHelperApiUrl || '';
}
if (typeof inputGpcHelperCardKey !== 'undefined' && inputGpcHelperCardKey) {
inputGpcHelperCardKey.value = state?.gopayHelperCardKey || '';
}
if (typeof selectGpcHelperCountryCode !== 'undefined' && selectGpcHelperCountryCode) {
const normalizedCountryCode = window.GoPayUtils?.normalizeGoPayCountryCode
? window.GoPayUtils.normalizeGoPayCountryCode(state?.gopayHelperCountryCode)
: String(state?.gopayHelperCountryCode || '+86').trim();
const hasOption = Array.from(selectGpcHelperCountryCode.options || [])
.some((option) => option.value === normalizedCountryCode);
if (!hasOption && normalizedCountryCode) {
const option = document.createElement('option');
option.value = normalizedCountryCode;
option.textContent = `自定义 ${normalizedCountryCode}`;
selectGpcHelperCountryCode.appendChild(option);
}
selectGpcHelperCountryCode.value = normalizedCountryCode || '+86';
}
if (typeof inputGpcHelperPhone !== 'undefined' && inputGpcHelperPhone) {
inputGpcHelperPhone.value = state?.gopayHelperPhoneNumber || '';
}
if (typeof inputGpcHelperPin !== 'undefined' && inputGpcHelperPin) {
inputGpcHelperPin.value = state?.gopayHelperPin || '';
}
if (typeof displayGpcHelperBalance !== 'undefined' && displayGpcHelperBalance) {
const balanceText = String(state?.gopayHelperBalance || '').trim();
const balanceError = String(state?.gopayHelperBalanceError || '').trim();
const balanceAt = Number(state?.gopayHelperBalanceUpdatedAt) || 0;
displayGpcHelperBalance.textContent = balanceError
? `余额查询失败:${balanceError}`
: (balanceText || (balanceAt ? '余额已更新' : '余额未获取'));
}
if (typeof selectGoPayCountryCode !== 'undefined' && selectGoPayCountryCode) {
const normalizedGoPayCountryCode = window.GoPayUtils?.normalizeGoPayCountryCode
? window.GoPayUtils.normalizeGoPayCountryCode(state?.gopayCountryCode)
@@ -10663,6 +10803,34 @@ selectPlusPaymentMethod?.addEventListener('change', () => {
saveSettings({ silent: true }).catch(() => { });
});
btnGpcCardKeyPurchase?.addEventListener('click', () => {
openExternalUrl('https://pay.ldxp.cn/shop/gpc');
});
btnGpcHelperBalance?.addEventListener('click', async () => {
try {
const response = await chrome.runtime.sendMessage({
type: 'REFRESH_GPC_CARD_BALANCE',
source: 'sidepanel',
payload: {
gopayHelperApiUrl: inputGpcHelperApi?.value || '',
gopayHelperCardKey: inputGpcHelperCardKey?.value || '',
gopayHelperCountryCode: selectGpcHelperCountryCode?.value || '+86',
reason: 'manual',
},
});
if (response?.error) {
throw new Error(response.error);
}
if (displayGpcHelperBalance) {
displayGpcHelperBalance.textContent = response?.balance || '余额已更新';
}
showToast('GPC 余额已更新。', 'success');
} catch (error) {
showToast(error?.message || '查询 GPC 余额失败。', 'error');
}
});
selectPlusPaymentMethod?.addEventListener('change', () => {
updatePlusModeUI();
syncStepDefinitionsForMode(Boolean(inputPlusModeEnabled?.checked), {
@@ -10674,6 +10842,11 @@ selectPlusPaymentMethod?.addEventListener('change', () => {
});
[
inputGpcHelperApi,
inputGpcHelperCardKey,
selectGpcHelperCountryCode,
inputGpcHelperPhone,
inputGpcHelperPin,
selectGoPayCountryCode,
inputGoPayPhone,
inputGoPayOtp,
@@ -116,6 +116,9 @@ const DEFAULT_PHONE_SMS_PROVIDER = PHONE_SMS_PROVIDER_HERO_SMS;
const SIGNUP_METHOD_EMAIL = 'email';
const SIGNUP_METHOD_PHONE = 'phone';
const DEFAULT_SIGNUP_METHOD = SIGNUP_METHOD_EMAIL;
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const DEFAULT_FIVE_SIM_PRODUCT = 'openai';
const DEFAULT_NEX_SMS_SERVICE_CODE = 'ot';
const FIVE_SIM_COUNTRY_ID = 'vietnam';
@@ -123,6 +126,31 @@ const FIVE_SIM_COUNTRY_LABEL = '越南 (Vietnam)';
const FIVE_SIM_OPERATOR = 'any';
const FIVE_SIM_SUPPORTED_COUNTRY_ID_SET = new Set(['indonesia', 'thailand', 'vietnam']);
const HERO_SMS_SUPPORTED_COUNTRY_ID_SET = new Set(['6', '52', '10']);
const self = {
GoPayUtils: {
normalizeGoPayCountryCode(value) {
const digits = String(value || '').replace(/\\D/g, '');
return digits ? \`+\${digits}\` : '+86';
},
normalizeGoPayPhone(value) {
return String(value || '').trim().replace(/[^\\d+]/g, '');
},
normalizeGoPayOtp(value) {
return String(value || '').trim().replace(/[^\\d]/g, '');
},
normalizeGoPayPin(value) {
return String(value || '').trim().replace(/[^\\d]/g, '');
},
normalizeGpcHelperBaseUrl(value) {
return String(value || '')
.trim()
.replace(/\\/+$/g, '')
.replace(/\\/api\\/checkout\\/start$/i, '')
.replace(/\\/api\\/gopay\\/(?:otp|pin)$/i, '')
.replace(/\\/api\\/card\\/balance(?:\\?.*)?$/i, '');
},
},
};
const PERSISTED_SETTING_DEFAULTS = {
autoStepDelaySeconds: null,
mailProvider: '163',
@@ -155,8 +183,17 @@ return {
assert.equal(api.normalizePersistentSettingValue('accountRunHistoryTextEnabled', 1), true);
assert.equal(api.normalizePersistentSettingValue('phoneVerificationEnabled', 1), true);
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'gopay'), 'gopay');
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'gpc-helper'), 'gpc-helper');
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'paypal'), 'paypal');
assert.equal(api.normalizePersistentSettingValue('plusPaymentMethod', 'unknown'), 'paypal');
assert.equal(
api.normalizePersistentSettingValue('gopayHelperApiUrl', ' https://gopay.hwork.pro/api/checkout/start '),
'https://gopay.hwork.pro'
);
assert.equal(api.normalizePersistentSettingValue('gopayHelperCardKey', ' card_123 '), 'card_123');
assert.equal(api.normalizePersistentSettingValue('gopayHelperCountryCode', ' 86 '), '+86');
assert.equal(api.normalizePersistentSettingValue('gopayHelperPhoneNumber', ' +86 138-0013-8000 '), '+8613800138000');
assert.equal(api.normalizePersistentSettingValue('gopayHelperPin', ' 12-34-56 '), '123456');
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '7'), 7);
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '-1'), 0);
assert.equal(api.normalizePersistentSettingValue('phoneVerificationReplacementLimit', '9'), 9);
@@ -10,6 +10,9 @@ function createRouter(overrides = {}) {
const events = {
logs: [],
stepStatuses: [],
stateUpdates: [],
broadcasts: [],
balanceRefreshes: [],
emailStates: [],
signupPhoneStates: [],
signupPhoneSilentStates: [],
@@ -31,7 +34,9 @@ function createRouter(overrides = {}) {
buildLocalhostCleanupPrefix: () => '',
buildLuckmailSessionSettingsPayload: () => ({}),
buildPersistentSettingsPayload: () => ({}),
broadcastDataUpdate: () => {},
broadcastDataUpdate: (updates) => {
events.broadcasts.push(updates);
},
cancelScheduledAutoRun: async () => {},
checkIcloudSession: async () => {},
clearAutoRunTimerAlarm: async () => {},
@@ -123,7 +128,9 @@ function createRouter(overrides = {}) {
setLuckmailPurchasePreservedState: async () => {},
setLuckmailPurchaseUsedState: async () => {},
setPersistentSettings: async () => {},
setState: async () => {},
setState: async (updates) => {
events.stateUpdates.push(updates);
},
setStepStatus: async (step, status) => {
events.stepStatuses.push({ step, status });
},
@@ -134,6 +141,10 @@ function createRouter(overrides = {}) {
testHotmailAccountMailAccess: async () => {},
upsertHotmailAccount: async () => {},
verifyHotmailAccount: async () => {},
refreshGpcCardBalance: overrides.refreshGpcCardBalance || (async (state, options) => {
events.balanceRefreshes.push({ state, options });
return { balance: '余额 3' };
}),
});
return { router, events };
@@ -438,3 +449,55 @@ test('message router blocks manual step 4 execution when signup page tab is miss
assert.deepStrictEqual(events.invalidations, []);
assert.deepStrictEqual(events.executedSteps, []);
});
test('message router resolves GPC OTP manual confirmation without completing step early', async () => {
const state = {
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: 'otp-request-1',
plusManualConfirmationStep: 7,
plusManualConfirmationMethod: 'gopay-otp',
};
const { router, events } = createRouter({ state });
const response = await router.handleMessage({
type: 'RESOLVE_PLUS_MANUAL_CONFIRMATION',
source: 'sidepanel',
payload: {
step: 7,
requestId: 'otp-request-1',
confirmed: true,
otp: ' 12-34 56 ',
},
}, {});
assert.deepStrictEqual(response, { ok: true });
assert.equal(events.notifyCompletions.length, 0);
assert.equal(events.stepStatuses.length, 0);
assert.equal(events.stateUpdates[0].gopayHelperResolvedOtp, '123456');
assert.equal(events.stateUpdates[0].plusManualConfirmationPending, false);
assert.deepStrictEqual(events.broadcasts[0], events.stateUpdates[0]);
});
test('message router refreshes GPC balance through explicit sidepanel message', async () => {
const state = {
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'http://localhost:18473/',
gopayHelperCardKey: 'state_card',
};
const { router, events } = createRouter({ state });
const response = await router.handleMessage({
type: 'REFRESH_GPC_CARD_BALANCE',
source: 'sidepanel',
payload: {
gopayHelperCardKey: 'payload_card',
reason: 'manual',
},
}, {});
assert.deepStrictEqual(response, { ok: true, balance: '余额 3' });
assert.equal(events.balanceRefreshes.length, 1);
assert.equal(events.balanceRefreshes[0].state.gopayHelperApiUrl, 'http://localhost:18473/');
assert.equal(events.balanceRefreshes[0].state.gopayHelperCardKey, 'payload_card');
assert.deepStrictEqual(events.balanceRefreshes[0].options, { reason: 'manual' });
});
@@ -96,8 +96,11 @@ return {
});
test('background step definitions resolve titles from the frozen signup method', () => {
const api = new Function(`
const api = new Function(`
const captured = [];
const PLUS_PAYMENT_METHOD_PAYPAL = 'paypal';
const PLUS_PAYMENT_METHOD_GOPAY = 'gopay';
const PLUS_PAYMENT_METHOD_GPC_HELPER = 'gpc-helper';
const self = {
MultiPageStepDefinitions: {
getSteps(options) {
+89
View File
@@ -13,3 +13,92 @@ test('GoPay utils normalize manual OTP input', () => {
assert.equal(api.normalizeGoPayOtp(' 12-34 56 '), '123456');
assert.equal(api.normalizeGoPayOtp('abc'), '');
});
test('GoPay utils keeps GPC helper payment method distinct', () => {
const api = loadGoPayUtils();
assert.equal(api.normalizePlusPaymentMethod('gpc-helper'), 'gpc-helper');
assert.equal(api.normalizePlusPaymentMethod('gopay'), 'gopay');
assert.equal(api.normalizePlusPaymentMethod('unknown'), 'paypal');
});
test('GoPay utils builds GPC card balance URL from helper endpoints', () => {
const api = loadGoPayUtils();
assert.equal(
api.buildGpcCardBalanceUrl('http://localhost:18473/', ' card key/1 '),
'http://localhost:18473/api/card/balance?card_key=card%20key%2F1'
);
assert.equal(
api.buildGpcCardBalanceUrl('https://gopay.hwork.pro/api/checkout/start', 'GPC-1'),
'https://gopay.hwork.pro/api/card/balance?card_key=GPC-1'
);
assert.equal(
api.buildGpcCardBalanceUrl('https://gopay.hwork.pro/api/card/balance?card_key=old', 'new'),
'https://gopay.hwork.pro/api/card/balance?card_key=new'
);
});
test('GoPay utils builds GPC OTP/PIN payloads with card_key and flow_id', () => {
const api = loadGoPayUtils();
assert.deepEqual(
api.buildGpcOtpPayload({
reference_id: ' ref_1 ',
otp: ' 12-34 56 ',
card_key: ' card_1 ',
gopay_guid: ' guid_1 ',
flow_id: ' flow_1 ',
redirect_url: 'https://pm-redirects.stripe.com/test',
}),
{
reference_id: 'ref_1',
otp: '123456',
card_key: 'card_1',
flow_id: 'flow_1',
gopay_guid: 'guid_1',
redirect_url: 'https://pm-redirects.stripe.com/test',
}
);
assert.deepEqual(
api.buildGpcOtpRetryPayload({ referenceId: 'ref_1', otp: '123456', cardKey: 'card_1', flowId: 'flow_1' }),
{
reference_id: 'ref_1',
otp: '123456',
card_key: 'card_1',
flow_id: 'flow_1',
code: '123456',
}
);
assert.deepEqual(
api.buildGpcPinPayload({
referenceId: 'ref_1',
challengeId: 'challenge_1',
gopayGuid: 'guid_1',
pin: '65-43-21',
cardKey: 'card_1',
flowId: 'flow_1',
}),
{
reference_id: 'ref_1',
challenge_id: 'challenge_1',
gopay_guid: 'guid_1',
pin: '654321',
card_key: 'card_1',
flow_id: 'flow_1',
}
);
});
test('GoPay utils formats balance and maps linked-account errors', () => {
const api = loadGoPayUtils();
assert.equal(
api.formatGpcBalancePayload({ remaining_uses: 12, card_status: 'active', flow_id: 'flow_1' }),
'余额 12,状态 activeflow_id flow_1'
);
assert.equal(
api.extractGpcResponseErrorDetail({ errors: [{ loc: ['body', 'otp'], msg: 'Field required' }] }, 422),
'body.otp: Field required'
);
assert.equal(
api.extractGpcResponseErrorDetail({ error_messages: ['account already linked'] }, 406),
'GOPAY已经绑了订阅,需要手动解绑'
);
});
@@ -81,6 +81,7 @@ function createExecutorHarness({
readyByFrame = {},
fetchImpl = null,
getAddressSeedForCountry = () => createAddressSeed(),
getState = null,
markCurrentRegistrationAccountUsed = async () => {},
probeIpProxyExit = null,
submitRedirectUrl = 'https://www.paypal.com/checkoutnow',
@@ -153,6 +154,7 @@ function createExecutorHarness({
fetch: fetchImpl,
generateRandomName: () => ({ firstName: 'Ada', lastName: 'Lovelace' }),
getAddressSeedForCountry,
getState: typeof getState === 'function' ? getState : async () => ({}),
getTabId: async () => null,
isTabAlive: async () => false,
markCurrentRegistrationAccountUsed,
@@ -795,3 +797,149 @@ test('Plus checkout billing reports when the payment iframe exists but cannot re
/已定位到 PayPal 所在 iframeframeId=7),但账单脚本无法注入该 iframe/
);
});
test('GPC billing normalizes API URL and submits OTP then PIN with card_key and flow_id', async () => {
const fetchCalls = [];
let currentState = {
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: '',
};
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
getState: async () => currentState,
fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url.endsWith('/api/gopay/otp')) {
return {
ok: true,
status: 200,
json: async () => ({ reference_id: 'ref_123', challenge_id: 'challenge_456' }),
};
}
if (url.endsWith('/api/gopay/pin')) {
return {
ok: true,
status: 200,
json: async () => ({ stage: 'gopay_complete' }),
};
}
throw new Error(`unexpected url: ${url}`);
},
});
const run = executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperReferenceId: 'ref_123',
gopayHelperGoPayGuid: 'guid_789',
gopayHelperApiUrl: 'https://gopay.hwork.pro/api/checkout/start',
gopayHelperPin: '654321',
gopayHelperCardKey: 'card_billing_123',
gopayHelperFlowId: 'flow_billing_123',
});
await new Promise((resolve) => setTimeout(resolve, 20));
const pending = events.states.find((state) => state.plusManualConfirmationMethod === 'gopay-otp');
assert.ok(pending);
currentState = {
plusManualConfirmationPending: false,
plusManualConfirmationRequestId: pending.plusManualConfirmationRequestId,
gopayHelperResolvedOtp: '123456',
};
await run;
assert.equal(fetchCalls[0].url, 'https://gopay.hwork.pro/api/gopay/otp');
assert.deepEqual(JSON.parse(fetchCalls[0].options.body), {
reference_id: 'ref_123',
otp: '123456',
card_key: 'card_billing_123',
flow_id: 'flow_billing_123',
gopay_guid: 'guid_789',
});
assert.equal(fetchCalls[1].url, 'https://gopay.hwork.pro/api/gopay/pin');
assert.deepEqual(JSON.parse(fetchCalls[1].options.body), {
reference_id: 'ref_123',
challenge_id: 'challenge_456',
gopay_guid: 'guid_789',
pin: '654321',
card_key: 'card_billing_123',
flow_id: 'flow_billing_123',
});
assert.equal(events.completed[0].step, 7);
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
});
test('GPC billing retries OTP with compatibility field after HTTP 400', async () => {
const fetchCalls = [];
let currentState = {
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: '',
};
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
getState: async () => currentState,
fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url.endsWith('/api/gopay/otp') && fetchCalls.filter((call) => call.url.endsWith('/api/gopay/otp')).length === 1) {
return {
ok: false,
status: 400,
json: async () => ({ error: 'otp field invalid' }),
};
}
if (url.endsWith('/api/gopay/otp')) {
return {
ok: true,
status: 200,
json: async () => ({ challenge_id: 'challenge_retry' }),
};
}
if (url.endsWith('/api/gopay/pin')) {
return {
ok: true,
status: 200,
json: async () => ({ stage: 'gopay_complete' }),
};
}
throw new Error(`unexpected url: ${url}`);
},
});
const run = executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperReferenceId: 'ref_retry',
gopayHelperGoPayGuid: 'guid_retry',
gopayHelperRedirectUrl: 'https://pm-redirects.stripe.com/retry',
gopayHelperApiUrl: 'http://localhost:18473/',
gopayHelperPin: '654321',
gopayHelperCardKey: 'card_retry',
gopayHelperFlowId: 'flow_retry',
});
await new Promise((resolve) => setTimeout(resolve, 20));
const pending = events.states.find((state) => state.plusManualConfirmationMethod === 'gopay-otp');
currentState = {
plusManualConfirmationPending: false,
plusManualConfirmationRequestId: pending.plusManualConfirmationRequestId,
gopayHelperResolvedOtp: '123456',
};
await run;
assert.equal(fetchCalls.filter((call) => call.url.endsWith('/api/gopay/otp')).length, 2);
assert.deepEqual(JSON.parse(fetchCalls[1].options.body), {
reference_id: 'ref_retry',
otp: '123456',
card_key: 'card_retry',
flow_id: 'flow_retry',
gopay_guid: 'guid_retry',
redirect_url: 'https://pm-redirects.stripe.com/retry',
code: '123456',
});
assert.equal(events.logs.some((entry) => /兼容字段重试/.test(entry.message)), true);
assert.equal(events.completed[0].step, 7);
});
+218
View File
@@ -106,3 +106,221 @@ test('GoPay plus checkout create forwards gopay payment method to the checkout c
assert.deepStrictEqual(events[0]?.payload, { paymentMethod: 'gopay' });
});
test('GPC checkout injects Plus script before reading ChatGPT session token and sends card_key', async () => {
const events = [];
const fetchCalls = [];
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: 77 };
},
remove: async (tabId) => events.push({ type: 'tab-remove', tabId }),
},
},
completeStepFromBackground: async (step, payload) => events.push({ type: 'complete', step, payload }),
ensureContentScriptReadyOnTabUntilStopped: async (source, tabId, options) => events.push({ type: 'ready', source, tabId, options }),
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
json: async () => ({
reference_id: 'ref_123',
gopay_guid: 'guid_456',
next_action: 'enter_otp',
flow_id: 'flow_789',
}),
};
},
registerTab: async (source, tabId) => events.push({ type: 'register', source, tabId }),
sendTabMessageUntilStopped: async (tabId, source, message) => {
events.push({ type: 'tab-message', tabId, source, message });
return { accessToken: 'session-access-token' };
},
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({
email: 'Current.Round+GPC@Example.COM',
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
gopayHelperPhoneNumber: '+8613800138000',
gopayPhone: '',
gopayHelperCountryCode: '+86',
gopayHelperPin: '123456',
gopayHelperCardKey: 'card_test_123',
});
const readyIndex = events.findIndex((event) => event.type === 'ready');
const messageIndex = events.findIndex((event) => event.type === 'tab-message');
assert.ok(readyIndex >= 0);
assert.ok(messageIndex > readyIndex);
assert.equal(events[messageIndex].message.type, 'PLUS_CHECKOUT_GET_STATE');
assert.deepEqual(events[messageIndex].message.payload, {
includeSession: true,
includeAccessToken: true,
});
assert.equal(fetchCalls.length, 1);
assert.equal(fetchCalls[0].url, 'https://gopay.hwork.pro/api/checkout/start');
const helperPayload = JSON.parse(fetchCalls[0].options.body);
assert.equal(helperPayload.customer_email, 'current.round+gpc@example.com');
assert.equal(helperPayload.card_key, 'card_test_123');
assert.deepEqual(helperPayload.gopay_link, {
type: 'gopay',
country_code: '86',
phone_number: '13800138000',
});
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.plusCheckoutSource, 'gpc-helper');
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperReferenceId, 'ref_123');
assert.equal(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperFlowId, 'flow_789');
assert.equal(events.find((event) => event.type === 'complete')?.step, 6);
assert.equal(events.find((event) => event.type === 'complete')?.payload?.plusCheckoutSource, 'gpc-helper');
});
test('GPC checkout treats non-zero API amount as non-free-trial and does not create order', async () => {
const markCalls = [];
const fetchCalls = [];
const events = [];
const executor = api.createPlusCheckoutCreateExecutor({
addLog: async (message, level = 'info') => events.push({ type: 'log', message, level }),
chrome: {
tabs: {
create: async () => {
throw new Error('should not open token tab when direct access token exists');
},
remove: async () => {},
},
},
completeStepFromBackground: async () => {
throw new Error('should not complete step 6 for non-free-trial checkout');
},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
json: async () => ({
reference_id: 'ref_paid',
gopay_guid: 'guid_paid',
next_action: 'enter_otp',
checkout: { amount_due: 'Rp 29.000' },
}),
};
},
markCurrentRegistrationAccountUsed: async (state, options) => {
markCalls.push({ state, options });
return { updated: true };
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => {},
setState: async () => {},
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await assert.rejects(
() => executor.executePlusCheckoutCreate({
email: 'paid@example.com',
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
chatgptAccessToken: 'state-access-token',
gopayHelperPhoneNumber: '+8613800138000',
gopayHelperCountryCode: '+86',
gopayHelperPin: '123456',
gopayHelperCardKey: 'card_paid_456',
}),
/PLUS_CHECKOUT_NON_FREE_TRIAL::.*余额非 0/
);
assert.equal(fetchCalls.length, 1);
assert.equal(JSON.parse(fetchCalls[0].options.body).card_key, 'card_paid_456');
assert.equal(markCalls.length, 1);
assert.equal(markCalls[0].state.email, 'paid@example.com');
assert.equal(markCalls[0].options.reason, 'plus-checkout-non-free-trial');
assert.equal(events.some((event) => event.type === 'log' && /订单已创建/.test(event.message)), false);
});
test('GPC checkout does not fall back to browser GoPay phone fields', async () => {
const executor = api.createPlusCheckoutCreateExecutor({
addLog: async () => {},
chrome: {
tabs: {
create: async () => {
throw new Error('should not open token tab when direct access token exists');
},
remove: async () => {},
},
},
completeStepFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async () => {
throw new Error('should not call helper API without helper phone');
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => {},
setState: async () => {},
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await assert.rejects(
() => executor.executePlusCheckoutCreate({
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
chatgptAccessToken: 'state-access-token',
email: 'helper-phone-test@example.com',
gopayPhone: '+8613800138000',
gopayCountryCode: '+86',
gopayPin: '123456',
gopayHelperPhoneNumber: '',
gopayHelperPin: '123456',
gopayHelperCardKey: 'card_phone_test',
}),
/缺少手机号/
);
});
test('GPC checkout rejects missing card key before calling helper API', async () => {
const executor = api.createPlusCheckoutCreateExecutor({
addLog: async () => {},
chrome: {
tabs: {
create: async () => {
throw new Error('should not open token tab when direct access token exists');
},
remove: async () => {},
},
},
completeStepFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async () => {
throw new Error('should not call helper API without card key');
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => {},
setState: async () => {},
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await assert.rejects(
() => executor.executePlusCheckoutCreate({
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
chatgptAccessToken: 'state-access-token',
email: 'missing-card@example.com',
gopayHelperPhoneNumber: '+8613800138000',
gopayHelperCountryCode: '+86',
gopayHelperPin: '123456',
gopayHelperCardKey: '',
}),
/缺少卡密/
);
});
+188
View File
@@ -32,6 +32,35 @@ function extractFunction(name) {
return sidepanelSource.slice(start, end);
}
function extractLastFunction(name) {
const asyncStart = sidepanelSource.lastIndexOf(`async function ${name}`);
const normalStart = sidepanelSource.lastIndexOf(`function ${name}`);
const asyncInnerFunctionStart = asyncStart >= 0 ? asyncStart + 'async '.length : -1;
const start = asyncStart >= 0 && normalStart === asyncInnerFunctionStart
? asyncStart
: (asyncStart > normalStart ? asyncStart : normalStart);
if (start === -1) {
throw new Error(`Function ${name} not found`);
}
const signatureEnd = sidepanelSource.indexOf(')', start);
const bodyStart = sidepanelSource.indexOf('{', signatureEnd);
let depth = 0;
let end = bodyStart;
for (; end < sidepanelSource.length; end += 1) {
const char = sidepanelSource[end];
if (char === '{') {
depth += 1;
} else if (char === '}') {
depth -= 1;
if (depth === 0) {
end += 1;
break;
}
}
}
return sidepanelSource.slice(start, end);
}
test('sidepanel step definitions keep the selected Plus payment method', () => {
const bundle = [
extractFunction('normalizeSignupMethod'),
@@ -126,6 +155,107 @@ return { updatePlusModeUI, selectPlusPaymentMethod, rowPayPalAccount };
assert.equal(api.rowPayPalAccount.style.display, '');
});
test('sidepanel step definitions keep GPC helper mode distinct', () => {
const bundle = [
extractFunction('normalizeSignupMethod'),
extractFunction('normalizePlusPaymentMethod'),
extractFunction('getStepDefinitionsForMode'),
extractFunction('rebuildStepDefinitionState'),
extractFunction('syncStepDefinitionsForMode'),
].join('\n');
const api = new Function(`
const calls = [];
const window = {
MultiPageStepDefinitions: {
getSteps(options) {
calls.push({ type: 'getSteps', options });
return [{ id: options.plusPaymentMethod === 'gpc-helper' ? 13 : 6, order: 1 }];
},
},
};
let currentPlusModeEnabled = false;
let currentPlusPaymentMethod = 'paypal';
let currentSignupMethod = 'email';
const DEFAULT_SIGNUP_METHOD = 'email';
let stepDefinitions = [];
let STEP_IDS = [];
let STEP_DEFAULT_STATUSES = {};
let SKIPPABLE_STEPS = new Set();
function renderStepsList() {
calls.push({ type: 'render', stepIds: [...STEP_IDS] });
}
${bundle}
return {
calls,
syncStepDefinitionsForMode,
getCurrentPlusPaymentMethod: () => currentPlusPaymentMethod,
getStepIds: () => [...STEP_IDS],
};
`)();
api.syncStepDefinitionsForMode(true, 'gpc-helper', { render: true });
assert.equal(api.getCurrentPlusPaymentMethod(), 'gpc-helper');
assert.deepEqual(api.getStepIds(), [13]);
assert.deepEqual(api.calls[0], {
type: 'getSteps',
options: { plusModeEnabled: true, plusPaymentMethod: 'gpc-helper', signupMethod: 'email' },
});
});
test('sidepanel Plus UI shows GPC fields and purchase button only for GPC', () => {
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('updatePlusModeUI'),
].join('\n');
const api = new Function(`
let latestState = { plusPaymentMethod: 'gpc-helper' };
let currentPlusPaymentMethod = 'paypal';
const inputPlusModeEnabled = { checked: true };
const selectPlusPaymentMethod = { value: 'gpc-helper', style: { display: 'none' } };
const plusPaymentMethodCaption = { textContent: '' };
const btnGpcCardKeyPurchase = { style: { display: 'none' } };
const rowPayPalAccount = { style: { display: '' } };
const rowPlusPaymentMethod = { style: { display: 'none' } };
const rowGpcHelperApi = { style: { display: 'none' } };
const rowGpcHelperCardKey = { style: { display: 'none' } };
const rowGpcHelperCountryCode = { style: { display: 'none' } };
const rowGpcHelperPhone = { style: { display: 'none' } };
const rowGpcHelperPin = { style: { display: 'none' } };
const rowGoPayCountryCode = { style: { display: 'none' } };
const rowGoPayPhone = { style: { display: 'none' } };
const rowGoPayOtp = { style: { display: 'none' } };
const rowGoPayPin = { style: { display: 'none' } };
${bundle}
return {
updatePlusModeUI,
selectPlusPaymentMethod,
btnGpcCardKeyPurchase,
rowPayPalAccount,
plusPaymentMethodCaption,
rows: { rowGpcHelperApi, rowGpcHelperCardKey, rowGpcHelperCountryCode, rowGpcHelperPhone, rowGpcHelperPin },
};
`)();
api.updatePlusModeUI();
assert.equal(api.rowPayPalAccount.style.display, 'none');
assert.equal(api.btnGpcCardKeyPurchase.style.display, '');
assert.equal(api.rows.rowGpcHelperApi.style.display, '');
assert.equal(api.rows.rowGpcHelperCardKey.style.display, '');
assert.equal(api.rows.rowGpcHelperPhone.style.display, '');
assert.match(api.plusPaymentMethodCaption.textContent, /GPC/);
api.selectPlusPaymentMethod.value = 'gopay';
api.updatePlusModeUI();
assert.equal(api.btnGpcCardKeyPurchase.style.display, 'none');
assert.equal(api.rows.rowGpcHelperApi.style.display, 'none');
assert.equal(api.rowPayPalAccount.style.display, 'none');
});
test('sidepanel resolves pending GoPay manual confirmation from DATA_UPDATED state', async () => {
const bundle = [
extractFunction('openPlusManualConfirmationDialog'),
@@ -186,3 +316,61 @@ return { events, syncPlusManualConfirmationDialog };
assert.match(api.events[2].message, /GoPay/);
assert.equal(api.events[2].tone, 'info');
});
test('sidepanel resolves pending GPC OTP with typed code', async () => {
const bundle = [
extractLastFunction('openPlusManualConfirmationDialog'),
extractLastFunction('syncPlusManualConfirmationDialog'),
].join('\n');
const api = new Function(`
const events = [];
let latestState = {
plusManualConfirmationPending: true,
plusManualConfirmationRequestId: 'otp-request-1',
plusManualConfirmationStep: 7,
plusManualConfirmationMethod: 'gopay-otp',
plusManualConfirmationTitle: 'GPC OTP 验证',
plusManualConfirmationMessage: '请输入 OTP。',
};
let activePlusManualConfirmationRequestId = '';
let plusManualConfirmationDialogInFlight = false;
const sharedFormDialog = {
async open(options) {
events.push({ type: 'form', options });
return { otp: ' 12-34 56 ' };
},
};
function openActionModal(options) {
events.push({ type: 'modal', options });
return Promise.resolve('confirm');
}
function showToast(message, tone) {
events.push({ type: 'toast', message, tone });
}
const chrome = {
runtime: {
async sendMessage(message) {
events.push({ type: 'send', message });
latestState = { ...latestState, plusManualConfirmationPending: false };
return { ok: true };
},
},
};
${bundle}
return { events, syncPlusManualConfirmationDialog };
`)();
await api.syncPlusManualConfirmationDialog();
assert.equal(api.events[0].type, 'form');
assert.equal(api.events[0].options.confirmLabel, '提交 OTP');
const sendEvent = api.events.find((event) => event.type === 'send');
assert.deepEqual(sendEvent.message.payload, {
step: 7,
requestId: 'otp-request-1',
confirmed: true,
otp: '123456',
});
assert.equal(api.events.some((event) => event.type === 'modal'), false);
});
+29
View File
@@ -12,6 +12,7 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
const plusSteps = api.getSteps({ plusModeEnabled: true });
const plusPhoneSteps = api.getSteps({ plusModeEnabled: true, signupMethod: 'phone' });
const goPaySteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gopay' });
const gpcSteps = api.getSteps({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' });
assert.equal(Array.isArray(steps), true);
assert.equal(steps.length, 10);
@@ -90,6 +91,27 @@ test('step definitions module exposes ordered normal and Plus step metadata', ()
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gopay' }), 13);
assert.equal(goPaySteps[5].title, '打开 GoPay 订阅页');
assert.equal(goPaySteps[6].title, '等待 GoPay 订阅确认');
assert.deepStrictEqual(
gpcSteps.map((step) => step.key),
[
'open-chatgpt',
'submit-signup-email',
'fill-password',
'fetch-signup-code',
'fill-profile',
'plus-checkout-create',
'plus-checkout-billing',
'oauth-login',
'fetch-login-code',
'confirm-oauth',
'platform-verify',
]
);
assert.deepStrictEqual(api.getStepIds({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), [1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13]);
assert.equal(api.getLastStepId({ plusModeEnabled: true, plusPaymentMethod: 'gpc-helper' }), 13);
assert.equal(gpcSteps[5].title, '创建 GPC 订单');
assert.equal(gpcSteps[6].title, 'GPC OTP/PIN 验证');
});
test('sidepanel html loads shared step definitions before sidepanel bootstrap', () => {
@@ -111,5 +133,12 @@ test('sidepanel html exposes Plus mode, PayPal, and GoPay settings', () => {
assert.match(html, /id="input-gopay-phone"/);
assert.match(html, /id="input-gopay-otp"/);
assert.match(html, /id="input-gopay-pin"/);
assert.match(html, /<option value="gpc-helper">GPC<\/option>/);
assert.match(html, /id="btn-gpc-card-key-purchase"/);
assert.match(html, />购买卡密</);
assert.match(html, /id="input-gpc-helper-card-key"/);
assert.match(html, /id="btn-gpc-helper-balance"/);
assert.match(html, /id="input-gpc-helper-phone"/);
assert.match(html, /id="input-gpc-helper-pin"/);
assert.match(html, /id="shared-form-modal"/);
});