feat: answer GET INKEY / GET INPUT interactively (v3.6.38)
The menu chain paused only for DISPLAY TEXT / SELECT ITEM / SET UP MENU, so a GET INPUT was auto-answered with a bare success TR and no Text string - the live RemMobileID PIN prompt got an empty answer. Server: _parse_get_inkey/_parse_get_input read the prompt, qualifier flags (digits-only, UCS2, Yes/No, immediate, hidden, packed, help), Response length 91 and Default Text 9D; the fetch handler pauses with them; _menu_send_response(..., text=) validates the answer (digits set, min/max, one char for GET INKEY) and builds the Text string TLV 8D in the requested coding (SMS unpacked 04 / UCS2 08 / packed 00 via _pack_gsm7), Yes/No as 01/00, empty input as the null text string, help as result 13 and the variable-timeout Duration echoed (6.8.4). /api/menu-respond takes text; /api/stk-status carries pending_data so a card-initiated prompt opens the panel; _decode_cmd decodes both commands. PWA: stkInputHtml/stkInputHint/stkInputValidate render the constrained panel (password field for hidden entry, numeric keyboard, default text, Yes/No buttons, help, immediate-digit auto-submit); OK submits through stkMenuOk; the poll opens the panel for card-initiated prompts. sw cache simple-v311; tests: 517 Python / 674 Node.
This commit is contained in:
+155
-2
@@ -1705,7 +1705,7 @@
|
||||
<div id="stk-menu-buttons" class="flex justify-between mt-3 pt-3 border-t border-gray-200 dark:border-slate-700 hidden">
|
||||
<button id="stk-timeout-btn" onclick="stkMenuRespond('timeout')" class="px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-l10n="Timeout">Timeout</button>
|
||||
<button id="stk-cancel-btn" onclick="stkMenuRespond('cancel')" class="px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600" data-l10n="Cancel">Cancel</button>
|
||||
<button id="stk-ok-btn" onclick="stkMenuRespond('ok')" class="px-4 py-1 text-xs rounded-full bg-blue-600 text-white hover:bg-blue-700" data-l10n="OK">OK</button>
|
||||
<button id="stk-ok-btn" onclick="stkMenuOk()" class="px-4 py-1 text-xs rounded-full bg-blue-600 text-white hover:bg-blue-700" data-l10n="OK">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1732,7 +1732,7 @@
|
||||
// ===== Version =====
|
||||
// Single source of truth for the PWA version: shown in the header and used
|
||||
// by the server version check in pysimConnect().
|
||||
const SIMPLE_VERSION = '3.6.37';
|
||||
const SIMPLE_VERSION = '3.6.38';
|
||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||
|
||||
// ===== Tab switching =====
|
||||
@@ -9688,10 +9688,126 @@ async function stkMenuItemClick(itemId) {
|
||||
}
|
||||
}
|
||||
|
||||
// User-input prompt for a pending GET INKEY / GET INPUT (pure). The card's
|
||||
// constraints drive the field: hidden entry masks the value (TS 102 223
|
||||
// 6.4.3), digits-only switches the keyboard, Response length bounds the text.
|
||||
function stkInputHtml(pd) {
|
||||
pd = pd || {};
|
||||
let html = '<div class="text-sm whitespace-pre-wrap p-2 bg-gray-50 dark:bg-slate-700 rounded mb-2">' + esc(pd.text || '') + '</div>';
|
||||
if (pd.yes_no) {
|
||||
html += '<div class="flex gap-2 justify-center">' +
|
||||
'<button onclick="stkInputSubmit(\'01\')" class="px-4 py-1 text-sm rounded bg-blue-600 text-white hover:bg-blue-700">' + esc(t('Yes')) + '</button>' +
|
||||
'<button onclick="stkInputSubmit(\'00\')" class="px-4 py-1 text-sm rounded bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600">' + esc(t('No')) + '</button>' +
|
||||
'</div>';
|
||||
return html;
|
||||
}
|
||||
const max = Number(pd.max) || 0;
|
||||
const attrs = [
|
||||
'id="stk-input-field"',
|
||||
'class="w-full px-2 py-1 text-sm rounded border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-900 font-mono"',
|
||||
'type="' + (pd.hidden ? 'password' : 'text') + '"',
|
||||
pd.digits_only ? 'inputmode="numeric"' : '',
|
||||
(max && max !== 0xFF) ? 'maxlength="' + max + '"' : '',
|
||||
pd.immediate ? 'oninput="stkInputImmediate()"' : '',
|
||||
'value="' + esc(pd.default || '') + '"',
|
||||
].filter(Boolean).join(' ');
|
||||
html += '<input ' + attrs + '>';
|
||||
const hint = stkInputHint(pd);
|
||||
if (hint) html += '<div class="text-xs text-gray-400 dark:text-slate-500 mt-1">' + esc(hint) + '</div>';
|
||||
if (pd.help) {
|
||||
html += '<div class="mt-2 text-center"><button onclick="stkMenuRespond(\'help\')" class="text-xs underline text-gray-500 dark:text-slate-400">' + esc(t('Help')) + '</button></div>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
// '4–16 characters · digits 0-9 * # + · hidden entry' (pure).
|
||||
function stkInputHint(pd) {
|
||||
pd = pd || {};
|
||||
const parts = [];
|
||||
const lo = Number(pd.min) || 0;
|
||||
const hi = Number(pd.max) || 0;
|
||||
const hasHi = hi && hi !== 0xFF;
|
||||
if (pd.type === 'get_inkey') {
|
||||
parts.push(t('one character'));
|
||||
} else if (lo && hasHi) {
|
||||
parts.push(lo + '\u2013' + hi + ' ' + t('characters'));
|
||||
} else if (hasHi) {
|
||||
parts.push(t('up to') + ' ' + hi + ' ' + t('characters'));
|
||||
} else if (lo) {
|
||||
parts.push(t('At least') + ' ' + lo + ' ' + t('characters'));
|
||||
}
|
||||
if (pd.digits_only || pd.hidden) parts.push(t('digits 0-9 * # +'));
|
||||
if (pd.hidden) parts.push(t('hidden entry'));
|
||||
return parts.join(' \u00b7 ');
|
||||
}
|
||||
|
||||
// Validate the typed answer against the card's request (pure; mirrors the
|
||||
// server rules so the panel can explain before sending).
|
||||
function stkInputValidate(pd, value) {
|
||||
pd = pd || {};
|
||||
const v = value == null ? '' : String(value);
|
||||
const digits = /^[0-9*#+]*$/;
|
||||
if (pd.type === 'get_inkey') {
|
||||
if (v.length !== 1) return { ok: false, error: t('Enter exactly one character') };
|
||||
if (pd.digits_only && !digits.test(v)) return { ok: false, error: t('Digits only (0-9, *, #, +)') };
|
||||
return { ok: true };
|
||||
}
|
||||
const lo = Number(pd.min) || 0;
|
||||
const hi = Number(pd.max) || 0;
|
||||
if (v.length < lo) return { ok: false, error: t('At least') + ' ' + lo + ' ' + t('characters') };
|
||||
if (hi && hi !== 0xFF && v.length > hi) return { ok: false, error: t('At most') + ' ' + hi + ' ' + t('characters') };
|
||||
if ((pd.digits_only || pd.hidden) && !digits.test(v)) return { ok: false, error: t('Digits only (0-9, *, #, +)') };
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
let _stkInputPending = null;
|
||||
|
||||
// Send the typed (or Yes/No) answer; the server validates again and builds
|
||||
// the Text string TERMINAL RESPONSE.
|
||||
async function stkInputSubmit(text) {
|
||||
const pd = _stkInputPending || {};
|
||||
if (text !== '01' && text !== '00') {
|
||||
const field = document.getElementById('stk-input-field');
|
||||
const value = field ? field.value : '';
|
||||
const v = stkInputValidate(pd, value);
|
||||
if (!v.ok) {
|
||||
document.getElementById('stk-menu-panel-content').insertAdjacentHTML('beforeend',
|
||||
'<div class="text-xs text-red-500 mt-1">' + esc(v.error) + '</div>');
|
||||
return;
|
||||
}
|
||||
text = value;
|
||||
}
|
||||
const content = document.getElementById('stk-menu-panel-content');
|
||||
content.innerHTML = '<span class="text-gray-500 text-sm">' + esc(t('Sending...')) + '</span>';
|
||||
try {
|
||||
const data = await pysimFetch('/api/menu-respond', { result: 'ok', text: text });
|
||||
stkMenuHandleResponse(data);
|
||||
} catch (e) {
|
||||
content.innerHTML = '<span class="text-red-500 text-sm">Error: ' + esc(e.message) + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
// The OK button: submit the pending input, otherwise the plain 'ok' answer.
|
||||
function stkMenuOk() {
|
||||
if (_stkInputPending && (_stkInputPending.type === 'get_input' || _stkInputPending.type === 'get_inkey')) {
|
||||
stkInputSubmit(null);
|
||||
return;
|
||||
}
|
||||
stkMenuRespond('ok');
|
||||
}
|
||||
|
||||
// Immediate digit response (GET INKEY qualifier bit 4): pass the first
|
||||
// character on without asking for confirmation.
|
||||
function stkInputImmediate() {
|
||||
const field = document.getElementById('stk-input-field');
|
||||
if (field && field.value.length >= 1) stkInputSubmit(null);
|
||||
}
|
||||
|
||||
function stkMenuHandleResponse(data) {
|
||||
const content = document.getElementById('stk-menu-panel-content');
|
||||
const backBtn = document.getElementById('stk-back-btn');
|
||||
const btns = document.getElementById('stk-menu-buttons');
|
||||
_stkInputPending = null;
|
||||
btns.classList.add('hidden');
|
||||
if (data.type === 'display_text') {
|
||||
content.innerHTML = '<div class="text-sm whitespace-pre-wrap p-2 bg-gray-50 dark:bg-slate-700 rounded">' + esc(data.text) + '</div>';
|
||||
@@ -9704,6 +9820,12 @@ function stkMenuHandleResponse(data) {
|
||||
backBtn.style.display = '';
|
||||
document.getElementById('stk-ok-btn').style.display = 'none';
|
||||
btns.classList.remove('hidden');
|
||||
} else if (data.type === 'get_input' || data.type === 'get_inkey') {
|
||||
_stkInputPending = data;
|
||||
content.innerHTML = stkInputHtml(data);
|
||||
backBtn.style.display = '';
|
||||
document.getElementById('stk-ok-btn').style.display = data.yes_no ? 'none' : '';
|
||||
btns.classList.remove('hidden');
|
||||
} else {
|
||||
content.innerHTML = '<span class="text-gray-500 text-sm">SW: ' + esc(data.sw || '?') + '</span>';
|
||||
stkSetPillsDisabled(false);
|
||||
@@ -9726,6 +9848,7 @@ async function stkSubItemClick(itemId) {
|
||||
// itself (the menu watchdog's timeout TERMINAL RESPONSE, or a new selection
|
||||
// that shadowed it): the dialog can no longer be answered (v3.6.35).
|
||||
function stkMenuPanelReset() {
|
||||
_stkInputPending = null;
|
||||
const panel = document.getElementById('stk-menu-panel');
|
||||
if (panel) panel.classList.add('hidden');
|
||||
const btns = document.getElementById('stk-menu-buttons');
|
||||
@@ -9734,6 +9857,18 @@ function stkMenuPanelReset() {
|
||||
if (back) back.style.display = 'none';
|
||||
}
|
||||
|
||||
// Open the panel for a pending command the server paused on by itself (a
|
||||
// card-initiated GET INPUT / SELECT ITEM): the reply to our own click renders
|
||||
// through stkMenuHandleResponse instead.
|
||||
function stkMenuShowPending(pd) {
|
||||
document.getElementById('stk-menu-panel-title').textContent = document.getElementById('stk-menu-title').textContent;
|
||||
stkSetPillsDisabled(true);
|
||||
stkMenuStack = [];
|
||||
document.getElementById('stk-back-btn').style.display = 'none';
|
||||
document.getElementById('stk-menu-panel').classList.remove('hidden');
|
||||
stkMenuHandleResponse(pd);
|
||||
}
|
||||
|
||||
// The /api/menu-respond reply for a command the server already answered.
|
||||
function stkNoPendingError(data) {
|
||||
return !!(data && typeof data.error === 'string'
|
||||
@@ -11921,6 +12056,12 @@ function pysimStartBackendPoll() {
|
||||
// A command the server answered by itself must not leave a stale
|
||||
// dialog behind (the panel is not refreshed when the tab is hidden)
|
||||
if (stkChanged && !stk.pending) stkMenuPanelReset();
|
||||
// A command the card sent on its own (not the reply to our click)
|
||||
// opens the panel with its prompt (v3.6.38)
|
||||
if (stkChanged && stk.pending && stk.pending_data && isViewVisible('tab-phone')
|
||||
&& document.getElementById('stk-menu-panel').classList.contains('hidden')) {
|
||||
stkMenuShowPending(stk.pending_data);
|
||||
}
|
||||
if (ts) {
|
||||
const wasRunning = testRunActive();
|
||||
_testRunState = ts;
|
||||
@@ -17497,6 +17638,18 @@ const LANG_RU = {
|
||||
'Event sent.': 'Событие отправлено.',
|
||||
'Event data sent.': 'Данные события отправлены.',
|
||||
'Sending...': 'Отправка...',
|
||||
'Yes': 'Да',
|
||||
'No': 'Нет',
|
||||
'Help': 'Справка',
|
||||
'one character': 'один символ',
|
||||
'characters': 'символов',
|
||||
'up to': 'до',
|
||||
'At least': 'Не менее',
|
||||
'At most': 'Не более',
|
||||
'digits 0-9 * # +': 'цифры 0-9 * # +',
|
||||
'hidden entry': 'скрытый ввод',
|
||||
'Enter exactly one character': 'Введите ровно один символ',
|
||||
'Digits only (0-9, *, #, +)': 'Только цифры (0-9, *, #, +)',
|
||||
'Invalid': 'Некорректное значение',
|
||||
'Service Status': 'Статус сервиса',
|
||||
'Normal service': 'Обычный сервис',
|
||||
|
||||
Reference in New Issue
Block a user