Plus中添加GPC模式
This commit is contained in:
+212
-14
@@ -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 = {}) {
|
||||
|
||||
@@ -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 代理自动同步能力尚未接入。');
|
||||
|
||||
@@ -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(`步骤 6:GPC 接口返回余额非 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::步骤 6:GPC 接口返回余额非 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('步骤 6:GPC 模式获取 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(`步骤 6:Plus Checkout 页面已就绪(${paymentMethodLabel} / ${result.country || 'DE'} ${result.currency || 'EUR'}),准备继续下一步。`, 'info');
|
||||
|
||||
@@ -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(`步骤 7:GPC 接口返回 ${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('步骤 7:GPC 模式缺少 reference_id,请先执行步骤 6。');
|
||||
}
|
||||
if (!apiUrl) {
|
||||
throw new Error('步骤 7:GPC 模式缺少 API 地址。');
|
||||
}
|
||||
if (!cardKey) {
|
||||
throw new Error('步骤 7:GPC 模式缺少卡密。');
|
||||
}
|
||||
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(`步骤 7:OTP 验证失败:${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('步骤 7:GPC OTP 验证后未返回 challenge_id。');
|
||||
}
|
||||
const pin = String(state?.gopayHelperPin || '').trim().replace(/[^\d]/g, '');
|
||||
if (!pin) {
|
||||
throw new Error('步骤 7:GPC 模式缺少 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(`步骤 7:PIN 验证失败:${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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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,状态 active,flow_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 所在 iframe(frameId=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);
|
||||
});
|
||||
|
||||
@@ -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: '',
|
||||
}),
|
||||
/缺少卡密/
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user