feat: add GPC SMS helper functionality with local SMS support

- Introduced GPC OTP channel selection (WhatsApp/SMS) in the sidepanel.
- Added local SMS helper toggle and URL input for macOS users.
- Implemented normalization functions for OTP channel and local SMS URL.
- Updated settings payload to include new GPC helper configurations.
- Enhanced Plus checkout process to utilize local SMS helper for OTP retrieval.
- Added tests for GPC SMS helper script and its integration in the checkout flow.
- Updated documentation to reflect new GPC helper features and configurations.
This commit is contained in:
QLHazyCoder
2026-05-06 01:39:11 +08:00
parent 7b9f4a12ec
commit aa57e0f645
16 changed files with 980 additions and 27 deletions
@@ -78,6 +78,8 @@ test('background account history settings are normalized independently from hotm
extractFunction('normalizeFiveSimMaxPrice'),
extractFunction('normalizeFiveSimCountryFallback'),
extractFunction('normalizeSub2ApiGroupNames'),
extractFunction('normalizeBoundedIntegerSetting'),
extractFunction('normalizeLocalHttpBaseUrl'),
extractFunction('normalizePersistentSettingValue'),
].join('\n');
@@ -197,6 +199,15 @@ return {
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('gopayHelperOtpChannel', 'SMS'), 'sms');
assert.equal(api.normalizePersistentSettingValue('gopayHelperOtpChannel', 'unknown'), 'whatsapp');
assert.equal(api.normalizePersistentSettingValue('gopayHelperLocalSmsHelperEnabled', 1), true);
assert.equal(
api.normalizePersistentSettingValue('gopayHelperLocalSmsHelperUrl', 'http://127.0.0.1:18767/otp?x=1'),
'http://127.0.0.1:18767'
);
assert.equal(api.normalizePersistentSettingValue('gopayHelperLocalSmsTimeoutSeconds', '999'), 300);
assert.equal(api.normalizePersistentSettingValue('gopayHelperLocalSmsPollIntervalSeconds', '0'), 1);
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '7'), 7);
assert.equal(api.normalizePersistentSettingValue('verificationResendCount', '-1'), 0);
assert.equal(api.normalizePersistentSettingValue('phoneVerificationReplacementLimit', '9'), 9);
+3
View File
@@ -12,6 +12,9 @@ test('GoPay utils normalize manual OTP input', () => {
const api = loadGoPayUtils();
assert.equal(api.normalizeGoPayOtp(' 12-34 56 '), '123456');
assert.equal(api.normalizeGoPayOtp('abc'), '');
assert.equal(api.normalizeGpcOtpChannel('sms'), 'sms');
assert.equal(api.normalizeGpcOtpChannel('wa'), 'whatsapp');
assert.equal(api.normalizeGpcOtpChannel('unknown'), 'whatsapp');
});
test('GoPay utils keeps GPC helper payment method distinct', () => {
+81
View File
@@ -0,0 +1,81 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
const path = require('node:path');
const scriptPath = path.join(process.cwd(), 'scripts/gpc_sms_helper_macos.py');
function runPython(args, options = {}) {
for (const command of ['python3', 'python']) {
const result = spawnSync(command, args, {
encoding: 'utf8',
...options,
env: {
...process.env,
PYTHONIOENCODING: 'utf-8',
...(options.env || {}),
},
});
if (result.error && result.error.code === 'ENOENT') {
continue;
}
if (process.platform === 'win32' && result.status === 9009) {
continue;
}
return result;
}
return null;
}
test('GPC SMS helper shows macOS and iPhone forwarding guidance on non-macOS', () => {
const help = runPython([scriptPath, '--help']);
if (!help) {
return;
}
assert.equal(help.status, 0);
if (process.platform === 'darwin') {
return;
}
const run = runPython([scriptPath, '--db', '/tmp/nonexistent-gpc-chat.db'], {
timeout: 3000,
});
assert.ok(run);
assert.notEqual(run.status, 0);
assert.match(run.stderr, /仅支持 macOS/);
assert.match(run.stderr, /iPhone 短信已转发|短信转发/);
});
test('GPC SMS helper filters cached OTP records by order timestamp', () => {
const code = `
import importlib.util
import json
script_path = ${JSON.stringify(scriptPath)}
spec = importlib.util.spec_from_file_location("gpc_sms_helper_macos", script_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
old = {"otp": "111111", "code": "111111", "message_id": "old", "received_at": "2026-05-05T00:00:00+00:00"}
fresh = {"otp": "222222", "code": "222222", "message_id": "fresh", "received_at": "2026-05-05T00:00:10+00:00"}
state = {"last_otp": fresh, "otps": [fresh, old]}
payload = {
"without_filter": module.select_otp_record(state, 0)["otp"],
"fresh_only": module.select_otp_record(state, module.parse_timestamp_ms("2026-05-05T00:00:05+00:00"))["otp"],
"none_after_fresh": module.select_otp_record(state, module.parse_timestamp_ms("2026-05-05T00:00:11+00:00")) is None,
}
print(json.dumps(payload))
`;
const run = runPython(['-c', code], {
timeout: 3000,
env: { GPC_SMS_HELPER_ALLOW_NON_MAC: '1' },
});
if (!run) {
return;
}
assert.equal(run.status, 0, run.stderr);
assert.deepEqual(JSON.parse(run.stdout.trim()), {
without_filter: '222222',
fresh_only: '222222',
none_after_fresh: true,
});
});
@@ -871,6 +871,67 @@ test('GPC billing normalizes API URL and submits OTP then PIN with card_key and
assert.equal(events.completed[0].payload.plusCheckoutSource, 'gpc-helper');
});
test('GPC billing reads OTP from local SMS helper when enabled', async () => {
const fetchCalls = [];
const { events, executor } = createExecutorHarness({
frames: [],
stateByFrame: {},
fetchImpl: async (url, options = {}) => {
fetchCalls.push({ url, options });
if (url.startsWith('http://127.0.0.1:18767/otp')) {
return {
ok: true,
status: 200,
json: async () => ({ ok: true, otp: '654321', message_id: 'sms-1' }),
};
}
if (url.endsWith('/api/gopay/otp')) {
return {
ok: true,
status: 200,
json: async () => ({ reference_id: 'ref_sms', challenge_id: 'challenge_sms' }),
};
}
if (url.endsWith('/api/gopay/pin')) {
return {
ok: true,
status: 200,
json: async () => ({ stage: 'gopay_complete' }),
};
}
throw new Error(`unexpected url: ${url}`);
},
});
await executor.executePlusCheckoutBilling({
plusPaymentMethod: 'gpc-helper',
plusCheckoutSource: 'gpc-helper',
gopayHelperReferenceId: 'ref_sms',
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
gopayHelperPin: '654321',
gopayHelperCardKey: 'card_sms',
gopayHelperOtpChannel: 'sms',
gopayHelperLocalSmsHelperEnabled: true,
gopayHelperLocalSmsHelperUrl: 'http://127.0.0.1:18767',
gopayHelperPhoneNumber: '+8613800138000',
gopayHelperOrderCreatedAt: 1710000000000,
});
assert.equal(events.states.some((state) => state.plusManualConfirmationMethod === 'gopay-otp'), false);
assert.equal(events.states.some((state) => state.gopayHelperResolvedOtp === '654321'), true);
const helperUrl = new URL(fetchCalls[0].url);
assert.equal(helperUrl.origin + helperUrl.pathname, 'http://127.0.0.1:18767/otp');
assert.equal(helperUrl.searchParams.get('reference_id'), 'ref_sms');
assert.equal(helperUrl.searchParams.get('phone_number'), '+8613800138000');
assert.equal(helperUrl.searchParams.get('after_ms'), '1710000000000');
assert.deepEqual(JSON.parse(fetchCalls[1].options.body), {
reference_id: 'ref_sms',
otp: '654321',
card_key: 'card_sms',
});
assert.equal(events.completed[0].step, 7);
});
test('GPC billing retries OTP with compatibility field after HTTP 400', async () => {
const fetchCalls = [];
let currentState = {
+46
View File
@@ -175,14 +175,60 @@ test('GPC checkout injects Plus script before reading ChatGPT session token and
type: 'gopay',
country_code: '86',
phone_number: '13800138000',
phone_mode: 'manual',
otp_channel: 'whatsapp',
});
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.ok(events.find((event) => event.type === 'set-state')?.payload?.gopayHelperOrderCreatedAt > 0);
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 forwards selected SMS OTP channel', async () => {
const fetchCalls = [];
const executor = api.createPlusCheckoutCreateExecutor({
addLog: async () => {},
chrome: {
tabs: {
create: async () => ({ id: 88 }),
remove: async () => {},
},
},
completeStepFromBackground: async () => {},
ensureContentScriptReadyOnTabUntilStopped: async () => {},
fetch: async (url, options = {}) => {
fetchCalls.push({ url, options });
return {
ok: true,
status: 200,
json: async () => ({ reference_id: 'ref_sms', next_action: 'enter_otp' }),
};
},
registerTab: async () => {},
sendTabMessageUntilStopped: async () => ({ accessToken: 'session-access-token' }),
setState: async () => {},
sleepWithStop: async () => {},
waitForTabCompleteUntilStopped: async () => {},
});
await executor.executePlusCheckoutCreate({
email: 'sms@example.com',
plusPaymentMethod: 'gpc-helper',
gopayHelperApiUrl: 'https://gopay.hwork.pro/',
gopayHelperPhoneNumber: '+8613800138000',
gopayHelperCountryCode: '+86',
gopayHelperPin: '123456',
gopayHelperCardKey: 'card_sms',
gopayHelperOtpChannel: 'sms',
});
const helperPayload = JSON.parse(fetchCalls[0].options.body);
assert.equal(helperPayload.gopay_link.phone_mode, 'manual');
assert.equal(helperPayload.gopay_link.otp_channel, 'sms');
});
test('GPC checkout treats non-zero API amount as non-free-trial and does not create order', async () => {
const markCalls = [];
const fetchCalls = [];
+28 -1
View File
@@ -132,6 +132,7 @@ test('sidepanel Plus UI hides PayPal account selector while GoPay is selected',
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
@@ -208,6 +209,7 @@ test('sidepanel Plus UI shows GPC fields and purchase button only for GPC withou
const bundle = [
extractFunction('normalizePlusPaymentMethod'),
extractFunction('getSelectedPlusPaymentMethod'),
extractFunction('normalizeGpcOtpChannelValue'),
extractFunction('updatePlusModeUI'),
].join('\n');
@@ -224,6 +226,11 @@ const rowGpcHelperApi = { style: { display: 'none' } };
const rowGpcHelperCardKey = { style: { display: 'none' } };
const rowGpcHelperCountryCode = { style: { display: 'none' } };
const rowGpcHelperPhone = { style: { display: 'none' } };
const rowGpcHelperOtpChannel = { style: { display: 'none' } };
const selectGpcHelperOtpChannel = { value: 'whatsapp' };
const rowGpcHelperLocalSmsEnabled = { style: { display: 'none' } };
const inputGpcHelperLocalSmsEnabled = { checked: false };
const rowGpcHelperLocalSmsUrl = { style: { display: 'none' } };
const rowGpcHelperPin = { style: { display: 'none' } };
const rowGoPayCountryCode = { style: { display: 'none' } };
const rowGoPayPhone = { style: { display: 'none' } };
@@ -233,10 +240,12 @@ ${bundle}
return {
updatePlusModeUI,
selectPlusPaymentMethod,
selectGpcHelperOtpChannel,
inputGpcHelperLocalSmsEnabled,
btnGpcCardKeyPurchase,
rowPayPalAccount,
plusPaymentMethodCaption,
rows: { rowGpcHelperApi, rowGpcHelperCardKey, rowGpcHelperCountryCode, rowGpcHelperPhone, rowGpcHelperPin },
rows: { rowGpcHelperApi, rowGpcHelperCardKey, rowGpcHelperCountryCode, rowGpcHelperPhone, rowGpcHelperOtpChannel, rowGpcHelperLocalSmsEnabled, rowGpcHelperLocalSmsUrl, rowGpcHelperPin },
};
`)();
@@ -247,8 +256,26 @@ return {
assert.equal(api.rows.rowGpcHelperApi.style.display, 'none');
assert.equal(api.rows.rowGpcHelperCardKey.style.display, '');
assert.equal(api.rows.rowGpcHelperPhone.style.display, '');
assert.equal(api.rows.rowGpcHelperOtpChannel.style.display, '');
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, 'none');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
assert.match(api.plusPaymentMethodCaption.textContent, /GPC/);
api.selectGpcHelperOtpChannel.value = 'sms';
api.updatePlusModeUI();
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, '');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
api.inputGpcHelperLocalSmsEnabled.checked = true;
api.updatePlusModeUI();
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, '');
api.selectGpcHelperOtpChannel.value = 'whatsapp';
api.updatePlusModeUI();
assert.equal(api.inputGpcHelperLocalSmsEnabled.checked, false);
assert.equal(api.rows.rowGpcHelperLocalSmsEnabled.style.display, 'none');
assert.equal(api.rows.rowGpcHelperLocalSmsUrl.style.display, 'none');
api.selectPlusPaymentMethod.value = 'gopay';
api.updatePlusModeUI();
assert.equal(api.btnGpcCardKeyPurchase.style.display, 'none');
+3
View File
@@ -141,6 +141,9 @@ test('sidepanel html exposes Plus mode, PayPal, and GoPay settings', () => {
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="select-gpc-helper-otp-channel"/);
assert.match(html, /id="input-gpc-helper-local-sms-enabled"/);
assert.match(html, /id="input-gpc-helper-local-sms-url"/);
assert.match(html, /id="input-gpc-helper-pin"/);
assert.match(html, /id="shared-form-modal"/);
});