diff --git a/.github/workflows/build_rust_sdk.yml b/.github/workflows/build_rust_sdk.yml index 2eb48df3f..54a4ee6dd 100644 --- a/.github/workflows/build_rust_sdk.yml +++ b/.github/workflows/build_rust_sdk.yml @@ -18,7 +18,6 @@ jobs: uses: actions/checkout@v4 with: path: c_sdk - ref: master - name: Clone Rust SDK uses: actions/checkout@v4 with: diff --git a/.github/workflows/check_clang_static_analyzer.yml b/.github/workflows/check_clang_static_analyzer.yml index 8eea72f42..213541bda 100644 --- a/.github/workflows/check_clang_static_analyzer.yml +++ b/.github/workflows/check_clang_static_analyzer.yml @@ -38,7 +38,7 @@ jobs: make scan-build -j ENABLE_SDK_WERROR=1 DEBUG=${{ matrix.debug }} - name: Upload scan result - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 if: failure() with: name: scan-build diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index 155dc7cd0..5986a0e85 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -52,7 +52,7 @@ jobs: genhtml coverage.info -o coverage - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: code-coverage path: unit-tests/lib_*/coverage diff --git a/Makefile.defines b/Makefile.defines index 9b83c46cd..b66338d2a 100644 --- a/Makefile.defines +++ b/Makefile.defines @@ -20,7 +20,7 @@ __MAKEFILE_DEFINES__ := 1 include $(BOLOS_SDK)/Makefile.target -API_LEVEL := 0 +API_LEVEL := 22 # APPNAME exposed to the app as a CFLAG because it might contain spaces CFLAGS += -DAPPNAME=\"$(APPNAME)\" @@ -50,23 +50,23 @@ endif SYSROOT = $(shell $(GCCPATH)arm-none-eabi-gcc -print-sysroot) ifeq ($(SYSROOT),) -CFLAGS += -I/usr/include -else -CFLAGS += --sysroot="$(SYSROOT)" + # path for Debian-based systems + SYSROOT = /usr/lib/arm-none-eabi endif +CFLAGS += --sysroot="$(SYSROOT)" -# optimization and debug levels +# optimization and debugging levels ifneq ($(DEBUG),0) -OPTI_LVL = -Og -OPTI_ALVL = $(OPTI_LVL) -DBG_LVL = -g3 + OPTI_LVL = g + # any higher won't work with LLVM >= 14 + # will be fixed in LLVM 20 : https://github.com/llvm/llvm-project/pull/116956 + DBG_LVL = 1 else -OPTI_LVL = -Oz -OPTI_ALVL = -Os # assembler does not handle -Oz, use -Os instead -DBG_LVL = -g0 + OPTI_LVL = z + DBG_LVL = 0 endif +CFLAGS += -O$(OPTI_LVL) -g$(DBG_LVL) -CFLAGS += $(OPTI_LVL) $(DBG_LVL) CFLAGS += -fomit-frame-pointer -momit-leaf-frame-pointer CFLAGS += -fno-common -mlittle-endian @@ -91,9 +91,6 @@ CFLAGS += -fropi CFLAGS += -fno-jump-tables # avoid jump tables for switch to avoid problems with invalid PIC access CFLAGS += -nostdlib -nodefaultlibs -AFLAGS += $(OPTI_ALVL) $(DBG_LVL) -fno-common - -LDFLAGS += $(OPTI_LVL) $(DBG_LVL) LDFLAGS += -fomit-frame-pointer LDFLAGS += -Wall LDFLAGS += -fno-common -ffunction-sections -fdata-sections -fwhole-program @@ -103,7 +100,7 @@ LDFLAGS += -Wl,--gc-sections -Wl,-Map,$(DBG_DIR)/app.map ifeq ($(TARGET_NAME),$(filter $(TARGET_NAME),TARGET_NANOS)) CFLAGS += --target=armv6m-none-eabi -mthumb CFLAGS += -mcpu=cortex-m0plus -mthumb -mtune=cortex-m0plus -AFLAGS += -mcpu=cortex-m0plus -mtune=cortex-m0plus -mthumb +AFLAGS += -mcpu=cortex-m0plus -mthumb LDFLAGS += -mcpu=cortex-m0plus -mthumb LDFLAGS += -nostartfiles --specs=nano.specs endif @@ -117,7 +114,7 @@ LDFLAGS += -mtune=cortex-m0plus -mlittle-endian -mcpu=cortex-m0plus LDFLAGS += -nostdlib -nodefaultlibs #-nostartfiles LDFLAGS += -mno-movt LDFLAGS += -L$(BOLOS_SDK)/arch/st33/lib/ -AFLAGS += -mcpu=cortex-m0plus -mtune=cortex-m0plus -mthumb +AFLAGS += -mcpu=cortex-m0plus -mthumb endif ifeq ($(TARGET_NAME),$(filter $(TARGET_NAME),TARGET_STAX TARGET_FLEX TARGET_NANOS2)) diff --git a/Makefile.rules_generic b/Makefile.rules_generic index a3170dccd..bf4fded01 100644 --- a/Makefile.rules_generic +++ b/Makefile.rules_generic @@ -92,11 +92,11 @@ $(OBJ_DIR)/app/%.o: $(APP_DIR)/%.c $(BUILD_DEPENDENCIES) prepare $(OBJ_DIR)/app/%.o: $(APP_DIR)/%.s $(BUILD_DEPENDENCIES) prepare @echo "[AS] $@" - $(L)$(call as_cmdline,$(INCLUDES_PATH), $(DEFINES),$<,$@) + $(L)$(call as_cmdline,$(INCLUDES_PATH),$<,$@) $(OBJ_DIR)/app/%.o: $(APP_DIR)/%.S $(BUILD_DEPENDENCIES) prepare @echo "[AS] $@" - $(L)$(call as_cmdline,$(INCLUDES_PATH), $(DEFINES),$<,$@) + $(L)$(call as_cmdline,$(INCLUDES_PATH),$<,$@) # SDK files targets $(OBJ_DIR)/sdk/%.o: $(BOLOS_SDK)/%.c $(BUILD_DEPENDENCIES) prepare @@ -105,11 +105,11 @@ $(OBJ_DIR)/sdk/%.o: $(BOLOS_SDK)/%.c $(BUILD_DEPENDENCIES) prepare $(OBJ_DIR)/sdk/%.o: $(BOLOS_SDK)/%.s $(BUILD_DEPENDENCIES) prepare @echo "[AS] $@" - $(L)$(call as_cmdline,$(INCLUDES_PATH), $(DEFINES),$<,$@) + $(L)$(call as_cmdline,$(INCLUDES_PATH),$<,$@) $(OBJ_DIR)/sdk/%.o: $(BOLOS_SDK)/%.S $(BUILD_DEPENDENCIES) prepare @echo "[AS] $@" - $(L)$(call as_cmdline,$(INCLUDES_PATH), $(DEFINES),$<,$@) + $(L)$(call as_cmdline,$(INCLUDES_PATH),$<,$@) # Generic targets $(OBJ_DIR)/%.o: %.c $(BUILD_DEPENDENCIES) prepare @@ -118,11 +118,11 @@ $(OBJ_DIR)/%.o: %.c $(BUILD_DEPENDENCIES) prepare $(OBJ_DIR)/%.o: %.s $(BUILD_DEPENDENCIES) prepare @echo "[AS] $@" - $(L)$(call as_cmdline,$(INCLUDES_PATH), $(DEFINES),$<,$@) + $(L)$(call as_cmdline,$(INCLUDES_PATH),$<,$@) $(OBJ_DIR)/%.o: %.S $(BUILD_DEPENDENCIES) prepare @echo "[AS] $@" - $(L)$(call as_cmdline,$(INCLUDES_PATH), $(DEFINES),$<,$@) + $(L)$(call as_cmdline,$(INCLUDES_PATH),$<,$@) ifeq (,$(filter $(DEFINES),BOLOS_OS_UPGRADER_APP)) ifneq ($(SCRIPT_LD),) @@ -197,7 +197,7 @@ endif # dependency files are generated along the object file cc_cmdline = $(CC) -c $(CFLAGS) -MMD -MT $(4) -MF $(subst $(OBJ_DIR), $(DEP_DIR), $(addsuffix .d, $(basename $(4)))) $(addprefix -D,$(2)) $(addprefix -I,$(1)) -o $(4) $(3) -as_cmdline = $(AS) -c $(AFLAGS) $(addprefix -D,$(2)) $(addprefix -I,$(1)) -o $(4) $(3) +as_cmdline = $(AS) -c $(AFLAGS) $(addprefix -I,$(1)) -o $(3) $(2) ### END GCC COMPILER RULES diff --git a/Makefile.standard_app b/Makefile.standard_app index 2cee343d9..95c02823a 100644 --- a/Makefile.standard_app +++ b/Makefile.standard_app @@ -218,8 +218,11 @@ APP_FLAGS_APP_LOAD_PARAMS = $(shell printf '0x%x' $$(( $(STANDARD_APP_FLAGS) + $ # COMPILER SETTINGS # ##################################################################### CC := $(CLANGPATH)clang -AS := $(GCCPATH)arm-none-eabi-gcc -LD := $(GCCPATH)arm-none-eabi-gcc +AS := $(CLANGPATH)clang +LD := $(CLANGPATH)clang +ifeq ($(AS),$(CLANGPATH)clang) + AFLAGS += --target=arm-arm-none-eabi +endif LDLIBS += -lm -lgcc -lc ##################################################################### diff --git a/README.md b/README.md index 31608c075..61538a176 100644 --- a/README.md +++ b/README.md @@ -55,34 +55,23 @@ In short, to build an app for an OS, you should: ## About API_LEVEL branches -This list the API_LEVEL branches with their purposed (corresponding OS) and state if they should still be patched or not (OS not “active” anymore). -OS release candidates are only kept in the list when a corresponding OS release production might be released on the same API_LEVEL. - -| Name | Related OS | Active -|---------|-------------------------------------------------------------------------------------------------------------------------------|------------------- -| LNS |
nanos_2.1.0 | :heavy_check_mark: -| 1 | nanox_2.1.0
nanos+_1.1.0 | :x: -| 2 | only rc releases | :x: -| 3 | only rc releases | :x: -| 4 | only rc releases | :x: -| 5 | nanox_2.2.{0, 1, 2, 3}
nanos+ 1.1.1 | :heavy_check_mark: -| 6 | only rc releases | :x: -| 7 / 7.1 | only rc releases | :x: -| 7.2 | only rc releases
(not compatible due
to font update) | :x: -| 8 | stax_1.0.0 | :x: -| 9 | only rc releases | :x: -| 10 | stax_1.1.0 | :x: -| 11 | stax_1.2.0
stax_1.2.1 | :x: -| 12 | nanox_2.3.0-rc2
nanos+_1.2.0-rc2 | :x: -| 13 | stax_1.3.0 | :x: -| 14 | only rc releases | :x: -| 15 | stax_1.4.0 | :heavy_check_mark: -| 16 | europa_0.1.0-re1 | :x: -| 17 | europa_0.1.0-re2 | :x: -| 18 | europa_0.1.0-re3
flex_0.2.0-rc1
flex_0.2.0-rc2
nanox_2.3.0-rc5
nanos+_1.2.0-rc5
nanos+_1.2.0 | :heavy_check_mark: -| 19 | flex_1.0.0-rc1
flex_1.0.0-rc2
flex_1.0.0
flex_1.0.1
| :x: -| 20 | stax_1.5.0-rc1
flex_1.1.0-rc1 | :x: -| 21 | stax_1.5.0-rc2
flex_1.1.0-rc2
stax_1.5.0
flex_1.1.0
flex_1.1.1
| :heavy_check_mark: +This list the main API_LEVEL branches with their purposed (corresponding OS) and state if they should still be patched or not (OS not “active” anymore). + +The full mapping of API_LEVEL branches, including OS release candidates, is available [here](api_levels.json). + +| Name | Related OS | Active | +| ---- | -------------------------------------------------- | ------------------ | +| LNS |
nanos_2.1.0 | :heavy_check_mark: | +| 1 | nanox_2.1.0
nanos+_1.1.0 | :x: | +| 5 | nanox_2.2.{0, 1, 2, 3}
nanos+ 1.1.1 | :heavy_check_mark: | +| 8 | stax_1.0.0 | :x: | +| 10 | stax_1.1.0 | :x: | +| 11 | stax_1.2.0
stax_1.2.1 | :x: | +| 13 | stax_1.3.0 | :x: | +| 15 | stax_1.4.0 | :x: | +| 18 | nanos+_1.2.0 | :heavy_check_mark: | +| 19 | flex_1.0.0
flex_1.0.1
| :x: | +| 21 | stax_1.5.0
flex_1.1.0
flex_1.1.1
| :heavy_check_mark: | ### Cherry-picking process: diff --git a/api_levels.json b/api_levels.json new file mode 100644 index 000000000..8dffb911a --- /dev/null +++ b/api_levels.json @@ -0,0 +1,98 @@ +{ + "LNS": [ + "nanos_2.1.0" + ], + "1": [ + "nanox_2.1.0", + "nanos+_1.1.0" + ], + "2": [ + "only rc releases" + ], + "3": [ + "only rc releases" + ], + "4": [ + "only rc releases" + ], + "5": [ + "nanox_2.2.0", + "nanox_2.2.1", + "nanox_2.2.2", + "nanox_2.2.3", + "nanos+_1.1.1" + ], + "6": [ + "only rc releases" + ], + "7": [ + "only rc releases" + ], + "7.1": [ + "only rc releases" + ], + "7.2": [ + "only rc releases" + ], + "8": [ + "stax_1.0.0" + ], + "9": [ + "only rc releases" + ], + "10": [ + "stax_1.1.0" + ], + "11": [ + "stax_1.2.0", + "stax_1.2.1" + ], + "12": [ + "nanox_2.3.0-rc2", + "nanos+_1.2.0-rc2" + ], + "13": [ + "stax_1.3.0" + ], + "14": [ + "only rc releases" + ], + "15": [ + "stax_1.4.0" + ], + "16": [ + "europa_0.1.0-re1" + ], + "17": [ + "europa_0.1.0-re2" + ], + "18": [ + "europa_0.1.0-re3", + "flex_0.2.0-rc1", + "flex_0.2.0-rc2", + "nanox_2.3.0-rc5", + "nanos+_1.2.0-rc5", + "nanos+_1.2.0" + ], + "19": [ + "flex_1.0.0-rc1", + "flex_1.0.0-rc2", + "flex_1.0.0", + "flex_1.0.1" + ], + "20": [ + "stax_1.5.0-rc1", + "flex_1.1.0-rc1" + ], + "21": [ + "stax_1.5.0-rc2", + "flex_1.1.0-rc2", + "stax_1.5.0", + "flex_1.1.0", + "flex_1.1.1", + "nanos+_1.3.0-rc1", + "nanox_2.4.0-rc1", + "stax_1.6.0-rc1", + "flex_1.2.0-rc1" + ] +} diff --git a/doc/Doxyfile b/doc/Doxyfile index ac9121848..56bc0322c 100644 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -263,12 +263,6 @@ TAB_SIZE = 4 ALIASES = -# This tag can be used to specify a number of word-keyword mappings (TCL only). -# A mapping has the form "name=value". For example adding "class=itcl::class" -# will allow you to use the command class in the itcl::class meaning. - -TCL_SUBST = - # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources # only. Doxygen will then generate output that is more tailored for C. For # instance, some of the names that are used will be different. The list of all @@ -1182,13 +1176,6 @@ CLANG_DATABASE_PATH = ALPHABETICAL_INDEX = YES -# The COLS_IN_ALPHA_INDEX tag can be used to specify the number of columns in -# which the alphabetical index list will be split. -# Minimum value: 1, maximum value: 20, default value: 5. -# This tag requires that the tag ALPHABETICAL_INDEX is set to YES. - -COLS_IN_ALPHA_INDEX = 5 - # In case all classes in a project start with a common prefix, all classes will # be put under the same header in the alphabetical index. The IGNORE_PREFIX tag # can be used to specify a prefix (or a list of prefixes) that should be ignored diff --git a/doc/mainpage.dox b/doc/mainpage.dox index 5d5943464..2bbc66517 100644 --- a/doc/mainpage.dox +++ b/doc/mainpage.dox @@ -3,7 +3,7 @@ @section mainpage_intro Introduction This documentation describes the different interfaces of Embedded SDK, -for \b Nano X, \b Nano S+ and \b Stax applications development. +for \b Nano X, \b Nano S+ \b Stax and \b Flex applications development. @section ble_presentation BlueTooth Low-Energy Interface @@ -16,7 +16,7 @@ The @subpage cxng_mainpage page contains all information necessary to understand @section nbgl_presentation Screen drawing API The @subpage nbgl_mainpage page contains all information necessary to understand the Graphical Library for -\b Stax, \b NanoX and \b NanoS+ applications development. +\b Stax, \b Flex, \b NanoX and \b NanoS+ applications development. @section nfc_presentation NFC Interface @@ -50,7 +50,7 @@ The @subpage ux_mainpage page contains all information necessary to user the Use @subsection ux_nbgl_presentation User eXperience on Applications using \b NBGL The @subpage ux_nbgl_mainpage page contains all information necessary to user the User eXperience for -\b Stax, \b NanoX and \b NanoS+ applications development using \b NBGL (most recent ones). +\b Stax, \b Flex, \b NanoX and \b NanoS+ applications development using \b NBGL (most recent ones). */ diff --git a/include/cx_stubs.h b/include/cx_stubs.h index b0d5a5e7e..40ec737dd 100644 --- a/include/cx_stubs.h +++ b/include/cx_stubs.h @@ -145,3 +145,4 @@ #define _NR_cx_cmac_start 0x8d #define _NR_cx_cmac_update 0x8e #define _NR_cx_cmac_finish 0x8f +#define _NR_cx_aes_siv_reset 0x90 diff --git a/include/os_app.h b/include/os_app.h index 3668ed3a4..76b29f571 100644 --- a/include/os_app.h +++ b/include/os_app.h @@ -28,6 +28,9 @@ typedef void (*appmain_t)(void); // application slot description typedef struct application_s { + // special flags for this application + uint64_t flags; + // nvram start address for this application (to check overlap when loading, and mpu lock) unsigned char *nvram_begin; // nvram stop address (exclusive) for this application (to check overlap when loading, and mpu @@ -38,9 +41,6 @@ typedef struct application_s { // into Thumb code appmain_t main; - // special flags for this application - uint64_t flags; - // Memory organization: [ code (RX) |alignpage| data (RW) |alignpage| install params (R) ] // length of the code section of the application (RX) diff --git a/include/os_nvm.h b/include/os_nvm.h index 3f1811bda..8fd5e370f 100644 --- a/include/os_nvm.h +++ b/include/os_nvm.h @@ -23,7 +23,7 @@ SYSCALL void nvm_erase(void *dst_adr PLENGTH(len), unsigned int len); SUDOCALL void nvm_write_page(unsigned int page_adr, bool force); // erase a nvm page at given address, only callable by the privileged APIs -SUDOCALL void nvm_erase_page(unsigned int page_adr); +SUDOCALL unsigned int nvm_erase_page(unsigned int page_adr); // any application can wipe the global pin, global seed, user's keys // disabled for security reasons // SYSCALL void diff --git a/include/ox_aes.h b/include/ox_aes.h index f09cf751d..2ee7b87f8 100644 --- a/include/ox_aes.h +++ b/include/ox_aes.h @@ -99,7 +99,7 @@ cx_aes_set_key_hw(const cx_aes_key_t *key PLENGTH(sizeof(cx_aes_key_t)), uint32_ /** * @brief Resets the AES context. */ -SYSCALL void cx_aes_reset_hw(void); +SYSCALL cx_err_t cx_aes_reset_hw(void); /** * @brief Encrypts or decrypts a block with AES. diff --git a/lib_cxng/cx.export b/lib_cxng/cx.export index 19d47e726..b935b75ff 100644 --- a/lib_cxng/cx.export +++ b/lib_cxng/cx.export @@ -148,3 +148,4 @@ cx_cipher_reset cx_cmac_start cx_cmac_update cx_cmac_finish +cx_aes_siv_reset diff --git a/lib_cxng/include/lcx_aes_siv.h b/lib_cxng/include/lcx_aes_siv.h index 06905dc69..b4db323b2 100644 --- a/lib_cxng/include/lcx_aes_siv.h +++ b/lib_cxng/include/lcx_aes_siv.h @@ -55,6 +55,17 @@ typedef struct _cx_aes_siv_context { */ WARN_UNUSED_RESULT cx_err_t cx_aes_siv_init(cx_aes_siv_context_t *ctx); +/** + * @brief Reset the AES-SIV context and HW used. + * @details The cipher context must be initialized beforehand. + * This function must be called after calls to #cx_aes_siv_finish or + * #cx_aes_siv_update. + * + * @param[in] ctx Pointer to the AES-SIV context. + * @return Error code. + */ +WARN_UNUSED_RESULT cx_err_t cx_aes_siv_reset(cx_aes_siv_context_t *ctx); + /** * @brief Sets the key to compute AES-SIV. * diff --git a/lib_cxng/src/cx_aes.c b/lib_cxng/src/cx_aes.c index 338f5386c..49dd20cdd 100644 --- a/lib_cxng/src/cx_aes.c +++ b/lib_cxng/src/cx_aes.c @@ -28,21 +28,24 @@ cx_err_t cx_aes_init_key_no_throw(const uint8_t *raw_key, size_t key_len, cx_aes cx_err_t cx_aes_enc_block(const cx_aes_key_t *key, const uint8_t *inblock, uint8_t *outblock) { cx_err_t error; + cx_err_t err_reset = CX_INTERNAL_ERROR; CX_CHECK(cx_aes_set_key_hw(key, CX_ENCRYPT)); CX_CHECK(cx_aes_block_hw(inblock, outblock)); - cx_aes_reset_hw(); end: - return error; + err_reset = cx_aes_reset_hw(); + return error == CX_OK ? err_reset : error; } cx_err_t cx_aes_dec_block(const cx_aes_key_t *key, const uint8_t *inblock, uint8_t *outblock) { cx_err_t error; + cx_err_t err_reset = CX_INTERNAL_ERROR; + CX_CHECK(cx_aes_set_key_hw(key, CX_ENCRYPT)); CX_CHECK(cx_aes_set_key_hw(key, CX_DECRYPT)); CX_CHECK(cx_aes_block_hw(inblock, outblock)); - cx_aes_reset_hw(); end: - return error; + err_reset = cx_aes_reset_hw(); + return error == CX_OK ? err_reset : error; } cx_err_t cx_aes_iv_no_throw(const cx_aes_key_t *key, diff --git a/lib_cxng/src/cx_aes_siv.c b/lib_cxng/src/cx_aes_siv.c index ed106d3dd..e7d8fd762 100644 --- a/lib_cxng/src/cx_aes_siv.c +++ b/lib_cxng/src/cx_aes_siv.c @@ -22,6 +22,19 @@ cx_err_t cx_aes_siv_init(cx_aes_siv_context_t *ctx) return error; } +cx_err_t cx_aes_siv_reset(cx_aes_siv_context_t *ctx) +{ + cx_err_t error = CX_INVALID_PARAMETER_VALUE; + if (ctx->cipher_ctx == NULL) { + goto end; + } + CX_CHECK(ctx->cipher_ctx->cipher_info->base->ctx_reset()); + cx_cipher_reset(ctx->cipher_ctx); + +end: + return error; +} + cx_err_t cx_aes_siv_set_key(cx_aes_siv_context_t *ctx, const uint8_t *key, size_t key_bitlen) { // AES SIV uses two keys of either 128, 192 or 256 bits each @@ -156,12 +169,14 @@ cx_err_t cx_aes_siv_encrypt(cx_aes_siv_context_t *ctx, uint8_t *tag) { cx_err_t error; + cx_err_t err_reset = CX_INTERNAL_ERROR; CX_CHECK(cx_aes_siv_start(ctx, CX_ENCRYPT, NULL, 0)); CX_CHECK(cx_aes_siv_update_aad(ctx, aad, aad_len)); CX_CHECK(cx_aes_siv_finish(ctx, input, in_len, tag)); CX_CHECK(cx_aes_siv_update(ctx, input, output, in_len)); end: - return error; + err_reset = cx_aes_siv_reset(ctx); + return error == CX_OK ? err_reset : error; } cx_err_t cx_aes_siv_decrypt(cx_aes_siv_context_t *ctx, @@ -173,14 +188,16 @@ cx_err_t cx_aes_siv_decrypt(cx_aes_siv_context_t *ctx, uint8_t *tag) { cx_err_t error; + cx_err_t err_reset = CX_INTERNAL_ERROR; CX_CHECK(cx_aes_siv_start(ctx, CX_DECRYPT, tag, CX_AES_BLOCK_SIZE)); CX_CHECK(cx_aes_siv_update(ctx, input, output, in_len)); - cx_cipher_reset(ctx->cipher_ctx); + CX_CHECK(cx_aes_siv_reset(ctx)); CX_CHECK(cx_aes_siv_update_aad(ctx, aad, aad_len)); CX_CHECK(cx_aes_siv_finish(ctx, output, in_len, tag)); end: - return error; + err_reset = cx_aes_siv_reset(ctx); + return error == CX_OK ? err_reset : error; } #endif // HAVE_AES && HAVE_CMAC diff --git a/lib_cxng/src/cx_cipher.h b/lib_cxng/src/cx_cipher.h index 88eddd8a6..7b8741a25 100644 --- a/lib_cxng/src/cx_cipher.h +++ b/lib_cxng/src/cx_cipher.h @@ -31,7 +31,7 @@ extern const cx_cipher_info_t cx_aes_256_info; /** HW support */ WARN_UNUSED_RESULT cx_err_t cx_aes_set_key_hw(const cx_aes_key_t *keys, uint32_t mode); WARN_UNUSED_RESULT cx_err_t cx_aes_block_hw(const uint8_t *inblock, uint8_t *outblock); -void cx_aes_reset_hw(void); +WARN_UNUSED_RESULT cx_err_t cx_aes_reset_hw(void); #endif // HAVE_AES #endif /* CX_CIPHER_H */ diff --git a/lib_nbgl/doc/mainpage_nanos.dox b/lib_nbgl/doc/mainpage_nanos.dox index 5cb0e4afc..0d4b564a5 100644 --- a/lib_nbgl/doc/mainpage_nanos.dox +++ b/lib_nbgl/doc/mainpage_nanos.dox @@ -1,7 +1,7 @@ #ifndef HAVE_SE_TOUCH -/** @page nbgl_mainpage New BOLOS Graphic API for Nanos +/** @page nbgl_mainpage New BOLOS Graphic API for Nano devices (Nano X and Nano S+) -@section mainpage_intro Introduction +@section nbgl_mainpage_intro Introduction This documentation describes the different interfaces of NBGL, the library that is targeted to be integrated in Nano products (Nano X and Nano S+). @@ -23,15 +23,11 @@ This is the most preferred API level to write an embedded Application. The @subpage nbgl_app_use_case_nanos page contains all information necessary to understand and use \b NBGL Application Use cases API -@subsection nbgl_flow_level_api_1 Flow API -The @subpage nbgl_flow page contains all information necessary to understand -and use \b NBGL Flow API, managing a scenario flow, made of one or several steps - -@subsection nbgl_step_level_api_1 Step API +@subsection nbgl_step_level_api_1 High-Level API The @subpage nbgl_step page contains all information necessary to understand -and use \b NBGL Step API, managing the page(s) of a scenario step +and use \b NBGL High-Level API, using predefined steps -@subsection nbgl_mid_level_api_1 Complex objects (layout) API +@subsection nbgl_mid_level_api_1 Mid-Level API The @subpage nbgl_layout page contains all information necessary to understand and use \b NBGL Mid-Level API, using complex objects @@ -48,6 +44,7 @@ In order to reduce the footprint (RAM+Flash), some features are only activated i Define | Meaning | Default state ------------- | ------------- | ------------- +\b NBGL_STEP | Activate High-Level API | Active in SDK, and in OS \b NBGL_USE_CASE | Activate Use Case API | Active in SDK, not in OS \b NBGL_KEYBOARD | Activate Keyboard object | Active in OS, not in SDK \b NBGL_KEYPAD | Activate Keypad object | Active in OS, not in SDK diff --git a/lib_nbgl/doc/nbgl_layout.dox b/lib_nbgl/doc/nbgl_layout.dox index 608d61b8f..d57f134cb 100644 --- a/lib_nbgl/doc/nbgl_layout.dox +++ b/lib_nbgl/doc/nbgl_layout.dox @@ -50,10 +50,10 @@ For example 1: @code nbgl_layoutDescription_t layoutDescription = { - .modal = false, // not modal (so on plane 0) - .onActionCallback = &myActionCallback, // generic callback for all controls - .onTapText = NULL, // no "tapable" main container - .ticker.callback = NULL // no ticker + .modal = false, // not modal (so on plane 0) + .onActionCallback = &myActionCallback, // generic callback for all controls + .onTapText = NULL, // no "tapable" main container + .ticker.callback = NULL // no ticker }; // create layout nbgl_layout_t *layout = nbgl_layoutGet(&layoutDescription); @@ -63,11 +63,11 @@ For example 2: @code nbgl_layoutDescription_t layoutDescription = { - .modal = false, // not modal (so on plane 0) - .onActionCallback = &myActionCallback, // generic callback for all controls - .onTapText = "Tap to continue", // A "tapable" main container is necessary, with this text - .onTapToken = TAP_TOKEN, // token to be used when main container is touched - .ticker.callback = NULL // no ticker + .modal = false, // not modal (so on plane 0) + .onActionCallback = &myActionCallback, // generic callback for all controls + .onTapText = "Tap to continue", // A "tapable" main container is necessary, with this text + .onTapToken = TAP_TOKEN, // token to be used when main container is touched + .ticker.callback = NULL // no ticker }; /* create layout */ nbgl_layout_t *layout = nbgl_layoutGet(&layoutDescription); @@ -665,7 +665,7 @@ void app_keyboard(void) { Some rare screens require displaying a keypad at the bottom of the page, to enter digits. -The digits to enter are usually a PIN code, so enter or to confirm. +The digits to enter are usually a PIN code, to enter or to confirm. Here are some example of these pages in Bolos UX: diff --git a/lib_nbgl/doc/nbgl_layout_nanos.dox b/lib_nbgl/doc/nbgl_layout_nanos.dox index 7b8b22c46..64531bca2 100644 --- a/lib_nbgl/doc/nbgl_layout_nanos.dox +++ b/lib_nbgl/doc/nbgl_layout_nanos.dox @@ -40,9 +40,9 @@ For example: @code nbgl_layoutDescription_t layoutDescription = { - .modal = false, // not modal (so on plane 0) - .onActionCallback = &myActionCallback, // generic callback for all controls - .ticker.callback = NULL // no ticker + .modal = false, // not modal (so on plane 0) + .onActionCallback = &myActionCallback, // generic callback for all controls + .ticker.callback = NULL // no ticker }; // create layout nbgl_layout_t *layout = nbgl_layoutGet(&layoutDescription); @@ -187,22 +187,22 @@ static void keyboardCallback(char touchedKey) { void app_keyboard(void) { nbgl_layoutDescription_t layoutDescription = { - .modal = false, + .modal = false, .onActionCallback = NULL }; nbgl_layoutKbd_t kbdInfo = { - .callback = keyboardCallback, - .keyMask = 1<<26, // no masked letter but masked backspace - .lettersOnly = true // only letters are allowed + .callback = keyboardCallback, + .keyMask = 1<<26, // no masked letter but masked backspace + .lettersOnly = true // only letters are allowed }; nbgl_layoutCenteredInfo_t centeredInfo = { .text1 = descriptionTxt, .text2 = NULL, - .icon = NULL, + .icon = NULL, .onTop = true }; nbgl_layoutNavigation_t navInfo = { - .direction = HORIZONTAL_NAV, + .direction = HORIZONTAL_NAV, .indication = LEFT_ARROW | RIGHT_ARROW }; int status; @@ -261,19 +261,26 @@ used to modify the active keys of an existing keypad (backspace and validate key @note at creation time, backspace and validate keys are inactive -@subsubsection keypad_sub_section_2 Adding/Updating hidden digits area +@subsubsection keypad_sub_section_2 Adding/Updating keypad content -This object, displayed on top of the keypad, consists in up to 8 discs (plain or empty) corresponding to -hidden entered/not entered digits. +This object consists in: +- an optional title +- either: + - up to 12 discs (invisible or visible) corresponding to hidden entered/not entered digits. + - or up to 12 digits if not hidden The parameters to configure this object are: +- The optional title (NULL if not used) +- A boolean to indicate whether digits are hidden or not - The number of total digits to be displayed (all digits are considered as "not entered") +- The digits to be displayed if not hidden -The API to insert such an object is @ref nbgl_layoutAddHiddenDigits(). +The API to insert such an object is @ref nbgl_layoutAddKeypadContent(). -This function returns a positive integer (if successful) to be used as an index in @ref nbgl_layoutUpdateHiddenDigits() function, -used to modify the number of entered digits. +This function returns a positive integer (if successful). + +The @ref nbgl_layoutUpdateKeypadContent() function can be used to modify the number of entered digits or the digits. @section refresh_layout Refreshing screen @@ -286,7 +293,7 @@ It will only refresh the rectangle part of the screen having changed (with objec @section callbacks Control actions management -Some controls, like a touchable bar, or a choice by radio buttons, can be interacted with thanks to the Touchscreen. +Some controls can be interacted with thanks to the buttons. The developer can subscribe to these events by providing an action callback in @ref nbgl_layoutGet(), with @ref nbgl_layoutTouchCallback_t prototype. @@ -294,10 +301,10 @@ The first parameter (*token*) of this function is a token provided along with th The second parameter (*index*) is only used for some types of complex objects: -- **Navigation bar**: in this case, *index* gives the index of the new active page, when navigating. +- **Bar List choices**: in this case, *index* gives the index of the selected bar. - **Radio button choices**: in this case, *index* gives the index of the selected choice. -- **Switches**: in this case, if *index* is 0 it means OFF, otherwise it means ON. -- **Choice buttons**: in this case, if *index* is 0, it means top-button (choice 1), otherwise it means bottom-buttom (choice 2) +- **Switches**: in this case, *index* is 0: it is up to the Apps to know what to do here. +- **Info buttons**: in this case, *index* is 0 (useless) @subsection release Releasing a layout diff --git a/lib_nbgl/doc/nbgl_page.dox b/lib_nbgl/doc/nbgl_page.dox index bee008ef8..568bb9c5a 100644 --- a/lib_nbgl/doc/nbgl_page.dox +++ b/lib_nbgl/doc/nbgl_page.dox @@ -1,5 +1,5 @@ #ifdef NBGL_PAGE -/** @page nbgl_page Predefined pages API +/** @page nbgl_page Predefined Pages API @section nbgl_page_intro Introduction This chapter describes briefly the high-level API of Advanced BOLOS Graphic Library. diff --git a/lib_nbgl/doc/nbgl_step.dox b/lib_nbgl/doc/nbgl_step.dox index 4b5838878..ebc228391 100644 --- a/lib_nbgl/doc/nbgl_step.dox +++ b/lib_nbgl/doc/nbgl_step.dox @@ -1,5 +1,5 @@ #ifdef NBGL_STEP -/** @page nbgl_step Step API +/** @page nbgl_step Predefined Step API @section nbgl_step_intro Introduction This chapter describes briefly the high-level API of Advanced BOLOS Graphic Library. diff --git a/lib_nbgl/doc/nbgl_use_case.dox b/lib_nbgl/doc/nbgl_use_case.dox index 5035fd80a..9fc4e8e5b 100644 --- a/lib_nbgl/doc/nbgl_use_case.dox +++ b/lib_nbgl/doc/nbgl_use_case.dox @@ -50,16 +50,19 @@ A few APIs are available to draw typical Use-Cases, such as: - @ref nbgl_useCaseReview() to draw the pages of a regular coin transaction review, when all info are available from the beginning (see @subpage use_case_review) - @ref nbgl_useCaseReviewLight() to draw the pages of a transaction review with a simple button confirmation, when all info are available from the beginning (see @subpage use_case_review_light) - @ref nbgl_useCaseReviewStreamingStart() to draw the pages of a regular coin transaction review, when all info are not available from the beginning (see @subpage use_case_review_streaming) +- for reviews with a warning prolog: + - @ref nbgl_useCaseReviewWithWarning() to draw the pages of a regular coin transaction review, when all info are available from the beginning, but with an identified risk requiring a warning prolog (see @subpage use_case_review_with_warning) + - @ref nbgl_useCaseReviewStreamingWithWarningStart() to draw the pages of a regular coin transaction review, when all info are not available from the beginning, but with an identified risk requiring a warning prolog (see @subpage use_case_review_with_warning and @subpage use_case_review_streaming) - for address verification: - @ref nbgl_useCaseAddressReview() to draw an address confirmation page, with a possibility to see it as QR Code and some extra tag/value pairs (see @subpage use_case_addr_review) - for keypad: - @ref nbgl_useCaseKeypadPIN() to draw a default keypad implementation with hidden digits (see @subpage use_case_keypad) - @ref nbgl_useCaseKeypadDigits() to draw a default keypad implementation, showing digits (see @subpage use_case_keypad) +- for generic navigable content: + - @ref nbgl_useCaseNavigableContent() to draw a level of generic content navigable pages Some APIs have also been kept for backward compatibility, and for some rare cases: -- for Settings: - - @ref nbgl_useCaseSettings() to draw a level of settings pages - for most used reviews: - @ref nbgl_useCaseReviewStart() to draw the cover page of a review (initial page, without data) - @ref nbgl_useCaseStaticReview() to draw the data pages of a regular review, when all info are available from the beginning (all pages but the cover one) @@ -179,11 +182,14 @@ Here is the code to display the example picture (and a status page for confirmat @code static void confirmationCallback(void) { // draw a status screen which continues by returning to appMain - nbgl_useCaseStatus("Transaction rejected",false,appMain); + nbgl_useCaseStatus("Transaction rejected", false, appMain); } void onRejectTransaction(void) { - nbgl_useCaseConfirm("Reject transaction?","Yes, Reject","Go back to transaction",confirmationCallback); + nbgl_useCaseConfirm("Reject transaction?", + "Yes, Reject", + "Go back to transaction", + confirmationCallback); } @endcode @@ -213,7 +219,8 @@ void onRejectTransaction(void) { nbgl_useCaseChoice(&C_key_64px, "Delete all credentials?", "This includes all saved\ncredentials from all websites\nand services.", - "Yes, erase","No, don't erase", + "Yes, erase", + "No, don't erase", onChoice); } @endcode @@ -226,12 +233,12 @@ A status is a transient page, without control, to display during a short time, f The @ref nbgl_useCaseStatus() function enables to create such a page, with the following arguments: - a message string to set in middle of page -- a boolean to indicate if true, that the message is drawn in a Ledger style (with corners) -- a quit callback, called when quit timer times out (or the page is "tapped") +- a boolean to indicate if true, that the message is drawn in a Ledger style (with corners) and select the icon +- a quit callback, called when timer times out (or the page is "tapped") -If it's a success status, a "success" tune will be automatically played. +If it's a success status, a *success* tune will be automatically played. -@subsection use_case_review_status Pre-defined status Use Case +@subsection use_case_review_status Pre-defined review status Use Case \image{inline} html UseCase-Review-Status.png "caption" height=300 @@ -239,9 +246,9 @@ Similar as @subpage use_case_status, this is used to display transient page, wit The @ref nbgl_useCaseReviewStatus() function enables to create such a page, with the following arguments: - a type of status (with predefined message) -- a quit callback, called when quit timer times out (or the page is "tapped") +- a quit callback, called when timer times out (or the page is "tapped") -If it's a success status, a "success" tune will be automatically played. +If it's a success status, a *success* tune will be automatically played. @subsection use_case_review Review Use Case @@ -249,13 +256,12 @@ If it's a success status, a "success" tune will be automatically played. In most cases, the developer may know all tag/value pairs of a transaction when it is submitted. -In this case, what we call a "static" review can be used. The number of pages is computed automatically and pages can be navigated forward -and backward. +Thus, the number of pages is computed automatically and pages can be navigated forward and backward. -In case of a tag/value pair too long to be fully displayed, the "more" button will be automatically drawn and its handling +In case of a tag/value pair too long to be fully displayed, the *more* button will be automatically drawn and its handling automatically performed by NBGL by building a detailed modal view. -When the user taps on "Reject" in any page, a confirmation page is automatically drawned to let user confirm that he rejects +When the user taps on *Reject* in any page, a confirmation page is automatically drawned to let user confirm that he rejects the transaction. In this case, the given callback is called and it's up to app's developer to call @ref nbgl_useCaseReviewStatus(), as in case of long-press. @@ -289,15 +295,15 @@ static void onReviewResult(bool confirm) { } } -void staticReview(void) { - // static review, providing the whole list of pairs +void startReview(void) { + // standard review, providing the whole list of pairs nbgl_useCaseReview(TYPE_TRANSACTION, // type of operation - &pairList, // list of tag/value pairs - coinIcon, // icon of the coin - "Review transaction\nto send coin", // title of the first page - NULL, // sub-title of the first page - "Sign transaction to\nsend coin?", // title of the last page - onReviewResult); // callback on result of the review + &pairList, // list of tag/value pairs + coinIcon, // icon of the coin + "Review transaction\nto send coin", // title of the first page + NULL, // sub-title of the first page + "Sign transaction to\nsend coin?", // title of the last page + onReviewResult); // callback on result of the review } @endcode @@ -351,15 +357,15 @@ static nbgl_layoutTagValue_t* getPair(uint8_t index) { return &pair; } -void staticReview(void) { - // static review, providing the whole list of pairs +void startReview(void) { + // standard review, providing the whole list of pairs nbgl_useCaseReview(TYPE_TRANSACTION, // type of operation - &pairList, // list of tag/value pairs - coinIcon, // icon of the coin - "Review transaction\nto send coin", // title of the first page - NULL, // sub-title of the first page - "Sign transaction to\nsend coin?", // title of the last page - onReviewResult); // callback on result of the review + &pairList, // list of tag/value pairs + coinIcon, // icon of the coin + "Review transaction\nto send coin", // title of the first page + NULL, // sub-title of the first page + "Sign transaction to\nsend coin?", // title of the last page + onReviewResult); // callback on result of the review } @endcode @@ -380,13 +386,13 @@ The API to initiate the display of the series of pages is @ref nbgl_useCaseRevie In some cases, the application cannot know all tag/value pairs of a transaction when the review is started. -In this case, what we call a "streaming" review can be used. The pages to display for each "stream are computed automatically and pages can be navigated forward -and backward (only within a "stream" for backward). +In this case, what we call a *streaming* review can be used. The pages to display for each *stream* are computed automatically and pages can be navigated forward +and backward (only within a *stream* for backward). -In case of a tag/value pair too long to be fully displayed, the "more" button will be automatically drawn and its handling +In case of a tag/value pair too long to be fully displayed, the *more* button will be automatically drawn and its handling automatically performed by NBGL by building a detailed modal view. -When the user taps on "Reject" in any page, a confirmation page is automatically drawned to let user confirm that he rejects +When the user taps on *Reject* in any page, a confirmation page is automatically drawned to let user confirm that he rejects the transaction. In this case, the given callback is called and it's up to app's developer to call @ref nbgl_useCaseReviewStatus(), as in case of long-press. @@ -394,7 +400,7 @@ The API to initiate the display of the series of pages is @ref nbgl_useCaseRevie - the type of operation to review (transaction, message or generic operation) - the texts/icon to use in presentation page -- a callback with one boolean parameter. +- a callback with one boolean parameter: - If this parameter is *false*, it means that the transaction is rejected. - If this parameter is *true*, it means that NBGL is waiting for new data, sent with @ref nbgl_useCaseReviewStreamingContinue() or @ref nbgl_useCaseReviewStreamingContinueExt() @@ -402,11 +408,11 @@ As long as there are new tag/value pairs to send, the API to call is either @ref @ref nbgl_useCaseReviewStreamingContinue(), providing: - the list of tag/value pairs (or a callback to get them one by one) -- a callback with one boolean parameter. +- a callback with one boolean parameter: - If this parameter is *false*, it means that the transaction is rejected. - If this parameter is *true*, it means that NBGL is waiting for new data, to be sent with @ref nbgl_useCaseReviewStreamingContinue() or @ref nbgl_useCaseReviewStreamingContinueExt() -When there is no more data to senf, the API to call is either @ref nbgl_useCaseReviewStreamingFinish(), providing: +When there is no more data to send, the API to call is @ref nbgl_useCaseReviewStreamingFinish(), providing: - the title to use for last page (with long-press button) - a callback called when the long press button on last page or reject confirmation is used. The callback's param is *true* for confirmation, *false* for rejection. @@ -445,13 +451,178 @@ static void onTransactionContinue(bool askMore) } } -void staticReview(void) { +void startReview(void) { // initiate the streaming review nbgl_useCaseReviewStreamingStart(TYPE_TRANSACTION, - &C_ic_asset_cardano_64, // icon on first and last page - "Review transaction", // title of the first page - NULL, // sub-title of the first page - onTransactionContinue); // callback to reject or ask more data + &C_ic_asset_cardano_64, // icon on first and last page + "Review transaction", // title of the first page + NULL, // sub-title of the first page + onTransactionContinue); // callback to reject or ask more data +} +@endcode + +@subsection use_case_review_with_warning Review with warning Use Case + +\image{inline} html UseCase-Review-With-Warning.png "caption" height=500 + +The review itself behaves like in @subpage use_case_review. The main differences are: + +- The review itself is preceded by a warning page offering the possibility to cancel the review ("Back to safety") or to start it ("Continue anyway") +- In the first and last pages of the actual review, a top-right button offers the possibility to get more information about the warning + +The API to initiate the display of the series of pages is @ref nbgl_useCaseReviewWithWarning(), providing: + +- the type of operation to review (transaction, message or generic operation) +- the list of tag/value pairs (or a callback to get them one by one) +- the texts/icon to use in presentation page and in last page +- the configuration to use for the warning (see @ref nbgl_warning_t structure) +- a callback called when the long press button on last page or reject confirmation is used. The callback's param is *true* for confirmation, *false* for rejection. + +@note the recommended configuration for warning is the predefined one. In this case, one just has to fill the *predefinedSet* field of @ref nbgl_warning_t with the appropriate warning +causes (bitfield) and the *reportProvider* field with the name of the 3rd party providing the Web3Checks report, if necessary. + +Here is the code to display something similar to example picture: + +@code +// 4 pairs of tag/value to display +static nbgl_layoutTagValue_t pairs[4]; + +static const nbgl_contentTagValueList_t pairList = { + .nbMaxLinesForValue = 0, + .nbPairs = 4, + .pairs = (nbgl_layoutTagValue_t*)pairs +}; + +// warning description (cannot be in call stack) +static nbgl_warning_t warningDesc; + +// called when long press button on 3rd page is long-touched or when reject footer is touched +static void onReviewResult(bool confirm) { + // display a status page and go back to main + if (confirm) { + nbgl_useCaseReviewStatus(STATUS_TYPE_TRANSACTION_SIGNED, appMain); + } + else { + nbgl_useCaseReviewStatus(STATUS_TYPE_TRANSACTION_REJECTED, appMain); + } +} + +void staticReview(void) { + warningDesc.predefinedSet = (1 << W3C_LOSING_SWAP_WARN) | (1 << BLIND_SIGNING_WARN); + warningDesc.reportProvider = "Blockaid"; + + // static review, providing the whole list of pairs + nbgl_useCaseReviewWithWarning(TYPE_TRANSACTION, // type of operation + &pairList, // list of tag/value pairs + coinIcon, // icon of the coin + "Review transaction\nto send coin", // title of the first page + NULL, // sub-title of the first page + "Sign transaction to\nsend coin?", // title of the last page + NULL, // no tip-box in first page of review + warningDesc, // description of warning causes + onReviewResult); // callback on result of the review +} +@endcode + +Here is another version of the example code, not using predefined text: + +@code +// 4 pairs of tag/value to display +static nbgl_layoutTagValue_t pairs[4]; + +static const nbgl_contentTagValueList_t pairList = { + .nbMaxLinesForValue = 0, + .nbPairs = 4, + .pairs = (nbgl_layoutTagValue_t*)pairs +}; + +// icons for first level warning details +static const nbgl_icon_details_t *barListIcons[] = {&WARNING_ICON, &INFO_I_ICON}; + +// first level warning details +static const char *const barListTexts[] = {"Blind signing", "Risk detected"}; +static const char *const barListSubTexts[] = {"This transaction cannot be fully decoded.", "Web3 Checks found a risk:\nLosing swap"}; +// second level warning details in prolog +static const struct nbgl_warningDetails_s barListIntroDetails[] = { + {.title = "Back", + .type = QRCODE_WARNING, + .qrCode = {.url = "ledger.com/e8", .text1 = "ledger.com/e8", .text2 = "Scan to learn about the risks of blind signing.", .centered = true}}, + {.title = "Back", + .type = QRCODE_WARNING, + .qrCode = {.url = "url.com/od24xz", .text1 = "url.com/od24xz", .text2 = "Scan to view the risk report from Blockaid.", .centered = true}} +}; + +// second level warning details in review +static const struct nbgl_warningDetails_s barListIntroDetails[] = { + {.title = "Back", + .type = CENTERED_INFO_WARNING, + .centeredInfo = {.icon = &C_Warning_64px, .title = "Blind signing", .description = "This transaction’s details are not fully verifiable. If you sign it, you could lose all your assets.\n\nLearn about blind signing: +ledger.com/e8"}}, + {.title = "Back", + .type = CENTERED_INFO_WARNING, + .centeredInfo = {.icon = &C_Warning_64px, .title = "Risk detected", .description = "This transaction was scanned as risky by Web3 Checks.\n\nView full Blockaid report:url.com/od24xz"}} +}; + +// info in main warning page +static const nbgl_contentCenter_t warningInfo = { + .icon = &C_Warning_64px, + .title = "Dangerous transaction", + .description = "This transaction cannot be fully decoded, and was marked risky by Web3 Checks." +}; + +// first level warning details in prolog +static const nbgl_warningDetails_t warningIntroDetails = { + .title = "Security report", + .type = BAR_LIST_WARNING, + .barList.nbBars = 2, + .barList.icons = barListIcons, + .texts = barListTexts, + .subTexts = barListSubTexts, + .barList.details = barListIntroDetails +}; + +// first level warning details in review (when pressing top-right from first and last pages) +static const nbgl_warningDetails_t warningReviewDetails = { + .title = "Security report", + .type = BAR_LIST_WARNING, + .barList.nbBars = 2, + .barList.icons = barListIcons, + .texts = barListTexts, + .subTexts = barListSubTexts, + .barList.details = barListReviewDetails +}; + +// warning description (cannot be in call stack) +static const nbgl_warning_t warningDesc = { + .info = &warningInfo, + .introTopRightIcon = &PRIVACY_ICON, + .reviewTopRightIcon = &WARNING_ICON, + .introDetails = &warningIntroDetails, + .reviewDetails = &warningReviewDetails +}; + +// called when long press button on 3rd page is long-touched or when reject footer is touched +static void onReviewResult(bool confirm) { + // display a status page and go back to main + if (confirm) { + nbgl_useCaseReviewStatus(STATUS_TYPE_TRANSACTION_SIGNED, appMain); + } + else { + nbgl_useCaseReviewStatus(STATUS_TYPE_TRANSACTION_REJECTED, appMain); + } +} + +void staticReview(void) { + // static review, providing the whole list of pairs + nbgl_useCaseReviewWithWarning(TYPE_TRANSACTION, // type of operation + &pairList, // list of tag/value pairs + coinIcon, // icon of the coin + "Review transaction\nto send coin", // title of the first page + NULL, // sub-title of the first page + "Sign transaction to\nsend coin?", // title of the last page + NULL, // no tip-box in first page of review + warningDesc, // description of warning causes + onReviewResult); // callback on result of the review } @endcode @@ -485,10 +656,10 @@ static const nbgl_contentTagValueList_t pairList = { // called when either confirm button or reject token is called static void displayAddressCallback(bool confirm) { if (confirm) { - nbgl_useCaseStatus("ADDRESS\nVERIFIED",true,app_fullEthereum); + nbgl_useCaseReviewStatus(STATUS_TYPE_ADDRESS_VERIFIED, app_fullEthereum); } else { - nbgl_useCaseStatus("Address rejected",false,app_fullEthereum); + nbgl_useCaseReviewStatus(STATUS_TYPE_ADDRESS_REJECTED, app_fullEthereum); } } @@ -497,7 +668,8 @@ void app_ethereumVerifyAddress(void) { &pairList myAppIcon, "Verify MyCoin\naddress", - NULL,"Cancel", + NULL, + "Cancel", appMain); } @endcode @@ -506,12 +678,9 @@ void app_ethereumVerifyAddress(void) { \image{inline} html UseCase-Spinner.png "caption" height=300 -When an address needs to be confirmed, it can be displayed in a Address Confirmation Use Case, at first as simple page with -the raw address (as text) and a black button/Footer pair to choose to confirm or reject the address. +This Use Case is simply to display a static *waiting page* -An extra button under the raw address enables to open a modal page to see the address as a QR code. - -The @ref nbgl_useCaseSpinner() function enables to create such a page, with the following parameters: +The @ref nbgl_useCaseSpinner() function enables to create such a page, without any parameters. @subsection use_case_keypad Keypad Use Case @@ -559,13 +728,13 @@ static void pinentry_cb(int token, uint8_t index) { void ui_menu_pinentry_display(unsigned int value) { // Draw the keypad nbgl_useCaseKeypadPIN("Enter User PIN", - 6, - 12, - TOKEN_PIN_ENTRY_BACK, - false, - TUNE_TAP_CASUAL, - validate_pin, - pinentry_cb); + 6, + 12, + TOKEN_PIN_ENTRY_BACK, + false, + TUNE_TAP_CASUAL, + validate_pin, + pinentry_cb); } @endcode @@ -574,6 +743,5 @@ void ui_menu_pinentry_display(unsigned int value) { After having drawn graphic objects in **framebuffer**, all functions of this API automatically refresh the screen. So no need to call @ref nbgl_refresh(). - */ #endif // HAVE_SE_TOUCH diff --git a/lib_nbgl/doc/nbgl_use_case_nanos.dox b/lib_nbgl/doc/nbgl_use_case_nanos.dox index 88b18a20f..734c0f19c 100644 --- a/lib_nbgl/doc/nbgl_use_case_nanos.dox +++ b/lib_nbgl/doc/nbgl_use_case_nanos.dox @@ -15,7 +15,7 @@ A full description of each predefined use-case can be found in this document @section nbgl_use_case_concepts Concepts -This layer uses the Step API described in @ref nbgl_step, but offers to developer more than a single step. +This layer uses the high-level API described in @ref nbgl_step, but offers to developer more than a single step. The goal is to simplify the usage of NBGL, but also to offer a better homogeneity across applications, by pushing developers to use common API for common use-cases. @@ -25,354 +25,548 @@ for end-users, getting more and more familiar with the user experience of applic @subsection nbgl_use_case_example_1 Example 1: transaction review -\image{inline} html UseCase-Nano-Review1.png "caption" width=1000 +\image{inline} html UseCase-Nano-Review-ex.png "caption" width=1000 In this example, a transaction review consists in 5 successive pages, and can be seen as a use-case -@subsection nbgl_use_case_example_2 Example 2: about pages +@subsection nbgl_use_case_example_2 Example 2: Settings pages -\image{inline} html UseCase-Nano-About1.png "caption" width=800 +\image{inline} html UseCase-Nano-Settings-ex.png "caption" width=750 -In this example, the settings/about (accessed from "Settings"/"About" in Home, single level) consists in 4 pages, and can be seen as another use-case. +In this example, the settings (accessed from *Settings* in Home, single level) consists in 3 pages, and can be seen as another use-case. @section nbgl_use_cases Use Cases A few APIs are available to draw typical Use-Cases, such as: -- for Home Screen: - - @ref nbgl_useCaseHome() to draw the home screen of an application (see @subpage use_case_home) -- for Settings: - - @ref nbgl_useCaseSettings() to draw a level of settings pages (see @subpage use_case_settings) -- for most used reviews: - - @ref nbgl_useCaseStaticReview() to draw the pages of a regular review, when all info are available from the beginning (see @subpage use_case_static_review) - - @ref nbgl_useCaseRegularReview() to draw the pages of a regular review (all pages but the cover one) (see @subpage use_case_regular_review) +- For Home Screen & Settings: + - @ref nbgl_useCaseHomeAndSettings() to draw the home page and settings/info pages (see @subpage use_case_home_settings) +- For Individual pages: + - @ref nbgl_useCaseConfirm() to draw a typical confirmation page, for example when rejecting a transaction (see @subpage use_case_confirm) + - @ref nbgl_useCaseChoice() to draw a typical dual choice page (see @subpage use_case_choice) + - @ref nbgl_useCaseStatus() to draw a transient status page, without control, for example when a transaction is successfully signed (see @subpage use_case_status) + - @ref nbgl_useCaseSpinner() to draw an infinite spinner page (see @subpage use_case_spinner) +- For most used reviews: + - @ref nbgl_useCaseReview() to draw the pages of a regular coin transaction review, when all info are available from the beginning (see @subpage use_case_review) + - @ref nbgl_useCaseReviewLight() to draw the pages of a transaction review with a simple button confirmation, when all info are available from the beginning (see @subpage use_case_review_light) + - @ref nbgl_useCaseReviewStreamingStart() to draw the pages of a regular coin transaction review, when all info are not available from the beginning (see @subpage use_case_review_streaming) +- For address verification: + - @ref nbgl_useCaseAddressReview() to draw an address confirmation page, with a possibility to see some extra tag/value pairs (see @subpage use_case_addr_review) +- For keypad: + - @ref nbgl_useCaseKeypadPIN() to draw a default keypad implementation with hidden digits (see @subpage use_case_keypad) + - @ref nbgl_useCaseKeypadDigits() to draw a default keypad implementation, showing digits (see @subpage use_case_keypad) +- For generic navigable content: + - @ref nbgl_useCaseNavigableContent() to draw a level of generic content navigable pages + +@subsection use_case_home_settings Home & Settings screen Use Case + +\image{inline} html UseCase-Nano-Home.png "caption" width=1000 + +Ledger would like all application to have the same layout for home screen and settings/info, so the @ref nbgl_useCaseHomeAndSettings() function enables to +create such a set of page, the configurable parameters being: + +- The application name (appName) +- The application icon (appIcon) +- The tagline, a text under app name (if NULL, it will be " is ready") +- The callback when touching *quit application* button +- The settings pages description +- The info pages description -@subsection use_case_home Home screen Use Case +@code +extern const nbgl_icon_details_t *app_icon; -\image{inline} html UseCase-Nano-Home.png "caption" width=800 +enum { + SWITCH1_TOKEN = FIRST_USER_TOKEN, + SWITCH2_TOKEN +}; -Ledger would like all application to have the same home screen, so the @ref nbgl_useCaseHome() function enables to -create such a page, the only configurable parameters being: +static const nbgl_layoutSwitch_t switches[] = { + {.initState = false, + .text = "Dummy 1", + .subText = "Allow dummy 1\nin transactions", + .token = SWITCH1_TOKEN}, + {.initState = true, + .text = "Dummy 2", + .subText = "Allow dummy 2\nin transactions", + .token = SWITCH2_TOKEN} +}; +static const char *infoTypes[] = {"Version", "Developer"}; +static const char *infoContents[] = {"1.1.0", "Ledger"}; + +// function called in case of action on switches +static void controlsCallback(int token, uint8_t index, int page) { + if (token == SWITCH1_TOKEN) { + if (index == 0) { + // deactivate something related with Dummy1 + } + else { + // activate something related with Dummy1 + } + } + else if (token == SWITCH2_TOKEN) { + if (index == 0) { + // deactivate something related with Dummy2 + } + else { + // activate something related with Dummy2 + } + } +} -- the application name -- the application icon -- the application version -- the tagline, which is the text of the first page (if NULL, it will be the .") -- the callbacks when touching *quit* or *about* buttons -- the type of *about* button (about or settings) +static const nbgl_content_t contentsList = { + .content.switchesList.nbSwitches = 2, + .content.switchesList.switches = switches, + .type = SWITCHES_LIST, + .contentActionCallback = controlsCallback +}; -@code -extern const nbgl_icon_details_t *myAppIcon; +static const nbgl_genericContents_t settingContents = { + .contentsList = &contentsList, + .nbContents = 1 +}; +static const nbgl_contentInfoList_t infosList = { + .nbInfos = 2, + .infoTypes = infoTypes, + .infoContents = infoContents +}; -void onAbout(void) { - // draw settings page here -} void onQuit(void) { // exit app here } void appMain(void) { - nbgl_useCaseHome("MyApp", - &myAppIcon, - "1.0", // App version - NULL, // no tagline - false, // with settings button - onAbout, - onQuit); + nbgl_useCaseHomeAndSettings("NBGL Tests", + app_icon, + NULL, // use default tag line + INIT_HOME_PAGE, // start at home page + &esettingContents, // description of settings + &infosList, // description of app info + NULL, // no action button on home screen + onQuit); // when quitting } @endcode +@subsubsection use_case_home_settings_with_action Home & Settings screen with action button Use Case -@subsection use_case_settings Settings Use Case - -\image{inline} html UseCase-Nano-About1.png "caption" width=800 - -Usually settings (or information) of an application consists in a list of pages, each page containing either: +\image{inline} html UseCase-Nano-Settings-Action.png "caption" width=1000 -- a static information (version, developer) -- a changeable settings (boolean, list of values) +For some rare applications, one may need an action button in the Home screen, to perform either: -The API to initiate the display of the series of pages is @ref nbgl_useCaseSettings(), providing: +- The main action of the Application +- Or an side-action, as to display an address -- the page in which to start -- the number of pages -- a callback called when the "Back" page is double-pressed -- a navigation callback called when left or right buttons are pressed (and also to fill the initial page), to retrieve -the content of the page -- a last callback called when a page is selected (double-pressed) +The \b action argument of @ref nbgl_useCaseHomeAndSettings() can be used for that. This structure (@ref nbgl_homeAction_t) +enables to specify: -Here is the source code to display the example of settings: +- A text +- An optional icon +- A function to be called when the buttons are pressed -@code -typedef struct { - const char *text; ///< main text for the switch - nbgl_state_t initState; ///< initial state of the switch -} Switch_t; - -/********************** - * STATIC VARIABLES - **********************/ -static Switch_t switches[] = { - { - .initState = false, - .text = "Blind signing", - }, - { - .initState = true, - .text = "Debug", - }, - { - .initState = true, - .text = "Nonce", - }, -}; +@subsection use_case_confirm Confirmation Use Case -static bool navCallback(uint8_t page, nbgl_pageContent_t *content) -{ - if (page < 3) { - content->text = switches[page].text; - content->subText = switches[page].initState ? "Enabled" : "Disabled"; - } - else { - return false; - } - return true; -} +\image{inline} html UseCase-Nano-Confirm.png "caption" width=750 -static void actionCallback(uint8_t page) { - switches[page].initState = !switches[page].initState; - nbgl_useCaseSettings(page, 3, app_fullEthereum, navCallback, actionCallback); -} +A confirmation use-case consists in 3 modal pages sequence: -void myAppSettings(void) { - // draw the settings Use Case (3 pages), starting at page 0 - nbgl_useCaseSettings(0, // init page - 3, // number of pages - appMain, // "Back" callback - navCallback, // navigation callback, to get content of pages - actionCallback // action callback, when a page is selected - ); -} -@endcode +- 1st page containing a fixed icon, a configurable message +- 2nd page to confirm (pressing both buttons) +- 3rd page to reject (pressing both buttons) +The @ref nbgl_useCaseConfirm() function enables to create such a page. -@subsection use_case_regular_review Regular Review Use Case +The *callback* argument is called upon confirmation. -\image{inline} html UseCase-Nano-Review1.png "caption" width=1000 +When rejected, this modal screen is simply dismissed, revealing the previous page on background. -Usually transaction/message reviews consist in a sequence of pages, each page containing: +Here is the code to display the example picture (and a status page for confirmation) -- one tag/value pair page (sometimes with a multi-pages value) -- two selectable pages at the end, to accept or reject the transaction +@code +static void confirmationCallback(void) { + // draw a status screen which continues by returning to appMain +} -To navigate between pages, right and left buttons can be pressed. And the last two pages can be selected with a double-press. +void onRejectTransaction(void) { + nbgl_useCaseConfirm("Do you confirm\nThis message?", + "Confirm", + "Cancel", + confirmationCallback); +} +@endcode -The API to initiate the display of the series of pages is @ref nbgl_useCaseRegularReview(), providing: +@subsection use_case_choice Choice Use Case -- the page in which to start (usually 0) -- the number of pages (if unknown, set to 0) -- a navigation callback called when right or left button is pressed, to retrieve -the content of the previous/next page. It is also called to fill the initial page. +\image{inline} html UseCase-Nano-Choice.png "caption" width=800 -Here is the code to display something similar to the example picture: +A choice use-case consists in a 4-page sequence: -@code -// 2 pairs of tag/value to display -static nbgl_layoutTagValue_t pairs[2] = { - { - .item = "Address", - .value = "0xEAEAEAEAEAEAEAEAE" - }, - { - .item = "Amount", - .value = "BOL 0.666" - } -}; +- 1st page containing a configurable icon and a configurable title +- 2nd page containing a configurable message (text and subText) +- 3rd page to confirm, with a configurable message (pressing both buttons) +- 4th page to reject, with a configurable message (preesing both buttons) -static void onApprove(void) { - // confirm transaction +The @ref nbgl_useCaseChoice() function enables to create such a page. - // and go back to main - appMain(); -} +The *callback* argument is called when a choice is done. Its argument is a boolean which is *true* upon confirmation, else *false*. -static void onReject(void) { - // reject transaction +Here is the code to display the example picture - // and go back to main - appMain(); -} - -// called to get the content of the given page -static bool navCallback(uint8_t page, nbgl_pageContent_t *content) { - memset(content, 0, sizeof(nbgl_pageContent_t)); - if (page == 0) { - // the first page is used to display the title of the review - content->text = "Review\ntransaction"; - content->icon = &C_icon_eye; - } - else if (page < 3) { - // the 2 next pages contain the two pairs of tag/values - content->text = pairs[page - 1].item; - content->subText = pairs[page - 1].value; - } - else if (page == 3) { - // this page is for approval - content->text = "Approve"; - content->icon = &C_icon_validate; - content->callback = onApprove; - } - else if (page == 4) { - // this page is for rejection - content->text = "Reject"; - content->icon = &C_icon_crossmark; - content->callback = onReject; +@code +static void onChoice(bool confirm) { + if (confirm) { + // do something } else { - return false; + // do something } - // valid page so return true - return true; } -void startReview(void) { - // review on 3 pages, starting at 0 - nbgl_useCaseRegularReview(0, // start at first page - 5, // number of pages - navCallback); +void onRejectTransaction(void) { + nbgl_useCaseChoice(&C_icon_warning, + "Dummy 2", + "Are you sure to\nallow dummy 2\nin transactions?", + "I understand, confirm", + "Cancel", + onChoice); } @endcode -@subsection use_case_static_review Static Review Use Case +@subsection use_case_status Status Use Case + +\image{inline} html UseCase-Nano-Status.png "caption" width=600 + +A status is a transient page, without control, to display during a short time, for example when a transaction is successfully signed. +The @ref nbgl_useCaseStatus() function enables to create such a page, with the following arguments: + +- A message string to set in middle of page +- A boolean to indicate if true (to determine the icon) +- A quit callback, called when timer times out (or both buttons are pressed) -\image{inline} html UseCase-Nano-Review1.png "caption" width=1000 +@subsection use_case_review_status Pre-defined review status Use Case -In some cases, the developer may know all tag/value pairs of a transaction when it is submitted. -And he may want to use a more standardized transaction review Use Case, with less interaction. +\image{inline} html UseCase-Nano-Review-Status.png "caption" width=600 -In this case, what we call a "static" review can be used. It is similar to a regular review with known number of pages in terms of -presentation and interactions with end-users, but much easier to use for developer. +Similar as @subpage use_case_status, this is used to display transient page, without control, during a short time, for example when a transaction is successfully signed. +The @ref nbgl_useCaseReviewStatus() function enables to create such a page, with the following arguments: -Indeed, in this case, NBGL automatically computes the number of pages, and the pairs to draw in each page. -In case of a tag/value pair too long to be fully displayed, it is split in a multi-pages steps. +- A type of status (with predefined message and icon) +- A quit callback, called when timer times out (or both buttons are pressed) -The API to initiate the display of the series of pages is @ref nbgl_useCaseStaticReview(), providing: +@subsection use_case_review Review Use Case -- the list of tag/value pairs (or a callback to get them one by one) -- the text to use in first page -- an action callback called when a page's content is selectable (double-pressed) -- a callback called when the "accept" (with param equal to \b true) or "reject" (with param equal to \b false) last two pages is selected +\image{inline} html UseCase-Nano-Review.png "caption" height=300 + +In most cases, the developer may know all tag/value pairs of a transaction when it is submitted. + +Thus, the number of pages is computed automatically and pages can be navigated forward and backward. + +@note In case of a tag/value pair too long to be fully displayed, the value is automatically split on successive pages, adding an indication `(n/m)` in the name. + +At the end of the review flow, the user has 2 pages to *Approve* or *Reject* the transaction. +Finally, the given callback is called and it's up to app's developer to call @ref nbgl_useCaseReviewStatus(). + +The API to initiate the display of the series of pages is @ref nbgl_useCaseReview(), providing: + +- The type of operation to review (*transaction*, *message* or generic *operation*) +- The title and icon for the presentation page +- The list of tag/value pairs (or a callback to get them one by one) +- A callback called when the review is *Approved* or *Rejected*. The callback's param is *true* for approval, *false* for rejection. Here is the code to display something similar to example picture: @code -// 2 pairs of tag/value to display -static nbgl_layoutTagValue_t pairs[2] = { - { - .item = "Address", - .value = "0xEAEAEAEAEAEAEAEAE" - }, - { - .item = "Amount", - .value = "BOL 0.666" - } -}; +extern const nbgl_icon_details_t *app_icon; + +// pairs of tag/value to display +static nbgl_layoutTagValue_t pairs[2]; -static nbgl_contentTagValueList_t pairList = { - .nbMaxLinesForValue = 0, +static const nbgl_contentTagValueList_t pairList = { .nbPairs = 2, .pairs = (nbgl_layoutTagValue_t*)pairs }; -// called when "Approve" or "Reject" page is selected (double-press) -static void reviewChoice(bool confirm) { +// called when review is approved or rejected +static void review_choice(bool confirm) { + // display a status page and go back to main if (confirm) { - // confirm transaction + nbgl_useCaseReviewStatus(STATUS_TYPE_TRANSACTION_SIGNED, appMain); } else { - // reject transaction + nbgl_useCaseReviewStatus(STATUS_TYPE_TRANSACTION_REJECTED, appMain); } - // go back to main - appMain(); } -void startStaticReview(void) { - // static review, providing the whole list of pairs - nbgl_useCaseStaticReview(&pairList, - &C_icon_eye, - "Review\ntransaction", - "Approve", - "Reject", - reviewChoice); +void startReview(void) { + // Setup data to display + pairs[0].item = "Amount"; + pairs[0].value = "NBT 0.99"; + pairs[1].item = "Address"; + pairs[1].value = "0x1234567890"; + + // standard review, providing the whole list of pairs + nbgl_useCaseReview(TYPE_TRANSACTION, // type of operation + &pairList, // list of tag/value pairs + app_icon, // icon of the coin + "Review transaction\nto send NBT", // title of the first page + NULL, // sub-title of the first page + NULL, // title of the last page + review_choice); // callback on result of the review } @endcode Here is another version of the example code, using a callback mechanism to get tag/value pairs: @code +extern const nbgl_icon_details_t *app_icon; + // common tag/value pair to return static nbgl_layoutTagValue_t pair; static nbgl_layoutTagValue_t* getPair(uint8_t index); -static nbgl_contentTagValueList_t pairList = { - .nbMaxLinesForValue = 0, - .nbPairs = 5, +static const nbgl_contentTagValueList_t pairList = { + .nbPairs = 2, .pairs = NULL, // to indicate that callback should be used .callback = getPair, - .startIndex = 0 }; -// called when "Approve" or "Reject" page is selected (double-press) -static void reviewChoice(bool confirm) { +// called when review is approved or rejected +static void review_choice(bool confirm) { + // display a status page and go back to main if (confirm) { - // confirm transaction + nbgl_useCaseReviewStatus(STATUS_TYPE_TRANSACTION_SIGNED, appMain); } else { - // reject transaction + nbgl_useCaseReviewStatus(STATUS_TYPE_TRANSACTION_REJECTED, appMain); } - // go back to main - appMain(); } // function called by NBGL to get the pair indexed by "index" static nbgl_layoutTagValue_t* getPair(uint8_t index) { switch (index) { case 0: - pair.item = "To"; - pair.value = "0x123456"; - break; - case 1: - pair.item = "From"; - pair.value = "0x654321"; - break; - case 2: pair.item = "Amount"; - pair.value = "1.2345 BTC"; + pair.value = "NBT 0.99"; break; - case 3: - pair.item = "Fees"; - pair.value = "0.1 BTC"; + case 1: + pair.item = "Address"; + pair.value = "0x1234567890"; break; } return &pair; } -void startStaticReview(void) { - // static review, providing the description of pairs - nbgl_useCaseStaticReview(&pairList, - &C_icon_eye, - "Review\ntransaction", - "Approve", - "Reject", - reviewChoice); +void startReview(void) { + // standard review, providing the whole list of pairs + nbgl_useCaseReview(TYPE_TRANSACTION, // type of operation + &pairList, // list of tag/value pairs + app_icon, // icon of the coin + "Review transaction\nto send NBT", // title of the first page + NULL, // sub-title of the first page + NULL, // title of the last page + review_choice); // callback on result of the review +} +@endcode + +@subsection use_case_review_light Light review Use Case + +Not really useful on Nano devices, and thus, directly mapped to @subpage use_case_review. + +@subsection use_case_review_streaming Streaming review Use Case + +\image{inline} html UseCase-Nano-Streaming.png "caption" width=1000 + +In some cases, the application cannot know all tag/value pairs of a transaction when the review is started. + +In this case, what we call a *streaming* review can be used. The pages to display for each *stream* are computed automatically +and pages can be navigated forward and backward. + +In case of a tag/value pair too long to be fully displayed, the value is automatically split on +successive pages, adding an indication `(n/m)` in the name. + +At the end of the review flow, the user has 2 pages to *Approve* or *Reject* the transaction. +Finally, the given callback is called and it's up to app's developer to call @ref nbgl_useCaseReviewStatus(). + +The API to initiate the display of the series of pages is @ref nbgl_useCaseReviewStreamingStart(), providing: + +- The type of operation to review (transaction, message or generic operation) +- The title and icon for the presentation page +- A callback with one boolean parameter: + - If this parameter is *false*, it means that the transaction is rejected. + - If this parameter is *true*, it means that NBGL is waiting for new data, sent with @ref nbgl_useCaseReviewStreamingContinue() + +As long as there are new tag/value pairs to send, the API to call is @ref nbgl_useCaseReviewStreamingContinue(), providing: + +- The list of tag/value pairs (or a callback to get them one by one) +- A callback with one boolean parameter: + - If this parameter is *false*, it means that the transaction is rejected. + - If this parameter is *true*, it means that NBGL is waiting for new data, to be sent with @ref nbgl_useCaseReviewStreamingContinue() + +@note Considering *skip* is not available on Nano device, @ref nbgl_useCaseReviewStreamingContinueExt() is useless and directly mapped to @ref nbgl_useCaseReviewStreamingContinue() + +When there is no more data to send, the API to call is @ref nbgl_useCaseReviewStreamingFinish(), providing: + +- A callback called when the review is *Approved* or *Rejected*. The callback's param is *true* for approval, *false* for rejection. + +Here is the code to display something similar to example picture: + +@code +extern const nbgl_icon_details_t *app_icon; + +// called when long press button on last page is long-touched or when reject footer is touched +static void onReviewResult(bool confirm) { + // display a status page and go back to main + if (confirm) { + nbgl_useCaseReviewStatus(STATUS_TYPE_TRANSACTION_SIGNED, appMain); + } + else { + nbgl_useCaseReviewStatus(STATUS_TYPE_TRANSACTION_REJECTED, appMain); + } +} + +static void onTransactionContinue(bool askMore) +{ + if (askMore) { + nbgl_contentTagValueList_t pairsToSend; + // try to get more data + if (moreDataToSend(&pairsToSend)) { + nbgl_useCaseReviewStreamingContinue(&pairsToSend, onTransactionContinue); + } + else { + // all data sent, so finish + nbgl_useCaseReviewStreamingFinish("Sign transaction?", // title on last page + onReviewResult); // callback to handle reject/accept + } + } + else { + onReviewResult(false); + } +} + +void startReview(void) { + // initiate the streaming review + nbgl_useCaseReviewStreamingStart(TYPE_TRANSACTION, + app_icon, // icon on first page + "Review transaction\nto send NBT", // title of the first page + NULL, // sub-title + onTransactionContinue); // callback to reject or ask more data +} +@endcode + +@subsection use_case_addr_review Address Review Use Case + +\image{inline} html UseCase-Nano-AddressReview.png "caption" width=1500 + +When an address needs to be confirmed, it can be displayed in a Address Review Use Case. + +After a title page, a second page is displayed with the raw address (as text). + +Moreover, if extra information need to be displayed, in the form of tags/values, they are show in successive pages. + +@note In case of a tag/value pair too long to be fully displayed, the value is automatically split on successive pages, adding an indication `(n/m)` in the name. + + +At the end of the review flow, the user has 2 pages to *Approve* or *Reject* the review. +Finally, the given callback is called and it's up to app's developer to call @ref nbgl_useCaseReviewStatus(). + +The @ref nbgl_useCaseAddressReview() function enables to create such a set of pages, with the following parameters: + +- The address to confirm (NULL terminated string) +- A callback called when the review is *Approved* or *Rejected* +- The optional list of extra tag/value pairs + +Here is the code to display something similar to example picture: + +@code +// 2 pairs of tag/value to display in second page +static nbgl_layoutTagValue_t pairs[2]; + +static const nbgl_contentTagValueList_t pairList = { + .nbPairs = 2, + .pairs = (nbgl_layoutTagValue_t*)pairs +}; + +// called when either confirm button or reject token is called +static void review_choice(bool confirm) { + if (confirm) { + nbgl_useCaseReviewStatus(STATUS_TYPE_ADDRESS_VERIFIED, ui_main); + } + else { + nbgl_useCaseReviewStatus(STATUS_TYPE_ADDRESS_REJECTED, ui_main); + } +} + +void app_ethereumVerifyAddress(void) { + // Setup data to display + pairs[0].item = "Type address"; + pairs[0].value = "dummy type"; + pairs[1].item = "Sub address"; + pairs[1].value = "dummy sub address"; + + // Setup list + pairList.nbMaxLinesForValue = 0; + pairList.nbPairs = 2; + pairList.pairs = pairs; + + nbgl_useCaseAddressReview( + "5A8FgbMkmG2e3J41sBdjvjaBUyz8qHohsQcGtRf63qEUTMBvmA45fpp5pSacMdSg7A3b71RejLzB8EkGbfjp5PELVHCRUaE", + &pairList, + &ICON_APP, + "Verify NBT address", + NULL, + review_choice); } @endcode @subsection use_case_spinner Spinner Use Case -This Use Case is simply to display a static "waiting page" +\image{inline} html UseCase-Nano-Spinner.png "caption" width=300 + +This Use Case is simply to display a static *waiting page* -The @ref nbgl_useCaseSpinner() function enables to create such a page. +The @ref nbgl_useCaseSpinner() function enables to create such a page, without any parameters. + +@subsection use_case_keypad Keypad Use Case + +\image{inline} html UseCase-Nano-Keypad.png "caption" height=350 + +We have here 2 different variants, allowing to \b show or \b hide the entered digits. + +When a pincode is requested, a default keypad can be displayed, with hidden digits. + +The @ref nbgl_useCaseKeypadPIN() function enables to create such page, with the following parameters: + +- A title +- The min and max lengths +- A boolean to request a *shuffled* keypad +- Callbacks for navigation and pin validation + +The other variant, where digits don't need to be hidden is @ref nbgl_useCaseKeypadDigits(); +it takes the same parameters. + +@note The \em backspace and \em validate buttons will be shown or hidden automatically. +@note The \em backspace button is also used as a *cancel* button, when no digits are selected. +@note The max nb of supported digits is \b 12. + +Here is the code to display something similar to example picture: + +@code +static void validate_pin(const uint8_t *pinentry, uint8_t length) { + // Code to validate the entered pin code +} + +static void quit_cb() { + // Back to the main app menu +} + +void ui_menu_pinentry_display(unsigned int value) { + // Draw the keypad + nbgl_useCaseKeypadPIN("PIN Keypad (5555)", + 4, + 4, + false, + validate_pin, + quit_cb); +} +@endcode @section use_case_refresh_page Refreshing screen diff --git a/lib_nbgl/doc/resources/UseCase-Nano-About1.png b/lib_nbgl/doc/resources/UseCase-Nano-About1.png deleted file mode 100755 index 86ffb0442..000000000 Binary files a/lib_nbgl/doc/resources/UseCase-Nano-About1.png and /dev/null differ diff --git a/lib_nbgl/doc/resources/UseCase-Nano-AddressReview.png b/lib_nbgl/doc/resources/UseCase-Nano-AddressReview.png new file mode 100644 index 000000000..5726cb4c3 Binary files /dev/null and b/lib_nbgl/doc/resources/UseCase-Nano-AddressReview.png differ diff --git a/lib_nbgl/doc/resources/UseCase-Nano-Choice.png b/lib_nbgl/doc/resources/UseCase-Nano-Choice.png new file mode 100644 index 000000000..7f1d32e98 Binary files /dev/null and b/lib_nbgl/doc/resources/UseCase-Nano-Choice.png differ diff --git a/lib_nbgl/doc/resources/UseCase-Nano-Confirm.png b/lib_nbgl/doc/resources/UseCase-Nano-Confirm.png new file mode 100644 index 000000000..332c13ac3 Binary files /dev/null and b/lib_nbgl/doc/resources/UseCase-Nano-Confirm.png differ diff --git a/lib_nbgl/doc/resources/UseCase-Nano-Home.png b/lib_nbgl/doc/resources/UseCase-Nano-Home.png old mode 100755 new mode 100644 index 981cbd26b..227a9d80a Binary files a/lib_nbgl/doc/resources/UseCase-Nano-Home.png and b/lib_nbgl/doc/resources/UseCase-Nano-Home.png differ diff --git a/lib_nbgl/doc/resources/UseCase-Nano-Keypad.png b/lib_nbgl/doc/resources/UseCase-Nano-Keypad.png new file mode 100644 index 000000000..2ba4bdd63 Binary files /dev/null and b/lib_nbgl/doc/resources/UseCase-Nano-Keypad.png differ diff --git a/lib_nbgl/doc/resources/UseCase-Nano-Review-Status.png b/lib_nbgl/doc/resources/UseCase-Nano-Review-Status.png new file mode 100644 index 000000000..462cf63e7 Binary files /dev/null and b/lib_nbgl/doc/resources/UseCase-Nano-Review-Status.png differ diff --git a/lib_nbgl/doc/resources/UseCase-Nano-Review-ex.png b/lib_nbgl/doc/resources/UseCase-Nano-Review-ex.png new file mode 100644 index 000000000..09aa91f55 Binary files /dev/null and b/lib_nbgl/doc/resources/UseCase-Nano-Review-ex.png differ diff --git a/lib_nbgl/doc/resources/UseCase-Nano-Review.png b/lib_nbgl/doc/resources/UseCase-Nano-Review.png new file mode 100644 index 000000000..b8d86bdcb Binary files /dev/null and b/lib_nbgl/doc/resources/UseCase-Nano-Review.png differ diff --git a/lib_nbgl/doc/resources/UseCase-Nano-Review1.png b/lib_nbgl/doc/resources/UseCase-Nano-Review1.png deleted file mode 100755 index f0afae8ac..000000000 Binary files a/lib_nbgl/doc/resources/UseCase-Nano-Review1.png and /dev/null differ diff --git a/lib_nbgl/doc/resources/UseCase-Nano-Settings-Action.png b/lib_nbgl/doc/resources/UseCase-Nano-Settings-Action.png new file mode 100644 index 000000000..b96ff40f8 Binary files /dev/null and b/lib_nbgl/doc/resources/UseCase-Nano-Settings-Action.png differ diff --git a/lib_nbgl/doc/resources/UseCase-Nano-Settings-ex.png b/lib_nbgl/doc/resources/UseCase-Nano-Settings-ex.png new file mode 100644 index 000000000..06b5b6f10 Binary files /dev/null and b/lib_nbgl/doc/resources/UseCase-Nano-Settings-ex.png differ diff --git a/lib_nbgl/doc/resources/UseCase-Nano-Spinner.png b/lib_nbgl/doc/resources/UseCase-Nano-Spinner.png new file mode 100644 index 000000000..85826dccd Binary files /dev/null and b/lib_nbgl/doc/resources/UseCase-Nano-Spinner.png differ diff --git a/lib_nbgl/doc/resources/UseCase-Nano-Status.png b/lib_nbgl/doc/resources/UseCase-Nano-Status.png new file mode 100644 index 000000000..e2fc0ce6c Binary files /dev/null and b/lib_nbgl/doc/resources/UseCase-Nano-Status.png differ diff --git a/lib_nbgl/doc/resources/UseCase-Nano-Streaming.png b/lib_nbgl/doc/resources/UseCase-Nano-Streaming.png new file mode 100644 index 000000000..b6df3b5fe Binary files /dev/null and b/lib_nbgl/doc/resources/UseCase-Nano-Streaming.png differ diff --git a/lib_nbgl/doc/resources/UseCase-Review-With-Warning.png b/lib_nbgl/doc/resources/UseCase-Review-With-Warning.png new file mode 100755 index 000000000..7829ed476 Binary files /dev/null and b/lib_nbgl/doc/resources/UseCase-Review-With-Warning.png differ diff --git a/lib_nbgl/glyphs/32px/Exclamation_32px.png b/lib_nbgl/glyphs/32px/Exclamation_32px.png new file mode 100755 index 000000000..1eee85008 Binary files /dev/null and b/lib_nbgl/glyphs/32px/Exclamation_32px.png differ diff --git a/lib_nbgl/glyphs/32px/Privacy_32px.png b/lib_nbgl/glyphs/32px/Privacy_32px.png new file mode 100755 index 000000000..700f60a21 Binary files /dev/null and b/lib_nbgl/glyphs/32px/Privacy_32px.png differ diff --git a/lib_nbgl/glyphs/40px/Exclamation_40px.png b/lib_nbgl/glyphs/40px/Exclamation_40px.png new file mode 100755 index 000000000..37f8b56a1 Binary files /dev/null and b/lib_nbgl/glyphs/40px/Exclamation_40px.png differ diff --git a/lib_nbgl/glyphs/40px/Important_Circle_40px.png b/lib_nbgl/glyphs/40px/Important_Circle_40px.png new file mode 100755 index 000000000..5037b98d5 Binary files /dev/null and b/lib_nbgl/glyphs/40px/Important_Circle_40px.png differ diff --git a/lib_nbgl/glyphs/40px/Privacy_40px.png b/lib_nbgl/glyphs/40px/Privacy_40px.png new file mode 100755 index 000000000..1664a2088 Binary files /dev/null and b/lib_nbgl/glyphs/40px/Privacy_40px.png differ diff --git a/lib_nbgl/include/nbgl_content.h b/lib_nbgl/include/nbgl_content.h index 5dcbb47d2..47ad63ca9 100644 --- a/lib_nbgl/include/nbgl_content.h +++ b/lib_nbgl/include/nbgl_content.h @@ -123,8 +123,10 @@ typedef struct { * */ typedef enum { - ENS_ALIAS = 0, ///< alias comes from ENS - ADDRESS_BOOK_ALIAS ///< alias comes from Address Book + NO_ALIAS_TYPE = 0, + ENS_ALIAS, ///< alias comes from ENS + ADDRESS_BOOK_ALIAS, ///< alias comes from Address Book + QR_CODE_ALIAS ///< alias is an address to be displayed as a QR Code } nbgl_contentValueAliasType_t; /** @@ -134,7 +136,9 @@ typedef enum { typedef struct { const char *fullValue; ///< full string of the value when used as an alias const char *explanation; ///< string displayed in gray, explaing where the alias comes from - ///< if NULL, a default explanation is provided, depending of the type + ///< only used if aliasType is @ref NO_ALIAS_TYPE + const char *title; ///< if not NULL and aliasType is @ref QR_CODE_ALIAS, is used as title of + ///< the QR Code nbgl_contentValueAliasType_t aliasType; ///< type of alias } nbgl_contentValueExt_t; @@ -264,7 +268,14 @@ typedef struct nbgl_pageSwitchesList_s { typedef struct { const char *const *infoTypes; ///< array of types of infos (in black/bold) const char *const *infoContents; ///< array of contents of infos (in black) - uint8_t nbInfos; ///< number of elements in infoTypes and infoContents array + const nbgl_contentValueExt_t + *infoExtensions; ///< if not NULL, gives additional info on type field + ///< any {0} element of this array is considered as invalid + uint8_t nbInfos; ///< number of elements in infoTypes and infoContents array + uint8_t token; ///< token to use with extensions, if withExtensions is true and infoExtensions + ///< is not NULL + bool withExtensions; /// if set to TRUE and if infoExtensions is not NULL, use this + /// infoExtensions field } nbgl_contentInfoList_t; /** diff --git a/lib_nbgl/include/nbgl_flow.h b/lib_nbgl/include/nbgl_flow.h index 142e3b2bd..3143d7846 100644 --- a/lib_nbgl/include/nbgl_flow.h +++ b/lib_nbgl/include/nbgl_flow.h @@ -51,7 +51,22 @@ typedef struct nbgl_stepDesc_s { #endif // HAVE_LANGUAGE_PACK } nbgl_stepDesc_t; -typedef nbgl_stepDesc_t nbgl_pageContent_t; +/** + * @brief This structure contains data to build a page in multi-pages mode (@ref + * nbgl_pageDrawGenericContent) + */ +typedef struct nbgl_pageContent_s { + nbgl_contentType_t type; ///< type of page content in the following union + union { + nbgl_contentCenteredInfo_t centeredInfo; ///< @ref CENTERED_INFO type + nbgl_contentInfoButton_t infoButton; ///< @ref INFO_BUTTON type + nbgl_contentTagValueList_t tagValueList; ///< @ref TAG_VALUE_LIST type + nbgl_contentSwitchesList_t switchesList; ///< @ref SWITCHES_LIST type + nbgl_contentInfoList_t infosList; ///< @ref INFOS_LIST type + nbgl_contentRadioChoice_t choicesList; ///< @ref CHOICES_LIST type + nbgl_contentBarsList_t barsList; ///< @ref BARS_LIST type + }; +} nbgl_pageContent_t; /********************** * GLOBAL PROTOTYPES diff --git a/lib_nbgl/include/nbgl_layout.h b/lib_nbgl/include/nbgl_layout.h index 0fd7dca6d..3a18e93b3 100644 --- a/lib_nbgl/include/nbgl_layout.h +++ b/lib_nbgl/include/nbgl_layout.h @@ -593,6 +593,11 @@ int nbgl_layoutAddTopRightButton(nbgl_layout_t *layout, int nbgl_layoutAddTouchableBar(nbgl_layout_t *layout, const nbgl_layoutBar_t *barLayout); int nbgl_layoutAddSwitch(nbgl_layout_t *layout, const nbgl_layoutSwitch_t *switchLayout); int nbgl_layoutAddText(nbgl_layout_t *layout, const char *text, const char *subText); +int nbgl_layoutAddTextWithAlias(nbgl_layout_t *layout, + const char *text, + const char *subText, + uint8_t token, + uint8_t index); int nbgl_layoutAddSubHeaderText(nbgl_layout_t *layout, const char *text); int nbgl_layoutAddRadioChoice(nbgl_layout_t *layout, const nbgl_layoutRadioChoice_t *choices); int nbgl_layoutAddQRCode(nbgl_layout_t *layout, const nbgl_layoutQRCode_t *info); @@ -738,6 +743,14 @@ int nbgl_layoutUpdateKeypad(nbgl_layout_t *layout, bool enableBackspace); int nbgl_layoutAddHiddenDigits(nbgl_layout_t *layout, uint8_t nbDigits); int nbgl_layoutUpdateHiddenDigits(nbgl_layout_t *layout, uint8_t index, uint8_t nbActive); +int nbgl_layoutAddKeypadContent(nbgl_layout_t *layout, + bool hidden, + uint8_t nbDigits, + const char *text); +int nbgl_layoutUpdateKeypadContent(nbgl_layout_t *layout, + bool hidden, + uint8_t nbActiveDigits, + const char *text); #endif // HAVE_SE_TOUCH #endif // NBGL_KEYPAD diff --git a/lib_nbgl/include/nbgl_obj.h b/lib_nbgl/include/nbgl_obj.h index 75f0c516b..d4d730795 100644 --- a/lib_nbgl/include/nbgl_obj.h +++ b/lib_nbgl/include/nbgl_obj.h @@ -56,11 +56,11 @@ extern "C" { #else #define KEYPAD_KEY_HEIGHT 104 #endif -#define KEYPAD_MAX_DIGITS 12 #else // HAVE_SE_TOUCH #define KEYPAD_WIDTH 114 #define KEYPAD_HEIGHT 18 #endif // HAVE_SE_TOUCH +#define KEYPAD_MAX_DIGITS 12 #ifdef HAVE_SE_TOUCH // external margin in pixels @@ -127,6 +127,9 @@ extern "C" { #define QRCODE_ICON C_QRCode_32px #define MINI_PUSH_ICON C_Mini_Push_32px #define WARNING_ICON C_Warning_32px +#define ROUND_WARN_ICON C_Important_Circle_32px +#define PRIVACY_ICON C_Privacy_32px +#define EXCLAMATION_ICON C_Exclamation_32px #else // TARGET_STAX #define SPACE_ICON C_Space_40px #define BACKSPACE_ICON C_Erase_40px @@ -146,6 +149,9 @@ extern "C" { #define QRCODE_ICON C_QRCode_40px #define MINI_PUSH_ICON C_Mini_Push_40px #define WARNING_ICON C_Warning_40px +#define ROUND_WARN_ICON C_Important_Circle_40px +#define PRIVACY_ICON C_Privacy_40px +#define EXCLAMATION_ICON C_Exclamation_40px #endif // TARGET_STAX // For backward compatibility, to be remove later @@ -195,7 +201,7 @@ typedef enum { * @brief prototype of function to be called when a button event is received by an object (TODO: * change to screen?) * @param obj the concerned object - * @param buttonState event on buttons + * @param buttonEvent event on buttons */ typedef void (*nbgl_buttonCallback_t)(void *obj, nbgl_buttonEvent_t buttonEvent); diff --git a/lib_nbgl/include/nbgl_step.h b/lib_nbgl/include/nbgl_step.h index 38fdd27a0..10a50e2e7 100644 --- a/lib_nbgl/include/nbgl_step.h +++ b/lib_nbgl/include/nbgl_step.h @@ -54,6 +54,7 @@ typedef void (*nbgl_stepMenuListCallback_t)(uint8_t choiceIndex); /** * @brief prototype of function to be called when buttons are touched on a screen + * @param stepCtx context returned by the nbgl_stepDrawXXX function * @param event type of button event */ typedef void (*nbgl_stepButtonCallback_t)(nbgl_step_t stepCtx, nbgl_buttonEvent_t event); diff --git a/lib_nbgl/include/nbgl_types.h b/lib_nbgl/include/nbgl_types.h index c994d0632..d39f30123 100644 --- a/lib_nbgl/include/nbgl_types.h +++ b/lib_nbgl/include/nbgl_types.h @@ -309,8 +309,6 @@ typedef enum { * Same as POST_REFRESH_FORCE_POWER_ON, but with pipeline enabled. * When using pipeline, refreshes are faster, but some constraints must be respected: successive * draws & refreshes areas must not overlap. - * If overlapping is needed, the function @ref nbgl_driver_waitPipeline() must be called, in order - * to wait for pipelined operations to finish. */ typedef enum nbgl_post_refresh_t { POST_REFRESH_FORCE_POWER_OFF, ///< Force screen power off after refresh diff --git a/lib_nbgl/include/nbgl_use_case.h b/lib_nbgl/include/nbgl_use_case.h index 3d56a73f1..136209aa5 100644 --- a/lib_nbgl/include/nbgl_use_case.h +++ b/lib_nbgl/include/nbgl_use_case.h @@ -130,10 +130,10 @@ typedef void (*nbgl_actionCallback_t)(uint8_t page); /** * @brief prototype of pin validation callback function - * @param content pin value - * @param length pin length + * @param pin pin value + * @param pinLen pin length */ -typedef void (*nbgl_pinValidCallback_t)(const uint8_t *content, uint8_t page); +typedef void (*nbgl_pinValidCallback_t)(const uint8_t *pin, uint8_t pinLen); /** * @brief prototype of content navigation callback function @@ -184,10 +184,91 @@ typedef struct { const char *modalTitle; ///< title given to modal window displayed when tip-box is touched nbgl_contentType_t type; ///< type of page content in the following union union { - const nbgl_contentInfoList_t infos; ///< infos pairs displayed in modal. + nbgl_contentInfoList_t infos; ///< infos pairs displayed in modal. }; } nbgl_tipBox_t; +/** + * @brief The different types of warning page contents + * + */ +typedef enum { + CENTERED_INFO_WARNING = 0, ///< Centered info + QRCODE_WARNING, ///< QR Code + BAR_LIST_WARNING ///< list of touchable bars, to display sub-pages +} nbgl_warningDetailsType_t; + +/** + * @brief The necessary parameters to build a list of touchable bars, to display sub-pages + * + */ +typedef struct { + uint8_t nbBars; ///< number of touchable bars + const char *const *texts; ///< array of texts for each bar (nbBars items, in black/bold) + const char *const *subTexts; ///< array of texts for each bar (nbBars items, in black) + const nbgl_icon_details_t **icons; ///< array of icons for each bar (nbBars items) + const struct nbgl_warningDetails_s + *details; ///< array of nbBars structures giving what to display when each bar is touched. +} nbgl_warningBarList_t; + +/** + * @brief The necessary parameters to build the page(s) displayed when the top-right button is + * touched in intro page (before review) + * + */ +typedef struct nbgl_warningDetails_s { + const char *title; ///< text of the page (used to go back) + nbgl_warningDetailsType_t + type; ///< type of content in the page, determining what to use in the following union + union { + nbgl_contentCenter_t + centeredInfo; ///< centered info, if type == @ref CENTERED_INFO_WARNING +#ifdef NBGL_QRCODE + nbgl_layoutQRCode_t qrCode; ///< QR code, if type == @ref QRCODE_WARNING +#endif // NBGL_QRCODE + nbgl_warningBarList_t barList; ///< touchable bars list, if type == @ref BAR_LIST_WARNING + }; +} nbgl_warningDetails_t; + +/** + * @brief The different types of pre-defined warnings + * + */ +typedef enum { + BLIND_SIGNING_WARN = 0, ///< Blind signing + W3C_ISSUE_WARN, ///< Web3 Checks issue + W3C_LOSING_SWAP_WARN, ///< Web3 Checks: Losing Swap risk + W3C_THREAT_DETECTED_WARN, ///< Web3 Checks: Thread detexted, malicious (know drainer) + NB_WARNING_TYPES +} nbgl_warningType_t; + +/** + * @brief The necessary parameters to build a warning page preceding a review. + * One can either use `predefinedSet` when the warnings are already supported in @ref + * nbgl_warningType_t list, or use `introDetails` or `reviewDetails` to configure manually the + * warning pages + * @note it's also used to build the modal pages displayed when touching the top-right button in the + * review's first page + * + */ +typedef struct { + uint32_t predefinedSet; ///< bitfield of pre-defined warnings, to be taken in @ref + ///< nbgl_warningType_t set it to 0 if not using pre-defined warnings + const char *reportProvider; ///< name of the security report provider, used in some strings + const nbgl_warningDetails_t + *introDetails; ///< details displayed when top-right button is touched in intro page + ///< (before review) if using pre-defined configuration, set to NULL, + const nbgl_warningDetails_t + *reviewDetails; ///< details displayed when top-right button is touched in first/last page + ///< of review if using pre-defined configuration, set to NULL + const nbgl_contentCenter_t + *info; ///< parameters to build the intro warning page, if not using pre-defined + const nbgl_icon_details_t + *introTopRightIcon; ///< icon to use in the intro warning page, if not using pre-defined + const nbgl_icon_details_t *reviewTopRightIcon; ///< icon to use in the first/last page of + ///< review, if not using pre-defined +} nbgl_warning_t; + /** * @brief The different types of operation to review * @@ -265,7 +346,15 @@ void nbgl_useCaseReviewBlindSigning(nbgl_operationType_t operationT const char *finishTitle, const nbgl_tipBox_t *tipBox, nbgl_choiceCallback_t choiceCallback); - +void nbgl_useCaseReviewWithWarning(nbgl_operationType_t operationType, + const nbgl_contentTagValueList_t *tagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + const char *finishTitle, + const nbgl_tipBox_t *tipBox, + const nbgl_warning_t *warning, + nbgl_choiceCallback_t choiceCallback); void nbgl_useCaseAdvancedReview(nbgl_operationType_t operationType, const nbgl_contentTagValueList_t *tagValueList, const nbgl_icon_details_t *icon, @@ -305,6 +394,13 @@ void nbgl_useCaseReviewStreamingBlindSigningStart(nbgl_operationType_t ope const char *reviewSubTitle, nbgl_choiceCallback_t choiceCallback); +void nbgl_useCaseReviewStreamingWithWarningStart(nbgl_operationType_t operationType, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + const nbgl_warning_t *warning, + nbgl_choiceCallback_t choiceCallback); + void nbgl_useCaseReviewStreamingContinueExt(const nbgl_contentTagValueList_t *tagValueList, nbgl_choiceCallback_t choiceCallback, nbgl_callback_t skipCallback); @@ -324,6 +420,12 @@ void nbgl_useCaseGenericConfiguration(const char *title, const nbgl_genericContents_t *contents, nbgl_callback_t quitCallback); +void nbgl_useCaseGenericSettings(const char *appName, + uint8_t initPage, + const nbgl_genericContents_t *settingContents, + const nbgl_contentInfoList_t *infosList, + nbgl_callback_t quitCallback); + void nbgl_useCaseChoice(const nbgl_icon_details_t *icon, const char *message, const char *subMessage, @@ -333,9 +435,20 @@ void nbgl_useCaseChoice(const nbgl_icon_details_t *icon, void nbgl_useCaseStatus(const char *message, bool isSuccess, nbgl_callback_t quitCallback); +void nbgl_useCaseConfirm(const char *message, + const char *subMessage, + const char *confirmText, + const char *rejectText, + nbgl_callback_t callback); void nbgl_useCaseSpinner(const char *text); -#ifdef HAVE_SE_TOUCH +void nbgl_useCaseNavigableContent(const char *title, + uint8_t initPage, + uint8_t nbPages, + nbgl_callback_t quitCallback, + nbgl_navCallback_t navCallback, + nbgl_layoutTouchCallback_t controlsCallback); + // utils uint8_t nbgl_useCaseGetNbTagValuesInPage(uint8_t nbPairs, const nbgl_contentTagValueList_t *tagValueList, @@ -364,62 +477,56 @@ uint8_t nbgl_useCaseGetNbChoicesInPage(uint8_t nbChoice bool withNav); uint8_t nbgl_useCaseGetNbPagesForTagValueList(const nbgl_contentTagValueList_t *tagValueList); +#ifdef HAVE_SE_TOUCH // use case drawing -void nbgl_useCaseHome(const char *appName, - const nbgl_icon_details_t *appIcon, - const char *tagline, - bool withSettings, - nbgl_callback_t topRightCallback, - nbgl_callback_t quitCallback); -void nbgl_useCaseHomeExt(const char *appName, - const nbgl_icon_details_t *appIcon, - const char *tagline, - bool withSettings, - const char *actionButtonText, - nbgl_callback_t actionCallback, - nbgl_callback_t topRightCallback, - nbgl_callback_t quitCallback); -void nbgl_useCaseSettings(const char *settingsTitle, - uint8_t initPage, - uint8_t nbPages, - bool touchableTitle, - nbgl_callback_t quitCallback, - nbgl_navCallback_t navCallback, - nbgl_layoutTouchCallback_t controlsCallback); -void nbgl_useCaseGenericSettings(const char *appName, - uint8_t initPage, - const nbgl_genericContents_t *settingContents, - const nbgl_contentInfoList_t *infosList, - nbgl_callback_t quitCallback); -void nbgl_useCaseConfirm(const char *message, - const char *subMessage, - const char *confirmText, - const char *rejectText, - nbgl_callback_t callback); -void nbgl_useCaseReviewStart(const nbgl_icon_details_t *icon, - const char *reviewTitle, - const char *reviewSubTitle, - const char *rejectText, - nbgl_callback_t continueCallback, - nbgl_callback_t rejectCallback); -void nbgl_useCaseRegularReview(uint8_t initPage, - uint8_t nbPages, - const char *rejectText, - nbgl_layoutTouchCallback_t buttonCallback, - nbgl_navCallback_t navCallback, - nbgl_choiceCallback_t choiceCallback); -void nbgl_useCaseStaticReview(const nbgl_contentTagValueList_t *tagValueList, - const nbgl_pageInfoLongPress_t *infoLongPress, - const char *rejectText, - nbgl_choiceCallback_t callback); -void nbgl_useCaseStaticReviewLight(const nbgl_contentTagValueList_t *tagValueList, - const nbgl_pageInfoLongPress_t *infoLongPress, - const char *rejectText, - nbgl_choiceCallback_t callback); -void nbgl_useCaseAddressConfirmation(const char *address, nbgl_choiceCallback_t callback); -void nbgl_useCaseAddressConfirmationExt(const char *address, - nbgl_choiceCallback_t callback, - const nbgl_contentTagValueList_t *tagValueList); +DEPRECATED void nbgl_useCaseHome(const char *appName, + const nbgl_icon_details_t *appIcon, + const char *tagline, + bool withSettings, + nbgl_callback_t topRightCallback, + nbgl_callback_t quitCallback); +DEPRECATED void nbgl_useCaseHomeExt(const char *appName, + const nbgl_icon_details_t *appIcon, + const char *tagline, + bool withSettings, + const char *actionButtonText, + nbgl_callback_t actionCallback, + nbgl_callback_t topRightCallback, + nbgl_callback_t quitCallback); +DEPRECATED void nbgl_useCaseSettings(const char *settingsTitle, + uint8_t initPage, + uint8_t nbPages, + bool touchableTitle, + nbgl_callback_t quitCallback, + nbgl_navCallback_t navCallback, + nbgl_layoutTouchCallback_t controlsCallback); +void nbgl_useCaseReviewStart(const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + const char *rejectText, + nbgl_callback_t continueCallback, + nbgl_callback_t rejectCallback); +void nbgl_useCaseRegularReview(uint8_t initPage, + uint8_t nbPages, + const char *rejectText, + nbgl_layoutTouchCallback_t buttonCallback, + nbgl_navCallback_t navCallback, + nbgl_choiceCallback_t choiceCallback); +void nbgl_useCaseStaticReview(const nbgl_contentTagValueList_t *tagValueList, + const nbgl_pageInfoLongPress_t *infoLongPress, + const char *rejectText, + nbgl_choiceCallback_t callback); +void nbgl_useCaseStaticReviewLight(const nbgl_contentTagValueList_t *tagValueList, + const nbgl_pageInfoLongPress_t *infoLongPress, + const char *rejectText, + nbgl_choiceCallback_t callback); + +DEPRECATED void nbgl_useCaseAddressConfirmationExt(const char *address, + nbgl_choiceCallback_t callback, + const nbgl_contentTagValueList_t *tagValueList); +#define nbgl_useCaseAddressConfirmation(__address, __callback) \ + nbgl_useCaseAddressConfirmationExt(__address, __callback, NULL) + #ifdef NBGL_KEYPAD void nbgl_useCaseKeypadDigits(const char *title, uint8_t minDigits, @@ -438,6 +545,22 @@ void nbgl_useCaseKeypadPIN(const char *title, nbgl_pinValidCallback_t validatePinCallback, nbgl_layoutTouchCallback_t actionCallback); #endif // NBGL_KEYPAD + +#else // HAVE_SE_TOUCH +#ifdef NBGL_KEYPAD +void nbgl_useCaseKeypadDigits(const char *title, + uint8_t minDigits, + uint8_t maxDigits, + bool shuffled, + nbgl_pinValidCallback_t validatePinCallback, + nbgl_callback_t backCallbackk); +void nbgl_useCaseKeypadPIN(const char *title, + uint8_t minDigits, + uint8_t maxDigits, + bool shuffled, + nbgl_pinValidCallback_t validatePinCallback, + nbgl_callback_t backCallback); +#endif // NBGL_KEYPAD #endif // HAVE_SE_TOUCH #ifdef __cplusplus diff --git a/lib_nbgl/src/nbgl_layout.c b/lib_nbgl/src/nbgl_layout.c index 179716f86..6d0907d0d 100644 --- a/lib_nbgl/src/nbgl_layout.c +++ b/lib_nbgl/src/nbgl_layout.c @@ -878,6 +878,110 @@ static nbgl_container_t *addContentCenter(nbgl_layoutInternal_t *layoutInt, return container; } +static int addText(nbgl_layout_t *layout, + const char *text, + const char *subText, + uint8_t token, + uint8_t index, + bool withAlias) +{ + nbgl_layoutInternal_t *layoutInt = (nbgl_layoutInternal_t *) layout; + nbgl_container_t *container; + nbgl_text_area_t *textArea; + nbgl_text_area_t *subTextArea; + uint16_t fullHeight = 0; + + if (layout == NULL) { + return -1; + } + container = (nbgl_container_t *) nbgl_objPoolGet(CONTAINER, layoutInt->layer); + + // get container children + container->children = nbgl_containerPoolGet(withAlias ? 3 : 2, layoutInt->layer); + container->obj.area.width = AVAILABLE_WIDTH; + + if (text != NULL) { + textArea = (nbgl_text_area_t *) nbgl_objPoolGet(TEXT_AREA, layoutInt->layer); + + textArea->textColor = BLACK; + textArea->text = PIC(text); + textArea->textAlignment = MID_LEFT; + textArea->fontId = SMALL_BOLD_FONT; + textArea->style = NO_STYLE; + textArea->wrapping = true; + textArea->obj.alignment = NO_ALIGNMENT; + textArea->obj.alignmentMarginY = PRE_TEXT_MARGIN; + textArea->obj.area.width = container->obj.area.width; + if (withAlias == true) { + textArea->obj.area.width -= 12 + MINI_PUSH_ICON.width; + } + textArea->obj.area.height = nbgl_getTextHeightInWidth( + textArea->fontId, textArea->text, textArea->obj.area.width, textArea->wrapping); + fullHeight += textArea->obj.area.height + textArea->obj.alignmentMarginY; + container->children[container->nbChildren] = (nbgl_obj_t *) textArea; + container->nbChildren++; + if (withAlias == true) { + nbgl_image_t *image = (nbgl_image_t *) nbgl_objPoolGet(IMAGE, layoutInt->layer); + // the whole container is touchable + layoutObj_t *obj + = layoutAddCallbackObj(layoutInt, (nbgl_obj_t *) container, token, TUNE_TAP_CASUAL); + obj->index = index; + image->foregroundColor = BLACK; + image->buffer = &PUSH_ICON; + image->obj.alignment = RIGHT_TOP; + image->obj.alignmentMarginX = 12; + image->obj.alignTo = (nbgl_obj_t *) textArea; + container->obj.touchMask = (1 << TOUCHED); + container->obj.touchId = VALUE_BUTTON_1_ID + index; + + container->children[container->nbChildren] = (nbgl_obj_t *) image; + container->nbChildren++; + } + } + if (subText != NULL) { + subTextArea = (nbgl_text_area_t *) nbgl_objPoolGet(TEXT_AREA, layoutInt->layer); + subTextArea->textColor = BLACK; + subTextArea->text = PIC(subText); + subTextArea->fontId = SMALL_REGULAR_FONT; + subTextArea->style = NO_STYLE; + subTextArea->wrapping = true; + subTextArea->obj.area.width = container->obj.area.width; + subTextArea->obj.area.height = nbgl_getTextHeightInWidth(subTextArea->fontId, + subTextArea->text, + subTextArea->obj.area.width, + subTextArea->wrapping); + subTextArea->textAlignment = MID_LEFT; + subTextArea->obj.alignment = NO_ALIGNMENT; + if (text != NULL) { + subTextArea->obj.alignmentMarginY = TEXT_SUBTEXT_MARGIN; + fullHeight += POST_SUBTEXT_MARGIN; // under the subText + } + else { +#ifdef TARGET_STAX + subTextArea->obj.alignmentMarginY = BORDER_MARGIN; + fullHeight += BORDER_MARGIN; +#else // TARGET_STAX + subTextArea->obj.alignmentMarginY = 26; + fullHeight += 26; // under the subText +#endif // TARGET_STAX + } + container->children[container->nbChildren] = (nbgl_obj_t *) subTextArea; + container->nbChildren++; + fullHeight += subTextArea->obj.area.height + subTextArea->obj.alignmentMarginY; + } + else { + fullHeight += PRE_TEXT_MARGIN; + } + container->obj.area.height = fullHeight; + container->layout = VERTICAL; + container->obj.alignmentMarginX = BORDER_MARGIN; + container->obj.alignment = NO_ALIGNMENT; + // set this new obj as child of main container + layoutAddObject(layoutInt, (nbgl_obj_t *) container); + + return container->obj.area.height; +} + /********************** * GLOBAL FUNCTIONS **********************/ @@ -1187,7 +1291,7 @@ int nbgl_layoutAddSwitch(nbgl_layout_t *layout, const nbgl_layoutSwitch_t *switc } /** - * @brief Creates an area with given text and sub text (in gray) + * @brief Creates an area with given text (in bold) and sub text (in regular) * * @param layout the current layout * @param text main text (in small bold font), optional @@ -1196,82 +1300,29 @@ int nbgl_layoutAddSwitch(nbgl_layout_t *layout, const nbgl_layoutSwitch_t *switc */ int nbgl_layoutAddText(nbgl_layout_t *layout, const char *text, const char *subText) { - nbgl_layoutInternal_t *layoutInt = (nbgl_layoutInternal_t *) layout; - nbgl_container_t *container; - nbgl_text_area_t *textArea; - nbgl_text_area_t *subTextArea; - uint16_t fullHeight = 0; - LOG_DEBUG(LAYOUT_LOGGER, "nbgl_layoutAddText():\n"); - if (layout == NULL) { - return -1; - } - container = (nbgl_container_t *) nbgl_objPoolGet(CONTAINER, layoutInt->layer); - - // get container children - container->children = nbgl_containerPoolGet(2, layoutInt->layer); - container->obj.area.width = AVAILABLE_WIDTH; - - if (text != NULL) { - textArea = (nbgl_text_area_t *) nbgl_objPoolGet(TEXT_AREA, layoutInt->layer); - - textArea->textColor = BLACK; - textArea->text = PIC(text); - textArea->textAlignment = MID_LEFT; - textArea->fontId = SMALL_BOLD_FONT; - textArea->style = NO_STYLE; - textArea->wrapping = true; - textArea->obj.alignment = NO_ALIGNMENT; - textArea->obj.alignmentMarginY = PRE_TEXT_MARGIN; - textArea->obj.area.width = container->obj.area.width; - textArea->obj.area.height = nbgl_getTextHeightInWidth( - textArea->fontId, textArea->text, textArea->obj.area.width, textArea->wrapping); - fullHeight += textArea->obj.area.height + textArea->obj.alignmentMarginY; - container->children[container->nbChildren] = (nbgl_obj_t *) textArea; - container->nbChildren++; - } - if (subText != NULL) { - subTextArea = (nbgl_text_area_t *) nbgl_objPoolGet(TEXT_AREA, layoutInt->layer); - subTextArea->textColor = BLACK; - subTextArea->text = PIC(subText); - subTextArea->fontId = SMALL_REGULAR_FONT; - subTextArea->style = NO_STYLE; - subTextArea->wrapping = true; - subTextArea->obj.area.width = container->obj.area.width; - subTextArea->obj.area.height = nbgl_getTextHeightInWidth(subTextArea->fontId, - subTextArea->text, - subTextArea->obj.area.width, - subTextArea->wrapping); - subTextArea->textAlignment = MID_LEFT; - subTextArea->obj.alignment = NO_ALIGNMENT; - if (text != NULL) { - subTextArea->obj.alignmentMarginY = TEXT_SUBTEXT_MARGIN; - fullHeight += POST_SUBTEXT_MARGIN; // under the subText - } - else { -#ifdef TARGET_STAX - subTextArea->obj.alignmentMarginY = BORDER_MARGIN; - fullHeight += BORDER_MARGIN; -#else // TARGET_STAX - subTextArea->obj.alignmentMarginY = 26; - fullHeight += 26; // under the subText -#endif // TARGET_STAX - } - container->children[container->nbChildren] = (nbgl_obj_t *) subTextArea; - container->nbChildren++; - fullHeight += subTextArea->obj.area.height + subTextArea->obj.alignmentMarginY; - } - else { - fullHeight += PRE_TEXT_MARGIN; - } - container->obj.area.height = fullHeight; - container->layout = VERTICAL; - container->obj.alignmentMarginX = BORDER_MARGIN; - container->obj.alignment = NO_ALIGNMENT; - // set this new obj as child of main container - layoutAddObject(layoutInt, (nbgl_obj_t *) container); + return addText(layout, text, subText, 0, 0, false); +} - return container->obj.area.height; +/** + * @brief Creates an area with given text (in bold) and sub text (in regular), with a + * > icon on right of text to activate an action when touched, with the given token + * + * @param layout the current layout + * @param text main text (in small bold font), optional + * @param subText description under main text (in small regular font), optional + * @param token token to use in callback when > icon is touched + * @param index index to use in callback when > icon is touched + * @return height of the control if OK + */ +int nbgl_layoutAddTextWithAlias(nbgl_layout_t *layout, + const char *text, + const char *subText, + uint8_t token, + uint8_t index) +{ + LOG_DEBUG(LAYOUT_LOGGER, "nbgl_layoutAddTextWithAlias():\n"); + return addText(layout, text, subText, token, index, true); } /** diff --git a/lib_nbgl/src/nbgl_layout_keypad_nanos.c b/lib_nbgl/src/nbgl_layout_keypad_nanos.c index 877a00b3f..24b29e8b0 100644 --- a/lib_nbgl/src/nbgl_layout_keypad_nanos.c +++ b/lib_nbgl/src/nbgl_layout_keypad_nanos.c @@ -53,7 +53,6 @@ * @param layout the current layout * @param callback function called when any of the key is touched * @param text text to use as title for the keypad - * @param textId ID of the text to use as title for the keypad * @param shuffled if set to true, digits are shuffled in keypad * @return the index of keypad in layout, to use in @ref nbgl_layoutUpdateKeypad() */ @@ -136,8 +135,8 @@ int nbgl_layoutUpdateKeypad(nbgl_layout_t *layout, // if validate key is enabled and was not, select it directly keypad->selectedKey = 11; } - else { - // otherwise let the draw function pick a new selected + // Shuffle if selected key was not backspace or if the last pin entry has been deleted + else if ((keypad->selectedKey != 0) || (keypad->enableBackspace && !enableBackspace)) { keypad->selectedKey = 0xFF; } keypad->enableValidate = enableValidate; @@ -160,109 +159,188 @@ int nbgl_layoutUpdateKeypad(nbgl_layout_t *layout, * @return the index of digits set, to use in @ref nbgl_layoutUpdateHiddenDigits() */ int nbgl_layoutAddHiddenDigits(nbgl_layout_t *layout, uint8_t nbDigits) +{ + return nbgl_layoutAddKeypadContent(layout, true, nbDigits, NULL); +} + +/** + * @brief Updates an existing set of hidden digits, with the given configuration + * + * @param layout the current layout + * @param index index returned by @ref nbgl_layoutAddHiddenDigits() + * @param nbActive number of "active" digits (represented by discs instead of circles) + * @return >=0 if OK + */ +int nbgl_layoutUpdateHiddenDigits(nbgl_layout_t *layout, uint8_t index, uint8_t nbActive) +{ + UNUSED(index); + if (nbgl_layoutUpdateKeypadContent(layout, true, nbActive, NULL) < 0) { + return -1; + } + return 0; +} + +/** + * @brief Adds an area with a title and a placeholder for hidden digits on top of a keypad, to + * represent the entered digits as small discs. + * + * @note It must be the only object added in the layout, after the keypad itself + * + * @param layout the current layout + * @param hidden if set to true, digits appear as discs, otherwise as visible digits (given in text + * param) + * @param nbDigits number of digits to be displayed (only used if hidden is true) + * @param text only used if hidden is false + * @return the height of this area, if no error, < 0 otherwise + */ +int nbgl_layoutAddKeypadContent(nbgl_layout_t *layout, + bool hidden, + uint8_t nbDigits, + const char *text) { nbgl_layoutInternal_t *layoutInt = (nbgl_layoutInternal_t *) layout; nbgl_container_t *container; + nbgl_text_area_t *textArea; - LOG_DEBUG(LAYOUT_LOGGER, "nbgl_layoutAddHiddenDigits():\n"); + LOG_DEBUG(LAYOUT_LOGGER, "nbgl_layoutAddKeypadContent():\n"); if (layout == NULL) { return -1; } - // create a container, invisible or bordered - container = (nbgl_container_t *) nbgl_objPoolGet(CONTAINER, layoutInt->layer); - container->nbChildren = nbDigits; - container->children = nbgl_containerPoolGet(container->nbChildren, layoutInt->layer); - // 1 pixel between each icon (knowing that the effective bullets are 8px large) - container->obj.area.width = nbDigits * C_pin_bullet_empty.width + (nbDigits - 1); - container->obj.area.height = C_pin_bullet_empty.height; - // distance from top to digits is fixed to 24 px - container->obj.alignmentMarginY = 24; - container->obj.alignTo = NULL; - container->obj.alignment = TOP_MIDDLE; - - // set this new container as child of the main container - layoutAddObject(layoutInt, (nbgl_obj_t *) container); - - // create children of the container, as images (empty circles) - nbgl_objPoolGetArray(IMAGE, nbDigits, layoutInt->layer, (nbgl_obj_t **) container->children); - for (int i = 0; i < nbDigits; i++) { - nbgl_image_t *image = (nbgl_image_t *) container->children[i]; - image->buffer = &C_pin_bullet_empty; - image->foregroundColor = WHITE; - if (i > 0) { - image->obj.alignment = MID_RIGHT; - image->obj.alignTo = (nbgl_obj_t *) container->children[i - 1]; - image->obj.alignmentMarginX = 1; - } - else { - image->obj.alignment = NO_ALIGNMENT; + if (hidden) { + // create a container, invisible + container = (nbgl_container_t *) nbgl_objPoolGet(CONTAINER, layoutInt->layer); + container->nbChildren = nbDigits; + container->children = nbgl_containerPoolGet(container->nbChildren, layoutInt->layer); + // 1 pixel between each icon (knowing that the effective bullets are 8px large) + container->obj.area.width = nbDigits * C_pin_bullet_empty.width + (nbDigits - 1); + container->obj.area.height = C_pin_bullet_empty.height; + // distance from top to digits is fixed to 24 px + container->obj.alignmentMarginY = 24; + container->obj.alignTo = NULL; + container->obj.alignment = TOP_MIDDLE; + + // set this new container as child of the main container + layoutAddObject(layoutInt, (nbgl_obj_t *) container); + + // create children of the container, as images (empty circles) + nbgl_objPoolGetArray( + IMAGE, nbDigits, layoutInt->layer, (nbgl_obj_t **) container->children); + for (int i = 0; i < nbDigits; i++) { + nbgl_image_t *image = (nbgl_image_t *) container->children[i]; + image->buffer = &C_pin_bullet_empty; + image->foregroundColor = WHITE; + if (i > 0) { + image->obj.alignment = MID_RIGHT; + image->obj.alignTo = (nbgl_obj_t *) container->children[i - 1]; + image->obj.alignmentMarginX = 1; + } + else { + image->obj.alignment = NO_ALIGNMENT; + } } } - // return index of container to be modified later on - return (layoutInt->nbChildren - 1); + else { + // create text area + textArea = (nbgl_text_area_t *) nbgl_objPoolGet(TEXT_AREA, layoutInt->layer); + textArea->textColor = WHITE; + textArea->text = text; + textArea->textAlignment = CENTER; + textArea->fontId = BAGL_FONT_OPEN_SANS_EXTRABOLD_11px_1bpp; + textArea->obj.area.width = AVAILABLE_WIDTH; + textArea->obj.area.height = nbgl_getFontLineHeight(textArea->fontId); + textArea->autoHideLongLine = true; + // distance from top to digits is fixed to 20 px + textArea->obj.alignmentMarginY = 20; + textArea->obj.alignTo = NULL; + textArea->obj.alignment = TOP_MIDDLE; + // set this new text area as child of the main container + layoutAddObject(layoutInt, (nbgl_obj_t *) textArea); + } + + // return 0 + return 0; } /** * @brief Updates an existing set of hidden digits, with the given configuration * * @param layout the current layout - * @param index index returned by @ref nbgl_layoutAddHiddenDigits() - * @param nbActive number of "active" digits (represented by discs instead of circles) + * @param hidden if set to true, digits appear as discs, otherwise as visible digits (given in text + * param) + * @param nbActiveDigits number of "active" digits (represented by discs instead of circles) (only + * used if hidden is true) + * @param text only used if hidden is false + * * @return >=0 if OK */ -int nbgl_layoutUpdateHiddenDigits(nbgl_layout_t *layout, uint8_t index, uint8_t nbActive) +int nbgl_layoutUpdateKeypadContent(nbgl_layout_t *layout, + bool hidden, + uint8_t nbActiveDigits, + const char *text) { nbgl_layoutInternal_t *layoutInt = (nbgl_layoutInternal_t *) layout; nbgl_container_t *container; nbgl_image_t *image; - LOG_DEBUG(LAYOUT_LOGGER, "nbgl_layoutUpdateHiddenDigits(): nbActive = %d\n", nbActive); + LOG_DEBUG(LAYOUT_LOGGER, "nbgl_layoutUpdateHiddenDigits(): nbActive = %d\n", nbActiveDigits); if (layout == NULL) { return -1; } - // get container - container = (nbgl_container_t *) layoutInt->children[index]; - // sanity check - if ((container == NULL) || (container->obj.type != CONTAINER)) { - return -1; - } - if (nbActive > container->nbChildren) { - return -1; - } - if (nbActive == 0) { - // deactivate the first digit - image = (nbgl_image_t *) container->children[0]; - if ((image == NULL) || (image->obj.type != IMAGE)) { + if (hidden) { + // get container (3rd child of main container) + container = (nbgl_container_t *) layoutInt->children[2]; + // sanity check + if ((container == NULL) || (container->obj.type != CONTAINER)) { return -1; } - image->buffer = &C_pin_bullet_empty; - } - else { - image = (nbgl_image_t *) container->children[nbActive - 1]; - if ((image == NULL) || (image->obj.type != IMAGE)) { + if (nbActiveDigits > container->nbChildren) { return -1; } - // if the last "active" is already active, it means that we are decreasing the number of - // active otherwise we are increasing it - if (image->buffer == &C_pin_bullet_filled) { - // all digits are already active - if (nbActive == container->nbChildren) { - return 0; + if (nbActiveDigits == 0) { + // deactivate the first digit + image = (nbgl_image_t *) container->children[0]; + if ((image == NULL) || (image->obj.type != IMAGE)) { + return -1; } - // deactivate the next digit - image = (nbgl_image_t *) container->children[nbActive]; image->buffer = &C_pin_bullet_empty; } else { - image->buffer = &C_pin_bullet_filled; + image = (nbgl_image_t *) container->children[nbActiveDigits - 1]; + if ((image == NULL) || (image->obj.type != IMAGE)) { + return -1; + } + // if the last "active" is already active, it means that we are decreasing the number of + // active otherwise we are increasing it + if (image->buffer == &C_pin_bullet_filled) { + // all digits are already active + if (nbActiveDigits == container->nbChildren) { + return 0; + } + // deactivate the next digit + image = (nbgl_image_t *) container->children[nbActiveDigits]; + image->buffer = &C_pin_bullet_empty; + } + else { + image->buffer = &C_pin_bullet_filled; + } } - } - nbgl_objDraw((nbgl_obj_t *) image); + nbgl_objDraw((nbgl_obj_t *) image); + } + else { + // update main text area (second child of the main container) + nbgl_text_area_t *textArea = (nbgl_text_area_t *) layoutInt->children[2]; + if ((textArea == NULL) || (textArea->obj.type != TEXT_AREA)) { + return -1; + } + textArea->text = text; + nbgl_objDraw((nbgl_obj_t *) textArea); + } return 0; } + #endif // NBGL_KEYPAD #endif // HAVE_SE_TOUCH diff --git a/lib_nbgl/src/nbgl_obj_keypad_nanos.c b/lib_nbgl/src/nbgl_obj_keypad_nanos.c index 75dd229d8..20f98414e 100644 --- a/lib_nbgl/src/nbgl_obj_keypad_nanos.c +++ b/lib_nbgl/src/nbgl_obj_keypad_nanos.c @@ -165,24 +165,8 @@ static void keypadInitSelected(nbgl_keypad_t *keypad) keypad->selectedKey = 1 + INIT_DIGIT_VALUE; } else { - uint8_t nbChoices = 10; - uint8_t random; - if (keypad->enableBackspace) { - nbChoices++; - } - if (keypad->enableValidate) { - nbChoices++; - } - random = cx_rng_u32_range(0, nbChoices); - if (random < 10) { - keypad->selectedKey = 1 + random; - } - else if (random == 10) { - keypad->selectedKey = 0; - } - else if (random == 11) { - keypad->selectedKey = 11; - } + // Exclude backspace and validate keys ([0,11]), shuffle only digits + keypad->selectedKey = cx_rng_u32_range(1, 11); } } diff --git a/lib_nbgl/src/nbgl_page.c b/lib_nbgl/src/nbgl_page.c index 25f0a2a05..f0e037f8b 100644 --- a/lib_nbgl/src/nbgl_page.c +++ b/lib_nbgl/src/nbgl_page.c @@ -199,8 +199,22 @@ static void addContent(nbgl_pageContent_t *content, case INFOS_LIST: { uint8_t i; for (i = 0; i < content->infosList.nbInfos; i++) { - availableHeight -= nbgl_layoutAddText( - layout, content->infosList.infoTypes[i], content->infosList.infoContents[i]); + // if the extension is valid for this index, use a Text with Alias + if ((content->infosList.withExtensions == true) + && (content->infosList.infoExtensions != NULL) + && (content->infosList.infoExtensions[i].fullValue != NULL)) { + availableHeight + -= nbgl_layoutAddTextWithAlias(layout, + content->infosList.infoTypes[i], + content->infosList.infoContents[i], + content->infosList.token, + i); + } + else { + availableHeight -= nbgl_layoutAddText(layout, + content->infosList.infoTypes[i], + content->infosList.infoContents[i]); + } // do not draw a separation line if too low in the container if (availableHeight > 10) { nbgl_layoutAddSeparationLine(layout); diff --git a/lib_nbgl/src/nbgl_step.c b/lib_nbgl/src/nbgl_step.c index 774fb0a6a..95e4f9118 100644 --- a/lib_nbgl/src/nbgl_step.c +++ b/lib_nbgl/src/nbgl_step.c @@ -251,6 +251,10 @@ static void displayTextPage(StepContext_t *ctx, uint8_t textPage) BAGL_FONT_OPEN_SANS_REGULAR_11px_1bpp, txt, AVAILABLE_WIDTH, nbLines, &len, true); // memorize next position to save processing ctx->textContext.nextPageStart = txt + len; + // if the next char is '\n', skip it to avoid starting with an empty line + if (*ctx->textContext.nextPageStart == '\n') { + ctx->textContext.nextPageStart++; + } } else { ctx->textContext.nextPageStart = NULL; @@ -309,7 +313,9 @@ static void actionCallback(nbgl_layout_t *layout, nbgl_buttonEvent_t event) } else if ((ctx->textContext.pos == LAST_STEP) || (ctx->textContext.pos == NEITHER_FIRST_NOR_LAST_STEP)) { - ctx->textContext.onActionCallback((nbgl_step_t) ctx, event); + if (ctx->textContext.onActionCallback != NULL) { + ctx->textContext.onActionCallback((nbgl_step_t) ctx, event); + } } } else if (event == BUTTON_RIGHT_PRESSED) { @@ -319,11 +325,15 @@ static void actionCallback(nbgl_layout_t *layout, nbgl_buttonEvent_t event) } else if ((ctx->textContext.pos == FIRST_STEP) || (ctx->textContext.pos == NEITHER_FIRST_NOR_LAST_STEP)) { - ctx->textContext.onActionCallback((nbgl_step_t) ctx, event); + if (ctx->textContext.onActionCallback != NULL) { + ctx->textContext.onActionCallback((nbgl_step_t) ctx, event); + } } } else if (event == BUTTON_BOTH_PRESSED) { - ctx->textContext.onActionCallback((nbgl_step_t) ctx, event); + if (ctx->textContext.onActionCallback != NULL) { + ctx->textContext.onActionCallback((nbgl_step_t) ctx, event); + } } } @@ -526,9 +536,12 @@ nbgl_step_t nbgl_stepDrawCenteredInfo(nbgl_stepPosition_t pos, // initialize context (already set to 0 by getFreeContext()) ctx->textContext.onActionCallback = onActionCallback; if (ticker) { - ctx->ticker.tickerCallback = ticker->tickerCallback; - ctx->ticker.tickerIntervale = ticker->tickerIntervale; - ctx->ticker.tickerValue = ticker->tickerValue; + ctx->ticker.tickerCallback = ticker->tickerCallback; + ctx->ticker.tickerIntervale = ticker->tickerIntervale; + ctx->ticker.tickerValue = ticker->tickerValue; + layoutDescription.ticker.tickerCallback = ticker->tickerCallback; + layoutDescription.ticker.tickerIntervale = ticker->tickerIntervale; + layoutDescription.ticker.tickerValue = ticker->tickerValue; } ctx->textContext.nbPages = 1; diff --git a/lib_nbgl/src/nbgl_use_case.c b/lib_nbgl/src/nbgl_use_case.c index bb77a2e50..8beba798f 100644 --- a/lib_nbgl/src/nbgl_use_case.c +++ b/lib_nbgl/src/nbgl_use_case.c @@ -16,7 +16,6 @@ #include "os_pic.h" #include "os_print.h" #include "os_helpers.h" -#include "decorators.h" /********************* * DEFINES @@ -49,9 +48,18 @@ #define QRCODE_REDUCED_ADDR_LEN 128 // macros to ease access to shared contexts -#define keypadContext sharedContext.keypad -#define addressConfirmationContext sharedContext.addressConfirmation -#define blindSigningContext sharedContext.blindSigning +#define keypadContext sharedContext.keypad +#define reviewWithWarnCtx sharedContext.reviewWithWarning + +/* max length of the string displaying the description of the Web3 Checks report */ +#define W3C_DESCRIPTION_MAX_LEN 128 + +/** + * @brief This is to use in @ref nbgl_operationType_t when the operation is concerned by an internal + * warning This is used to indicate a warning with a top-right button in review first & last page + * + */ +#define RISKY_OPERATION (1 << 6) /********************** * TYPEDEFS @@ -70,8 +78,15 @@ enum { CONFIRM_TOKEN, REJECT_TOKEN, VALUE_ALIAS_TOKEN, + INFO_ALIAS_TOKEN, BLIND_WARNING_TOKEN, - TIP_BOX_TOKEN + WARNING_BUTTON_TOKEN, + TIP_BOX_TOKEN, + WARNING_CHOICE_TOKEN, + DISMISS_QR_TOKEN, + DISMISS_WARNING_TOKEN, + FIRST_WARN_BAR_TOKEN, + LAST_WARN_BAR_TOKEN = (FIRST_WARN_BAR_TOKEN + NB_WARNING_TYPES - 1), }; typedef enum { @@ -92,7 +107,7 @@ typedef struct DetailsContext_s { typedef struct AddressConfirmationContext_s { nbgl_layoutTagValue_t tagValuePair; - nbgl_layout_t modalLayout; + nbgl_layout_t *modalLayout; } AddressConfirmationContext_t; #ifdef NBGL_KEYPAD @@ -115,18 +130,21 @@ typedef struct BlindSigningContext_s { const char *reviewSubTitle; const char *finishTitle; const nbgl_tipBox_t *tipBox; + const nbgl_warning_t *warning; nbgl_choiceCallback_t choiceCallback; nbgl_layout_t *layoutCtx; -} BlindSigningContext_t; + nbgl_layout_t *modalLayout; + uint8_t securityReportLevel; // level 1 is the first level of menus + bool isIntro; // set to true during intro (before actual review) +} ReviewWithWarningContext_t; // this union is intended to save RAM for context storage -// indeed, these three contexts cannot happen simultaneously +// indeed, these 2 contexts cannot happen simultaneously typedef union { #ifdef NBGL_KEYPAD KeypadContext_t keypad; #endif - AddressConfirmationContext_t addressConfirmation; - BlindSigningContext_t blindSigning; + ReviewWithWarningContext_t reviewWithWarning; } SharedContext_t; typedef struct { @@ -144,7 +162,7 @@ typedef struct { nbgl_contentTagValueCallback_t currentCallback; // to be used to retrieve the pairs with value alias - nbgl_layout_t modalLayout; + nbgl_layout_t *modalLayout; } GenericContext_t; typedef struct { @@ -176,12 +194,18 @@ typedef union { nbgl_reviewStreamingContext_t reviewStreaming; } nbgl_BundleNavContext_t; +typedef struct { + const nbgl_icon_details_t *icon; + const char *text; + const char *subText; +} SecurityReportItem_t; + /********************** * STATIC VARIABLES **********************/ // char buffers to build some strings -static char appDescription[APP_DESCRIPTION_MAX_LEN]; +static char tmpString[W3C_DESCRIPTION_MAX_LEN]; // multi-purposes callbacks static nbgl_callback_t onQuit; @@ -217,6 +241,9 @@ static DetailsContext_t detailsContext; // multi-purpose context shared for non-concurrent usages static SharedContext_t sharedContext; +// context for address review +static AddressConfirmationContext_t addressConfirmationContext; + // contexts for generic navigation static GenericContext_t genericContext; static nbgl_content_t @@ -241,6 +268,25 @@ static const uint8_t nbMaxElementsPerContentType[] = { 5, // BARS_LIST (computed dynamically) }; +static const SecurityReportItem_t securityReportItems[NB_WARNING_TYPES] = { + [BLIND_SIGNING_WARN] = {.icon = &WARNING_ICON, + .text = "Blind signing", + .subText = "This transaction cannot be fully decoded." }, + [W3C_ISSUE_WARN] = {.icon = &ROUND_WARN_ICON, + .text = "Web3 Checks issue", + .subText = "Web3 Checks could not verify this transaction." }, + [W3C_LOSING_SWAP_WARN] = {.icon = &ROUND_WARN_ICON, + .text = "Risk detected", + .subText = "Web3 Checks found a risk:\nLosing swap" }, + [W3C_THREAT_DETECTED_WARN] + = {.icon = &ROUND_WARN_ICON, + .text = "Threat detected", + .subText = "This transaction was scanned as malicious by Web3 Checks."} +}; + +// configuration of warning when using @ref nbgl_useCaseReviewBlindSigning() +static const nbgl_warning_t blindSigningWarning = {.predefinedSet = (1 << BLIND_SIGNING_WARN)}; + #ifdef NBGL_QRCODE /* buffer to store reduced address under QR Code */ static char reducedAddress[QRCODE_REDUCED_ADDR_LEN]; @@ -251,15 +297,15 @@ static char reducedAddress[QRCODE_REDUCED_ADDR_LEN]; **********************/ static void displayReviewPage(uint8_t page, bool forceFullRefresh); static void displayDetailsPage(uint8_t page, bool forceFullRefresh); -static void displayFullValuePage(const nbgl_contentTagValue_t *pair); -static void displayBlindWarning(void); +static void displayFullValuePage(const char *backText, + const char *aliasText, + const nbgl_contentValueExt_t *extension); static void displayTipBoxModal(void); static void displaySettingsPage(uint8_t page, bool forceFullRefresh); static void displayGenericContextPage(uint8_t pageIdx, bool forceFullRefresh); static void pageCallback(int token, uint8_t index); #ifdef NBGL_QRCODE static void displayAddressQRCode(void); -static void addressLayoutTouchCallbackQR(int token, uint8_t index); #endif // NBGL_QRCODE static void modalLayoutTouchCallback(int token, uint8_t index); static void displaySkipWarning(void); @@ -269,7 +315,9 @@ static void bundleNavStartSettingsAtPage(uint8_t initSettingPage); static void bundleNavStartSettings(void); static void bundleNavReviewStreamingChoice(bool confirm); -static void blindSigningWarning(void); +static void displaySecurityReport(uint32_t set); +static void displayCustomizedSecurityReport(const nbgl_warningDetails_t *details); +static void displayInitialWarning(void); static void useCaseReview(nbgl_operationType_t operationType, const nbgl_contentTagValueList_t *tagValueList, const nbgl_icon_details_t *icon, @@ -444,6 +492,14 @@ static const char *getRejectReviewText(nbgl_operationType_t operationType) static void pageModalCallback(int token, uint8_t index) { LOG_DEBUG(USE_CASE_LOGGER, "pageModalCallback, token = %d, index = %d\n", token, index); + + if (token == INFO_ALIAS_TOKEN) { + // the icon next to info type alias has been touched + displayFullValuePage(tipBoxInfoList.infoTypes[index], + tipBoxInfoList.infoContents[index], + &tipBoxInfoList.infoExtensions[index]); + return; + } nbgl_pageRelease(modalPageContext); modalPageContext = NULL; if (token == NAV_TOKEN) { @@ -580,10 +636,29 @@ static void pageCallback(int token, uint8_t index) else { pair = genericContext.currentCallback(genericContext.currentElementIdx + index); } - displayFullValuePage(pair); + displayFullValuePage(pair->item, pair->value, pair->extension); } else if (token == BLIND_WARNING_TOKEN) { - displayBlindWarning(); + reviewWithWarnCtx.isIntro = false; + reviewWithWarnCtx.warning = NULL; + displaySecurityReport(1 << BLIND_SIGNING_WARN); + } + else if (token == WARNING_BUTTON_TOKEN) { + // top-right button, display the security modal + reviewWithWarnCtx.securityReportLevel = 1; + // if preset is used + if (reviewWithWarnCtx.warning->predefinedSet) { + displaySecurityReport(reviewWithWarnCtx.warning->predefinedSet); + } + else { + // use customized warning + if (reviewWithWarnCtx.isIntro) { + displayCustomizedSecurityReport(reviewWithWarnCtx.warning->introDetails); + } + else { + displayCustomizedSecurityReport(reviewWithWarnCtx.warning->reviewDetails); + } + } } else if (token == TIP_BOX_TOKEN) { displayTipBoxModal(); @@ -714,7 +789,9 @@ static const nbgl_content_t *genericContextComputeNextPageParams(uint8_t // Handle case where the content to be displayed is in the next content // In such case start at element index 0. - if (nextElementIdx >= genericContext.currentContentElementNb) { + // If currentContentElementNb == 0, means not initialized, so skip + if ((nextElementIdx >= genericContext.currentContentElementNb) + && (genericContext.currentContentElementNb > 0)) { nextContentIdx += 1; nextElementIdx = 0; } @@ -958,16 +1035,24 @@ static bool genericContextPreparePageContent(const nbgl_content_t *p_content, bool isFirstOrLastPage = ((p_content->type == CENTERED_INFO) || (p_content->type == EXTENDED_CENTER)) || (p_content->type == INFO_LONG_PRESS); - bool isStreamingNavAndBlindOperation + nbgl_operationType_t operationType = (navType == STREAMING_NAV) - && (bundleNavContext.reviewStreaming.operationType & BLIND_OPERATION); - bool isGenericNavAndBlindOperation - = (navType == GENERIC_NAV) && (bundleNavContext.review.operationType & BLIND_OPERATION); + ? bundleNavContext.reviewStreaming.operationType + : ((navType == GENERIC_NAV) ? bundleNavContext.review.operationType : 0); - // if first or last page of review and blind operation, add the warning top-right button - if (isFirstOrLastPage && (isStreamingNavAndBlindOperation || isGenericNavAndBlindOperation)) { - pageContent->topRightIcon = &WARNING_ICON; - pageContent->topRightToken = BLIND_WARNING_TOKEN; + // if first or last page of review and blind/risky operation, add the warning top-right button + if (isFirstOrLastPage && (operationType & (BLIND_OPERATION | RISKY_OPERATION))) { + // if issue is only Web3Checks issue, use '!' icon + if ((operationType & RISKY_OPERATION) + && (reviewWithWarnCtx.warning->predefinedSet == (1 << W3C_ISSUE_WARN))) { + pageContent->topRightIcon = &EXCLAMATION_ICON; + } + else { + pageContent->topRightIcon = &WARNING_ICON; + } + + pageContent->topRightToken + = (operationType & BLIND_OPERATION) ? BLIND_WARNING_TOKEN : WARNING_BUTTON_TOKEN; } return true; @@ -1133,7 +1218,9 @@ static void displayDetailsPage(uint8_t detailsPage, bool forceFullRefresh) } // function used to display the content of a full value, when touching an alias of a tag/value pair -static void displayFullValuePage(const nbgl_contentTagValue_t *pair) +static void displayFullValuePage(const char *backText, + const char *aliasText, + const nbgl_contentValueExt_t *extension) { nbgl_layoutDescription_t layoutDescription = {.modal = true, .withLeftBorder = true, @@ -1143,60 +1230,38 @@ static void displayFullValuePage(const nbgl_contentTagValue_t *pair) .separationLine = false, .backAndText.token = 0, .backAndText.tuneId = TUNE_TAP_CASUAL, - .backAndText.text = PIC(pair->item)}; - const char *info; - genericContext.modalLayout = nbgl_layoutGet(&layoutDescription); + .backAndText.text = PIC(backText)}; + genericContext.modalLayout = nbgl_layoutGet(&layoutDescription); // add header with the tag part of the pair, to go back nbgl_layoutAddHeader(genericContext.modalLayout, &headerDesc); - // add full value text - if (pair->extension->explanation == NULL) { - if (pair->extension->aliasType == ENS_ALIAS) { + // add either QR Code or full value text + if (extension->aliasType == QR_CODE_ALIAS) { +#ifdef NBGL_QRCODE + nbgl_layoutQRCode_t qrCode + = {.url = extension->fullValue, + .text1 = (extension->title != NULL) ? extension->title : extension->fullValue, + .text2 = extension->explanation, + .centered = true, + .offsetY = 0}; + + nbgl_layoutAddQRCode(genericContext.modalLayout, &qrCode); +#endif // NBGL_QRCODE + } + else { + const char *info; + // add full value text + if (extension->aliasType == ENS_ALIAS) { info = "ENS names are resolved by Ledger backend."; } - else if (pair->extension->aliasType == ADDRESS_BOOK_ALIAS) { + else if (extension->aliasType == ADDRESS_BOOK_ALIAS) { info = "This account label comes from your Address Book in Ledger Live."; } else { - info = ""; + info = extension->explanation; } + nbgl_layoutAddTextContent( + genericContext.modalLayout, aliasText, extension->fullValue, info); } - else { - info = pair->extension->explanation; - } - nbgl_layoutAddTextContent( - genericContext.modalLayout, pair->value, pair->extension->fullValue, info); - - // draw & refresh - nbgl_layoutDraw(genericContext.modalLayout); - nbgl_refresh(); -} - -// function used to display the modal warning when touching the alert symbol of a blind review -static void displayBlindWarning(void) -{ - nbgl_layoutDescription_t layoutDescription = {.modal = true, - .withLeftBorder = true, - .onActionCallback = &modalLayoutTouchCallback, - .tapActionText = NULL}; - nbgl_layoutHeader_t headerDesc = {.type = HEADER_BACK_AND_TEXT, - .separationLine = false, - .backAndText.token = 0, - .backAndText.tuneId = TUNE_TAP_CASUAL, - .backAndText.text = NULL}; - nbgl_layoutCenteredInfo_t centeredInfo - = {.icon = NULL, .text3 = NULL, .style = LARGE_CASE_INFO, .offsetY = 0, .onTop = false}; - centeredInfo.text1 = "Security risk detected"; - centeredInfo.text2 - = "This transaction or message cannot be decoded fully. If you choose to sign, you could " - "be authorizing malicious actions that can drain your wallet.\n\n" - "Learn more: ledger.com/e8"; - - genericContext.modalLayout = nbgl_layoutGet(&layoutDescription); - // add header with the tag part of the pair, to go back - nbgl_layoutAddHeader(genericContext.modalLayout, &headerDesc); - // add full value text - nbgl_layoutAddCenteredInfo(genericContext.modalLayout, ¢eredInfo); - // draw & refresh nbgl_layoutDraw(genericContext.modalLayout); nbgl_refresh(); @@ -1215,13 +1280,17 @@ static void displayTipBoxModal(void) .navWithButtons.quitText = NULL, .progressIndicator = false, .tuneId = TUNE_TAP_CASUAL}; - nbgl_pageContent_t content = {.type = INFOS_LIST, - .topRightIcon = NULL, - .infosList.nbInfos = tipBoxInfoList.nbInfos, - .infosList.infoTypes = tipBoxInfoList.infoTypes, - .infosList.infoContents = tipBoxInfoList.infoContents, - .title = tipBoxModalTitle, - .titleToken = QUIT_TOKEN}; + nbgl_pageContent_t content = {.type = INFOS_LIST, + .topRightIcon = NULL, + .infosList.nbInfos = tipBoxInfoList.nbInfos, + .infosList.withExtensions = tipBoxInfoList.withExtensions, + .infosList.infoTypes = tipBoxInfoList.infoTypes, + .infosList.infoContents = tipBoxInfoList.infoContents, + .infosList.infoExtensions = tipBoxInfoList.infoExtensions, + .infosList.token = INFO_ALIAS_TOKEN, + .title = tipBoxModalTitle, + .titleToken = QUIT_TOKEN, + .tuneId = TUNE_TAP_CASUAL}; if (modalPageContext != NULL) { nbgl_pageRelease(modalPageContext); @@ -1237,7 +1306,7 @@ static void displayAddressQRCode(void) // display the address as QR Code nbgl_layoutDescription_t layoutDescription = {.modal = true, .withLeftBorder = true, - .onActionCallback = &addressLayoutTouchCallbackQR, + .onActionCallback = &modalLayoutTouchCallback, .tapActionText = NULL}; nbgl_layoutHeader_t headerDesc = { .type = HEADER_EMPTY, .separationLine = false, .emptySpace.height = SMALL_CENTERING_HEADER}; @@ -1269,38 +1338,128 @@ static void displayAddressQRCode(void) nbgl_layoutAddQRCode(addressConfirmationContext.modalLayout, &qrCode); - nbgl_layoutAddFooter(addressConfirmationContext.modalLayout, "Close", 0, TUNE_TAP_CASUAL); + nbgl_layoutAddFooter( + addressConfirmationContext.modalLayout, "Close", DISMISS_QR_TOKEN, TUNE_TAP_CASUAL); nbgl_layoutDraw(addressConfirmationContext.modalLayout); nbgl_refresh(); } -// called when quit button is touched on Address verification page -static void addressLayoutTouchCallbackQR(int token, uint8_t index) -{ - UNUSED(token); - UNUSED(index); - - // dismiss modal - nbgl_layoutRelease(addressConfirmationContext.modalLayout); - addressConfirmationContext.modalLayout = NULL; - nbgl_screenRedraw(); - nbgl_refresh(); -} #endif // NBGL_QRCODE // called when header is touched on modal page, to dismiss it static void modalLayoutTouchCallback(int token, uint8_t index) { - UNUSED(token); UNUSED(index); - - // dismiss modal - nbgl_layoutRelease(genericContext.modalLayout); - genericContext.modalLayout = NULL; - nbgl_screenRedraw(); + if (token == DISMISS_QR_TOKEN) { + // dismiss modal + nbgl_layoutRelease(addressConfirmationContext.modalLayout); + addressConfirmationContext.modalLayout = NULL; + nbgl_screenRedraw(); + } + else if (token == DISMISS_WARNING_TOKEN) { + // dismiss modal + nbgl_layoutRelease(reviewWithWarnCtx.modalLayout); + // if already at first level, simply redraw the background + if (reviewWithWarnCtx.securityReportLevel <= 1) { + reviewWithWarnCtx.modalLayout = NULL; + nbgl_screenRedraw(); + } + else { + // We are at level 2 of warning modal, so re-display the level 1 + reviewWithWarnCtx.securityReportLevel = 1; + // if preset is used + if (reviewWithWarnCtx.warning->predefinedSet) { + displaySecurityReport(reviewWithWarnCtx.warning->predefinedSet); + } + else { + // use customized warning + const nbgl_warningDetails_t *details + = (reviewWithWarnCtx.isIntro) ? reviewWithWarnCtx.warning->introDetails + : reviewWithWarnCtx.warning->reviewDetails; + displayCustomizedSecurityReport(details); + } + return; + } + } + else if ((token >= FIRST_WARN_BAR_TOKEN) && (token <= LAST_WARN_BAR_TOKEN)) { + // dismiss modal before creating a new one + nbgl_layoutRelease(reviewWithWarnCtx.modalLayout); + reviewWithWarnCtx.securityReportLevel = 2; + // if preset is used + if (reviewWithWarnCtx.warning->predefinedSet) { + displaySecurityReport(1 << (token - FIRST_WARN_BAR_TOKEN)); + } + else { + // use customized warning + const nbgl_warningDetails_t *details = (reviewWithWarnCtx.isIntro) + ? reviewWithWarnCtx.warning->introDetails + : reviewWithWarnCtx.warning->reviewDetails; + if (details->type == BAR_LIST_WARNING) { + displayCustomizedSecurityReport( + &details->barList.details[token - FIRST_WARN_BAR_TOKEN]); + } + } + return; + } + else { + // dismiss modal + nbgl_layoutRelease(genericContext.modalLayout); + genericContext.modalLayout = NULL; + nbgl_screenRedraw(); + } nbgl_refresh(); } +// called when item are touched in layout +static void layoutTouchCallback(int token, uint8_t index) +{ + // choice buttons in initial warning page + if (token == WARNING_CHOICE_TOKEN) { + if (index == 0) { // top button to exit + reviewWithWarnCtx.choiceCallback(false); + } + else { // bottom button to continue to review + reviewWithWarnCtx.isIntro = false; + if (reviewWithWarnCtx.isStreaming) { + useCaseReviewStreamingStart(reviewWithWarnCtx.operationType, + reviewWithWarnCtx.icon, + reviewWithWarnCtx.reviewTitle, + reviewWithWarnCtx.reviewSubTitle, + reviewWithWarnCtx.choiceCallback, + false); + } + else { + useCaseReview(reviewWithWarnCtx.operationType, + reviewWithWarnCtx.tagValueList, + reviewWithWarnCtx.icon, + reviewWithWarnCtx.reviewTitle, + reviewWithWarnCtx.reviewSubTitle, + reviewWithWarnCtx.finishTitle, + reviewWithWarnCtx.tipBox, + reviewWithWarnCtx.choiceCallback, + false, + false); + } + } + } + // top-right button in initial warning page + else if (token == WARNING_BUTTON_TOKEN) { + // start at first level of security report + reviewWithWarnCtx.securityReportLevel = 1; + // if preset is used + if (reviewWithWarnCtx.warning->predefinedSet) { + displaySecurityReport(reviewWithWarnCtx.warning->predefinedSet); + } + else { + // use customized warning + const nbgl_warningDetails_t *details = (reviewWithWarnCtx.isIntro) + ? reviewWithWarnCtx.warning->introDetails + : reviewWithWarnCtx.warning->reviewDetails; + displayCustomizedSecurityReport(details); + } + } +} + // called when skip button is touched in footer, during forward only review static void displaySkipWarning(void) { @@ -1640,7 +1799,7 @@ static void bundleNavReviewAskRejectionConfirmation(nbgl_operationType_t operati const char *title; const char *confirmText; // clear skip and blind bits - operationType &= ~(SKIPPABLE_OPERATION | BLIND_OPERATION); + operationType &= ~(SKIPPABLE_OPERATION | BLIND_OPERATION | RISKY_OPERATION); if (operationType == TYPE_TRANSACTION) { title = "Reject transaction?"; confirmText = "Go back to transaction"; @@ -1689,51 +1848,311 @@ static void bundleNavReviewStreamingChoice(bool confirm) } } -// function called when the warning page of Blind Signing review buttons are pressed -static void blindSigningWarningCallback(bool confirm) +// function used to display the security level page in modal +// it can be either a single page with text or QR code, or a list of touchable bars leading +// to single pages +static void displaySecurityReport(uint32_t set) { - if (confirm) { // top button to exit - blindSigningContext.choiceCallback(false); + nbgl_layoutDescription_t layoutDescription = {.modal = true, + .withLeftBorder = true, + .onActionCallback = modalLayoutTouchCallback, + .tapActionText = NULL}; + nbgl_layoutHeader_t headerDesc = {.type = HEADER_BACK_AND_TEXT, + .separationLine = true, + .backAndText.tuneId = TUNE_TAP_CASUAL, + .backAndText.token = DISMISS_WARNING_TOKEN}; + nbgl_layoutFooter_t footerDesc + = {.type = FOOTER_EMPTY, .separationLine = false, .emptySpace.height = 0}; + uint8_t i; + uint8_t nbWarnings = 0; + const char *provider; + + reviewWithWarnCtx.modalLayout = nbgl_layoutGet(&layoutDescription); + + // count the number of warnings + for (i = 0; i < NB_WARNING_TYPES; i++) { + if (set & (1 << i)) { + nbWarnings++; + } + } + + if ((nbWarnings > 1) && (reviewWithWarnCtx.securityReportLevel == 1)) { + // if more than one warning warning, so use a list of touchable bars + for (i = 0; i < NB_WARNING_TYPES; i++) { + if (reviewWithWarnCtx.warning->predefinedSet & (1 << i)) { + nbgl_layoutBar_t bar; + bar.text = securityReportItems[i].text; + bar.subText = securityReportItems[i].subText; + bar.iconRight = &PUSH_ICON; + bar.iconLeft = securityReportItems[i].icon; + bar.token = FIRST_WARN_BAR_TOKEN + i; + bar.tuneId = TUNE_TAP_CASUAL; + bar.large = false; + bar.inactive = false; + nbgl_layoutAddTouchableBar(reviewWithWarnCtx.modalLayout, &bar); + nbgl_layoutAddSeparationLine(reviewWithWarnCtx.modalLayout); + } + } + headerDesc.backAndText.text = "Security report"; + nbgl_layoutAddHeader(reviewWithWarnCtx.modalLayout, &headerDesc); + nbgl_layoutDraw(reviewWithWarnCtx.modalLayout); + nbgl_refresh(); + return; + } + if (reviewWithWarnCtx.warning && reviewWithWarnCtx.warning->reportProvider) { + provider = reviewWithWarnCtx.warning->reportProvider; + } + else { + provider = "[unknown]"; } - else { // bottom button to continue to review - if (blindSigningContext.isStreaming) { - useCaseReviewStreamingStart(blindSigningContext.operationType, - blindSigningContext.icon, - blindSigningContext.reviewTitle, - blindSigningContext.reviewSubTitle, - blindSigningContext.choiceCallback, - false); + if (set & (1 << BLIND_SIGNING_WARN)) { + if (reviewWithWarnCtx.isIntro) { +#ifdef NBGL_QRCODE + // display a QR Code if in intro + nbgl_layoutQRCode_t qrCode + = {.url = "ledger.com/e8", + .text1 = "ledger.com/e8", + .text2 = "Scan to learn about the risks of blind signing.", + .centered = true, + .offsetY = 0}; + nbgl_layoutAddQRCode(reviewWithWarnCtx.modalLayout, &qrCode); + footerDesc.emptySpace.height = 24; +#endif // NBGL_QRCODE + headerDesc.backAndText.text = "Blind signing report"; } else { - useCaseReview(blindSigningContext.operationType, - blindSigningContext.tagValueList, - blindSigningContext.icon, - blindSigningContext.reviewTitle, - blindSigningContext.reviewSubTitle, - blindSigningContext.finishTitle, - blindSigningContext.tipBox, - blindSigningContext.choiceCallback, - false, - false); + // display a centered if in review + nbgl_contentCenter_t info = {0}; + info.icon = &C_Warning_64px; + info.title = "Blind Signing"; + info.description + = "This transaction's details are not fully verifiable. If you sign it, you could " + "lose all your assets.\n\n" + "Learn about blind signing:\nledger.com/e8"; + nbgl_layoutAddContentCenter(reviewWithWarnCtx.modalLayout, &info); + footerDesc.emptySpace.height = MEDIUM_CENTERING_HEADER; + headerDesc.separationLine = false; + } + } + else if (set & (1 << W3C_ISSUE_WARN)) { + // if W3 Checks issue, display a centered info + nbgl_contentCenter_t info = {0}; + info.icon = &C_Important_Circle_64px; + info.title = "Web3 Checks could not verify this message"; + info.description = "An issue prevented Web3 Checks from running.\nGet help: ledger.com/e11"; + nbgl_layoutAddContentCenter(reviewWithWarnCtx.modalLayout, &info); + footerDesc.emptySpace.height = MEDIUM_CENTERING_HEADER; + headerDesc.separationLine = false; + } + else if (set & (1 << W3C_THREAT_DETECTED_WARN)) { + if (reviewWithWarnCtx.isIntro) { +#ifdef NBGL_QRCODE + // display a QR Code if in intro + nbgl_layoutQRCode_t qrCode = {.url = "url.com/od24xz", + .text1 = "url.com/od24xz", + .text2 = tmpString, + .centered = true, + .offsetY = 0}; + snprintf(tmpString, + W3C_DESCRIPTION_MAX_LEN, + "Scan to view the threat report from %s.", + provider); + nbgl_layoutAddQRCode(reviewWithWarnCtx.modalLayout, &qrCode); + footerDesc.emptySpace.height = 24; +#endif // NBGL_QRCODE + headerDesc.backAndText.text = "Web3 Checks threat report"; } + else { + // display a centered if in review + nbgl_contentCenter_t info = {0}; + info.icon = &C_Warning_64px; + info.title = "Threat detected"; + info.smallTitle = "Known drainer contract"; + info.description = tmpString; + snprintf(tmpString, + W3C_DESCRIPTION_MAX_LEN, + "This transaction was scanned as malicious by Web3 Checks.\n\nView full %s " + "report:\nurl.com/od24xz", + provider); + nbgl_layoutAddContentCenter(reviewWithWarnCtx.modalLayout, &info); + footerDesc.emptySpace.height = MEDIUM_CENTERING_HEADER; + headerDesc.separationLine = false; + } + } + else if (set & (1 << W3C_LOSING_SWAP_WARN)) { + if (reviewWithWarnCtx.isIntro) { +#ifdef NBGL_QRCODE + // display a QR Code if in intro + nbgl_layoutQRCode_t qrCode = {.url = "url.com/od24xz", + .text1 = "url.com/od24xz", + .text2 = tmpString, + .centered = true, + .offsetY = 0}; + snprintf(tmpString, + W3C_DESCRIPTION_MAX_LEN, + "Scan to view the risk report from %s.", + provider); + nbgl_layoutAddQRCode(reviewWithWarnCtx.modalLayout, &qrCode); + footerDesc.emptySpace.height = 24; +#endif // NBGL_QRCODE + headerDesc.backAndText.text = "Web3 Checks risk report"; + } + else { + // display a centered if in review + nbgl_contentCenter_t info = {0}; + info.icon = &C_Warning_64px; + info.title = "Risk detected"; + info.smallTitle = "Losing swap"; + info.description = tmpString; + snprintf(tmpString, + W3C_DESCRIPTION_MAX_LEN, + "This transaction was scanned as risky by Web3 Checks.\n\n" + "View full %s report:\\nurl.com/od24xz", + provider); + nbgl_layoutAddContentCenter(reviewWithWarnCtx.modalLayout, &info); + footerDesc.emptySpace.height = MEDIUM_CENTERING_HEADER; + headerDesc.separationLine = false; + } + } + nbgl_layoutAddHeader(reviewWithWarnCtx.modalLayout, &headerDesc); + if (footerDesc.emptySpace.height > 0) { + nbgl_layoutAddExtendedFooter(reviewWithWarnCtx.modalLayout, &footerDesc); + } + nbgl_layoutDraw(reviewWithWarnCtx.modalLayout); + nbgl_refresh(); +} + +// function used to display the security level page in modal +// it can be either a single page with text or QR code, or a list of touchable bars leading +// to single pages +static void displayCustomizedSecurityReport(const nbgl_warningDetails_t *details) +{ + nbgl_layoutDescription_t layoutDescription = {.modal = true, + .withLeftBorder = true, + .onActionCallback = modalLayoutTouchCallback, + .tapActionText = NULL}; + nbgl_layoutHeader_t headerDesc = {.type = HEADER_BACK_AND_TEXT, + .separationLine = true, + .backAndText.tuneId = TUNE_TAP_CASUAL, + .backAndText.token = DISMISS_WARNING_TOKEN}; + uint8_t i; + + reviewWithWarnCtx.modalLayout = nbgl_layoutGet(&layoutDescription); + headerDesc.backAndText.text = details->title; + nbgl_layoutAddHeader(reviewWithWarnCtx.modalLayout, &headerDesc); + if (details->type == BAR_LIST_WARNING) { + // if more than one warning warning, so use a list of touchable bars + for (i = 0; i < details->barList.nbBars; i++) { + nbgl_layoutBar_t bar; + bar.text = details->barList.texts[i]; + bar.subText = details->barList.subTexts[i]; + bar.iconRight = &PUSH_ICON; + bar.iconLeft = details->barList.icons[i]; + bar.token = FIRST_WARN_BAR_TOKEN + i; + bar.tuneId = TUNE_TAP_CASUAL; + bar.large = false; + bar.inactive = false; + nbgl_layoutAddTouchableBar(reviewWithWarnCtx.modalLayout, &bar); + nbgl_layoutAddSeparationLine(reviewWithWarnCtx.modalLayout); + } + } + else if (details->type == QRCODE_WARNING) { +#ifdef NBGL_QRCODE + // display a QR Code + nbgl_layoutAddQRCode(reviewWithWarnCtx.modalLayout, &details->qrCode); +#endif // NBGL_QRCODE + headerDesc.backAndText.text = details->title; } + else if (details->type == CENTERED_INFO_WARNING) { + // display a centered info + nbgl_layoutAddContentCenter(reviewWithWarnCtx.modalLayout, &details->centeredInfo); + headerDesc.separationLine = false; + } + nbgl_layoutDraw(reviewWithWarnCtx.modalLayout); + nbgl_refresh(); } -// function used to display the warning page when starting a Bling Signing review -static void blindSigningWarning(void) +// function used to display the initial warning page when starting a "review with warning" +static void displayInitialWarning(void) { // Play notification sound #ifdef HAVE_PIEZO_SOUND io_seproxyhal_play_tune(TUNE_LOOK_AT_ME); #endif // HAVE_PIEZO_SOUND - nbgl_useCaseChoice(&C_Warning_64px, - "Blind signing ahead", - "The details of this transaction or message are not fully verifiable. If " - "you sign it, you could lose all " - "your assets.", - "Back to safety", - "Continue anyway", - blindSigningWarningCallback); + nbgl_layoutDescription_t layoutDescription; + nbgl_layoutChoiceButtons_t buttonsInfo = {.bottomText = "Continue anyway", + .token = WARNING_CHOICE_TOKEN, + .topText = "Back to safety", + .style = ROUNDED_AND_FOOTER_STYLE, + .tuneId = TUNE_TAP_CASUAL}; + nbgl_layoutHeader_t headerDesc = {.type = HEADER_EMPTY, + .separationLine = false, + .emptySpace.height = MEDIUM_CENTERING_HEADER}; + + reviewWithWarnCtx.isIntro = true; + + layoutDescription.modal = false; + layoutDescription.withLeftBorder = true; + + layoutDescription.onActionCallback = layoutTouchCallback; + layoutDescription.tapActionText = NULL; + + layoutDescription.ticker.tickerCallback = NULL; + reviewWithWarnCtx.layoutCtx = nbgl_layoutGet(&layoutDescription); + + nbgl_layoutAddHeader(reviewWithWarnCtx.layoutCtx, &headerDesc); + // do not display top-right icon if only Web3 Checks issue + if (reviewWithWarnCtx.warning->predefinedSet != (1 << W3C_ISSUE_WARN)) { + if (reviewWithWarnCtx.warning->predefinedSet != 0) { + nbgl_layoutAddTopRightButton( + reviewWithWarnCtx.layoutCtx, &PRIVACY_ICON, WARNING_BUTTON_TOKEN, TUNE_TAP_CASUAL); + } + else if (reviewWithWarnCtx.warning->introTopRightIcon != NULL) { + nbgl_layoutAddTopRightButton(reviewWithWarnCtx.layoutCtx, + reviewWithWarnCtx.warning->introTopRightIcon, + WARNING_BUTTON_TOKEN, + TUNE_TAP_CASUAL); + } + } + // add button and footer on bottom + nbgl_layoutAddChoiceButtons(reviewWithWarnCtx.layoutCtx, &buttonsInfo); + // add main content + // if predefined content is configured, use it preferably + if (reviewWithWarnCtx.warning->predefinedSet != 0) { + nbgl_contentCenter_t info = {0}; + info.icon = &C_Warning_64px; + if (reviewWithWarnCtx.warning->predefinedSet == (1 << BLIND_SIGNING_WARN)) { + info.title = "Blind signing ahead"; + info.description + = "The details of this transaction or message are not fully verifiable. If " + "you sign it, you could lose all " + "your assets."; + } + else if (reviewWithWarnCtx.warning->predefinedSet == (1 << W3C_ISSUE_WARN)) { + info.icon = &C_Important_Circle_64px; + info.title = "Web3 Checks could not verify this message"; + info.description + = "An issue prevented Web3 Checks from running.\nGet help: ledger.com/e11"; + } + else if (reviewWithWarnCtx.warning->predefinedSet == (1 << W3C_THREAT_DETECTED_WARN)) { + info.title = "Threat detected"; + info.smallTitle = "Known drainer contract"; + info.description = "This transaction was scanned as malicious by Web3 Checks."; + } + else { + info.title = "Dangerous transaction"; + info.description + = "This transaction cannot be fully decoded, and was not verified by Web3 Checks."; + } + nbgl_layoutAddContentCenter(reviewWithWarnCtx.layoutCtx, &info); + } + else if (reviewWithWarnCtx.warning->info != NULL) { + // if no predefined content, use custom one + nbgl_layoutAddContentCenter(reviewWithWarnCtx.layoutCtx, reviewWithWarnCtx.warning->info); + } + + nbgl_layoutDraw(reviewWithWarnCtx.layoutCtx); + nbgl_refresh(); } // function to factorize code for all simple reviews @@ -1768,6 +2187,9 @@ static void useCaseReview(nbgl_operationType_t operationType, prepareReviewFirstPage( &STARTING_CONTENT.content.extendedCenter.contentCenter, icon, reviewTitle, reviewSubTitle); if (tipBox != NULL) { + // do not display "Swipe to review" if a tip-box is displayed + STARTING_CONTENT.content.extendedCenter.contentCenter.subText = NULL; + STARTING_CONTENT.content.extendedCenter.tipBox.icon = tipBox->icon; STARTING_CONTENT.content.extendedCenter.tipBox.text = tipBox->text; STARTING_CONTENT.content.extendedCenter.tipBox.token = TIP_BOX_TOKEN; @@ -1775,7 +2197,11 @@ static void useCaseReview(nbgl_operationType_t operationType, tipBoxModalTitle = tipBox->modalTitle; // the only supported type yet is @ref INFOS_LIST if (tipBox->type == INFOS_LIST) { - memcpy(&tipBoxInfoList, &tipBox->infos, sizeof(nbgl_contentInfoList_t)); + tipBoxInfoList.nbInfos = tipBox->infos.nbInfos; + tipBoxInfoList.withExtensions = tipBox->infos.withExtensions; + tipBoxInfoList.infoTypes = PIC(tipBox->infos.infoTypes); + tipBoxInfoList.infoContents = PIC(tipBox->infos.infoContents); + tipBoxInfoList.infoExtensions = PIC(tipBox->infos.infoExtensions); } } @@ -1911,12 +2337,12 @@ static void useCaseHomeExt(const char *appName, } if (tagline == NULL) { if (strlen(appName) > MAX_APP_NAME_FOR_SDK_TAGLINE) { - snprintf(appDescription, + snprintf(tmpString, APP_DESCRIPTION_MAX_LEN, "This app enables signing\ntransactions on its network."); } else { - snprintf(appDescription, + snprintf(tmpString, APP_DESCRIPTION_MAX_LEN, "%s %s\n%s", TAGLINE_PART1, @@ -1926,16 +2352,15 @@ static void useCaseHomeExt(const char *appName, // If there is more than 3 lines, it means the appName was split, so we put it on the next // line - if (nbgl_getTextNbLinesInWidth(SMALL_REGULAR_FONT, appDescription, AVAILABLE_WIDTH, false) - > 3) { - snprintf(appDescription, + if (nbgl_getTextNbLinesInWidth(SMALL_REGULAR_FONT, tmpString, AVAILABLE_WIDTH, false) > 3) { + snprintf(tmpString, APP_DESCRIPTION_MAX_LEN, "%s\n%s %s", TAGLINE_PART1, appName, TAGLINE_PART2); } - info.centeredInfo.text2 = appDescription; + info.centeredInfo.text2 = tmpString; } else { info.centeredInfo.text2 = tagline; @@ -2330,29 +2755,30 @@ uint8_t nbgl_useCaseGetNbPagesForTagValueList(const nbgl_contentTagValueList_t * * @deprecated * See #nbgl_useCaseHomeAndSettings */ -DEPRECATED void nbgl_useCaseHome(const char *appName, - const nbgl_icon_details_t *appIcon, - const char *tagline, - bool withSettings, - nbgl_callback_t topRightCallback, - nbgl_callback_t quitCallback) -{ - nbgl_useCaseHomeExt( - appName, appIcon, tagline, withSettings, NULL, NULL, topRightCallback, quitCallback); +void nbgl_useCaseHome(const char *appName, + const nbgl_icon_details_t *appIcon, + const char *tagline, + bool withSettings, + nbgl_callback_t topRightCallback, + nbgl_callback_t quitCallback) +{ + nbgl_homeAction_t homeAction = {0}; + useCaseHomeExt( + appName, appIcon, tagline, withSettings, &homeAction, topRightCallback, quitCallback); } /** * @deprecated * See #nbgl_useCaseHomeAndSettings */ -DEPRECATED void nbgl_useCaseHomeExt(const char *appName, - const nbgl_icon_details_t *appIcon, - const char *tagline, - bool withSettings, - const char *actionButtonText, - nbgl_callback_t actionCallback, - nbgl_callback_t topRightCallback, - nbgl_callback_t quitCallback) +void nbgl_useCaseHomeExt(const char *appName, + const nbgl_icon_details_t *appIcon, + const char *tagline, + bool withSettings, + const char *actionButtonText, + nbgl_callback_t actionCallback, + nbgl_callback_t topRightCallback, + nbgl_callback_t quitCallback) { nbgl_homeAction_t homeAction = {.callback = actionCallback, .icon = NULL, @@ -2364,28 +2790,25 @@ DEPRECATED void nbgl_useCaseHomeExt(const char *appName, } /** - * @brief Draws the settings pages of an app with as many pages as given - * For each page, the given navCallback will be called to get the content. Only 'type' and - * union has to be set in this content + * @brief Initiates the drawing a set of pages of generic content, with a touchable header (usually + * to go back or to an upper level) For each page (including the first one), the given 'navCallback' + * will be called to get the content. Only 'type' and union has to be set in this content. * - * @param title string to set in touchable title + * @param title string to set in touchable title (header) * @param initPage page on which to start [0->(nbPages-1)] * @param nbPages number of pages - * @param touchable unused, it is always on - * @param quitCallback callback called when quit button (or title) is pressed + * @param quitCallback callback called title is pressed * @param navCallback callback called when navigation arrows are pressed * @param controlsCallback callback called when any controls in the settings (radios, switches) is * called (the tokens must be >= @ref FIRST_USER_TOKEN) */ -void nbgl_useCaseSettings(const char *title, - uint8_t initPage, - uint8_t nbPages, - bool touchable, - nbgl_callback_t quitCallback, - nbgl_navCallback_t navCallback, - nbgl_layoutTouchCallback_t controlsCallback) +void nbgl_useCaseNavigableContent(const char *title, + uint8_t initPage, + uint8_t nbPages, + nbgl_callback_t quitCallback, + nbgl_navCallback_t navCallback, + nbgl_layoutTouchCallback_t controlsCallback) { - UNUSED(touchable); reset_callbacks(); // memorize context @@ -2401,6 +2824,24 @@ void nbgl_useCaseSettings(const char *title, displaySettingsPage(initPage, true); } +/** + * @deprecated + * See #nbgl_useCaseHomeAndSettings if used in a 'Settings' context, or nbgl_useCaseNavigableContent + * otherwise + */ +void nbgl_useCaseSettings(const char *title, + uint8_t initPage, + uint8_t nbPages, + bool touchable, + nbgl_callback_t quitCallback, + nbgl_navCallback_t navCallback, + nbgl_layoutTouchCallback_t controlsCallback) +{ + UNUSED(touchable); + nbgl_useCaseNavigableContent( + title, initPage, nbPages, quitCallback, navCallback, controlsCallback); +} + /** * @brief Draws the settings pages of an app with automatic pagination depending on content * to be displayed that is passed through settingContents and infosList @@ -2990,20 +3431,64 @@ void nbgl_useCaseReviewBlindSigning(nbgl_operationType_t operationT const nbgl_tipBox_t *tipBox, nbgl_choiceCallback_t choiceCallback) { - memset(&blindSigningContext, 0, sizeof(blindSigningContext)); - - blindSigningContext.isStreaming = false; - blindSigningContext.operationType = operationType | BLIND_OPERATION; - blindSigningContext.tagValueList = tagValueList; - blindSigningContext.icon = icon; - blindSigningContext.reviewTitle = reviewTitle; - blindSigningContext.reviewSubTitle = reviewSubTitle; - blindSigningContext.finishTitle = finishTitle; - blindSigningContext.tipBox = tipBox; - blindSigningContext.choiceCallback = choiceCallback; + nbgl_useCaseReviewWithWarning(operationType, + tagValueList, + icon, + reviewTitle, + reviewSubTitle, + finishTitle, + tipBox, + &blindSigningWarning, + choiceCallback); +} - blindSigningWarning(); +/** + * @brief Draws a flow of pages of a review requiring a warning page before the review. + * Moreover, the first and last pages of review display a top-right button, that displays more + * information about the warnings + * + * Navigation operates with either swipe or navigation + * keys at bottom right. The last page contains a long-press button with the given finishTitle and + * the given icon. + * @note All tag/value pairs are provided in the API and the number of pages is automatically + * computed, the last page being a long press one + * + * @param operationType type of operation (Operation, Transaction, Message) + * @param tagValueList list of tag/value pairs + * @param icon icon used on first and last review page + * @param reviewTitle string used in the first review page + * @param reviewSubTitle string to set under reviewTitle (can be NULL) + * @param finishTitle string used in the last review page + * @param tipBox parameter to build a tip-box and necessary modal (can be NULL) + * @param warning structure to build the initial warning page (cannot be NULL) + * @param choiceCallback callback called when operation is accepted (param is true) or rejected + * (param is false) + */ +void nbgl_useCaseReviewWithWarning(nbgl_operationType_t operationType, + const nbgl_contentTagValueList_t *tagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + const char *finishTitle, + const nbgl_tipBox_t *tipBox, + const nbgl_warning_t *warning, + nbgl_choiceCallback_t choiceCallback) +{ + memset(&reviewWithWarnCtx, 0, sizeof(reviewWithWarnCtx)); + reviewWithWarnCtx.isStreaming = false; + reviewWithWarnCtx.operationType = operationType | RISKY_OPERATION; + reviewWithWarnCtx.tagValueList = tagValueList; + reviewWithWarnCtx.icon = icon; + reviewWithWarnCtx.reviewTitle = reviewTitle; + reviewWithWarnCtx.reviewSubTitle = reviewSubTitle; + reviewWithWarnCtx.finishTitle = finishTitle; + reviewWithWarnCtx.tipBox = tipBox; + reviewWithWarnCtx.warning = warning; + reviewWithWarnCtx.choiceCallback = choiceCallback; + + displayInitialWarning(); } + /** * @brief Draws a flow of pages of a light review. Navigation operates with either swipe or * navigation keys at bottom right. The last page contains a button/footer with the given @@ -3117,16 +3602,43 @@ void nbgl_useCaseReviewStreamingBlindSigningStart(nbgl_operationType_t ope const char *reviewSubTitle, nbgl_choiceCallback_t choiceCallback) { - memset(&blindSigningContext, 0, sizeof(blindSigningContext)); + nbgl_useCaseReviewStreamingWithWarningStart( + operationType, icon, reviewTitle, reviewSubTitle, &blindSigningWarning, choiceCallback); +} + +/** + * @brief Start drawing the flow of pages of a blind-signing review. The review is preceded by a + * warning page + * @note This should be followed by calls to @ref nbgl_useCaseReviewStreamingContinue and finally + * to + * @ref nbgl_useCaseReviewStreamingFinish. + * + * @param operationType type of operation (Operation, Transaction, Message) + * @param icon icon used on first and last review page + * @param reviewTitle string used in the first review page + * @param reviewSubTitle string to set under reviewTitle (can be NULL) + * @param warning structure to build the initial warning page (cannot be NULL) + * @param choiceCallback callback called when more operation data are needed (param is true) or + * operation is rejected (param is false) + */ +void nbgl_useCaseReviewStreamingWithWarningStart(nbgl_operationType_t operationType, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + const nbgl_warning_t *warning, + nbgl_choiceCallback_t choiceCallback) +{ + memset(&reviewWithWarnCtx, 0, sizeof(reviewWithWarnCtx)); - blindSigningContext.isStreaming = true; - blindSigningContext.operationType = operationType | BLIND_OPERATION; - blindSigningContext.icon = icon; - blindSigningContext.reviewTitle = reviewTitle; - blindSigningContext.reviewSubTitle = reviewSubTitle; - blindSigningContext.choiceCallback = choiceCallback; + reviewWithWarnCtx.isStreaming = true; + reviewWithWarnCtx.operationType = operationType | RISKY_OPERATION; + reviewWithWarnCtx.icon = icon; + reviewWithWarnCtx.reviewTitle = reviewTitle; + reviewWithWarnCtx.reviewSubTitle = reviewSubTitle; + reviewWithWarnCtx.choiceCallback = choiceCallback; + reviewWithWarnCtx.warning = warning; - blindSigningWarning(); + displayInitialWarning(); } /** @@ -3245,18 +3757,9 @@ void nbgl_useCaseReviewStreamingFinish(const char *finishTitle, * @deprecated * See #nbgl_useCaseAddressReview */ -DEPRECATED void nbgl_useCaseAddressConfirmation(const char *address, nbgl_choiceCallback_t callback) -{ - nbgl_useCaseAddressConfirmationExt(address, callback, NULL); -} - -/** - * @deprecated - * See #nbgl_useCaseAddressReview - */ -DEPRECATED void nbgl_useCaseAddressConfirmationExt(const char *address, - nbgl_choiceCallback_t callback, - const nbgl_contentTagValueList_t *tagValueList) +void nbgl_useCaseAddressConfirmationExt(const char *address, + nbgl_choiceCallback_t callback, + const nbgl_contentTagValueList_t *tagValueList) { reset_callbacks(); memset(&genericContext, 0, sizeof(genericContext)); diff --git a/lib_nbgl/src/nbgl_use_case_nanos.c b/lib_nbgl/src/nbgl_use_case_nanos.c index 0b821c461..ab30fddfe 100644 --- a/lib_nbgl/src/nbgl_use_case_nanos.c +++ b/lib_nbgl/src/nbgl_use_case_nanos.c @@ -20,6 +20,8 @@ /********************* * DEFINES *********************/ +#define WITH_HORIZONTAL_CHOICES_LIST +#define WITH_HORIZONTAL_BARS_LIST /********************** * TYPEDEFS @@ -30,6 +32,7 @@ typedef struct ReviewContext_s { const nbgl_contentTagValueList_t *tagValueList; const nbgl_icon_details_t *icon; const char *reviewTitle; + const char *reviewSubTitle; const char *address; // for address confirmation review } ReviewContext_t; @@ -42,26 +45,67 @@ typedef struct ChoiceContext_s { nbgl_choiceCallback_t onChoice; } ChoiceContext_t; +typedef struct ConfirmContext_s { + const char *message; + const char *subMessage; + const char *confirmText; + const char *cancelText; + nbgl_callback_t onConfirm; + nbgl_step_t currentStep; +} ConfirmContext_t; + +typedef struct ContentContext_s { + const char *title; // For CHOICES_LIST /BARS_LIST + nbgl_genericContents_t genericContents; + const char *rejectText; + nbgl_layoutTouchCallback_t controlsCallback; + nbgl_callback_t quitCallback; +} ContentContext_t; + typedef struct HomeContext_s { const char *appName; const nbgl_icon_details_t *appIcon; const char *tagline; const nbgl_genericContents_t *settingContents; const nbgl_contentInfoList_t *infosList; + const nbgl_homeAction_t *homeAction; nbgl_callback_t quitCallback; } HomeContext_t; +#ifdef NBGL_KEYPAD +typedef struct KeypadContext_s { + uint8_t pinEntry[8]; + uint8_t pinLen; + uint8_t pinMinDigits; + uint8_t pinMaxDigits; + nbgl_layout_t *layoutCtx; + bool hidden; + uint8_t keypadIndex; + nbgl_pinValidCallback_t validatePin; + nbgl_callback_t backCallback; +} KeypadContext_t; +#endif + typedef enum { NONE_USE_CASE, + SPINNER_USE_CASE, REVIEW_USE_CASE, + GENERIC_REVIEW_USE_CASE, + REVIEW_BLIND_SIGN_USE_CASE, ADDRESS_REVIEW_USE_CASE, + STREAMING_BLIND_SIGN_START_REVIEW_USE_CASE, STREAMING_START_REVIEW_USE_CASE, STREAMING_CONTINUE_REVIEW_USE_CASE, STREAMING_FINISH_REVIEW_USE_CASE, CHOICE_USE_CASE, + STATUS_USE_CASE, + CONFIRM_USE_CASE, + KEYPAD_USE_CASE, HOME_USE_CASE, INFO_USE_CASE, SETTINGS_USE_CASE, + GENERIC_SETTINGS, + CONTENT_USE_CASE, } ContextType_t; typedef struct UseCaseContext_s { @@ -71,9 +115,14 @@ typedef struct UseCaseContext_s { nbgl_stepCallback_t stepCallback; ///< if not NULL, function to be called on "double-key" action union { - ReviewContext_t review; - ChoiceContext_t choice; - HomeContext_t home; + ReviewContext_t review; + ChoiceContext_t choice; + ConfirmContext_t confirm; + HomeContext_t home; + ContentContext_t content; +#ifdef NBGL_KEYPAD + KeypadContext_t keypad; +#endif }; } UseCaseContext_t; @@ -91,22 +140,36 @@ static void displayHomePage(nbgl_stepPosition_t pos); static void displayInfoPage(nbgl_stepPosition_t pos); static void displaySettingsPage(nbgl_stepPosition_t pos, bool toogle_state); static void displayChoicePage(nbgl_stepPosition_t pos); +static void displayConfirm(nbgl_stepPosition_t pos); +static void displayContent(nbgl_stepPosition_t pos, bool toogle_state); +static void displaySpinner(const char *text); static void startUseCaseHome(void); static void startUseCaseInfo(void); static void startUseCaseSettings(void); static void startUseCaseSettingsAtPage(uint8_t initSettingPage); +static void startUseCaseContent(void); + +static void statusTickerCallback(void); // Simple helper to get the number of elements inside a nbgl_content_t static uint8_t getContentNbElement(const nbgl_content_t *content) { switch (content->type) { + case CENTERED_INFO: + return 1; + case INFO_BUTTON: + return 1; case TAG_VALUE_LIST: return content->content.tagValueList.nbPairs; case SWITCHES_LIST: return content->content.switchesList.nbSwitches; case INFOS_LIST: return content->content.infosList.nbInfos; + case CHOICES_LIST: + return content->content.choicesList.nbChoices; + case BARS_LIST: + return content->content.barsList.nbBars; default: return 0; } @@ -124,6 +187,10 @@ static const nbgl_content_t *getContentAtIdx(const nbgl_genericContents_t *gener } if (genericContents->callbackCallNeeded) { + if (content == NULL) { + LOG_DEBUG(USE_CASE_LOGGER, "Invalid content variable\n"); + return NULL; + } // Retrieve content through callback, but first memset the content. memset(content, 0, sizeof(nbgl_content_t)); genericContents->contentGetterCallback(contentIdx, content); @@ -137,15 +204,28 @@ static const nbgl_content_t *getContentAtIdx(const nbgl_genericContents_t *gener // Helper to retrieve the content inside a nbgl_genericContents_t using // either the contentsList or using the contentGetterCallback -static const nbgl_content_t *getContentElemAtIdx(const nbgl_genericContents_t *genericContents, - uint8_t elemIdx, - uint8_t *elemContentIdx, - nbgl_content_t *content) +static const nbgl_content_t *getContentElemAtIdx(uint8_t elemIdx, + uint8_t *elemContentIdx, + nbgl_content_t *content) { - const nbgl_content_t *p_content; - uint8_t nbPages = 0; - uint8_t elemNbPages = 0; - + const nbgl_genericContents_t *genericContents = NULL; + const nbgl_content_t *p_content = NULL; + uint8_t nbPages = 0; + uint8_t elemNbPages = 0; + + switch (context.type) { + case SETTINGS_USE_CASE: + case HOME_USE_CASE: + case GENERIC_SETTINGS: + genericContents = context.home.settingContents; + break; + case CONTENT_USE_CASE: + case GENERIC_REVIEW_USE_CASE: + genericContents = &context.content.genericContents; + break; + default: + return NULL; + } for (int i = 0; i < genericContents->nbContents; i++) { p_content = getContentAtIdx(genericContents, i, content); elemNbPages = getContentNbElement(p_content); @@ -159,6 +239,80 @@ static const nbgl_content_t *getContentElemAtIdx(const nbgl_genericContents_t *g return p_content; } +static const char *getChoiceName(uint8_t choiceIndex) +{ + uint8_t elemIdx; + uint8_t nbValues; + const nbgl_content_t *p_content = NULL; + nbgl_content_t content = {0}; + nbgl_contentRadioChoice_t *contentChoices = NULL; + nbgl_contentBarsList_t *contentBars = NULL; + char **names = NULL; + + p_content = getContentElemAtIdx(context.currentPage, &elemIdx, &content); + if (p_content == NULL) { + return NULL; + } + switch (p_content->type) { + case CHOICES_LIST: + contentChoices = (nbgl_contentRadioChoice_t *) PIC(&p_content->content.choicesList); + names = (char **) PIC(contentChoices->names); + nbValues = contentChoices->nbChoices; + break; + case BARS_LIST: + contentBars = ((nbgl_contentBarsList_t *) PIC(&p_content->content.barsList)); + names = (char **) PIC(contentBars->barTexts); + nbValues = contentBars->nbBars; + break; + default: + // Not supported as vertical MenuList + return NULL; + } + if (choiceIndex >= nbValues) { + // Last item is always "Back" button + return "Back"; + } + return (const char *) PIC(names[choiceIndex]); +} + +static void onChoiceSelected(uint8_t choiceIndex) +{ + uint8_t elemIdx; + uint8_t token = 255; + const nbgl_content_t *p_content = NULL; + nbgl_content_t content = {0}; + nbgl_contentRadioChoice_t *contentChoices = NULL; + nbgl_contentBarsList_t *contentBars = NULL; + + p_content = getContentElemAtIdx(context.currentPage, &elemIdx, &content); + if (p_content == NULL) { + return; + } + switch (p_content->type) { + case CHOICES_LIST: + contentChoices = (nbgl_contentRadioChoice_t *) PIC(&p_content->content.choicesList); + if (choiceIndex < contentChoices->nbChoices) { + token = contentChoices->token; + } + break; + case BARS_LIST: + contentBars = ((nbgl_contentBarsList_t *) PIC(&p_content->content.barsList)); + if (choiceIndex < contentBars->nbBars) { + token = contentBars->tokens[choiceIndex]; + } + break; + default: + // Not supported as vertical MenuList + break; + } + if ((token != 255) && (context.content.controlsCallback != NULL)) { + context.content.controlsCallback(token, 0); + } + else if (context.content.quitCallback != NULL) { + context.content.quitCallback(); + } +} + static void getPairData(const nbgl_contentTagValueList_t *tagValueList, uint8_t index, const char **item, @@ -204,42 +358,118 @@ static void onChoiceReject(void) } } -static void onSettingsAction(void) +static void onConfirmAccept(void) +{ + if (context.confirm.currentStep) { + nbgl_stepRelease(context.confirm.currentStep); + } + if (context.confirm.onConfirm) { + context.confirm.onConfirm(); + } +} + +static void onConfirmReject(void) { - nbgl_content_t content; - uint8_t elemIdx; + if (context.confirm.currentStep) { + nbgl_stepRelease(context.confirm.currentStep); + nbgl_screenRedraw(); + nbgl_refresh(); + } +} - const nbgl_content_t *p_content = getContentElemAtIdx( - context.home.settingContents, context.currentPage, &elemIdx, &content); +static void onSwitchAction(void) +{ + const nbgl_contentSwitch_t *contentSwitch = NULL; + const nbgl_content_t *p_content = NULL; + nbgl_content_t content = {0}; + uint8_t elemIdx; - switch (p_content->type) { - case SWITCHES_LIST: { - const nbgl_contentSwitch_t *contentSwitch = &((const nbgl_contentSwitch_t *) PIC( - p_content->content.switchesList.switches))[elemIdx]; - nbgl_state_t state = (contentSwitch->initState == ON_STATE) ? OFF_STATE : ON_STATE; + p_content = getContentElemAtIdx(context.currentPage, &elemIdx, &content); + if ((p_content == NULL) || (p_content->type != SWITCHES_LIST)) { + return; + } + contentSwitch + = &((const nbgl_contentSwitch_t *) PIC(p_content->content.switchesList.switches))[elemIdx]; + switch (context.type) { + case SETTINGS_USE_CASE: + case HOME_USE_CASE: + case GENERIC_SETTINGS: displaySettingsPage(FORWARD_DIRECTION, true); - if (p_content->contentActionCallback != NULL) { - nbgl_contentActionCallback_t onContentAction - = PIC(p_content->contentActionCallback); - onContentAction(contentSwitch->token, state, context.currentPage); - } break; - } + case CONTENT_USE_CASE: + case GENERIC_REVIEW_USE_CASE: + displayContent(FORWARD_DIRECTION, true); + break; default: break; } + if (p_content->contentActionCallback != NULL) { + nbgl_contentActionCallback_t actionCallback = PIC(p_content->contentActionCallback); + actionCallback(contentSwitch->token, 0, context.currentPage); + } + else if (context.content.controlsCallback != NULL) { + context.content.controlsCallback(contentSwitch->token, 0); + } } static void drawStep(nbgl_stepPosition_t pos, const nbgl_icon_details_t *icon, const char *txt, const char *subTxt, - nbgl_stepButtonCallback_t onActionCallback) + nbgl_stepButtonCallback_t onActionCallback, + bool modal) { + uint8_t elemIdx; + nbgl_step_t newStep = NULL; + const nbgl_content_t *p_content = NULL; + nbgl_content_t content = {0}; + nbgl_contentRadioChoice_t *contentChoices = NULL; + nbgl_contentBarsList_t *contentBars = NULL; + nbgl_screenTickerConfiguration_t *p_ticker = NULL; + nbgl_layoutMenuList_t list = {0}; + nbgl_screenTickerConfiguration_t ticker = { + .tickerCallback = PIC(statusTickerCallback), + .tickerIntervale = 0, // not periodic + .tickerValue = 3000 // 3 seconds + }; + pos |= GET_POS_OF_STEP(context.currentPage, context.nbPages); - if (icon == NULL) { - nbgl_stepDrawText(pos, onActionCallback, NULL, txt, subTxt, BOLD_TEXT1_INFO, false); + if ((context.type == STATUS_USE_CASE) || (context.type == SPINNER_USE_CASE)) { + p_ticker = &ticker; + } + if ((context.type == CONFIRM_USE_CASE) && (context.confirm.currentStep != NULL)) { + nbgl_stepRelease(context.confirm.currentStep); + } + + if (txt == NULL) { + p_content = getContentElemAtIdx(context.currentPage, &elemIdx, &content); + if (p_content) { + switch (p_content->type) { + case CHOICES_LIST: + contentChoices + = ((nbgl_contentRadioChoice_t *) PIC(&p_content->content.choicesList)); + list.nbChoices = contentChoices->nbChoices + 1; // For Back button + list.selectedChoice = contentChoices->initChoice; + list.callback = getChoiceName; + newStep = nbgl_stepDrawMenuList(onChoiceSelected, p_ticker, &list, modal); + break; + case BARS_LIST: + contentBars = ((nbgl_contentBarsList_t *) PIC(&p_content->content.barsList)); + list.nbChoices = contentBars->nbBars + 1; // For Back button + list.selectedChoice = 0; + list.callback = getChoiceName; + newStep = nbgl_stepDrawMenuList(onChoiceSelected, p_ticker, &list, modal); + break; + default: + // Not supported as vertical MenuList + break; + } + } + } + else if (icon == NULL) { + newStep = nbgl_stepDrawText( + pos, onActionCallback, p_ticker, txt, subTxt, BOLD_TEXT1_INFO, modal); } else { nbgl_layoutCenteredInfo_t info; @@ -248,12 +478,21 @@ static void drawStep(nbgl_stepPosition_t pos, info.text2 = subTxt; info.onTop = false; info.style = BOLD_TEXT1_INFO; - nbgl_stepDrawCenteredInfo(pos, onActionCallback, NULL, &info, false); + newStep = nbgl_stepDrawCenteredInfo(pos, onActionCallback, p_ticker, &info, modal); + } + if (context.type == CONFIRM_USE_CASE) { + context.confirm.currentStep = newStep; } } static bool buttonGenericCallback(nbgl_buttonEvent_t event, nbgl_stepPosition_t *pos) { + uint8_t elemIdx; + uint8_t token = 0; + uint8_t index = 0; + const nbgl_content_t *p_content = NULL; + nbgl_content_t content = {0}; + if (event == BUTTON_LEFT_PRESSED) { if (context.currentPage > 0) { context.currentPage--; @@ -275,8 +514,43 @@ static bool buttonGenericCallback(nbgl_buttonEvent_t event, nbgl_stepPosition_t *pos = FORWARD_DIRECTION; } else { - if ((event == BUTTON_BOTH_PRESSED) && (context.stepCallback != NULL)) { - context.stepCallback(); + if (event == BUTTON_BOTH_PRESSED) { + if (context.stepCallback != NULL) { + context.stepCallback(); + } + else if ((context.type == CONTENT_USE_CASE) + || (context.type == GENERIC_REVIEW_USE_CASE)) { + p_content = getContentElemAtIdx(context.currentPage, &elemIdx, &content); + if (p_content != NULL) { + switch (p_content->type) { + case CENTERED_INFO: + // No associated callback + return false; + case INFO_BUTTON: + token = p_content->content.infoButton.buttonToken; + break; + case SWITCHES_LIST: + token = p_content->content.switchesList.switches->token; + break; + case BARS_LIST: + token = p_content->content.barsList.tokens[context.currentPage]; + break; + case CHOICES_LIST: + token = p_content->content.choicesList.token; + index = context.currentPage; + break; + default: + break; + } + + if ((p_content) && (p_content->contentActionCallback != NULL)) { + p_content->contentActionCallback(token, 0, context.currentPage); + } + else if (context.content.controlsCallback != NULL) { + context.content.controlsCallback(token, index); + } + } + } } return false; } @@ -355,6 +629,18 @@ static void genericChoiceCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event) displayChoicePage(pos); } +static void genericConfirmCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event) +{ + UNUSED(stepCtx); + nbgl_stepPosition_t pos; + + if (!buttonGenericCallback(event, &pos)) { + return; + } + + displayConfirm(pos); +} + static void statusButtonCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event) { UNUSED(stepCtx); @@ -365,6 +651,18 @@ static void statusButtonCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event) } } +static void contentCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event) +{ + UNUSED(stepCtx); + nbgl_stepPosition_t pos; + + if (!buttonGenericCallback(event, &pos)) { + return; + } + + displayContent(pos, false); +} + // callback used for timeout static void statusTickerCallback(void) { @@ -376,85 +674,171 @@ static void statusTickerCallback(void) // function used to display the current page in review static void displayReviewPage(nbgl_stepPosition_t pos) { - const char *text = NULL; - const char *subText = NULL; - const nbgl_icon_details_t *icon = NULL; + uint8_t reviewPages = 0; + uint8_t finalPages = 0; + uint8_t pairIndex = 0; + const char *text = NULL; + const char *subText = NULL; + const nbgl_icon_details_t *icon = NULL; + uint8_t currentIndex = 0; + uint8_t warnIndex = 255; + uint8_t titleIndex = 255; + uint8_t subIndex = 255; + uint8_t approveIndex = 255; + uint8_t rejectIndex = 255; context.stepCallback = NULL; - if (context.currentPage == 0) { // title page - icon = context.review.icon; - text = context.review.reviewTitle; + // Determine the 1st page to display tag/values + if (context.type == REVIEW_BLIND_SIGN_USE_CASE) { + // Warning page to display + warnIndex = currentIndex++; + reviewPages++; } - else if (context.currentPage == (context.nbPages - 2)) { // accept page - icon = &C_icon_validate_14; - text = "Approve"; - context.stepCallback = onReviewAccept; + // Title page to display + titleIndex = currentIndex++; + reviewPages++; + if (context.review.reviewSubTitle) { + // subtitle page to display + subIndex = currentIndex++; + reviewPages++; } - else if (context.currentPage == (context.nbPages - 1)) { // reject page - icon = &C_icon_crossmark; - text = "Reject"; - context.stepCallback = onReviewReject; + approveIndex = context.nbPages - 2; + rejectIndex = context.nbPages - 1; + finalPages = approveIndex; + + // Determine which page to display + if (context.currentPage >= finalPages) { + if (context.currentPage == approveIndex) { + // Approve page + icon = &C_icon_validate_14; + text = "Approve"; + context.stepCallback = onReviewAccept; + } + else if (context.currentPage == rejectIndex) { + // Reject page + icon = &C_icon_crossmark; + text = "Reject"; + context.stepCallback = onReviewReject; + } + } + else if (context.currentPage < reviewPages) { + if (context.currentPage == warnIndex) { + // Blind Signing Warning page + icon = &C_icon_warning; + text = "Blind\nsigning"; + } + else if (context.currentPage == titleIndex) { + // Title page + icon = context.review.icon; + text = context.review.reviewTitle; + } + else if (context.currentPage == subIndex) { + // SubTitle page + text = context.review.reviewSubTitle; + } } - else if ((context.review.address != NULL) - && (context.currentPage == 1)) { // address confirmation and 2nd page + else if ((context.review.address != NULL) && (context.currentPage == reviewPages)) { + // address confirmation and 2nd page text = "Address"; subText = context.review.address; } else { - uint8_t pairIndex = (context.review.address != NULL) ? (context.currentPage - 2) - : (context.currentPage - 1); + pairIndex = context.currentPage - reviewPages; + if (context.review.address != NULL) { + pairIndex--; + } getPairData(context.review.tagValueList, pairIndex, &text, &subText); } - drawStep(pos, icon, text, subText, reviewCallback); + drawStep(pos, icon, text, subText, reviewCallback, false); nbgl_refresh(); } // function used to display the current page in review static void displayStreamingReviewPage(nbgl_stepPosition_t pos) { - const char *text = NULL; - const char *subText = NULL; - const nbgl_icon_details_t *icon = NULL; + const char *text = NULL; + const char *subText = NULL; + const nbgl_icon_details_t *icon = NULL; + uint8_t reviewPages = 0; + uint8_t warnIndex = 255; + uint8_t titleIndex = 255; + uint8_t subIndex = 255; context.stepCallback = NULL; - if (context.type == STREAMING_START_REVIEW_USE_CASE) { - if (context.currentPage == 0) { // title page - icon = context.review.icon; - text = context.review.reviewTitle; - } - else { - nbgl_useCaseSpinner("Processing"); - onReviewAccept(); - return; - } - } - else if (context.type == STREAMING_CONTINUE_REVIEW_USE_CASE) { - if (context.currentPage < context.review.tagValueList->nbPairs) { + switch (context.type) { + case STREAMING_START_REVIEW_USE_CASE: + case STREAMING_BLIND_SIGN_START_REVIEW_USE_CASE: + if (context.type == STREAMING_START_REVIEW_USE_CASE) { + // Title page to display + titleIndex = reviewPages++; + if (context.review.reviewSubTitle) { + // subtitle page to display + subIndex = reviewPages++; + } + } + else { + // warning page to display + warnIndex = reviewPages++; + // Title page to display + titleIndex = reviewPages++; + if (context.review.reviewSubTitle) { + // subtitle page to display + subIndex = reviewPages++; + } + } + // Determine which page to display + if (context.currentPage >= reviewPages) { + displaySpinner("Processing"); + onReviewAccept(); + return; + } + // header page(s) + if (context.currentPage == warnIndex) { + // warning page + icon = &C_icon_warning; + text = "Blind\nsigning"; + } + else if (context.currentPage == titleIndex) { + // title page + icon = context.review.icon; + text = context.review.reviewTitle; + } + else if (context.currentPage == subIndex) { + // subtitle page + text = context.review.reviewSubTitle; + } + break; + + case STREAMING_CONTINUE_REVIEW_USE_CASE: + if (context.currentPage >= context.review.tagValueList->nbPairs) { + displaySpinner("Processing"); + onReviewAccept(); + return; + } getPairData(context.review.tagValueList, context.currentPage, &text, &subText); - } - else { - nbgl_useCaseSpinner("Processing"); - onReviewAccept(); - return; - } - } - else { - if (context.currentPage == 0) { // accept page - icon = &C_icon_validate_14; - text = "Approve"; - context.stepCallback = onReviewAccept; - } - else { // reject page - icon = &C_icon_crossmark; - text = "Reject"; - context.stepCallback = onReviewReject; - } + break; + + case STREAMING_FINISH_REVIEW_USE_CASE: + default: + if (context.currentPage == 0) { + // accept page + icon = &C_icon_validate_14; + text = "Approve"; + context.stepCallback = onReviewAccept; + } + else { + // reject page + icon = &C_icon_crossmark; + text = "Reject"; + context.stepCallback = onReviewReject; + } + break; } - drawStep(pos, icon, text, subText, streamingReviewCallback); + drawStep(pos, icon, text, subText, streamingReviewCallback, false); nbgl_refresh(); } @@ -479,10 +863,95 @@ static void displayInfoPage(nbgl_stepPosition_t pos) context.stepCallback = startUseCaseHome; } - drawStep(pos, icon, text, subText, infoCallback); + drawStep(pos, icon, text, subText, infoCallback, false); nbgl_refresh(); } +// function used to get the current page content +static void getContentPage(bool toogle_state, + const char **text, + const char **subText, + const nbgl_icon_details_t **icon) +{ + static char fullText[75]; + uint8_t elemIdx; + nbgl_state_t state = OFF_STATE; + const nbgl_content_t *p_content = NULL; + nbgl_content_t content = {0}; + nbgl_contentSwitch_t *contentSwitch = NULL; +#ifdef WITH_HORIZONTAL_CHOICES_LIST + nbgl_contentRadioChoice_t *contentChoices = NULL; + char **names = NULL; +#endif + + p_content = getContentElemAtIdx(context.currentPage, &elemIdx, &content); + if (p_content == NULL) { + return; + } + switch (p_content->type) { + case CENTERED_INFO: + *text = PIC(p_content->content.centeredInfo.text1); + *subText = PIC(p_content->content.centeredInfo.text2); + break; + case INFO_BUTTON: + *icon = PIC(p_content->content.infoButton.icon); + *text = PIC(p_content->content.infoButton.text); + *subText = PIC(p_content->content.infoButton.buttonText); + break; + case TAG_VALUE_LIST: + getPairData(&p_content->content.tagValueList, elemIdx, text, subText); + break; + case SWITCHES_LIST: + contentSwitch = &( + (nbgl_contentSwitch_t *) PIC(p_content->content.switchesList.switches))[elemIdx]; + *text = contentSwitch->text; + state = contentSwitch->initState; + if (toogle_state) { + state = (state == ON_STATE) ? OFF_STATE : ON_STATE; + } + if (state == ON_STATE) { + snprintf(fullText, sizeof(fullText), "%s\nEnabled", contentSwitch->subText); + } + else { + snprintf(fullText, sizeof(fullText), "%s\nDisabled", contentSwitch->subText); + } + context.stepCallback = onSwitchAction; + *subText = fullText; + break; + case INFOS_LIST: + *text = ((const char *const *) PIC(p_content->content.infosList.infoTypes))[elemIdx]; + *subText + = ((const char *const *) PIC(p_content->content.infosList.infoContents))[elemIdx]; + break; + case CHOICES_LIST: +#ifdef WITH_HORIZONTAL_CHOICES_LIST + if ((context.type == CONTENT_USE_CASE) && (context.content.title != NULL)) { + *text = PIC(context.content.title); + *subText = PIC(p_content->content.choicesList.names[elemIdx]); + } + else { + contentChoices = (nbgl_contentRadioChoice_t *) PIC(&p_content->content.choicesList); + names = (char **) PIC(contentChoices->names); + *text = (const char *) PIC(names[elemIdx]); + } +#endif + break; + case BARS_LIST: +#ifdef WITH_HORIZONTAL_BARS_LIST + if ((context.type == CONTENT_USE_CASE) && (context.content.title != NULL)) { + *text = PIC(context.content.title); + *subText = PIC(p_content->content.barsList.barTexts[elemIdx]); + } + else { + *text = PIC(p_content->content.barsList.barTexts[elemIdx]); + } +#endif + break; + default: + break; + } +} + // function used to display the current page in settings static void displaySettingsPage(nbgl_stepPosition_t pos, bool toogle_state) { @@ -493,68 +962,56 @@ static void displaySettingsPage(nbgl_stepPosition_t pos, bool toogle_state) context.stepCallback = NULL; if (context.currentPage < (context.nbPages - 1)) { - nbgl_content_t nbgl_content; - uint8_t elemIdx; - - const nbgl_content_t *p_nbgl_content = getContentElemAtIdx( - context.home.settingContents, context.currentPage, &elemIdx, &nbgl_content); - - switch (p_nbgl_content->type) { - case TAG_VALUE_LIST: - getPairData(&p_nbgl_content->content.tagValueList, elemIdx, &text, &subText); - break; - case SWITCHES_LIST: { - const nbgl_contentSwitch_t *contentSwitch = &((const nbgl_contentSwitch_t *) PIC( - p_nbgl_content->content.switchesList.switches))[elemIdx]; - text = contentSwitch->text; - // switch subtext is ignored - nbgl_state_t state = contentSwitch->initState; - if (toogle_state) { - state = (state == ON_STATE) ? OFF_STATE : ON_STATE; - } - if (state == ON_STATE) { - subText = "Enabled"; - } - else { - subText = "Disabled"; - } - context.stepCallback = onSettingsAction; - break; - } - case INFOS_LIST: - text = ((const char *const *) PIC( - p_nbgl_content->content.infosList.infoTypes))[elemIdx]; - subText = ((const char *const *) PIC( - p_nbgl_content->content.infosList.infoContents))[elemIdx]; - break; - default: - break; - } + getContentPage(toogle_state, &text, &subText, &icon); } else { // last page is for quit - icon = &C_icon_back_x; - text = "Back"; - context.stepCallback = startUseCaseHome; + icon = &C_icon_back_x; + text = "Back"; + if (context.type == GENERIC_SETTINGS) { + context.stepCallback = context.home.quitCallback; + } + else { + context.stepCallback = startUseCaseHome; + } } - drawStep(pos, icon, text, subText, settingsCallback); + drawStep(pos, icon, text, subText, settingsCallback, false); nbgl_refresh(); } static void startUseCaseHome(void) { - if (context.type == SETTINGS_USE_CASE) { - context.currentPage = 1; + int8_t addPages = 0; + if (context.home.homeAction) { + // Action page index + addPages++; } - else if (context.type == INFO_USE_CASE) { - context.currentPage = 2; - } - else { - context.currentPage = 0; + switch (context.type) { + case SETTINGS_USE_CASE: + // Settings page index + context.currentPage = 1 + addPages; + break; + case INFO_USE_CASE: + // Info page index + context.currentPage = 2 + addPages; + break; + default: + // Home page index + context.currentPage = 0; + break; } - context.type = HOME_USE_CASE; - context.nbPages = 4; + context.type = HOME_USE_CASE; + context.nbPages = 2; // Home + Quit + if (context.home.settingContents) { + context.nbPages++; + } + if (context.home.infosList) { + context.nbPages++; + } + if (context.home.homeAction) { + context.nbPages += addPages; + } displayHomePage(FORWARD_DIRECTION); } @@ -569,10 +1026,13 @@ static void startUseCaseInfo(void) static void startUseCaseSettingsAtPage(uint8_t initSettingPage) { - nbgl_content_t content; - const nbgl_content_t *p_content; + nbgl_content_t content = {0}; + const nbgl_content_t *p_content = NULL; - context.type = SETTINGS_USE_CASE; + if (context.type == 0) { + // Not yet init, it is not a GENERIC_SETTINGS + context.type = SETTINGS_USE_CASE; + } context.nbPages = 1; // For back screen for (int i = 0; i < context.home.settingContents->nbContents; i++) { p_content = getContentAtIdx(context.home.settingContents, i, &content); @@ -588,70 +1048,84 @@ static void startUseCaseSettings(void) startUseCaseSettingsAtPage(0); } +static void startUseCaseContent(void) +{ + uint8_t contentIdx = 0; + const nbgl_content_t *p_content = NULL; + nbgl_content_t content = {0}; + + context.nbPages = 1; // Quit + + for (contentIdx = 0; contentIdx < context.content.genericContents.nbContents; contentIdx++) { + p_content = getContentAtIdx(&context.content.genericContents, contentIdx, &content); + context.nbPages += getContentNbElement(p_content); + } + + displayContent(FORWARD_DIRECTION, false); +} + // function used to display the current page in home static void displayHomePage(nbgl_stepPosition_t pos) { - const char *text = NULL; - const char *subText = NULL; - const nbgl_icon_details_t *icon = NULL; + const char *text = NULL; + const char *subText = NULL; + const nbgl_icon_details_t *icon = NULL; + uint8_t currentIndex = 0; + uint8_t homeIndex = 255; + uint8_t actionIndex = 255; + uint8_t settingsIndex = 255; + uint8_t infoIndex = 255; context.stepCallback = NULL; - // Handle case where there is no settings - if (context.home.settingContents == NULL && context.currentPage == 1) { - if (pos & BACKWARD_DIRECTION) { - context.currentPage -= 1; - } - else { - context.currentPage += 1; - if (context.home.infosList == NULL) { - context.currentPage += 1; - } - } + // Determine which pages are present + homeIndex = currentIndex++; + if (context.home.homeAction) { + actionIndex = currentIndex++; + } + if (context.home.settingContents) { + settingsIndex = currentIndex++; + } + if (context.home.infosList) { + infoIndex = currentIndex++; } - // Handle case where there is no info - if (context.home.infosList == NULL && context.currentPage == 2) { - if (pos & BACKWARD_DIRECTION) { - context.currentPage -= 1; - if (context.home.settingContents == NULL) { - context.currentPage -= 1; - } + if (context.currentPage == homeIndex) { + // Home page + icon = context.home.appIcon; + if (context.home.tagline != NULL) { + text = context.home.tagline; } else { - context.currentPage += 1; + text = context.home.appName; + subText = "is ready"; } } - - switch (context.currentPage) { - case 0: - icon = context.home.appIcon; - if (context.home.tagline != NULL) { - text = context.home.tagline; - } - else { - text = context.home.appName; - subText = "is ready"; - } - break; - case 1: - icon = &C_icon_coggle; - text = "Settings"; - context.stepCallback = startUseCaseSettings; - break; - case 2: - icon = &C_icon_certificate; - text = "About"; - context.stepCallback = startUseCaseInfo; - break; - default: - icon = &C_icon_dashboard_x; - text = "Quit"; - context.stepCallback = context.home.quitCallback; - break; + else if (context.currentPage == actionIndex) { + // Action page + icon = context.home.homeAction->icon; + text = PIC(context.home.homeAction->text); + context.stepCallback = context.home.homeAction->callback; + } + else if (context.currentPage == settingsIndex) { + // Settings page + icon = &C_icon_coggle; + text = "Settings"; + context.stepCallback = startUseCaseSettings; + } + else if (context.currentPage == infoIndex) { + // About page + icon = &C_icon_certificate; + text = "About"; + context.stepCallback = startUseCaseInfo; + } + else { + icon = &C_icon_dashboard_x; + text = "Quit"; + context.stepCallback = context.home.quitCallback; } - drawStep(pos, icon, text, subText, homeCallback); + drawStep(pos, icon, text, subText, homeCallback, false); nbgl_refresh(); } @@ -700,15 +1174,437 @@ static void displayChoicePage(nbgl_stepPosition_t pos) context.stepCallback = onChoiceReject; } - drawStep(pos, icon, text, subText, genericChoiceCallback); + drawStep(pos, icon, text, subText, genericChoiceCallback, false); nbgl_refresh(); } -/********************** - * GLOBAL FUNCTIONS - **********************/ - -/** +// function used to display the Confirm page +static void displayConfirm(nbgl_stepPosition_t pos) +{ + const char *text = NULL; + const char *subText = NULL; + const nbgl_icon_details_t *icon = NULL; + + context.stepCallback = NULL; + switch (context.currentPage) { + case 0: + // title page + text = context.confirm.message; + subText = context.confirm.subMessage; + break; + case 1: + // confirm page + icon = &C_icon_validate_14; + text = context.confirm.confirmText; + context.stepCallback = onConfirmAccept; + break; + case 2: + // cancel page + icon = &C_icon_crossmark; + text = context.confirm.cancelText; + context.stepCallback = onConfirmReject; + break; + } + + drawStep(pos, icon, text, subText, genericConfirmCallback, true); + nbgl_refresh(); +} + +// function used to display the current navigable content +static void displayContent(nbgl_stepPosition_t pos, bool toogle_state) +{ + const char *text = NULL; + const char *subText = NULL; + const nbgl_icon_details_t *icon = NULL; + + context.stepCallback = NULL; + + if (context.currentPage < (context.nbPages - 1)) { + getContentPage(toogle_state, &text, &subText, &icon); + } + else { // last page is for quit + if (context.content.rejectText) { + text = context.content.rejectText; + } + else { + text = "Back"; + } + if (context.type == GENERIC_REVIEW_USE_CASE) { + icon = &C_icon_crossmark; + } + else { + icon = &C_icon_back_x; + } + context.stepCallback = context.content.quitCallback; + } + + drawStep(pos, icon, text, subText, contentCallback, false); + nbgl_refresh(); +} + +static void displaySpinner(const char *text) +{ + drawStep(SINGLE_STEP, &C_icon_processing, text, NULL, NULL, false); + nbgl_refresh(); +} + +// function to factorize code for all simple reviews +static void useCaseReview(ContextType_t type, + const nbgl_contentTagValueList_t *tagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + const char *finishTitle, + nbgl_choiceCallback_t choiceCallback) +{ + UNUSED(finishTitle); // TODO dedicated screen for it? + + memset(&context, 0, sizeof(UseCaseContext_t)); + context.type = type; + context.review.tagValueList = tagValueList; + context.review.reviewTitle = reviewTitle; + context.review.reviewSubTitle = reviewSubTitle; + context.review.icon = icon; + context.review.onChoice = choiceCallback; + context.currentPage = 0; + // 1 page for title and 2 pages at the end for accept/reject + context.nbPages = tagValueList->nbPairs + 3; + if (type == REVIEW_BLIND_SIGN_USE_CASE) { + context.nbPages++; // 1 page for warning + } + if (reviewSubTitle) { + context.nbPages++; // 1 page for subtitle page + } + + displayReviewPage(FORWARD_DIRECTION); +} + +#ifdef NBGL_KEYPAD +static void setPinCodeText(void) +{ + bool enableValidate = false; + bool enableBackspace = true; + + // pin can be validated when min digits is entered + enableValidate = (context.keypad.pinLen >= context.keypad.pinMinDigits); + // backspace is disabled when no digit is entered and back vallback is not provided + enableBackspace = (context.keypad.pinLen > 0) || (context.keypad.backCallback != NULL); + nbgl_layoutUpdateKeypadContent(context.keypad.layoutCtx, + context.keypad.hidden, + context.keypad.pinLen, + (const char *) context.keypad.pinEntry); + nbgl_layoutUpdateKeypad( + context.keypad.layoutCtx, context.keypad.keypadIndex, enableValidate, enableBackspace); + nbgl_layoutDraw(context.keypad.layoutCtx); + nbgl_refresh(); +} + +// called when a key is touched on the keypad +static void keypadCallback(char touchedKey) +{ + switch (touchedKey) { + case BACKSPACE_KEY: + if (context.keypad.pinLen > 0) { + context.keypad.pinLen--; + context.keypad.pinEntry[context.keypad.pinLen] = 0; + } + else if (context.keypad.backCallback != NULL) { + context.keypad.backCallback(); + break; + } + setPinCodeText(); + break; + + case VALIDATE_KEY: + context.keypad.validatePin(context.keypad.pinEntry, context.keypad.pinLen); + break; + + default: + if ((touchedKey >= 0x30) && (touchedKey < 0x40)) { + if (context.keypad.pinLen < context.keypad.pinMaxDigits) { + context.keypad.pinEntry[context.keypad.pinLen] = touchedKey; + context.keypad.pinLen++; + } + setPinCodeText(); + } + break; + } +} + +// called to create a keypad, with either hidden or visible digits +static void keypadGenericUseCase(const char *title, + uint8_t minDigits, + uint8_t maxDigits, + bool shuffled, + bool hidden, + nbgl_pinValidCallback_t validatePinCallback, + nbgl_callback_t backCallback) +{ + nbgl_layoutDescription_t layoutDescription = {0}; + int status = -1; + + // reset the keypad context + memset(&context, 0, sizeof(KeypadContext_t)); + context.type = KEYPAD_USE_CASE; + context.currentPage = 0; + context.nbPages = 1; + context.keypad.validatePin = validatePinCallback; + context.keypad.backCallback = backCallback; + context.keypad.pinMinDigits = minDigits; + context.keypad.pinMaxDigits = maxDigits; + context.keypad.hidden = hidden; + context.keypad.layoutCtx = nbgl_layoutGet(&layoutDescription); + + // add keypad + status = nbgl_layoutAddKeypad(context.keypad.layoutCtx, keypadCallback, title, shuffled); + if (status < 0) { + return; + } + context.keypad.keypadIndex = status; + // add digits + status = nbgl_layoutAddKeypadContent(context.keypad.layoutCtx, hidden, maxDigits, ""); + if (status < 0) { + return; + } + + nbgl_layoutDraw(context.keypad.layoutCtx); + if (context.keypad.backCallback != NULL) { + // force backspace to be visible at first digit, to be used as quit + nbgl_layoutUpdateKeypad(context.keypad.layoutCtx, context.keypad.keypadIndex, false, true); + } + nbgl_refresh(); +} +#endif // NBGL_KEYPAD + +/********************** + * GLOBAL FUNCTIONS + **********************/ + +/** + * @brief with Nano Screen, only a single tag/value pair is displayable in a page + * + * @param nbPairs unused + * @param tagValueList unused + * @param startIndex unused + * @param requireSpecificDisplay (output) set to true if the tag/value needs a specific display: + * - centeredInfo flag is enabled + * - the tag/value doesn't fit in a page + * @return the number of tag/value pairs fitting in a page + */ +uint8_t nbgl_useCaseGetNbTagValuesInPage(uint8_t nbPairs, + const nbgl_contentTagValueList_t *tagValueList, + uint8_t startIndex, + bool *requireSpecificDisplay) +{ + UNUSED(nbPairs); + UNUSED(tagValueList); + UNUSED(startIndex); + *requireSpecificDisplay = true; + return 1; +} + +/** + * @brief with Nano Screen, only a single tag/value pair is displayable in a page + * + * @param nbPairs unused + * @param tagValueList unused + * @param startIndex unused + * @param isSkippable unused + * @param requireSpecificDisplay (output) set to true if the tag/value needs a specific display: + * - centeredInfo flag is enabled + * - the tag/value doesn't fit in a page + * @return the number of tag/value pairs fitting in a page + */ +uint8_t nbgl_useCaseGetNbTagValuesInPageExt(uint8_t nbPairs, + const nbgl_contentTagValueList_t *tagValueList, + uint8_t startIndex, + bool isSkippable, + bool *requireSpecificDisplay) +{ + UNUSED(nbPairs); + UNUSED(tagValueList); + UNUSED(startIndex); + UNUSED(isSkippable); + *requireSpecificDisplay = true; + return 1; +} + +/** + * @brief with Nano Screen, only a single info is displayable in a page + * + * @param nbInfos unused + * @param infosList unused + * @param startIndex unused + * @return the number of infos fitting in a page + */ +uint8_t nbgl_useCaseGetNbInfosInPage(uint8_t nbInfos, + const nbgl_contentInfoList_t *infosList, + uint8_t startIndex, + bool withNav) +{ + UNUSED(nbInfos); + UNUSED(infosList); + UNUSED(startIndex); + UNUSED(withNav); + return 1; +} + +/** + * @brief with Nano Screen, only a single switch is displayable in a page + * + * @param nbSwitches unused + * @param switchesList unused + * @param startIndex unused + * @return the number of switches fitting in a page + */ +uint8_t nbgl_useCaseGetNbSwitchesInPage(uint8_t nbSwitches, + const nbgl_contentSwitchesList_t *switchesList, + uint8_t startIndex, + bool withNav) +{ + UNUSED(nbSwitches); + UNUSED(switchesList); + UNUSED(startIndex); + UNUSED(withNav); + return 1; +} + +/** + * @brief with Nano Screen, only a single bar is displayable in a page + * + * @param nbBars unused + * @param barsList unused + * @param startIndex unused + * @return the number of bars fitting in a page + */ +uint8_t nbgl_useCaseGetNbBarsInPage(uint8_t nbBars, + const nbgl_contentBarsList_t *barsList, + uint8_t startIndex, + bool withNav) +{ + UNUSED(nbBars); + UNUSED(barsList); + UNUSED(startIndex); + UNUSED(withNav); + return 1; +} + +/** + * @brief with Nano Screen, only a single radio choice displayable in a page + * + * @param nbChoices unused + * @param choicesList unused + * @param startIndex unused + * @return the number of radio choices fitting in a page + */ +uint8_t nbgl_useCaseGetNbChoicesInPage(uint8_t nbChoices, + const nbgl_contentRadioChoice_t *choicesList, + uint8_t startIndex, + bool withNav) +{ + UNUSED(nbChoices); + UNUSED(choicesList); + UNUSED(startIndex); + UNUSED(withNav); + return 1; +} + +/** + * @brief computes the number of pages necessary to display the given list of tag/value pairs + * + * @param tagValueList list of tag/value pairs + * @return the number of pages necessary to display the given list of tag/value pairs + */ +uint8_t nbgl_useCaseGetNbPagesForTagValueList(const nbgl_contentTagValueList_t *tagValueList) +{ + uint8_t nbPages = 0; + uint8_t nbPairs = tagValueList->nbPairs; + uint8_t nbPairsInPage; + uint8_t i = 0; + bool flag; + + while (i < tagValueList->nbPairs) { + // upper margin + nbPairsInPage = nbgl_useCaseGetNbTagValuesInPageExt(nbPairs, tagValueList, i, false, &flag); + i += nbPairsInPage; + nbPairs -= nbPairsInPage; + nbPages++; + } + return nbPages; +} + +/** + * @brief Initiates the drawing a set of pages of generic content, with a touchable header (usually + * to go back or to an upper level) For each page (including the first one), the given 'navCallback' + * will be called to get the content. Only 'type' and union has to be set in this content. + * + * @param title string to set in touchable title (header) + * @param initPage page on which to start [0->(nbPages-1)] + * @param nbPages number of pages + * @param quitCallback callback called title is pressed + * @param navCallback callback called when navigation arrows are pressed + * @param controlsCallback callback called when any controls in the settings (radios, switches) is + * called (the tokens must be >= @ref FIRST_USER_TOKEN) + */ +void nbgl_useCaseNavigableContent(const char *title, + uint8_t initPage, + uint8_t nbPages, + nbgl_callback_t quitCallback, + nbgl_navCallback_t navCallback, + nbgl_layoutTouchCallback_t controlsCallback) +{ + nbgl_pageContent_t pageContent = {0}; + static nbgl_content_t contentsList = {0}; + + if (initPage >= nbPages) { + return; + } + // Use Callback to get the page content + if (navCallback(initPage, &pageContent) == false) { + return; + } + memset(&context, 0, sizeof(UseCaseContext_t)); + context.type = CONTENT_USE_CASE; + context.content.quitCallback = quitCallback; + context.content.controlsCallback = controlsCallback; + context.content.genericContents.callbackCallNeeded = false; + context.content.genericContents.nbContents = nbPages; + + contentsList.type = pageContent.type; + switch (pageContent.type) { + case CENTERED_INFO: + contentsList.content.centeredInfo = pageContent.centeredInfo; + break; + case INFO_BUTTON: + contentsList.content.infoButton = pageContent.infoButton; + break; + case TAG_VALUE_LIST: + contentsList.content.tagValueList = pageContent.tagValueList; + break; + case SWITCHES_LIST: + contentsList.content.switchesList = pageContent.switchesList; + break; + case INFOS_LIST: + contentsList.content.infosList = pageContent.infosList; + break; + case CHOICES_LIST: + contentsList.content.choicesList = pageContent.choicesList; + context.content.title = title; + context.currentPage = pageContent.choicesList.initChoice; + break; + case BARS_LIST: + contentsList.content.barsList = pageContent.barsList; + context.content.title = title; + break; + default: + break; + } + context.content.genericContents.contentsList = (const nbgl_content_t *) &contentsList; + + startUseCaseContent(); +} + +/** * @brief Draws the extended version of home page of an app (page on which we land when launching it * from dashboard) with automatic support of setting display. * @@ -731,14 +1627,13 @@ void nbgl_useCaseHomeAndSettings(const char *appName, const nbgl_homeAction_t *action, nbgl_callback_t quitCallback) { - UNUSED(action); // TODO support it at some point? - memset(&context, 0, sizeof(UseCaseContext_t)); context.home.appName = appName; context.home.appIcon = appIcon; context.home.tagline = tagline; context.home.settingContents = PIC(settingContents); context.home.infosList = PIC(infosList); + context.home.homeAction = action; context.home.quitCallback = quitCallback; if (initSettingPage != INIT_HOME_PAGE) { @@ -749,6 +1644,53 @@ void nbgl_useCaseHomeAndSettings(const char *appName, } } +/** + * @brief Draws the settings pages of an app with automatic pagination depending on content + * to be displayed that is passed through settingContents and infosList + * + * @param appName string to use as title + * @param initPage page on which to start, can be != 0 if you want to display a specific page + * after a setting confirmation change or something. Then the value should be taken from the + * nbgl_contentActionCallback_t callback call. + * @param settingContents contents to be displayed + * @param infosList infos to be displayed (version, license, developer, ...) + * @param quitCallback callback called when quit button (or title) is pressed + */ +void nbgl_useCaseGenericSettings(const char *appName, + uint8_t initPage, + const nbgl_genericContents_t *settingContents, + const nbgl_contentInfoList_t *infosList, + nbgl_callback_t quitCallback) +{ + memset(&context, 0, sizeof(UseCaseContext_t)); + context.type = GENERIC_SETTINGS; + context.home.appName = appName; + context.home.settingContents = PIC(settingContents); + context.home.infosList = PIC(infosList); + context.home.quitCallback = quitCallback; + + startUseCaseSettingsAtPage(initPage); +} + +/** + * @brief Draws a set of pages with automatic pagination depending on content + * to be displayed that is passed through contents. + * + * @param title string to use as title + * @param initPage page on which to start, can be != 0 if you want to display a specific page + * after a confirmation change or something. Then the value should be taken from the + * nbgl_contentActionCallback_t callback call. + * @param contents contents to be displayed + * @param quitCallback callback called when quit button (or title) is pressed + */ +void nbgl_useCaseGenericConfiguration(const char *title, + uint8_t initPage, + const nbgl_genericContents_t *contents, + nbgl_callback_t quitCallback) +{ + nbgl_useCaseGenericSettings(title, initPage, contents, NULL, quitCallback); +} + /** * @brief Draws a flow of pages of a review. * @note All tag/value pairs are provided in the API and the number of pages is automatically @@ -771,21 +1713,93 @@ void nbgl_useCaseReview(nbgl_operationType_t operationType, const char *finishTitle, nbgl_choiceCallback_t choiceCallback) { - UNUSED(operationType); // TODO adapt accept and reject text depending on this value? - UNUSED(reviewSubTitle); // TODO dedicated screen for it? - UNUSED(finishTitle); // TODO dedicated screen for it? + UNUSED(operationType); // TODO adapt accept and reject text depending on this value? + + useCaseReview(REVIEW_USE_CASE, + tagValueList, + icon, + reviewTitle, + reviewSubTitle, + finishTitle, + choiceCallback); +} - memset(&context, 0, sizeof(UseCaseContext_t)); - context.type = REVIEW_USE_CASE; - context.review.tagValueList = tagValueList; - context.review.reviewTitle = reviewTitle; - context.review.icon = icon; - context.review.onChoice = choiceCallback; - context.currentPage = 0; - // + 3 because 1 page for title and 2 pages at the end for accept/reject - context.nbPages = tagValueList->nbPairs + 3; +/** + * @brief Draws a flow of pages of a review. Navigation operates with either swipe or navigation + * keys at bottom right. The last page contains a long-press button with the given finishTitle and + * the given icon. + * @note All tag/value pairs are provided in the API and the number of pages is automatically + * computed, the last page being a long press one + * + * @param operationType type of operation (Operation, Transaction, Message) + * @param tagValueList list of tag/value pairs + * @param icon icon used on first and last review page + * @param reviewTitle string used in the first review page + * @param reviewSubTitle string to set under reviewTitle (can be NULL) + * @param finishTitle string used in the last review page + * @param dummy inconsistent parameter on Nano devices (ignored) + * @param choiceCallback callback called when operation is accepted (param is true) or rejected + * (param is false) + */ +void nbgl_useCaseAdvancedReview(nbgl_operationType_t operationType, + const nbgl_contentTagValueList_t *tagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + const char *finishTitle, + const nbgl_tipBox_t *dummy, + nbgl_choiceCallback_t choiceCallback) +{ + UNUSED(operationType); // TODO adapt accept and reject text depending on this value? + UNUSED(dummy); + + useCaseReview(REVIEW_USE_CASE, + tagValueList, + icon, + reviewTitle, + reviewSubTitle, + finishTitle, + choiceCallback); +} - displayReviewPage(FORWARD_DIRECTION); +/** + * @brief Draws a flow of pages of a blind-signing review. The review is preceded by a warning page + * + * Navigation operates with either swipe or navigation + * keys at bottom right. The last page contains a long-press button with the given finishTitle and + * the given icon. + * @note All tag/value pairs are provided in the API and the number of pages is automatically + * computed, the last page being a long press one + * + * @param operationType type of operation (Operation, Transaction, Message) + * @param tagValueList list of tag/value pairs + * @param icon icon used on first and last review page + * @param reviewTitle string used in the first review page + * @param reviewSubTitle string to set under reviewTitle (can be NULL) + * @param finishTitle string used in the last review page + * @param dummy inconsistent parameter on Nano devices (ignored) + * @param choiceCallback callback called when operation is accepted (param is true) or rejected + * (param is false) + */ +void nbgl_useCaseReviewBlindSigning(nbgl_operationType_t operationType, + const nbgl_contentTagValueList_t *tagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + const char *finishTitle, + const nbgl_tipBox_t *dummy, + nbgl_choiceCallback_t choiceCallback) +{ + UNUSED(operationType); // TODO adapt accept and reject text depending on this value? + UNUSED(dummy); + + useCaseReview(REVIEW_BLIND_SIGN_USE_CASE, + tagValueList, + icon, + reviewTitle, + reviewSubTitle, + finishTitle, + choiceCallback); } /** @@ -842,27 +1856,52 @@ void nbgl_useCaseAddressReview(const char *address, const char *reviewSubTitle, nbgl_choiceCallback_t choiceCallback) { - UNUSED(reviewSubTitle); // TODO dedicated screen for it? - memset(&context, 0, sizeof(UseCaseContext_t)); - context.type = ADDRESS_REVIEW_USE_CASE; - context.review.address = address; - context.review.reviewTitle = reviewTitle; - context.review.icon = icon; - context.review.onChoice = choiceCallback; - context.currentPage = 0; + context.type = ADDRESS_REVIEW_USE_CASE; + context.review.address = address; + context.review.reviewTitle = reviewTitle; + context.review.reviewSubTitle = reviewSubTitle; + context.review.icon = icon; + context.review.onChoice = choiceCallback; + context.currentPage = 0; // + 4 because 1 page for title, 1 for address and 2 pages at the end for approve/reject context.nbPages = 4; if (additionalTagValueList) { - memcpy(&context.review.tagValueList, - additionalTagValueList, - sizeof(nbgl_contentTagValueList_t)); + context.review.tagValueList = PIC(additionalTagValueList); context.nbPages += additionalTagValueList->nbPairs; } displayReviewPage(FORWARD_DIRECTION); } +/** + * @brief Draws a flow of pages of a review with automatic pagination depending on content + * to be displayed that is passed through contents. + * + * @param contents contents to be displayed + * @param rejectText text to use in footer + * @param rejectCallback callback called when reject is pressed + */ +void nbgl_useCaseGenericReview(const nbgl_genericContents_t *contents, + const char *rejectText, + nbgl_callback_t rejectCallback) +{ + memset(&context, 0, sizeof(UseCaseContext_t)); + context.type = GENERIC_REVIEW_USE_CASE; + context.content.rejectText = rejectText; + context.content.quitCallback = rejectCallback; + context.content.genericContents.nbContents = contents->nbContents; + context.content.genericContents.callbackCallNeeded = contents->callbackCallNeeded; + if (contents->callbackCallNeeded) { + context.content.genericContents.contentGetterCallback = contents->contentGetterCallback; + } + else { + context.content.genericContents.contentsList = PIC(contents->contentsList); + } + + startUseCaseContent(); +} + /** * @brief Draws a transient (3s) status page, either of success or failure, with the given message * @@ -872,22 +1911,16 @@ void nbgl_useCaseAddressReview(const char *address, */ void nbgl_useCaseStatus(const char *message, bool isSuccess, nbgl_callback_t quitCallback) { - UNUSED(isSuccess); // TODO add icon depending on isSuccess? + const nbgl_icon_details_t *icon = NULL; memset(&context, 0, sizeof(UseCaseContext_t)); + context.type = STATUS_USE_CASE; context.stepCallback = quitCallback; context.currentPage = 0; context.nbPages = 1; - nbgl_screenTickerConfiguration_t ticker = { - .tickerCallback = PIC(statusTickerCallback), - .tickerIntervale = 0, // not periodic - .tickerValue = 3000 // 3 seconds - }; - - nbgl_stepDrawText( - SINGLE_STEP, statusButtonCallback, &ticker, message, NULL, BOLD_TEXT1_INFO, false); - nbgl_refresh(); + icon = isSuccess ? &C_icon_validate_14 : &C_icon_crossmark; + drawStep(SINGLE_STEP, icon, message, NULL, statusButtonCallback, false); } /** @@ -931,7 +1964,7 @@ void nbgl_useCaseReviewStatus(nbgl_reviewStatusType_t reviewStatusType, isSuccess = true; break; case STATUS_TYPE_ADDRESS_REJECTED: - msg = "Verification\ncancelled"; + msg = "Address verification cancelled"; isSuccess = false; break; default: @@ -958,16 +1991,49 @@ void nbgl_useCaseReviewStreamingStart(nbgl_operationType_t operationType, const char *reviewSubTitle, nbgl_choiceCallback_t choiceCallback) { - UNUSED(operationType); // TODO adapt accept and reject text depending on this value? - UNUSED(reviewSubTitle); // TODO dedicated screen for it? + UNUSED(operationType); // TODO adapt accept and reject text depending on this value? memset(&context, 0, sizeof(UseCaseContext_t)); - context.type = STREAMING_START_REVIEW_USE_CASE; - context.review.reviewTitle = reviewTitle; - context.review.icon = icon; - context.review.onChoice = choiceCallback; - context.currentPage = 0; - context.nbPages = 1 + 1; // Start page + trick for review continue + context.type = STREAMING_START_REVIEW_USE_CASE; + context.review.reviewTitle = reviewTitle; + context.review.reviewSubTitle = reviewSubTitle; + context.review.icon = icon; + context.review.onChoice = choiceCallback; + context.currentPage = 0; + context.nbPages = 2; // Start page + trick for review continue + + displayStreamingReviewPage(FORWARD_DIRECTION); +} + +/** + * @brief Start drawing the flow of pages of a blind-signing review. The review is preceded by a + * warning page + * @note This should be followed by calls to nbgl_useCaseReviewStreamingContinue and finally to + * nbgl_useCaseReviewStreamingFinish. + * + * @param operationType type of operation (Operation, Transaction, Message) + * @param icon icon used on first and last review page + * @param reviewTitle string used in the first review page + * @param reviewSubTitle string to set under reviewTitle (can be NULL) + * @param choiceCallback callback called when more operation data are needed (param is true) or + * operation is rejected (param is false) + */ +void nbgl_useCaseReviewStreamingBlindSigningStart(nbgl_operationType_t operationType, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + nbgl_choiceCallback_t choiceCallback) +{ + UNUSED(operationType); // TODO adapt accept and reject text depending on this value? + + memset(&context, 0, sizeof(UseCaseContext_t)); + context.type = STREAMING_BLIND_SIGN_START_REVIEW_USE_CASE; + context.review.reviewTitle = reviewTitle; + context.review.reviewSubTitle = reviewSubTitle; + context.review.icon = icon; + context.review.onChoice = choiceCallback; + context.currentPage = 0; + context.nbPages = 3; // Warning + Start page + trick for review continue displayStreamingReviewPage(FORWARD_DIRECTION); } @@ -1036,8 +2102,12 @@ void nbgl_useCaseReviewStreamingFinish(const char *finishTitle, */ void nbgl_useCaseSpinner(const char *text) { - drawStep(SINGLE_STEP, &C_icon_processing, text, NULL, NULL); - nbgl_refresh(); + memset(&context, 0, sizeof(UseCaseContext_t)); + context.type = SPINNER_USE_CASE; + context.currentPage = 0; + context.nbPages = 1; + + displaySpinner(text); } void nbgl_useCaseChoice(const nbgl_icon_details_t *icon, @@ -1061,5 +2131,96 @@ void nbgl_useCaseChoice(const nbgl_icon_details_t *icon, displayChoicePage(FORWARD_DIRECTION); }; +/** + * @brief Draws a page to confirm or not an action, described in a centered info (with info icon), + * thanks to a button and a footer at the bottom of the page. The given callback is called if the + * button is touched. If the footer is touched, the page is only "dismissed" + * @note This page is displayed as a modal (so the content of the previous page will be visible when + * dismissed). + * + * @param message string to set in center of page (32px) + * @param subMessage string to set under message (24px) (can be NULL) + * @param confirmText string to set in button, to confirm + * @param cancelText string to set in footer, to reject + * @param callback callback called when confirmation button is touched + */ +void nbgl_useCaseConfirm(const char *message, + const char *subMessage, + const char *confirmText, + const char *cancelText, + nbgl_callback_t callback) +{ + memset(&context, 0, sizeof(UseCaseContext_t)); + context.type = CONFIRM_USE_CASE; + context.confirm.message = message; + context.confirm.subMessage = subMessage; + context.confirm.confirmText = confirmText; + context.confirm.cancelText = cancelText; + context.confirm.onConfirm = callback; + context.currentPage = 0; + context.nbPages = 1 + 2; // 2 pages at the end for confirm/cancel + + displayConfirm(FORWARD_DIRECTION); +} + +#ifdef NBGL_KEYPAD +/** + * @brief draws a standard keypad modal page with visible digits. It contains + * - a navigation bar at the top + * - a title for the pin code + * - a visible digit entry + * - the keypad at the bottom + * + * @note callbacks allow to control the behavior. + * backspace and validation button are shown/hidden automatically + * + * @param title string to set in pin code title + * @param minDigits pin minimum number of digits + * @param maxDigits maximum number of digits to be displayed + * @param shuffled if set to true, digits are shuffled in keypad + * @param validatePinCallback function calledto validate the pin code + * @param backCallback callback called on backspace is "pressed" in first digit + */ +void nbgl_useCaseKeypadDigits(const char *title, + uint8_t minDigits, + uint8_t maxDigits, + bool shuffled, + nbgl_pinValidCallback_t validatePinCallback, + nbgl_callback_t backCallback) +{ + keypadGenericUseCase( + title, minDigits, maxDigits, shuffled, false, validatePinCallback, backCallback); +} + +/** + * @brief draws a standard keypad modal page with hidden digits. It contains + * - a navigation bar at the top + * - a title for the pin code + * - a hidden digit entry + * - the keypad at the bottom + * + * @note callbacks allow to control the behavior. + * backspace and validation button are shown/hidden automatically + * + * @param title string to set in pin code title + * @param minDigits pin minimum number of digits + * @param maxDigits maximum number of digits to be displayed + * @param backToken token used with actionCallback (0 if unused)) + * @param shuffled if set to true, digits are shuffled in keypad + * @param validatePinCallback function calledto validate the pin code + * @param backCallback callback called on backspace is "pressed" in first digit + */ +void nbgl_useCaseKeypadPIN(const char *title, + uint8_t minDigits, + uint8_t maxDigits, + bool shuffled, + nbgl_pinValidCallback_t validatePinCallback, + nbgl_callback_t backCallback) +{ + keypadGenericUseCase( + title, minDigits, maxDigits, shuffled, true, validatePinCallback, backCallback); +} +#endif // NBGL_KEYPAD + #endif // HAVE_SE_TOUCH #endif // NBGL_USE_CASE diff --git a/lib_standard_app/main.c b/lib_standard_app/main.c index a04b360e2..fc0b0fbb5 100644 --- a/lib_standard_app/main.c +++ b/lib_standard_app/main.c @@ -46,8 +46,10 @@ WEAK void common_app_init(void) io_seproxyhal_init(); +#ifdef HAVE_IO_USB USB_power(0); USB_power(1); +#endif #ifdef HAVE_BLE BLE_power(0, NULL); @@ -83,7 +85,9 @@ WEAK void standalone_app_main(void) // - the NanoX goes on battery power and display the lock screen // - the user plug the NanoX instead of entering its pin // - the device is frozen, battery should be removed +#ifdef HAVE_IO_USB USB_power(0); +#endif #ifdef HAVE_BLE BLE_power(0, NULL); #endif diff --git a/src/cx_stubs.S b/src/cx_stubs.S index 49d1ade68..c4ab57a23 100644 --- a/src/cx_stubs.S +++ b/src/cx_stubs.S @@ -160,6 +160,7 @@ CX_TRAMPOLINE _NR_cx_cipher_reset cx_cipher_reset CX_TRAMPOLINE _NR_cx_cmac_start cx_cmac_start CX_TRAMPOLINE _NR_cx_cmac_update cx_cmac_update CX_TRAMPOLINE _NR_cx_cmac_finish cx_cmac_finish +CX_TRAMPOLINE _NR_cx_aes_siv_reset cx_aes_siv_reset .thumb_func cx_trampoline_helper: diff --git a/src/syscalls.c b/src/syscalls.c index 076b95ece..3e8b5edc3 100644 --- a/src/syscalls.c +++ b/src/syscalls.c @@ -252,11 +252,11 @@ cx_err_t cx_aes_set_key_hw(const cx_aes_key_t *keys, uint32_t mode) return SVC_cx_call(SYSCALL_cx_aes_set_key_hw_ID, parameters); } -void cx_aes_reset_hw(void) +cx_err_t cx_aes_reset_hw(void) { unsigned int parameters[2]; parameters[1] = 0; - SVC_cx_call(SYSCALL_cx_aes_reset_hw_ID, parameters); + return SVC_cx_call(SYSCALL_cx_aes_reset_hw_ID, parameters); } cx_err_t cx_aes_block_hw(const unsigned char *inblock, unsigned char *outblock) @@ -1835,13 +1835,12 @@ void nvm_write_page(unsigned int page_adr, bool force) return; } -void nvm_erase_page(unsigned int page_adr) +unsigned int nvm_erase_page(unsigned int page_adr) { unsigned int parameters[2]; parameters[0] = (unsigned int) page_adr; parameters[1] = 0; - SVC_Call(SYSCALL_nvm_erase_page_ID, parameters); - return; + return (unsigned int) SVC_Call(SYSCALL_nvm_erase_page_ID, parameters); } try_context_t *try_context_get(void) diff --git a/unit-tests/lib_nbgl/CMakeLists.txt b/unit-tests/lib_nbgl/CMakeLists.txt index 815bc2a08..5ee877e25 100644 --- a/unit-tests/lib_nbgl/CMakeLists.txt +++ b/unit-tests/lib_nbgl/CMakeLists.txt @@ -78,7 +78,7 @@ add_library(nbgl_obj SHARED ../../lib_nbgl/src/nbgl_obj.c) target_link_libraries(test_nbgl_fonts PUBLIC cmocka gcov nbgl_fonts nbgl_stubs) target_link_libraries(test_nbgl_obj_pool PUBLIC cmocka gcov nbgl_obj_pool) -target_link_libraries(test_nbgl_screen PUBLIC cmocka gcov nbgl_screen nbgl_obj_pool) +target_link_libraries(test_nbgl_screen PUBLIC cmocka gcov nbgl_screen nbgl_obj_pool nbgl_stubs) target_link_libraries(test_nbgl_obj PUBLIC cmocka gcov nbgl_obj nbgl_screen nbgl_obj_pool nbgl_fonts nbgl_stubs) add_test(test_nbgl_fonts test_nbgl_fonts) diff --git a/unit-tests/lib_nbgl/nbgl_stubs.c b/unit-tests/lib_nbgl/nbgl_stubs.c index c9913caf8..9605d7215 100644 --- a/unit-tests/lib_nbgl/nbgl_stubs.c +++ b/unit-tests/lib_nbgl/nbgl_stubs.c @@ -8,7 +8,9 @@ #include #include #include "nbgl_fonts.h" +#include "nbgl_touch.h" #include "ux_loc.h" +#include "os_helpers.h" #include "os_task.h" void fetch_language_packs(void); diff --git a/unit-tests/lib_nbgl/test_nbgl_screen.c b/unit-tests/lib_nbgl/test_nbgl_screen.c index 3ef09bff4..8f542b207 100644 --- a/unit-tests/lib_nbgl/test_nbgl_screen.c +++ b/unit-tests/lib_nbgl/test_nbgl_screen.c @@ -9,6 +9,7 @@ #include #include "nbgl_screen.h" #include "nbgl_debug.h" +#include "nbgl_touch.h" #include "ux_loc.h" #include "os_task.h" @@ -46,6 +47,15 @@ void nbgl_objDraw(nbgl_obj_t *obj) UNUSED(obj); } +void nbgl_touchHandler(bool fromUx, + nbgl_touchStatePosition_t *touchStatePosition, + uint32_t currentTime) +{ + UNUSED(fromUx); + UNUSED(touchStatePosition); + UNUSED(currentTime); +} + static void test_push_pop(void **state __attribute__((unused))) { nbgl_obj_t **elements0, **elements1, **elements2, **elements3;