diff --git a/Makefile.standard_app b/Makefile.standard_app index 5412343f6..5254f607b 100644 --- a/Makefile.standard_app +++ b/Makefile.standard_app @@ -79,6 +79,38 @@ ifneq ($(DISABLE_DEFAULT_IO_SEPROXY_BUFFER_SIZE), 1) endif endif +##################################################################### +# NBGL # +##################################################################### +ifeq ($(TARGET_NAME),$(filter $(TARGET_NAME),TARGET_NANOS TARGET_NANOX TARGET_NANOS2)) +ifeq ($(ENABLE_NBGL_FOR_NANO_DEVICES), 1) +USE_NBGL = 1 +else +USE_NBGL = 0 +endif +else +USE_NBGL = 1 +endif + +ifeq ($(ENABLE_NBGL_QRCODE), 1) +ifeq ($(TARGET_NAME),$(filter $(TARGET_NAME),TARGET_STAX TARGET_FLEX)) + DEFINES += NBGL_QRCODE + SDK_SOURCE_PATH += qrcode +endif +endif + +ifeq ($(ENABLE_NBGL_KEYBOARD), 1) +ifeq ($(TARGET_NAME),$(filter $(TARGET_NAME),TARGET_STAX TARGET_FLEX)) + DEFINES += NBGL_KEYBOARD +endif +endif + +ifeq ($(ENABLE_NBGL_KEYPAD), 1) +ifeq ($(TARGET_NAME),$(filter $(TARGET_NAME),TARGET_STAX TARGET_FLEX)) + DEFINES += NBGL_KEYPAD +endif +endif + ##################################################################### # STANDARD DEFINES # ##################################################################### @@ -112,7 +144,7 @@ ifneq ($(DISABLE_STANDARD_WEBUSB), 1) endif ifneq ($(DISABLE_STANDARD_BAGL_UX_FLOW), 1) -ifeq ($(USE_NBGL),0) +ifneq ($(USE_NBGL), 1) DEFINES += HAVE_UX_FLOW endif endif @@ -126,31 +158,15 @@ SDK_SOURCE_PATH += lib_standard_app endif ifneq ($(DISABLE_STANDARD_APP_SYNC_RAPDU), 1) +# On LNS only activate it by default if using NBGL. +# This impact stack usage and shouldn't be activated on all apps silently ifneq ($(TARGET_NAME),TARGET_NANOS) DEFINES += STANDARD_APP_SYNC_RAPDU +else +ifeq ($(ENABLE_NBGL_FOR_NANO_DEVICES), 1) +DEFINES += STANDARD_APP_SYNC_RAPDU endif endif - -##################################################################### -# NBGL # -##################################################################### -ifeq ($(ENABLE_NBGL_QRCODE), 1) -ifeq ($(TARGET_NAME),$(filter $(TARGET_NAME),TARGET_STAX TARGET_FLEX)) - DEFINES += NBGL_QRCODE - SDK_SOURCE_PATH += qrcode -endif -endif - -ifeq ($(ENABLE_NBGL_KEYBOARD), 1) -ifeq ($(TARGET_NAME),$(filter $(TARGET_NAME),TARGET_STAX TARGET_FLEX)) - DEFINES += NBGL_KEYBOARD -endif -endif - -ifeq ($(ENABLE_NBGL_KEYPAD), 1) -ifeq ($(TARGET_NAME),$(filter $(TARGET_NAME),TARGET_STAX TARGET_FLEX)) - DEFINES += NBGL_KEYPAD -endif endif ##################################################################### diff --git a/lib_nbgl/include/nbgl_use_case.h b/lib_nbgl/include/nbgl_use_case.h index d6a5d3f13..6751d9938 100644 --- a/lib_nbgl/include/nbgl_use_case.h +++ b/lib_nbgl/include/nbgl_use_case.h @@ -235,6 +235,17 @@ void nbgl_useCaseGenericConfiguration(const char *title, const nbgl_genericContents_t *contents, nbgl_callback_t quitCallback); +void nbgl_useCaseChoice(const nbgl_icon_details_t *icon, + const char *message, + const char *subMessage, + const char *confirmText, + const char *rejectString, + nbgl_choiceCallback_t callback); + +void nbgl_useCaseStatus(const char *message, bool isSuccess, nbgl_callback_t quitCallback); + +void nbgl_useCaseSpinner(const char *text); + #ifdef HAVE_SE_TOUCH // utils uint8_t nbgl_useCaseGetNbTagValuesInPage(uint8_t nbPairs, @@ -278,18 +289,11 @@ void nbgl_useCaseGenericSettings(const char *appName, 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, - const char *confirmText, - const char *rejectString, - nbgl_choiceCallback_t callback); void nbgl_useCaseConfirm(const char *message, const char *subMessage, const char *confirmText, const char *rejectText, nbgl_callback_t callback); -void nbgl_useCaseStatus(const char *message, bool isSuccess, nbgl_callback_t quitCallback); void nbgl_useCaseReviewStart(const nbgl_icon_details_t *icon, const char *reviewTitle, const char *reviewSubTitle, @@ -362,38 +366,9 @@ DEPRECATED static inline void nbgl_useCaseKeypad(const char *titl validatePinCallback, actionCallback); } -#endif -#else // HAVE_SE_TOUCH -void nbgl_useCaseHome(const char *appName, - const nbgl_icon_details_t *appIcon, - const char *appVersion, - const char *tagline, - nbgl_callback_t aboutCallback, - nbgl_callback_t quitCallback); -void nbgl_useCaseSettings(uint8_t initPage, - uint8_t nbPages, - nbgl_callback_t quitCallback, - nbgl_navCallback_t navCallback, - nbgl_actionCallback_t actionCallback); -void nbgl_useCaseRegularReview(uint8_t initPage, uint8_t nbPages, nbgl_navCallback_t navCallback); -void nbgl_useCaseForwardOnlyReview(nbgl_navCallback_t navCallback); -void nbgl_useCaseStaticReview(nbgl_contentTagValueList_t *tagValueList, - const nbgl_icon_details_t *icon, - const char *reviewTitle, - const char *acceptText, - const char *rejectText, - nbgl_choiceCallback_t callback); -void nbgl_useCaseAddressConfirmation(const nbgl_icon_details_t *icon, - const char *title, - const char *address, - nbgl_choiceCallback_t callback); -void nbgl_useCaseAddressConfirmationExt(const nbgl_icon_details_t *icon, - const char *title, - const char *address, - nbgl_choiceCallback_t callback, - const nbgl_contentTagValueList_t *tagValueList); +#endif // NBGL_KEYPAD #endif // HAVE_SE_TOUCH -void nbgl_useCaseSpinner(const char *text); + #ifdef __cplusplus } /* extern "C" */ #endif diff --git a/lib_nbgl/src/nbgl_use_case_nanos.c b/lib_nbgl/src/nbgl_use_case_nanos.c index b122da533..ce0aeda00 100644 --- a/lib_nbgl/src/nbgl_use_case_nanos.c +++ b/lib_nbgl/src/nbgl_use_case_nanos.c @@ -26,287 +26,681 @@ **********************/ typedef struct ReviewContext_s { - nbgl_navCallback_t onNav; - nbgl_choiceCallback_t onChoice; - nbgl_contentTagValueList_t tagValueList; - const nbgl_icon_details_t *icon; - const char *reviewTitle; - const char *address; // for address confirmation review - const char *acceptText; - const char *rejectText; - bool forwardNavOnly; + nbgl_choiceCallback_t onChoice; + const nbgl_contentTagValueList_t *tagValueList; + const nbgl_icon_details_t *icon; + const char *reviewTitle; + const char *address; // for address confirmation review } ReviewContext_t; +typedef struct ChoiceContext_s { + const nbgl_icon_details_t *icon; + const char *message; + const char *subMessage; + const char *confirmText; + const char *cancelText; + nbgl_choiceCallback_t onChoice; +} ChoiceContext_t; + typedef struct HomeContext_s { - const char *appName; - const nbgl_icon_details_t *appIcon; - const char *appVersion; - const char *tagline; - nbgl_callback_t aboutCallback; - nbgl_callback_t quitCallback; + const char *appName; + const nbgl_icon_details_t *appIcon; + const char *tagline; + const nbgl_genericContents_t *settingContents; + const nbgl_contentInfoList_t *infosList; + nbgl_callback_t quitCallback; } HomeContext_t; -typedef struct SettingsContext_s { - nbgl_navCallback_t onNav; - nbgl_callback_t quitCallback; - nbgl_actionCallback_t actionCallback; -} SettingsContext_t; - typedef enum { + NONE_USE_CASE, REVIEW_USE_CASE, + ADDRESS_REVIEW_USE_CASE, + STREAMING_START_REVIEW_USE_CASE, + STREAMING_CONTINUE_REVIEW_USE_CASE, + STREAMING_FINISH_REVIEW_USE_CASE, + CHOICE_USE_CASE, HOME_USE_CASE, - SETTINGS_USE_CASE + INFO_USE_CASE, + SETTINGS_USE_CASE, } ContextType_t; typedef struct UseCaseContext_s { - ContextType_t type; - uint8_t nbPages; - int8_t currentPage; - nbgl_step_t stepCtx; - nbgl_stepDesc_t step; + ContextType_t type; + uint8_t nbPages; + int8_t currentPage; + nbgl_stepCallback_t + stepCallback; ///< if not NULL, function to be called on "double-key" action union { - ReviewContext_t review; - HomeContext_t home; - SettingsContext_t settings; + ReviewContext_t review; + ChoiceContext_t choice; + HomeContext_t home; }; } UseCaseContext_t; /********************** * STATIC VARIABLES **********************/ -// char buffers to build some strings -static char appDescription[APP_DESCRIPTION_MAX_LEN]; - static UseCaseContext_t context; /********************** * STATIC FUNCTIONS **********************/ - -static void buttonCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event); static void displayReviewPage(nbgl_stepPosition_t pos); +static void displayStreamingReviewPage(nbgl_stepPosition_t pos); 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 startUseCaseHome(void); +static void startUseCaseInfo(void); +static void startUseCaseSettings(void); +static void startUseCaseSettingsAtPage(uint8_t initSettingPage); + +// 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 TAG_VALUE_LIST: + return content->content.tagValueList.nbPairs; + case SWITCHES_LIST: + return content->content.switchesList.nbSwitches; + case INFOS_LIST: + return content->content.infosList.nbInfos; + default: + return 0; + } +} -static void onAccept(void) +// Helper to retrieve the content inside a nbgl_genericContents_t using +// either the contentsList or using the contentGetterCallback +static const nbgl_content_t *getContentAtIdx(const nbgl_genericContents_t *genericContents, + int8_t contentIdx, + nbgl_content_t *content) +{ + if (contentIdx < 0 || contentIdx >= genericContents->nbContents) { + LOG_DEBUG(USE_CASE_LOGGER, "No content available at %d\n", contentIdx); + return NULL; + } + + if (genericContents->callbackCallNeeded) { + // Retrieve content through callback, but first memset the content. + memset(content, 0, sizeof(nbgl_content_t)); + genericContents->contentGetterCallback(contentIdx, content); + return content; + } + else { + // Retrieve content through list + return PIC(&genericContents->contentsList[contentIdx]); + } +} + +// 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) +{ + const nbgl_content_t *p_content; + uint8_t nbPages = 0; + uint8_t elemNbPages = 0; + + for (int i = 0; i < genericContents->nbContents; i++) { + p_content = getContentAtIdx(genericContents, i, content); + elemNbPages = getContentNbElement(p_content); + if (nbPages + elemNbPages > elemIdx) { + *elemContentIdx = context.currentPage - nbPages; + break; + } + nbPages += elemNbPages; + } + + return p_content; +} + +static void getPairData(const nbgl_contentTagValueList_t *tagValueList, + uint8_t index, + const char **item, + const char **value) +{ + const nbgl_contentTagValue_t *pair; + + if (tagValueList->pairs != NULL) { + pair = PIC(&tagValueList->pairs[index]); + } + else { + pair = PIC(tagValueList->callback(index)); + } + *item = pair->item; + *value = pair->value; +} + +static void onReviewAccept(void) { if (context.review.onChoice) { context.review.onChoice(true); } } -static void onReject(void) +static void onReviewReject(void) { if (context.review.onChoice) { context.review.onChoice(false); } } +static void onChoiceAccept(void) +{ + if (context.choice.onChoice) { + context.choice.onChoice(true); + } +} + +static void onChoiceReject(void) +{ + if (context.choice.onChoice) { + context.choice.onChoice(false); + } +} + static void onSettingsAction(void) { - if (context.settings.actionCallback) { - context.settings.actionCallback(context.currentPage); + nbgl_content_t content; + uint8_t elemIdx; + + const nbgl_content_t *p_content = getContentElemAtIdx( + context.home.settingContents, context.currentPage, &elemIdx, &content); + + 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; + displaySettingsPage(FORWARD_DIRECTION, true); + if (p_content->contentActionCallback != NULL) { + nbgl_contentActionCallback_t onContentAction + = PIC(p_content->contentActionCallback); + onContentAction(contentSwitch->token, state, context.currentPage); + } + break; + } + default: + break; } } static void drawStep(nbgl_stepPosition_t pos, const nbgl_icon_details_t *icon, -#ifdef BUILD_SCREENSHOTS - uint16_t txtId, - uint16_t subTxtId, -#endif // BUILD_SCREENSHOTS - const char *txt, - const char *subTxt) + const char *txt, + const char *subTxt, + nbgl_stepButtonCallback_t onActionCallback) { - if (context.nbPages > 1) { - pos |= NEITHER_FIRST_NOR_LAST_STEP; - } - else { - pos |= GET_POS_OF_STEP(context.currentPage, context.nbPages); - } + pos |= GET_POS_OF_STEP(context.currentPage, context.nbPages); if (icon == NULL) { - context.stepCtx = nbgl_stepDrawText(pos, - buttonCallback, - NULL, -#ifdef BUILD_SCREENSHOTS - txtId, - subTxtId, -#endif // BUILD_SCREENSHOTS - txt, - subTxt, - BOLD_TEXT1_INFO, - false); + nbgl_stepDrawText(pos, onActionCallback, NULL, txt, subTxt, BOLD_TEXT1_INFO, false); } else { nbgl_layoutCenteredInfo_t info; info.icon = icon; info.text1 = txt; info.text2 = subTxt; -#ifdef BUILD_SCREENSHOTS - info.textId1 = 0xFFFF; // There is no valid string ID, here - info.textId2 = 0xFFFF; -#endif // BUILD_SCREENSHOTS - info.onTop = false; - info.style = BOLD_TEXT1_INFO; - context.stepCtx = nbgl_stepDrawCenteredInfo(pos, buttonCallback, NULL, &info, false); + info.onTop = false; + info.style = BOLD_TEXT1_INFO; + nbgl_stepDrawCenteredInfo(pos, onActionCallback, NULL, &info, false); } } -static void buttonCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event) +static bool buttonGenericCallback(nbgl_buttonEvent_t event, nbgl_stepPosition_t *pos) { - nbgl_stepPosition_t pos; - - UNUSED(stepCtx); - // create text_area for main text if (event == BUTTON_LEFT_PRESSED) { if (context.currentPage > 0) { context.currentPage--; } else { - context.currentPage = (context.nbPages - 1); + // Drop the event + return false; } - pos = BACKWARD_DIRECTION; + *pos = BACKWARD_DIRECTION; } else if (event == BUTTON_RIGHT_PRESSED) { if (context.currentPage < (int) (context.nbPages - 1)) { context.currentPage++; } else { - context.currentPage = 0; + // Drop the event + return false; } - pos = FORWARD_DIRECTION; + *pos = FORWARD_DIRECTION; } else { - if ((event == BUTTON_BOTH_PRESSED) && (context.step.callback != NULL)) { - context.step.callback(); + if ((event == BUTTON_BOTH_PRESSED) && (context.stepCallback != NULL)) { + context.stepCallback(); } + return false; + } + return true; +} + +static void reviewCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event) +{ + UNUSED(stepCtx); + nbgl_stepPosition_t pos; + + if (!buttonGenericCallback(event, &pos)) { return; } - if ((context.type == REVIEW_USE_CASE) || (context.type == SETTINGS_USE_CASE)) { - displayReviewPage(pos); + + displayReviewPage(pos); +} + +static void streamingReviewCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event) +{ + UNUSED(stepCtx); + nbgl_stepPosition_t pos; + + if (!buttonGenericCallback(event, &pos)) { + return; } - else { - displayHomePage(pos); + + displayStreamingReviewPage(pos); +} + +static void settingsCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event) +{ + UNUSED(stepCtx); + nbgl_stepPosition_t pos; + + if (!buttonGenericCallback(event, &pos)) { + return; + } + + displaySettingsPage(pos, false); +} + +static void infoCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event) +{ + UNUSED(stepCtx); + nbgl_stepPosition_t pos; + + if (!buttonGenericCallback(event, &pos)) { + return; + } + + displayInfoPage(pos); +} + +static void homeCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event) +{ + UNUSED(stepCtx); + nbgl_stepPosition_t pos; + + if (!buttonGenericCallback(event, &pos)) { + return; + } + + displayHomePage(pos); +} + +static void choiceCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event) +{ + UNUSED(stepCtx); + nbgl_stepPosition_t pos; + + if (!buttonGenericCallback(event, &pos)) { + return; + } + + displayChoicePage(pos); +} + +static void statusButtonCallback(nbgl_step_t stepCtx, nbgl_buttonEvent_t event) +{ + UNUSED(stepCtx); + if (event == BUTTON_BOTH_PRESSED) { + if (context.stepCallback != NULL) { + context.stepCallback(); + } + } +} + +// callback used for timeout +static void statusTickerCallback(void) +{ + if (context.stepCallback != NULL) { + context.stepCallback(); } } // function used to display the current page in review static void displayReviewPage(nbgl_stepPosition_t pos) { - memset(&context.step, 0, sizeof(context.step)); + const char *text = NULL; + const char *subText = NULL; + const nbgl_icon_details_t *icon = NULL; - if (context.type == REVIEW_USE_CASE) { - if (context.review.onNav != NULL) { - // try to get content for this page/step - if (context.review.onNav(context.currentPage, &context.step) == false) { - return; - } + context.stepCallback = NULL; + + if (context.currentPage == 0) { // title page + icon = context.review.icon; + text = context.review.reviewTitle; + } + else if (context.currentPage == (context.nbPages - 2)) { // accept page + icon = &C_icon_validate_14; + text = "Approve"; + context.stepCallback = onReviewAccept; + } + else if (context.currentPage == (context.nbPages - 1)) { // reject page + icon = &C_icon_crossmark; + text = "Reject"; + context.stepCallback = onReviewReject; + } + else if ((context.review.address != NULL) + && (context.currentPage == 1)) { // 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); + getPairData(context.review.tagValueList, pairIndex, &text, &subText); + } + + drawStep(pos, icon, text, subText, reviewCallback); + 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; + + 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 { - if (context.currentPage == 0) { // title page - context.step.icon = context.review.icon; - context.step.text = context.review.reviewTitle; - } - else if (context.currentPage == (context.nbPages - 2)) { // accept page - context.step.icon = &C_icon_validate_14; - context.step.text = context.review.acceptText; - context.step.callback = onAccept; - } - else if (context.currentPage == (context.nbPages - 1)) { // reject page - context.step.icon = &C_icon_crossmark; - context.step.text = context.review.rejectText; - context.step.callback = onReject; - } - else if ((context.review.address != NULL) - && (context.currentPage == 1)) { // address confirmation and 2nd page - context.step.text = "Address"; - context.step.subText = context.review.address; - } - else { - uint8_t pairIndex = (context.review.address != NULL) ? (context.currentPage - 2) - : (context.currentPage - 1); - context.step.text = context.review.tagValueList.pairs[pairIndex].item; - context.step.subText = context.review.tagValueList.pairs[pairIndex].value; - } + nbgl_useCaseSpinner("Processing"); + onReviewAccept(); + return; } } - else if (context.type == SETTINGS_USE_CASE) { - if (context.currentPage < (context.nbPages - 1)) { - // try to get content for this page/step - if ((context.settings.onNav == NULL) - || (context.settings.onNav(context.currentPage, &context.step) == false)) { - return; - } - context.step.callback = onSettingsAction; + else if (context.type == STREAMING_CONTINUE_REVIEW_USE_CASE) { + if (context.currentPage < context.review.tagValueList->nbPairs) { + getPairData(context.review.tagValueList, context.currentPage, &text, &subText); } - else { // last page is for quit - context.step.icon = &C_icon_back_x; - context.step.text = "Back"; - context.step.callback = context.settings.quitCallback; + 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; + } + } + + drawStep(pos, icon, text, subText, streamingReviewCallback); + nbgl_refresh(); +} - const char *txt = NULL; - if (context.step.text != NULL) { - txt = context.step.text; +// function used to display the current page in info +static void displayInfoPage(nbgl_stepPosition_t pos) +{ + const char *text = NULL; + const char *subText = NULL; + const nbgl_icon_details_t *icon = NULL; + + context.stepCallback = NULL; + + if (context.currentPage < (context.nbPages - 1)) { + text = PIC( + ((const char *const *) PIC(context.home.infosList->infoTypes))[context.currentPage]); + subText = PIC( + ((const char *const *) PIC(context.home.infosList->infoContents))[context.currentPage]); } - if (context.step.init != NULL) { - context.step.init(); + else { + icon = &C_icon_back_x; + text = "Back"; + context.stepCallback = startUseCaseHome; } - drawStep(pos, - context.step.icon, -#ifdef BUILD_SCREENSHOTS - context.step.textId, - context.step.subTextId, -#endif // BUILD_SCREENSHOTS - txt, - context.step.subText); + + drawStep(pos, icon, text, subText, infoCallback); + nbgl_refresh(); +} + +// function used to display the current page in settings +static void displaySettingsPage(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)) { + 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; + } + } + else { // last page is for quit + icon = &C_icon_back_x; + text = "Back"; + context.stepCallback = startUseCaseHome; + } + + drawStep(pos, icon, text, subText, settingsCallback); nbgl_refresh(); } +static void startUseCaseHome(void) +{ + if (context.type == SETTINGS_USE_CASE) { + context.currentPage = 1; + } + else if (context.type == INFO_USE_CASE) { + context.currentPage = 2; + } + else { + context.currentPage = 0; + } + context.type = HOME_USE_CASE; + context.nbPages = 4; + + displayHomePage(FORWARD_DIRECTION); +} + +static void startUseCaseInfo(void) +{ + context.type = INFO_USE_CASE; + context.nbPages = context.home.infosList->nbInfos + 1; // For back screen + context.currentPage = 0; + + displayInfoPage(FORWARD_DIRECTION); +} + +static void startUseCaseSettingsAtPage(uint8_t initSettingPage) +{ + nbgl_content_t content; + const nbgl_content_t *p_content; + + 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); + context.nbPages += getContentNbElement(p_content); + } + context.currentPage = initSettingPage; + + displaySettingsPage(FORWARD_DIRECTION, false); +} + +static void startUseCaseSettings(void) +{ + startUseCaseSettingsAtPage(0); +} + // function used to display the current page in home static void displayHomePage(nbgl_stepPosition_t pos) { - memset(&context.step, 0, sizeof(context.step)); + const char *text = NULL; + const char *subText = NULL; + const nbgl_icon_details_t *icon = NULL; + + 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; + } + } + } + + // 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; + } + } + else { + context.currentPage += 1; + } + } switch (context.currentPage) { case 0: - context.step.icon = context.home.appIcon; - context.step.text = context.home.tagline; + icon = context.home.appIcon; + if (context.home.tagline != NULL) { + text = context.home.tagline; + } + else { + text = context.home.appName; + subText = "is ready"; + } break; case 1: - context.step.text = "Version"; - context.step.subText = context.home.appVersion; + icon = &C_icon_coggle; + text = "Settings"; + context.stepCallback = startUseCaseSettings; break; case 2: - context.step.icon = &C_icon_certificate; - context.step.text = "About"; - context.step.callback = context.home.aboutCallback; - break; - case 3: - context.step.icon = &C_icon_dashboard_x; - context.step.text = "Quit"; - context.step.callback = context.home.quitCallback; + icon = &C_icon_certificate; + text = "About"; + context.stepCallback = startUseCaseInfo; break; default: + icon = &C_icon_dashboard_x; + text = "Quit"; + context.stepCallback = context.home.quitCallback; break; } - const char *txt = NULL; - if (context.step.text != NULL) { - txt = context.step.text; + drawStep(pos, icon, text, subText, homeCallback); + nbgl_refresh(); +} + +// function used to display the current page in choice +static void displayChoicePage(nbgl_stepPosition_t pos) +{ + const char *text = NULL; + const char *subText = NULL; + const nbgl_icon_details_t *icon = NULL; + + context.stepCallback = NULL; + + // Handle case where there is no icon or subMessage + if (context.currentPage == 1 + && (context.choice.icon == NULL || context.choice.subMessage == NULL)) { + if (pos & BACKWARD_DIRECTION) { + context.currentPage -= 1; + } + else { + context.currentPage += 1; + } + } + + if (context.currentPage == 0) { // title page + text = context.choice.message; + if (context.choice.icon != NULL) { + icon = context.choice.icon; + } + else { + subText = context.choice.subMessage; + } + } + else if (context.currentPage == 1) { // sub-title page + // displayed only if there is both icon and submessage + text = context.choice.message; + subText = context.choice.subMessage; + } + else if (context.currentPage == 2) { // confirm page + icon = &C_icon_validate_14; + text = context.choice.confirmText; + context.stepCallback = onChoiceAccept; } - if (context.step.init != NULL) { - context.step.init(); + else { // cancel page + icon = &C_icon_crossmark; + text = context.choice.cancelText; + context.stepCallback = onChoiceReject; } - drawStep(pos, - context.step.icon, -#ifdef BUILD_SCREENSHOTS - context.step.textId, - context.step.subTextId, -#endif // BUILD_SCREENSHOTS - txt, - context.step.subText); + + drawStep(pos, icon, text, subText, choiceCallback); nbgl_refresh(); } @@ -315,226 +709,327 @@ static void displayHomePage(nbgl_stepPosition_t pos) **********************/ /** - * @brief draws the home page of an app (page on which we land when launching it from dashboard) + * @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. * * @param appName app name * @param appIcon app icon - * @param appVersion app version - * @param tagline text under app name (if NULL, it will be "\nisready") - * @param aboutCallback callback called when the "about" step is selected (double key) - * @param quitCallback callback called when the "quit" step is selected (double key) + * @param tagline text under app name (if NULL, it will be "\n is ready") + * @param initSettingPage if not INIT_HOME_PAGE, start directly the corresponding setting page + * @param settingContents setting contents to be displayed + * @param infosList infos to be displayed (version, license, developer, ...) + * @param action if not NULL, info used for an action button (on top of "Quit + * App" button/footer) + * @param quitCallback callback called when quit button is touched */ -void nbgl_useCaseHome(const char *appName, - const nbgl_icon_details_t *appIcon, - const char *appVersion, - const char *tagline, - nbgl_callback_t aboutCallback, - nbgl_callback_t quitCallback) +void nbgl_useCaseHomeAndSettings(const char *appName, + const nbgl_icon_details_t *appIcon, + const char *tagline, + const uint8_t initSettingPage, + const nbgl_genericContents_t *settingContents, + const nbgl_contentInfoList_t *infosList, + const nbgl_homeAction_t *action, + nbgl_callback_t quitCallback) { + UNUSED(action); // TODO support it at some point? + memset(&context, 0, sizeof(UseCaseContext_t)); - context.type = HOME_USE_CASE; - context.home.aboutCallback = aboutCallback; - context.home.quitCallback = quitCallback; + context.home.appName = appName; + context.home.appIcon = appIcon; + context.home.tagline = tagline; + context.home.settingContents = PIC(settingContents); + context.home.infosList = PIC(infosList); + context.home.quitCallback = quitCallback; - if (tagline == NULL) { - snprintf(appDescription, APP_DESCRIPTION_MAX_LEN, "%s\nis ready", appName); - context.home.tagline = appDescription; + if (initSettingPage != INIT_HOME_PAGE) { + startUseCaseSettingsAtPage(initSettingPage); } else { - context.home.tagline = tagline; + startUseCaseHome(); } - - context.home.appName = appName; - context.home.appIcon = appIcon; - context.home.appVersion = appVersion; - - context.nbPages = 4; - context.currentPage = 0; - - displayHomePage(FORWARD_DIRECTION); } /** - * @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 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 + * computed. * - * @param initPage page on which to start [0->(nbPages-1)] - * @param nbPages number of pages - * @param quitCallback callback called when "quit" step is selected (double button) - * @param navCallback callback called when pages are navigated with buttons - * @param actionCallback callback called when one of the navigations page is selected (double - * button) + * @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 choiceCallback callback called when operation is accepted (param is true) or rejected + * (param is false) */ -void nbgl_useCaseSettings(uint8_t initPage, - uint8_t nbPages, - nbgl_callback_t quitCallback, - nbgl_navCallback_t navCallback, - nbgl_actionCallback_t actionCallback) +void nbgl_useCaseReview(nbgl_operationType_t operationType, + 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(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? + memset(&context, 0, sizeof(UseCaseContext_t)); - // memorize context - context.type = SETTINGS_USE_CASE; - context.settings.onNav = navCallback; - context.settings.quitCallback = quitCallback; - context.settings.actionCallback = actionCallback; - - context.nbPages = nbPages + 1; - context.currentPage = initPage; + 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; + displayReviewPage(FORWARD_DIRECTION); } /** - * @brief Draws a flow of pages of a review. Navigation is available for all pages - * For each page, the given navCallback will be called to get the content. - * When navigating before the first page of after the last page, the page number will be -1 + * @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 + * computed. * - * @param initPage page on which to start [0->(nbPages-1)] - * @param nbPages number of pages. - * @param navCallback callback called when navigation is touched + * @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 choiceCallback callback called when operation is accepted (param is true) or rejected + * (param is false) */ -void nbgl_useCaseRegularReview(uint8_t initPage, uint8_t nbPages, nbgl_navCallback_t navCallback) +void nbgl_useCaseReviewLight(nbgl_operationType_t operationType, + const nbgl_contentTagValueList_t *tagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + const char *finishTitle, + nbgl_choiceCallback_t choiceCallback) { - memset(&context, 0, sizeof(UseCaseContext_t)); - context.type = REVIEW_USE_CASE; - // memorize context - context.review.onNav = navCallback; - context.review.forwardNavOnly = false; - - context.currentPage = initPage; - context.nbPages = nbPages; - - displayReviewPage(FORWARD_DIRECTION); + return nbgl_useCaseReview(operationType, + tagValueList, + icon, + reviewTitle, + reviewSubTitle, + finishTitle, + choiceCallback); } /** - * @brief Draws a flow of pages of a review, without back key. - * It is possible to go to next page thanks to "tap to continue". - * 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 Draws a flow of pages of an extended address verification page. + * @note All tag/value pairs are provided in the API and the number of pages is automatically + * computed. * - * @param navCallback callback called when navigation "tap to continue" is touched, to get the - * content of next page + * @param address address to confirm (NULL terminated string) + * @param additionalTagValueList list of tag/value pairs (can be NULL) (must be persistent because + * no copy) + * @param callback callback called when button or footer is touched (if true, button, if false + * footer) + * @param icon icon used on the first 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 transaction is accepted (param is true) or rejected + * (param is false) */ -void nbgl_useCaseForwardOnlyReview(nbgl_navCallback_t navCallback) +void nbgl_useCaseAddressReview(const char *address, + const nbgl_contentTagValueList_t *additionalTagValueList, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + nbgl_choiceCallback_t choiceCallback) { - // memorize context - context.type = REVIEW_USE_CASE; - context.review.onNav = navCallback; - context.review.forwardNavOnly = true; + 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; + // + 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.nbPages += additionalTagValueList->nbPairs; + } + + displayReviewPage(FORWARD_DIRECTION); } /** - * @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 - * computed, the last page being a long press one + * @brief Draws a transient (3s) status page, either of success or failure, with the given message * - * @param tagValueList list of tag/value pairs - * @param icon icon to use in first page - * @param reviewTitle text to use in title page of the transaction - * @param acceptText text to use in validation page - * @param rejectText text to use in rejection page - * @param callback callback called when transaction is accepted (param is true) or rejected (param - * is false) + * @param message string to set in middle of page (Upper case for success) + * @param isSuccess if true, message is drawn in a Ledger style (with corners) + * @param quitCallback callback called when quit timer times out or status is manually dismissed */ -void nbgl_useCaseStaticReview(nbgl_contentTagValueList_t *tagValueList, - const nbgl_icon_details_t *icon, - const char *reviewTitle, - const char *acceptText, - const char *rejectText, - nbgl_choiceCallback_t callback) -{ - // memorize context - memset(&context, 0, sizeof(UseCaseContext_t)); - context.review.forwardNavOnly = false; - context.type = REVIEW_USE_CASE; - - memcpy(&context.review.tagValueList, tagValueList, sizeof(nbgl_contentTagValueList_t)); +void nbgl_useCaseStatus(const char *message, bool isSuccess, nbgl_callback_t quitCallback) +{ + UNUSED(isSuccess); // TODO add icon depending on isSuccess? - context.review.reviewTitle = reviewTitle; - context.review.icon = icon; - context.review.acceptText = acceptText; - context.review.rejectText = rejectText; - context.review.onChoice = callback; + memset(&context, 0, sizeof(UseCaseContext_t)); + context.stepCallback = quitCallback; + context.currentPage = 0; + context.nbPages = 1; - context.currentPage = 0; - // + 3 because 1 page for title and 2 pages at the end for accept/reject - context.nbPages = tagValueList->nbPairs + 3; + nbgl_screenTickerConfiguration_t ticker = { + .tickerCallback = PIC(statusTickerCallback), + .tickerIntervale = 0, // not periodic + .tickerValue = 3000 // 3 seconds + }; - displayReviewPage(FORWARD_DIRECTION); + nbgl_stepDrawText( + SINGLE_STEP, statusButtonCallback, &ticker, message, NULL, BOLD_TEXT1_INFO, false); + nbgl_refresh(); } /** - * @brief Draws an address confirmation use case. This page contains the given address in a - * tag/value layout + * @brief Draws a transient (3s) status page for the reviewStatusType * - * @param icon icon to be used on first page of address review - * @param title text to be used on first page of address review (NULL terminated string) - * @param address address to confirm (NULL terminated string) - * @param callback callback called when either confirm or reject page is double pressed + * @param reviewStatusType type of status to display + * @param quitCallback callback called when quit timer times out or status is manually dismissed */ -void nbgl_useCaseAddressConfirmation(const nbgl_icon_details_t *icon, - const char *title, - const char *address, - nbgl_choiceCallback_t callback) +void nbgl_useCaseReviewStatus(nbgl_reviewStatusType_t reviewStatusType, + nbgl_callback_t quitCallback) { - nbgl_useCaseAddressConfirmationExt(icon, title, address, callback, NULL); + const char *msg; + bool isSuccess; + switch (reviewStatusType) { + case STATUS_TYPE_OPERATION_SIGNED: + msg = "Operation signed"; + isSuccess = true; + break; + case STATUS_TYPE_OPERATION_REJECTED: + msg = "Operation rejected"; + isSuccess = false; + break; + case STATUS_TYPE_TRANSACTION_SIGNED: + msg = "Transaction signed"; + isSuccess = true; + break; + case STATUS_TYPE_TRANSACTION_REJECTED: + msg = "Transaction rejected"; + isSuccess = false; + break; + case STATUS_TYPE_MESSAGE_SIGNED: + msg = "Message signed"; + isSuccess = true; + break; + case STATUS_TYPE_MESSAGE_REJECTED: + msg = "Message rejected"; + isSuccess = false; + break; + case STATUS_TYPE_ADDRESS_VERIFIED: + msg = "Address verified"; + isSuccess = true; + break; + case STATUS_TYPE_ADDRESS_REJECTED: + msg = "Verification\ncancelled"; + isSuccess = false; + break; + default: + return; + } + nbgl_useCaseStatus(msg, isSuccess, quitCallback); } /** - * @brief draws an extended address verification page. This page contains the given address in a - * tag/value layout. + * @brief Start drawing the flow of pages of a review. + * @note This should be followed by calls to nbgl_useCaseReviewStreamingContinue and finally to + * nbgl_useCaseReviewStreamingFinish. * - * @param icon icon to be used on first page of address review - * @param title text to be used on first page of address review (NULL terminated string) - * @param address address to confirm (NULL terminated string) - * @param callback callback called when either confirm or reject page is double pressed - * @param tagValueList list of tag/value pairs (must be persistent because no copy) + * @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_useCaseAddressConfirmationExt(const nbgl_icon_details_t *icon, - const char *title, - const char *address, - nbgl_choiceCallback_t callback, - const nbgl_contentTagValueList_t *tagValueList) +void nbgl_useCaseReviewStreamingStart(nbgl_operationType_t operationType, + const nbgl_icon_details_t *icon, + const char *reviewTitle, + const char *reviewSubTitle, + nbgl_choiceCallback_t choiceCallback) { - // memorize context + UNUSED(operationType); // TODO adapt accept and reject text depending on this value? + UNUSED(reviewSubTitle); // TODO dedicated screen for it? + memset(&context, 0, sizeof(UseCaseContext_t)); - context.review.forwardNavOnly = false; - context.type = REVIEW_USE_CASE; + 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 - if (tagValueList) { - memcpy(&context.review.tagValueList, tagValueList, sizeof(nbgl_contentTagValueList_t)); - } + displayStreamingReviewPage(FORWARD_DIRECTION); +} - context.review.address = address; - context.review.reviewTitle = title; - context.review.icon = icon; - context.review.acceptText = "Approve"; - context.review.rejectText = "Reject"; - context.review.onChoice = callback; +void nbgl_useCaseReviewStreamingContinue(const nbgl_contentTagValueList_t *tagValueList, + nbgl_choiceCallback_t choiceCallback) +{ + memset(&context, 0, sizeof(UseCaseContext_t)); + context.type = STREAMING_CONTINUE_REVIEW_USE_CASE; + context.review.tagValueList = tagValueList; + context.review.onChoice = choiceCallback; + context.currentPage = 0; + context.nbPages = tagValueList->nbPairs + 1; // data + trick for review continue - 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 (tagValueList) { - context.nbPages += tagValueList->nbPairs; - } + displayStreamingReviewPage(FORWARD_DIRECTION); +} - displayReviewPage(FORWARD_DIRECTION); +void nbgl_useCaseReviewStreamingFinish(const char *finishTitle, + nbgl_choiceCallback_t choiceCallback) +{ + UNUSED(finishTitle); // TODO dedicated screen for it? + + memset(&context, 0, sizeof(UseCaseContext_t)); + context.type = STREAMING_FINISH_REVIEW_USE_CASE; + context.review.onChoice = choiceCallback; + context.currentPage = 0; + context.nbPages = 2; // 2 pages at the end for accept/reject + + displayStreamingReviewPage(FORWARD_DIRECTION); } /** - * @brief draw a spinner page with the given parameters. The spinner will "turn" automatically every - * 800 ms + * @brief draw a spinner page with the given parameters. * - * @param text text to use under spinner + * @param text text to use with the spinner */ void nbgl_useCaseSpinner(const char *text) { - // pageContext = nbgl_pageDrawSpinner(NULL, (const char*)text); - UNUSED(text); + drawStep(SINGLE_STEP, &C_icon_processing, text, NULL, NULL); nbgl_refresh(); } +void nbgl_useCaseChoice(const nbgl_icon_details_t *icon, + const char *message, + const char *subMessage, + const char *confirmText, + const char *cancelText, + nbgl_choiceCallback_t callback) +{ + memset(&context, 0, sizeof(UseCaseContext_t)); + context.type = CHOICE_USE_CASE; + context.choice.icon = icon; + context.choice.message = message; + context.choice.subMessage = subMessage; + context.choice.confirmText = confirmText; + context.choice.cancelText = cancelText; + context.choice.onChoice = callback; + context.currentPage = 0; + context.nbPages = 1 + 1 + 2; // 2 pages at the end for confirm/cancel + + displayChoicePage(FORWARD_DIRECTION); +}; + #endif // HAVE_SE_TOUCH #endif // NBGL_USE_CASE diff --git a/src/ledger_assert.c b/src/ledger_assert.c index 8cd5bf3fe..820267877 100644 --- a/src/ledger_assert.c +++ b/src/ledger_assert.c @@ -134,8 +134,14 @@ void __attribute__((noreturn)) assert_display_exit(void) #endif #ifdef HAVE_NBGL +#if defined(TARGET_NANOS) || defined(TARGET_NANOX) || defined(TARGET_NANOS2) +#define ICON_APP_WARNING C_icon_warning +#elif defined(TARGET_STAX) || defined(TARGET_FLEX) +#define ICON_APP_WARNING C_round_warning_64px +#endif + nbgl_useCaseChoice( - &C_Important_Circle_64px, "App error", assert_buffer, "Exit app", "Exit app", assert_exit); + &ICON_APP_WARNING, "App error", assert_buffer, "Exit app", "Exit app", assert_exit); #endif // Block until the user approve and the app is quit