Documentation
UI guidelines
Advanced display management

Advanced display management

Introduction

This article talks about more advanced flows that are sometimes needed when writing applications.

Cherry-picking steps at runtime

Usecase

In Display Management you learned how to use the UX_FLOW macro. This is quite handy but it comes with its drawbacks: everything needs to be declared at compilation time. What if you wished to change the flow at runtime?

Imagine an app where you have a flow for a transaction signature.

UX_FLOW(ux_transaction_signature,
         &step_review,
         &step_amount,
         &step_fee,
         &step_address,
         &step_accept,
         &step_reject
      );

This works for a transaction signature. But suppose there's an update to the protocol: now some transactions can also sign some arbitrary data. However, not all transactions are required to sign the arbitrary data, so you need two flows:

// The standard signature, nothing has changed.
UX_FLOW(ux_vanilla_transaction_signature,
         &step_review,
         &step_amount,
         &step_fee,
         &step_address,
         &step_accept,
         &step_reject
      );
 
// This flow is almost the same as the one above, except is contains "step_arbitrary_data".
UX_FLOW(ux_data_transaction_signature,
         &step_review,
         &step_amount,
         &step_fee,
         &step_address,
         &step_arbitrary_data, // Arbitrary data
         &step_accept,
         &step_reject
      );

And now somewhere in your app at runtime you will be able to decide which flow to use:

void launch_flow() {
   if (g.has_arbitrary_data) {
      ux_flow_init(0, ux_data_transaction_signature, NULL);
   } else {
      ux_flow_init(0, ux_vanilla_transaction_signature, NULL);
   }
}

Great, this works. But imagine that advanced users in the community also wants to be able to see the nonce for the transaction. But they want it to be optional.

You then need a flow for the vanilla signature, as well as a flow for the vanilla signature where you show the nonce. You also need the option to display the nonce when there is arbitrary data to sign, so you also need the "data signature" flow and the "data signature + nonce" flow.

// The standard signature, nothing has changed.
UX_FLOW(ux_vanilla_transaction_signature,
         &step_review,
         &step_amount,
         &step_fee,
         &step_address,
         &step_accept,
         &step_reject
      );
 
// The vanilla flow with a step to display the nonce.
UX_FLOW(ux_vanilla_nonce_transaction_signature,
         &step_review,
         &step_amount,
         &step_fee,
         &step_address,
         &step_nonce, // Nonce
         &step_accept,
         &step_reject
      );
 
// This is identical to the previous example, where we signed some arbitrary data.
UX_FLOW(ux_data_transaction_signature,
         &step_review,
         &step_amount,
         &step_fee,
         &step_address,
         &step_arbitrary_data, // Arbitrary data
         &step_accept,
         &step_reject
      );
 
// This is identical to the flow just above, except there is step to display the nonce.
UX_FLOW(ux_data_nonce_transaction_signature,
         &step_review,
         &step_amount,
         &step_fee,
         &step_address,
         &step_arbitrary_data, // Arbitrary data
         &step_nonce, // Nonce
         &step_accept,
         &step_reject
      );

And now somewhere in you app at runtime you can decide which flow to use:

void launch_flow() {
   if (g.has_arbitrary_data) {
      if (g.display_nonce) {
         ux_flow_init(0, ux_data_nonce_transaction_signature, NULL);
      } else {
         ux_flow_init(0, ux_data_transaction_signature, NULL);
      }
   } else {
      if (g.display_nonce) {
         ux_flow_init(0, ux_vanilla_nonce_transaction_signature, NULL);
      } else {
         ux_flow_init(0, ux_vanilla_transaction_signature, NULL);
      }
   }
}

Here the app has been updated to support the protocol and the advanced users in the community can display the nonce. But imagine a new upgrade to the protocol allows the fees to be paid by another user of the blockchain, called a relayer. Some transactions would need to hide the fees (displaying a fee of 0.000 is not an option because it would confuse users more than anything).

So you need 8 different flows. That escalated quickly! Indeed, for every little upgrade, the number of flows is doubled. Soon enough you'll end up with 16 or even 32 different flows. Notice that whilst the number of flows will grow exponentially, the number of different steps will only grow linearly (one for every new feature).

