cryptnox-pos 1.0.0
Standalone USDC payment terminal firmware (ESP32 + Cryptnox smart card)
Loading...
Searching...
No Matches
ui_sale.cpp
Go to the documentation of this file.
1/*
2 * SPDX-License-Identifier: LGPL-3.0-or-later
3 * Copyright (c) 2026 Cryptnox SA
4 */
5
11
12#include "ui_internal.h"
13
14#define CHIP_W 56
15
27/* How far below the line box's middle the middle of a digit's ink sits. */
28static lv_coord_t amount_ink_shift(const lv_font_t *f) {
29 lv_font_glyph_dsc_t g;
30 if (!lv_font_get_glyph_dsc(f, &g, '0', 0)) { return 0; }
31 const lv_coord_t ink_mid = f->line_height - f->base_line - g.ofs_y - (g.box_h / 2);
32 return (f->line_height / 2) - ink_mid;
33}
34
35static void amount_row_place(void) {
36 if (s_amount_row == NULL) { return; }
37
38 /* Centred, then measured there: the pill and the figure are both
39 * content-sized (chevron on/off, 3- or 4-letter ticker). */
40 lv_obj_align(s_amount_row, LV_ALIGN_TOP_MID, 0, ASSET_BTN_Y);
41 lv_obj_update_layout(s_amount_row);
42
43 /* Measured off the pill's own left edge, not SCR_W: the row lives on the
44 * card. All in absolute coordinates, as lv_obj_get_coords reports. */
45 lv_area_t r, pr;
46 lv_obj_get_coords(s_amount_row, &r);
47 lv_coord_t stop = r.x2;
48 if (s_asset_btn != NULL) {
49 lv_obj_get_coords(s_asset_btn, &pr);
50 stop = pr.x1 - AMOUNT_ROW_GAP;
51 }
52 lv_coord_t x = (r.x2 > stop) ? (stop - r.x2) : 0;
53
54 /* ...but never off the left edge: the leading digits decide what the
55 * customer is charged. Overlapping the pill is survivable; silently losing
56 * the left-hand digits is not, so the clamp wins. Measured against the
57 * row's parent (the inset card), not the screen. */
58 lv_area_t cr;
59 lv_obj_get_coords(lv_obj_get_parent(s_amount_row), &cr);
60 if ((r.x1 + x) < (cr.x1 + AMOUNT_ROW_MIN_X)) {
61 x = (cr.x1 + AMOUNT_ROW_MIN_X) - r.x1;
62 }
63
64 lv_obj_align(s_amount_row, LV_ALIGN_TOP_MID, x,
65 ASSET_BTN_Y + ((ASSET_BTN_H - lv_area_get_height(&r)) / 2)
67}
68
79static void charge_set_enabled(bool on) {
80 if (s_charge_btn == NULL) { return; } /* not the amount screen */
81
82 if (on) { lv_obj_clear_state(s_charge_btn, LV_STATE_DISABLED); }
83 else { lv_obj_add_state(s_charge_btn, LV_STATE_DISABLED); }
84
85 lv_obj_set_style_bg_color(s_charge_btn, on ? COL_ACTION : COL_SURFACE,
86 LV_PART_MAIN);
87 lv_obj_t *lbl = lv_obj_get_child(s_charge_btn, 0);
88 if (lbl != NULL) {
89 lv_obj_set_style_text_color(lbl, on ? COL_BG : COL_DIM, LV_PART_MAIN);
90 }
91}
92
100void charge_set_busy(void) {
101 if (s_charge_btn == NULL) { return; }
102 s_charge_busy = true;
103 /* LVGL's hit test drops taps on a disabled object (lv_obj_pos.c). */
104 lv_obj_add_state(s_charge_btn, LV_STATE_DISABLED);
105}
106
107static void amount_update_display(void) {
108 char buf[24];
109 const bool split = (s_amount_cents >= AMOUNT_SMALL_CENTS_FROM);
110
111 if (s_amount_label != NULL) {
112 if (split) {
113 snprintf(buf, sizeof(buf), "%" PRIu64, s_amount_cents / 100ULL);
114 } else {
115 snprintf(buf, sizeof(buf), "%" PRIu64 ".%02" PRIu64,
116 s_amount_cents / 100ULL, s_amount_cents % 100ULL);
117 }
118 lv_label_set_text(s_amount_label, buf);
119 }
120 if (s_amount_cents_label != NULL) {
121 snprintf(buf, sizeof(buf), ".%02" PRIu64, s_amount_cents % 100ULL);
122 /* "" rather than the hidden flag: an empty label measures zero and the
123 * flex row re-centres on its own. */
124 lv_label_set_text(s_amount_cents_label, split ? buf : "");
125 }
126
127 /* The selector keeps its chevron until there is an amount, then gives the
128 * digits the room back. The ticker stays either way. */
130 /* Left alone while a tap is in flight: the keypad stays live, so a digit
131 * typed in that window would re-arm a button that has already asked. */
132 if (!s_charge_busy) {
134 }
135
136 /* Last: the pill has just changed width and the labels have just changed text,
137 * and the row is placed against both. */
139
140 s_amount_units = amount_cents_to_units(s_amount_cents); /* -> 6-decimal base units */
141}
142
143/* The drawn backspace, in units of half its height — see kbd_backspace_draw_cb().
144 * The tag is 2*BSP_H tall and BSP_W wide with a BSP_NOSE-deep point on the left,
145 * which is the outline the LVGL symbol draws solid. */
146#define BSP_W 16 /* half-length; 32 wide against 18 tall reads as the
147 * conventional backspace proportion (tuned by eye) */
148#define BSP_H 9
149#define BSP_NOSE 6
150#define BSP_CROSS 4
151#define BSP_LINE 2
152/* The corner radius. The right-hand corners are rounded over two segments each
153 * (a right-angle chamfer reads as a flat cut); the nose's 124-degree corners are
154 * chamfered, since a fillet there would eat most of the 6 px nose.
155 *
156 * Lines, not lv_draw_arc: a tiny arc antialiases differently from the straight
157 * runs and top/bottom differently from each other, so the corners look jittery.
158 * Two segments through 45 degrees are within 0.4 px of a true quarter-circle.
159 *
160 * Integers everywhere, so the cuts above and below the centre line mirror
161 * exactly. The nose's (2,3) is one third of its 6:9 slope, near enough BSP_R. */
162#define BSP_R 3
163#define BSP_R45 2 /* round(BSP_R * sin 45) — the fillet's middle point */
164#define BSP_NOSE_DX 2 /* the nose's cut, one third of its run... */
165#define BSP_NOSE_DY 3 /* ...and one third of its rise */
166
167/* Keys tile the pad top to bottom: the default theme's btnmatrix padding and row
168 * gap are dead zones where a touch lands on no key. */
169static void kbd_fill_rows(lv_obj_t *kb) {
170 lv_obj_set_style_pad_ver(kb, 0, LV_PART_MAIN);
171 lv_obj_set_style_pad_row(kb, 0, LV_PART_MAIN);
172}
173
183 */
184static void kbd_backspace_draw_cb(lv_event_t *e) {
185 lv_obj_draw_part_dsc_t *dsc = lv_event_get_draw_part_dsc(e);
186 if ((dsc == NULL) || (dsc->class_p != &lv_btnmatrix_class) ||
187 (dsc->type != LV_BTNMATRIX_DRAW_PART_BTN)) {
188 return;
189 }
190 const char *key = lv_btnmatrix_get_btn_text(lv_event_get_target(e), dsc->id);
191 if ((key == NULL) || (strcmp(key, LV_SYMBOL_BACKSPACE) != 0)) { return; }
192
193 /* The glyph is still in the map — amount_kbd_cb() matches on that string —
194 * so it is hidden at draw time instead of removed. */
195 if (lv_event_get_code(e) == LV_EVENT_DRAW_PART_BEGIN) {
196 if (dsc->label_dsc != NULL) { dsc->label_dsc->opa = LV_OPA_TRANSP; }
197 return;
198 }
199
200 const lv_coord_t cx = (dsc->draw_area->x1 + dsc->draw_area->x2) / 2;
201 const lv_coord_t cy = (dsc->draw_area->y1 + dsc->draw_area->y2) / 2;
202
203 lv_draw_line_dsc_t ld;
204 lv_draw_line_dsc_init(&ld);
205 ld.color = COL_TEXT; /* the digits' own ink, as asked */
206 ld.width = BSP_LINE;
207 ld.round_start = 1;
208 ld.round_end = 1;
209
210 /* Every coordinate named once, as lv_coord_t: int arithmetic inside the
211 * braced point list would be a narrowing conversion. */
212 const lv_coord_t xr = (lv_coord_t)(cx + BSP_W); /* right edge */
213 const lv_coord_t xt = (lv_coord_t)(cx - BSP_W); /* the point */
214 const lv_coord_t nx = (lv_coord_t)(cx - BSP_W + BSP_NOSE); /* nose's base*/
215 const lv_coord_t xn = (lv_coord_t)(nx - BSP_NOSE_DX); /* its cut */
216 const lv_coord_t l = (lv_coord_t)(nx + BSP_R); /* straight runs start/end */
217 const lv_coord_t r = (lv_coord_t)(xr - BSP_R);
218 const lv_coord_t t = (lv_coord_t)(cy - BSP_H);
219 const lv_coord_t b = (lv_coord_t)(cy + BSP_H);
220 const lv_coord_t tr = (lv_coord_t)(t + BSP_R); /* where the arcs take over */
221 const lv_coord_t br = (lv_coord_t)(b - BSP_R);
222 const lv_coord_t tn = (lv_coord_t)(t + BSP_NOSE_DY);
223 const lv_coord_t bn = (lv_coord_t)(b - BSP_NOSE_DY);
224 /* The 45-degree point of each right-hand fillet: its centre is (r, tr) at the
225 * top and (r, br) at the bottom. Written from t and b so the two mirror by
226 * construction. */
227 const lv_coord_t xd = (lv_coord_t)(r + BSP_R45);
228 const lv_coord_t td = (lv_coord_t)(t + BSP_R - BSP_R45);
229 const lv_coord_t bd = (lv_coord_t)(b - BSP_R + BSP_R45);
230
231 /* The whole outline in one primitive, clockwise from the top: straight run,
232 * fillet, right edge, fillet, straight run, then the nose. */
233 const lv_point_t seg[11][2] = {
234 { { l, t }, { r, t } },
235 { { r, t }, { xd, td } },
236 { { xd, td }, { xr, tr } },
237 { { xr, tr }, { xr, br } },
238 { { xr, br }, { xd, bd } },
239 { { xd, bd }, { r, b } },
240 { { r, b }, { l, b } },
241 { { l, b }, { xn, bn } },
242 { { xn, bn }, { xt, cy } },
243 { { xt, cy }, { xn, tn } },
244 { { xn, tn }, { l, t } },
245 };
246 for (int i = 0; i < 11; i++) {
247 lv_draw_line(dsc->draw_ctx, &ld, &seg[i][0], &seg[i][1]);
248 }
249
250 /* The cross, centred in the square end: cx + BSP_NOSE/2 whatever BSP_W is. */
251 const lv_coord_t kx = cx + (BSP_NOSE / 2);
252 const lv_point_t a1 = { (lv_coord_t)(kx - BSP_CROSS), (lv_coord_t)(cy - BSP_CROSS) };
253 const lv_point_t a2 = { (lv_coord_t)(kx + BSP_CROSS), (lv_coord_t)(cy + BSP_CROSS) };
254 const lv_point_t b1 = { (lv_coord_t)(kx + BSP_CROSS), (lv_coord_t)(cy - BSP_CROSS) };
255 const lv_point_t b2 = { (lv_coord_t)(kx - BSP_CROSS), (lv_coord_t)(cy + BSP_CROSS) };
256 lv_draw_line(dsc->draw_ctx, &ld, &a1, &a2);
257 lv_draw_line(dsc->draw_ctx, &ld, &b1, &b2);
258}
259
260/* Keypad on the amount screen — cents entry: each digit shifts in
261 * from the right (1 -> 0.01, 12 -> 0.12, 1250 -> 12.50). No decimal point. */
262static void amount_kbd_cb(lv_event_t *e) {
263 lv_obj_t *bm = lv_event_get_target(e);
264 const char *txt = lv_btnmatrix_get_btn_text(bm, lv_btnmatrix_get_selected_btn(bm));
265 if (txt == NULL) { return; }
266
267 const uint64_t cap = amount_cents_max();
268 if (strcmp(txt, LV_SYMBOL_BACKSPACE) == 0) {
270 } else if (strcmp(txt, "00") == 0) {
272 } else if ((txt[0] >= '0') && (txt[0] <= '9') && (txt[1] == '\0')) {
274 static_cast<unsigned>(txt[0] - '0'), cap);
275 }
277}
278
286 */
287void code_field_reveal(lv_obj_t *ta, lv_obj_t *eye_lbl) {
288 if (ta == NULL) { return; }
289 const bool masked = lv_textarea_get_password_mode(ta);
290 lv_textarea_set_password_mode(ta, !masked);
291 if (eye_lbl != NULL) {
292 lv_label_set_text(eye_lbl, masked ? LV_SYMBOL_EYE_CLOSE
293 : LV_SYMBOL_EYE_OPEN);
294 }
296void build_splash(void) {
297 clear_screen();
298 /* The logo is black-on-white; put the whole splash on white so it blends. */
299 lv_obj_set_style_bg_color(lv_scr_act(), lv_color_white(), LV_PART_MAIN);
300
301 lv_obj_t *logo = lv_img_create(lv_scr_act());
302 lv_img_set_src(logo, &logo_img);
303 lv_obj_align(logo, LV_ALIGN_CENTER, LOGO_X_NUDGE, -36);
304 /* No pop_in() here: at power-on the backlight has only just come up, so a
305 * scale-in reads as the logo blinking. Kept for the tx check/cross. */
306
307 /* Tight under the logo (the image carries its own breathing margin). */
308 make_label(lv_scr_act(), "cryptnox-pos", lv_color_black(),
309 &font_pjs_20_medium, LV_ALIGN_CENTER, 0, 40);
310
311 /* Boot feedback — the splash stays up while Wi-Fi/SNTP/RPC come up, so
312 * show a discreet spinner instead of looking frozen. */
313 lv_obj_t *sp = lv_spinner_create(lv_scr_act(), 1000, 60);
314 lv_obj_set_size(sp, 28, 28);
315 lv_obj_align(sp, LV_ALIGN_BOTTOM_MID, 0, -48);
316 lv_obj_set_style_arc_width(sp, 3, LV_PART_MAIN);
317 lv_obj_set_style_arc_width(sp, 3, LV_PART_INDICATOR);
318 lv_obj_set_style_arc_color(sp, COL_SURFACE, LV_PART_MAIN);
319 lv_obj_set_style_arc_color(sp, COL_ACCENT, LV_PART_INDICATOR);
320
321 /* Which boot step is running, so a slow start is legible rather than
322 * looking frozen. Discreet, and elided by width to never overflow. */
325 LV_ALIGN_BOTTOM_MID, 0, -20);
326 lv_obj_set_width(s_boot_step_lbl, SCR_W - 24);
327 lv_label_set_long_mode(s_boot_step_lbl, LV_LABEL_LONG_DOT);
328 lv_obj_set_style_text_align(s_boot_step_lbl, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
329 lv_obj_align(s_boot_step_lbl, LV_ALIGN_BOTTOM_MID, 0, -20);
331void build_amount(void) {
332 /* Step one of four. The admin panel is behind the swipe up from the home
333 * indicator, so the only chrome on the teal is the step dashes and test chip. */
334 lv_obj_t *card = build_page(PAY_STEP_AMOUNT);
336
337 /* One row: the amount with the asset selector at its right-hand end, so the
338 * figure and currency read together (placed by amount_row_place()). The
339 * card's 262px is fully spent: row 10..52, keypad 58..206, Charge 210..254. */
340 make_asset_button(card);
341
342 /* Figure and small cents in one content-sized flex row, so the group centres
343 * itself whatever the fonts measure. Bottom-aligned, which puts the cents on
344 * the big font's baseline near enough (LVGL 8 has no baseline align). */
345 lv_obj_t *row = lv_obj_create(card);
346 lv_obj_remove_style_all(row);
347 lv_obj_set_size(row, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
348 lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
349 lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
350 lv_obj_set_flex_align(row, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_END,
351 LV_FLEX_ALIGN_CENTER);
352 s_amount_row = row; /* placed by amount_update_display below */
353
354 s_amount_label = make_label(row, "0.00", COL_TEXT,
355 &font_pjs_28_semibold, LV_ALIGN_DEFAULT, 0, 0);
357 &font_pjs_20_semibold, LV_ALIGN_DEFAULT, 0, 0);
358
359 /* Numeric keypad: digits, double-zero, backspace (cents entry). */
360 static const char *amap[] = {
361 "1", "2", "3", "\n",
362 "4", "5", "6", "\n",
363 "7", "8", "9", "\n",
364 "00", "0", LV_SYMBOL_BACKSPACE, ""
365 };
366 lv_obj_t *kb = lv_btnmatrix_create(card);
367 lv_btnmatrix_set_map(kb, amap);
368 lv_obj_set_size(kb, CARD_W - (2 * CARD_PAD), 148);
369 lv_obj_align(kb, LV_ALIGN_TOP_MID, 0, 58);
370 lv_obj_set_style_bg_opa(kb, LV_OPA_TRANSP, LV_PART_MAIN);
371 lv_obj_set_style_border_width(kb, 0, LV_PART_MAIN);
372 /* Same columns as the PIN and admin pads (see make_numeric_keypad), so the
373 * digits stand in one place on every keypad screen. */
374 lv_obj_set_style_pad_hor(kb, 0, LV_PART_MAIN);
375 kbd_fill_rows(kb);
376 /* Minimal keypad: no key boxes — black glyphs on white, grey flash on press. */
377 lv_obj_set_style_bg_opa(kb, LV_OPA_TRANSP, LV_PART_ITEMS);
378 lv_obj_set_style_bg_color(kb, COL_SURFACE, LV_PART_ITEMS | LV_STATE_PRESSED);
379 lv_obj_set_style_bg_opa(kb, LV_OPA_COVER, LV_PART_ITEMS | LV_STATE_PRESSED);
380 lv_obj_set_style_border_width(kb, 0, LV_PART_ITEMS);
381 lv_obj_set_style_shadow_width(kb, 0, LV_PART_ITEMS);
382 lv_obj_set_style_text_color(kb, COL_TEXT, LV_PART_ITEMS);
383 lv_obj_set_style_text_font(kb, &font_pjs_28_light, LV_PART_ITEMS);
384 lv_obj_set_style_radius(kb, 8, LV_PART_ITEMS);
385 lv_obj_add_event_cb(kb, amount_kbd_cb, LV_EVENT_VALUE_CHANGED, NULL);
386 /* Redraws the backspace as an outline — see kbd_backspace_draw_cb(). Both
387 * halves: BEGIN hides the solid glyph, END draws over the key. */
388 lv_obj_add_event_cb(kb, kbd_backspace_draw_cb, LV_EVENT_DRAW_PART_BEGIN, NULL);
389 lv_obj_add_event_cb(kb, kbd_backspace_draw_cb, LV_EVENT_DRAW_PART_END, NULL);
390
391 s_charge_btn = make_button(card, "Charge", COL_ACTION, COL_BG,
393 LV_ALIGN_BOTTOM_MID, 0, CARD_BTN_Y, ACT_CONFIRM,
395
396 /* Also settles the Charge button: the screen is rebuilt on an asset change
397 * with whatever was typed still standing, so it must not come back lit on an
398 * empty figure or dead on a full one. */
399 amount_update_display(); /* keep s_amount_units in sync with the string */
401void build_confirm(void) {
402 /* Step two of four. */
403 lv_obj_t *card = build_page(PAY_STEP_REVIEW);
404
405 /* Ledger-style transaction review: dim caption / value rows, everything
406 * the operator should verify — amount, beneficiary AND the USDC contract
407 * the terminal is about to call. All coordinates are the card's. */
408 const lv_coord_t VW = CARD_W - (2 * CARD_PAD);
409 char buf[24];
410 amount_format(s_confirm_amount, buf, sizeof(buf));
411
412 /* The vertical budget is fully spent: Total to 58, the two address rows to
413 * 180 (both always wrap to two lines), the test warning to 203, buttons from
414 * 208. Move anything down and the warning runs under the buttons. */
415 make_label(card, "Total", COL_DIM, &font_inter_14,
416 LV_ALIGN_TOP_LEFT, CARD_PAD, 8);
417
418 /* Opposite "Total", costing no height: on testnet, which deployment, in red.
419 * Mainnet stays silent (as settings_net_str does), which is what makes the
420 * testnet form stand out on the last screen before the card is tapped. */
421 const bool mainnet = settings_get_mainnet();
422 if (!mainnet) {
423 make_label(card, pos_net_info(asset()->net)->sub_test, COL_DANGER,
424 &font_inter_14, LV_ALIGN_TOP_RIGHT, -CARD_PAD, 8);
425 }
426
427 lv_obj_t *amt = make_label(card, buf, COL_TEXT, &font_pjs_28_semibold,
428 LV_ALIGN_TOP_LEFT, CARD_PAD, 24);
429 lv_obj_t *cusdc = make_asset_badge(card, settings_get_chain());
430 lv_obj_align_to(cusdc, amt, LV_ALIGN_OUT_RIGHT_MID, 6, 0);
431 /* Ticker beside the mark: the mark alone only names the asset to someone
432 * who knows the logos. Gaps of 6 and 4 because "9999.99", the badge and a
433 * four-letter ticker reach x=212 against the card's 214. */
434 lv_obj_t *tick = make_label(card, asset_name(), COL_TEXT,
435 &font_inter_14_medium, LV_ALIGN_DEFAULT, 0, 0);
436 lv_obj_align_to(tick, cusdc, LV_ALIGN_OUT_RIGHT_MID, 4, 0);
437
438 make_label(card, "To", COL_DIM, &font_inter_14,
439 LV_ALIGN_TOP_LEFT, CARD_PAD, 64);
440 /* The most the card can be charged in network fees on top of the total —
441 * the customer is agreeing to that too. Opposite "To" on its caption row, the
442 * way the test-network name sits opposite "Total": the card has no height
443 * left to give it a row of its own. ~150px at its longest against 204. */
444 if (s_confirm_fee[0] != '\0') {
446 LV_ALIGN_TOP_RIGHT, -CARD_PAD, 64);
447 }
448 lv_obj_t *addr = make_label(card,
451 LV_ALIGN_TOP_LEFT, CARD_PAD, 82);
452 lv_label_set_long_mode(addr, LV_LABEL_LONG_WRAP);
453 lv_obj_set_width(addr, VW);
454
456 COL_DIM, &font_inter_14, LV_ALIGN_TOP_LEFT, CARD_PAD, 126);
457 lv_obj_t *ctr = make_label(card,
458 (s_addr_usdc != NULL) ? s_addr_usdc : "-",
460 LV_ALIGN_TOP_LEFT, CARD_PAD, 144);
461 lv_label_set_long_mode(ctr, LV_LABEL_LONG_WRAP);
462 lv_obj_set_width(ctr, VW);
463
464 /* The warning in words, aligned under the contract row (which wraps to one
465 * or two lines). Deliberately NOT given a width: a wrapped second line would
466 * run under the buttons, and the sentence is ~190px against the card's 204. */
467 if (!mainnet) {
468 lv_obj_t *tn = make_label(card, "Test network - no real funds",
470 LV_ALIGN_DEFAULT, 0, 0);
471 lv_obj_update_layout(ctr);
472 lv_obj_align_to(tn, ctr, LV_ALIGN_OUT_BOTTOM_LEFT, 0, 6);
473 }
474
475 /* Cancel is a ghost button: available, without competing with the button
476 * that commits. */
477 const lv_coord_t half = (CARD_W - (2 * CARD_PAD) - 8) / 2;
478 make_ghost_button(card, "Cancel", half,
479 LV_ALIGN_BOTTOM_LEFT, CARD_PAD, CARD_BTN_Y, ACT_CANCEL);
480 make_button(card, "Confirm", COL_ACTION, COL_BG, half, CARD_BTN_H,
481 LV_ALIGN_BOTTOM_RIGHT, -CARD_PAD, CARD_BTN_Y, ACT_SEND,
483}
484
485/* OK pressed on the keypad: stash the PIN for main and move to the tx screen. */
486static void pin_submit(void) {
487 if (s_pin_ta == NULL) { return; }
488 const char *p = lv_textarea_get_text(s_pin_ta);
489 size_t len = (p != NULL) ? strlen(p) : 0U;
490 if (len < 4U) { return; } /* require at least 4 digits */
491
492 strncpy(s_pin, p, sizeof(s_pin) - 1);
493 s_pin[sizeof(s_pin) - 1] = '\0';
494 s_pin_len = static_cast<uint8_t>(strlen(s_pin));
495
496 /* Same keypad, two errands. A card read is not a payment: it gets the card
497 * screen and its own event, so main cannot mistake it for a sale and the tx
498 * screen's "Declined" wording never appears over a setup step. */
499 if (s_pin_for_card) {
500 s_pin_for_card = false;
501 strncpy(s_card_note, "Reading your payout addresses",
502 sizeof(s_card_note) - 1);
503 s_card_note[sizeof(s_card_note) - 1] = '\0';
505 if (s_cb != NULL) { s_cb(UI_EVENT_CARD_PIN, 0); }
506 return;
507 }
508
510 strncpy(s_tx_info, "Preparing...", sizeof(s_tx_info) - 1);
511 s_tx_info[sizeof(s_tx_info) - 1] = '\0';
513 if (s_cb != NULL) { s_cb(UI_EVENT_PIN_ENTERED, 0); }
514}
516static void pin_kbd_cb(lv_event_t *e) {
517 lv_obj_t *bm = lv_event_get_target(e);
518 uint32_t id = lv_btnmatrix_get_selected_btn(bm);
519 const char *txt = lv_btnmatrix_get_btn_text(bm, id);
520 if ((txt == NULL) || (s_pin_ta == NULL)) { return; }
521
522 if (strcmp(txt, LV_SYMBOL_OK) == 0) {
523 pin_submit();
524 } else if (strcmp(txt, LV_SYMBOL_BACKSPACE) == 0) {
525 lv_textarea_del_char(s_pin_ta);
526 } else if ((txt[0] >= '0') && (txt[0] <= '9') && (txt[1] == '\0')) {
527 lv_textarea_add_char(s_pin_ta, static_cast<uint32_t>(txt[0]));
528 }
529}
530/* Show the hint while the field is empty, hide it once anything is typed.
531 * Driven by the textarea's VALUE_CHANGED, which LVGL sends from every mutating
532 * call, so backspacing to empty or a reset brings it back without the caller. */
533static void code_hint_cb(lv_event_t *e) {
534 lv_obj_t *ta = lv_event_get_target(e);
535 lv_obj_t *hint = static_cast<lv_obj_t *>(lv_event_get_user_data(e));
536 const char *txt = lv_textarea_get_text(ta);
537 if (hint == NULL) { return; }
538 if ((txt != NULL) && (txt[0] != '\0')) {
539 lv_obj_add_flag(hint, LV_OBJ_FLAG_HIDDEN);
540 } else {
541 lv_obj_clear_flag(hint, LV_OBJ_FLAG_HIDDEN);
542 }
543}
545static lv_obj_t *make_code_field(uint32_t max_len, lv_coord_t x1, lv_coord_t y,
546 const char *hint, lv_coord_t w,
547 lv_obj_t *parent = NULL) {
548 lv_obj_t *host = (parent != NULL) ? parent : lv_scr_act();
549 lv_obj_t *ta = lv_textarea_create(host);
550 lv_textarea_set_password_mode(ta, true);
551 /* Echo each digit briefly so the operator can confirm the keypress, then
552 * mask — a third of LVGL's 1500 ms default, which leaks the whole code. */
553 lv_textarea_set_password_show_time(ta, 500);
554 lv_textarea_set_one_line(ta, true);
555 lv_textarea_set_max_length(ta, max_len);
556 lv_textarea_set_text(ta, "");
557 lv_obj_clear_flag(ta, LV_OBJ_FLAG_CLICKABLE);
558 lv_obj_set_width(ta, w);
559 /* Trimmed off the theme's field padding, which made a one-line box as tall
560 * as two keypad rows. Fill, radius and text colour come from s_st_field. */
561 lv_obj_set_style_pad_ver(ta, FIELD_PAD_V, LV_PART_MAIN);
562 lv_obj_align(ta, LV_ALIGN_TOP_LEFT, x1, y);
563 lv_obj_set_style_text_align(ta, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
564
565 /* What to type, in the box it is typed into. A label of our own, NOT
566 * lv_textarea's placeholder: draw_placeholder() sets LV_TEXT_FLAG_EXPAND on
567 * one-line fields, so it cannot be centred. Aligned to the field itself. */
568 if (hint != NULL) {
569 lv_obj_t *l = make_label(host, hint, COL_DIM,
570 &font_inter_14, LV_ALIGN_DEFAULT, 0, 0);
571 lv_obj_update_layout(ta);
572 lv_obj_align_to(l, ta, LV_ALIGN_CENTER, 0, 0);
573 lv_obj_add_event_cb(ta, code_hint_cb, LV_EVENT_VALUE_CHANGED, l);
574 }
575 return ta;
577lv_obj_t *make_numeric_keypad(lv_event_cb_t cb, lv_obj_t *parent,
578 lv_coord_t w,
579 lv_coord_t h) {
580 static const char *kbd_map[] = {
581 "1", "2", "3", "\n",
582 "4", "5", "6", "\n",
583 "7", "8", "9", "\n",
584 LV_SYMBOL_BACKSPACE, "0", LV_SYMBOL_OK, ""
585 };
586 lv_obj_t *kb = lv_btnmatrix_create((parent != NULL) ? parent : lv_scr_act());
587 lv_btnmatrix_set_map(kb, kbd_map);
588 lv_obj_set_size(kb, w, h);
589 lv_obj_align(kb, LV_ALIGN_BOTTOM_MID, 0, -6);
590 lv_obj_set_style_bg_opa(kb, LV_OPA_TRANSP, LV_PART_MAIN);
591 lv_obj_set_style_border_width(kb, 0, LV_PART_MAIN);
592 /* No side inset: the outer keys' press slabs run to the keypad's edges, the
593 * code field's left edge and the eye's right edge. */
594 lv_obj_set_style_pad_hor(kb, 0, LV_PART_MAIN);
595 kbd_fill_rows(kb);
596 lv_obj_set_style_bg_opa(kb, LV_OPA_TRANSP, LV_PART_ITEMS);
597 lv_obj_set_style_bg_color(kb, COL_SURFACE, LV_PART_ITEMS | LV_STATE_PRESSED);
598 lv_obj_set_style_bg_opa(kb, LV_OPA_COVER, LV_PART_ITEMS | LV_STATE_PRESSED);
599 lv_obj_set_style_border_width(kb, 0, LV_PART_ITEMS);
600 lv_obj_set_style_shadow_width(kb, 0, LV_PART_ITEMS);
601 lv_obj_set_style_text_color(kb, COL_TEXT, LV_PART_ITEMS);
602 lv_obj_set_style_text_font(kb, &font_pjs_28_light, LV_PART_ITEMS);
603 lv_obj_set_style_radius(kb, 8, LV_PART_ITEMS);
604 lv_obj_add_event_cb(kb, cb, LV_EVENT_VALUE_CHANGED, NULL);
605 /* The amount screen's outline backspace, on the PIN and admin pads too. */
606 lv_obj_add_event_cb(kb, kbd_backspace_draw_cb, LV_EVENT_DRAW_PART_BEGIN, NULL);
607 lv_obj_add_event_cb(kb, kbd_backspace_draw_cb, LV_EVENT_DRAW_PART_END, NULL);
608 return kb;
609}
610
611/* The field and its reveal eye, laid on the keypad's columns below them: the
612 * field spans the 1 and 2 keys, the eye is the 3 key's column with its glyph
613 * centred over the 3/6/9 glyphs. Read off the keypad's own button areas rather
614 * than recomputed, so the row cannot drift from the keys whatever gap the theme
615 * puts between them. Build the keypad first. */
616lv_obj_t *make_code_row(lv_obj_t *host, lv_obj_t *kb, uint32_t max_len,
617 const char *hint, BtnAction eye_act,
618 lv_obj_t **eye_lbl) {
619 lv_obj_update_layout(kb);
620 const lv_area_t *k = reinterpret_cast<lv_btnmatrix_t *>(kb)->button_areas;
621 const lv_coord_t kx = lv_obj_get_x(kb); /* k[] is relative to the keypad */
622 /* The left edge comes in from the 1 key's slab (the eye reads the pad's edge
623 * off the digits); the right runs @c grow past the 2 key's slab into the eye
624 * button's empty side. Tune both by eye. */
625 const lv_coord_t inset = 8;
626 const lv_coord_t grow = 12;
627 const lv_coord_t w = ((k[1].x2 - k[0].x1) + 1) - inset + grow;
628 /* No reveal (see build_pin()): nothing in the 3 key's column, so the field
629 * centres over the pad rather than leaving that column empty beside it. */
630 const lv_coord_t x1 = (eye_lbl == NULL)
631 ? kx + ((lv_obj_get_width(kb) - w) / 2)
632 : kx + k[0].x1 + inset;
633 lv_obj_t *ta = make_code_field(max_len, x1, 44, hint, w, host);
634 if (eye_lbl == NULL) { return ta; }
635 /* make_icon_button() places itself top-left for the burger; move it, and
636 * keep the label handle so the glyph can be swapped in place. */
637 lv_obj_t *eye = make_icon_button(LV_SYMBOL_EYE_OPEN, eye_act, host);
638 lv_obj_set_width(eye, (k[2].x2 - k[2].x1) + 1);
639 lv_obj_align_to(eye, ta, LV_ALIGN_OUT_RIGHT_MID,
640 (k[2].x1 - k[1].x2) - 1 - grow, 0); /* still on the 3 key */
641 *eye_lbl = lv_obj_get_child(eye, 0);
642 return ta;
644void build_pin(void) {
645 /* Step three of four — authorising the card. A card read during first-run
646 * setup borrows this screen too, and that is not a sale, so it gets the
647 * chrome without a lit dash. */
649 !s_pin_for_card); /* setup read: no band */
650 /* Wipe any stale PIN from a previous attempt. */
651 CW_Utils::secure_wipe(reinterpret_cast<uint8_t *>(s_pin), sizeof(s_pin));
652 s_pin_len = 0;
653
654 (void)make_title(s_pin_for_card ? "Card PIN" : "Enter PIN", true, card);
655 /* Back (cancel) icon, top-left of the card. */
656 (void)make_icon_button(LV_SYMBOL_LEFT, ACT_PIN_CANCEL, card);
657
658 /* The reveal eye exists because a PIN typed blind on a resistive panel and
659 * refused costs a card attempt. Only on the setup read, though: during a sale
660 * this screen faces the customer and the PIN is theirs, so there is no eye
661 * and no per-digit echo for a bystander to read. */
662 lv_obj_t *kb = make_numeric_keypad(pin_kbd_cb, card, CODE_KBD_W, 170);
663 s_pin_ta = make_code_row(card, kb, 9U, "Card PIN", ACT_PIN_REVEAL,
664 s_pin_for_card ? &s_pin_eye_lbl : NULL);
665 if (!s_pin_for_card) { lv_textarea_set_password_show_time(s_pin_ta, 0); }
666}
667
668/* Animate an object's zoom (256 = 100%) — used for the success-check pop. */
669static void zoom_anim_cb(void *obj, int32_t v) {
670 lv_obj_set_style_transform_zoom(static_cast<lv_obj_t *>(obj), v, LV_PART_MAIN);
671}
672
673/* Pop-in: scale from small to full with a slight overshoot/bounce. */
674void pop_in(lv_obj_t *obj) {
675 lv_obj_update_layout(obj);
676 lv_obj_set_style_transform_pivot_x(obj, lv_obj_get_width(obj) / 2, LV_PART_MAIN);
677 lv_obj_set_style_transform_pivot_y(obj, lv_obj_get_height(obj) / 2, LV_PART_MAIN);
678 /* Apply the start scale before the first render: lv_anim_start() only calls
679 * the exec cb on its first tick, so the object would flash at full size. */
680 zoom_anim_cb(obj, 10);
681 lv_anim_t a;
682 lv_anim_init(&a);
683 lv_anim_set_var(&a, obj);
684 lv_anim_set_exec_cb(&a, zoom_anim_cb);
685 lv_anim_set_values(&a, 10, 256); /* ~4% -> 100% (pronounced pop) */
686 lv_anim_set_time(&a, 420);
687 lv_anim_set_path_cb(&a, lv_anim_path_overshoot);
688 lv_anim_start(&a);
689}
690
691/* "0x1234...abcd" — head and tail of a transaction hash. 66 characters do not
692 * fit on a 240px panel at a readable size, and the ends are what somebody
693 * matches against a block explorer. Tron hashes carry no 0x, which is why this
694 * takes six characters from the front rather than skipping a prefix. */
695static void hash_short(const char *h, char *out, size_t n) {
696 size_t len = strlen(h);
697 if ((len <= 14U) || (n < 16U)) {
698 snprintf(out, n, "%s", h);
699 return;
700 }
701 snprintf(out, n, "%.6s...%s", h, h + len - 4U);
702}
703
704/* "<amount> [coin mark] <TICKER>" centred at offset y from the top.
705 *
706 * @p ticker names the asset beside its mark, the way the amount and confirm
707 * screens do. It is on for the tap screen and the "Approved" receipt: the mark
708 * alone only says which asset to somebody who already knows the logos.
709 *
710 * A content-sized flex row, so the group centres itself whatever its widths.
711 * @p y is where the figure's line box starts; the taller badge lifts the row by
712 * half the difference. */
713static void tx_amount_row(lv_obj_t *parent, const char *amt,
714 const lv_font_t *font, lv_coord_t y, bool ticker) {
715 lv_obj_t *row = lv_obj_create(parent);
716 lv_obj_remove_style_all(row);
717 lv_obj_set_size(row, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
718 lv_obj_clear_flag(row, LV_OBJ_FLAG_SCROLLABLE);
719 lv_obj_set_flex_flow(row, LV_FLEX_FLOW_ROW);
720 lv_obj_set_flex_align(row, LV_FLEX_ALIGN_CENTER, LV_FLEX_ALIGN_CENTER,
721 LV_FLEX_ALIGN_CENTER);
722 lv_obj_set_style_pad_column(row, 6, LV_PART_MAIN);
723
724 lv_obj_t *al = make_label(row, amt, COL_TEXT, font, LV_ALIGN_DEFAULT, 0, 0);
726 if (ticker) {
728 LV_ALIGN_DEFAULT, 0, 0);
729 }
730
731 lv_obj_update_layout(row);
732 lv_obj_align(row, LV_ALIGN_TOP_MID, 0,
733 y - ((lv_obj_get_height(row) - lv_obj_get_height(al)) / 2));
735void build_tx_status(void) {
736 /* The last step of the sale: "Total", the figure, the prompt, the
737 * contactless mark, one way out. */
738 lv_obj_t *card = build_page(PAY_STEP_TAP);
739 const lv_coord_t VW = CARD_W - (2 * CARD_PAD);
740
741 char amt[24];
742 amount_format(s_confirm_amount, amt, sizeof(amt));
743
745 /* Total at 8 and the figure at 38: the badge is taller than the 28px
746 * line it is centred on and needs the clearance from "Total". */
747 make_label(card, "Total", COL_DIM, &font_inter_14,
748 LV_ALIGN_TOP_MID, 0, 8);
749 tx_amount_row(card, amt, &font_pjs_28_semibold, 38, true);
750
751 make_label(card, "Tap your card", COL_DIM, &font_pjs_20_medium,
752 LV_ALIGN_TOP_MID, 0, 82);
753
754 /* Centred in the band between the prompt's 20px line (ends ~104) and
755 * the Cancel button (starts 208). */
756 make_tap_mark(card, 106 + ((102 - TAP_MARK_H) / 2));
757
758 (void)make_ghost_button(card, "Cancel", CARD_BTN_W,
759 LV_ALIGN_BOTTOM_MID, 0, CARD_BTN_Y, ACT_CANCEL);
760 return;
761 }
762
764 lv_obj_t *chk = make_label(card, LV_SYMBOL_OK, COL_SUCCESS,
765 &font_icons_48, LV_ALIGN_TOP_MID, 0, 30);
766 pop_in(chk);
767 make_label(card, "Approved", COL_TEXT, &font_pjs_20_medium,
768 LV_ALIGN_TOP_MID, 0, 92);
769 tx_amount_row(card, amt, &font_pjs_28_semibold, 122, true);
770
771 /* The hash main hands over with DONE, so the merchant can look the
772 * settled sale up. */
773 if (s_tx_info[0] != '\0') {
774 char shrt[24];
775 hash_short(s_tx_info, shrt, sizeof(shrt));
776 make_label(card, shrt, COL_DIM, &font_inter_14,
777 LV_ALIGN_TOP_MID, 0, 168);
778 }
779
781 LV_ALIGN_BOTTOM_MID, 0, CARD_BTN_Y, ACT_NEW,
783 return;
784 }
785
787 /* The sale the terminal cannot call either way: signed and possibly on
788 * the chain, with no verdict in 120 s. Amber and a warning mark, not the
789 * red cross — "Declined" is what made a merchant take the money twice.
790 * Recheck polls the same hash; Clear is the operator saying they have
791 * looked, and is deliberately the quieter of the two. */
792 lv_obj_t *warn = make_label(card, LV_SYMBOL_WARNING, COL_WARN,
793 &font_icons_48, LV_ALIGN_TOP_MID, 0, 26);
794 pop_in(warn);
795 make_label(card, "Not confirmed yet", COL_TEXT,
796 &font_pjs_20_medium, LV_ALIGN_TOP_MID, 0, 86);
797 char shrt[24];
798 hash_short(s_tx_info, shrt, sizeof(shrt));
800 LV_ALIGN_TOP_MID, 0, 114);
801 lv_obj_t *note = make_label(card,
802 "It may still arrive. Check the explorer "
803 "before charging again.",
804 COL_DIM, &font_inter_14, LV_ALIGN_TOP_MID, 0, 138);
805 lv_label_set_long_mode(note, LV_LABEL_LONG_WRAP);
806 lv_obj_set_width(note, VW);
807 lv_obj_set_style_text_align(note, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
808 lv_obj_align(note, LV_ALIGN_TOP_MID, 0, 138);
809
810 const lv_coord_t half = (CARD_W - (2 * CARD_PAD) - 8) / 2;
811 make_ghost_button(card, "Clear", half,
812 LV_ALIGN_BOTTOM_LEFT, CARD_PAD, CARD_BTN_Y, ACT_NEW);
813 make_button(card, "Recheck", COL_ACTION, COL_BG, half, CARD_BTN_H,
814 LV_ALIGN_BOTTOM_RIGHT, -CARD_PAD, CARD_BTN_Y, ACT_TX_RECHECK,
816 return;
817 }
818
820 lv_obj_t *cross = make_label(card, LV_SYMBOL_CLOSE, lv_color_hex(0xEC5B5B),
821 &font_icons_48, LV_ALIGN_TOP_MID, 0, 30);
822 pop_in(cross);
823 make_label(card, "Declined", COL_TEXT,
824 &font_pjs_20_medium, LV_ALIGN_TOP_MID, 0, 92);
825 lv_obj_t *info = make_label(card, s_tx_info, COL_DIM,
826 &font_inter_14, LV_ALIGN_TOP_MID, 0, 124);
827 lv_label_set_long_mode(info, LV_LABEL_LONG_WRAP);
828 lv_obj_set_width(info, VW);
829 lv_obj_set_style_text_align(info, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
830 lv_obj_align(info, LV_ALIGN_TOP_MID, 0, 124);
831
833 LV_ALIGN_BOTTOM_MID, 0, CARD_BTN_Y, ACT_NEW,
835 return;
836 }
837
838 /* PROCESSING / SIGNING / SENDING — animated spinner + status text. The
839 * spinner keeps turning because lv_timer_handler runs on the UI task while
840 * the main task is busy connecting/signing/broadcasting. */
841 const char *state_str =
842 (s_tx_state == UI_TX_STATE_PROCESSING) ? "Processing" :
843 (s_tx_state == UI_TX_STATE_SIGNING) ? "Signing" :
844 (s_tx_state == UI_TX_STATE_CONFIRMING) ? "Confirming" : "Authorizing";
845
846 lv_obj_t *sp = lv_spinner_create(card, 1000, 60);
847 lv_obj_set_size(sp, 60, 60);
848 lv_obj_align(sp, LV_ALIGN_TOP_MID, 0, 34);
849 lv_obj_set_style_arc_width(sp, 6, LV_PART_MAIN);
850 lv_obj_set_style_arc_width(sp, 6, LV_PART_INDICATOR);
851 lv_obj_set_style_arc_color(sp, COL_SURFACE, LV_PART_MAIN); /* track */
852 lv_obj_set_style_arc_color(sp, COL_ACCENT, LV_PART_INDICATOR); /* moving arc */
853
854 make_label(card, state_str, COL_TEXT, &font_pjs_20_medium,
855 LV_ALIGN_TOP_MID, 0, 112);
856
857 /* Held: "Confirming" can stand for two minutes, and ui_set_tx_info() writes
858 * the countdown straight into this label — rebuilding the screen per tick
859 * would restart the spinner above it. */
861 &font_inter_14, LV_ALIGN_TOP_MID, 0, 146);
862 lv_label_set_long_mode(s_tx_info_lbl, LV_LABEL_LONG_WRAP);
863 lv_obj_set_width(s_tx_info_lbl, VW);
864 lv_obj_set_style_text_align(s_tx_info_lbl, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
865 lv_obj_align(s_tx_info_lbl, LV_ALIGN_TOP_MID, 0, 146);
866}
867
868/* Startup fault. Not the transaction screen: its red cross and "Declined" would
869 * make a wiring problem read as a refused sale. No action button — nothing here is
870 * recoverable from the touchscreen, and main restarts the terminal by itself
871 * after BOOT_FAULT_RESTART_S (30 s), so the body text says that. */
872void build_boot_error(void) {
873 clear_screen();
874 build_header("Startup");
875
876 lv_obj_t *warn = make_label(lv_scr_act(), LV_SYMBOL_WARNING, COL_DANGER,
877 &font_icons_48, LV_ALIGN_TOP_MID, 0, 62);
878 pop_in(warn);
879
880 const char *title;
881 const char *body;
882 switch (s_boot_err) {
884 title = "Wallet not ready";
885 body = "The card reader answered but the Cryptnox wallet could not "
886 "be initialised.\n\n"
887 "The terminal restarts by itself in 30 s. If this keeps "
888 "happening, the reader or its firmware is at fault.";
889 break;
891 title = "Invalid configuration";
892 body = "An address built into this firmware (config.h) does not "
893 "parse.\n\n"
894 "The terminal restarts in 30 s. Install a build with a "
895 "corrected config.h.";
896 break;
897 case UI_BOOT_ERR_NFC:
898 default:
899 title = "NFC reader not found";
900 body = "The PN532 module did not answer on the I2C bus.\n\n"
901 "Check the SDA/SCL wiring, the RST pin and the 3V3 supply. "
902 "The terminal restarts by itself in 30 s.";
903 break;
904 }
905
906 make_label(lv_scr_act(), title, COL_TEXT, &font_pjs_20_medium,
907 LV_ALIGN_TOP_MID, 0, 124);
908
909 lv_obj_t *b = make_label(lv_scr_act(), body, COL_DIM,
910 &font_inter_14, LV_ALIGN_TOP_MID, 0, 158);
911 lv_label_set_long_mode(b, LV_LABEL_LONG_WRAP);
912 lv_obj_set_width(b, 216);
913 lv_obj_set_style_text_align(b, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
914
915 /* Technician detail, kept off the main message so the operator reads the
916 * instruction and not the error code. At the foot, but never above the
917 * body, so long text clips at the bottom rather than overprinting it. */
918 if (s_boot_detail[0] != '\0') {
919 lv_obj_t *d = make_label(lv_scr_act(), s_boot_detail, COL_DIM,
921 LV_ALIGN_BOTTOM_MID, 0, -8);
922 lv_label_set_long_mode(d, LV_LABEL_LONG_WRAP);
923 lv_obj_set_width(d, 216);
924 lv_obj_set_style_text_align(d, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN);
925
926 lv_obj_update_layout(b);
927 lv_obj_update_layout(d);
928 const lv_coord_t body_end = lv_obj_get_y(b) + lv_obj_get_height(b) + 8;
929 if (lv_obj_get_y(d) < body_end) {
930 lv_obj_align(d, LV_ALIGN_TOP_MID, 0, body_end);
931 }
932 }
933}
static const pos_net_info_t * pos_net_info(pos_net_t net)
Names for net, or Ethereum's for a value out of range.
Definition assets.h:190
lv_font_t font_icons_48
lv_font_t font_inter_14
lv_font_t font_inter_14_medium
lv_font_t font_pjs_20_medium
lv_font_t font_pjs_20_semibold
lv_font_t font_pjs_28_light
lv_font_t font_pjs_28_semibold
static uint64_t amount_key_00(uint64_t cents, uint64_t cap)
The "00" key: two zeroes shifted in, clamped to cap.
Definition money.h:65
static uint64_t amount_key_back(uint64_t cents)
The backspace key: the last digit dropped.
Definition money.h:72
static void amount_format(uint64_t units, char *out, size_t n)
"12.50": 6-decimal base units to two places, truncated.
Definition money.h:84
static uint64_t amount_key_digit(uint64_t cents, unsigned digit, uint64_t cap)
A digit shifted in from the right; refused whole if it passes cap.
Definition money.h:58
static uint64_t amount_cents_to_units(uint64_t cents)
Keypad cents to 6-decimal base units.
Definition money.h:78
static ui_event_cb_t s_cb
pos_chain_t settings_get_chain(void)
Selected chain, or POS_CHAIN_ETH_USDC if never set.
Definition settings.cpp:165
bool settings_get_mainnet(void)
true when the terminal is on the production networks.
Definition settings.cpp:189
uint64_t s_amount_cents
Definition ui.cpp:77
const char * asset_caption(pos_chain_t c)
Definition ui.cpp:210
char s_confirm_fee[40]
Definition ui.cpp:42
bool s_charge_busy
Definition ui.cpp:94
lv_obj_t * s_pin_ta
Definition ui.cpp:98
lv_obj_t * s_amount_row
Definition ui.cpp:76
void request_screen(ui_screen_t s)
Definition ui.cpp:241
uint64_t amount_cents_max(void)
Definition ui.cpp:265
ui_tx_state_t s_tx_state
Definition ui.cpp:46
lv_obj_t * s_charge_btn
Definition ui.cpp:86
lv_obj_t * s_amount_cents_label
Definition ui.cpp:73
ui_boot_err_t s_boot_err
Definition ui.cpp:143
char s_boot_step[40]
Definition ui.cpp:119
lv_obj_t * s_tx_info_lbl
Definition ui.cpp:48
char s_pin[16]
Definition ui.cpp:99
lv_obj_t * s_pin_eye_lbl
Definition ui.cpp:108
char s_confirm_addr[64]
Definition ui.cpp:41
uint64_t s_amount_units
Definition ui.cpp:37
uint8_t s_pin_len
Definition ui.cpp:100
lv_obj_t * s_amount_label
Definition ui.cpp:72
bool s_pin_for_card
Definition ui.cpp:164
char s_tx_info[72]
Definition ui.cpp:47
lv_obj_t * s_boot_step_lbl
Definition ui.cpp:120
uint64_t s_confirm_amount
Definition ui.cpp:40
const char * asset_name(pos_chain_t c)
Definition ui.cpp:194
lv_obj_t * s_asset_btn
Definition ui.cpp:81
const pos_asset_t * asset(pos_chain_t c)
Definition ui.cpp:189
char s_card_note[64]
Definition ui.cpp:67
const char * s_addr_usdc
Definition ui.cpp:167
char s_boot_detail[64]
Definition ui.cpp:144
@ UI_SCREEN_TX_STATUS
Definition ui.h:44
@ UI_SCREEN_CARD_WAIT
Definition ui.h:50
@ UI_TX_STATE_SIGNING
Definition ui.h:97
@ UI_TX_STATE_UNCONFIRMED
Definition ui.h:102
@ UI_TX_STATE_CONFIRMING
Definition ui.h:99
@ UI_TX_STATE_PLACE_CARD
Definition ui.h:95
@ UI_TX_STATE_DONE
Definition ui.h:100
@ UI_TX_STATE_FAILED
Definition ui.h:101
@ UI_TX_STATE_PROCESSING
Definition ui.h:96
@ UI_EVENT_PIN_ENTERED
Definition ui.h:59
@ UI_EVENT_CARD_PIN
Definition ui.h:80
@ UI_BOOT_ERR_WALLET
Definition ui.h:115
@ UI_BOOT_ERR_CONFIG
Definition ui.h:116
@ UI_BOOT_ERR_NFC
Definition ui.h:114
Private to the ui*.cpp files: the state and helpers they share.
#define AMOUNT_ROW_GAP
@ PAY_STEP_NONE
@ PAY_STEP_AUTH
@ PAY_STEP_AMOUNT
@ PAY_STEP_TAP
@ PAY_STEP_REVIEW
#define LOGO_X_NUDGE
#define AMOUNT_ROW_MIN_X
lv_obj_t * build_page(int step, bool band=true)
lv_obj_t * make_asset_badge(lv_obj_t *parent, pos_chain_t chain)
#define COL_DANGER
#define CODE_KBD_W
lv_obj_t * make_button(lv_obj_t *parent, const char *label, lv_color_t bg, lv_color_t fg, lv_coord_t w, lv_coord_t h, lv_align_t align, lv_coord_t x, lv_coord_t y, BtnAction act, const lv_font_t *font)
void asset_btn_set_compact(bool compact)
lv_obj_t * make_icon_button(const char *sym, BtnAction act, lv_obj_t *parent=NULL)
#define ASSET_BTN_H
lv_obj_t * make_ghost_button(lv_obj_t *parent, const char *label, lv_coord_t w, lv_align_t align, lv_coord_t x, lv_coord_t y, BtnAction act)
#define COL_WARN
lv_obj_t * make_label(lv_obj_t *parent, const char *txt, lv_color_t color, const lv_font_t *font, lv_align_t align, lv_coord_t x, lv_coord_t y)
#define COL_SURFACE
#define COL_DIM
#define CARD_BTN_Y
void make_tap_mark(lv_obj_t *parent, lv_coord_t y)
#define SCR_W
#define CARD_PAD
#define CARD_BTN_W
#define COL_TEXT
#define COL_ACCENT
#define FIELD_PAD_V
#define TAP_MARK_H
void build_header(const char *title)
#define ASSET_BTN_Y
void clear_screen(void)
#define COL_SUCCESS
BtnAction
@ ACT_PIN_CANCEL
@ ACT_TX_RECHECK
@ ACT_SEND
@ ACT_NEW
@ ACT_PIN_REVEAL
@ ACT_CANCEL
@ ACT_CONFIRM
#define CARD_BTN_H
lv_obj_t * make_title(const char *txt, bool has_icon, lv_obj_t *parent=NULL)
Screen title, centred but never underneath the top-left icon button.
#define COL_BG
void add_test_chip(void)
#define COL_ACTION
#define CARD_W
lv_obj_t * make_asset_button(lv_obj_t *parent)
#define AMOUNT_SMALL_CENTS_FROM
static void amount_update_display(void)
Definition ui_sale.cpp:107
#define BSP_NOSE_DY
Definition ui_sale.cpp:164
void build_pin(void)
Definition ui_sale.cpp:643
#define BSP_H
Definition ui_sale.cpp:147
void pop_in(lv_obj_t *obj)
Definition ui_sale.cpp:673
#define BSP_CROSS
Definition ui_sale.cpp:149
static void kbd_backspace_draw_cb(lv_event_t *e)
Definition ui_sale.cpp:183
lv_obj_t * make_code_row(lv_obj_t *host, lv_obj_t *kb, uint32_t max_len, const char *hint, BtnAction eye_act, lv_obj_t **eye_lbl)
Definition ui_sale.cpp:615
#define BSP_R
Definition ui_sale.cpp:161
static void code_hint_cb(lv_event_t *e)
Definition ui_sale.cpp:532
void build_splash(void)
Definition ui_sale.cpp:295
static void amount_row_place(void)
Definition ui_sale.cpp:35
static void pin_submit(void)
Definition ui_sale.cpp:485
void code_field_reveal(lv_obj_t *ta, lv_obj_t *eye_lbl)
Definition ui_sale.cpp:286
static void tx_amount_row(lv_obj_t *parent, const char *amt, const lv_font_t *font, lv_coord_t y, bool ticker)
Definition ui_sale.cpp:712
void build_tx_status(void)
Definition ui_sale.cpp:734
void build_boot_error(void)
Definition ui_sale.cpp:871
#define BSP_R45
Definition ui_sale.cpp:162
static void amount_kbd_cb(lv_event_t *e)
Definition ui_sale.cpp:261
static void kbd_fill_rows(lv_obj_t *kb)
Definition ui_sale.cpp:168
static lv_coord_t amount_ink_shift(const lv_font_t *f)
Definition ui_sale.cpp:28
#define BSP_W
Definition ui_sale.cpp:146
lv_obj_t * make_numeric_keypad(lv_event_cb_t cb, lv_obj_t *parent, lv_coord_t w, lv_coord_t h)
Definition ui_sale.cpp:576
void charge_set_busy(void)
Definition ui_sale.cpp:100
static lv_obj_t * make_code_field(uint32_t max_len, lv_coord_t x1, lv_coord_t y, const char *hint, lv_coord_t w, lv_obj_t *parent=NULL)
Definition ui_sale.cpp:544
#define BSP_NOSE_DX
Definition ui_sale.cpp:163
#define BSP_LINE
Definition ui_sale.cpp:150
static void charge_set_enabled(bool on)
Definition ui_sale.cpp:79
static void zoom_anim_cb(void *obj, int32_t v)
Definition ui_sale.cpp:668
void build_amount(void)
Definition ui_sale.cpp:330
static void pin_kbd_cb(lv_event_t *e)
Definition ui_sale.cpp:515
#define BSP_NOSE
Definition ui_sale.cpp:148
void build_confirm(void)
Definition ui_sale.cpp:400
static void hash_short(const char *h, char *out, size_t n)
Definition ui_sale.cpp:694