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:
2026-09-29 01:30:40 +03:00
parent ddf5cbb8e7
commit 0027c405c1
8 changed files with 571 additions and 14 deletions
+49
View File
@@ -26,6 +26,9 @@ code += extractFunc(html, 'stkNoPendingError') + '\n';
code += extractFunc(html, 'stkMenuPanelReset') + '\n';
code += extractFunc(html, 'stkMenuNaiSuffix') + '\n';
code += extractFunc(html, 'stkMenuItemsHtml') + '\n';
code += extractFunc(html, 'stkInputHtml') + '\n';
code += extractFunc(html, 'stkInputHint') + '\n';
code += extractFunc(html, 'stkInputValidate') + '\n';
code += 'globalThis.esc = s => s;\n';
eval(code);
@@ -132,3 +135,49 @@ test('an already-answered respond does not reach the dialog handler', async () =
assert.strictEqual(calls.handled, null);
assert.strictEqual(calls.rendered, 0);
});
test('stkInputHtml builds the card-driven input field', () => {
globalThis.t = s => s;
const out = stkInputHtml({ type: 'get_input', text: 'PIN', min: 4, max: 16,
digits_only: true, hidden: true });
assert.ok(out.includes('PIN'), out);
assert.ok(out.includes('type="password"'), out); // hidden entry
assert.ok(out.includes('inputmode="numeric"'), out); // digits only
assert.ok(out.includes('maxlength="16"'), out);
assert.ok(out.includes('4\u201316 characters'), out);
assert.ok(out.includes('hidden entry'), out);
// UCS2 alphabet entry: a plain text field without the numeric keyboard
const ucs2 = stkInputHtml({ type: 'get_input', text: 'Name', min: 0, max: 10, ucs2: true });
assert.ok(ucs2.includes('type="text"'), ucs2);
assert.ok(!ucs2.includes('inputmode'), ucs2);
assert.ok(ucs2.includes('up to 10 characters'), ucs2);
// 0xFF = no maximum (TS 102 223 8.11): no maxlength and no range hint
const noMax = stkInputHtml({ type: 'get_input', text: 'x', min: 0, max: 255 });
assert.ok(!noMax.includes('maxlength'), noMax);
assert.ok(!noMax.includes('characters'), noMax);
// Yes/No: buttons instead of a field
const yesNo = stkInputHtml({ type: 'get_inkey', text: 'Continue?', yes_no: true });
assert.ok(yesNo.includes("stkInputSubmit('01')"), yesNo);
assert.ok(yesNo.includes("stkInputSubmit('00')"), yesNo);
assert.ok(!yesNo.includes('stk-input-field'), yesNo);
// the card's default text is prefilled; help adds the help action
const def = stkInputHtml({ type: 'get_input', text: 'x', default: '1234', help: true });
assert.ok(def.includes('value="1234"'), def);
assert.ok(def.includes("stkMenuRespond('help')"), def);
});
test('stkInputValidate mirrors the server input rules', () => {
globalThis.t = s => s;
const pd = { type: 'get_input', min: 4, max: 16, digits_only: true, hidden: true };
assert.strictEqual(stkInputValidate(pd, '1234').ok, true);
assert.strictEqual(stkInputValidate(pd, '12').ok, false);
assert.strictEqual(stkInputValidate(pd, '12345678901234567').ok, false);
assert.strictEqual(stkInputValidate(pd, 'abcd').ok, false);
// hidden entry allows only the digits set even without digits_only
assert.strictEqual(stkInputValidate({ type: 'get_input', hidden: true }, 'ab').ok, false);
// GET INKEY: exactly one character, digits-only honours * # +
assert.strictEqual(stkInputValidate({ type: 'get_inkey' }, 'a').ok, true);
assert.strictEqual(stkInputValidate({ type: 'get_inkey' }, 'ab').ok, false);
assert.strictEqual(stkInputValidate({ type: 'get_inkey', digits_only: true }, '*').ok, true);
assert.strictEqual(stkInputValidate({ type: 'get_inkey', digits_only: true }, 'a').ok, false);
});