To fix this problem, you would need to define the UX_FLOW at runtime, cherry-picking which steps you wish to include depending on the details of our transaction.

Don't worry, Ledger has got your back! The fix is quite simple, so let's dive right into it.

Cherry-picking explained

The idea is to create an array of steps that is big enough to fit all the steps. Since steps grow linearly, this array won't get too big. Once this array created, you simply need to fill it with the steps you wish to include. Finally, you need to add a last step FLOW_END_STEP for it to work properly.

You can then call the ux_flow_init and pass in you array as argument.

// The maximum number of steps you will ever need. Here it's 8: step_review, step_amount,
// step_fee, step_address, step_arbitrary_data, step_nonce, step_accept, step_reject.
#define MAX_NUM_STEPS 8
 
// The array of steps. Notice the type used, as it's important if you wish to use ux_flow_init.
// Add `+ 1` to ensure there always is extra room for the last step, FLOW_END_STEP.
const ux_flow_step_t *ux_signature_flow[MAX_NUM_STEPS + 1];
void start_display() {
   uint8_t index = 0;
 
   // Set the first step to be `step_review`, and then increment `index`.
   ux_signature_flow[index++] = step_review;
   // Set the second step to be `step_amount`, and then increment `index`.
   ux_signature_flow[index++] = step_amount;
   // etc...
   ux_signature_flow[index++] = step_fee;
   ux_signature_flow[index++] = step_address;
   // You can now conditionally add steps at runtime!
   if (g.has_arbitrary_data) {
      ux_signature_flow[index++] = step_arbitrary_data;
   }
   if (g.display_nonce) {
      ux_signature_flow[index++] = step_nonce;
   }
   ux_signature_flow[index++] = step_accept;
   ux_signature_flow[index++] = step_reject;
 
   // Don't forget to finish your flow with this special step.
   ux_signature_flow[index++] = FLOW_END_STEP;
 
   // Start the display!
   ux_flow_init(0, ux_signature_flow, NULL);
}

Defining steps at runtime

In the previous section we saw that we could define a UX_FLOW at runtime. But we did this whilst still having steps defined statically. What if you wish to define steps at runtime too? This would give you a very fine-grained control over what you wish to display, without having to declare a step everytime.

Finding a step that would be generic enough to fit all you needs. Naively, the code we would expect would look something like that:

// Naive definition of a UX_FLOW.
// Note: Keep step_review, step_accept and
// step_reject because your flow will need those anyway.
UX_FLOW(ideal_dynamic_fow,
         &step_welcome,
         &step_generic, // A generic step that would fit all our needs.
         &step_quit
         FLOW_LOOP
   );

However this wouldn't work: if you only defined a single step, then how could it dynamically change its information? Pressing the right button would make it loop and you would end up on the same exact screen, and pressing the left button would lead to the same situation.

So here's a solution:

  • Add an extra step just before the step_generic step, and another one right after it.
  • Those extra steps are delimiters for the step_generic . They allow you to execute special logic to update the screen and redisplay it. They also allow us to keep track of an index, so that you know whether you are still within the array boundaries or not.

Here's what the code looks like.

UX_FLOW(dynamic_flow,
         &step_welcome,
 
         &step_upper_delimiter, // A special step that serves as the upper delimiter. It won't print anything on the screen.
         &step_generic, // The generic step that will actually display stuff on the screen.
         &step_lower_delimiter, // A special step that serves as the lower delimiter. It won't print anything on the screen.
 
         &step_quit,
         FLOW_LOOP
   );

The definition of step_upper_delimiter, step_lower_delimiter and step_generic could look like this:

// Note we're using UX_STEP_INIT because this step won't display anything.
UX_STEP_INIT(
   step_upper_delimiter,
   NULL,
   NULL,
   {
      // This function will be detailed later on.
      display_next_state(true);
   }
);
 
UX_STEP_NOCB(
   step_generic,
   bnnn_paging,
   {
      .title = global.title,
      .text = global.text,
   }
);
 
