From 280278887233204fa52a9874f1031c14b71a70dd Mon Sep 17 00:00:00 2001 From: Charles-Edouard de la Vergne Date: Fri, 3 Jan 2025 16:14:33 +0100 Subject: [PATCH] Add missing API or feature for Nano on NBGL --- lib_nbgl/src/nbgl_use_case.c | 1 + lib_nbgl/src/nbgl_use_case_nanos.c | 641 +++++++++++++++++++++++------ 2 files changed, 527 insertions(+), 115 deletions(-) diff --git a/lib_nbgl/src/nbgl_use_case.c b/lib_nbgl/src/nbgl_use_case.c index 79eb6a636..1fffdf91d 100644 --- a/lib_nbgl/src/nbgl_use_case.c +++ b/lib_nbgl/src/nbgl_use_case.c @@ -3056,6 +3056,7 @@ void nbgl_useCaseReviewBlindSigning(nbgl_operationType_t operationT blindSigningWarning(); } + /** * @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 diff --git a/lib_nbgl/src/nbgl_use_case_nanos.c b/lib_nbgl/src/nbgl_use_case_nanos.c index 0b821c461..e3ec74cda 100644 --- a/lib_nbgl/src/nbgl_use_case_nanos.c +++ b/lib_nbgl/src/nbgl_use_case_nanos.c @@ -30,6 +30,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; @@ -48,20 +49,24 @@ typedef struct HomeContext_s { const char *tagline; const nbgl_genericContents_t *settingContents; const nbgl_contentInfoList_t *infosList; + const nbgl_homeAction_t *action; nbgl_callback_t quitCallback; } HomeContext_t; typedef enum { NONE_USE_CASE, 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, HOME_USE_CASE, - INFO_USE_CASE, + ACTION_USE_CASE, SETTINGS_USE_CASE, + INFO_USE_CASE, } ContextType_t; typedef struct UseCaseContext_s { @@ -376,34 +381,80 @@ 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); } @@ -414,16 +465,70 @@ static void displayReviewPage(nbgl_stepPosition_t pos) // 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; + uint8_t reviewPages = 0; + 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; + if (context.type == STREAMING_BLIND_SIGN_START_REVIEW_USE_CASE) { + // Determine the 1st page to display tag/values + reviewPages = 1; // 1st page is for warning + reviewPages++; // 2nd page is for title + if (context.review.reviewSubTitle) { + reviewPages++; // 3rd page is for subtitle + } + // Determine which page to display + if (context.currentPage < reviewPages) { + // header page(s) + switch (context.currentPage) { + case 0: + // warning page + icon = &C_icon_warning; + text = "Blind\nsigning"; + break; + case 1: + // title page + icon = context.review.icon; + text = context.review.reviewTitle; + break; + case 2: + // subtitle page + text = context.review.reviewSubTitle; + break; + default: + break; + } + } + else { + nbgl_useCaseSpinner("Processing"); + onReviewAccept(); + return; + } + } + else if (context.type == STREAMING_START_REVIEW_USE_CASE) { + // Determine the 1st page to display tag/values + reviewPages = 1; // 1st page is for title + if (context.review.reviewSubTitle) { + reviewPages++; // 3rd page is for subtitle + } + // Determine which page to display + if (context.currentPage < reviewPages) { + // header page(s) + switch (context.currentPage) { + case 0: + // title page + icon = context.review.icon; + text = context.review.reviewTitle; + break; + case 1: + // subtitle page + text = context.review.reviewSubTitle; + break; + default: + break; + } } else { nbgl_useCaseSpinner("Processing"); @@ -543,18 +648,40 @@ static void displaySettingsPage(nbgl_stepPosition_t pos, bool toogle_state) static void startUseCaseHome(void) { - if (context.type == SETTINGS_USE_CASE) { - context.currentPage = 1; - } - else if (context.type == INFO_USE_CASE) { - context.currentPage = 2; + int8_t addPages = 0; + if (context.home.action) { + addPages++; } - else { - context.currentPage = 0; + switch (context.type) { + case ACTION_USE_CASE: + // Action page index + context.currentPage = 1; + break; + 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.action) { + context.nbPages += addPages; + } displayHomePage(FORWARD_DIRECTION); } @@ -591,64 +718,61 @@ static void startUseCaseSettings(void) // 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; - } - } + homeIndex = currentIndex++; + if (context.home.action) { + 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.action->icon; + text = PIC(context.home.action->text); + context.stepCallback = context.home.action->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); @@ -704,10 +828,194 @@ static void displayChoicePage(nbgl_stepPosition_t pos) 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; + // + 3 because 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); +} + /********************** * 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 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 +1039,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.action = action; context.home.quitCallback = quitCallback; if (initSettingPage != INIT_HOME_PAGE) { @@ -771,21 +1078,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,15 +1221,14 @@ 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) { @@ -958,16 +1336,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 = 1 + 1; // 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 = 1 + 1 + 1; // Warning + Start page + trick for review continue displayStreamingReviewPage(FORWARD_DIRECTION); }