ui: don't show waiting screen during password stretch
What changed, and why it matters
This commit is a user-interface polish change for the BitBox02 hardware wallet. It replaces a generic 'waiting' spinner with the first frame of the unlock animation while the device is busy stretching the user's password. There is no security vulnerability or fix here—only a visual improvement to make the device look smoother during unlock.
No security action required. Treat as a normal UI/UX improvement during code review and regression testing.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change splits the unlock animation into a two-phase API: unlock_animation_create() pushes the first (paused) frame onto the screen stack, and unlock_animation_play() starts the actual animation. This lets callers display the locked-lock frame as a filler screen during async secure-chip/password-stretching operations, instead of showing the default waiting screen. The commit updates the Rust UI trait, real and stub UI implementations, C unlock animation component, and unit tests. It explicitly notes that other workflows still show the waiting screen and are not addressed.
Changed components
src/rust/bitbox-hal/src/ui.rssrc/rust/bitbox02-rust/src/hal/testing/ui.rssrc/rust/bitbox02-rust/src/hww.rssrc/rust/bitbox02-rust/src/hww/api/change_password.rssrc/rust/bitbox02-rust/src/hww/api/restore.rssrc/rust/bitbox02-rust/src/hww/api/set_password.rssrc/rust/bitbox02-rust/src/workflow/unlock.rssrc/rust/bitbox02-sys/build.rssrc/rust/bitbox02/src/hal/ui.rssrc/rust/bitbox02/src/ui/ui.rssrc/rust/bitbox02/src/ui/ui_stub.rssrc/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rssrc/rust/bitbox03/src/ui.rssrc/ui/components/unlock_animation.csrc/ui/components/unlock_animation.hInspect captured patch +247 / −58
diff --git a/src/rust/bitbox-hal/src/ui.rs b/src/rust/bitbox-hal/src/ui.rs
index 5fa0e43..1c300b0 100644
--- a/src/rust/bitbox-hal/src/ui.rs
+++ b/src/rust/bitbox-hal/src/ui.rs
@@ -72,6 +72,7 @@ pub trait Empty {}
pub trait Ui {
type Progress: Progress;
type Empty: Empty;
+ type UnlockAnimation;
/// Returns `Ok(())` if the user accepts, `Err(UserAbort)` if the user rejects.
async fn confirm(&mut self, params: &ConfirmParams<'_>) -> Result<(), UserAbort>;
@@ -88,7 +89,9 @@ pub trait Ui {
longtouch: bool,
) -> Result<(), UserAbort>;
- async fn unlock_animation(&mut self);
+ fn unlock_animation_create(&mut self) -> Self::UnlockAnimation;
+
+ async fn unlock_animation_play(&mut self, animation: Self::UnlockAnimation);
async fn status(&mut self, title: &str, status_success: bool);
diff --git a/src/rust/bitbox02-rust/src/hal/testing/ui.rs b/src/rust/bitbox02-rust/src/hal/testing/ui.rs
index 8bd4661..6437fe4 100644
--- a/src/rust/bitbox02-rust/src/hal/testing/ui.rs
+++ b/src/rust/bitbox02-rust/src/hal/testing/ui.rs
@@ -48,6 +48,8 @@ pub enum Screen {
choices: Vec<String>,
selected: u8,
},
+ UnlockAnimationPaused,
+ UnlockAnimationPlayed,
More,
}
@@ -78,9 +80,12 @@ pub struct NoopEmpty;
impl Empty for NoopEmpty {}
+pub struct NoopUnlockAnimation;
+
impl Ui for TestingUi<'_> {
type Progress = NoopProgress;
type Empty = NoopEmpty;
+ type UnlockAnimation = NoopUnlockAnimation;
fn progress_create(&mut self, _title: &str) -> Self::Progress {
NoopProgress
@@ -90,6 +95,15 @@ impl Ui for TestingUi<'_> {
NoopEmpty
}
+ fn unlock_animation_create(&mut self) -> Self::UnlockAnimation {
+ self.screens.push(Screen::UnlockAnimationPaused);
+ NoopUnlockAnimation
+ }
+
+ async fn unlock_animation_play(&mut self, _animation: Self::UnlockAnimation) {
+ self.screens.push(Screen::UnlockAnimationPlayed);
+ }
+
async fn confirm(&mut self, params: &ConfirmParams<'_>) -> Result<(), UserAbort> {
self.confirm_display_sizes.push(params.display_size);
self.screens.push(Screen::Confirm {
@@ -159,8 +173,6 @@ impl Ui for TestingUi<'_> {
Ok(())
}
- async fn unlock_animation(&mut self) {}
-
async fn status(&mut self, title: &str, status_success: bool) {
self.screens.push(Screen::Status {
title: title.into(),
@@ -513,4 +525,15 @@ mod tests {
Err(UserAbort)
));
}
+
+ #[async_test::test]
+ async fn test_unlock_animation_records_screen() {
+ let mut ui = TestingUi::new();
+ let animation = ui.unlock_animation_create();
+ ui.unlock_animation_play(animation).await;
+ assert_eq!(
+ ui.screens,
+ vec![Screen::UnlockAnimationPaused, Screen::UnlockAnimationPlayed]
+ );
+ }
}
diff --git a/src/rust/bitbox02-rust/src/hww.rs b/src/rust/bitbox02-rust/src/hww.rs
index 9476628..17a41b6 100644
--- a/src/rust/bitbox02-rust/src/hww.rs
+++ b/src/rust/bitbox02-rust/src/hww.rs
@@ -309,10 +309,14 @@ mod tests {
.unwrap();
assert_eq!(
mock_hal.ui.screens,
- vec![Screen::Status {
- title: "Success".into(),
- success: true,
- }]
+ vec![
+ Screen::Status {
+ title: "Success".into(),
+ success: true,
+ },
+ Screen::UnlockAnimationPaused,
+ Screen::UnlockAnimationPlayed,
+ ]
);
assert!(!crate::keystore::is_locked());
@@ -500,10 +504,14 @@ mod tests {
.unwrap();
assert_eq!(
mock_hal.ui.screens,
- vec![Screen::Status {
- title: "Success".into(),
- success: true,
- }]
+ vec![
+ Screen::Status {
+ title: "Success".into(),
+ success: true,
+ },
+ Screen::UnlockAnimationPaused,
+ Screen::UnlockAnimationPlayed,
+ ]
);
mock_hal.ui = crate::hal::testing::TestingUi::new();
@@ -703,7 +711,9 @@ mod tests {
Screen::Status {
title: "Success".into(),
success: true,
- }
+ },
+ Screen::UnlockAnimationPaused,
+ Screen::UnlockAnimationPlayed,
]
);
diff --git a/src/rust/bitbox02-rust/src/hww/api/change_password.rs b/src/rust/bitbox02-rust/src/hww/api/change_password.rs
index e331527..cb9ed3d 100644
--- a/src/rust/bitbox02-rust/src/hww/api/change_password.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/change_password.rs
@@ -59,8 +59,10 @@ mod tests {
keystore::encrypt_and_store_seed(&mut hal, &seed, old_password)
.await
.unwrap();
- unlock::unlock_bip39(&mut hal, &seed).await;
+ let unlock_animation = hal.ui.unlock_animation_create();
+ unlock::unlock_bip39(&mut hal, &seed, unlock_animation).await;
hal.memory.set_initialized().unwrap();
+ hal.ui.screens.clear();
// Allow exactly 3 prompts
hal.ui.set_enter_string(Box::new(|params| {
@@ -141,9 +143,11 @@ mod tests {
keystore::encrypt_and_store_seed(&mut hal, &seed, correct_password)
.await
.unwrap();
- unlock::unlock_bip39(&mut hal, &seed).await;
+ let unlock_animation = hal.ui.unlock_animation_create();
+ unlock::unlock_bip39(&mut hal, &seed, unlock_animation).await;
hal.memory.set_initialized().unwrap();
keystore::lock();
+ hal.ui.screens.clear();
hal.ui.set_enter_string(Box::new(|params| {
prompt_counter += 1;
@@ -200,9 +204,11 @@ mod tests {
keystore::encrypt_and_store_seed(&mut hal, &seed, old_password)
.await
.unwrap();
- unlock::unlock_bip39(&mut hal, &seed).await;
+ let unlock_animation = hal.ui.unlock_animation_create();
+ unlock::unlock_bip39(&mut hal, &seed, unlock_animation).await;
hal.memory.set_initialized().unwrap();
keystore::lock();
+ hal.ui.screens.clear();
hal.ui.set_enter_string(Box::new(|params| {
prompt_counter += 1;
diff --git a/src/rust/bitbox02-rust/src/hww/api/restore.rs b/src/rust/bitbox02-rust/src/hww/api/restore.rs
index c7d35b6..46b4bbc 100644
--- a/src/rust/bitbox02-rust/src/hww/api/restore.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/restore.rs
@@ -56,8 +56,10 @@ pub async fn from_file(
}
let password = password::enter_twice(hal).await?;
+ let unlock_animation = hal.ui().unlock_animation_create();
let seed = data.get_seed();
if let Err(err) = crate::keystore::encrypt_and_store_seed(hal, seed, &password).await {
+ drop(unlock_animation);
hal.ui()
.status(&format!("Could not\nrestore backup\n{:?}", err), false)
.await;
@@ -79,7 +81,7 @@ pub async fn from_file(
// Ignore non-critical error.
let _ = hal.memory().set_device_name(&metadata.name);
- unlock::unlock_bip39(hal, seed).await;
+ unlock::unlock_bip39(hal, seed, unlock_animation).await;
Ok(Response::Success(pb::Success {}))
}
@@ -132,8 +134,10 @@ pub async fn from_mnemonic(
Ok(password) => break password,
}
};
+ let unlock_animation = hal.ui().unlock_animation_create();
if let Err(err) = crate::keystore::encrypt_and_store_seed(hal, &seed, &password).await {
+ drop(unlock_animation);
hal.ui()
.status(&format!("Could not\nrestore backup\n{:?}", err), false)
.await;
@@ -149,7 +153,7 @@ pub async fn from_mnemonic(
hal.memory().set_initialized().or(Err(Error::Memory))?;
- unlock::unlock_bip39(hal, &seed).await;
+ unlock::unlock_bip39(hal, &seed, unlock_animation).await;
Ok(Response::Success(pb::Success {}))
}
diff --git a/src/rust/bitbox02-rust/src/hww/api/set_password.rs b/src/rust/bitbox02-rust/src/hww/api/set_password.rs
index 614646f..d7bc24c 100644
--- a/src/rust/bitbox02-rust/src/hww/api/set_password.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/set_password.rs
@@ -24,12 +24,14 @@ pub async fn process(
return Err(Error::InvalidInput);
}
let password = password::enter_twice(hal).await?;
+ let unlock_animation = hal.ui().unlock_animation_create();
if let Err(err) = keystore::create_and_store_seed(hal, &password, entropy).await {
+ drop(unlock_animation);
hal.ui().status(&format!("Error\n{:?}", err), false).await;
return Err(Error::Generic);
}
let seed = keystore::copy_seed(hal).await?;
- unlock::unlock_bip39(hal, &seed).await;
+ unlock::unlock_bip39(hal, &seed, unlock_animation).await;
Ok(Response::Success(pb::Success {}))
}
diff --git a/src/rust/bitbox02-rust/src/workflow/unlock.rs b/src/rust/bitbox02-rust/src/workflow/unlock.rs
index 498caf5..638250e 100644
--- a/src/rust/bitbox02-rust/src/workflow/unlock.rs
+++ b/src/rust/bitbox02-rust/src/workflow/unlock.rs
@@ -40,6 +40,7 @@ async fn confirm_mnemonic_passphrase(
hal.ui().confirm(¶ms).await
}
+#[derive(Debug)]
pub enum UnlockError {
UserAbort,
IncorrectPassword,
@@ -131,7 +132,15 @@ pub async fn unlock_keystore(
/// Performs the BIP39 keystore unlock, including unlock animation. If the optional passphrase
/// feature is enabled, the user will be asked for the passphrase.
-pub async fn unlock_bip39(hal: &mut impl crate::hal::Hal, seed: &[u8]) {
+///
+/// `unlock_animation` must already be on the screen stack. It stays paused on the first frame
+/// while the workflow transitions from password entry to the BIP39 unlock, then starts playing
+/// immediately before the BIP39 unlock work begins.
+pub async fn unlock_bip39<H: crate::hal::Hal>(
+ hal: &mut H,
+ seed: &[u8],
+ unlock_animation: <H::Ui as crate::hal::ui::Ui>::UnlockAnimation,
+) {
// Empty passphrase by default.
let mut mnemonic_passphrase = zeroize::Zeroizing::new("".into());
@@ -169,7 +178,7 @@ pub async fn unlock_bip39(hal: &mut impl crate::hal::Hal, seed: &[u8]) {
crate::keystore::KeystoreHalImpl::new(eeprom, memory, random, securechip);
let ((), result) = futures_lite::future::zip(
- ui.unlock_animation(),
+ ui.unlock_animation_play(unlock_animation),
crate::keystore::unlock_bip39(
&mut keystore_hal,
seed,
@@ -207,10 +216,12 @@ pub async fn unlock(hal: &mut impl crate::hal::Hal) -> Result<(), ()> {
return Ok(());
}
+ let unlock_animation = hal.ui().unlock_animation_create();
+
// Loop unlock until the password is correct or the device resets.
loop {
if let Ok(seed) = unlock_keystore(hal, "Enter password", CanCancel::No).await {
- unlock_bip39(hal, &seed).await;
+ unlock_bip39(hal, &seed, unlock_animation).await;
return Ok(());
}
}
@@ -256,6 +267,10 @@ mod tests {
assert_eq!(mock_hal.securechip.get_event_counter(), 6);
assert!(!crate::keystore::is_locked());
+ assert_eq!(
+ mock_hal.ui.screens,
+ vec![Screen::UnlockAnimationPaused, Screen::UnlockAnimationPlayed,]
+ );
assert_eq!(
crate::keystore::copy_bip39_seed(&mut mock_hal)
@@ -271,6 +286,56 @@ mod tests {
assert!(password_entered);
}
+ #[async_test::test]
+ async fn test_unlock_retry_uses_same_unlock_animation_handle() {
+ let mut password_entries = 0;
+ let mut mock_hal = TestingHal::new();
+
+ crate::keystore::encrypt_and_store_seed(
+ &mut mock_hal,
+ &hex!("c7940c13479b8d9a6498f4e50d5a42e0d617bc8e8ac9f2b8cecf97e94c2b035c"),
+ "password",
+ )
+ .await
+ .unwrap();
+ mock_hal.memory.set_initialized().unwrap();
+ crate::keystore::lock();
+
+ mock_hal.ui.set_enter_string(Box::new(|_params| {
+ password_entries += 1;
+ match password_entries {
+ 1 => Ok("wrong password".into()),
+ 2 => Ok("password".into()),
+ _ => panic!("too many user inputs"),
+ }
+ }));
+
+ assert_eq!(unlock(&mut mock_hal).await, Ok(()));
+
+ assert_eq!(
+ mock_hal.ui.screens,
+ vec![
+ Screen::UnlockAnimationPaused,
+ Screen::Status {
+ title: "Wrong password".into(),
+ success: false,
+ },
+ Screen::Confirm {
+ title: "WARNING".into(),
+ body: format!(
+ "You have {}\npassword attempts\nleft.",
+ crate::keystore::MAX_UNLOCK_ATTEMPTS - 1
+ ),
+ longtouch: false,
+ },
+ Screen::UnlockAnimationPlayed,
+ ],
+ );
+
+ drop(mock_hal); // to remove mutable borrow of `password_entries`
+ assert_eq!(password_entries, 2);
+ }
+
#[async_test::test]
async fn test_unlock_keystore_wrong_password() {
let mut password_entered = false;
diff --git a/src/rust/bitbox02-sys/build.rs b/src/rust/bitbox02-sys/build.rs
index 6565fd2..67998c8 100644
--- a/src/rust/bitbox02-sys/build.rs
+++ b/src/rust/bitbox02-sys/build.rs
@@ -165,6 +165,7 @@ const ALLOWLIST_FNS: &[&str] = &[
"trinary_choice_create",
"trinary_input_string_create",
"trinary_input_string_set_input",
+ "unlock_animation_play",
"u2f_packet_init",
"u2f_packet_process",
"u2f_packet_timeout_get",
diff --git a/src/rust/bitbox02/src/hal/ui.rs b/src/rust/bitbox02/src/hal/ui.rs
index eaf6680..440bfbf 100644
--- a/src/rust/bitbox02/src/hal/ui.rs
+++ b/src/rust/bitbox02/src/hal/ui.rs
@@ -92,6 +92,7 @@ impl<Timer> Default for BitBox02Ui<Timer> {
impl<Timer: bitbox_hal::timer::Timer> Ui for BitBox02Ui<Timer> {
type Progress = BitBox02Progress;
type Empty = BitBox02Empty;
+ type UnlockAnimation = crate::ui::UnlockAnimation;
fn progress_create(&mut self, title: &str) -> Self::Progress {
let mut component = crate::ui::progress_create(title);
@@ -107,6 +108,15 @@ impl<Timer: bitbox_hal::timer::Timer> Ui for BitBox02Ui<Timer> {
}
}
+ fn unlock_animation_create(&mut self) -> Self::UnlockAnimation {
+ crate::ui::unlock_animation_create()
+ }
+
+ #[inline(always)]
+ async fn unlock_animation_play(&mut self, animation: Self::UnlockAnimation) {
+ animation.play().await
+ }
+
#[inline(always)]
async fn confirm(&mut self, params: &ConfirmParams<'_>) -> Result<(), UserAbort> {
let params = to_bitbox02_confirm_params(params);
@@ -145,11 +155,6 @@ impl<Timer: bitbox_hal::timer::Timer> Ui for BitBox02Ui<Timer> {
}
}
- #[inline(always)]
- async fn unlock_animation(&mut self) {
- crate::ui::unlock_animation().await
- }
-
#[inline(always)]
async fn status(&mut self, title: &str, status_success: bool) {
let mut component = crate::ui::status_create(title, status_success);
diff --git a/src/rust/bitbox02/src/ui/ui.rs b/src/rust/bitbox02/src/ui/ui.rs
index a7feea6..32904ca 100644
--- a/src/rust/bitbox02/src/ui/ui.rs
+++ b/src/rust/bitbox02/src/ui/ui.rs
@@ -44,6 +44,41 @@ impl Drop for Component {
}
}
+struct UnlockAnimationSharedState {
+ waker: Option<Waker>,
+ result: Option<()>,
+}
+
+pub struct UnlockAnimation {
+ component: Component,
+ shared_state: Box<RefCell<UnlockAnimationSharedState>>,
+}
+
+impl UnlockAnimation {
+ pub async fn play(self) {
+ let _no_screensaver = crate::screen_saver::ScreensaverInhibitor::new();
+ unsafe {
+ bitbox02_sys::unlock_animation_play(self.component.component);
+ }
+
+ let UnlockAnimation {
+ component: _component,
+ shared_state,
+ } = self;
+ core::future::poll_fn(move |cx| {
+ let mut shared_state = shared_state.borrow_mut();
+
+ if let Some(result) = shared_state.result.take() {
+ Poll::Ready(result)
+ } else {
+ shared_state.waker = Some(cx.waker().clone());
+ Poll::Pending
+ }
+ })
+ .await
+ }
+}
+
pub async fn trinary_input_string(
params: &TrinaryInputStringParams<'_>,
can_cancel: bool,
@@ -757,22 +792,16 @@ pub fn empty_create() -> Component {
}
}
-pub async fn unlock_animation() {
- let _no_screensaver = crate::screen_saver::ScreensaverInhibitor::new();
-
- // Shared between the async context and the c callback
- struct SharedState {
- waker: Option<Waker>,
- result: Option<()>,
- }
- let shared_state = Box::new(RefCell::new(SharedState {
+pub fn unlock_animation_create() -> UnlockAnimation {
+ let shared_state = Box::new(RefCell::new(UnlockAnimationSharedState {
waker: None,
result: None,
}));
- let shared_state_ptr = shared_state.as_ref() as *const RefCell<SharedState> as *mut c_void;
+ let shared_state_ptr =
+ shared_state.as_ref() as *const RefCell<UnlockAnimationSharedState> as *mut c_void;
unsafe extern "C" fn callback(user_data: *mut c_void) {
- let shared_state = unsafe { &*(user_data as *mut RefCell<SharedState>) };
+ let shared_state = unsafe { &*(user_data as *mut RefCell<UnlockAnimationSharedState>) };
let mut shared_state = shared_state.borrow_mut();
if shared_state.result.is_none() {
shared_state.result = Some(());
@@ -795,21 +824,10 @@ pub async fn unlock_animation() {
};
component.screen_stack_push();
- core::future::poll_fn({
- let shared_state = &shared_state;
- move |cx| {
- let mut shared_state = shared_state.borrow_mut();
-
- if let Some(result) = shared_state.result.take() {
- Poll::Ready(result)
- } else {
- // Store the waker so the callback can wake up this task
- shared_state.waker = Some(cx.waker().clone());
- Poll::Pending
- }
- }
- })
- .await
+ UnlockAnimation {
+ component,
+ shared_state,
+ }
}
pub async fn choose_orientation() -> bool {
diff --git a/src/rust/bitbox02/src/ui/ui_stub.rs b/src/rust/bitbox02/src/ui/ui_stub.rs
index f5ef5e9..c10d4ac 100644
--- a/src/rust/bitbox02/src/ui/ui_stub.rs
+++ b/src/rust/bitbox02/src/ui/ui_stub.rs
@@ -99,7 +99,21 @@ pub fn empty_create() -> Component {
Component { is_pushed: false }
}
-pub async fn unlock_animation() {}
+pub struct UnlockAnimation {
+ _component: Component,
+}
+
+pub fn unlock_animation_create() -> UnlockAnimation {
+ let mut component = Component { is_pushed: false };
+ component.screen_stack_push();
+ UnlockAnimation {
+ _component: component,
+ }
+}
+
+impl UnlockAnimation {
+ pub async fn play(self) {}
+}
pub async fn choose_orientation() -> bool {
false
diff --git a/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs b/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs
index b6c3746..e53ff8e 100644
--- a/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs
+++ b/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs
@@ -122,7 +122,21 @@ pub fn empty_create() -> Component {
Component { is_pushed: false }
}
-pub async fn unlock_animation() {}
+pub struct UnlockAnimation {
+ _component: Component,
+}
+
+pub fn unlock_animation_create() -> UnlockAnimation {
+ let mut component = Component { is_pushed: false };
+ component.screen_stack_push();
+ UnlockAnimation {
+ _component: component,
+ }
+}
+
+impl UnlockAnimation {
+ pub async fn play(self) {}
+}
pub async fn choose_orientation() -> bool {
false
diff --git a/src/rust/bitbox03/src/ui.rs b/src/rust/bitbox03/src/ui.rs
index 366bff8..f6830ee 100644
--- a/src/rust/bitbox03/src/ui.rs
+++ b/src/rust/bitbox03/src/ui.rs
@@ -49,6 +49,7 @@ impl<Timer: bitbox_hal::timer::Timer> hal::ui::Ui for BitBox03Ui<Timer> {
type Progress = BitBox03UiProgress;
type Empty = BitBox03UiEmpty;
+ type UnlockAnimation = BitBox03UiEmpty;
async fn confirm(
&mut self,
@@ -90,10 +91,6 @@ impl<Timer: bitbox_hal::timer::Timer> hal::ui::Ui for BitBox03Ui<Timer> {
todo!()
}
- async fn unlock_animation(&mut self) {
- self.status("TODO\nunlock_animation", true).await
- }
-
async fn status(&mut self, title: &str, status_success: bool) {
let screen = status::build_status_screen(title, status_success);
let _screen = self.push_guard(screen);
@@ -122,6 +119,14 @@ impl<Timer: bitbox_hal::timer::Timer> hal::ui::Ui for BitBox03Ui<Timer> {
todo!()
}
+ fn unlock_animation_create(&mut self) -> Self::UnlockAnimation {
+ BitBox03UiEmpty
+ }
+
+ async fn unlock_animation_play(&mut self, _animation: Self::UnlockAnimation) {
+ self.status("TODO\nunlock_animation", true).await
+ }
+
async fn enter_string(
&mut self,
params: &bitbox_hal::ui::EnterStringParams<'_>,
diff --git a/src/ui/components/unlock_animation.c b/src/ui/components/unlock_animation.c
index b246aa5..016d86b 100644
--- a/src/ui/components/unlock_animation.c
+++ b/src/ui/components/unlock_animation.c
@@ -8,6 +8,7 @@
#include <ui/screen_process.h>
#include <ui/ui_util.h>
+#include <stdbool.h>
#include <stdint.h>
#include <string.h>
@@ -29,6 +30,7 @@
typedef struct {
int frame;
+ bool playing;
void (*on_done)(void*);
void* on_done_param;
} data_t;
@@ -152,6 +154,16 @@ static const uint8_t* _get_frame(int frame_idx)
static void _render(component_t* component)
{
data_t* data = (data_t*)component->data;
+ if (!data->playing) {
+ position_t pos = {
+ .left = (SCREEN_WIDTH - LOCK_ANIMATION_FRAME_WIDTH) / 2,
+ .top = (SCREEN_HEIGHT - LOCK_ANIMATION_FRAME_HEIGHT) / 2};
+ dimension_t dim = {
+ .width = LOCK_ANIMATION_FRAME_WIDTH, .height = LOCK_ANIMATION_FRAME_HEIGHT};
+ in_buffer_t image = {.data = _get_frame(0), .len = LOCK_ANIMATION_FRAME_SIZE};
+ graphics_draw_image(&pos, &dim, &image);
+ return;
+ }
int frame = data->frame / SLOWDOWN_FACTOR;
if (frame >= LOCK_ANIMATION_N_FRAMES + LOCK_ANIMATION_FRAMES_STOP_TIME) {
@@ -201,3 +213,9 @@ component_t* unlock_animation_create(void (*on_done)(void*), void* on_done_param
component->dimension.height = SCREEN_HEIGHT;
return component;
}
+
+void unlock_animation_play(component_t* component)
+{
+ data_t* data = (data_t*)component->data;
+ data->playing = true;
+}
diff --git a/src/ui/components/unlock_animation.h b/src/ui/components/unlock_animation.h
index 37b55ac..85aae10 100644
--- a/src/ui/components/unlock_animation.h
+++ b/src/ui/components/unlock_animation.h
@@ -6,5 +6,6 @@
#include <ui/component.h>
component_t* unlock_animation_create(void (*on_done)(void*), void* on_done_param);
+void unlock_animation_play(component_t* component);
#endif
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.