emv
Version:
EMV / Chip and PIN CLI and library for PC/SC card readers
139 lines • 6.82 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useState, useCallback } from 'react';
import { Box, Text, useInput } from 'ink';
import SelectInput from 'ink-select-input';
import { format as formatTlv, findTagInBuffer, formatGpoResponse } from '../../emv-tags.js';
import { Header, Footer, CardBox, StatusBar, LoadingSpinner } from '../components/index.js';
export function ExploreScreen({ emv, app, onBack }) {
const [loading, setLoading] = useState(false);
const [data, setData] = useState([]);
const [error, setError] = useState(null);
const [selectedAction, setSelectedAction] = useState(null);
useInput((_input, key) => {
if (key.escape) {
if (selectedAction) {
setSelectedAction(null);
}
else {
onBack();
}
}
});
const fetchData = useCallback(async (action) => {
setLoading(true);
setError(null);
setData([]);
try {
switch (action) {
case 'gpo': {
const response = await emv.getProcessingOptions();
if (response.isOk()) {
const formatted = formatGpoResponse(response.buffer);
setData([
{ tag: 'GPO', name: 'GET_PROCESSING_OPTIONS', value: formatted },
]);
}
else {
setError(`GET PROCESSING OPTIONS failed: SW=${response.sw1.toString(16)}${response.sw2.toString(16)}`);
}
break;
}
case 'records': {
const records = [];
for (let sfi = 1; sfi <= 10; sfi++) {
for (let rec = 1; rec <= 10; rec++) {
const response = await emv.readRecord(sfi, rec);
if (response.isOk()) {
const formatted = formatTlv(response);
records.push({
tag: `SFI${String(sfi)}:R${String(rec)}`,
name: 'RECORD',
value: formatted || response.buffer.toString('hex'),
});
}
else if (response.sw1 === 0x6a && response.sw2 === 0x83) {
// Record not found, try next SFI
break;
}
}
}
if (records.length === 0) {
setError('No records found');
}
else {
setData(records);
}
break;
}
case 'pincount': {
const response = await emv.getData(0x9f17);
if (response.isOk()) {
const tagValue = findTagInBuffer(response.buffer, 0x9f17);
const count = tagValue?.[0];
setData([
{
tag: '9F17',
name: 'PIN_TRY_COUNT',
value: count !== undefined ? String(count) : 'Unknown',
},
]);
}
else {
setError(`PIN try count not available: SW=${response.sw1.toString(16)}${response.sw2.toString(16)}`);
}
break;
}
case 'atc': {
const response = await emv.getData(0x9f36);
if (response.isOk()) {
const tagValue = findTagInBuffer(response.buffer, 0x9f36);
const atcValue = tagValue && tagValue.length >= 2
? tagValue.readUInt16BE(0)
: undefined;
setData([
{
tag: '9F36',
name: 'APP_TRANSACTION_COUNTER',
value: atcValue !== undefined ? String(atcValue) : 'Unknown',
},
]);
}
else {
setError(`ATC not available: SW=${response.sw1.toString(16)}${response.sw2.toString(16)}`);
}
break;
}
}
}
catch (err) {
setError(err instanceof Error ? err.message : String(err));
}
finally {
setLoading(false);
}
}, [emv]);
const items = [
{ label: '📊 Get Processing Options', value: 'gpo' },
{ label: '📁 Read All Records', value: 'records' },
{ label: '🔢 PIN Try Counter', value: 'pincount' },
{ label: '🔄 Application Transaction Counter', value: 'atc' },
{ label: '← Back', value: 'back' },
];
if (loading) {
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, {}), _jsx(LoadingSpinner, { message: "Reading card data..." })] }));
}
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Header, {}), _jsxs(CardBox, { title: `Exploring: ${app.label ?? app.aid}`, children: [error && _jsx(StatusBar, { message: error, type: "error" }), data.length > 0 && (_jsx(Box, { flexDirection: "column", marginBottom: 1, children: data.map((item, i) => (_jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [_jsxs(Text, { color: "cyan", bold: true, children: [item.tag, " (", item.name, "):"] }), _jsx(Text, { color: "yellow", wrap: "wrap", children: item.value })] }, i))) })), _jsx(SelectInput, { items: items, onSelect: (item) => {
if (item.value === 'back') {
onBack();
}
else {
setSelectedAction(item.value);
void fetchData(item.value);
}
}, itemComponent: ({ isSelected, label }) => isSelected ? (_jsxs(Text, { color: "cyan", bold: true, children: ['▸ ', label] })) : (_jsxs(Text, { children: [' ', label] })) })] }), _jsx(Footer, { hints: [
{ keys: '↑↓', description: 'Navigate' },
{ keys: 'Enter', description: 'Select' },
{ keys: 'Esc', description: 'Back' },
] })] }));
}
//# sourceMappingURL=ExploreScreen.js.map