Plus中添加GPC模式
This commit is contained in:
@@ -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