// Note we're using UX_STEP_INIT because this step won't display anything.
UX_STEP_INIT(
   step_lower_delimiter,
   NULL,
   NULL,
   {
      // This function will be detailed later on.
      display_next_state(false);
   }
);

As you can see, step_upper_delimiter and step_lower_delimiter are very similar. step_generic is a simple bnnn_paging that will always display what's located in global.title and global.text.

And now you only need to implement the special logic for this to work!

Inside the .h file, you only need to add an enum definition and an instance of this enum in our global structure:

// State of the dynamic display.
// Use to keep track of whether we are displaying screens that are inside the
// array (dynamic), or outside the array (static).
enum e_state {
   STATIC_SCREEN,
   DYNAMIC_SCREEN,
};
 
struct global {
   // An instance of our new enum
   enum e_state current_state;
 
   // The rest of the global stays unchanged...
}

Add all the business logic in the .c file. A helper function is used throughout the code. This function is yours to define, and basically fills the title\_buffer and the text\_buffer with the appropriate strings. Is returns a bool corresponding to whether or not it found data to fill the buffers. The :code:`bool forward parameter is used to indicate whether you wish to display the next screen or the previous screen.

The code is commented thoroughly, so take your time and read it carefully.

// This is a special function you must call for bnnn_paging to work properly in an edgecase.
// It does some weird stuff with the `G_ux` global which is defined by the SDK.
// No need to dig deeper into the code, a simple copy-paste will do.
void bnnn_paging_edgecase() {
    G_ux.flow_stack[G_ux.stack_count - 1].prev_index = G_ux.flow_stack[G_ux.stack_count - 1].index - 2;
    G_ux.flow_stack[G_ux.stack_count - 1].index--;
    ux_flow_relayout();
}
 
// Main function that handles all the business logic for our new display architecture.
void display_next_state(bool is_upper_delimiter) {
   if (is_upper_delimiter) { // We're called from the upper delimiter.
      if (global.current_state == STATIC_SCREEN) {
         // Fetch new data.
         bool dynamic_data = get_next_data(&global.title, &global.text, true);
 
         if (dynamic_data) {
             // We found some data to display so we now enter in dynamic mode.
             global.current_state = DYNAMIC_SCREEN;
         }
 
         // Move to the next step, which will display the screen.
         ux_flow_next();
      } else {
         // The previous screen was NOT a static screen, so we were already in a dynamic screen.
 
         // Fetch new data.
         bool dynamic_data = get_next_data(&global.title, &global.text, false);
         if (dynamic_data) {
             // We found some data so simply display it.
             ux_flow_next();
         } else {
             // There's no more dynamic data to display, so
             // update the current state accordingly.
             global.current_state = STATIC_SCREEN;
 
             // Display the previous screen which should be a static one.
             ux_flow_prev();
         }
      }
   } else {
            // We're called from the lower delimiter.
 
      if (global.current_state == STATIC_SCREEN) {
         // Fetch new data.
         bool dynamic_data = get_next_data(&global.title, &global.text, false);
 
         if (dynamic_data) {
           // We found some data to display so enter in dynamic mode.
           global.current_state = DYNAMIC_SCREEN;
         }
 
         // Display the data.
         ux_flow_prev();
      } else {
         // We're being called from a dynamic screen, so the user was already browsing the array.
 
         // Fetch new data.
         bool dynamic_data = get_next_data(&global.title, &global.text, true);
         if (dynamic_data) {
           // We found some data, so display it.
           // Similar to `ux_flow_prev()` but updates layout to account for `bnnn_paging`'s weird behaviour.
           bnnn_paging_edgecase();
         } else {
           // We found no data so make sure we update the state accordingly.
           global.current_state = STATIC_SCREEN;
 
           // Display the next screen
           ux_flow_next();
         }
      }
   }
}

That was a mouthful, but you did it: you managed to dynamically adapt our flow AND our steps!

Ledger
Copyright © Ledger SAS. All rights reserved. Ledger, Ledger Nano S, Ledger Vault, Bolos are registered trademarks of Ledger SAS