From d5d2497baa69f4db4998989c15a5625b71dbfafd Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 2 Jun 2025 16:44:57 +0200 Subject: [PATCH 01/25] refactor: Split backup settings content view --- .../ui/settings/BackupSettingsScreen.kt | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt index a9ea0450f..723d5ef94 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt @@ -7,21 +7,44 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.navigateToBackupWalletSettings +import to.bitkit.ui.navigateToHome import to.bitkit.ui.navigateToRestoreWalletSettings import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.scaffold.CloseNavIcon import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.theme.AppThemeSurface @Composable fun BackupSettingsScreen( navController: NavController, +) { + BackupSettingsScreenContent( + onBackupClick = { navController.navigateToBackupWalletSettings() }, + onResetAndRestoreClick = { navController.navigateToRestoreWalletSettings() }, + onBack = { navController.popBackStack() }, + onClose = { navController.navigateToHome() }, + ) +} + +@Composable +private fun BackupSettingsScreenContent( + onBackupClick: () -> Unit, + onResetAndRestoreClick: () -> Unit, + onBack: () -> Unit, + onClose: () -> Unit, ) { ScreenColumn { - AppTopBar(stringResource(R.string.settings__backup__title), onBackClick = { navController.popBackStack() }) + AppTopBar( + titleText = stringResource(R.string.settings__backup__title), + onBackClick = onBack, + actions = { CloseNavIcon(onClick = onClose) }, + ) Column( modifier = Modifier .padding(horizontal = 16.dp) @@ -29,12 +52,25 @@ fun BackupSettingsScreen( ) { SettingsButtonRow( title = stringResource(R.string.settings__backup__wallet), - onClick = { navController.navigateToBackupWalletSettings() } + onClick = onBackupClick, ) SettingsButtonRow( title = stringResource(R.string.settings__backup__reset), - onClick = { navController.navigateToRestoreWalletSettings() } + onClick = onResetAndRestoreClick, ) } } } + +@Preview +@Composable +private fun Preview() { + AppThemeSurface { + BackupSettingsScreenContent( + onBackupClick = {}, + onResetAndRestoreClick = {}, + onBack = {}, + onClose = {}, + ) + } +} From aa1ce850b491113e8b2df60c26cc66b31ea6b23a Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 3 Jun 2025 15:09:36 +0200 Subject: [PATCH 02/25] feat: Backup mnemonic screen ui polish --- .../ui/settings/backups/BackupWalletScreen.kt | 334 +++++++++++++----- 1 file changed, 250 insertions(+), 84 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupWalletScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupWalletScreen.kt index 20552e9d7..a5e924242 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupWalletScreen.kt @@ -1,140 +1,306 @@ package to.bitkit.ui.settings.backups +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.EaseOutQuart +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.BlurredEdgeTreatment +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.models.Toast import to.bitkit.ui.appViewModel +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.BodyMSB +import to.bitkit.ui.components.BodyS +import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.withAccent import to.bitkit.utils.Logger +import to.bitkit.utils.bip39Words @Composable fun BackupWalletScreen( navController: NavController, ) { + val app = appViewModel ?: return + val context = LocalContext.current + val clipboard = LocalClipboardManager.current + + var mnemonic by remember { mutableStateOf("") } + var showMnemonic by remember { mutableStateOf(false) } + var isLoading by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + DisposableEffect(Unit) { + onDispose { + mnemonic = "" // Clear mnemonic from memory when leaving screen + } + } + + BackupWalletContent( + mnemonic = mnemonic, + showMnemonic = showMnemonic, + isLoading = isLoading, + onBackClick = { navController.popBackStack() }, + onRevealClick = { + scope.launch { + try { + isLoading = true + delay(200) + val loadedMnemonic = app.loadMnemonic()!! + mnemonic = loadedMnemonic + showMnemonic = true + } catch (e: Throwable) { + Logger.error("Failed to load mnemonic", e) + app.toast( + type = Toast.ToastType.WARNING, + title = context.getString(R.string.security__mnemonic_error), + description = context.getString(R.string.security__mnemonic_error_description), + ) + } + } + }, + onCopyClick = { + clipboard.setText(AnnotatedString(mnemonic)) + }, + ) +} + +@Composable +private fun BackupWalletContent( + mnemonic: String, + showMnemonic: Boolean, + isLoading: Boolean, + onBackClick: () -> Unit, + onRevealClick: () -> Unit, + onCopyClick: () -> Unit, +) { + val blurRadius by animateFloatAsState( + targetValue = if (showMnemonic) 0f else 10f, + animationSpec = tween(durationMillis = 800, easing = EaseOutQuart), + label = "blurRadius" + ) + + val buttonAlpha by animateFloatAsState( + targetValue = if (showMnemonic) 0f else 1f, + animationSpec = tween(durationMillis = 400), + label = "buttonAlpha" + ) + + val mnemonicWords = if (mnemonic.isNotEmpty()) mnemonic.split(" ") else emptyList() + ScreenColumn { - AppTopBar(stringResource(R.string.security__mnemonic_your), onBackClick = { navController.popBackStack() }) + AppTopBar( + titleText = stringResource(R.string.security__mnemonic_your), + onBackClick = onBackClick, + ) + Column( - verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier - .padding(horizontal = 16.dp) + .fillMaxSize() + .padding(horizontal = 32.dp) .verticalScroll(rememberScrollState()) ) { - val app = appViewModel ?: return@Column - - var mnemonic by remember { mutableStateOf("") } - var showMnemonic by remember { mutableStateOf(false) } - val clipboard = LocalClipboardManager.current - val scope = rememberCoroutineScope() - - val mnemonicWords = mnemonic.split(" ") - val columnLength = mnemonicWords.size / 2 - - Column { - if (showMnemonic) { - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "Write down these ${mnemonicWords.size} words in the right order and store them in a safe place.", - textAlign = TextAlign.Center, + Spacer(modifier = Modifier.height(16.dp)) + + AnimatedContent( + targetState = showMnemonic, + transitionSpec = { fadeIn(tween(300)).togetherWith(fadeOut(tween(300))) }, + label = "topText" + ) { isRevealed -> + BodyM( + text = if (isRevealed) { + stringResource(R.string.security__mnemonic_write).replace("{length}", "${mnemonicWords.size}") + } else { + stringResource(R.string.security__mnemonic_use) + }, + color = Colors.White64, + modifier = Modifier.fillMaxWidth(), + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 32.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .background(color = Colors.White10) + .clickable(enabled = showMnemonic && mnemonic.isNotEmpty(), onClick = onCopyClick) + .padding(horizontal = 32.dp, vertical = 32.dp) + ) { + MnemonicWordsGrid( + actualWords = mnemonicWords, + showMnemonic = showMnemonic, + blurRadius = blurRadius, ) - Spacer(modifier = Modifier.height(32.dp)) - Row( + } + + if (buttonAlpha > 0f) { + Box( + contentAlignment = Alignment.Center, modifier = Modifier .fillMaxWidth() - .clickable { - clipboard.setText(AnnotatedString(mnemonic)) - } - .background( - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.12f), - shape = MaterialTheme.shapes.medium, - ) - .padding(16.dp) + .matchParentSize() ) { - // First Column - Column(Modifier.weight(1f)) { - mnemonicWords.take(columnLength).forEachIndexed { index, word -> - Row(Modifier.padding(vertical = 4.dp)) { - Text( - text = "${index + 1}.", - color = Colors.White64, - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = word) - } - } - } - Spacer(modifier = Modifier.weight(0.6f)) - // Second Column - Column(Modifier.weight(1f)) { - mnemonicWords.drop(columnLength).forEachIndexed { index, word -> - Row(Modifier.padding(vertical = 4.dp)) { - Text( - text = "${columnLength + index + 1}.", - color = Colors.White64, - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = word) - } - } - } + PrimaryButton( + text = stringResource(R.string.security__mnemonic_reveal), + fullWidth = false, + isLoading = isLoading, + onClick = onRevealClick, + color = Colors.Black50, + modifier = Modifier.alpha(buttonAlpha) + ) } - } else { - Button( - onClick = { - scope.launch { - try { - mnemonic = app.loadMnemonic()!! - showMnemonic = true - } catch (e: Exception) { - Logger.error("Failed to load mnemonic", e) - app.toast( - type = Toast.ToastType.ERROR, - title = "Error", - description = "Could not retrieve backup phrase", - ) - } - } - }, - colors = ButtonDefaults.buttonColors( - contentColor = MaterialTheme.colorScheme.onSurface, - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), - modifier = Modifier.fillMaxWidth() - ) { - Text(text = "Tap To Reveal") + } + } + + BodyS( + text = stringResource(R.string.security__mnemonic_never_share).withAccent(accentColor = Colors.Brand), + color = Colors.White64, + ) + } + } +} + +@Composable +private fun MnemonicWordsGrid( + actualWords: List, + showMnemonic: Boolean, + blurRadius: Float, +) { + val placeholderWords = remember { List(24) { "secret" } } + + Box( + modifier = Modifier + .fillMaxWidth() + .blur(radius = blurRadius.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded) + ) { + Crossfade( + targetState = showMnemonic, + animationSpec = tween(durationMillis = 600), + label = "mnemonicCrossfade" + ) { isRevealed -> + val wordsToShow = if (isRevealed && actualWords.isNotEmpty()) actualWords else placeholderWords + + Row( + horizontalArrangement = Arrangement.spacedBy(32.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + wordsToShow.take(wordsToShow.size / 2).forEachIndexed { index, word -> + WordItem( + number = index + 1, + word = word + ) + } + } + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + wordsToShow.drop(wordsToShow.size / 2).forEachIndexed { index, word -> + WordItem( + number = wordsToShow.size / 2 + index + 1, + word = word + ) } } } } } } + +@Composable +private fun WordItem( + number: Int, + word: String, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + BodyMSB(text = "$number.", color = Colors.White64) + Spacer(modifier = Modifier.width(8.dp)) + BodyMSB(text = word, color = Colors.White) + } +} + +@Preview +@Composable +private fun Preview() { + AppThemeSurface { + BackupWalletContent( + mnemonic = "", + showMnemonic = false, + isLoading = false, + onBackClick = {}, + onRevealClick = {}, + onCopyClick = {}, + ) + } +} + +@Preview +@Composable +private fun PreviewShown() { + AppThemeSurface { + BackupWalletContent( + mnemonic = List(24) { bip39Words.random() }.joinToString(" "), + showMnemonic = true, + isLoading = false, + onBackClick = {}, + onRevealClick = {}, + onCopyClick = {}, + ) + } +} From 352e0bbbd023204d4f91cd3e0609fe987b32ffae Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 3 Jun 2025 20:41:42 +0200 Subject: [PATCH 03/25] refactor: Add sheet size constants --- app/src/main/java/to/bitkit/ui/components/SheetHost.kt | 6 ++++++ .../bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt | 3 ++- .../to/bitkit/ui/screens/wallets/send/SendOptionsView.kt | 3 ++- .../java/to/bitkit/ui/settings/pin/PinNavigationSheet.kt | 3 ++- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt index 88df6b446..37d9629bf 100644 --- a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt +++ b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt @@ -25,6 +25,12 @@ import to.bitkit.ui.screens.wallets.send.SendRoute import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.Colors +object SheetSize { + const val LARGE = .875f + const val MEDIUM = .755f + const val SMALL = .5f +} + sealed class BottomSheetType { data class Send(val route: SendRoute = SendRoute.Options) : BottomSheetType() data object Receive : BottomSheetType() diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt index b96d5dad7..1207baa22 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/TagSelectorSheet.kt @@ -27,6 +27,7 @@ import to.bitkit.ui.activityListViewModel import to.bitkit.ui.appViewModel import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton +import to.bitkit.ui.components.SheetSize import to.bitkit.ui.components.TagButton import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.util.gradientBackground @@ -66,7 +67,7 @@ private fun TagSelectorSheetContent( Column( modifier = Modifier .fillMaxWidth() - .fillMaxHeight(.5f) + .fillMaxHeight(SheetSize.SMALL) .gradientBackground() .navigationBarsPadding() .padding(horizontal = 16.dp) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendOptionsView.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendOptionsView.kt index 400b809bf..47b0f3b25 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendOptionsView.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendOptionsView.kt @@ -35,6 +35,7 @@ import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.ui.appViewModel import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.RectangleButton +import to.bitkit.ui.components.SheetSize import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.screens.scanner.QrScanningScreen import to.bitkit.ui.shared.util.gradientBackground @@ -65,7 +66,7 @@ fun SendOptionsView( Column( modifier = Modifier .fillMaxWidth() - .fillMaxHeight(.875f) + .fillMaxHeight(SheetSize.LARGE) .imePadding() ) { val navController = rememberNavController() diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/PinNavigationSheet.kt b/app/src/main/java/to/bitkit/ui/settings/pin/PinNavigationSheet.kt index 17658e4eb..b075e274d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/PinNavigationSheet.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/PinNavigationSheet.kt @@ -10,6 +10,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import kotlinx.serialization.Serializable +import to.bitkit.ui.components.SheetSize @Composable fun PinNavigationSheet( @@ -21,7 +22,7 @@ fun PinNavigationSheet( Column( modifier = Modifier .fillMaxWidth() - .fillMaxHeight(.775f) + .fillMaxHeight(SheetSize.MEDIUM) ) { NavHost( navController = navController, From d3cbb918a1d3e1103c607543668998c9236f73bf Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 3 Jun 2025 21:08:03 +0200 Subject: [PATCH 04/25] feat: Backup sheet flow WIP --- .../java/to/bitkit/ui/components/SheetHost.kt | 1 + .../bitkit/ui/screens/wallets/HomeScreen.kt | 9 + .../wallets/receive/ReceiveQrScreen.kt | 3 +- .../settings/backups/BackupNavigationSheet.kt | 136 +++++++ .../bitkit/ui/settings/backups/BackupSheet.kt | 47 +-- .../ui/settings/backups/BackupWalletScreen.kt | 297 +-------------- .../settings/backups/ConfirmMnemonicScreen.kt | 249 +++++++++++++ .../backups/ConfirmPassphraseScreen.kt | 176 +++++++++ .../ui/settings/backups/MetadataScreen.kt | 135 +++++++ .../settings/backups/MultipleDevicesScreen.kt | 114 ++++++ .../ui/settings/backups/ShowMnemonicScreen.kt | 346 ++++++++++++++++++ .../settings/backups/ShowPassphraseScreen.kt | 97 +++++ .../ui/settings/backups/SuccessScreen.kt | 121 ++++++ .../ui/settings/backups/WarningScreen.kt | 117 ++++++ .../java/to/bitkit/viewmodels/AppViewModel.kt | 4 + 15 files changed, 1520 insertions(+), 332 deletions(-) create mode 100644 app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt create mode 100644 app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt create mode 100644 app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt create mode 100644 app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt create mode 100644 app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt create mode 100644 app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt create mode 100644 app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt create mode 100644 app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt create mode 100644 app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt diff --git a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt index 37d9629bf..76e737f54 100644 --- a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt +++ b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt @@ -36,6 +36,7 @@ sealed class BottomSheetType { data object Receive : BottomSheetType() data object PinSetup : BottomSheetType() data object Backup : BottomSheetType() + data object BackupNavigation : BottomSheetType() data object ActivityDateRangeSelector : BottomSheetType() data object ActivityTagSelector : BottomSheetType() } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt index a2a16521e..b579d3b51 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt @@ -88,6 +88,7 @@ import to.bitkit.ui.screens.wallets.send.SendOptionsView import to.bitkit.ui.screens.widgets.facts.FactsCard import to.bitkit.ui.screens.widgets.headlines.HeadlineCard import to.bitkit.ui.settings.backups.BackupSheet +import to.bitkit.ui.settings.backups.BackupNavigationSheet import to.bitkit.ui.settings.pin.PinNavigationSheet import to.bitkit.ui.settingsViewModel import to.bitkit.ui.shared.util.clickableAlpha @@ -152,9 +153,17 @@ fun HomeScreen( BottomSheetType.Backup -> BackupSheet( onDismiss = { appViewModel.hideSheet() }, + onBackupClick = { + appViewModel.hideSheet() + appViewModel.showSheet(BottomSheetType.BackupNavigation) + }, walletViewModel = walletViewModel ) + BottomSheetType.BackupNavigation -> BackupNavigationSheet( + onDismiss = { appViewModel.hideSheet() }, + ) + null -> Unit } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt index fd7a0d241..ad46aff3b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveQrScreen.kt @@ -66,6 +66,7 @@ import to.bitkit.ui.components.Tooltip import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.screens.wallets.send.AddTagScreen import to.bitkit.ui.components.PagerWithIndicator +import to.bitkit.ui.components.SheetSize import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.shared.util.shareText import to.bitkit.ui.theme.AppShapes @@ -120,7 +121,7 @@ fun ReceiveQrSheet( Column( modifier = modifier .fillMaxWidth() - .fillMaxHeight(.875f) + .fillMaxHeight(SheetSize.LARGE) .imePadding() ) { NavHost( diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt new file mode 100644 index 000000000..8ab7a5d9f --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt @@ -0,0 +1,136 @@ +package to.bitkit.ui.settings.backups + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute +import kotlinx.serialization.Serializable +import to.bitkit.ui.components.SheetSize + +@Composable +fun BackupNavigationSheet( + onDismiss: () -> Unit, +) { + val navController = rememberNavController() + + Column( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight(SheetSize.MEDIUM) + ) { + NavHost( + navController = navController, + startDestination = BackupRoute.ShowMnemonic, + ) { + composable { + ShowMnemonicScreen( + onContinue = { seed, bip39Passphrase -> + if (bip39Passphrase.isNotEmpty()) { + navController.navigate(BackupRoute.ShowPassphrase(seed, bip39Passphrase)) + } else { + navController.navigate(BackupRoute.ConfirmMnemonic(seed, bip39Passphrase)) + } + }, + onDismiss = onDismiss, + ) + } + composable { backStackEntry -> + val route = backStackEntry.toRoute() + ShowPassphraseScreen( + seed = route.seed, + bip39Passphrase = route.bip39Passphrase, + onContinue = { + navController.navigate(BackupRoute.ConfirmMnemonic(route.seed, route.bip39Passphrase)) + }, + onBack = { navController.popBackStack() }, + ) + } + composable { backStackEntry -> + val route = backStackEntry.toRoute() + ConfirmMnemonicScreen( + seed = route.seed, + bip39Passphrase = route.bip39Passphrase, + onContinue = { + if (route.bip39Passphrase.isNotEmpty()) { + navController.navigate(BackupRoute.ConfirmPassphrase(route.bip39Passphrase)) + } else { + navController.navigate(BackupRoute.Warning) + } + }, + onBack = { navController.popBackStack() }, + ) + } + composable { backStackEntry -> + val route = backStackEntry.toRoute() + ConfirmPassphraseScreen( + bip39Passphrase = route.bip39Passphrase, + onContinue = { + navController.navigate(BackupRoute.Warning) + }, + onBack = { navController.popBackStack() }, + ) + } + composable { + WarningScreen( + onContinue = { + navController.navigate(BackupRoute.Success) + }, + onBack = { navController.popBackStack() }, + ) + } + composable { + SuccessScreen( + onContinue = { + navController.navigate(BackupRoute.MultipleDevices) + }, + onBack = { navController.popBackStack() }, + ) + } + composable { + MultipleDevicesScreen( + onContinue = { + navController.navigate(BackupRoute.Metadata) + }, + onBack = { navController.popBackStack() }, + ) + } + composable { + MetadataScreen( + onDismiss = onDismiss, + onBack = { navController.popBackStack() }, + ) + } + } + } +} + +object BackupRoute { + @Serializable + data object ShowMnemonic + + @Serializable + data class ShowPassphrase(val seed: List, val bip39Passphrase: String) + + @Serializable + data class ConfirmMnemonic(val seed: List, val bip39Passphrase: String) + + @Serializable + data class ConfirmPassphrase(val bip39Passphrase: String) + + @Serializable + data object Warning + + @Serializable + data object Success + + @Serializable + data object MultipleDevices + + @Serializable + data object Metadata +} diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupSheet.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupSheet.kt index 113c43c8c..3d9f1f16c 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupSheet.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupSheet.kt @@ -4,55 +4,30 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.rememberNavController -import kotlinx.serialization.Serializable +import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.models.BalanceState -import to.bitkit.ui.utils.composableWithDefaultTransitions +import to.bitkit.ui.components.SheetSize import to.bitkit.viewmodels.WalletViewModel -import androidx.compose.runtime.getValue -import androidx.lifecycle.compose.collectAsStateWithLifecycle @Composable fun BackupSheet( onDismiss: () -> Unit, + onBackupClick: () -> Unit, walletViewModel: WalletViewModel, ) { - val navController = rememberNavController() + val balance : BalanceState by walletViewModel.balanceState.collectAsStateWithLifecycle() Column( modifier = Modifier .fillMaxWidth() - .fillMaxHeight(.775f) + .fillMaxHeight(SheetSize.MEDIUM) ) { - NavHost( - navController = navController, - startDestination = BackupRoute.Intro, - ) { - composableWithDefaultTransitions { - val balance : BalanceState by walletViewModel.balanceState.collectAsStateWithLifecycle() - BackupIntroScreen( - hasFunds = balance.totalSats > 0u, - onClose = onDismiss, - onConfirm = { - navController.navigate(BackupRoute.Backup) - } - ) - } - composableWithDefaultTransitions { - BackupWalletScreen( - navController = navController - ) - } - } + BackupIntroScreen( + hasFunds = balance.totalSats > 0u, + onClose = onDismiss, + onConfirm = onBackupClick, + ) } } - -object BackupRoute { - @Serializable - data object Intro - - @Serializable - data object Backup -} diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupWalletScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupWalletScreen.kt index a5e924242..c908063d8 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupWalletScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupWalletScreen.kt @@ -1,306 +1,13 @@ package to.bitkit.ui.settings.backups -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.Crossfade -import androidx.compose.animation.core.EaseOutQuart -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.togetherWith -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.BlurredEdgeTreatment -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.blur -import androidx.compose.ui.draw.clip -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import to.bitkit.R -import to.bitkit.models.Toast -import to.bitkit.ui.appViewModel -import to.bitkit.ui.components.BodyM -import to.bitkit.ui.components.BodyMSB -import to.bitkit.ui.components.BodyS -import to.bitkit.ui.components.PrimaryButton -import to.bitkit.ui.scaffold.AppTopBar -import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.theme.AppThemeSurface -import to.bitkit.ui.theme.Colors -import to.bitkit.ui.utils.withAccent -import to.bitkit.utils.Logger -import to.bitkit.utils.bip39Words @Composable fun BackupWalletScreen( navController: NavController, ) { - val app = appViewModel ?: return - val context = LocalContext.current - val clipboard = LocalClipboardManager.current - - var mnemonic by remember { mutableStateOf("") } - var showMnemonic by remember { mutableStateOf(false) } - var isLoading by remember { mutableStateOf(false) } - val scope = rememberCoroutineScope() - - DisposableEffect(Unit) { - onDispose { - mnemonic = "" // Clear mnemonic from memory when leaving screen - } - } - - BackupWalletContent( - mnemonic = mnemonic, - showMnemonic = showMnemonic, - isLoading = isLoading, - onBackClick = { navController.popBackStack() }, - onRevealClick = { - scope.launch { - try { - isLoading = true - delay(200) - val loadedMnemonic = app.loadMnemonic()!! - mnemonic = loadedMnemonic - showMnemonic = true - } catch (e: Throwable) { - Logger.error("Failed to load mnemonic", e) - app.toast( - type = Toast.ToastType.WARNING, - title = context.getString(R.string.security__mnemonic_error), - description = context.getString(R.string.security__mnemonic_error_description), - ) - } - } - }, - onCopyClick = { - clipboard.setText(AnnotatedString(mnemonic)) - }, + BackupNavigationSheet( + onDismiss = { navController.popBackStack() }, ) } - -@Composable -private fun BackupWalletContent( - mnemonic: String, - showMnemonic: Boolean, - isLoading: Boolean, - onBackClick: () -> Unit, - onRevealClick: () -> Unit, - onCopyClick: () -> Unit, -) { - val blurRadius by animateFloatAsState( - targetValue = if (showMnemonic) 0f else 10f, - animationSpec = tween(durationMillis = 800, easing = EaseOutQuart), - label = "blurRadius" - ) - - val buttonAlpha by animateFloatAsState( - targetValue = if (showMnemonic) 0f else 1f, - animationSpec = tween(durationMillis = 400), - label = "buttonAlpha" - ) - - val mnemonicWords = if (mnemonic.isNotEmpty()) mnemonic.split(" ") else emptyList() - - ScreenColumn { - AppTopBar( - titleText = stringResource(R.string.security__mnemonic_your), - onBackClick = onBackClick, - ) - - Column( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 32.dp) - .verticalScroll(rememberScrollState()) - ) { - Spacer(modifier = Modifier.height(16.dp)) - - AnimatedContent( - targetState = showMnemonic, - transitionSpec = { fadeIn(tween(300)).togetherWith(fadeOut(tween(300))) }, - label = "topText" - ) { isRevealed -> - BodyM( - text = if (isRevealed) { - stringResource(R.string.security__mnemonic_write).replace("{length}", "${mnemonicWords.size}") - } else { - stringResource(R.string.security__mnemonic_use) - }, - color = Colors.White64, - modifier = Modifier.fillMaxWidth(), - ) - } - - Spacer(modifier = Modifier.height(32.dp)) - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 32.dp) - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .clip(MaterialTheme.shapes.medium) - .background(color = Colors.White10) - .clickable(enabled = showMnemonic && mnemonic.isNotEmpty(), onClick = onCopyClick) - .padding(horizontal = 32.dp, vertical = 32.dp) - ) { - MnemonicWordsGrid( - actualWords = mnemonicWords, - showMnemonic = showMnemonic, - blurRadius = blurRadius, - ) - } - - if (buttonAlpha > 0f) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .fillMaxWidth() - .matchParentSize() - ) { - PrimaryButton( - text = stringResource(R.string.security__mnemonic_reveal), - fullWidth = false, - isLoading = isLoading, - onClick = onRevealClick, - color = Colors.Black50, - modifier = Modifier.alpha(buttonAlpha) - ) - } - } - } - - BodyS( - text = stringResource(R.string.security__mnemonic_never_share).withAccent(accentColor = Colors.Brand), - color = Colors.White64, - ) - } - } -} - -@Composable -private fun MnemonicWordsGrid( - actualWords: List, - showMnemonic: Boolean, - blurRadius: Float, -) { - val placeholderWords = remember { List(24) { "secret" } } - - Box( - modifier = Modifier - .fillMaxWidth() - .blur(radius = blurRadius.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded) - ) { - Crossfade( - targetState = showMnemonic, - animationSpec = tween(durationMillis = 600), - label = "mnemonicCrossfade" - ) { isRevealed -> - val wordsToShow = if (isRevealed && actualWords.isNotEmpty()) actualWords else placeholderWords - - Row( - horizontalArrangement = Arrangement.spacedBy(32.dp), - modifier = Modifier.fillMaxWidth() - ) { - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - wordsToShow.take(wordsToShow.size / 2).forEachIndexed { index, word -> - WordItem( - number = index + 1, - word = word - ) - } - } - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - wordsToShow.drop(wordsToShow.size / 2).forEachIndexed { index, word -> - WordItem( - number = wordsToShow.size / 2 + index + 1, - word = word - ) - } - } - } - } - } -} - -@Composable -private fun WordItem( - number: Int, - word: String, -) { - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - BodyMSB(text = "$number.", color = Colors.White64) - Spacer(modifier = Modifier.width(8.dp)) - BodyMSB(text = word, color = Colors.White) - } -} - -@Preview -@Composable -private fun Preview() { - AppThemeSurface { - BackupWalletContent( - mnemonic = "", - showMnemonic = false, - isLoading = false, - onBackClick = {}, - onRevealClick = {}, - onCopyClick = {}, - ) - } -} - -@Preview -@Composable -private fun PreviewShown() { - AppThemeSurface { - BackupWalletContent( - mnemonic = List(24) { bip39Words.random() }.joinToString(" "), - showMnemonic = true, - isLoading = false, - onBackClick = {}, - onRevealClick = {}, - onCopyClick = {}, - ) - } -} diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt new file mode 100644 index 000000000..183ef7a10 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt @@ -0,0 +1,249 @@ +package to.bitkit.ui.settings.backups + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import to.bitkit.R +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.BodyMSB +import to.bitkit.ui.components.ButtonSize +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors +import to.bitkit.utils.bip39Words + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun ConfirmMnemonicScreen( + seed: List, + bip39Passphrase: String, + onContinue: () -> Unit, + onBack: () -> Unit, +) { + // State to track user selection + var selectedWords by remember { mutableStateOf(arrayOfNulls(seed.size)) } + var pressedStates by remember { mutableStateOf(BooleanArray(seed.size) { false }) } + + // Shuffle the words for selection + val shuffledWords = remember { seed.shuffled() } + + DisposableEffect(Unit) { + onDispose { + // Clear selected words from memory + selectedWords = arrayOfNulls(seed.size) + } + } + + ConfirmMnemonicContent( + originalSeed = seed, + shuffledWords = shuffledWords, + selectedWords = selectedWords, + pressedStates = pressedStates, + onWordPress = { word, shuffledIndex -> + // Find index of the last filled word + val lastIndex = selectedWords.indexOfFirst { it == null } - 1 + val nextIndex = if (lastIndex == -1) 0 else lastIndex + 1 + + // If the word is correct and pressed, do nothing + if (pressedStates[shuffledIndex] && nextIndex > 0 && seed[lastIndex] == selectedWords[lastIndex]) { + return@ConfirmMnemonicContent + } + + // If previous word is incorrect, allow unchecking + if (lastIndex >= 0 && selectedWords[lastIndex] != seed[lastIndex]) { + // Uncheck if we tap on it + if (pressedStates[shuffledIndex] && word == selectedWords[lastIndex]) { + pressedStates = pressedStates.copyOf().apply { this[shuffledIndex] = false } + selectedWords = selectedWords.copyOf().apply { this[lastIndex] = null } + } + return@ConfirmMnemonicContent + } + + // Mark word as pressed and add it to the seed + if (nextIndex < seed.size) { + pressedStates = pressedStates.copyOf().apply { this[shuffledIndex] = true } + selectedWords = selectedWords.copyOf().apply { this[nextIndex] = word } + } + }, + onContinue = onContinue, + onBack = onBack, + ) +} + +@OptIn(ExperimentalLayoutApi::class) +@Composable +private fun ConfirmMnemonicContent( + originalSeed: List, + shuffledWords: List, + selectedWords: Array, + pressedStates: BooleanArray, + onWordPress: (String, Int) -> Unit, + onContinue: () -> Unit, + onBack: () -> Unit, +) { + // Check if all words are correct + val isComplete = selectedWords.all { it != null } && + selectedWords.zip(originalSeed).all { (selected, original) -> selected == original } + + Column( + modifier = Modifier + .fillMaxSize() + .gradientBackground() + .padding(horizontal = 32.dp) + ) { + SheetTopBar( + titleText = stringResource(R.string.security__mnemonic_confirm), + onBack = onBack, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + BodyM( + text = stringResource(R.string.security__mnemonic_confirm_tap), + color = Colors.White64, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Shuffled word buttons + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalArrangement = Arrangement.spacedBy(5.dp), + modifier = Modifier.fillMaxWidth() + ) { + shuffledWords.forEachIndexed { index, word -> + PrimaryButton( + text = word, + color = if (pressedStates[index]) Colors.White32 else Colors.White16, + fullWidth = false, + size = ButtonSize.Small, + onClick = { onWordPress(word, index) } + ) + } + } + + Spacer(modifier = Modifier.height(22.dp)) + + // Selected words display (2 columns) + Row( + horizontalArrangement = Arrangement.spacedBy(32.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + selectedWords.take(selectedWords.size / 2).forEachIndexed { index, word -> + SelectedWordItem( + number = index + 1, + word = word ?: "", + correct = word == originalSeed[index] + ) + } + } + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + selectedWords.drop(selectedWords.size / 2).forEachIndexed { index, word -> + val actualIndex = selectedWords.size / 2 + index + SelectedWordItem( + number = actualIndex + 1, + word = word ?: "", + correct = word == originalSeed[actualIndex] + ) + } + } + } + + Spacer(modifier = Modifier.height(22.dp)) + + PrimaryButton( + text = stringResource(R.string.common__continue), + onClick = onContinue, + enabled = isComplete, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(32.dp)) + } + } +} + +@Composable +private fun SelectedWordItem( + number: Int, + word: String, + correct: Boolean, +) { + Row { + BodyMSB(text = "$number.", color = Colors.White64) + Spacer(modifier = Modifier.width(4.dp)) + BodyMSB( + text = if (word.isEmpty()) "" else word, + color = if (word.isEmpty()) Colors.White64 else if (correct) Colors.Green else Colors.Red + ) + } +} + +@Preview +@Composable +private fun Preview() { + AppThemeSurface { + val testSeed = listOf("abandon", "ability", "able", "about", "above", "absent") + ConfirmMnemonicContent( + originalSeed = testSeed, + shuffledWords = testSeed.shuffled(), + selectedWords = arrayOfNulls(testSeed.size), + pressedStates = BooleanArray(testSeed.size) { false }, + onWordPress = { _, _ -> }, + onContinue = {}, + onBack = {}, + ) + } +} + +@Preview +@Composable +private fun Preview2() { + AppThemeSurface { + val testSeed = List(24) { bip39Words.random() } + ConfirmMnemonicContent( + originalSeed = testSeed, + shuffledWords = testSeed.shuffled(), + selectedWords = testSeed.take(12).toTypedArray() + arrayOfNulls(12), + pressedStates = BooleanArray(testSeed.size) { it < 12 }, + onWordPress = { _, _ -> }, + onContinue = {}, + onBack = {}, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt new file mode 100644 index 000000000..b2b2625f1 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt @@ -0,0 +1,176 @@ +package to.bitkit.ui.settings.backups + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import to.bitkit.R +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors + +@Composable +fun ConfirmPassphraseScreen( + bip39Passphrase: String, + onContinue: () -> Unit, + onBack: () -> Unit, +) { + var enteredPassphrase by remember { mutableStateOf("") } + val keyboardController = LocalSoftwareKeyboardController.current + + DisposableEffect(Unit) { + onDispose { + enteredPassphrase = "" // Clear passphrase from memory + } + } + + ConfirmPassphraseContent( + enteredPassphrase = enteredPassphrase, + originalPassphrase = bip39Passphrase, + onPassphraseChange = { enteredPassphrase = it }, + onContinue = { + keyboardController?.hide() + onContinue() + }, + onBack = onBack, + ) +} + +@Composable +private fun ConfirmPassphraseContent( + enteredPassphrase: String, + originalPassphrase: String, + onPassphraseChange: (String) -> Unit, + onContinue: () -> Unit, + onBack: () -> Unit, +) { + val isValid = enteredPassphrase == originalPassphrase + val keyboardController = LocalSoftwareKeyboardController.current + + Column( + modifier = Modifier + .fillMaxSize() + .gradientBackground() + .padding(horizontal = 32.dp) + .imePadding() + ) { + SheetTopBar( + titleText = stringResource(R.string.security__pass_confirm), + onBack = onBack, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + BodyM( + text = stringResource(R.string.security__pass_confirm_text), + color = Colors.White64, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(32.dp)) + + OutlinedTextField( + value = enteredPassphrase, + onValueChange = onPassphraseChange, + placeholder = { + BodyM( + text = stringResource(R.string.security__pass).replaceFirstChar { it.uppercase() }, + color = Colors.White32 + ) + }, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.None, + imeAction = ImeAction.Done, + autoCorrect = false + ), + keyboardActions = KeyboardActions( + onDone = { + if (isValid) { + keyboardController?.hide() + onContinue() + } + } + ), + colors = OutlinedTextFieldDefaults.colors( + focusedTextColor = Colors.White, + unfocusedTextColor = Colors.White, + focusedBorderColor = Colors.White32, + unfocusedBorderColor = Colors.White16, + focusedContainerColor = Colors.White10, + unfocusedContainerColor = Colors.White10, + cursorColor = Colors.Brand, + ), + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.weight(1f)) + + PrimaryButton( + text = stringResource(R.string.common__continue), + onClick = onContinue, + enabled = isValid, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(32.dp)) + } + } +} + +@Preview +@Composable +private fun Preview() { + AppThemeSurface { + ConfirmPassphraseContent( + enteredPassphrase = "", + originalPassphrase = "test123", + onPassphraseChange = {}, + onContinue = {}, + onBack = {}, + ) + } +} + +@Preview +@Composable +private fun Preview2() { + AppThemeSurface { + ConfirmPassphraseContent( + enteredPassphrase = "test123", + originalPassphrase = "test123", + onPassphraseChange = {}, + onContinue = {}, + onBack = {}, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt new file mode 100644 index 000000000..f6333d5f6 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt @@ -0,0 +1,135 @@ +package to.bitkit.ui.settings.backups + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import to.bitkit.R +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.BodyS +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +@Composable +fun MetadataScreen( + onDismiss: () -> Unit, + onBack: () -> Unit, +) { + MetadataContent( + onDismiss = onDismiss, + onBack = onBack, + ) +} + +@Composable +private fun MetadataContent( + onDismiss: () -> Unit, + onBack: () -> Unit, +) { + // Mock the latest backup time (in reality this would come from backup state) + val currentTime = System.currentTimeMillis() + val formatter = SimpleDateFormat("MMMM d, yyyy 'at' h:mm a", Locale.getDefault()) + val formattedTime = formatter.format(Date(currentTime)) + + Column( + modifier = Modifier + .fillMaxSize() + .gradientBackground() + .padding(horizontal = 32.dp) + ) { + SheetTopBar( + titleText = stringResource(R.string.security__mnemonic_data_header), + onBack = onBack, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.SpaceBetween + ) { + Column { + Spacer(modifier = Modifier.height(16.dp)) + + BodyM( + text = stringResource(R.string.security__mnemonic_data_text), + color = Colors.White64, + modifier = Modifier.fillMaxWidth(), + ) + } + + // Illustration in center + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + Image( + painter = painterResource(R.drawable.card), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .width(256.dp) + .aspectRatio(1f) + ) + } + + Column { + // Latest backup time info + BodyS( + text = stringResource(R.string.security__mnemonic_latest_backup) + .replace("{time}", formattedTime), + color = Colors.White64, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) + + PrimaryButton( + text = stringResource(R.string.common__ok), + onClick = onDismiss, // Close the sheet + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(32.dp)) + } + } + } +} + +@Preview +@Composable +private fun Preview() { + AppThemeSurface { + MetadataContent( + onDismiss = {}, + onBack = {}, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt new file mode 100644 index 000000000..22bda1303 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt @@ -0,0 +1,114 @@ +package to.bitkit.ui.settings.backups + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import to.bitkit.R +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors + +@Composable +fun MultipleDevicesScreen( + onContinue: () -> Unit, + onBack: () -> Unit, +) { + MultipleDevicesContent( + onContinue = onContinue, + onBack = onBack, + ) +} + +@Composable +private fun MultipleDevicesContent( + onContinue: () -> Unit, + onBack: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .gradientBackground() + .padding(horizontal = 32.dp) + ) { + SheetTopBar( + titleText = stringResource(R.string.security__mnemonic_multiple_header), + onBack = onBack, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.SpaceBetween + ) { + Column { + Spacer(modifier = Modifier.height(16.dp)) + + BodyM( + text = stringResource(R.string.security__mnemonic_multiple_text), + color = Colors.White64, + modifier = Modifier.fillMaxWidth(), + ) + } + + // Illustration in center + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + Image( + painter = painterResource(R.drawable.phone), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .width(256.dp) + .aspectRatio(1f) + ) + } + + Column { + PrimaryButton( + text = stringResource(R.string.common__ok), + onClick = onContinue, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(32.dp)) + } + } + } +} + +@Preview +@Composable +private fun Preview() { + AppThemeSurface { + MultipleDevicesContent( + onContinue = {}, + onBack = {}, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt new file mode 100644 index 000000000..a6ff7754d --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -0,0 +1,346 @@ +package to.bitkit.ui.settings.backups + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.EaseOutQuart +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.BlurredEdgeTreatment +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.blur +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import to.bitkit.R +import to.bitkit.models.Toast +import to.bitkit.ui.appViewModel +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.BodyMSB +import to.bitkit.ui.components.BodyS +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.withAccent +import to.bitkit.utils.Logger +import to.bitkit.utils.bip39Words + +@Composable +fun ShowMnemonicScreen( + onContinue: (seed: List, bip39Passphrase: String) -> Unit, + onDismiss: () -> Unit, +) { + val app = appViewModel ?: return + val context = LocalContext.current + val clipboard = LocalClipboardManager.current + + var mnemonic by remember { mutableStateOf("") } + var bip39Passphrase by remember { mutableStateOf("") } + var showMnemonic by remember { mutableStateOf(false) } + var isLoading by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() + + DisposableEffect(Unit) { + onDispose { + mnemonic = "" // Clear mnemonic from memory when leaving screen + bip39Passphrase = "" + } + } + + ShowMnemonicContent( + mnemonic = mnemonic, + showMnemonic = showMnemonic, + isLoading = isLoading, + onDismiss = onDismiss, + onRevealClick = { + scope.launch { + try { + isLoading = true + delay(200) + val loadedMnemonic = app.loadMnemonic()!! + val loadedPassphrase = app.loadBip39Passphrase() + mnemonic = loadedMnemonic + bip39Passphrase = loadedPassphrase + showMnemonic = true + } catch (e: Throwable) { + Logger.error("Failed to load mnemonic", e) + app.toast( + type = Toast.ToastType.WARNING, + title = context.getString(R.string.security__mnemonic_error), + description = context.getString(R.string.security__mnemonic_error_description), + ) + } finally { + isLoading = false + } + } + }, + onCopyClick = { + clipboard.setText(AnnotatedString(mnemonic)) + }, + onContinueClick = { + onContinue(mnemonic.split(" "), bip39Passphrase) + }, + ) +} + +@Composable +private fun ShowMnemonicContent( + mnemonic: String, + showMnemonic: Boolean, + isLoading: Boolean, + onDismiss: () -> Unit, + onRevealClick: () -> Unit, + onCopyClick: () -> Unit, + onContinueClick: () -> Unit, +) { + val blurRadius by animateFloatAsState( + targetValue = if (showMnemonic) 0f else 10f, + animationSpec = tween(durationMillis = 800, easing = EaseOutQuart), + label = "blurRadius" + ) + + val buttonAlpha by animateFloatAsState( + targetValue = if (showMnemonic) 0f else 1f, + animationSpec = tween(durationMillis = 400), + label = "buttonAlpha" + ) + + val mnemonicWords = if (mnemonic.isNotEmpty()) mnemonic.split(" ") else emptyList() + val scrollState = rememberScrollState() + val scope = rememberCoroutineScope() + + // Scroll to bottom when mnemonic is revealed + LaunchedEffect(showMnemonic) { + if (showMnemonic) { + delay(300) // Wait for the animation to start + scope.launch { + scrollState.animateScrollTo(scrollState.maxValue) + } + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .gradientBackground() + .padding(horizontal = 32.dp) + ) { + SheetTopBar( + titleText = stringResource(R.string.security__mnemonic_your), + onBack = onDismiss, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + ) { + Spacer(modifier = Modifier.height(16.dp)) + + AnimatedContent( + targetState = showMnemonic, + transitionSpec = { fadeIn(tween(300)).togetherWith(fadeOut(tween(300))) }, + label = "topText" + ) { isRevealed -> + BodyM( + text = if (isRevealed) { + stringResource(R.string.security__mnemonic_write).replace("{length}", "${mnemonicWords.size}") + } else { + stringResource(R.string.security__mnemonic_use) + }, + color = Colors.White64, + modifier = Modifier.fillMaxWidth(), + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + Box( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 32.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .background(color = Colors.White10) + .clickable(enabled = showMnemonic && mnemonic.isNotEmpty(), onClick = onCopyClick) + .padding(horizontal = 32.dp, vertical = 32.dp) + ) { + MnemonicWordsGrid( + actualWords = mnemonicWords, + showMnemonic = showMnemonic, + blurRadius = blurRadius, + ) + } + + if (buttonAlpha > 0f) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .matchParentSize() + ) { + PrimaryButton( + text = stringResource(R.string.security__mnemonic_reveal), + fullWidth = false, + isLoading = isLoading, + onClick = onRevealClick, + color = Colors.Black50, + modifier = Modifier.alpha(buttonAlpha) + ) + } + } + } + + BodyS( + text = stringResource(R.string.security__mnemonic_never_share).withAccent(accentColor = Colors.Brand), + color = Colors.White64, + ) + + Spacer(modifier = Modifier.height(24.dp)) + + PrimaryButton( + text = stringResource(R.string.common__continue), + onClick = onContinueClick, + enabled = showMnemonic, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(32.dp)) + } + } +} + +@Composable +private fun MnemonicWordsGrid( + actualWords: List, + showMnemonic: Boolean, + blurRadius: Float, +) { + val placeholderWords = remember { List(24) { "secret" } } + + Box( + modifier = Modifier + .fillMaxWidth() + .blur(radius = blurRadius.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded) + ) { + Crossfade( + targetState = showMnemonic, + animationSpec = tween(durationMillis = 600), + label = "mnemonicCrossfade" + ) { isRevealed -> + val wordsToShow = if (isRevealed && actualWords.isNotEmpty()) actualWords else placeholderWords + + Row( + horizontalArrangement = Arrangement.spacedBy(32.dp), + modifier = Modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + wordsToShow.take(wordsToShow.size / 2).forEachIndexed { index, word -> + WordItem( + number = index + 1, + word = word + ) + } + } + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + wordsToShow.drop(wordsToShow.size / 2).forEachIndexed { index, word -> + WordItem( + number = wordsToShow.size / 2 + index + 1, + word = word + ) + } + } + } + } + } +} + +@Composable +private fun WordItem( + number: Int, + word: String, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + BodyMSB(text = "$number.", color = Colors.White64) + Spacer(modifier = Modifier.width(8.dp)) + BodyMSB(text = word, color = Colors.White) + } +} + +@Preview +@Composable +private fun Preview() { + AppThemeSurface { + ShowMnemonicContent( + mnemonic = "", + showMnemonic = false, + isLoading = false, + onDismiss = {}, + onRevealClick = {}, + onCopyClick = {}, + onContinueClick = {}, + ) + } +} + +@Preview +@Composable +private fun PreviewShown() { + AppThemeSurface { + ShowMnemonicContent( + mnemonic = List(24) { bip39Words.random() }.joinToString(" "), + showMnemonic = true, + isLoading = false, + onDismiss = {}, + onRevealClick = {}, + onCopyClick = {}, + onContinueClick = {}, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt new file mode 100644 index 000000000..8b7021630 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt @@ -0,0 +1,97 @@ +package to.bitkit.ui.settings.backups + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import to.bitkit.R +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors + +@Composable +fun ShowPassphraseScreen( + seed: List, + bip39Passphrase: String, + onContinue: () -> Unit, + onBack: () -> Unit, +) { + ShowPassphraseContent( + seed = seed, + bip39Passphrase = bip39Passphrase, + onContinue = onContinue, + onBack = onBack, + ) +} + +@Composable +private fun ShowPassphraseContent( + seed: List, + bip39Passphrase: String, + onContinue: () -> Unit, + onBack: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .gradientBackground() + .padding(horizontal = 32.dp) + ) { + SheetTopBar( + titleText = "BIP39 Passphrase", + onBack = onBack, + ) + + Column( + modifier = Modifier.fillMaxSize() + ) { + Spacer(modifier = Modifier.height(16.dp)) + + BodyM( + text = "Your wallet also uses this BIP39 passphrase:", + color = Colors.White64, + modifier = Modifier.fillMaxWidth(), + ) + + Spacer(modifier = Modifier.height(32.dp)) + + BodyM( + text = "Passphrase: $bip39Passphrase", + color = Colors.White, + ) + + Spacer(modifier = Modifier.weight(1f)) + + PrimaryButton( + text = stringResource(R.string.common__continue), + onClick = onContinue, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(32.dp)) + } + } +} + +@Preview +@Composable +private fun Preview() { + AppThemeSurface { + ShowPassphraseContent( + seed = listOf("word1", "word2", "word3"), + bip39Passphrase = "test passphrase", + onContinue = {}, + onBack = {}, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt new file mode 100644 index 000000000..1f53322f8 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt @@ -0,0 +1,121 @@ +package to.bitkit.ui.settings.backups + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import to.bitkit.R +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.withAccent + +@Composable +fun SuccessScreen( + onContinue: () -> Unit, + onBack: () -> Unit, +) { + SuccessContent( + onContinue = onContinue, + onBack = onBack, + ) +} + +@Composable +private fun SuccessContent( + onContinue: () -> Unit, + onBack: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .gradientBackground() + .padding(horizontal = 32.dp) + ) { + SheetTopBar( + titleText = stringResource(R.string.security__mnemonic_result_header), + onBack = onBack, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.SpaceBetween + ) { + Column { + Spacer(modifier = Modifier.height(16.dp)) + + BodyM( + text = stringResource(R.string.security__mnemonic_result_text).withAccent( + accentColor = Colors.White + ), + color = Colors.White64, + modifier = Modifier.fillMaxWidth(), + ) + } + + // Illustration in center + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + Image( + painter = painterResource(R.drawable.check), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .width(256.dp) + .aspectRatio(1f) + ) + } + + Column { + PrimaryButton( + text = stringResource(R.string.common__ok), + onClick = { + // TODO: Dispatch backup verification + // dispatch(verifyBackup()) + onContinue() + }, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(32.dp)) + } + } + } +} + +@Preview +@Composable +private fun Preview() { + AppThemeSurface { + SuccessContent( + onContinue = {}, + onBack = {}, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt new file mode 100644 index 000000000..a30fc8406 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt @@ -0,0 +1,117 @@ +package to.bitkit.ui.settings.backups + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import to.bitkit.R +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.withAccent + +@Composable +fun WarningScreen( + onContinue: () -> Unit, + onBack: () -> Unit, +) { + WarningContent( + onContinue = onContinue, + onBack = onBack, + ) +} + +@Composable +private fun WarningContent( + onContinue: () -> Unit, + onBack: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .gradientBackground() + .padding(horizontal = 32.dp) + ) { + SheetTopBar( + titleText = stringResource(R.string.security__mnemonic_keep_header), + onBack = onBack, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.SpaceBetween + ) { + Column { + Spacer(modifier = Modifier.height(16.dp)) + + BodyM( + text = stringResource(R.string.security__mnemonic_keep_text).withAccent( + accentColor = Colors.White + ), + color = Colors.White64, + modifier = Modifier.fillMaxWidth(), + ) + } + + // Illustration in center + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + Image( + painter = painterResource(R.drawable.exclamation_mark), + contentDescription = null, + contentScale = ContentScale.Fit, + modifier = Modifier + .width(256.dp) + .aspectRatio(1f) + ) + } + + Column { + PrimaryButton( + text = stringResource(R.string.common__ok), + onClick = onContinue, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(32.dp)) + } + } + } +} + +@Preview +@Composable +private fun Preview() { + AppThemeSurface { + WarningContent( + onContinue = {}, + onBack = {}, + ) + } +} diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index a544b2218..ae0643180 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -784,6 +784,10 @@ class AppViewModel @Inject constructor( return keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) } + fun loadBip39Passphrase(): String { + return keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name) ?: "" + } + // region security fun resetIsAuthenticatedState() { viewModelScope.launch { From 8bb03d3a9d7cccb831a444ac47381eb6543aa41d Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 3 Jun 2025 21:50:24 +0200 Subject: [PATCH 05/25] feat: Sheets drag handle & title UI polish --- .../java/to/bitkit/ui/components/SheetHost.kt | 21 +++++++++++++++++++ .../main/java/to/bitkit/ui/components/Text.kt | 2 ++ .../java/to/bitkit/ui/scaffold/SheetTopBar.kt | 7 +++---- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt index 76e737f54..855ccdff2 100644 --- a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt +++ b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt @@ -7,11 +7,14 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material3.BottomSheetScaffold import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SheetState import androidx.compose.material3.SheetValue +import androidx.compose.material3.Surface import androidx.compose.material3.rememberBottomSheetScaffoldState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -19,6 +22,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import to.bitkit.ui.screens.wallets.send.SendRoute @@ -77,6 +82,7 @@ fun SheetHost( sheetPeekHeight = 0.dp, sheetShape = AppShapes.sheet, sheetContent = sheets, + sheetDragHandle = { SheetDragHandle() }, sheetContainerColor = Colors.Gray6, sheetContentColor = MaterialTheme.colorScheme.onSurface, ) { @@ -92,6 +98,21 @@ fun SheetHost( } } +@Composable +private fun SheetDragHandle( + modifier: Modifier = Modifier, +) { + Surface( + color = Colors.White32, + shape = MaterialTheme.shapes.extraLarge, + modifier = modifier + .padding(top = 12.dp, bottom = 2.dp) + .semantics { contentDescription = "Drag handle" } + ) { + Box(Modifier.size(width = 32.dp, height = 4.dp)) + } +} + @Composable @OptIn(ExperimentalMaterial3Api::class) private fun Scrim( diff --git a/app/src/main/java/to/bitkit/ui/components/Text.kt b/app/src/main/java/to/bitkit/ui/components/Text.kt index dae278bf1..26b4e6617 100644 --- a/app/src/main/java/to/bitkit/ui/components/Text.kt +++ b/app/src/main/java/to/bitkit/ui/components/Text.kt @@ -130,6 +130,7 @@ fun Subtitle( text: String, modifier: Modifier = Modifier, color: Color = MaterialTheme.colorScheme.primary, + textAlign: TextAlign = TextAlign.Start, ) { Text( text = text, @@ -139,6 +140,7 @@ fun Subtitle( letterSpacing = 0.4.sp, fontFamily = InterFontFamily, color = color, + textAlign = textAlign, ), modifier = modifier, ) diff --git a/app/src/main/java/to/bitkit/ui/scaffold/SheetTopBar.kt b/app/src/main/java/to/bitkit/ui/scaffold/SheetTopBar.kt index f4f9a485b..03c4b4d04 100644 --- a/app/src/main/java/to/bitkit/ui/scaffold/SheetTopBar.kt +++ b/app/src/main/java/to/bitkit/ui/scaffold/SheetTopBar.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R +import to.bitkit.ui.components.Subtitle import to.bitkit.ui.theme.AppThemeSurface @Composable @@ -40,15 +41,13 @@ fun SheetTopBar( .fillMaxWidth() .height(42.dp) ) { - Text( + Subtitle( text = titleText, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.ExtraBold, textAlign = TextAlign.Center, modifier = Modifier .fillMaxWidth() .padding(horizontal = 24.dp) - .align(Alignment.Center), + .align(Alignment.Center) ) onBack?.let { callback -> From fffa94c1a98df8a49ad96d2030d629de369ba903 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Tue, 3 Jun 2025 22:49:35 +0200 Subject: [PATCH 06/25] refactor: Add global sheet host and use it --- app/src/main/java/to/bitkit/ui/ContentView.kt | 527 ++++++++++-------- .../bitkit/ui/screens/wallets/HomeScreen.kt | 337 +++++------ .../ui/settings/BackupSettingsScreen.kt | 7 +- .../settings/backups/BackupNavigationSheet.kt | 18 +- .../ui/settings/backups/BackupWalletScreen.kt | 13 - .../ui/settings/backups/ShowMnemonicScreen.kt | 2 - .../ui/utils/AutoReadClipboardHandler.kt | 5 - 7 files changed, 454 insertions(+), 455 deletions(-) delete mode 100644 app/src/main/java/to/bitkit/ui/settings/backups/BackupWalletScreen.kt diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 39f6ae859..8ad1d4f14 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -27,6 +27,7 @@ import androidx.navigation.compose.navigation import androidx.navigation.compose.rememberNavController import androidx.navigation.navOptions import androidx.navigation.toRoute +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.serialization.Serializable @@ -35,6 +36,7 @@ import to.bitkit.models.NodeLifecycleState import to.bitkit.models.WidgetType import to.bitkit.ui.components.AuthCheckScreen import to.bitkit.ui.components.BottomSheetType +import to.bitkit.ui.components.SheetHost import to.bitkit.ui.onboarding.InitializingWalletView import to.bitkit.ui.onboarding.WalletInitResult import to.bitkit.ui.onboarding.WalletInitResultView @@ -66,11 +68,14 @@ import to.bitkit.ui.screens.transfer.external.ExternalSuccessScreen import to.bitkit.ui.screens.wallets.HomeScreen import to.bitkit.ui.screens.wallets.activity.ActivityDetailScreen import to.bitkit.ui.screens.wallets.activity.ActivityExploreScreen +import to.bitkit.ui.screens.wallets.activity.DateRangeSelectorSheet +import to.bitkit.ui.screens.wallets.activity.TagSelectorSheet +import to.bitkit.ui.screens.wallets.receive.ReceiveQrSheet +import to.bitkit.ui.screens.wallets.send.SendOptionsView import to.bitkit.ui.screens.wallets.suggestion.BuyIntroScreen import to.bitkit.ui.screens.widgets.AddWidgetsScreen import to.bitkit.ui.screens.widgets.WidgetsIntroScreen import to.bitkit.ui.screens.widgets.facts.FactsEditScreen -import to.bitkit.ui.screens.widgets.facts.FactsPreviewContent import to.bitkit.ui.screens.widgets.facts.FactsPreviewScreen import to.bitkit.ui.screens.widgets.facts.FactsViewModel import to.bitkit.ui.screens.widgets.headlines.HeadlinesEditScreen @@ -89,7 +94,8 @@ import to.bitkit.ui.settings.LogsScreen import to.bitkit.ui.settings.OrderDetailScreen import to.bitkit.ui.settings.SecuritySettingsScreen import to.bitkit.ui.settings.SettingsScreen -import to.bitkit.ui.settings.backups.BackupWalletScreen +import to.bitkit.ui.settings.backups.BackupNavigationSheet +import to.bitkit.ui.settings.backups.BackupSheet import to.bitkit.ui.settings.backups.RestoreWalletScreen import to.bitkit.ui.settings.general.DefaultUnitSettingsScreen import to.bitkit.ui.settings.general.GeneralSettingsScreen @@ -101,6 +107,7 @@ import to.bitkit.ui.settings.pin.ChangePinNewScreen import to.bitkit.ui.settings.pin.ChangePinResultScreen import to.bitkit.ui.settings.pin.ChangePinScreen import to.bitkit.ui.settings.pin.DisablePinScreen +import to.bitkit.ui.settings.pin.PinNavigationSheet import to.bitkit.ui.settings.quickPay.QuickPayIntroScreen import to.bitkit.ui.settings.quickPay.QuickPaySettingsScreen import to.bitkit.ui.settings.support.ReportIssueResultScreen @@ -119,6 +126,7 @@ import to.bitkit.viewmodels.BlocktankViewModel import to.bitkit.viewmodels.CurrencyViewModel import to.bitkit.viewmodels.ExternalNodeViewModel import to.bitkit.viewmodels.MainScreenEffect +import to.bitkit.viewmodels.SendEvent import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.TransferViewModel import to.bitkit.viewmodels.WalletViewModel @@ -270,225 +278,301 @@ fun ContentView( ) { AutoReadClipboardHandler() - NavHost(navController, startDestination = Routes.Home) { - home(walletViewModel, appViewModel, activityListViewModel, settingsViewModel, navController) - settings(navController, settingsViewModel) - profile(navController, settingsViewModel) - shop(navController, settingsViewModel) - nodeState(walletViewModel, navController) - generalSettings(navController) - advancedSettings(navController) - aboutSettings(navController) - transactionSpeedSettings(navController) - securitySettings(navController) - disablePin(navController) - changePin(navController) - changePinNew(navController) - changePinConfirm(navController) - changePinResult(navController) - defaultUnitSettings(currencyViewModel, navController) - localCurrencySettings(currencyViewModel, navController) - backupSettings(navController) - backupWalletSettings(navController) - restoreWalletSettings(navController) - channelOrdersSettings(navController) - orderDetailSettings(navController) - cjitDetailSettings(navController) - lightning(walletViewModel, navController) - devSettings(walletViewModel, navController) - regtestSettings(navController) - activityItem(activityListViewModel, navController) - qrScanner(appViewModel, navController) - authCheck(navController) - logs(navController) - suggestions(navController) - widgets(navController, settingsViewModel, currencyViewModel) - - // TODO extract transferNavigation - navigation( - startDestination = Routes.TransferIntro, - ) { - composable { - TransferIntroScreen( - onContinueClick = { - navController.navigateToTransferFunding() - settingsViewModel.setHasSeenTransferIntro(true) - }, - onCloseClick = { navController.navigateToHome() }, - ) - } - composable { - SavingsIntroScreen( - onContinueClick = { - navController.navigate(Routes.SavingsAvailability) - settingsViewModel.setHasSeenSavingsIntro(true) - }, - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.navigateToHome() }, - ) - } - composable { - SavingsAvailabilityScreen( - onBackClick = { navController.popBackStack() }, - onCancelClick = { navController.navigateToHome() }, - onContinueClick = { navController.navigate(Routes.SavingsConfirm) }, - ) - } - composable { - SavingsConfirmScreen( - onConfirm = { navController.navigate(Routes.SavingsProgress) }, - onAdvancedClick = { navController.navigate(Routes.SavingsAdvanced) }, - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.navigateToHome() }, - ) - } - composable { - SavingsAdvancedScreen( - onContinueClick = { navController.popBackStack(inclusive = false) }, - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.navigateToHome() }, - ) - } - composable { - SavingsProgressScreen( - onContinueClick = { navController.navigateToHome() }, - onCloseClick = { navController.navigateToHome() }, - ) - } - composable { - SpendingIntroScreen( - onContinueClick = { - navController.navigate(Routes.SpendingAmount) - settingsViewModel.setHasSeenSpendingIntro(true) - }, - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.navigateToHome() }, - ) - } - composable { - SpendingAmountScreen( - viewModel = transferViewModel, - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.navigateToHome() }, - onOrderCreated = { navController.navigate(Routes.SpendingConfirm) }, - ) - } - composable { - SpendingConfirmScreen( - viewModel = transferViewModel, - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.navigateToHome() }, - onLearnMoreClick = { navController.navigate(Routes.TransferLiquidity) }, - onAdvancedClick = { navController.navigate(Routes.SpendingAdvanced) }, - onConfirm = { navController.navigate(Routes.SettingUp) }, - ) - } - composable { - SpendingAdvancedScreen( - viewModel = transferViewModel, - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.navigateToHome() }, - onOrderCreated = { navController.popBackStack(inclusive = false) }, - ) - } - composable { - LiquidityScreen( - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.navigateToHome() }, - onContinueClick = { navController.popBackStack() } - ) - } - composable { - SettingUpScreen( - viewModel = transferViewModel, - onCloseClick = { navController.navigateToHome() }, - onContinueClick = { navController.navigateToHome() }, - ) - } - composable { - val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsState() - FundingScreen( - onTransfer = { - if (!hasSeenSpendingIntro) { - navController.navigateToTransferSpendingIntro() - } else { - navController.navigateToTransferSpendingAmount() + val currentSheet by appViewModel.currentSheet + SheetHost( + shouldExpand = currentSheet != null, + onDismiss = { appViewModel.hideSheet() }, + sheets = { + when (val sheet = currentSheet) { + is BottomSheetType.Send -> { + SendOptionsView( + appViewModel = appViewModel, + walletViewModel = walletViewModel, + startDestination = sheet.route, + onComplete = { txSheet -> + appViewModel.setSendEvent(SendEvent.Reset) + appViewModel.hideSheet() + txSheet?.let { appViewModel.showNewTransactionSheet(it) } } - }, - onFund = { - scope.launch { - // TODO show receive sheet -> ReceiveAmount - navController.navigateToHome() - delay(500) // Wait for nav to actually finish - appViewModel.showSheet(BottomSheetType.Receive) + ) + } + + is BottomSheetType.Receive -> { + val walletUiState by walletViewModel.uiState.collectAsState() + ReceiveQrSheet( + walletState = walletUiState, + navigateToExternalConnection = { + navController.navigate(Routes.ExternalConnection) } + ) + } + + is BottomSheetType.ActivityDateRangeSelector -> DateRangeSelectorSheet() + is BottomSheetType.ActivityTagSelector -> TagSelectorSheet() + + is BottomSheetType.PinSetup -> PinNavigationSheet( + onDismiss = { appViewModel.hideSheet() }, + ) + + BottomSheetType.Backup -> BackupSheet( + onDismiss = { appViewModel.hideSheet() }, + onBackupClick = { + appViewModel.hideSheet() + appViewModel.showSheet(BottomSheetType.BackupNavigation) }, - onAdvanced = { navController.navigate(Routes.FundingAdvanced) }, - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.navigateUp() }, + walletViewModel = walletViewModel ) - } - composable { - FundingAdvancedScreen( - onLnUrl = { navController.navigateToQrScanner() }, - onManual = { navController.navigate(Routes.ExternalNav) }, - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.popBackStack(inclusive = true) }, + + BottomSheetType.BackupNavigation -> BackupNavigationSheet( + onDismiss = { appViewModel.hideSheet() }, ) + + null -> Unit } - navigation( - startDestination = Routes.ExternalConnection, - ) { - composable { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ExternalNav) } - val viewModel = hiltViewModel(parentEntry) - - ExternalConnectionScreen( - viewModel = viewModel, - onNodeConnected = { navController.navigate(Routes.ExternalAmount) }, - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.popBackStack(inclusive = true) }, - ) - } - composable { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ExternalNav) } - val viewModel = hiltViewModel(parentEntry) - - ExternalAmountScreen( - viewModel = viewModel, - onContinue = { navController.navigate(Routes.ExternalConfirm) }, - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.popBackStack(inclusive = true) }, - ) - } - composable { - val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ExternalNav) } - val viewModel = hiltViewModel(parentEntry) - - ExternalConfirmScreen( - viewModel = viewModel, - onConfirm = { - walletViewModel.refreshState() - navController.navigate(Routes.ExternalSuccess) - }, - onNetworkFeeClick = { navController.navigate(Routes.ExternalFeeCustom) }, - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.popBackStack(inclusive = true) }, - ) - } - composable { - ExternalSuccessScreen( - onContinue = { navController.navigateToHome() }, - onClose = { navController.navigateToHome() }, - ) + } + ) { + RootNavHost( + navController = navController, + walletViewModel = walletViewModel, + appViewModel = appViewModel, + activityListViewModel = activityListViewModel, + settingsViewModel = settingsViewModel, + currencyViewModel = currencyViewModel, + transferViewModel = transferViewModel, + scope = scope, + ) + } + } + } +} + +@Composable +private fun RootNavHost( + navController: NavHostController, + walletViewModel: WalletViewModel, + appViewModel: AppViewModel, + activityListViewModel: ActivityListViewModel, + settingsViewModel: SettingsViewModel, + currencyViewModel: CurrencyViewModel, + transferViewModel: TransferViewModel, + scope: CoroutineScope, +) { + NavHost(navController, startDestination = Routes.Home) { + home(walletViewModel, appViewModel, activityListViewModel, settingsViewModel, navController) + settings(navController, settingsViewModel) + profile(navController, settingsViewModel) + shop(navController, settingsViewModel) + nodeState(walletViewModel, navController) + generalSettings(navController) + advancedSettings(navController) + aboutSettings(navController) + transactionSpeedSettings(navController) + securitySettings(navController) + disablePin(navController) + changePin(navController) + changePinNew(navController) + changePinConfirm(navController) + changePinResult(navController) + defaultUnitSettings(currencyViewModel, navController) + localCurrencySettings(currencyViewModel, navController) + backupSettings(navController) + restoreWalletSettings(navController) + channelOrdersSettings(navController) + orderDetailSettings(navController) + cjitDetailSettings(navController) + lightning(walletViewModel, navController) + devSettings(walletViewModel, navController) + regtestSettings(navController) + activityItem(activityListViewModel, navController) + qrScanner(appViewModel, navController) + authCheck(navController) + logs(navController) + suggestions(navController) + widgets(navController, settingsViewModel, currencyViewModel) + + // TODO extract transferNavigation + navigation( + startDestination = Routes.TransferIntro, + ) { + composable { + TransferIntroScreen( + onContinueClick = { + navController.navigateToTransferFunding() + settingsViewModel.setHasSeenTransferIntro(true) + }, + onCloseClick = { navController.navigateToHome() }, + ) + } + composable { + SavingsIntroScreen( + onContinueClick = { + navController.navigate(Routes.SavingsAvailability) + settingsViewModel.setHasSeenSavingsIntro(true) + }, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.navigateToHome() }, + ) + } + composable { + SavingsAvailabilityScreen( + onBackClick = { navController.popBackStack() }, + onCancelClick = { navController.navigateToHome() }, + onContinueClick = { navController.navigate(Routes.SavingsConfirm) }, + ) + } + composable { + SavingsConfirmScreen( + onConfirm = { navController.navigate(Routes.SavingsProgress) }, + onAdvancedClick = { navController.navigate(Routes.SavingsAdvanced) }, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.navigateToHome() }, + ) + } + composable { + SavingsAdvancedScreen( + onContinueClick = { navController.popBackStack(inclusive = false) }, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.navigateToHome() }, + ) + } + composable { + SavingsProgressScreen( + onContinueClick = { navController.navigateToHome() }, + onCloseClick = { navController.navigateToHome() }, + ) + } + composable { + SpendingIntroScreen( + onContinueClick = { + navController.navigate(Routes.SpendingAmount) + settingsViewModel.setHasSeenSpendingIntro(true) + }, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.navigateToHome() }, + ) + } + composable { + SpendingAmountScreen( + viewModel = transferViewModel, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.navigateToHome() }, + onOrderCreated = { navController.navigate(Routes.SpendingConfirm) }, + ) + } + composable { + SpendingConfirmScreen( + viewModel = transferViewModel, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.navigateToHome() }, + onLearnMoreClick = { navController.navigate(Routes.TransferLiquidity) }, + onAdvancedClick = { navController.navigate(Routes.SpendingAdvanced) }, + onConfirm = { navController.navigate(Routes.SettingUp) }, + ) + } + composable { + SpendingAdvancedScreen( + viewModel = transferViewModel, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.navigateToHome() }, + onOrderCreated = { navController.popBackStack(inclusive = false) }, + ) + } + composable { + LiquidityScreen( + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.navigateToHome() }, + onContinueClick = { navController.popBackStack() } + ) + } + composable { + SettingUpScreen( + viewModel = transferViewModel, + onCloseClick = { navController.navigateToHome() }, + onContinueClick = { navController.navigateToHome() }, + ) + } + composable { + val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsState() + FundingScreen( + onTransfer = { + if (!hasSeenSpendingIntro) { + navController.navigateToTransferSpendingIntro() + } else { + navController.navigateToTransferSpendingAmount() } - composable { - ExternalFeeCustomScreen( - onBackClick = { navController.popBackStack() }, - onCloseClick = { navController.popBackStack(inclusive = true) }, - ) + }, + onFund = { + scope.launch { + // TODO show receive sheet -> ReceiveAmount + navController.navigateToHome() + delay(500) // Wait for nav to actually finish + appViewModel.showSheet(BottomSheetType.Receive) } - } + }, + onAdvanced = { navController.navigate(Routes.FundingAdvanced) }, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.navigateUp() }, + ) + } + composable { + FundingAdvancedScreen( + onLnUrl = { navController.navigateToQrScanner() }, + onManual = { navController.navigate(Routes.ExternalNav) }, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.popBackStack(inclusive = true) }, + ) + } + navigation( + startDestination = Routes.ExternalConnection, + ) { + composable { + val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ExternalNav) } + val viewModel = hiltViewModel(parentEntry) + + ExternalConnectionScreen( + viewModel = viewModel, + onNodeConnected = { navController.navigate(Routes.ExternalAmount) }, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.popBackStack(inclusive = true) }, + ) + } + composable { + val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ExternalNav) } + val viewModel = hiltViewModel(parentEntry) + + ExternalAmountScreen( + viewModel = viewModel, + onContinue = { navController.navigate(Routes.ExternalConfirm) }, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.popBackStack(inclusive = true) }, + ) + } + composable { + val parentEntry = remember(it) { navController.getBackStackEntry(Routes.ExternalNav) } + val viewModel = hiltViewModel(parentEntry) + + ExternalConfirmScreen( + viewModel = viewModel, + onConfirm = { + walletViewModel.refreshState() + navController.navigate(Routes.ExternalSuccess) + }, + onNetworkFeeClick = { navController.navigate(Routes.ExternalFeeCustom) }, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.popBackStack(inclusive = true) }, + ) + } + composable { + ExternalSuccessScreen( + onContinue = { navController.navigateToHome() }, + onClose = { navController.navigateToHome() }, + ) + } + composable { + ExternalFeeCustomScreen( + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.popBackStack(inclusive = true) }, + ) } } } @@ -693,14 +777,6 @@ private fun NavGraphBuilder.backupSettings( } } -private fun NavGraphBuilder.backupWalletSettings( - navController: NavHostController, -) { - composableWithDefaultTransitions { - BackupWalletScreen(navController) - } -} - private fun NavGraphBuilder.restoreWalletSettings( navController: NavHostController, ) { @@ -964,7 +1040,7 @@ private fun NavGraphBuilder.widgets( val parentEntry = remember(it) { navController.getBackStackEntry(Routes.Facts) } val viewModel = hiltViewModel(parentEntry) - FactsEditScreen ( + FactsEditScreen( factsViewModel = viewModel, onClose = { navController.navigateToHome() }, onBack = { navController.popBackStack() }, @@ -1046,10 +1122,6 @@ fun NavController.navigateToBackupSettings() = navigate( route = Routes.BackupSettings, ) -fun NavController.navigateToBackupWalletSettings() = navigate( - route = Routes.BackupWalletSettings, -) - fun NavController.navigateToRestoreWalletSettings() = navigate( route = Routes.RestoreWalletSettings, ) @@ -1217,9 +1289,6 @@ object Routes { @Serializable data object BackupSettings - @Serializable - data object BackupWalletSettings - @Serializable data object RestoreWalletSettings diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt index b579d3b51..62f668a59 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeScreen.kt @@ -115,219 +115,166 @@ fun HomeScreen( rootNavController: NavController, ) { val uiState: MainUiState by walletViewModel.uiState.collectAsStateWithLifecycle() - val currentSheet by appViewModel.currentSheet - - SheetHost( - shouldExpand = currentSheet != null, - onDismiss = { appViewModel.hideSheet() }, - sheets = { - when (val sheet = currentSheet) { - is BottomSheetType.Send -> { - SendOptionsView( - appViewModel = appViewModel, - walletViewModel = walletViewModel, - startDestination = sheet.route, - onComplete = { txSheet -> - appViewModel.setSendEvent(SendEvent.Reset) - appViewModel.hideSheet() - txSheet?.let { appViewModel.showNewTransactionSheet(it) } - } - ) - } - - is BottomSheetType.Receive -> { - ReceiveQrSheet( - walletState = uiState, - navigateToExternalConnection = { - rootNavController.navigate(Routes.ExternalConnection) - } - ) - } - - is BottomSheetType.ActivityDateRangeSelector -> DateRangeSelectorSheet() - is BottomSheetType.ActivityTagSelector -> TagSelectorSheet() - - is BottomSheetType.PinSetup -> PinNavigationSheet( - onDismiss = { appViewModel.hideSheet() }, - ) - BottomSheetType.Backup -> BackupSheet( - onDismiss = { appViewModel.hideSheet() }, - onBackupClick = { - appViewModel.hideSheet() - appViewModel.showSheet(BottomSheetType.BackupNavigation) + Box(modifier = Modifier.fillMaxSize()) { + val walletNavController = rememberNavController() + NavHost( + navController = walletNavController, + startDestination = HomeRoutes.Home, + ) { + composable { + val context = LocalContext.current + val homeViewModel: HomeViewModel = hiltViewModel() + val homeUiState: HomeUiState by homeViewModel.uiState.collectAsStateWithLifecycle() + val hasSeenTransferIntro by settingsViewModel.hasSeenTransferIntro.collectAsStateWithLifecycle() + val hasSeenShopIntro by settingsViewModel.hasSeenShopIntro.collectAsStateWithLifecycle() + val hasSeenProfileIntro by settingsViewModel.hasSeenProfileIntro.collectAsStateWithLifecycle() + val quickPayIntroSeen by settingsViewModel.quickPayIntroSeen.collectAsStateWithLifecycle() + val hasSeenWidgetsIntro by settingsViewModel.hasSeenWidgetsIntro.collectAsStateWithLifecycle() + + HomeContentView( + mainUiState = uiState, + homeUiState = homeUiState, + rootNavController = rootNavController, + walletNavController = walletNavController, + onRefresh = { + walletViewModel.onPullToRefresh() + activityListViewModel.syncLdkNodePayments() }, - walletViewModel = walletViewModel - ) - - BottomSheetType.BackupNavigation -> BackupNavigationSheet( - onDismiss = { appViewModel.hideSheet() }, - ) - - null -> Unit - } - } - ) { - Box(modifier = Modifier.fillMaxSize()) { - val walletNavController = rememberNavController() - NavHost( - navController = walletNavController, - startDestination = HomeRoutes.Home, - ) { - composable { - val context = LocalContext.current - val homeViewModel: HomeViewModel = hiltViewModel() - val homeUiState: HomeUiState by homeViewModel.uiState.collectAsStateWithLifecycle() - val hasSeenTransferIntro by settingsViewModel.hasSeenTransferIntro.collectAsStateWithLifecycle() - val hasSeenShopIntro by settingsViewModel.hasSeenShopIntro.collectAsStateWithLifecycle() - val hasSeenProfileIntro by settingsViewModel.hasSeenProfileIntro.collectAsStateWithLifecycle() - val quickPayIntroSeen by settingsViewModel.quickPayIntroSeen.collectAsStateWithLifecycle() - val hasSeenWidgetsIntro by settingsViewModel.hasSeenWidgetsIntro.collectAsStateWithLifecycle() - - HomeContentView( - mainUiState = uiState, - homeUiState = homeUiState, - rootNavController = rootNavController, - walletNavController = walletNavController, - onRefresh = { - walletViewModel.onPullToRefresh() - activityListViewModel.syncLdkNodePayments() - }, - onRemoveSuggestion = { suggestion -> - homeViewModel.removeSuggestion(suggestion) - }, - onClickSuggestion = { suggestion -> - when (suggestion) { - Suggestion.BUY -> { - rootNavController.navigate(Routes.BuyIntro) - } + onRemoveSuggestion = { suggestion -> + homeViewModel.removeSuggestion(suggestion) + }, + onClickSuggestion = { suggestion -> + when (suggestion) { + Suggestion.BUY -> { + rootNavController.navigate(Routes.BuyIntro) + } - Suggestion.SPEND -> { - if (!hasSeenTransferIntro) { - rootNavController.navigateToTransferIntro() - } else { - rootNavController.navigateToTransferFunding() - } + Suggestion.SPEND -> { + if (!hasSeenTransferIntro) { + rootNavController.navigateToTransferIntro() + } else { + rootNavController.navigateToTransferFunding() } + } - Suggestion.BACK_UP -> { - appViewModel.showSheet(BottomSheetType.Backup) - } + Suggestion.BACK_UP -> { + appViewModel.showSheet(BottomSheetType.Backup) + } - Suggestion.SECURE -> { - appViewModel.showSheet(BottomSheetType.PinSetup) - } + Suggestion.SECURE -> { + appViewModel.showSheet(BottomSheetType.PinSetup) + } - Suggestion.SUPPORT -> { - rootNavController.navigate(Routes.Support) - } + Suggestion.SUPPORT -> { + rootNavController.navigate(Routes.Support) + } - Suggestion.INVITE -> { - shareText( - context, - context.getString(R.string.settings__about__shareText) - .replace("{appStoreUrl}", Env.APP_STORE_URL) - .replace("{playStoreUrl}", Env.PLAY_STORE_URL) - ) - } + Suggestion.INVITE -> { + shareText( + context, + context.getString(R.string.settings__about__shareText) + .replace("{appStoreUrl}", Env.APP_STORE_URL) + .replace("{playStoreUrl}", Env.PLAY_STORE_URL) + ) + } - Suggestion.PROFILE -> { - if (!hasSeenProfileIntro) { - rootNavController.navigate(Routes.ProfileIntro) - } else { - rootNavController.navigate(Routes.CreateProfile) - } + Suggestion.PROFILE -> { + if (!hasSeenProfileIntro) { + rootNavController.navigate(Routes.ProfileIntro) + } else { + rootNavController.navigate(Routes.CreateProfile) } + } - Suggestion.SHOP -> { - if (!hasSeenShopIntro) { - rootNavController.navigate(Routes.ShopIntro) - } else { - rootNavController.navigate(Routes.ShopDiscover) - } + Suggestion.SHOP -> { + if (!hasSeenShopIntro) { + rootNavController.navigate(Routes.ShopIntro) + } else { + rootNavController.navigate(Routes.ShopDiscover) } + } - Suggestion.QUICK_PAY -> { - if (!quickPayIntroSeen) { - rootNavController.navigate(Routes.QuickPayIntro) - } else { - rootNavController.navigate(Routes.QuickPaySettings) - } + Suggestion.QUICK_PAY -> { + if (!quickPayIntroSeen) { + rootNavController.navigate(Routes.QuickPayIntro) + } else { + rootNavController.navigate(Routes.QuickPaySettings) } } - }, - onClickAddWidget = { - if (!hasSeenWidgetsIntro) { - rootNavController.navigate(Routes.WidgetsIntro) - } else { - rootNavController.navigate(Routes.AddWidget) - } } - ) - } - composable( - enterTransition = { screenSlideIn }, - exitTransition = { screenSlideOut }, - ) { - val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsStateWithLifecycle() - SavingsWalletScreen( - onAllActivityButtonClick = { walletNavController.navigate(HomeRoutes.AllActivity) }, - onActivityItemClick = { rootNavController.navigateToActivityItem(it) }, - onEmptyActivityRowClick = { appViewModel.showSheet(BottomSheetType.Receive) }, - onTransferToSpendingClick = { - if (!hasSeenSpendingIntro) { - rootNavController.navigateToTransferSpendingIntro() - } else { - rootNavController.navigateToTransferSpendingAmount() - } - }, - onBackClick = { walletNavController.popBackStack() }, - ) - } - composable( - enterTransition = { screenSlideIn }, - exitTransition = { screenSlideOut }, - ) { - val hasSeenSavingsIntro by settingsViewModel.hasSeenSavingsIntro.collectAsStateWithLifecycle() - SpendingWalletScreen( - uiState = uiState, - onAllActivityButtonClick = { walletNavController.navigate(HomeRoutes.AllActivity) }, - onActivityItemClick = { rootNavController.navigateToActivityItem(it) }, - onEmptyActivityRowClick = { appViewModel.showSheet(BottomSheetType.Receive) }, - onTransferToSavingsClick = { - if (!hasSeenSavingsIntro) { - rootNavController.navigateToTransferSavingsIntro() - } else { - rootNavController.navigateToTransferSavingsAvailability() - } - }, - onBackClick = { walletNavController.popBackStack() }, - ) - } - composable( - enterTransition = { screenSlideIn }, - exitTransition = { screenSlideOut }, - ) { - AllActivityScreen( - viewModel = activityListViewModel, - onBack = { - activityListViewModel.clearFilters() - walletNavController.popBackStack() - }, - onActivityItemClick = { rootNavController.navigateToActivityItem(it) }, - ) - } + }, + onClickAddWidget = { + if (!hasSeenWidgetsIntro) { + rootNavController.navigate(Routes.WidgetsIntro) + } else { + rootNavController.navigate(Routes.AddWidget) + } + } + ) + } + composable( + enterTransition = { screenSlideIn }, + exitTransition = { screenSlideOut }, + ) { + val hasSeenSpendingIntro by settingsViewModel.hasSeenSpendingIntro.collectAsStateWithLifecycle() + SavingsWalletScreen( + onAllActivityButtonClick = { walletNavController.navigate(HomeRoutes.AllActivity) }, + onActivityItemClick = { rootNavController.navigateToActivityItem(it) }, + onEmptyActivityRowClick = { appViewModel.showSheet(BottomSheetType.Receive) }, + onTransferToSpendingClick = { + if (!hasSeenSpendingIntro) { + rootNavController.navigateToTransferSpendingIntro() + } else { + rootNavController.navigateToTransferSpendingAmount() + } + }, + onBackClick = { walletNavController.popBackStack() }, + ) + } + composable( + enterTransition = { screenSlideIn }, + exitTransition = { screenSlideOut }, + ) { + val hasSeenSavingsIntro by settingsViewModel.hasSeenSavingsIntro.collectAsStateWithLifecycle() + SpendingWalletScreen( + uiState = uiState, + onAllActivityButtonClick = { walletNavController.navigate(HomeRoutes.AllActivity) }, + onActivityItemClick = { rootNavController.navigateToActivityItem(it) }, + onEmptyActivityRowClick = { appViewModel.showSheet(BottomSheetType.Receive) }, + onTransferToSavingsClick = { + if (!hasSeenSavingsIntro) { + rootNavController.navigateToTransferSavingsIntro() + } else { + rootNavController.navigateToTransferSavingsAvailability() + } + }, + onBackClick = { walletNavController.popBackStack() }, + ) + } + composable( + enterTransition = { screenSlideIn }, + exitTransition = { screenSlideOut }, + ) { + AllActivityScreen( + viewModel = activityListViewModel, + onBack = { + activityListViewModel.clearFilters() + walletNavController.popBackStack() + }, + onActivityItemClick = { rootNavController.navigateToActivityItem(it) }, + ) } - - TabBar( - onSendClick = { appViewModel.showSheet(BottomSheetType.Send()) }, - onReceiveClick = { appViewModel.showSheet(BottomSheetType.Receive) }, - onScanClick = { rootNavController.navigateToQrScanner() }, - modifier = Modifier - .align(Alignment.BottomCenter) - .systemBarsPadding() - ) } + + TabBar( + onSendClick = { appViewModel.showSheet(BottomSheetType.Send()) }, + onReceiveClick = { appViewModel.showSheet(BottomSheetType.Receive) }, + onScanClick = { rootNavController.navigateToQrScanner() }, + modifier = Modifier + .align(Alignment.BottomCenter) + .systemBarsPadding() + ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt index 723d5ef94..fb0f17e6f 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BackupSettingsScreen.kt @@ -11,8 +11,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController import to.bitkit.R +import to.bitkit.ui.appViewModel +import to.bitkit.ui.components.BottomSheetType import to.bitkit.ui.components.settings.SettingsButtonRow -import to.bitkit.ui.navigateToBackupWalletSettings import to.bitkit.ui.navigateToHome import to.bitkit.ui.navigateToRestoreWalletSettings import to.bitkit.ui.scaffold.AppTopBar @@ -24,8 +25,10 @@ import to.bitkit.ui.theme.AppThemeSurface fun BackupSettingsScreen( navController: NavController, ) { + val app = appViewModel ?: return + BackupSettingsScreenContent( - onBackupClick = { navController.navigateToBackupWalletSettings() }, + onBackupClick = { app.showSheet(BottomSheetType.BackupNavigation) }, onResetAndRestoreClick = { navController.navigateToRestoreWalletSettings() }, onBack = { navController.popBackStack() }, onClose = { navController.navigateToHome() }, diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt index 8ab7a5d9f..6e439c35b 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt @@ -6,11 +6,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import kotlinx.serialization.Serializable import to.bitkit.ui.components.SheetSize +import to.bitkit.ui.utils.composableWithDefaultTransitions @Composable fun BackupNavigationSheet( @@ -27,7 +27,7 @@ fun BackupNavigationSheet( navController = navController, startDestination = BackupRoute.ShowMnemonic, ) { - composable { + composableWithDefaultTransitions { ShowMnemonicScreen( onContinue = { seed, bip39Passphrase -> if (bip39Passphrase.isNotEmpty()) { @@ -39,7 +39,7 @@ fun BackupNavigationSheet( onDismiss = onDismiss, ) } - composable { backStackEntry -> + composableWithDefaultTransitions { backStackEntry -> val route = backStackEntry.toRoute() ShowPassphraseScreen( seed = route.seed, @@ -50,7 +50,7 @@ fun BackupNavigationSheet( onBack = { navController.popBackStack() }, ) } - composable { backStackEntry -> + composableWithDefaultTransitions { backStackEntry -> val route = backStackEntry.toRoute() ConfirmMnemonicScreen( seed = route.seed, @@ -65,7 +65,7 @@ fun BackupNavigationSheet( onBack = { navController.popBackStack() }, ) } - composable { backStackEntry -> + composableWithDefaultTransitions { backStackEntry -> val route = backStackEntry.toRoute() ConfirmPassphraseScreen( bip39Passphrase = route.bip39Passphrase, @@ -75,7 +75,7 @@ fun BackupNavigationSheet( onBack = { navController.popBackStack() }, ) } - composable { + composableWithDefaultTransitions { WarningScreen( onContinue = { navController.navigate(BackupRoute.Success) @@ -83,7 +83,7 @@ fun BackupNavigationSheet( onBack = { navController.popBackStack() }, ) } - composable { + composableWithDefaultTransitions { SuccessScreen( onContinue = { navController.navigate(BackupRoute.MultipleDevices) @@ -91,7 +91,7 @@ fun BackupNavigationSheet( onBack = { navController.popBackStack() }, ) } - composable { + composableWithDefaultTransitions { MultipleDevicesScreen( onContinue = { navController.navigate(BackupRoute.Metadata) @@ -99,7 +99,7 @@ fun BackupNavigationSheet( onBack = { navController.popBackStack() }, ) } - composable { + composableWithDefaultTransitions { MetadataScreen( onDismiss = onDismiss, onBack = { navController.popBackStack() }, diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupWalletScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupWalletScreen.kt deleted file mode 100644 index c908063d8..000000000 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupWalletScreen.kt +++ /dev/null @@ -1,13 +0,0 @@ -package to.bitkit.ui.settings.backups - -import androidx.compose.runtime.Composable -import androidx.navigation.NavController - -@Composable -fun BackupWalletScreen( - navController: NavController, -) { - BackupNavigationSheet( - onDismiss = { navController.popBackStack() }, - ) -} diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index a6ff7754d..e6d112fb2 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -104,8 +104,6 @@ fun ShowMnemonicScreen( title = context.getString(R.string.security__mnemonic_error), description = context.getString(R.string.security__mnemonic_error_description), ) - } finally { - isLoading = false } } }, diff --git a/app/src/main/java/to/bitkit/ui/utils/AutoReadClipboardHandler.kt b/app/src/main/java/to/bitkit/ui/utils/AutoReadClipboardHandler.kt index 95af4681f..ba45f6477 100644 --- a/app/src/main/java/to/bitkit/ui/utils/AutoReadClipboardHandler.kt +++ b/app/src/main/java/to/bitkit/ui/utils/AutoReadClipboardHandler.kt @@ -1,8 +1,6 @@ package to.bitkit.ui.utils import android.content.Context -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -12,10 +10,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner @@ -27,7 +23,6 @@ import to.bitkit.ext.getClipboardText import to.bitkit.ui.appViewModel import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.settingsViewModel -import to.bitkit.ui.theme.AppThemeSurface import uniffi.bitkitcore.decode @Composable From 8cd592cf281f9a9f3318776f9c3a4477b83712b6 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Jun 2025 11:59:16 +0200 Subject: [PATCH 07/25] fix: Sheet drag handle bottom padding --- app/src/main/java/to/bitkit/ui/components/SheetHost.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt index 855ccdff2..2e913a33d 100644 --- a/app/src/main/java/to/bitkit/ui/components/SheetHost.kt +++ b/app/src/main/java/to/bitkit/ui/components/SheetHost.kt @@ -106,7 +106,7 @@ private fun SheetDragHandle( color = Colors.White32, shape = MaterialTheme.shapes.extraLarge, modifier = modifier - .padding(top = 12.dp, bottom = 2.dp) + .padding(top = 12.dp, bottom = 4.dp) .semantics { contentDescription = "Drag handle" } ) { Box(Modifier.size(width = 32.dp, height = 4.dp)) From afa11b625489078a324ad4e0e8bca4618538fee7 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Jun 2025 11:59:37 +0200 Subject: [PATCH 08/25] fix: Remove back button on show mnemonic screen --- .../ui/settings/backups/BackupNavigationSheet.kt | 1 - .../bitkit/ui/settings/backups/ShowMnemonicScreen.kt | 12 ++---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt index 6e439c35b..2d6b78448 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt @@ -36,7 +36,6 @@ fun BackupNavigationSheet( navController.navigate(BackupRoute.ConfirmMnemonic(seed, bip39Passphrase)) } }, - onDismiss = onDismiss, ) } composableWithDefaultTransitions { backStackEntry -> diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index e6d112fb2..6e502fdd5 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -63,7 +63,6 @@ import to.bitkit.utils.bip39Words @Composable fun ShowMnemonicScreen( onContinue: (seed: List, bip39Passphrase: String) -> Unit, - onDismiss: () -> Unit, ) { val app = appViewModel ?: return val context = LocalContext.current @@ -77,7 +76,7 @@ fun ShowMnemonicScreen( DisposableEffect(Unit) { onDispose { - mnemonic = "" // Clear mnemonic from memory when leaving screen + mnemonic = "" bip39Passphrase = "" } } @@ -86,7 +85,6 @@ fun ShowMnemonicScreen( mnemonic = mnemonic, showMnemonic = showMnemonic, isLoading = isLoading, - onDismiss = onDismiss, onRevealClick = { scope.launch { try { @@ -121,7 +119,6 @@ private fun ShowMnemonicContent( mnemonic: String, showMnemonic: Boolean, isLoading: Boolean, - onDismiss: () -> Unit, onRevealClick: () -> Unit, onCopyClick: () -> Unit, onContinueClick: () -> Unit, @@ -158,10 +155,7 @@ private fun ShowMnemonicContent( .gradientBackground() .padding(horizontal = 32.dp) ) { - SheetTopBar( - titleText = stringResource(R.string.security__mnemonic_your), - onBack = onDismiss, - ) + SheetTopBar(titleText = stringResource(R.string.security__mnemonic_your)) Column( modifier = Modifier @@ -319,7 +313,6 @@ private fun Preview() { mnemonic = "", showMnemonic = false, isLoading = false, - onDismiss = {}, onRevealClick = {}, onCopyClick = {}, onContinueClick = {}, @@ -335,7 +328,6 @@ private fun PreviewShown() { mnemonic = List(24) { bip39Words.random() }.joinToString(" "), showMnemonic = true, isLoading = false, - onDismiss = {}, onRevealClick = {}, onCopyClick = {}, onContinueClick = {}, From fd39a54c5706d7511403472d19dd604cac45bbb3 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Jun 2025 13:43:51 +0200 Subject: [PATCH 09/25] fix: Crash on last word click to remove after added --- .../settings/backups/BackupNavigationSheet.kt | 2 +- .../settings/backups/ConfirmMnemonicScreen.kt | 40 ++++++++++--------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt index 2d6b78448..234e52ca6 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt @@ -21,7 +21,7 @@ fun BackupNavigationSheet( Column( modifier = Modifier .fillMaxWidth() - .fillMaxHeight(SheetSize.MEDIUM) + .fillMaxHeight(SheetSize.LARGE) ) { NavHost( navController = navController, diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt index 183ef7a10..0a5ab2852 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt @@ -63,28 +63,32 @@ fun ConfirmMnemonicScreen( pressedStates = pressedStates, onWordPress = { word, shuffledIndex -> // Find index of the last filled word - val lastIndex = selectedWords.indexOfFirst { it == null } - 1 - val nextIndex = if (lastIndex == -1) 0 else lastIndex + 1 - - // If the word is correct and pressed, do nothing - if (pressedStates[shuffledIndex] && nextIndex > 0 && seed[lastIndex] == selectedWords[lastIndex]) { - return@ConfirmMnemonicContent - } - - // If previous word is incorrect, allow unchecking - if (lastIndex >= 0 && selectedWords[lastIndex] != seed[lastIndex]) { - // Uncheck if we tap on it - if (pressedStates[shuffledIndex] && word == selectedWords[lastIndex]) { - pressedStates = pressedStates.copyOf().apply { this[shuffledIndex] = false } - selectedWords = selectedWords.copyOf().apply { this[lastIndex] = null } + val firstNullIndex = selectedWords.indexOfFirst { it == null } + val lastFilledIndex = if (firstNullIndex == -1) selectedWords.size - 1 else firstNullIndex - 1 + val nextEmptyIndex = if (firstNullIndex == -1) -1 else firstNullIndex + + // If this word is already pressed/selected + if (pressedStates[shuffledIndex]) { + // Allow deselecting only if it's the last word that was selected + // or if the word at the last position is incorrect + if (lastFilledIndex >= 0) { + val wordAtLastPosition = selectedWords[lastFilledIndex] + val isLastWordIncorrect = wordAtLastPosition != seed[lastFilledIndex] + val isThisTheLastWord = wordAtLastPosition == word + + if (isThisTheLastWord && (isLastWordIncorrect || firstNullIndex == -1)) { + // Deselect this word + pressedStates = pressedStates.copyOf().apply { this[shuffledIndex] = false } + selectedWords = selectedWords.copyOf().apply { this[lastFilledIndex] = null } + } } return@ConfirmMnemonicContent } - // Mark word as pressed and add it to the seed - if (nextIndex < seed.size) { + // If we have space and word is not already pressed, add it + if (nextEmptyIndex >= 0 && nextEmptyIndex < seed.size) { pressedStates = pressedStates.copyOf().apply { this[shuffledIndex] = true } - selectedWords = selectedWords.copyOf().apply { this[nextIndex] = word } + selectedWords = selectedWords.copyOf().apply { this[nextEmptyIndex] = word } } }, onContinue = onContinue, @@ -111,7 +115,6 @@ private fun ConfirmMnemonicContent( modifier = Modifier .fillMaxSize() .gradientBackground() - .padding(horizontal = 32.dp) ) { SheetTopBar( titleText = stringResource(R.string.security__mnemonic_confirm), @@ -121,6 +124,7 @@ private fun ConfirmMnemonicContent( Column( modifier = Modifier .fillMaxSize() + .padding(horizontal = 32.dp) .verticalScroll(rememberScrollState()) ) { Spacer(modifier = Modifier.height(16.dp)) From c63405da656196b67fb0be4d8bbc348e7d50d0bd Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Jun 2025 14:29:42 +0200 Subject: [PATCH 10/25] fix: ReportIssueScreen preview --- .../to/bitkit/ui/settings/support/ReportIssueScreen.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/support/ReportIssueScreen.kt b/app/src/main/java/to/bitkit/ui/settings/support/ReportIssueScreen.kt index f8da0ea62..6387ce9eb 100644 --- a/app/src/main/java/to/bitkit/ui/settings/support/ReportIssueScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/support/ReportIssueScreen.kt @@ -160,10 +160,13 @@ fun ReportIssueContent( @Composable private fun Preview() { AppThemeSurface { - ReportIssueScreen( + ReportIssueContent( onBack = {}, onClose = {}, - navigateResultScreen = {} + onConfirm = {}, + onUpdateEmail = {}, + onUpdateMessage = {}, + uiState = ReportIssueUiState() ) } } From 1b2f6cc8124a3fdffb387b8de13471d931020869 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Jun 2025 14:31:46 +0200 Subject: [PATCH 11/25] fix: ConfirmPassphraseScreen input --- .../backups/ConfirmPassphraseScreen.kt | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt index b2b2625f1..919619c88 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.components.TextInput import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface @@ -99,19 +100,15 @@ private fun ConfirmPassphraseContent( Spacer(modifier = Modifier.height(32.dp)) - OutlinedTextField( + TextInput( + placeholder = stringResource(R.string.security__pass).replaceFirstChar { it.uppercase() }, value = enteredPassphrase, onValueChange = onPassphraseChange, - placeholder = { - BodyM( - text = stringResource(R.string.security__pass).replaceFirstChar { it.uppercase() }, - color = Colors.White32 - ) - }, + singleLine = true, keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.None, imeAction = ImeAction.Done, - autoCorrect = false + autoCorrectEnabled = false ), keyboardActions = KeyboardActions( onDone = { @@ -121,15 +118,6 @@ private fun ConfirmPassphraseContent( } } ), - colors = OutlinedTextFieldDefaults.colors( - focusedTextColor = Colors.White, - unfocusedTextColor = Colors.White, - focusedBorderColor = Colors.White32, - unfocusedBorderColor = Colors.White16, - focusedContainerColor = Colors.White10, - unfocusedContainerColor = Colors.White10, - cursorColor = Colors.Brand, - ), modifier = Modifier.fillMaxWidth() ) From 0799990d5aafdfba9227787f2295be60f547c04f Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Jun 2025 14:34:25 +0200 Subject: [PATCH 12/25] refactor: Remove redundant bip39Passphrase --- .../java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt | 1 - .../java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt index 234e52ca6..cb8902d21 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt @@ -53,7 +53,6 @@ fun BackupNavigationSheet( val route = backStackEntry.toRoute() ConfirmMnemonicScreen( seed = route.seed, - bip39Passphrase = route.bip39Passphrase, onContinue = { if (route.bip39Passphrase.isNotEmpty()) { navController.navigate(BackupRoute.ConfirmPassphrase(route.bip39Passphrase)) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt index 0a5ab2852..161953aa6 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt @@ -38,7 +38,6 @@ import to.bitkit.utils.bip39Words @Composable fun ConfirmMnemonicScreen( seed: List, - bip39Passphrase: String, onContinue: () -> Unit, onBack: () -> Unit, ) { From 202aff02070c4a7ee9700b433c24ace1fa27d722 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Jun 2025 14:41:14 +0200 Subject: [PATCH 13/25] fix: SheetTopBar padding for backup flow screens --- .../to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt | 2 +- .../java/to/bitkit/ui/settings/backups/MetadataScreen.kt | 2 +- .../to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt | 2 +- .../java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt | 2 +- .../to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt | 5 +++-- .../main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt | 2 +- .../main/java/to/bitkit/ui/settings/backups/WarningScreen.kt | 2 +- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt index 919619c88..4eb370de6 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt @@ -77,7 +77,6 @@ private fun ConfirmPassphraseContent( modifier = Modifier .fillMaxSize() .gradientBackground() - .padding(horizontal = 32.dp) .imePadding() ) { SheetTopBar( @@ -88,6 +87,7 @@ private fun ConfirmPassphraseContent( Column( modifier = Modifier .fillMaxSize() + .padding(horizontal = 32.dp) .verticalScroll(rememberScrollState()) ) { Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt index f6333d5f6..8df2d4c55 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt @@ -59,7 +59,6 @@ private fun MetadataContent( modifier = Modifier .fillMaxSize() .gradientBackground() - .padding(horizontal = 32.dp) ) { SheetTopBar( titleText = stringResource(R.string.security__mnemonic_data_header), @@ -69,6 +68,7 @@ private fun MetadataContent( Column( modifier = Modifier .fillMaxSize() + .padding(horizontal = 32.dp) .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.SpaceBetween ) { diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt index 22bda1303..f41cddd58 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt @@ -49,7 +49,6 @@ private fun MultipleDevicesContent( modifier = Modifier .fillMaxSize() .gradientBackground() - .padding(horizontal = 32.dp) ) { SheetTopBar( titleText = stringResource(R.string.security__mnemonic_multiple_header), @@ -59,6 +58,7 @@ private fun MultipleDevicesContent( Column( modifier = Modifier .fillMaxSize() + .padding(horizontal = 32.dp) .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.SpaceBetween ) { diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index 6e502fdd5..e884b2ec4 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -153,13 +153,13 @@ private fun ShowMnemonicContent( modifier = Modifier .fillMaxSize() .gradientBackground() - .padding(horizontal = 32.dp) ) { SheetTopBar(titleText = stringResource(R.string.security__mnemonic_your)) Column( modifier = Modifier .fillMaxSize() + .padding(horizontal = 32.dp) .verticalScroll(scrollState) ) { Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt index 8b7021630..b71723a29 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt @@ -45,7 +45,6 @@ private fun ShowPassphraseContent( modifier = Modifier .fillMaxSize() .gradientBackground() - .padding(horizontal = 32.dp) ) { SheetTopBar( titleText = "BIP39 Passphrase", @@ -53,7 +52,9 @@ private fun ShowPassphraseContent( ) Column( - modifier = Modifier.fillMaxSize() + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 32.dp) ) { Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt index 1f53322f8..f66b89472 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt @@ -50,7 +50,6 @@ private fun SuccessContent( modifier = Modifier .fillMaxSize() .gradientBackground() - .padding(horizontal = 32.dp) ) { SheetTopBar( titleText = stringResource(R.string.security__mnemonic_result_header), @@ -60,6 +59,7 @@ private fun SuccessContent( Column( modifier = Modifier .fillMaxSize() + .padding(horizontal = 32.dp) .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.SpaceBetween ) { diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt index a30fc8406..eb51f345f 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt @@ -50,7 +50,6 @@ private fun WarningContent( modifier = Modifier .fillMaxSize() .gradientBackground() - .padding(horizontal = 32.dp) ) { SheetTopBar( titleText = stringResource(R.string.security__mnemonic_keep_header), @@ -60,6 +59,7 @@ private fun WarningContent( Column( modifier = Modifier .fillMaxSize() + .padding(horizontal = 32.dp) .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.SpaceBetween ) { From 1ffd8b16e8efbdae7500bf0a7ed1c89968b4bbb6 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Jun 2025 14:44:24 +0200 Subject: [PATCH 14/25] feat: Scroll to bottom on mnemonic confirm --- .../settings/backups/ConfirmMnemonicScreen.kt | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt index 161953aa6..fd48abe34 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt @@ -15,14 +15,18 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB @@ -110,6 +114,19 @@ private fun ConfirmMnemonicContent( val isComplete = selectedWords.all { it != null } && selectedWords.zip(originalSeed).all { (selected, original) -> selected == original } + val scrollState = rememberScrollState() + val scope = rememberCoroutineScope() + + // Autoscroll to bottom when all words are correctly selected + LaunchedEffect(isComplete) { + if (isComplete) { + delay(300) // Wait for any UI updates to complete + scope.launch { + scrollState.animateScrollTo(scrollState.maxValue) + } + } + } + Column( modifier = Modifier .fillMaxSize() @@ -124,7 +141,7 @@ private fun ConfirmMnemonicContent( modifier = Modifier .fillMaxSize() .padding(horizontal = 32.dp) - .verticalScroll(rememberScrollState()) + .verticalScroll(scrollState) ) { Spacer(modifier = Modifier.height(16.dp)) From f3a6d0aac6b156bb7ea9ec513984ac5b80cd21a3 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Jun 2025 15:51:33 +0200 Subject: [PATCH 15/25] refactor: Extract string.withAccentBoldBright --- .../java/to/bitkit/ui/components/InfoScreenContent.kt | 6 ++---- .../ui/screens/transfer/SavingsAvailabilityScreen.kt | 6 ++---- .../bitkit/ui/screens/transfer/SavingsProgressScreen.kt | 9 +++------ .../to/bitkit/ui/screens/transfer/SettingUpScreen.kt | 6 ++---- .../screens/transfer/external/ExternalSuccessScreen.kt | 6 ++---- app/src/main/java/to/bitkit/ui/utils/Text.kt | 5 ++++- 6 files changed, 15 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/InfoScreenContent.kt b/app/src/main/java/to/bitkit/ui/components/InfoScreenContent.kt index dfe9f2a0d..92aff6aa8 100644 --- a/app/src/main/java/to/bitkit/ui/components/InfoScreenContent.kt +++ b/app/src/main/java/to/bitkit/ui/components/InfoScreenContent.kt @@ -18,8 +18,6 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R @@ -28,6 +26,7 @@ import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent +import to.bitkit.ui.utils.withAccentBoldBright @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -94,8 +93,7 @@ private fun Preview() { InfoScreenContent( navTitle = stringResource(R.string.lightning__transfer__nav_title), title = stringResource(R.string.lightning__savings_interrupted__title).withAccent(), - description = stringResource(R.string.lightning__savings_interrupted__text) - .withAccent(accentStyle = SpanStyle(color = Colors.White, fontWeight = FontWeight.Bold)), + description = stringResource(R.string.lightning__savings_interrupted__text).withAccentBoldBright(), image = painterResource(R.drawable.check), buttonText = stringResource(R.string.common__ok), onButtonClick = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsAvailabilityScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsAvailabilityScreen.kt index 3bfdc0d9e..627898499 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsAvailabilityScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsAvailabilityScreen.kt @@ -16,8 +16,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R @@ -30,6 +28,7 @@ import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent +import to.bitkit.ui.utils.withAccentBoldBright @Composable fun SavingsAvailabilityScreen( @@ -51,8 +50,7 @@ fun SavingsAvailabilityScreen( Display(text = stringResource(R.string.lightning__availability__title).withAccent()) Spacer(modifier = Modifier.height(8.dp)) BodyM( - text = stringResource(R.string.lightning__availability__text) - .withAccent(accentStyle = SpanStyle(color = Colors.White, fontWeight = FontWeight.Bold)), + text = stringResource(R.string.lightning__availability__text).withAccentBoldBright(), color = Colors.White64, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index 025120df9..9c72be2df 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -24,8 +24,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay @@ -42,6 +40,7 @@ import to.bitkit.ui.theme.Colors import to.bitkit.ui.transferViewModel import to.bitkit.ui.utils.removeAccentTags import to.bitkit.ui.utils.withAccent +import to.bitkit.ui.utils.withAccentBoldBright import to.bitkit.ui.walletViewModel enum class SavingsProgressState { PROGRESS, SUCCESS, INTERRUPTED } @@ -128,8 +127,7 @@ private fun SavingsProgressScreen( ) Spacer(modifier = Modifier.height(8.dp)) BodyM( - text = stringResource(R.string.lightning__savings_progress__text) - .withAccent(accentStyle = SpanStyle(color = Colors.White, fontWeight = FontWeight.Bold)), + text = stringResource(R.string.lightning__savings_progress__text).withAccentBoldBright(), color = Colors.White64, ) } @@ -147,8 +145,7 @@ private fun SavingsProgressScreen( Display(text = stringResource(R.string.lightning__savings_interrupted__title).withAccent()) Spacer(modifier = Modifier.height(8.dp)) BodyM( - text = stringResource(R.string.lightning__savings_interrupted__text) - .withAccent(accentStyle = SpanStyle(color = Colors.White, fontWeight = FontWeight.Bold)), + text = stringResource(R.string.lightning__savings_interrupted__text).withAccentBoldBright(), color = Colors.White64, ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SettingUpScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SettingUpScreen.kt index 13fef15b4..ef0f36647 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SettingUpScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SettingUpScreen.kt @@ -20,8 +20,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay @@ -41,6 +39,7 @@ import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.localizedRandom import to.bitkit.ui.utils.withAccent +import to.bitkit.ui.utils.withAccentBoldBright import to.bitkit.utils.Logger import to.bitkit.viewmodels.TransferViewModel import uniffi.bitkitcore.regtestMine @@ -122,8 +121,7 @@ private fun SettingUpScreen( ) Spacer(modifier = Modifier.height(8.dp)) BodyM( - text = stringResource(R.string.lightning__setting_up_text) - .withAccent(accentStyle = SpanStyle(color = Colors.White, fontWeight = FontWeight.Bold)), + text = stringResource(R.string.lightning__setting_up_text).withAccentBoldBright(), color = Colors.White64, ) } else { diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalSuccessScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalSuccessScreen.kt index 8dbe36b67..110a5de54 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalSuccessScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalSuccessScreen.kt @@ -3,8 +3,6 @@ package to.bitkit.ui.screens.transfer.external import androidx.compose.runtime.Composable import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import to.bitkit.R import to.bitkit.ui.components.InfoScreenContent @@ -12,6 +10,7 @@ import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.localizedRandom import to.bitkit.ui.utils.withAccent +import to.bitkit.ui.utils.withAccentBoldBright @Composable fun ExternalSuccessScreen( @@ -21,8 +20,7 @@ fun ExternalSuccessScreen( InfoScreenContent( navTitle = stringResource(R.string.lightning__external__nav_title), title = stringResource(R.string.lightning__external_success__title).withAccent(accentColor = Colors.Purple), - description = stringResource(R.string.lightning__external_success__text) - .withAccent(accentStyle = SpanStyle(color = Colors.White, fontWeight = FontWeight.Bold)), + description = stringResource(R.string.lightning__external_success__text).withAccentBoldBright(), image = painterResource(R.drawable.switch_box), buttonText = localizedRandom(R.string.common__ok_random), onButtonClick = onContinue, diff --git a/app/src/main/java/to/bitkit/ui/utils/Text.kt b/app/src/main/java/to/bitkit/ui/utils/Text.kt index d088383fd..a07ee95de 100644 --- a/app/src/main/java/to/bitkit/ui/utils/Text.kt +++ b/app/src/main/java/to/bitkit/ui/utils/Text.kt @@ -61,6 +61,9 @@ fun String.withAccent( } } +fun String.withAccentBoldBright() = + this.withAccent(accentStyle = SpanStyle(color = Colors.White, fontWeight = FontWeight.Bold)) + fun String.withAccentLink(url: String): AnnotatedString { val htmlText = this .replace("", "") @@ -115,7 +118,7 @@ fun String.withBold( } } -fun String.isValidEmail() = this.isNotBlank() && android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches() +fun String.isValidEmail() = this.isNotBlank() && android.util.Patterns.EMAIL_ADDRESS.matcher(this).matches() @Composable fun localizedRandom(@StringRes id: Int): String { From e2566377bafca2e59113b349ce017ec2ec00d0f4 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Jun 2025 15:51:48 +0200 Subject: [PATCH 16/25] fix: Mnemonic words size in text --- .../java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt index fd48abe34..49b5e6f26 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt @@ -146,7 +146,7 @@ private fun ConfirmMnemonicContent( Spacer(modifier = Modifier.height(16.dp)) BodyM( - text = stringResource(R.string.security__mnemonic_confirm_tap), + text = stringResource(R.string.security__mnemonic_confirm_tap).replace("12", "${originalSeed.size}"), color = Colors.White64, modifier = Modifier.fillMaxWidth(), ) From 8446b2fd742ce2962b649a2574ddf4e64b59a52d Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Jun 2025 16:14:58 +0200 Subject: [PATCH 17/25] feat: Backup flow screens ui polish & cleanup --- .../settings/backups/BackupNavigationSheet.kt | 1 - .../bitkit/ui/settings/backups/BackupSheet.kt | 2 +- .../settings/backups/ConfirmMnemonicScreen.kt | 16 ++- .../backups/ConfirmPassphraseScreen.kt | 16 +-- .../ui/settings/backups/MetadataScreen.kt | 94 +++++++----------- .../settings/backups/MultipleDevicesScreen.kt | 58 ++++------- .../ui/settings/backups/ShowMnemonicScreen.kt | 48 ++++----- .../settings/backups/ShowPassphraseScreen.kt | 62 ++++++++---- .../ui/settings/backups/SuccessScreen.kt | 73 +++++--------- .../ui/settings/backups/WarningScreen.kt | 62 ++++-------- app/src/main/res/drawable-nodpi/check.png | Bin 7164 -> 29080 bytes 11 files changed, 172 insertions(+), 260 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt index cb8902d21..c19f0da02 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt @@ -41,7 +41,6 @@ fun BackupNavigationSheet( composableWithDefaultTransitions { backStackEntry -> val route = backStackEntry.toRoute() ShowPassphraseScreen( - seed = route.seed, bip39Passphrase = route.bip39Passphrase, onContinue = { navController.navigate(BackupRoute.ConfirmMnemonic(route.seed, route.bip39Passphrase)) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupSheet.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupSheet.kt index 3d9f1f16c..ffa41cff7 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupSheet.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupSheet.kt @@ -17,7 +17,7 @@ fun BackupSheet( onBackupClick: () -> Unit, walletViewModel: WalletViewModel, ) { - val balance : BalanceState by walletViewModel.balanceState.collectAsStateWithLifecycle() + val balance: BalanceState by walletViewModel.balanceState.collectAsStateWithLifecycle() Column( modifier = Modifier diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt index 49b5e6f26..2133d58aa 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState @@ -131,11 +132,10 @@ private fun ConfirmMnemonicContent( modifier = Modifier .fillMaxSize() .gradientBackground() + .navigationBarsPadding() ) { - SheetTopBar( - titleText = stringResource(R.string.security__mnemonic_confirm), - onBack = onBack, - ) + SheetTopBar(stringResource(R.string.security__mnemonic_confirm), onBack = onBack) + Spacer(modifier = Modifier.height(16.dp)) Column( modifier = Modifier @@ -143,12 +143,9 @@ private fun ConfirmMnemonicContent( .padding(horizontal = 32.dp) .verticalScroll(scrollState) ) { - Spacer(modifier = Modifier.height(16.dp)) - BodyM( text = stringResource(R.string.security__mnemonic_confirm_tap).replace("12", "${originalSeed.size}"), color = Colors.White64, - modifier = Modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.height(16.dp)) @@ -204,16 +201,15 @@ private fun ConfirmMnemonicContent( } } - Spacer(modifier = Modifier.height(22.dp)) + Spacer(modifier = Modifier.height(24.dp)) PrimaryButton( text = stringResource(R.string.common__continue), onClick = onContinue, enabled = isComplete, - modifier = Modifier.fillMaxWidth() ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(16.dp)) } } } diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt index 4eb370de6..a8221be6e 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt @@ -6,13 +6,12 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue @@ -77,12 +76,11 @@ private fun ConfirmPassphraseContent( modifier = Modifier .fillMaxSize() .gradientBackground() + .navigationBarsPadding() .imePadding() ) { - SheetTopBar( - titleText = stringResource(R.string.security__pass_confirm), - onBack = onBack, - ) + SheetTopBar(stringResource(R.string.security__pass_confirm), onBack = onBack) + Spacer(modifier = Modifier.height(16.dp)) Column( modifier = Modifier @@ -90,12 +88,9 @@ private fun ConfirmPassphraseContent( .padding(horizontal = 32.dp) .verticalScroll(rememberScrollState()) ) { - Spacer(modifier = Modifier.height(16.dp)) - BodyM( text = stringResource(R.string.security__pass_confirm_text), color = Colors.White64, - modifier = Modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.height(32.dp)) @@ -127,10 +122,9 @@ private fun ConfirmPassphraseContent( text = stringResource(R.string.common__continue), onClick = onContinue, enabled = isValid, - modifier = Modifier.fillMaxWidth() ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(16.dp)) } } } diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt index 8df2d4c55..60aee9f68 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt @@ -1,25 +1,20 @@ package to.bitkit.ui.settings.backups import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R @@ -30,6 +25,7 @@ import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.withBold import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @@ -39,7 +35,11 @@ fun MetadataScreen( onDismiss: () -> Unit, onBack: () -> Unit, ) { + // TODO get last backup time from actual state + val lastBackupTimeMs = System.currentTimeMillis() + MetadataContent( + lastBackupTimeMs = lastBackupTimeMs, onDismiss = onDismiss, onBack = onBack, ) @@ -47,78 +47,57 @@ fun MetadataScreen( @Composable private fun MetadataContent( + lastBackupTimeMs: Long, onDismiss: () -> Unit, onBack: () -> Unit, ) { - // Mock the latest backup time (in reality this would come from backup state) - val currentTime = System.currentTimeMillis() - val formatter = SimpleDateFormat("MMMM d, yyyy 'at' h:mm a", Locale.getDefault()) - val formattedTime = formatter.format(Date(currentTime)) + val latestBackupTime = remember { + val formatter = SimpleDateFormat("MMMM d, yyyy 'at' h:mm a", Locale.getDefault()) + formatter.format(Date(lastBackupTimeMs)) + } Column( modifier = Modifier .fillMaxSize() .gradientBackground() + .navigationBarsPadding() ) { - SheetTopBar( - titleText = stringResource(R.string.security__mnemonic_data_header), - onBack = onBack, - ) + SheetTopBar(stringResource(R.string.security__mnemonic_data_header), onBack = onBack) + Spacer(modifier = Modifier.height(16.dp)) Column( modifier = Modifier .fillMaxSize() .padding(horizontal = 32.dp) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.SpaceBetween + .verticalScroll(rememberScrollState()) ) { - Column { - Spacer(modifier = Modifier.height(16.dp)) + BodyM( + text = stringResource(R.string.security__mnemonic_data_text), + color = Colors.White64, + ) - BodyM( - text = stringResource(R.string.security__mnemonic_data_text), - color = Colors.White64, - modifier = Modifier.fillMaxWidth(), - ) - } - - // Illustration in center - Box( - contentAlignment = Alignment.Center, + Image( + painter = painterResource(R.drawable.card), + contentDescription = null, modifier = Modifier .fillMaxWidth() .weight(1f) - ) { - Image( - painter = painterResource(R.drawable.card), - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .width(256.dp) - .aspectRatio(1f) - ) - } + ) + + BodyS( + text = stringResource(R.string.security__mnemonic_latest_backup) + .replace("{time}", latestBackupTime) + .withBold(), + ) - Column { - // Latest backup time info - BodyS( - text = stringResource(R.string.security__mnemonic_latest_backup) - .replace("{time}", formattedTime), - color = Colors.White64, - textAlign = TextAlign.Center, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 16.dp) - ) + Spacer(modifier = Modifier.height(16.dp)) - PrimaryButton( - text = stringResource(R.string.common__ok), - onClick = onDismiss, // Close the sheet - modifier = Modifier.fillMaxWidth() - ) + PrimaryButton( + text = stringResource(R.string.common__ok), + onClick = onDismiss, + ) - Spacer(modifier = Modifier.height(32.dp)) - } + Spacer(modifier = Modifier.height(16.dp)) } } } @@ -128,6 +107,7 @@ private fun MetadataContent( private fun Preview() { AppThemeSurface { MetadataContent( + lastBackupTimeMs = System.currentTimeMillis(), onDismiss = {}, onBack = {}, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt index f41cddd58..6ff75720f 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt @@ -1,22 +1,17 @@ package to.bitkit.ui.settings.backups import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -49,55 +44,36 @@ private fun MultipleDevicesContent( modifier = Modifier .fillMaxSize() .gradientBackground() + .navigationBarsPadding() ) { - SheetTopBar( - titleText = stringResource(R.string.security__mnemonic_multiple_header), - onBack = onBack, - ) + SheetTopBar(stringResource(R.string.security__mnemonic_multiple_header), onBack = onBack) + Spacer(modifier = Modifier.height(16.dp)) Column( modifier = Modifier .fillMaxSize() .padding(horizontal = 32.dp) .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.SpaceBetween ) { - Column { - Spacer(modifier = Modifier.height(16.dp)) - - BodyM( - text = stringResource(R.string.security__mnemonic_multiple_text), - color = Colors.White64, - modifier = Modifier.fillMaxWidth(), - ) - } + BodyM( + text = stringResource(R.string.security__mnemonic_multiple_text), + color = Colors.White64, + ) - // Illustration in center - Box( - contentAlignment = Alignment.Center, + Image( + painter = painterResource(R.drawable.phone), + contentDescription = null, modifier = Modifier .fillMaxWidth() .weight(1f) - ) { - Image( - painter = painterResource(R.drawable.phone), - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .width(256.dp) - .aspectRatio(1f) - ) - } + ) - Column { - PrimaryButton( - text = stringResource(R.string.common__ok), - onClick = onContinue, - modifier = Modifier.fillMaxWidth() - ) + PrimaryButton( + text = stringResource(R.string.common__ok), + onClick = onContinue, + ) - Spacer(modifier = Modifier.height(32.dp)) - } + Spacer(modifier = Modifier.height(16.dp)) } } } diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index e884b2ec4..692bb6fa2 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState @@ -71,9 +72,13 @@ fun ShowMnemonicScreen( var mnemonic by remember { mutableStateOf("") } var bip39Passphrase by remember { mutableStateOf("") } var showMnemonic by remember { mutableStateOf(false) } - var isLoading by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() + LaunchedEffect(Unit) { + mnemonic = app.loadMnemonic()!! + bip39Passphrase = app.loadBip39Passphrase() + } + DisposableEffect(Unit) { onDispose { mnemonic = "" @@ -84,16 +89,12 @@ fun ShowMnemonicScreen( ShowMnemonicContent( mnemonic = mnemonic, showMnemonic = showMnemonic, - isLoading = isLoading, onRevealClick = { scope.launch { try { - isLoading = true delay(200) - val loadedMnemonic = app.loadMnemonic()!! - val loadedPassphrase = app.loadBip39Passphrase() - mnemonic = loadedMnemonic - bip39Passphrase = loadedPassphrase + mnemonic = app.loadMnemonic()!! + bip39Passphrase = app.loadBip39Passphrase() showMnemonic = true } catch (e: Throwable) { Logger.error("Failed to load mnemonic", e) @@ -118,7 +119,6 @@ fun ShowMnemonicScreen( private fun ShowMnemonicContent( mnemonic: String, showMnemonic: Boolean, - isLoading: Boolean, onRevealClick: () -> Unit, onCopyClick: () -> Unit, onContinueClick: () -> Unit, @@ -153,8 +153,10 @@ private fun ShowMnemonicContent( modifier = Modifier .fillMaxSize() .gradientBackground() + .navigationBarsPadding() ) { - SheetTopBar(titleText = stringResource(R.string.security__mnemonic_your)) + SheetTopBar(stringResource(R.string.security__mnemonic_your)) + Spacer(modifier = Modifier.height(16.dp)) Column( modifier = Modifier @@ -162,30 +164,24 @@ private fun ShowMnemonicContent( .padding(horizontal = 32.dp) .verticalScroll(scrollState) ) { - Spacer(modifier = Modifier.height(16.dp)) - AnimatedContent( targetState = showMnemonic, transitionSpec = { fadeIn(tween(300)).togetherWith(fadeOut(tween(300))) }, label = "topText" ) { isRevealed -> BodyM( - text = if (isRevealed) { - stringResource(R.string.security__mnemonic_write).replace("{length}", "${mnemonicWords.size}") - } else { - stringResource(R.string.security__mnemonic_use) - }, + text = when (isRevealed) { + true -> stringResource(R.string.security__mnemonic_write) + else -> stringResource(R.string.security__mnemonic_use) + }.replace("{length}", "${mnemonicWords.size}"), color = Colors.White64, - modifier = Modifier.fillMaxWidth(), ) } Spacer(modifier = Modifier.height(32.dp)) Box( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 32.dp) + modifier = Modifier.fillMaxWidth() ) { Box( modifier = Modifier @@ -193,7 +189,7 @@ private fun ShowMnemonicContent( .clip(MaterialTheme.shapes.medium) .background(color = Colors.White10) .clickable(enabled = showMnemonic && mnemonic.isNotEmpty(), onClick = onCopyClick) - .padding(horizontal = 32.dp, vertical = 32.dp) + .padding(32.dp) ) { MnemonicWordsGrid( actualWords = mnemonicWords, @@ -212,7 +208,6 @@ private fun ShowMnemonicContent( PrimaryButton( text = stringResource(R.string.security__mnemonic_reveal), fullWidth = false, - isLoading = isLoading, onClick = onRevealClick, color = Colors.Black50, modifier = Modifier.alpha(buttonAlpha) @@ -221,21 +216,22 @@ private fun ShowMnemonicContent( } } + Spacer(modifier = Modifier.height(32.dp)) BodyS( text = stringResource(R.string.security__mnemonic_never_share).withAccent(accentColor = Colors.Brand), color = Colors.White64, ) + Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.height(24.dp)) PrimaryButton( text = stringResource(R.string.common__continue), onClick = onContinueClick, enabled = showMnemonic, - modifier = Modifier.fillMaxWidth() ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(16.dp)) } } } @@ -310,9 +306,8 @@ private fun WordItem( private fun Preview() { AppThemeSurface { ShowMnemonicContent( - mnemonic = "", + mnemonic = List(24) { bip39Words.random() }.joinToString(" "), showMnemonic = false, - isLoading = false, onRevealClick = {}, onCopyClick = {}, onContinueClick = {}, @@ -327,7 +322,6 @@ private fun PreviewShown() { ShowMnemonicContent( mnemonic = List(24) { bip39Words.random() }.joinToString(" "), showMnemonic = true, - isLoading = false, onRevealClick = {}, onCopyClick = {}, onContinueClick = {}, diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt index b71723a29..51c96e290 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt @@ -1,33 +1,43 @@ package to.bitkit.ui.settings.backups +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.BodyMSB +import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.withAccent @Composable fun ShowPassphraseScreen( - seed: List, bip39Passphrase: String, onContinue: () -> Unit, onBack: () -> Unit, ) { ShowPassphraseContent( - seed = seed, bip39Passphrase = bip39Passphrase, onContinue = onContinue, onBack = onBack, @@ -36,7 +46,6 @@ fun ShowPassphraseScreen( @Composable private fun ShowPassphraseContent( - seed: List, bip39Passphrase: String, onContinue: () -> Unit, onBack: () -> Unit, @@ -45,41 +54,59 @@ private fun ShowPassphraseContent( modifier = Modifier .fillMaxSize() .gradientBackground() + .navigationBarsPadding() ) { - SheetTopBar( - titleText = "BIP39 Passphrase", - onBack = onBack, - ) + SheetTopBar(stringResource(R.string.security__pass_your), onBack = onBack) + Spacer(modifier = Modifier.height(16.dp)) Column( modifier = Modifier .fillMaxSize() .padding(horizontal = 32.dp) ) { - Spacer(modifier = Modifier.height(16.dp)) - BodyM( - text = "Your wallet also uses this BIP39 passphrase:", + text = stringResource(R.string.security__pass_text), color = Colors.White64, - modifier = Modifier.fillMaxWidth(), ) Spacer(modifier = Modifier.height(32.dp)) - BodyM( - text = "Passphrase: $bip39Passphrase", - color = Colors.White, + Column( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .background(Colors.White10) + .heightIn(min = 235.dp) + .padding(32.dp) + ) { + BodyMSB( + text = stringResource(R.string.security__pass), + color = Colors.White64, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + BodyMSB( + text = bip39Passphrase, + color = Colors.White, + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + BodyS( + text = stringResource(R.string.security__pass_never_share).withAccent(accentColor = Colors.Brand), + color = Colors.White64, ) Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(24.dp)) PrimaryButton( text = stringResource(R.string.common__continue), onClick = onContinue, - modifier = Modifier.fillMaxWidth() ) - Spacer(modifier = Modifier.height(32.dp)) + Spacer(modifier = Modifier.height(16.dp)) } } } @@ -89,8 +116,7 @@ private fun ShowPassphraseContent( private fun Preview() { AppThemeSurface { ShowPassphraseContent( - seed = listOf("word1", "word2", "word3"), - bip39Passphrase = "test passphrase", + bip39Passphrase = "mypassphrase", onContinue = {}, onBack = {}, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt index f66b89472..f9a6a5a05 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt @@ -1,22 +1,17 @@ package to.bitkit.ui.settings.backups import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -28,7 +23,7 @@ import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.utils.withAccent +import to.bitkit.ui.utils.withAccentBoldBright @Composable fun SuccessScreen( @@ -36,7 +31,10 @@ fun SuccessScreen( onBack: () -> Unit, ) { SuccessContent( - onContinue = onContinue, + onContinue = { + // TODO: verify backup + onContinue() + }, onBack = onBack, ) } @@ -50,61 +48,36 @@ private fun SuccessContent( modifier = Modifier .fillMaxSize() .gradientBackground() + .navigationBarsPadding() ) { - SheetTopBar( - titleText = stringResource(R.string.security__mnemonic_result_header), - onBack = onBack, - ) + SheetTopBar(stringResource(R.string.security__mnemonic_result_header), onBack = onBack) + Spacer(modifier = Modifier.height(16.dp)) Column( modifier = Modifier .fillMaxSize() .padding(horizontal = 32.dp) - .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.SpaceBetween + .verticalScroll(rememberScrollState()) ) { - Column { - Spacer(modifier = Modifier.height(16.dp)) - - BodyM( - text = stringResource(R.string.security__mnemonic_result_text).withAccent( - accentColor = Colors.White - ), - color = Colors.White64, - modifier = Modifier.fillMaxWidth(), - ) - } + BodyM( + text = stringResource(R.string.security__mnemonic_result_text).withAccentBoldBright(), + color = Colors.White64, + ) - // Illustration in center - Box( - contentAlignment = Alignment.Center, + Image( + painter = painterResource(R.drawable.check), + contentDescription = null, modifier = Modifier .fillMaxWidth() .weight(1f) - ) { - Image( - painter = painterResource(R.drawable.check), - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .width(256.dp) - .aspectRatio(1f) - ) - } + ) - Column { - PrimaryButton( - text = stringResource(R.string.common__ok), - onClick = { - // TODO: Dispatch backup verification - // dispatch(verifyBackup()) - onContinue() - }, - modifier = Modifier.fillMaxWidth() - ) + PrimaryButton( + text = stringResource(R.string.common__ok), + onClick = onContinue, + ) - Spacer(modifier = Modifier.height(32.dp)) - } + Spacer(modifier = Modifier.height(16.dp)) } } } diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt index eb51f345f..0a09255b3 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt @@ -1,22 +1,17 @@ package to.bitkit.ui.settings.backups import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -28,7 +23,7 @@ import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.utils.withAccent +import to.bitkit.ui.utils.withAccentBoldBright @Composable fun WarningScreen( @@ -50,57 +45,36 @@ private fun WarningContent( modifier = Modifier .fillMaxSize() .gradientBackground() + .navigationBarsPadding() ) { - SheetTopBar( - titleText = stringResource(R.string.security__mnemonic_keep_header), - onBack = onBack, - ) + SheetTopBar(stringResource(R.string.security__mnemonic_keep_header), onBack = onBack) + Spacer(modifier = Modifier.height(16.dp)) Column( modifier = Modifier .fillMaxSize() .padding(horizontal = 32.dp) .verticalScroll(rememberScrollState()), - verticalArrangement = Arrangement.SpaceBetween ) { - Column { - Spacer(modifier = Modifier.height(16.dp)) - - BodyM( - text = stringResource(R.string.security__mnemonic_keep_text).withAccent( - accentColor = Colors.White - ), - color = Colors.White64, - modifier = Modifier.fillMaxWidth(), - ) - } + BodyM( + text = stringResource(R.string.security__mnemonic_keep_text).withAccentBoldBright(), + color = Colors.White64, + ) - // Illustration in center - Box( - contentAlignment = Alignment.Center, + Image( + painter = painterResource(R.drawable.exclamation_mark), + contentDescription = null, modifier = Modifier .fillMaxWidth() .weight(1f) - ) { - Image( - painter = painterResource(R.drawable.exclamation_mark), - contentDescription = null, - contentScale = ContentScale.Fit, - modifier = Modifier - .width(256.dp) - .aspectRatio(1f) - ) - } + ) - Column { - PrimaryButton( - text = stringResource(R.string.common__ok), - onClick = onContinue, - modifier = Modifier.fillMaxWidth() - ) + PrimaryButton( + text = stringResource(R.string.common__ok), + onClick = onContinue, + ) - Spacer(modifier = Modifier.height(32.dp)) - } + Spacer(modifier = Modifier.height(16.dp)) } } } diff --git a/app/src/main/res/drawable-nodpi/check.png b/app/src/main/res/drawable-nodpi/check.png index 744d084f3047b46b3f9347677fc41d34fd7212a4..8b234b85439cbdc0bcf359bf17cb12b4e67304c6 100644 GIT binary patch literal 29080 zcmW(+c|26#`#<;2!r1p+M%kATA<2x8B_Y{LA}NY2Wv!TNsU#s(DmAt~6h%pvFk>l7 zg|a3Y%GzRV8H~BV`Tp+fbzkTHbzk>9=bYzx-pjq_;$$nxC&vc>5VW_mb_D>#eZ&CX z{|zO+%tr1&2)8?Yo;yPSe>ag4+GMfVto6-} zbq;%jv$?U!wz099Uzj&AFkp-`w6(Q|zYc3?X!H&A{a5-(O-*fJaNydtR23DK;o%_~ zo%a0s^OBO1+qZA0rl!Wm#s&oid3kx++S;0!nCR>4YiVh1+qO+nQBhi2T3lRQSXhWa zAYgGg7z1-Z=bo$bU+y3m>FRU{K->!h<9G$c#gz@n-luM4RljcjHuh)T@mci(0O1+- z)|PIuUw@DGhK}e9;Ck{JQS@UQUO?k^bd|XF?LX&_jo@$Sx=K7U73RM4|1Y)vOy!C9 zu%8(y#K3sO+2qkzM+jN&Ug^Jj?A6gT;8&De$KaujTraIsf(;&1O<;W3^%n+Hr4f;t z>7+*zQW4+bt@x@Y9#BS&Qm{-TZ%wLw>1B4berE7Nu)ozEZNYhuSLaX3R;Q=tC$Ke} z@`A&n*)NQ=@~r>({_fk;apW;i_qUUJn<6q6&wsuN*S;R6c6*EG@0YIm;wM?PFC#L( zC|U%)iwX!}_pOgzV@~)NY2t!k?lJtNTRiajsXqLlyXz+-sXO50U}jOafBHY7>-P|| z$oxkT6;$6(xctJ%|HI6w+wxw`4!}6rUXswOVXL}gdrj3*=l-q#BzPV>=Zk9W_xign zEQazUK;)|x({f8s&=S<+;pt#-BZM=Ve?jF{!#;of^Cj@i~sc>Rqv@et8=R~ zEr{vsv34%6KDX>!2TQE9duMqEe0xvS+nvYO4HX}J5IfgvT>oSBt@#)`$l_eB`ex}) zJN?HMQigDJiPBT2=$d5wk28w#RU8HhZ{wCK6 z|5g)!qsdQNCyE-rZjQPCi8@l7X&-(w)3HeJiE?F6=wrFm%B_;lt7_hxFRQO0%;v-4<{B<6Eq>!<(zttEG+`du2>ap6>DVpHCmrly0PD?VQ@{VC`ZP^0mWn#|wnc&;=hldSkGe>mz`C}~|e0lxBgwONt z()F2n`NLwvQzivY*W>%9in11+V@7E1oU!A+`<1-Ae{ZCGQeD4ic>IXcmL)!$W5VOq ztnL&8<&I{NrZ1lrZ`#JXu79X-^$E~S@GP0K%;Jydx6$+b%UtC3bjh$SFvr%pB-~4Q zIQ7cVW?d|HFwp<*x0PYSM}ZMx0k-_RF#tWWw5og)nq0kJrQ~$%q;>Y|P3%d}Kcpeg zN8uJf*J&{+>!%++yVS8BIqBg`;z_^Lwt5C`G7L}Hw#wQ0EFyl6-RNO?Jde{k2Q()dgp4SHj={tD6v<^;exW4fMi& zJNGm-t1H9$VzDcfC~O@RsA|>U9usVeRmPHWZ5e7W$C^ANd0${`Q~|vDyRG3 zaox%*wyoxeH9uRdKKjc80~{~H@rcA3Pr+gbr!Z^&l{#hz5Yak!tx{A6b?r6t{ zq|0-@uJ@62^){vYw_Bq*lF5`MIbLSlJur7?d!X7z`pm3bal?9v1c&v){nh^KpT#zx z{j2mFVcelr~r;xdQQ0 zkN>g#wU)gIwb*^E7zSF0L#yL&(L{t1Xw^;ZzY7QzGq#5!r3v_u^(DGb_z?B{<``m%6I4f_)&Y! zex0Lt+`m>`^L0fLRCn2TQ9avpo1VN_9{Ns(F86~kV`cr-vD~@yP{P6KhbiU!6#Er% zmhR6nHx$~lPUtWvx1 z>@boE61d3Wa25;{Zm8!_D4Ulq#>c(DG6cdsf>sDU-a@ydwAmpUTiqK&cHdU=TH{|P zSy2d=Fq^9_L1y>a|btKBJrT zWg3Zna)zS5rth4*5R%L`I&eq+@YCC~{>GfumAg71gCN}Po$=?7i> zUI*Mqk2Ol}4jIQ&ODT-70@|mOHBSH09JaZz$aT7J(gcL2S9=%g zXAq{#>p1i}PT1lc9E(>32>y3PA!g?m8uYSlJd8SMKPJ7eiC2`E{c=bB>rKx!Ce_Vd3!D|_;r3VE`j=@ z`|L4=$upI5PtS})doixJDGxwFHs{h>{XsOjHzbs66ail_WL6@dAv%Turumy z+n-&Y+=(gFggM?a}pm@KBFmU zOTecdXQuiJ&EXIc=+iZMd;Yz#usWnYm*y`TSD!bFvRzSDKzJ zRUi5IHn~OP8J%iCEDX_@N@5XYd)4P(&o1mw>Fy8s*lIKI4T@PO&0DQg%=OHbdN}u| z1I!cz@IO@BNcxPs`NY5z-+V~QRy4YJ*xn%i8ad0MVK8);LYP6vNGIDq5$pjc*@PoD_M%GRZ>K*~P1erV^%#6K_Ve zGKB@a*c<=a&jQ^~s`dXqyJ-K~b?f^b$@q0itQffmbzb{lusEMLrddiEr-Fsh%8J>zMTyKiDa@7ZmMpVgb+^AlI?I%x_0`Ff(wyR(iDTFb# zO`)y(+FV8rGfX+Ox-7x)uSq?EMaix4dNX%jROREV-U|1tZ+w?HAn{T(u=6R}_I2#< zo=xs~qkQLPmi6g%%B%GXF?t{-TTx)9@_p?OQ@j3juf_a1pQBTdGCDQ}0xvU#*Qq+7 z4SmTAcsc(hdyz*;`)KcX9Y;u#Qx}>>7mLoGbpWl?jcbDIfeN!1L_^w7c_JAdU?^;%W8}EuxqBXgQnM=5$mB%{%4#xI5HVM z_>-ZuL@gHN90rb6mHXfT*rS@_OI3i)&6oMbyPpcIsOn_%)w_Nj`%rny!maY%k9@N7 z5LU(;!s4n5-;l1-ZkYqGbUrjN3DUnx+ez@o$7UA>Vcfc}{34yw`M2SvDtz*R8IwkM z1sSEH(#h!I8>rFonHAVNgUdmdni z3ZH_|jyxn_FN^x9kS)By|Lev>89ei-i1+Q9?o>yLS+URCUxr^?_s21tJbZ*N9b-R& z^O7mwfq@O%`*7H?30>~g1f;+Y%Ymr%wZ)l|q)z;@cy_!vFY>SC^wE9bKlC}MYC*9J z&P-p`oJ=wFA)m}wFd$WXFVitlV+Z~H4b{k$zaJ6;pEg%Nb-Vnp)|r=L0UTMr?UwA) zoXkuGG#9K#vXvy>a_6$RVen6KBdAPNgS;7fxRru-2F6l_D-;>(n| z1*HN|3k7!}o`Jy{Yvd6T^0MdT_N(XO)Iw)GiqAG8Kl2fRS=8h^c!`B|Oju~=0%=-N zL;=;1Ex&p&P(wjX%1uD3DAOz^Z0}67LywO^;@4|nFUHRc>|;Cc-&E2i?Skf1wY`g% z+_EwuKc0XeW5<=c{)(vXsmuOI+tq(#@u5Wl_^pVTB;gp3cu>r1sX+$mq@iEI#;*`? zJ~_hx#lg@=~?OxGh~~ZWqRU* z-u-gu)odB1^q?-_-)?eOP#zDEXLtPCg8`1Xg-<;fwVw%U{3chf>XlY^KRN%?;$_yw z2h`jSVfuNP#YbYf~<};~%`P*1igq{sQDDQ{T z3YyP!Cb-Zk;__7=WW(pLVOU@162#l%M7~|Ree7Xdie_A-klWvtIKHQHU%>%f)#=0B zQ0nb-0|i)(7zDw#Jmmc@ZF`sKb#G=6_kt!DUC_Dzbo6XQ!V!s93q9bSC@4|^wN3-w zI}3T^)ZJv?;q7e0B#H8*AUjjxlxt9|snrncQCMo-sVB3yD{6~8liGuZE}jDUFf%L= zlOn0dIn;^<=77BKp*e;8)dsyvymx*I{-bZ%c7$R`wT)8W=NU*aHEP^e1-k6YfO*>7 zf-?>&ISm}MEq%W+|7fa%eXYw+R})Xv-hYtNz{2-UZO<|U;q$scIG9P*=LYpZSr@S? z$VlZzEBmh;l(4r7t^rH^=)IrbEuZ3bQ zW@EJrLeHT|X7fCq+Vy5d!Oby{c6bCv#0li&u2WvTLH*w_=c#beXGv?T*5-hjeb!Xh zu+hov4tC7&*G%G*QR)^p>F}noIDG~@`eJzv-PJ)k&l(Xn=nf6ku!cNi&8=dls>>3N zbMx5_eVD!eg`p$>X36v8#5*K&gBKvBRwrf7R9fhsQmZ;aOjKtVkx_#bsOD?&q zHIFCifI$Yu;*uV}-*xu_INBVOZ6H7p$5qUKng6uslR{a!miXKurXyX@Z5~S(V(STT zNC2gFOenoQ;V&m91&23!0v}Fyg=G$ec*ku)02mUCr79$wQ~V(BK`Ov9c0gCb;d4?)GMe= z1JNA?rW?( z+;xp}Rr$C^>fz(RChmzAC@C+cnML*;q_*`AmTJ!1Z|F$;LZuT~((1@T)S2`iuF?gj z-bjWAr*X^Mzh(GZ9HFTXpA4)gD@g>-Z^3_rkklT^Jv77BLYKn2JbKm#yCS^7_%rURGrF9nf= z*L<9}Xt1`akdxP(k3voI=$B#tdJA`)OgWSx{_>So>t%WCS(gRk`h#nwL$9?fQ~F$$ zmiG7oWQ{tl#W~bYISBrP>2gep>+z8nr(S7mNDoL`VtMhciwtJ_ua9@93W41sDU!jtO0e60ePLs_B;5}NuH+5}{11bL z)X>w0eEKlsMcv5!YVSqJ{(*G&izT%K&v_XZRODL?EVn{2=L}7dEbgPpUkomhf8RAk z$g9TrbQIktE+TvXcfm#jBKZ?Yz|=Ir ze`Q0#HqN2zXbU|P{arB>bAW!vI5z%V7Zh@KeUkWvi)hyWogJ@0>S$wopu4e5XF(A> zst*C(p6S$-dV-yaQ;+!hB(44`hnYdl>c;xP_k-Z)1a+BofP96I-pkx<(g7Vk&;>{t zc9CJSXY~EF)x&rt_f$D)uV)z~T6JB-i)L{WVJ{!Z^uV?e(83Q2t%d?CaarQa|Bk9z z$wFlcfSXZnPmAwXds4%*w`)_q6WdPPg7SEhONBVn#j7#xq#JbKbnuOE@6ylXh-9hU zrH_1`HB)7e%fr_vn`z+9R=`BZ@$7C=@@X$r?`;YC7=RdJx8AU)s*uxL(qT1TnVti8 zj-|~@6~5oEAvZZX=H5a}v~@uTrI9a2rv$@ODF}Kp4eBb(COio}FuxI97c?rR>b7w- z3J;jrpW=>yd$#$(otpx!NaS1|NKLBTE#W@(=yEwQ`H#@<;{%ui+C4V_Hw|-JINvR( ztTj|#zZ1Ik&@|fS^+>AcphAFl)Pjt?Qf}RdA~3I(?D-CeI|a!CD4~LE(6F{w8ML>M zsMJdqS(2*FKAX01z|Yf>O+`CXbVoIFuDZ0}PRZxgz83@#lgt?At9JU1EntTDF zYNcq(~y$a=>L-Ru^F( zbm9a|6tr#{i&siacF`0b70|5I7IT>tP)R1Wk&MYK48!O^pc{Dj$qOntz+P&e8NMoe z^QsIzZLX3Yv;9@izr*jlhOnY(6iw8bRjG;C2%vdD6@Gb}*WpT-HS)*Hi>4CkD|(># z@MEV$j|k1P?cbF2K`cgQJnVl>(-n$7&=o<7kkj@^!U6y57I$WDkBUiPruQ!B4Wa=c z7Lp<8D=ora4WT1Ys|IoX&8Ji0@Ur;4kHCRtdjmZy-vd?4q*3}67<>oMI#2~B+J7ZL z5??Vzd)RdoG^_Te&Jn^{s&4(*=dQ-DyPo`%EfQOAcO{74Lj;@w267z$wm85Gwuiz* zp@--N1JX#er3a1pENZ8occ{2n4;a6XbV&AS8q*ixp(mJirLLA)S~~dcMXub?%bCfR zX3`^n7#hT4gpK|Fyrmq~1RW3Pq*HrO><4^MD`U{p2OfA5WjxEz%5{C;HU*1tLg3F7 zj2L%Y7sC1zN-{mibSu)KE%|17@%@>7$E=s{nrnIm0i`mP=eNRWN#bShJiMVm9b(hO zImJVR(1RL;{RCH_)3fIT#${`TBq3ORRA}p?##e`p~s*!1!HjmCy~9K8wcZKoLmC- z+Zab>@L2YEL-HuFFKrmdW^b>wx3?=0BLPUc*N z!~HPtJHo$RhGiU7v7FPHZ^AJy`=AMLDOw~$sjucj#EavE*i1?COV~^V;CfSv?= z=K?Z?-a8G@Q+2yno?ZuYpE>u>{5!cK`=L<7guaJCNDs$Y%jJQ;BAlBb(qK`RK)$Ts z-(8N(eDdU(JXomTMvA0cLa}=6+E;K{!v4rmAoUh7W%BMMxzlF;dDmQ6!n=$&C5|=% zb6=f3;`GAg zA71}E#dw`wOgOj?2)E!19^e=O9t=vz0+r=Sj(h{%%d>FNNcywos#~q1+>G(Z`=(E} z7YQr;E^P}hLFb=CsRzJS6S9gMSQ@7pVhea<*MD7RI)Bj@K-EL89*WP5ys9Lxfe`XR z^&Gda2?A5ax|}AQJ|A=>cL(l_1DC`$78;gdFYb<6y1so&+s(qoV=QlXEX5IN#WG)p z!r3PDH|5dlp!J8J6#9y{!@Ev*8X&E>SmsMK@#NRF3FjF zXh#;n!7on(S8)&yr}jJwiMq8@C;sF!i2$|Wk=xE^cyM*318R&2+`0^(pAR3R3G#*k z!ko_q>8abd+PohLA)OhyB9%6uC`}K?Nj)VCQ$egAKfRfNRPbZzfL@@)8?KzW*4p%D zPHt2uKJoAl3$*4ek{}?7Y^sCrzxcqe^&^<(`%sM}a`6~2bne4lI4PZ7()ZEWeb#U? z%+2<6LF>a@h5%h$CLekU%Pov;!d_Fkbbo2oqz@?X&bgEucXEMU#rCtVRh*Q}Uay z9=z+kAOs%rrZrw|xF^kV;WMpWe_IzCZ*a(F;V zylwdD4`&&E$hHb&LhM+&CpqJR3a8-{7rJKxB4-|3d0iovuy==AJOY2=p^ z_#KLL9$Dqsqa#=iL!iKpvn&~#ykvMV_3I{Q@ATzRza==_4z*1||4Pz#OpvAK5b?ds z>d)_oJNu~k50Hm)j2+(Xem7XK#73JAfhz<*UNE&2$H>@89~P>soqoch)HUN>o;nX^ z6@;e!$H#fTCMc5+ip2qkz<7Z3V~|I-WqoItjy=9*Lh31(%bFOdJNd`rDgC67hy-1Y zUjY!=&fHSW@8?qn0xAGQg?tm@kwo0b)_UCBHb^cjyjn)&aJC`J!v_#F>Z&oBa*%W0 zmdEHZC87mGcq^C=^lFpMJit}bjuMv6we@?WoYRRxLb_ezu#`3wbEbX z*$+kob@pD@ec8y?rzB(V&?kKiJ!&}4=-FvG@zHGYkZMOq&sJzkYrSK zoFk9-qiScElR*vIVP^EwHerlUc;toaP6 zbGLpTCv!$=B4iaZ>traOOe0i_vc8DWS{h2_hu3paANR!!?k*b_ENu)(Glm6A6v zZn_;??J$8I*Ok;5qG%&%oRi{ISA2#NaUc|G@BbA&46AEoR2=Q!Di*%4>|gBD`1iB~ zjKg*(NQRd#Pb>pW0z}?WKqN&>VLhkEDvz>#J`YTo|4nw^^m38>q?CrnsS7m*K5_aO z`i6G0l_u#70M1VbgtKG*V9BSUSK=U1w5nA(WUDl9Ud`Mo{nlEI>g!@e3HhPg-`m_@ zN0Afds~lhkHvDBd&@X|*)jhy*f=nhI;amlEi#{6x}b5=Sd`blvX8iiE_ zHLt;Y7th>X#ID0Zh(0VRl8PFPQ}C>LK1d%*(ldYc3sv z%1`c;#HsaKEb++i>Is>DXfeyYxGAdNnD{`g5C%KRBAnejA&)MnlVigct`!t9P$Sd6 zO1F}pk|QYM!cIpA^mc~zk?nQcx2L*jY$bl-f9fg0TUKpG5`)1I;L$EfW|c~Y_krgr z?7*u6;179qdt!C>=Dm+^t$r$4rxcnUm+1U>WsrZbRmFKByZE#u0{^N$=h6SVbOOLt zJ>epp;7tMWmmXg?jbR=Yz2=ii{Tov+|(14U}T6K9e3NHAK5|!Nm2CY zRf?fVFW)j53Ryhp2{?0h_|&IJ?1Yd=cYG@^Iri21>zNBRCP@|H)6=3@pTaXZe7XO6 zJ1H@ePdXek^}QMvl}GAS4qf^GG`QBiLYfCQD6KnYOUb@9fkG=FLBeQV?_`!;|Mq3; zQK7yE76})bbIXN&QkNO5^C)`q~&_T zkbq+lCOu@+#nkqs&6fViXVERb?po4~&^-b2Vepm}Q}aT=E|9ya3MAWUqsqt|l0r+e zYR_oY!pW=685$39HKaOC>(50~suZ7j-fOP+GI`6UKAulg8z6Gt5d3_h*jwTVpI;x~ z@)rMao@ALqar%el?;)H+4-;L}E{Y2GSikG-R-NJ9C4Uz_bF@{iJULzWt)BS><;_g( zo?c+x_5%D~CmF*tDGWZ|tw`xEpQs}zW8N4XS}qe!2)f35s89p%K}Q_OQ*uAgvn`AS zT7Hh1|0Z9R>=D?+ae0Hx8zP)h7{rmD4${2fzQ;BVhH{$*C3_zAd~V$Q7@_=r*W$!G z^Yhw_kJnmlS`VF(RY(a~sHEUr0BViIhT!QkoY&JRQ?cj#AwXv5sQq$IRIIVPTPh;U zI&VPHcnV@t>_YTEY#sk{W}D!@>yijg3~62=W3c0-yLIbI8}SQREqY)2tjx9o^fdrw|{HPnRJyv#?3*oUA3I)#tp zJYWIE!Y{u>gTQXzd?3Jf)=SZdE=g#AunG(wSZ ze!2k18X0~CPBDcS!tO9LZRyb;Pj+o#?!4{detXt*zXabjmDz1dZq>r1Ls<{2@x=W5 zp_c^QU&uqPF&LnZ1T&QcNv0gn(M6OWe<}-HR!xnW6q8Whu;^BE6Pe_#dbibVPryEb zzrJJNkNNsmh!7rX$Y>NFFB7mzTnulSOW@cS#zHvc0#_nYTkXpl*41sXUO;Gq zv}H-qO9A!K0hL9*Av{KAcNxLPk^22-ZRr|jFCA_(!;EM+(zQn|BfoCLI#OhOxJhTK z+2zbq$$Z3;El$~FsUKF0gZy@!WX+#~5V3+wxHN_&P~MC48{OEwJL8&%mi-7XF^ni9 zWLrG$$2@itx(RNO>$LXux$y)}X*lK6_wgPA+eJF2^7{3m;by)vcfGIK zo!cvOpSiU1C|l&*4aBf4GS{hhl80ZdkiaSeADEfgJi#TPo;1>Np+LINvM#>(f#e{M@ zM&h=wV45-3n6U(t6Vb>w%rQxfh((h6lf4I@*4@-;Ze|`J_#^KB?czx9VtmvVQ&2OY z+$go$0l$V5U!S3+SuA%*4>g^ALxaEeqruzC-FFzTQ;ZfSXxkX->@Pw``%rJ;SF>A# zMr#~yh5V2&z&SW6GUic(Z`=NPUjC_IH&uKo=f<95KQ|^q zd?3)u>i!yiNd^kSwf_=-Mpp6JOED96`)L#TB9x_6W5MCCN^cpU4t3(u$C5C~pBh0t zAoV5D0JersVTdB(9#TdH4Y@r)aA~6iJqvv>A_LA~{Sa29gQNgvvN*m!go6HVTq+;; zWe4^vti5PBf2&g*Ib zYvZP@_6r%!;T?2~7wc4%?}I+e@OR~V^=+!S?ASA0vf1?jer@*;gmWO_gcGmJv#@W) zz;SHlg2mSvv``6^XVy3Z3p8kfrVFxiXSu)!lnZH%1LOIK%1+?}E`;WWHL2byG zAEuN4iO@TjFQMWm(rp>@@mx(Q zYvc#FF)u`?+dvGkcvu^kW%RXkEM<7UU2NEz5-wU9y-zzIl%c0_FNIN+uG21PEjHdR zeg|8xv~BP`Z{xf}^;~JQP>oEIrzeDZb9Lqy$NpRUP7WZOj~MYZ;X(i#o^4#=3=<-x zGbnLTtVBP9va@S})U`l6_YJMs$(CQqWKjjj9xAe{4Tn0W!q*4t(g?icXigg`!*Raq zox<%be`7y80mcK%!gHYe#D2vducS(ZNKhyHet#v-{7yV&{EbE0ro2#1*E9R7Y->y43zTelNX(0 zM!nlRTx7Go26a15&z2F%NKsC2yQ^2CtN4w`YUAuX0tGU=`TSDRH$w~x%qpF!ootbQ zhMsNsH`rBt4QYX^6q06Yn-YzxqLYsCu`4|kc8&Vk*tsPfw zo1?=KANWmYI@4~s7jhmu=C{8)BaP|dPqL4zq1F3O&e5@s2Z56n%UNBBQ@=5G4woLV zk@vRPw^Eh!oZyuDeRbEn(|)Jx7;h+rn|~aS$$k9IAX;|q@cT$W1ju^4XV((We@n7A z)I(l4u1QT;)B|NlfrDXxID`h!1;++ED zOc*U@R|wh-?hXOQS8ogY{LVi<%6gat*&T%fAMJn|E9?<5&O3Cr9jDSxF;i^ZvkO8* zL`e2=@z?oQ*7%|Xy>Glr=bU8HmKikil{d7@AG22xH3>XcmN6n_i6!c#md3%Ey1`yJ zo>WP|g)=I>JyL*ckwtSmyBoOp7(JJa25#gTuZRoNi7`1sQrutcoAU0a5a85V=lQhnnTO~9l0~e-KT3$*;Ww1q{MW5sr=Y`hEIre` z_NFXSHN$ni9QDb(+?V*Qku*S*wq$Vrb&_f2?_B@$enxLkMtLV=%$gp~o9uk+iN*5S*6=?~ z*PmOVfy05=Td?2k$I6p1WX9Vs(GBbwwxVe99<98c+ zDF^J#lstDWs&Iv0I7C80FFR@~ke)8X2oq8*cH$L& zeeaWK2#IsGn#fstc&`yk{XylcI%_ZW8C}R@cIyCNtscF|-LW2drsUpIp>b;3(!$F$ z$_{A%R{GY>Z>KR2!|f_QOS@TN@{tOK#6lRU1V_s#4&a7b3WX1t8Lod)veW`xRo9!& zLv0y|k4QJ5HwsQrcl@9(M9wjkctvsuWz~=jk7-k3(G06M<2=81PXsp48JvKILM1CF zXyW5xocG1(7lJDJ8fZSW?TOUn9P%@rqqbdwL6N1?;;A;f{w&x-r`Fvg^SmMBOENKwy=yQKxRM+3IUS+<+h%M0^je$A1; zO)prkI& zi6|!thEJZJdHDgQBtd0jbQXEs)X${`3bjdZ_%>V=mPaWi$V~GJNFkzVuoB06AJ+(F zpswwEmvhxXCR$}z`bzLpgd0Eg`ZCFkyvtk!y_-yN0zWX9n~0x41z~rvc>mO!QJ@kI zr?Zu`xa#raAZbyKxHS570XaZ|Ib&lFH68cz_Z;;M?gj6TygS7f8MGzEsXqo%LCAlc ztCJscZ}6hG#`@p$sv`zabWlOPU^t~Mi3++@FJbf4khXQyaLaRio%<{rpy-HJe0Vn? z$X$yHEOJq#yV7vdA~PLkc0t|^E zhbrKx9p1*xrpd}X$rWiZIN0a|6FHu1t4mkZQ=)iMovC;xbk&R^^%1$rQl`f77 zwB2%M1ItmAD=36IlTiTyAWGliclS>fG*1g&#U0jMQV7QkMUokOJw7a7&job&(Xx6# z|7UaPy&}o?{DkZ=rkV(h&?mudfWG~s`Cr17w1uEqit7%iBDbkh9O?< zQR}3;E*NYZWnd3_mCrBCCK_>r^K6ub90zAcE+;H`4YiQo(tc?S-n+kw;oLCABX^O0 z&rUp(U#_N}NaC2Fd^lhRiKJ0To;i(x!6F^*+Tt?h<#vo)8$dKPdhL*2P>!dgKtCew zO5ohwDOiBx=$`qv0~nY<2N%~);wC@TH{)a zM+y$O^_lEY+P?STx+6!BRVhUOz|XF1W0gu|-OLm9Yu*YM5O_zpRYe`hgK{YZLIb+^ zuI)=jqoXew@VC`0}(=)h-lrK#0GeYE)*wu)FajUjkYPhSr+O#pwsf zPQh-2qNQ8V;c5R;v~3%go~7;GPF5?S#2Accj~IfPoYxKACU~ z*#xmfcXPVW?J&ZDR>}repCQ~%JKer8F9VF>$Lof)j+2NrFDf->1boZUWIX+Sem+rq zqaCW-o=R=CVOc?@2- zr}q#T^7vp#&VZ^ct5oUcYwbb8O~jWFDc?K0Mcz(NkH+)m0z0t!U?P{m=Q5V;HZLD@ zJR1#XD-FRs`X18)(f#gHunT%so=oFKbND#49xlaPjH?pxf+4PtbA5DLuUS&$6=-A% z(|f*sx@7ci0$nPX&yFv&vM0D)r*t9>x&`rDmwV<0|Kd$s(fo0$Nz8>6k}%YX{ZNeC zWhr~X(#XECN`UuShT0mWjg$ce9gwG2CnMbP3QA>Cqe{`l0Cmp%X~*3Eto3$>EDsDfK9d4xP~b&ceEv_U*;5VwoP)wS!O)?+*NtGfZ>=2$&0 zJlF&Hu{sXa@oqLNy}W6soLdb^Z=xT?8A@!WN{lrg@dk#nz-OL;)6J7kInmn3;p*tM zf}A$N0z9`d?4X$RDCq!4P3Yx;#MTw8veqm`bE9}*9;Coy*1CO(l zg#!YdoJ0uv^w~7zq^8PIR9@4Qkv0&4Ko59Sq#R-e_l z-gW9>-aw3z6!0*VMtyfLWHNm}yd&`GCA^>#CV+?7RFXLT2Unfv)D-oWd9zlxBL&!* z4f#!`3oKF}Nk2JwcDK(uqq69gU_lGNObW`&RC5xcbbqd;I)mxi!)fGlFDgXM11?ye`zXjiFeCX08#!i*q7@k|N4J8;{JDtp6(l_~Z3B+JtiyFxhz^hqXWX_GEz)1k%~mb*#&r4K0T!EoeXAO|bR29~hCc-aAL-^ZUZYyYL|$Q}^Os zvA~CGdL+2`a(zv-ks6+ZiC56=#gPA)5@bR?f%#^(7e&jZ&`12a&KPkX1U`+|7C)uht2=GU|gzzlpt5l4yxk0w@NNkoZLQaV$XATA_Tm_^TvSmS=0}Z z@jwUElYe3*wVA7FDb!wG^IfdWUkg;H^X$?T)JX^88%G~7xvo?X8CHK`oS5#a+rv4j zCdz3_UlY^$uoaUMEeo~I2n@I1u$`}Kk z8O+7fTMZdWh6!!2pwQ>bQpjnL_hZa19;B;+BC=3|5wstF#muDhuizt7Kss9=^WVmnm=pSOL`oP3gW(khZhOj=i76L~F3N_@^cPQv`p z3d9taE%>S;QViC}jzl3czR_#hdy{0qn#LkRbt)Z{y&UJ&|Eu6i!=d`y@bAoS4931Q zp-^NkqMA{5Vk}9U#*(E!MJXiBu|^4rqAXJpNttM|#gHw8q)=pJNtUwjX5M+vhqbJkc;rph`uR(`JqFuRDiny@0W!o{Ts$6p0}PEQ304O> z<~H~aKXQlXt`j&zs+UNxXZyXfk7-Z$Re6AVG_VF}AMn;_y8Us)l&4q)B61OrcD89Vn@B@5)g#ZRYh7kKS z@`$8|*}p+KHOI8ky}KwpQr#?f>CGh+uwk`8yBFm1dD!?=<{rh`TJE*MLM~5mB0&L3 zTwkM16R0?VPlA;tNS%)SZ;;;K&bt+bIp#Q7F z7AG9ZCI9=U_+(BaB&v|uE&zVjwux$gCW&hsvaM4F7`?*kHz%>@0+Iiir^}xOjGe;(FiE*wgI1> zPoF;v5UT;}|Cn1ltC@WJ`_WldS_}#|xoa%hn~&-nsWz87Lf6kp=O<;1<44F=6%c%o zL6yKoki-URTcnAS=4>Gm7RPkXYyYjs2<~z0oV}3h3>+sM+6+~e&Ato$x^V{nS9Ull z17|o2UMSynPT=F7$#Vn0I0u$>N~$88-+4 zmdHAc8VsW{&Zgf)$hu2}LFAC1%!T4t4IJ+Sk99JYS0@a2qA13uzP|W1pW2K1($}xK zZrcB{mJi*Rn^^Gq+Rtp1uLN(639lF{ zO9Bg?(pM&?y2o~u4hdwg?<<$X**)lhTUKLG`;B;P# zrcZ#2g3$dn`=E1X8nt(XRQ&Nh|AF@5@;SBU+9$eGyI2|AI=PfiFsl}-(5vQG-f$u{ zr6#gZZT;83wzK;30+_>3xFdN}Xy#D|Kr^QFi%S_r2XmA|*W;7#^uEUHXCe077%{)A zX6Fl;KEX>#!zd)+r;AOkX)CJ5J8EFr?_Rx5-RNF(fv0{J345ry^&V5U35s zV1PBPi_m(Fp}mT<@UpzOxHtP+HILJ(d`T^p*Mhx+)G`sAVLVIz6HxZwvvOuVpXlxo zyB>In!2_KBh7UUKrfu>xt&u!}kr6+0RyA^3wgov8fN~jc6fUE#efY%#7(=V%(>lC` z*bh{pD4<~riCY30NXuKOF0P7A2^1gq!RiiZ&AO6hw7Fno$)jF-A;w?wS(e2KEaq=x zfBfugC`GVX15&(Rw^i8EAG8`X)WP*%tG{s_-xQ1 zPei<=S$?HTwqiJml}nRROdV!2c%BdhdyIoRdFDEvSu2p{$WRmHJP9OI2L%KkLqhx_ zdcg13nVtVGD`Cazxm_x77J(=Y9IPX~%q#@c{~oKCcSaK53d=}BFMOJy6B&3I$s487 zF^P4GJL(vsg1QtUO5az-T%lNEE|qZ7$B|-AHy-Q4a7W`0eO!mvHmc-2CD@sC@!BPQ!Tdk%=2~iuON`V0{BI zwyyoiHOIDvG0K=`hEP$iEI&(%!pszm6Rk4H6eiKviVb)Ky#$C9X>OOmA`+e9*J@}wYm@?p(z8eBfv0uKhh-xRXUW|y{X_@sgd^@^!sb8oulbXQC5Xy z;7I~}W+Qqb+IU;JhU}TNLdnvz0oI}DMcvGZ&1DsMZ)YfqwGB4x4O7m4kez;~w`qHj zD%Z*o(m=c1txLhN9xd>#sv_75Vhb|Q!IG?lM7k8MroHs(zVxeqz~+AlIMWLLW<*h$ z`I}xO`J&`wJnkXJV+So@yZl9j>ocX|UtoAU{CzJ;6_fGhKS3V5@Ej#laEe}D7{^Pc z(B)3rg*mMa2#&73sWGRucSM_w)Wk&Bg&O@vu_y4dzzzN3Zm27Y-+~paUoMQS)`w@` zJ>;WK!o1(`<&rHIpD_wgVyg`oFWJ16$c_RX@YidHfiaG@2EWR01}fM2;p{-trBXoh z`?gt~m5W<^d0vk7K{oUXy7fXYcN>yvLJvdR4_K=n+Bjr9JW39lB(hv#_s@JYR3O0W z{(;7>=dx?>qvNNYQum-;Eh~BA1DFKe#K801v#7xB3&$r94aFRc@_HP~nW9hMD@+fi z$*3)OT0Y++sumdu;l6@UL{AkFcRUb4lfGRXY5%>5Xk#8#e3MYrNZxA7kWLxo(>VIh z1N;8W%f2S(aqW!sRPb?P#8dn6rM^={t}9A=wd4&T*(sljkzfNSU`issrK6Du_*s7> z|LV$ZIN^Py+Enl(&fXBc;L}%*WfWjv9Z!5X?x;=M_sPfdvDvRaaFN~xQhMG(VVZEn zyk*)o@8+2ws{L;jgP)_5R4Kw-OZdx+Er(;|$(QzGD6s@a-Z`YVViuH$fO&caZ9MWL zolS8-V?>E`g~&Ip^Rc@vExBI=eNQ3QqmwB=E4DHHQ@{UFy-_i{jN)aJE$ zK8qytq_h~9__GoNT3IZVGnWEY-VHKbe>C=@%7Xbj1?2AuG(>g%KA%MVGeOq{LPFz4 zl++#Dk@wm%`!)BdVER@r9#2i-M@f)E#X^4{V#MckA@pLQx|m;Rtbmt<8tFU5{Jg;~&8k<_>*S6*Pc^P6Q1keqcGQ-I$2OQKI=_`cLR| z0XKHy-_>k0{N_naLqhrs(KJ1pzm~P@xls{api*}^a1qfP8KF)xN((lg?L_P%F+qDW zNXe2Klo?tW9QT|Duv8$Is>?bwfbl^iZAnZfcY;b9Y_;R3{N-LZgVkCKew9t{FidH4 z+eHfHH_eBfXJYLyCy3myl;*F;1!{3Qxt|BUkkfGy^x>Eb>7Nv){8jiQoG+3GjI~)f9as#>$7lBbl;kvNZ*Yy zGXBF2_dwOc5)8$!OEv-4yMQI`;xYV-2aq0fT0f$GD%ehbI<#^?{aL*PBQg5lB}_vW zr02m>^8sZDTajYah!vpo6N7*Uu#WyoDHOzFWc}?fG1NdJx@B`Uc>$&NjBW|iLujCO$fYOj32+MM zHtUjhOQk&BEB)X`{ZVC_cJAR^4c^6TtTHNN<9(sWvai7LF76}wu1LMYYXXZ_?%TqD z8Ls|zU~LHrA%AKj)rd@U*bqptj72aT#C{EJyALOq=t~knl}6-V5aXVB^^=?ka(97m z_k;4CSFGG;JEdli#X^1$c=i`0DiyX&p>6N$B}~4EdSxQ@8dzP~`lzT2LQx_~@Y*qu zaI)}pg56b%o8cy2(DhC9UfOC0A~N2IH5pL~_T%9@t9?K4x7j7G@Tf!)4~PH3sk{VA zTW2)s=npjY+5iRT?CEexQ&n9Yf z{&4Ol_imy6U`gZxe=y$%9y5)2Cz|!!L zJZO1Qc4#RCP`7hCBYIB76I1wjjp@^bHsFn-YU5e^C#s8)yh++3eJ;(rcK~m~A+v{G zQo@#ODfNPeGT;9z`{4A`?P2!IQP_14sRfimiYGD7(eCk4pLV?lQfNxGI4LPB&QWQCBK43^YFHQN#+tAdC(^?fZSbxQ+e$aU}z(TM-&JOM0 zdoRKwcpDojP6*YnV8kXvhuZ)i#S*|DhYe4mGCYM@c+KYbb`#hViXC=L&{uKO50@h$ z0o%Roy*B*WYWsUYc~Md;xhaq7Gc@GU{?R@xkF;j!n}8Aq7RL1=Huo`NWdXBbZGL?h zPtn<*A_+qISpA^K1un9?iZKu2GWYAU5BjjU1gg3q2}3msK5+p#?UapXPc-Wnrms!z zs#=!twMV4QcB;b3Cn8@aoeX{%75{Chg-C=@{c!jP@Z?RVfYd{29ZL*45hnACJq0hK zXcDx`yF@{52OgJ1KLOX@B^T^NkUC4hO8;~C+2CsX5qKjNcpo2NrfdZfhc$pcZW zO>Tcx<+mnxQoPq5owSPsUrds#?)6(Q(GeN|nH&r7vTv8>8b z367eGb#H+oT@2g>HW=c>LtDAq0t+jyoSso|n_O2Lh1ci&5~I#*z7mEyVX#QIMNIc> zjPRS3`2g&k&^mqVy*Q(3nzHyLxq;wzcv@eHo#;Tkb(o7lz^K3jr&`S?Y>}n2-B-SBca3|v|wElgODhVATZq|A_cp%yieyK%F7Xje~vLU_}R{P zREy7@`y0{tqjU$(7eF=IzNt8-TxHb_p65o(QGuC(X+ht9E{_`eg}f5?w(|P}jiN18 zypcMyA!pVz9&XbVHAgMxC~DlmNs%7$XQ_j%QSLHXk}`*Yjnvv4btyP3%2aPW+XCv5P$6a`l!A54)AA@hzie6`Nfp8Zrw6F?Fyv_QF< zvrMJLB%(7Qf61y;l@MtEe?RpG!M0tDIozz@3;C|ff!K=9ok*P&djt%RD_#P5^>1$l zQd)rCXn7HH1$ z^jmrBI%QCqF{V}+E>5K2{r>x63{7uppj-DMc;vdzqEhD`)14k}I!#^l)TpCM8Susf zFy*%pj%rgZ6n(-A@V}tVhtuG!G@=}b!EIIr3g)shAb0w-z_A}jBDeKL8Oiz8%a=ns zQ)lm_5D$Ija#!}c>`yGd!@RB8ELbVG-chPwkW!ITVyx#J<~s(ohiYu9(w!TnXk(un zKnR8AaF(?rRNe3|)G}Z29k+K_zMPj7ErA;65k?A~1C$MXr)kk&qUQHeb=j>+t^Alk zGDQVJ*e51&7dk$@!x6pb7tS^mt1rwGbXPzpE{El{nPgUr>0<`H9bH%97hd23p-2VM zjn!WPCD`W=rg3@Bl#|txaF$@7l;Y^R#)US&dhv^Sl{N&^}yaU z$}a|E$L7(c6dm8)_sv?i{^aW?Ik|s?J`0(;dg*FjQLo?)<+0CW*ikBooD%)o{|e2h5p5>DZpjkSOgVS@hJjPrFijd~uo z5Z$)>Z%2fhg_lGWlq5O(z#XN^M?_^z;~53M$o3OtPt0FypXoWjgbIVMMiW0r(-gJ( zgs7K-ggzH3<|{%uzc%f|Q!NX`uiZ2~bs^$TcwK-u3aploKg%pRJDc2>g-DUN(lu)+ z&SMGJnG7EbU(VAdkwlB=^UjA`Q*y5eznw+qrFq_m8 z)$r&jW;z}UNrl944kE0BlnGesht+_^%@WF*G?T6a4gCIkID5M=VR}Pr9<~j1u3=#b zg;t1fjXtr+=T?@Hzjjfhcx4jBF1wKN&FJ(=P;+l*(i`Td3s&pV^$qZ$H8p9b#6e0q zxEUEE<{PEVDu!PMz|XFj=RN*M`o=Nq*_LXOgFBB~N;HqyQ{7E!!J2{*$Au zZQk(uY7=o2TXs-LI3}hjsrBS6L)*fn0aP7WClQJyqBqBgIWkeNWHubrS_G;6p!_11 zh7j?Uio^+4>YzK@21e8~Qp|9Qv~1Qnt6r7+LJCM!gXyTLOkLbPu;)AUX#KKY*|NtyVaR%HyBi&l(R%HHNb=ECXTg4`EB| zryaorGkb%h)He}V_;nPuYq!Sq*g4X(vMW*iQUz`_I3!Sng9h8fzdu9vR zVLc&>s#7Mq;>%JrRg=H1ChiUd4<>(#0>jz$<;4lln`*Y6B4UeDs@RUmB`0`DYEWt}EGUr{D~s6o;;Fx^M(VLl`#>p8tOL z@s!C~^y}B$wpl?9p{um29zIK*B_1~gQM>0HyJ|Q5pgt8!=I_J?DiLMyv-b|!IzgJr z>@edllTXf`RgmTof6F^JyMrrFG*(j_weh8;Qo~3uC88fXIRjio^__-H1hm6Y51+Ds zWaM?cb87;I*J9FwBzuT#li?R_aQZA{8rkJL`~AnQJK%{ZOZ#rZ~u0pHq|G zy8kW9N_)SocBBS0-Wvr?MM3?@C^f z#5MbLY4**-eiSxXhQ_c}u+I)wi!4uvueKdXO89T#Q=7yumN^ZX2#lx1-*63cZ$&qo zG{7@g+780ftZu`{z{I=mq#ln^&=0P-P(lU3F>Z;sw$n)H1Lg68&=89IMizF~v8(j_{74LQzcEkI0<&Z`8c^8>GmnF+OtmTTp%1DMh|9iAEQ$zKraC|E4LesMy9G zGr)OwD%*w-T0^b`sZ9<#mMl?Z=4W~6t^jbMZ0w?F$}Mnhs~IO_<+O#<^|S0X#Cj|g zrI>?6q@~oTnaO#n(H&5yE&O>GCr#fqD-EV2C|3{ z8kVQ#t1iX%NV`Z~_>32G0NLTjfdSR7y@=>9C)CIPK-kJggZ_|mFG|0dghSVLdYW0< zL0F1Mt@8{Zn@>RJc>mpi6$1)aefe3N3`G$JIh*gfbR*@pZtVF#;$ln!vytQh3}TUX zu=?!g-i}#|os1db)e9HVH>;57t6?fr?QkdJuH_gd_MW#e(ojh4@z^i#mu*0cAacq5 z;k$&t@Nx;MpG-LpV`lD~2 z7)FqcZ@RFqyrHh7c30C_O=nrpy}+m1;zqC8<(0x2x~v8)q3DKX^kTJMZ%MQqH&OEC zris)!{QrS0z`aWmec0Jd=R2rGE#yz$z@`GxQbt#TUw+LB;Xh6NI=OlITgCS724Toc zLA%yVkz&VQi(H4Yu*H37BGz04ndyr3w-5kt>B1mk8!h6yu0ZXM@keWh?7KrHraLtj zVBKJEMfTf<)=r0+CXr<=euf-K*fiL*wd+9m&h~CA`1U7QSaQFTcE~(<%v!z^0sjyY z{r#(%h?M>kihZ5WG8@DHWM^jn_5K;(uN*vFHHD6c8jl3@VqLcgIq54|!!8X(&wEpX{@vsYjuX_dLdh186;1E>ECa6&$+lH!3F_H>`l#}G_k@{3Mj z68ZbC+M(OSBGga3Sb{XixXVvU-YA8x{}r`s^q*^x#uR+8v-dx5jOcwgid-H=z7@hQ z)ZWsAOU?>)!t3HhM=zv5kOkZzdGpZX=}f`yz6!UWS*J^KSUNtWmkZmBK8$J_h`deFR6 zNTNh>)%JsxkD7NV2U5N3FMWi!&Q&nf3QrJ9CmHXho5lTqLPtbr@5l!>8%gQAgKxEZ zT@wcys`Wy|%#>@m=Fcf!N`V`Nm}`Y20hI<(`UcM^?^9?ZYLk+v*fg3u?vY7sKV=eX-dH=G^@$Y3dGy z9Dd>31WvnglKS(}%fkew0F)G! zb&i^e%5bs={=b&#rkglSyGm&=yBY-H;|*H`6V@E44uYr4kGRhx4!1_ywsR`L&Ka^U z^t7z*ccMv0i>sh-G9*z|uw9R}1a)5U?h-5iBWxLlPE-?mFZEx|(=@3+#-IMi6LM2u z1xmBVs1lgts(}qxecHQ}X^zklJT(l}bOgc|`X@sqrn`)2<46{;W8|ZS&t^tsI&f?%7>Y z8n(P$c`~8mOZ~bL3m&}N28wRfFtL{WUxLx5o0uZ&oGK5&o0#>?*Evflg}Xue-BKQ` z>zB2psd;tYVC_S$E0S#|Mmf%9=a@`(cB`Hnooow+AEoO)pi1A%^4zE5a+Wn-cgI4K zsY6{~t*sNBcz)p`g4LZF=y

%S5&M`I9O)gmdJc$yQ#A0{HSiVyg{MZUif_xFq`W znBc?(iFurq1w|y|oAAV`73tbU)Rs7OG$7_;1dNN;K;rw1QX))+z5!l7seuk3V;Mp@ z4oP2dl@T#~{ok4e2hGLnkRN*A)d;EicgC66+%qd4D&7(bMWxDuJRlCGlp&P#xnGAa zVN2!Chn9WyJ7n5zb8+V>U3RQg;OmJeQ_Pxk%`a{e- z-zbq1p|AdQKPFA0{LGLs!S%h29uuKfgE*80rb5;o-lcgd)fcOaU4KnwQu4MZMj6iMu2GVm(5fZ8>@?`{x_B35HA;#lfx zaq%-s>e;eqfQ1VhtUL$S;Wd63EwVg#f7u zMar+QG^->)iNou)10Y_Zj{heGy>T3rIc~ebxlG-!qW#1+RKk;`=o#l0dC{378UKS` zRiNZ8-nmcZTjj#=*_k6)!_%+&KM8q*e){9pT(`Ao3{0imQr5;2&gW|jZztqz&m`5^ zs3UaPu@S8BSx=GOCSv#r)lzx^QX`r6TzZkoCwS=*Sv|OD<HV+LZ`d2~gftN?FmLi$0u-96N23Rv@a3BA!WzwyGHp{Rl zv%(F4O1WCsdJ*QRXoW(|6>5ewMH3!1W_m%3JDJE=A5q}S#flWYj%^I68_n(e6)skC zD9lWzxb|XJt3r=wv#}jr(=$;!P=-F9ObY z^1?34zkq2Qk|<^_r&4l%Sm4VAy$BplOZ_CxHm7}vLr;Ycw6V`6v8^^%>}b#Vs*lXv za;%66R*DZjq0RUm2FsvdS4XOdQ)U!j8<`9WT#BTR`tBsU)aWAau7MyFMS|j~oRU`9 z%qS}^7F<*F9=mL4q_dl#vXvyU^3 z=o*FlO;ry|j=fBN=+}wOqk=nIcM;BppaOB|J@@YAn=Pn?h}DudM3L@240f#W+64k_ zK@vX^x;Y{rnU+ad_V{JgEaAindNov}7LYV6)s=~u!%%_|@Tc5^5e&Nw#^f}%Mn=ca zy1SG;Zb3fC>g!}8^n+3JxSx@(ZtGJjVxRjRYlHp7PE1FSJvP2=Ww7(p6StKGJClx+1^mRxu2gh8{L&#MA0CJ-t>7w(!ydbsEsz@Qq~-oK46rvJ zWq`6)#%w_ZoS;rBhlrOz`h4yBsg{DO1DV?%&i(nallA)XC4z9IMzR!hh<@I@y+v6& z2Vo?`Jt>#zs%MG68xgJ}nQDaSu2$;(Ig`?N%QxY2b(DoMV=9OHO(^{>I{IPI#-9<1 zNSX2OZd_x~O#*1W4$Iy3Kf9$V-c_?EI!(s={E2+um$TlJdE=V-j>~FaJzm=G`!VwJ z!t;adOS_o3YDE2aawfeCqUzsUm%OoWan;%mOxD^!pV~s?#Jtfd1+c5uzw7sQ!8MIL zXpUKcb=Q<};?1@GX$?C7gXpHIjjU zA zNQH5}g_xt}_i^!D{+}tXW$VcrJoPg#+nHeoV$fZkLbp>!CK4x~iqS0KpD7tS0w_u; zsJyAp2tzbI9yVuctb8(HeW{MT`H?pBc^y8|h|Rzt49Vcc4{_qKa$hkJA+UV!?pf^V zNS<*(><|ywxu%$EA&NX<0DRFmuW=SoU>o^yhhRo`>$d#8SIx`SV@)m-hco8bIudgq_d73Q;0d%rP!(Xrhzb4`({~@j1cm~3A!G< zahrZe=2HA6iB}?~HBUpW6J-&fa=33H+i^uI{8pSybIZ&8+*tFf(O_41Y!T95i5VnC z%H?)*X7Gj*a3z$2k{`ql>kz{e6uq~gOYEG_ zAp(cjb_JIRi5bS3pgopr!!%}ci_eI_ZCrV1-`-Zs5BNRN^BLM@tR|BmgE{K6KhS^3 zlHBoC?%-y?Cb+90KIYg{uG~tlXrQgAE=lkW5$nHqvy%RsRl};= zRFo9s#LtB&KxHMLotv_dcM?T`$1mzdv~Lxns&`Acq@-%FZ9V8?GX2cf4m(QGtA^kE zQrAy_RTV|Cec;xu{!N}tUSZLsB10SAqW@v!3x->;@YaARL{JdWB(saIUjJvp?CADSr`um-F)+s4# zL`M5R=OAl__su{DhT3_aD+G6HV~Eb(+%P6@$p1-x$T@*XQM6idj(MHceSG&8`bUHH z=VE3v+}6f@7oyb~FrqCM$0yhDGdqI2GT3wPkkuQ{L3L|xZ|2)C4Vife6F6^lgS=9* zQOkYvjI7G_&)Dj{ifT|eY2N)h=(m`8_^p82x0gp~+K8H(!GA+Kxw1n|y)-Z?!1mJ9 zs`+PldUf-{Hr$k+>^(~7KE}h1p)u}BE}!epy$un6FKw3)UdcLoz7O$Rn!U|}uEiaC zFh6wSlNiG*U*zfij3eTHd2KF^F6HBMo%V-(r&gMA%(nuZlY-p11Gc+4oVLZ@j3LhbMxF0&%wz8X)^y|)|YEVwP*F3?4G3`?|;6V6#jIpdg<$=3;CY^ zb1yZ8nD`@3cZ$n)eD_0Q<~uG-NpIYKjrkdUf9B2OfZ2r=3(2lDqXYX^H`YI-{EK`+ z`8U+%ldJ477S%5$tB=TB!mwV<`Ci?){f|AaRrcLGAp6hvvzB=~lvk^7^a$%#cZupt>Lwjwv#?D{w`OQE6< zzOcfRg}xmex%!|^I_b^qnpVpb7N_OECu?WC&qf3R%trf;8d8((q+s-Z%zQ)Ff2R8i zqfTE6^*#P*q~yEXznVzO?u!}%$wn(V5&0iJc{$AMqQi^RlMOl@UhZAg{C9h6aPDft z5v`E{^3(a_I^Iunzn_T~df!=f@Zh#4_Y)u4t8>TwKA&}R0(-!5; z*b`8J%X%y9RdKb!>=?Hvcr4eDpOK-IRHl}n>F1!7C`7Tn<`ZCPws6{~?$IT;H=p0X zUbuF`b~xQuvSwlLI*WM`zGe%zOajpnaiaN09hSL{@k z(JfV3(3mBDg_cv^Wal%h!u*=>kTSk$>99PBN_P$rzmQm2{`ef`X6y^YZE*=vA>9a< z$DJVs8yUr#L7`GM=o^|8&~xCssf4?_$P-k>a|w^WNDrUqqatEac}Yh5ocz{~yQUeo zTNq~#@m)P0+#sRVHK3hbG-8Y5#HwEOF~P3N*_-%$>z2JzTdbT z-DlqDkuFHpPoeBkKQjC{wSLDY*m`H)UsF?-5I9iq0%aouD6vS6GZCwDK>nL1p+wY8irO*->vxcz$huT4}8~X z+aG5_;DMm_LbNarNZDvuL(lPcJa6d#AKxgv56X6QpfeEw*c$);?||Gl?$X0kcXY5* QSO0%%WoBpk@(?ZRez+(a-4U=;!R} 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 Date: Wed, 4 Jun 2025 18:57:55 +0200 Subject: [PATCH 18/25] feat: Support 12 words mnemonic in backup flow --- .../ui/settings/backups/ShowMnemonicScreen.kt | 61 ++++++++++++------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index 692bb6fa2..cbccc4e14 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -68,15 +68,25 @@ fun ShowMnemonicScreen( val app = appViewModel ?: return val context = LocalContext.current val clipboard = LocalClipboardManager.current + val scope = rememberCoroutineScope() - var mnemonic by remember { mutableStateOf("") } + var mnemonic by remember { mutableStateOf(List(24) { "secret" }.joinToString(" ")) } var bip39Passphrase by remember { mutableStateOf("") } var showMnemonic by remember { mutableStateOf(false) } - val scope = rememberCoroutineScope() LaunchedEffect(Unit) { - mnemonic = app.loadMnemonic()!! - bip39Passphrase = app.loadBip39Passphrase() + try { + mnemonic = app.loadMnemonic()!! + bip39Passphrase = app.loadBip39Passphrase() + } catch (e: Throwable) { + Logger.error("Error loading mnemonic", e) + app.toast( + type = Toast.ToastType.WARNING, + title = context.getString(R.string.security__mnemonic_error), + description = context.getString(R.string.security__mnemonic_error_description), + ) + return@LaunchedEffect + } } DisposableEffect(Unit) { @@ -91,19 +101,8 @@ fun ShowMnemonicScreen( showMnemonic = showMnemonic, onRevealClick = { scope.launch { - try { - delay(200) - mnemonic = app.loadMnemonic()!! - bip39Passphrase = app.loadBip39Passphrase() - showMnemonic = true - } catch (e: Throwable) { - Logger.error("Failed to load mnemonic", e) - app.toast( - type = Toast.ToastType.WARNING, - title = context.getString(R.string.security__mnemonic_error), - description = context.getString(R.string.security__mnemonic_error_description), - ) - } + delay(200) + showMnemonic = true } }, onCopyClick = { @@ -135,10 +134,12 @@ private fun ShowMnemonicContent( label = "buttonAlpha" ) - val mnemonicWords = if (mnemonic.isNotEmpty()) mnemonic.split(" ") else emptyList() val scrollState = rememberScrollState() val scope = rememberCoroutineScope() + val mnemonicWords = if (mnemonic.isNotEmpty()) mnemonic.split(" ") else emptyList() + val wordsCount = mnemonicWords.size + // Scroll to bottom when mnemonic is revealed LaunchedEffect(showMnemonic) { if (showMnemonic) { @@ -171,9 +172,9 @@ private fun ShowMnemonicContent( ) { isRevealed -> BodyM( text = when (isRevealed) { - true -> stringResource(R.string.security__mnemonic_write) - else -> stringResource(R.string.security__mnemonic_use) - }.replace("{length}", "${mnemonicWords.size}"), + true -> stringResource(R.string.security__mnemonic_write).replace("{length}", "$wordsCount") + else -> stringResource(R.string.security__mnemonic_use).replace("12", "$wordsCount") + }, color = Colors.White64, ) } @@ -242,7 +243,7 @@ private fun MnemonicWordsGrid( showMnemonic: Boolean, blurRadius: Float, ) { - val placeholderWords = remember { List(24) { "secret" } } + val placeholderWords = remember(actualWords) { List(actualWords.size) { "secret" } } Box( modifier = Modifier @@ -306,7 +307,7 @@ private fun WordItem( private fun Preview() { AppThemeSurface { ShowMnemonicContent( - mnemonic = List(24) { bip39Words.random() }.joinToString(" "), + mnemonic = List(24) { "word" }.joinToString(" "), showMnemonic = false, onRevealClick = {}, onCopyClick = {}, @@ -328,3 +329,17 @@ private fun PreviewShown() { ) } } + +@Preview +@Composable +private fun Preview12Words() { + AppThemeSurface { + ShowMnemonicContent( + mnemonic = List(12) { bip39Words.random() }.joinToString(" "), + showMnemonic = true, + onRevealClick = {}, + onCopyClick = {}, + onContinueClick = {}, + ) + } +} From 524a5a2f7355e09cab2e3bfff0283595b9c699fa Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Jun 2025 21:33:09 +0200 Subject: [PATCH 19/25] feat: Refactor to use viewModel --- .../settings/backups/BackupNavigationSheet.kt | 98 ++++++------ .../settings/backups/ConfirmMnemonicScreen.kt | 126 ++++++++------- .../backups/ConfirmPassphraseScreen.kt | 40 ++--- .../ui/settings/backups/MetadataScreen.kt | 9 +- .../ui/settings/backups/ShowMnemonicScreen.kt | 72 +++------ .../settings/backups/ShowPassphraseScreen.kt | 9 +- .../to/bitkit/viewmodels/BackupViewModel.kt | 146 ++++++++++++++++++ .../viewmodels/ExternalNodeViewModel.kt | 6 +- 8 files changed, 312 insertions(+), 194 deletions(-) create mode 100644 app/src/main/java/to/bitkit/viewmodels/BackupViewModel.kt diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt index c19f0da02..6e81122cc 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt @@ -4,19 +4,52 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController -import androidx.navigation.toRoute import kotlinx.serialization.Serializable import to.bitkit.ui.components.SheetSize import to.bitkit.ui.utils.composableWithDefaultTransitions +import to.bitkit.viewmodels.BackupContract +import to.bitkit.viewmodels.BackupViewModel @Composable fun BackupNavigationSheet( onDismiss: () -> Unit, + viewModel: BackupViewModel = hiltViewModel(), ) { val navController = rememberNavController() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + DisposableEffect(Unit) { + onDispose { + viewModel.resetState() + } + } + + LaunchedEffect(Unit) { + viewModel.loadMnemonicData() + } + + LaunchedEffect(Unit) { + viewModel.effects.collect { effect -> + when (effect) { + BackupContract.SideEffect.NavigateToShowPassphrase -> navController.navigate(BackupRoute.ShowPassphrase) + BackupContract.SideEffect.NavigateToConfirmMnemonic -> navController.navigate(BackupRoute.ConfirmMnemonic) + BackupContract.SideEffect.NavigateToConfirmPassphrase -> navController.navigate(BackupRoute.ConfirmPassphrase) + BackupContract.SideEffect.NavigateToWarning -> navController.navigate(BackupRoute.Warning) + BackupContract.SideEffect.NavigateToSuccess -> navController.navigate(BackupRoute.Success) + BackupContract.SideEffect.NavigateToMultipleDevices -> navController.navigate(BackupRoute.MultipleDevices) + BackupContract.SideEffect.NavigateToMetadata -> navController.navigate(BackupRoute.Metadata) + BackupContract.SideEffect.DismissSheet -> onDismiss() + } + } + } Column( modifier = Modifier @@ -29,76 +62,55 @@ fun BackupNavigationSheet( ) { composableWithDefaultTransitions { ShowMnemonicScreen( - onContinue = { seed, bip39Passphrase -> - if (bip39Passphrase.isNotEmpty()) { - navController.navigate(BackupRoute.ShowPassphrase(seed, bip39Passphrase)) - } else { - navController.navigate(BackupRoute.ConfirmMnemonic(seed, bip39Passphrase)) - } - }, + uiState = uiState, + onRevealClick = viewModel::onRevealMnemonic, + onContinueClick = viewModel::onShowMnemonicContinue, ) } - composableWithDefaultTransitions { backStackEntry -> - val route = backStackEntry.toRoute() + composableWithDefaultTransitions { ShowPassphraseScreen( - bip39Passphrase = route.bip39Passphrase, - onContinue = { - navController.navigate(BackupRoute.ConfirmMnemonic(route.seed, route.bip39Passphrase)) - }, + uiState = uiState, + onContinue = viewModel::onShowPassphraseContinue, onBack = { navController.popBackStack() }, ) } - composableWithDefaultTransitions { backStackEntry -> - val route = backStackEntry.toRoute() + composableWithDefaultTransitions { ConfirmMnemonicScreen( - seed = route.seed, - onContinue = { - if (route.bip39Passphrase.isNotEmpty()) { - navController.navigate(BackupRoute.ConfirmPassphrase(route.bip39Passphrase)) - } else { - navController.navigate(BackupRoute.Warning) - } - }, + uiState = uiState, + onContinue = viewModel::onConfirmMnemonicContinue, onBack = { navController.popBackStack() }, ) } - composableWithDefaultTransitions { backStackEntry -> - val route = backStackEntry.toRoute() + composableWithDefaultTransitions { ConfirmPassphraseScreen( - bip39Passphrase = route.bip39Passphrase, - onContinue = { - navController.navigate(BackupRoute.Warning) - }, + uiState = uiState, + onPassphraseChange = viewModel::onPassphraseInput, + onContinue = viewModel::onConfirmPassphraseContinue, onBack = { navController.popBackStack() }, ) } composableWithDefaultTransitions { WarningScreen( - onContinue = { - navController.navigate(BackupRoute.Success) - }, + onContinue = viewModel::onWarningContinue, onBack = { navController.popBackStack() }, ) } composableWithDefaultTransitions { SuccessScreen( - onContinue = { - navController.navigate(BackupRoute.MultipleDevices) - }, + onContinue = viewModel::onSuccessContinue, onBack = { navController.popBackStack() }, ) } composableWithDefaultTransitions { MultipleDevicesScreen( - onContinue = { - navController.navigate(BackupRoute.Metadata) - }, + onContinue = viewModel::onMultipleDevicesContinue, onBack = { navController.popBackStack() }, ) } composableWithDefaultTransitions { MetadataScreen( - onDismiss = onDismiss, + uiState = uiState, + onDismiss = viewModel::onMetadataClose, onBack = { navController.popBackStack() }, ) } @@ -111,13 +123,13 @@ object BackupRoute { data object ShowMnemonic @Serializable - data class ShowPassphrase(val seed: List, val bip39Passphrase: String) + data object ShowPassphrase @Serializable - data class ConfirmMnemonic(val seed: List, val bip39Passphrase: String) + data object ConfirmMnemonic @Serializable - data class ConfirmPassphrase(val bip39Passphrase: String) + data object ConfirmPassphrase @Serializable data object Warning diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt index 2133d58aa..c4ac8414e 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -38,61 +37,59 @@ import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.utils.bip39Words +import to.bitkit.viewmodels.BackupContract @OptIn(ExperimentalLayoutApi::class) @Composable fun ConfirmMnemonicScreen( - seed: List, + uiState: BackupContract.UiState, onContinue: () -> Unit, onBack: () -> Unit, ) { - // State to track user selection - var selectedWords by remember { mutableStateOf(arrayOfNulls(seed.size)) } - var pressedStates by remember { mutableStateOf(BooleanArray(seed.size) { false }) } + val originalSeed = remember(uiState.mnemonicString) { + uiState.mnemonicString.split(" ").filter { it.isNotBlank() } + } + val shuffledWords = remember(originalSeed) { + originalSeed.shuffled() + } - // Shuffle the words for selection - val shuffledWords = remember { seed.shuffled() } + var selectedWords by remember { mutableStateOf(arrayOfNulls(originalSeed.size)) } + var pressedStates by remember { mutableStateOf(BooleanArray(shuffledWords.size) { false }) } - DisposableEffect(Unit) { - onDispose { - // Clear selected words from memory - selectedWords = arrayOfNulls(seed.size) - } - } + // Calculate if all words are correct + val isComplete = selectedWords.all { it != null } && + selectedWords.zip(originalSeed).all { (selected, original) -> selected == original } ConfirmMnemonicContent( - originalSeed = seed, + originalSeed = originalSeed, shuffledWords = shuffledWords, selectedWords = selectedWords, pressedStates = pressedStates, + isComplete = isComplete, onWordPress = { word, shuffledIndex -> // Find index of the last filled word - val firstNullIndex = selectedWords.indexOfFirst { it == null } - val lastFilledIndex = if (firstNullIndex == -1) selectedWords.size - 1 else firstNullIndex - 1 - val nextEmptyIndex = if (firstNullIndex == -1) -1 else firstNullIndex + val lastIndex = selectedWords.indexOfFirst { it == null } - 1 + val nextIndex = if (lastIndex == -1) 0 else lastIndex + 1 - // If this word is already pressed/selected - if (pressedStates[shuffledIndex]) { - // Allow deselecting only if it's the last word that was selected - // or if the word at the last position is incorrect - if (lastFilledIndex >= 0) { - val wordAtLastPosition = selectedWords[lastFilledIndex] - val isLastWordIncorrect = wordAtLastPosition != seed[lastFilledIndex] - val isThisTheLastWord = wordAtLastPosition == word + // If the word is correct and pressed, do nothing + if (pressedStates[shuffledIndex] && nextIndex > 0 && originalSeed[lastIndex] == selectedWords[lastIndex]) { + return@ConfirmMnemonicContent + } - if (isThisTheLastWord && (isLastWordIncorrect || firstNullIndex == -1)) { - // Deselect this word - pressedStates = pressedStates.copyOf().apply { this[shuffledIndex] = false } - selectedWords = selectedWords.copyOf().apply { this[lastFilledIndex] = null } - } + // If previous word is incorrect, allow unchecking + if (lastIndex >= 0 && selectedWords[lastIndex] != originalSeed[lastIndex]) { + // Uncheck if we tap on it + if (pressedStates[shuffledIndex] && word == selectedWords[lastIndex]) { + pressedStates = pressedStates.copyOf().apply { this[shuffledIndex] = false } + selectedWords = selectedWords.copyOf().apply { this[lastIndex] = null } } return@ConfirmMnemonicContent } - // If we have space and word is not already pressed, add it - if (nextEmptyIndex >= 0 && nextEmptyIndex < seed.size) { + // Mark word as pressed and add it to the seed + if (nextIndex < originalSeed.size) { pressedStates = pressedStates.copyOf().apply { this[shuffledIndex] = true } - selectedWords = selectedWords.copyOf().apply { this[nextEmptyIndex] = word } + selectedWords = selectedWords.copyOf().apply { this[nextIndex] = word } } }, onContinue = onContinue, @@ -107,14 +104,11 @@ private fun ConfirmMnemonicContent( shuffledWords: List, selectedWords: Array, pressedStates: BooleanArray, + isComplete: Boolean, onWordPress: (String, Int) -> Unit, onContinue: () -> Unit, onBack: () -> Unit, ) { - // Check if all words are correct - val isComplete = selectedWords.all { it != null } && - selectedWords.zip(originalSeed).all { (selected, original) -> selected == original } - val scrollState = rememberScrollState() val scope = rememberCoroutineScope() @@ -148,7 +142,7 @@ private fun ConfirmMnemonicContent( color = Colors.White64, ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(32.dp)) // Shuffled word buttons FlowRow( @@ -159,7 +153,7 @@ private fun ConfirmMnemonicContent( shuffledWords.forEachIndexed { index, word -> PrimaryButton( text = word, - color = if (pressedStates[index]) Colors.White32 else Colors.White16, + color = if (pressedStates.getOrNull(index) == true) Colors.White32 else Colors.White16, fullWidth = false, size = ButtonSize.Small, onClick = { onWordPress(word, index) } @@ -167,7 +161,7 @@ private fun ConfirmMnemonicContent( } } - Spacer(modifier = Modifier.height(22.dp)) + Spacer(modifier = Modifier.height(32.dp)) // Selected words display (2 columns) Row( @@ -182,7 +176,7 @@ private fun ConfirmMnemonicContent( SelectedWordItem( number = index + 1, word = word ?: "", - correct = word == originalSeed[index] + isCorrect = word == originalSeed.getOrNull(index), ) } } @@ -195,12 +189,13 @@ private fun ConfirmMnemonicContent( SelectedWordItem( number = actualIndex + 1, word = word ?: "", - correct = word == originalSeed[actualIndex] + isCorrect = word == originalSeed.getOrNull(actualIndex), ) } } } + Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.height(24.dp)) PrimaryButton( @@ -218,14 +213,14 @@ private fun ConfirmMnemonicContent( private fun SelectedWordItem( number: Int, word: String, - correct: Boolean, + isCorrect: Boolean, ) { Row { BodyMSB(text = "$number.", color = Colors.White64) Spacer(modifier = Modifier.width(4.dp)) BodyMSB( text = if (word.isEmpty()) "" else word, - color = if (word.isEmpty()) Colors.White64 else if (correct) Colors.Green else Colors.Red + color = if (word.isEmpty()) Colors.White64 else if (isCorrect) Colors.Green else Colors.Red ) } } @@ -233,13 +228,15 @@ private fun SelectedWordItem( @Preview @Composable private fun Preview() { + val testWords = bip39Words.take(24) + AppThemeSurface { - val testSeed = listOf("abandon", "ability", "able", "about", "above", "absent") ConfirmMnemonicContent( - originalSeed = testSeed, - shuffledWords = testSeed.shuffled(), - selectedWords = arrayOfNulls(testSeed.size), - pressedStates = BooleanArray(testSeed.size) { false }, + originalSeed = testWords, + shuffledWords = testWords.shuffled(), + selectedWords = arrayOfNulls(24), + pressedStates = BooleanArray(24) { false }, + isComplete = false, onWordPress = { _, _ -> }, onContinue = {}, onBack = {}, @@ -250,13 +247,34 @@ private fun Preview() { @Preview @Composable private fun Preview2() { + val testWords = bip39Words.take(24) + + AppThemeSurface { + ConfirmMnemonicContent( + originalSeed = testWords, + shuffledWords = testWords.shuffled(), + selectedWords = testWords.take(12).toTypedArray() + arrayOfNulls(12), + pressedStates = BooleanArray(24) { it < 12 }, + isComplete = false, + onWordPress = { _, _ -> }, + onContinue = {}, + onBack = {}, + ) + } +} + +@Preview +@Composable +private fun Preview12Words() { + val testWords = bip39Words.take(12) + AppThemeSurface { - val testSeed = List(24) { bip39Words.random() } ConfirmMnemonicContent( - originalSeed = testSeed, - shuffledWords = testSeed.shuffled(), - selectedWords = testSeed.take(12).toTypedArray() + arrayOfNulls(12), - pressedStates = BooleanArray(testSeed.size) { it < 12 }, + originalSeed = testWords, + shuffledWords = testWords.shuffled(), + selectedWords = testWords.take(6).toTypedArray() + arrayOfNulls(6), + pressedStates = BooleanArray(6) { it < 6 }, + isComplete = false, onWordPress = { _, _ -> }, onContinue = {}, onBack = {}, diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt index a8221be6e..087c85715 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt @@ -9,15 +9,9 @@ import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource @@ -33,26 +27,21 @@ import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.viewmodels.BackupContract @Composable fun ConfirmPassphraseScreen( - bip39Passphrase: String, + uiState: BackupContract.UiState, + onPassphraseChange: (String) -> Unit, onContinue: () -> Unit, onBack: () -> Unit, ) { - var enteredPassphrase by remember { mutableStateOf("") } val keyboardController = LocalSoftwareKeyboardController.current - DisposableEffect(Unit) { - onDispose { - enteredPassphrase = "" // Clear passphrase from memory - } - } - ConfirmPassphraseContent( - enteredPassphrase = enteredPassphrase, - originalPassphrase = bip39Passphrase, - onPassphraseChange = { enteredPassphrase = it }, + enteredPassphrase = uiState.enteredPassphrase, + isValid = uiState.isPassphraseValid, + onPassphraseChange = onPassphraseChange, onContinue = { keyboardController?.hide() onContinue() @@ -64,14 +53,11 @@ fun ConfirmPassphraseScreen( @Composable private fun ConfirmPassphraseContent( enteredPassphrase: String, - originalPassphrase: String, + isValid: Boolean, onPassphraseChange: (String) -> Unit, onContinue: () -> Unit, onBack: () -> Unit, ) { - val isValid = enteredPassphrase == originalPassphrase - val keyboardController = LocalSoftwareKeyboardController.current - Column( modifier = Modifier .fillMaxSize() @@ -105,14 +91,6 @@ private fun ConfirmPassphraseContent( imeAction = ImeAction.Done, autoCorrectEnabled = false ), - keyboardActions = KeyboardActions( - onDone = { - if (isValid) { - keyboardController?.hide() - onContinue() - } - } - ), modifier = Modifier.fillMaxWidth() ) @@ -135,7 +113,7 @@ private fun Preview() { AppThemeSurface { ConfirmPassphraseContent( enteredPassphrase = "", - originalPassphrase = "test123", + isValid = false, onPassphraseChange = {}, onContinue = {}, onBack = {}, @@ -149,7 +127,7 @@ private fun Preview2() { AppThemeSurface { ConfirmPassphraseContent( enteredPassphrase = "test123", - originalPassphrase = "test123", + isValid = true, onPassphraseChange = {}, onContinue = {}, onBack = {}, diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt index 60aee9f68..0d9a14b54 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt @@ -26,20 +26,19 @@ import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withBold +import to.bitkit.viewmodels.BackupContract import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @Composable fun MetadataScreen( + uiState: BackupContract.UiState, onDismiss: () -> Unit, onBack: () -> Unit, ) { - // TODO get last backup time from actual state - val lastBackupTimeMs = System.currentTimeMillis() - MetadataContent( - lastBackupTimeMs = lastBackupTimeMs, + lastBackupTimeMs = uiState.lastBackupTimeMs, onDismiss = onDismiss, onBack = onBack, ) @@ -51,7 +50,7 @@ private fun MetadataContent( onDismiss: () -> Unit, onBack: () -> Unit, ) { - val latestBackupTime = remember { + val latestBackupTime = remember(lastBackupTimeMs) { val formatter = SimpleDateFormat("MMMM d, yyyy 'at' h:mm a", Locale.getDefault()) formatter.format(Date(lastBackupTimeMs)) } diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index cbccc4e14..c91d4354a 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -25,13 +25,10 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.BlurredEdgeTreatment @@ -39,7 +36,6 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview @@ -47,8 +43,6 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import kotlinx.coroutines.launch import to.bitkit.R -import to.bitkit.models.Toast -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.BodyS @@ -58,65 +52,36 @@ import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent -import to.bitkit.utils.Logger import to.bitkit.utils.bip39Words +import to.bitkit.viewmodels.BackupContract @Composable fun ShowMnemonicScreen( - onContinue: (seed: List, bip39Passphrase: String) -> Unit, + uiState: BackupContract.UiState, + onRevealClick: () -> Unit, + onContinueClick: () -> Unit, ) { - val app = appViewModel ?: return - val context = LocalContext.current val clipboard = LocalClipboardManager.current - val scope = rememberCoroutineScope() - - var mnemonic by remember { mutableStateOf(List(24) { "secret" }.joinToString(" ")) } - var bip39Passphrase by remember { mutableStateOf("") } - var showMnemonic by remember { mutableStateOf(false) } - - LaunchedEffect(Unit) { - try { - mnemonic = app.loadMnemonic()!! - bip39Passphrase = app.loadBip39Passphrase() - } catch (e: Throwable) { - Logger.error("Error loading mnemonic", e) - app.toast( - type = Toast.ToastType.WARNING, - title = context.getString(R.string.security__mnemonic_error), - description = context.getString(R.string.security__mnemonic_error_description), - ) - return@LaunchedEffect - } - } - - DisposableEffect(Unit) { - onDispose { - mnemonic = "" - bip39Passphrase = "" - } + val mnemonicWords = remember(uiState.mnemonicString) { + uiState.mnemonicString.split(" ").filter { it.isNotBlank() } } ShowMnemonicContent( - mnemonic = mnemonic, - showMnemonic = showMnemonic, - onRevealClick = { - scope.launch { - delay(200) - showMnemonic = true - } - }, + mnemonic = uiState.mnemonicString, + mnemonicWords = mnemonicWords, + showMnemonic = uiState.showMnemonic, + onRevealClick = onRevealClick, onCopyClick = { - clipboard.setText(AnnotatedString(mnemonic)) - }, - onContinueClick = { - onContinue(mnemonic.split(" "), bip39Passphrase) + clipboard.setText(AnnotatedString(uiState.mnemonicString)) }, + onContinueClick = onContinueClick, ) } @Composable private fun ShowMnemonicContent( mnemonic: String, + mnemonicWords: List, showMnemonic: Boolean, onRevealClick: () -> Unit, onCopyClick: () -> Unit, @@ -137,7 +102,6 @@ private fun ShowMnemonicContent( val scrollState = rememberScrollState() val scope = rememberCoroutineScope() - val mnemonicWords = if (mnemonic.isNotEmpty()) mnemonic.split(" ") else emptyList() val wordsCount = mnemonicWords.size // Scroll to bottom when mnemonic is revealed @@ -218,6 +182,7 @@ private fun ShowMnemonicContent( } Spacer(modifier = Modifier.height(32.dp)) + BodyS( text = stringResource(R.string.security__mnemonic_never_share).withAccent(accentColor = Colors.Brand), color = Colors.White64, @@ -307,7 +272,8 @@ private fun WordItem( private fun Preview() { AppThemeSurface { ShowMnemonicContent( - mnemonic = List(24) { "word" }.joinToString(" "), + mnemonic = bip39Words.take(24).joinToString(" "), + mnemonicWords = bip39Words.take(24), showMnemonic = false, onRevealClick = {}, onCopyClick = {}, @@ -321,7 +287,8 @@ private fun Preview() { private fun PreviewShown() { AppThemeSurface { ShowMnemonicContent( - mnemonic = List(24) { bip39Words.random() }.joinToString(" "), + mnemonic = bip39Words.take(24).joinToString(" "), + mnemonicWords = bip39Words.take(24), showMnemonic = true, onRevealClick = {}, onCopyClick = {}, @@ -335,7 +302,8 @@ private fun PreviewShown() { private fun Preview12Words() { AppThemeSurface { ShowMnemonicContent( - mnemonic = List(12) { bip39Words.random() }.joinToString(" "), + mnemonic = bip39Words.take(12).joinToString(" "), + mnemonicWords = bip39Words.take(12), showMnemonic = true, onRevealClick = {}, onCopyClick = {}, diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt index 51c96e290..a622847ba 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt @@ -9,15 +9,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R @@ -30,15 +26,16 @@ import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent +import to.bitkit.viewmodels.BackupContract @Composable fun ShowPassphraseScreen( - bip39Passphrase: String, + uiState: BackupContract.UiState, onContinue: () -> Unit, onBack: () -> Unit, ) { ShowPassphraseContent( - bip39Passphrase = bip39Passphrase, + bip39Passphrase = uiState.bip39Passphrase, onContinue = onContinue, onBack = onBack, ) diff --git a/app/src/main/java/to/bitkit/viewmodels/BackupViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/BackupViewModel.kt new file mode 100644 index 000000000..771dbe69b --- /dev/null +++ b/app/src/main/java/to/bitkit/viewmodels/BackupViewModel.kt @@ -0,0 +1,146 @@ +package to.bitkit.viewmodels + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import to.bitkit.R +import to.bitkit.data.keychain.Keychain +import to.bitkit.models.Toast +import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.utils.Logger +import to.bitkit.viewmodels.BackupContract.SideEffect +import to.bitkit.viewmodels.BackupContract.UiState +import javax.inject.Inject + +@HiltViewModel +class BackupViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val keychain: Keychain, +) : ViewModel() { + + private val _uiState = MutableStateFlow(UiState()) + val uiState = _uiState.asStateFlow() + + private val _effects = MutableSharedFlow(extraBufferCapacity = 1) + val effects = _effects.asSharedFlow() + + private fun setEffect(effect: SideEffect) = viewModelScope.launch { _effects.emit(effect) } + + fun loadMnemonicData() { + viewModelScope.launch { + try { + val mnemonic = keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name)!! // NPE handled with UI toast + val bip39Passphrase = keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name) ?: "" + + _uiState.update { + it.copy( + mnemonicString = mnemonic, + bip39Passphrase = bip39Passphrase, + ) + } + } catch (e: Throwable) { + Logger.error("Error loading mnemonic", e) + ToastEventBus.send( + type = Toast.ToastType.WARNING, + title = context.getString(R.string.security__mnemonic_error), + description = context.getString(R.string.security__mnemonic_error_description), + ) + } + } + } + + fun onRevealMnemonic() { + viewModelScope.launch { + delay(200) // Small delay for better UX + _uiState.update { it.copy(showMnemonic = true) } + } + } + + fun onShowMnemonicContinue() { + val state = _uiState.value + if (state.bip39Passphrase.isNotEmpty()) { + setEffect(SideEffect.NavigateToShowPassphrase) + } else { + setEffect(SideEffect.NavigateToConfirmMnemonic) + } + } + + fun onShowPassphraseContinue() { + setEffect(SideEffect.NavigateToConfirmMnemonic) + } + + fun onConfirmMnemonicContinue() { + val state = _uiState.value + if (state.bip39Passphrase.isNotEmpty()) { + setEffect(SideEffect.NavigateToConfirmPassphrase) + } else { + setEffect(SideEffect.NavigateToWarning) + } + } + + fun onPassphraseInput(passphrase: String) { + _uiState.update { it.copy(enteredPassphrase = passphrase) } + } + + fun onConfirmPassphraseContinue() { + setEffect(SideEffect.NavigateToWarning) + } + + fun onWarningContinue() { + setEffect(SideEffect.NavigateToSuccess) + } + + fun onSuccessContinue() { + // TODO: mark backup as verified to hide suggestion card + setEffect(SideEffect.NavigateToMultipleDevices) + } + + fun onMultipleDevicesContinue() { + // TODO: get from actual repository state + val lastBackupTimeMs = System.currentTimeMillis() + _uiState.update { + it.copy(lastBackupTimeMs = lastBackupTimeMs) + } + setEffect(SideEffect.NavigateToMetadata) + } + + fun onMetadataClose() { + setEffect(SideEffect.DismissSheet) + } + + fun resetState() { + _uiState.update { UiState() } + } +} + +interface BackupContract { + data class UiState( + val mnemonicString: String = "", + val bip39Passphrase: String = "", + val showMnemonic: Boolean = false, + val enteredPassphrase: String = "", + val lastBackupTimeMs: Long = System.currentTimeMillis(), + ) { + val isPassphraseValid get() = enteredPassphrase == bip39Passphrase + } + + sealed interface SideEffect { + data object NavigateToShowPassphrase : SideEffect + data object NavigateToConfirmMnemonic : SideEffect + data object NavigateToConfirmPassphrase : SideEffect + data object NavigateToWarning : SideEffect + data object NavigateToSuccess : SideEffect + data object NavigateToMultipleDevices : SideEffect + data object NavigateToMetadata : SideEffect + data object DismissSheet : SideEffect + } +} diff --git a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt index 5e1c1afe6..f9beb9391 100644 --- a/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/ExternalNodeViewModel.kt @@ -147,8 +147,8 @@ interface ExternalNodeContract { val localBalance: Long = 0, ) - sealed class SideEffect { - data object ConnectionSuccess : SideEffect() - data object ConfirmSuccess : SideEffect() + sealed interface SideEffect { + data object ConnectionSuccess : SideEffect + data object ConfirmSuccess : SideEffect } } From 668a046220d1eecb063ccc584bdf5a45d55b491c Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Wed, 4 Jun 2025 21:46:49 +0200 Subject: [PATCH 20/25] fix: show mnemonic on load jitter --- .../ui/settings/backups/ConfirmMnemonicScreen.kt | 4 ++-- .../ui/settings/backups/ConfirmPassphraseScreen.kt | 2 +- .../bitkit/ui/settings/backups/ShowMnemonicScreen.kt | 8 ++++---- .../main/java/to/bitkit/viewmodels/AppViewModel.kt | 8 -------- .../java/to/bitkit/viewmodels/BackupViewModel.kt | 12 +++++++----- 5 files changed, 14 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt index c4ac8414e..fdb2e9831 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt @@ -46,8 +46,8 @@ fun ConfirmMnemonicScreen( onContinue: () -> Unit, onBack: () -> Unit, ) { - val originalSeed = remember(uiState.mnemonicString) { - uiState.mnemonicString.split(" ").filter { it.isNotBlank() } + val originalSeed = remember(uiState.bip39Mnemonic) { + uiState.bip39Mnemonic.split(" ").filter { it.isNotBlank() } } val shuffledWords = remember(originalSeed) { originalSeed.shuffled() diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt index 087c85715..2539f89a6 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt @@ -40,7 +40,7 @@ fun ConfirmPassphraseScreen( ConfirmPassphraseContent( enteredPassphrase = uiState.enteredPassphrase, - isValid = uiState.isPassphraseValid, + isValid = uiState.enteredPassphrase == uiState.bip39Passphrase, onPassphraseChange = onPassphraseChange, onContinue = { keyboardController?.hide() diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index c91d4354a..8bdf591c3 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -62,17 +62,17 @@ fun ShowMnemonicScreen( onContinueClick: () -> Unit, ) { val clipboard = LocalClipboardManager.current - val mnemonicWords = remember(uiState.mnemonicString) { - uiState.mnemonicString.split(" ").filter { it.isNotBlank() } + val mnemonicWords = remember(uiState.bip39Mnemonic) { + uiState.bip39Mnemonic.split(" ").filter { it.isNotBlank() } } ShowMnemonicContent( - mnemonic = uiState.mnemonicString, + mnemonic = uiState.bip39Mnemonic, mnemonicWords = mnemonicWords, showMnemonic = uiState.showMnemonic, onRevealClick = onRevealClick, onCopyClick = { - clipboard.setText(AnnotatedString(uiState.mnemonicString)) + clipboard.setText(AnnotatedString(uiState.bip39Mnemonic)) }, onContinueClick = onContinueClick, ) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index ae0643180..57f21d83b 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -780,14 +780,6 @@ class AppViewModel @Inject constructor( } // endregion - fun loadMnemonic(): String? { - return keychain.loadString(Keychain.Key.BIP39_MNEMONIC.name) - } - - fun loadBip39Passphrase(): String { - return keychain.loadString(Keychain.Key.BIP39_PASSPHRASE.name) ?: "" - } - // region security fun resetIsAuthenticatedState() { viewModelScope.launch { diff --git a/app/src/main/java/to/bitkit/viewmodels/BackupViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/BackupViewModel.kt index 771dbe69b..cbc899a00 100644 --- a/app/src/main/java/to/bitkit/viewmodels/BackupViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/BackupViewModel.kt @@ -43,7 +43,7 @@ class BackupViewModel @Inject constructor( _uiState.update { it.copy( - mnemonicString = mnemonic, + bip39Mnemonic = mnemonic, bip39Passphrase = bip39Passphrase, ) } @@ -123,15 +123,17 @@ class BackupViewModel @Inject constructor( } interface BackupContract { + companion object { + private val PLACEHOLDER_MNEMONIC = List(24) { "secret" }.joinToString(" ") + } + data class UiState( - val mnemonicString: String = "", + val bip39Mnemonic: String = PLACEHOLDER_MNEMONIC, val bip39Passphrase: String = "", val showMnemonic: Boolean = false, val enteredPassphrase: String = "", val lastBackupTimeMs: Long = System.currentTimeMillis(), - ) { - val isPassphraseValid get() = enteredPassphrase == bip39Passphrase - } + ) sealed interface SideEffect { data object NavigateToShowPassphrase : SideEffect From 32216d66580a0847cdbfa058b5638c6314bf5ca7 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 5 Jun 2025 11:54:03 +0200 Subject: [PATCH 21/25] feat: Hide home backup card when flow is done --- .../main/java/to/bitkit/data/SettingsStore.kt | 1 + .../bitkit/ui/screens/wallets/HomeViewModel.kt | 16 ++++++++-------- .../java/to/bitkit/viewmodels/BackupViewModel.kt | 8 ++++++-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/to/bitkit/data/SettingsStore.kt b/app/src/main/java/to/bitkit/data/SettingsStore.kt index 23acf8bc6..d8cb30a78 100644 --- a/app/src/main/java/to/bitkit/data/SettingsStore.kt +++ b/app/src/main/java/to/bitkit/data/SettingsStore.kt @@ -81,4 +81,5 @@ data class SettingsData( val hideBalanceOnOpen: Boolean = false, val enableAutoReadClipboard: Boolean = false, val enableSendAmountWarning: Boolean = false, + val backupVerified: Boolean = false, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt index d1e30aada..5ea769882 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/HomeViewModel.kt @@ -139,13 +139,13 @@ class HomeViewModel @Inject constructor( appStorage.removedSuggestionsFlow.map { stringList -> stringList.mapNotNull { it.toSuggestionOrNull() } }, - settingsStore.data.map { it.isPinEnabled }, - ) { balanceState, removedList, isPinEnabled -> + settingsStore.data, + ) { balanceState, removedList, settings -> val baseSuggestions = when { balanceState.totalLightningSats > 0uL -> { // With Lightning listOfNotNull( - Suggestion.BACK_UP, - Suggestion.SECURE.takeIf { !isPinEnabled }, + Suggestion.BACK_UP.takeIf { !settings.backupVerified }, + Suggestion.SECURE.takeIf { !settings.isPinEnabled }, Suggestion.BUY, Suggestion.SUPPORT, Suggestion.INVITE, @@ -157,9 +157,9 @@ class HomeViewModel @Inject constructor( balanceState.totalOnchainSats > 0uL -> { // Only on chain balance listOfNotNull( - Suggestion.BACK_UP, + Suggestion.BACK_UP.takeIf { !settings.backupVerified }, Suggestion.SPEND, - Suggestion.SECURE.takeIf { !isPinEnabled }, + Suggestion.SECURE.takeIf { !settings.isPinEnabled }, Suggestion.BUY, Suggestion.SUPPORT, Suggestion.INVITE, @@ -172,8 +172,8 @@ class HomeViewModel @Inject constructor( listOfNotNull( Suggestion.BUY, Suggestion.SPEND, - Suggestion.BACK_UP, - Suggestion.SECURE.takeIf { !isPinEnabled }, + Suggestion.BACK_UP.takeIf { !settings.backupVerified }, + Suggestion.SECURE.takeIf { !settings.isPinEnabled }, Suggestion.SUPPORT, Suggestion.INVITE, Suggestion.PROFILE, diff --git a/app/src/main/java/to/bitkit/viewmodels/BackupViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/BackupViewModel.kt index cbc899a00..7f69bc8ed 100644 --- a/app/src/main/java/to/bitkit/viewmodels/BackupViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/BackupViewModel.kt @@ -13,6 +13,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.R +import to.bitkit.data.SettingsStore import to.bitkit.data.keychain.Keychain import to.bitkit.models.Toast import to.bitkit.ui.shared.toast.ToastEventBus @@ -24,6 +25,7 @@ import javax.inject.Inject @HiltViewModel class BackupViewModel @Inject constructor( @ApplicationContext private val context: Context, + private val settingsStore: SettingsStore, private val keychain: Keychain, ) : ViewModel() { @@ -100,8 +102,10 @@ class BackupViewModel @Inject constructor( } fun onSuccessContinue() { - // TODO: mark backup as verified to hide suggestion card - setEffect(SideEffect.NavigateToMultipleDevices) + viewModelScope.launch { + settingsStore.update { it.copy(backupVerified = true) } + setEffect(SideEffect.NavigateToMultipleDevices) + } } fun onMultipleDevicesContinue() { From d35e92b069188f44499d765273cbc95abdadd681 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 5 Jun 2025 12:03:21 +0200 Subject: [PATCH 22/25] feat: Add test tags --- .../ui/settings/backups/BackupNavigationSheet.kt | 2 ++ .../ui/settings/backups/ConfirmMnemonicScreen.kt | 10 ++++++++-- .../ui/settings/backups/ConfirmPassphraseScreen.kt | 7 ++++++- .../to/bitkit/ui/settings/backups/MetadataScreen.kt | 4 ++++ .../ui/settings/backups/MultipleDevicesScreen.kt | 3 +++ .../bitkit/ui/settings/backups/ShowMnemonicScreen.kt | 8 +++++++- .../bitkit/ui/settings/backups/ShowPassphraseScreen.kt | 4 ++++ .../to/bitkit/ui/settings/backups/SuccessScreen.kt | 3 +++ .../to/bitkit/ui/settings/backups/WarningScreen.kt | 4 ++++ 9 files changed, 41 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt index 6e81122cc..12aa27726 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavigationSheet.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost @@ -55,6 +56,7 @@ fun BackupNavigationSheet( modifier = Modifier .fillMaxWidth() .fillMaxHeight(SheetSize.LARGE) + .testTag("backup_navigation_sheet") ) { NavHost( navController = navController, diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt index fdb2e9831..31817dee8 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -127,6 +128,7 @@ private fun ConfirmMnemonicContent( .fillMaxSize() .gradientBackground() .navigationBarsPadding() + .testTag("backup_confirm_mnemonic_screen") ) { SheetTopBar(stringResource(R.string.security__mnemonic_confirm), onBack = onBack) Spacer(modifier = Modifier.height(16.dp)) @@ -148,7 +150,9 @@ private fun ConfirmMnemonicContent( FlowRow( horizontalArrangement = Arrangement.spacedBy(4.dp), verticalArrangement = Arrangement.spacedBy(5.dp), - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .testTag("backup_shuffled_words_grid") ) { shuffledWords.forEachIndexed { index, word -> PrimaryButton( @@ -156,7 +160,8 @@ private fun ConfirmMnemonicContent( color = if (pressedStates.getOrNull(index) == true) Colors.White32 else Colors.White16, fullWidth = false, size = ButtonSize.Small, - onClick = { onWordPress(word, index) } + onClick = { onWordPress(word, index) }, + modifier = Modifier.testTag("backup_shuffled_word_button_$index") ) } } @@ -202,6 +207,7 @@ private fun ConfirmMnemonicContent( text = stringResource(R.string.common__continue), onClick = onContinue, enabled = isComplete, + modifier = Modifier.testTag("backup_confirm_mnemonic_continue_button") ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt index 2539f89a6..9b3f17982 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmPassphraseScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization @@ -64,6 +65,7 @@ private fun ConfirmPassphraseContent( .gradientBackground() .navigationBarsPadding() .imePadding() + .testTag("backup_confirm_passphrase_screen") ) { SheetTopBar(stringResource(R.string.security__pass_confirm), onBack = onBack) Spacer(modifier = Modifier.height(16.dp)) @@ -91,7 +93,9 @@ private fun ConfirmPassphraseContent( imeAction = ImeAction.Done, autoCorrectEnabled = false ), - modifier = Modifier.fillMaxWidth() + modifier = Modifier + .fillMaxWidth() + .testTag("backup_passphrase_input") ) Spacer(modifier = Modifier.weight(1f)) @@ -100,6 +104,7 @@ private fun ConfirmPassphraseContent( text = stringResource(R.string.common__continue), onClick = onContinue, enabled = isValid, + modifier = Modifier.testTag("backup_confirm_passphrase_continue_button") ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt index 0d9a14b54..a8c3d5c69 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/MetadataScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -60,6 +61,7 @@ private fun MetadataContent( .fillMaxSize() .gradientBackground() .navigationBarsPadding() + .testTag("backup_metadata_screen") ) { SheetTopBar(stringResource(R.string.security__mnemonic_data_header), onBack = onBack) Spacer(modifier = Modifier.height(16.dp)) @@ -87,6 +89,7 @@ private fun MetadataContent( text = stringResource(R.string.security__mnemonic_latest_backup) .replace("{time}", latestBackupTime) .withBold(), + modifier = Modifier.testTag("backup_time_text") ) Spacer(modifier = Modifier.height(16.dp)) @@ -94,6 +97,7 @@ private fun MetadataContent( PrimaryButton( text = stringResource(R.string.common__ok), onClick = onDismiss, + modifier = Modifier.testTag("backup_metadata_ok_button") ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt index 6ff75720f..75798f51d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/MultipleDevicesScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -45,6 +46,7 @@ private fun MultipleDevicesContent( .fillMaxSize() .gradientBackground() .navigationBarsPadding() + .testTag("multiple_devices_screen") ) { SheetTopBar(stringResource(R.string.security__mnemonic_multiple_header), onBack = onBack) Spacer(modifier = Modifier.height(16.dp)) @@ -71,6 +73,7 @@ private fun MultipleDevicesContent( PrimaryButton( text = stringResource(R.string.common__ok), onClick = onContinue, + modifier = Modifier.testTag("multiple_devices_ok_button") ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index 8bdf591c3..53b4e1d3b 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.blur import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.tooling.preview.Preview @@ -119,6 +120,7 @@ private fun ShowMnemonicContent( .fillMaxSize() .gradientBackground() .navigationBarsPadding() + .testTag("backup_show_mnemonic_screen") ) { SheetTopBar(stringResource(R.string.security__mnemonic_your)) Spacer(modifier = Modifier.height(16.dp)) @@ -155,6 +157,7 @@ private fun ShowMnemonicContent( .background(color = Colors.White10) .clickable(enabled = showMnemonic && mnemonic.isNotEmpty(), onClick = onCopyClick) .padding(32.dp) + .testTag("backup_mnemonic_words_box") ) { MnemonicWordsGrid( actualWords = mnemonicWords, @@ -175,7 +178,9 @@ private fun ShowMnemonicContent( fullWidth = false, onClick = onRevealClick, color = Colors.Black50, - modifier = Modifier.alpha(buttonAlpha) + modifier = Modifier + .alpha(buttonAlpha) + .testTag("backup_reveal_mnemonic_button") ) } } @@ -195,6 +200,7 @@ private fun ShowMnemonicContent( text = stringResource(R.string.common__continue), onClick = onContinueClick, enabled = showMnemonic, + modifier = Modifier.testTag("backup_show_mnemonic_continue_button") ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt index a622847ba..5b7c3ed38 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowPassphraseScreen.kt @@ -13,6 +13,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -52,6 +53,7 @@ private fun ShowPassphraseContent( .fillMaxSize() .gradientBackground() .navigationBarsPadding() + .testTag("backup_show_passphrase_screen") ) { SheetTopBar(stringResource(R.string.security__pass_your), onBack = onBack) Spacer(modifier = Modifier.height(16.dp)) @@ -86,6 +88,7 @@ private fun ShowPassphraseContent( BodyMSB( text = bip39Passphrase, color = Colors.White, + modifier = Modifier.testTag("backup_passphrase_text") ) } @@ -101,6 +104,7 @@ private fun ShowPassphraseContent( PrimaryButton( text = stringResource(R.string.common__continue), onClick = onContinue, + modifier = Modifier.testTag("backup_show_passphrase_continue_button") ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt index f9a6a5a05..0aa9fc966 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/SuccessScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -49,6 +50,7 @@ private fun SuccessContent( .fillMaxSize() .gradientBackground() .navigationBarsPadding() + .testTag("backup_success_screen") ) { SheetTopBar(stringResource(R.string.security__mnemonic_result_header), onBack = onBack) Spacer(modifier = Modifier.height(16.dp)) @@ -75,6 +77,7 @@ private fun SuccessContent( PrimaryButton( text = stringResource(R.string.common__ok), onClick = onContinue, + modifier = Modifier.testTag("backup_success_ok_button") ) Spacer(modifier = Modifier.height(16.dp)) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt index 0a09255b3..0e57c4c40 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/WarningScreen.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -46,6 +47,7 @@ private fun WarningContent( .fillMaxSize() .gradientBackground() .navigationBarsPadding() + .testTag("backup_warning_screen") ) { SheetTopBar(stringResource(R.string.security__mnemonic_keep_header), onBack = onBack) Spacer(modifier = Modifier.height(16.dp)) @@ -67,11 +69,13 @@ private fun WarningContent( modifier = Modifier .fillMaxWidth() .weight(1f) + .testTag("warning_image") ) PrimaryButton( text = stringResource(R.string.common__ok), onClick = onContinue, + modifier = Modifier.testTag("backup_warning_ok_button") ) Spacer(modifier = Modifier.height(16.dp)) From b1339d3f0b6ac23c4d66aef59925543ae43db02f Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 5 Jun 2025 13:10:58 +0200 Subject: [PATCH 23/25] chore: Convert check.png to webp --- app/src/main/res/drawable-nodpi/check.png | Bin 29080 -> 0 bytes app/src/main/res/drawable-nodpi/check.webp | Bin 0 -> 25590 bytes 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 app/src/main/res/drawable-nodpi/check.png create mode 100644 app/src/main/res/drawable-nodpi/check.webp diff --git a/app/src/main/res/drawable-nodpi/check.png b/app/src/main/res/drawable-nodpi/check.png deleted file mode 100644 index 8b234b85439cbdc0bcf359bf17cb12b4e67304c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29080 zcmW(+c|26#`#<;2!r1p+M%kATA<2x8B_Y{LA}NY2Wv!TNsU#s(DmAt~6h%pvFk>l7 zg|a3Y%GzRV8H~BV`Tp+fbzkTHbzk>9=bYzx-pjq_;$$nxC&vc>5VW_mb_D>#eZ&CX z{|zO+%tr1&2)8?Yo;yPSe>ag4+GMfVto6-} zbq;%jv$?U!wz099Uzj&AFkp-`w6(Q|zYc3?X!H&A{a5-(O-*fJaNydtR23DK;o%_~ zo%a0s^OBO1+qZA0rl!Wm#s&oid3kx++S;0!nCR>4YiVh1+qO+nQBhi2T3lRQSXhWa zAYgGg7z1-Z=bo$bU+y3m>FRU{K->!h<9G$c#gz@n-luM4RljcjHuh)T@mci(0O1+- z)|PIuUw@DGhK}e9;Ck{JQS@UQUO?k^bd|XF?LX&_jo@$Sx=K7U73RM4|1Y)vOy!C9 zu%8(y#K3sO+2qkzM+jN&Ug^Jj?A6gT;8&De$KaujTraIsf(;&1O<;W3^%n+Hr4f;t z>7+*zQW4+bt@x@Y9#BS&Qm{-TZ%wLw>1B4berE7Nu)ozEZNYhuSLaX3R;Q=tC$Ke} z@`A&n*)NQ=@~r>({_fk;apW;i_qUUJn<6q6&wsuN*S;R6c6*EG@0YIm;wM?PFC#L( zC|U%)iwX!}_pOgzV@~)NY2t!k?lJtNTRiajsXqLlyXz+-sXO50U}jOafBHY7>-P|| z$oxkT6;$6(xctJ%|HI6w+wxw`4!}6rUXswOVXL}gdrj3*=l-q#BzPV>=Zk9W_xign zEQazUK;)|x({f8s&=S<+;pt#-BZM=Ve?jF{!#;of^Cj@i~sc>Rqv@et8=R~ zEr{vsv34%6KDX>!2TQE9duMqEe0xvS+nvYO4HX}J5IfgvT>oSBt@#)`$l_eB`ex}) zJN?HMQigDJiPBT2=$d5wk28w#RU8HhZ{wCK6 z|5g)!qsdQNCyE-rZjQPCi8@l7X&-(w)3HeJiE?F6=wrFm%B_;lt7_hxFRQO0%;v-4<{B<6Eq>!<(zttEG+`du2>ap6>DVpHCmrly0PD?VQ@{VC`ZP^0mWn#|wnc&;=hldSkGe>mz`C}~|e0lxBgwONt z()F2n`NLwvQzivY*W>%9in11+V@7E1oU!A+`<1-Ae{ZCGQeD4ic>IXcmL)!$W5VOq ztnL&8<&I{NrZ1lrZ`#JXu79X-^$E~S@GP0K%;Jydx6$+b%UtC3bjh$SFvr%pB-~4Q zIQ7cVW?d|HFwp<*x0PYSM}ZMx0k-_RF#tWWw5og)nq0kJrQ~$%q;>Y|P3%d}Kcpeg zN8uJf*J&{+>!%++yVS8BIqBg`;z_^Lwt5C`G7L}Hw#wQ0EFyl6-RNO?Jde{k2Q()dgp4SHj={tD6v<^;exW4fMi& zJNGm-t1H9$VzDcfC~O@RsA|>U9usVeRmPHWZ5e7W$C^ANd0${`Q~|vDyRG3 zaox%*wyoxeH9uRdKKjc80~{~H@rcA3Pr+gbr!Z^&l{#hz5Yak!tx{A6b?r6t{ zq|0-@uJ@62^){vYw_Bq*lF5`MIbLSlJur7?d!X7z`pm3bal?9v1c&v){nh^KpT#zx z{j2mFVcelr~r;xdQQ0 zkN>g#wU)gIwb*^E7zSF0L#yL&(L{t1Xw^;ZzY7QzGq#5!r3v_u^(DGb_z?B{<``m%6I4f_)&Y! zex0Lt+`m>`^L0fLRCn2TQ9avpo1VN_9{Ns(F86~kV`cr-vD~@yP{P6KhbiU!6#Er% zmhR6nHx$~lPUtWvx1 z>@boE61d3Wa25;{Zm8!_D4Ulq#>c(DG6cdsf>sDU-a@ydwAmpUTiqK&cHdU=TH{|P zSy2d=Fq^9_L1y>a|btKBJrT zWg3Zna)zS5rth4*5R%L`I&eq+@YCC~{>GfumAg71gCN}Po$=?7i> zUI*Mqk2Ol}4jIQ&ODT-70@|mOHBSH09JaZz$aT7J(gcL2S9=%g zXAq{#>p1i}PT1lc9E(>32>y3PA!g?m8uYSlJd8SMKPJ7eiC2`E{c=bB>rKx!Ce_Vd3!D|_;r3VE`j=@ z`|L4=$upI5PtS})doixJDGxwFHs{h>{XsOjHzbs66ail_WL6@dAv%Turumy z+n-&Y+=(gFggM?a}pm@KBFmU zOTecdXQuiJ&EXIc=+iZMd;Yz#usWnYm*y`TSD!bFvRzSDKzJ zRUi5IHn~OP8J%iCEDX_@N@5XYd)4P(&o1mw>Fy8s*lIKI4T@PO&0DQg%=OHbdN}u| z1I!cz@IO@BNcxPs`NY5z-+V~QRy4YJ*xn%i8ad0MVK8);LYP6vNGIDq5$pjc*@PoD_M%GRZ>K*~P1erV^%#6K_Ve zGKB@a*c<=a&jQ^~s`dXqyJ-K~b?f^b$@q0itQffmbzb{lusEMLrddiEr-Fsh%8J>zMTyKiDa@7ZmMpVgb+^AlI?I%x_0`Ff(wyR(iDTFb# zO`)y(+FV8rGfX+Ox-7x)uSq?EMaix4dNX%jROREV-U|1tZ+w?HAn{T(u=6R}_I2#< zo=xs~qkQLPmi6g%%B%GXF?t{-TTx)9@_p?OQ@j3juf_a1pQBTdGCDQ}0xvU#*Qq+7 z4SmTAcsc(hdyz*;`)KcX9Y;u#Qx}>>7mLoGbpWl?jcbDIfeN!1L_^w7c_JAdU?^;%W8}EuxqBXgQnM=5$mB%{%4#xI5HVM z_>-ZuL@gHN90rb6mHXfT*rS@_OI3i)&6oMbyPpcIsOn_%)w_Nj`%rny!maY%k9@N7 z5LU(;!s4n5-;l1-ZkYqGbUrjN3DUnx+ez@o$7UA>Vcfc}{34yw`M2SvDtz*R8IwkM z1sSEH(#h!I8>rFonHAVNgUdmdni z3ZH_|jyxn_FN^x9kS)By|Lev>89ei-i1+Q9?o>yLS+URCUxr^?_s21tJbZ*N9b-R& z^O7mwfq@O%`*7H?30>~g1f;+Y%Ymr%wZ)l|q)z;@cy_!vFY>SC^wE9bKlC}MYC*9J z&P-p`oJ=wFA)m}wFd$WXFVitlV+Z~H4b{k$zaJ6;pEg%Nb-Vnp)|r=L0UTMr?UwA) zoXkuGG#9K#vXvy>a_6$RVen6KBdAPNgS;7fxRru-2F6l_D-;>(n| z1*HN|3k7!}o`Jy{Yvd6T^0MdT_N(XO)Iw)GiqAG8Kl2fRS=8h^c!`B|Oju~=0%=-N zL;=;1Ex&p&P(wjX%1uD3DAOz^Z0}67LywO^;@4|nFUHRc>|;Cc-&E2i?Skf1wY`g% z+_EwuKc0XeW5<=c{)(vXsmuOI+tq(#@u5Wl_^pVTB;gp3cu>r1sX+$mq@iEI#;*`? zJ~_hx#lg@=~?OxGh~~ZWqRU* z-u-gu)odB1^q?-_-)?eOP#zDEXLtPCg8`1Xg-<;fwVw%U{3chf>XlY^KRN%?;$_yw z2h`jSVfuNP#YbYf~<};~%`P*1igq{sQDDQ{T z3YyP!Cb-Zk;__7=WW(pLVOU@162#l%M7~|Ree7Xdie_A-klWvtIKHQHU%>%f)#=0B zQ0nb-0|i)(7zDw#Jmmc@ZF`sKb#G=6_kt!DUC_Dzbo6XQ!V!s93q9bSC@4|^wN3-w zI}3T^)ZJv?;q7e0B#H8*AUjjxlxt9|snrncQCMo-sVB3yD{6~8liGuZE}jDUFf%L= zlOn0dIn;^<=77BKp*e;8)dsyvymx*I{-bZ%c7$R`wT)8W=NU*aHEP^e1-k6YfO*>7 zf-?>&ISm}MEq%W+|7fa%eXYw+R})Xv-hYtNz{2-UZO<|U;q$scIG9P*=LYpZSr@S? z$VlZzEBmh;l(4r7t^rH^=)IrbEuZ3bQ zW@EJrLeHT|X7fCq+Vy5d!Oby{c6bCv#0li&u2WvTLH*w_=c#beXGv?T*5-hjeb!Xh zu+hov4tC7&*G%G*QR)^p>F}noIDG~@`eJzv-PJ)k&l(Xn=nf6ku!cNi&8=dls>>3N zbMx5_eVD!eg`p$>X36v8#5*K&gBKvBRwrf7R9fhsQmZ;aOjKtVkx_#bsOD?&q zHIFCifI$Yu;*uV}-*xu_INBVOZ6H7p$5qUKng6uslR{a!miXKurXyX@Z5~S(V(STT zNC2gFOenoQ;V&m91&23!0v}Fyg=G$ec*ku)02mUCr79$wQ~V(BK`Ov9c0gCb;d4?)GMe= z1JNA?rW?( z+;xp}Rr$C^>fz(RChmzAC@C+cnML*;q_*`AmTJ!1Z|F$;LZuT~((1@T)S2`iuF?gj z-bjWAr*X^Mzh(GZ9HFTXpA4)gD@g>-Z^3_rkklT^Jv77BLYKn2JbKm#yCS^7_%rURGrF9nf= z*L<9}Xt1`akdxP(k3voI=$B#tdJA`)OgWSx{_>So>t%WCS(gRk`h#nwL$9?fQ~F$$ zmiG7oWQ{tl#W~bYISBrP>2gep>+z8nr(S7mNDoL`VtMhciwtJ_ua9@93W41sDU!jtO0e60ePLs_B;5}NuH+5}{11bL z)X>w0eEKlsMcv5!YVSqJ{(*G&izT%K&v_XZRODL?EVn{2=L}7dEbgPpUkomhf8RAk z$g9TrbQIktE+TvXcfm#jBKZ?Yz|=Ir ze`Q0#HqN2zXbU|P{arB>bAW!vI5z%V7Zh@KeUkWvi)hyWogJ@0>S$wopu4e5XF(A> zst*C(p6S$-dV-yaQ;+!hB(44`hnYdl>c;xP_k-Z)1a+BofP96I-pkx<(g7Vk&;>{t zc9CJSXY~EF)x&rt_f$D)uV)z~T6JB-i)L{WVJ{!Z^uV?e(83Q2t%d?CaarQa|Bk9z z$wFlcfSXZnPmAwXds4%*w`)_q6WdPPg7SEhONBVn#j7#xq#JbKbnuOE@6ylXh-9hU zrH_1`HB)7e%fr_vn`z+9R=`BZ@$7C=@@X$r?`;YC7=RdJx8AU)s*uxL(qT1TnVti8 zj-|~@6~5oEAvZZX=H5a}v~@uTrI9a2rv$@ODF}Kp4eBb(COio}FuxI97c?rR>b7w- z3J;jrpW=>yd$#$(otpx!NaS1|NKLBTE#W@(=yEwQ`H#@<;{%ui+C4V_Hw|-JINvR( ztTj|#zZ1Ik&@|fS^+>AcphAFl)Pjt?Qf}RdA~3I(?D-CeI|a!CD4~LE(6F{w8ML>M zsMJdqS(2*FKAX01z|Yf>O+`CXbVoIFuDZ0}PRZxgz83@#lgt?At9JU1EntTDF zYNcq(~y$a=>L-Ru^F( zbm9a|6tr#{i&siacF`0b70|5I7IT>tP)R1Wk&MYK48!O^pc{Dj$qOntz+P&e8NMoe z^QsIzZLX3Yv;9@izr*jlhOnY(6iw8bRjG;C2%vdD6@Gb}*WpT-HS)*Hi>4CkD|(># z@MEV$j|k1P?cbF2K`cgQJnVl>(-n$7&=o<7kkj@^!U6y57I$WDkBUiPruQ!B4Wa=c z7Lp<8D=ora4WT1Ys|IoX&8Ji0@Ur;4kHCRtdjmZy-vd?4q*3}67<>oMI#2~B+J7ZL z5??Vzd)RdoG^_Te&Jn^{s&4(*=dQ-DyPo`%EfQOAcO{74Lj;@w267z$wm85Gwuiz* zp@--N1JX#er3a1pENZ8occ{2n4;a6XbV&AS8q*ixp(mJirLLA)S~~dcMXub?%bCfR zX3`^n7#hT4gpK|Fyrmq~1RW3Pq*HrO><4^MD`U{p2OfA5WjxEz%5{C;HU*1tLg3F7 zj2L%Y7sC1zN-{mibSu)KE%|17@%@>7$E=s{nrnIm0i`mP=eNRWN#bShJiMVm9b(hO zImJVR(1RL;{RCH_)3fIT#${`TBq3ORRA}p?##e`p~s*!1!HjmCy~9K8wcZKoLmC- z+Zab>@L2YEL-HuFFKrmdW^b>wx3?=0BLPUc*N z!~HPtJHo$RhGiU7v7FPHZ^AJy`=AMLDOw~$sjucj#EavE*i1?COV~^V;CfSv?= z=K?Z?-a8G@Q+2yno?ZuYpE>u>{5!cK`=L<7guaJCNDs$Y%jJQ;BAlBb(qK`RK)$Ts z-(8N(eDdU(JXomTMvA0cLa}=6+E;K{!v4rmAoUh7W%BMMxzlF;dDmQ6!n=$&C5|=% zb6=f3;`GAg zA71}E#dw`wOgOj?2)E!19^e=O9t=vz0+r=Sj(h{%%d>FNNcywos#~q1+>G(Z`=(E} z7YQr;E^P}hLFb=CsRzJS6S9gMSQ@7pVhea<*MD7RI)Bj@K-EL89*WP5ys9Lxfe`XR z^&Gda2?A5ax|}AQJ|A=>cL(l_1DC`$78;gdFYb<6y1so&+s(qoV=QlXEX5IN#WG)p z!r3PDH|5dlp!J8J6#9y{!@Ev*8X&E>SmsMK@#NRF3FjF zXh#;n!7on(S8)&yr}jJwiMq8@C;sF!i2$|Wk=xE^cyM*318R&2+`0^(pAR3R3G#*k z!ko_q>8abd+PohLA)OhyB9%6uC`}K?Nj)VCQ$egAKfRfNRPbZzfL@@)8?KzW*4p%D zPHt2uKJoAl3$*4ek{}?7Y^sCrzxcqe^&^<(`%sM}a`6~2bne4lI4PZ7()ZEWeb#U? z%+2<6LF>a@h5%h$CLekU%Pov;!d_Fkbbo2oqz@?X&bgEucXEMU#rCtVRh*Q}Uay z9=z+kAOs%rrZrw|xF^kV;WMpWe_IzCZ*a(F;V zylwdD4`&&E$hHb&LhM+&CpqJR3a8-{7rJKxB4-|3d0iovuy==AJOY2=p^ z_#KLL9$Dqsqa#=iL!iKpvn&~#ykvMV_3I{Q@ATzRza==_4z*1||4Pz#OpvAK5b?ds z>d)_oJNu~k50Hm)j2+(Xem7XK#73JAfhz<*UNE&2$H>@89~P>soqoch)HUN>o;nX^ z6@;e!$H#fTCMc5+ip2qkz<7Z3V~|I-WqoItjy=9*Lh31(%bFOdJNd`rDgC67hy-1Y zUjY!=&fHSW@8?qn0xAGQg?tm@kwo0b)_UCBHb^cjyjn)&aJC`J!v_#F>Z&oBa*%W0 zmdEHZC87mGcq^C=^lFpMJit}bjuMv6we@?WoYRRxLb_ezu#`3wbEbX z*$+kob@pD@ec8y?rzB(V&?kKiJ!&}4=-FvG@zHGYkZMOq&sJzkYrSK zoFk9-qiScElR*vIVP^EwHerlUc;toaP6 zbGLpTCv!$=B4iaZ>traOOe0i_vc8DWS{h2_hu3paANR!!?k*b_ENu)(Glm6A6v zZn_;??J$8I*Ok;5qG%&%oRi{ISA2#NaUc|G@BbA&46AEoR2=Q!Di*%4>|gBD`1iB~ zjKg*(NQRd#Pb>pW0z}?WKqN&>VLhkEDvz>#J`YTo|4nw^^m38>q?CrnsS7m*K5_aO z`i6G0l_u#70M1VbgtKG*V9BSUSK=U1w5nA(WUDl9Ud`Mo{nlEI>g!@e3HhPg-`m_@ zN0Afds~lhkHvDBd&@X|*)jhy*f=nhI;amlEi#{6x}b5=Sd`blvX8iiE_ zHLt;Y7th>X#ID0Zh(0VRl8PFPQ}C>LK1d%*(ldYc3sv z%1`c;#HsaKEb++i>Is>DXfeyYxGAdNnD{`g5C%KRBAnejA&)MnlVigct`!t9P$Sd6 zO1F}pk|QYM!cIpA^mc~zk?nQcx2L*jY$bl-f9fg0TUKpG5`)1I;L$EfW|c~Y_krgr z?7*u6;179qdt!C>=Dm+^t$r$4rxcnUm+1U>WsrZbRmFKByZE#u0{^N$=h6SVbOOLt zJ>epp;7tMWmmXg?jbR=Yz2=ii{Tov+|(14U}T6K9e3NHAK5|!Nm2CY zRf?fVFW)j53Ryhp2{?0h_|&IJ?1Yd=cYG@^Iri21>zNBRCP@|H)6=3@pTaXZe7XO6 zJ1H@ePdXek^}QMvl}GAS4qf^GG`QBiLYfCQD6KnYOUb@9fkG=FLBeQV?_`!;|Mq3; zQK7yE76})bbIXN&QkNO5^C)`q~&_T zkbq+lCOu@+#nkqs&6fViXVERb?po4~&^-b2Vepm}Q}aT=E|9ya3MAWUqsqt|l0r+e zYR_oY!pW=685$39HKaOC>(50~suZ7j-fOP+GI`6UKAulg8z6Gt5d3_h*jwTVpI;x~ z@)rMao@ALqar%el?;)H+4-;L}E{Y2GSikG-R-NJ9C4Uz_bF@{iJULzWt)BS><;_g( zo?c+x_5%D~CmF*tDGWZ|tw`xEpQs}zW8N4XS}qe!2)f35s89p%K}Q_OQ*uAgvn`AS zT7Hh1|0Z9R>=D?+ae0Hx8zP)h7{rmD4${2fzQ;BVhH{$*C3_zAd~V$Q7@_=r*W$!G z^Yhw_kJnmlS`VF(RY(a~sHEUr0BViIhT!QkoY&JRQ?cj#AwXv5sQq$IRIIVPTPh;U zI&VPHcnV@t>_YTEY#sk{W}D!@>yijg3~62=W3c0-yLIbI8}SQREqY)2tjx9o^fdrw|{HPnRJyv#?3*oUA3I)#tp zJYWIE!Y{u>gTQXzd?3Jf)=SZdE=g#AunG(wSZ ze!2k18X0~CPBDcS!tO9LZRyb;Pj+o#?!4{detXt*zXabjmDz1dZq>r1Ls<{2@x=W5 zp_c^QU&uqPF&LnZ1T&QcNv0gn(M6OWe<}-HR!xnW6q8Whu;^BE6Pe_#dbibVPryEb zzrJJNkNNsmh!7rX$Y>NFFB7mzTnulSOW@cS#zHvc0#_nYTkXpl*41sXUO;Gq zv}H-qO9A!K0hL9*Av{KAcNxLPk^22-ZRr|jFCA_(!;EM+(zQn|BfoCLI#OhOxJhTK z+2zbq$$Z3;El$~FsUKF0gZy@!WX+#~5V3+wxHN_&P~MC48{OEwJL8&%mi-7XF^ni9 zWLrG$$2@itx(RNO>$LXux$y)}X*lK6_wgPA+eJF2^7{3m;by)vcfGIK zo!cvOpSiU1C|l&*4aBf4GS{hhl80ZdkiaSeADEfgJi#TPo;1>Np+LINvM#>(f#e{M@ zM&h=wV45-3n6U(t6Vb>w%rQxfh((h6lf4I@*4@-;Ze|`J_#^KB?czx9VtmvVQ&2OY z+$go$0l$V5U!S3+SuA%*4>g^ALxaEeqruzC-FFzTQ;ZfSXxkX->@Pw``%rJ;SF>A# zMr#~yh5V2&z&SW6GUic(Z`=NPUjC_IH&uKo=f<95KQ|^q zd?3)u>i!yiNd^kSwf_=-Mpp6JOED96`)L#TB9x_6W5MCCN^cpU4t3(u$C5C~pBh0t zAoV5D0JersVTdB(9#TdH4Y@r)aA~6iJqvv>A_LA~{Sa29gQNgvvN*m!go6HVTq+;; zWe4^vti5PBf2&g*Ib zYvZP@_6r%!;T?2~7wc4%?}I+e@OR~V^=+!S?ASA0vf1?jer@*;gmWO_gcGmJv#@W) zz;SHlg2mSvv``6^XVy3Z3p8kfrVFxiXSu)!lnZH%1LOIK%1+?}E`;WWHL2byG zAEuN4iO@TjFQMWm(rp>@@mx(Q zYvc#FF)u`?+dvGkcvu^kW%RXkEM<7UU2NEz5-wU9y-zzIl%c0_FNIN+uG21PEjHdR zeg|8xv~BP`Z{xf}^;~JQP>oEIrzeDZb9Lqy$NpRUP7WZOj~MYZ;X(i#o^4#=3=<-x zGbnLTtVBP9va@S})U`l6_YJMs$(CQqWKjjj9xAe{4Tn0W!q*4t(g?icXigg`!*Raq zox<%be`7y80mcK%!gHYe#D2vducS(ZNKhyHet#v-{7yV&{EbE0ro2#1*E9R7Y->y43zTelNX(0 zM!nlRTx7Go26a15&z2F%NKsC2yQ^2CtN4w`YUAuX0tGU=`TSDRH$w~x%qpF!ootbQ zhMsNsH`rBt4QYX^6q06Yn-YzxqLYsCu`4|kc8&Vk*tsPfw zo1?=KANWmYI@4~s7jhmu=C{8)BaP|dPqL4zq1F3O&e5@s2Z56n%UNBBQ@=5G4woLV zk@vRPw^Eh!oZyuDeRbEn(|)Jx7;h+rn|~aS$$k9IAX;|q@cT$W1ju^4XV((We@n7A z)I(l4u1QT;)B|NlfrDXxID`h!1;++ED zOc*U@R|wh-?hXOQS8ogY{LVi<%6gat*&T%fAMJn|E9?<5&O3Cr9jDSxF;i^ZvkO8* zL`e2=@z?oQ*7%|Xy>Glr=bU8HmKikil{d7@AG22xH3>XcmN6n_i6!c#md3%Ey1`yJ zo>WP|g)=I>JyL*ckwtSmyBoOp7(JJa25#gTuZRoNi7`1sQrutcoAU0a5a85V=lQhnnTO~9l0~e-KT3$*;Ww1q{MW5sr=Y`hEIre` z_NFXSHN$ni9QDb(+?V*Qku*S*wq$Vrb&_f2?_B@$enxLkMtLV=%$gp~o9uk+iN*5S*6=?~ z*PmOVfy05=Td?2k$I6p1WX9Vs(GBbwwxVe99<98c+ zDF^J#lstDWs&Iv0I7C80FFR@~ke)8X2oq8*cH$L& zeeaWK2#IsGn#fstc&`yk{XylcI%_ZW8C}R@cIyCNtscF|-LW2drsUpIp>b;3(!$F$ z$_{A%R{GY>Z>KR2!|f_QOS@TN@{tOK#6lRU1V_s#4&a7b3WX1t8Lod)veW`xRo9!& zLv0y|k4QJ5HwsQrcl@9(M9wjkctvsuWz~=jk7-k3(G06M<2=81PXsp48JvKILM1CF zXyW5xocG1(7lJDJ8fZSW?TOUn9P%@rqqbdwL6N1?;;A;f{w&x-r`Fvg^SmMBOENKwy=yQKxRM+3IUS+<+h%M0^je$A1; zO)prkI& zi6|!thEJZJdHDgQBtd0jbQXEs)X${`3bjdZ_%>V=mPaWi$V~GJNFkzVuoB06AJ+(F zpswwEmvhxXCR$}z`bzLpgd0Eg`ZCFkyvtk!y_-yN0zWX9n~0x41z~rvc>mO!QJ@kI zr?Zu`xa#raAZbyKxHS570XaZ|Ib&lFH68cz_Z;;M?gj6TygS7f8MGzEsXqo%LCAlc ztCJscZ}6hG#`@p$sv`zabWlOPU^t~Mi3++@FJbf4khXQyaLaRio%<{rpy-HJe0Vn? z$X$yHEOJq#yV7vdA~PLkc0t|^E zhbrKx9p1*xrpd}X$rWiZIN0a|6FHu1t4mkZQ=)iMovC;xbk&R^^%1$rQl`f77 zwB2%M1ItmAD=36IlTiTyAWGliclS>fG*1g&#U0jMQV7QkMUokOJw7a7&job&(Xx6# z|7UaPy&}o?{DkZ=rkV(h&?mudfWG~s`Cr17w1uEqit7%iBDbkh9O?< zQR}3;E*NYZWnd3_mCrBCCK_>r^K6ub90zAcE+;H`4YiQo(tc?S-n+kw;oLCABX^O0 z&rUp(U#_N}NaC2Fd^lhRiKJ0To;i(x!6F^*+Tt?h<#vo)8$dKPdhL*2P>!dgKtCew zO5ohwDOiBx=$`qv0~nY<2N%~);wC@TH{)a zM+y$O^_lEY+P?STx+6!BRVhUOz|XF1W0gu|-OLm9Yu*YM5O_zpRYe`hgK{YZLIb+^ zuI)=jqoXew@VC`0}(=)h-lrK#0GeYE)*wu)FajUjkYPhSr+O#pwsf zPQh-2qNQ8V;c5R;v~3%go~7;GPF5?S#2Accj~IfPoYxKACU~ z*#xmfcXPVW?J&ZDR>}repCQ~%JKer8F9VF>$Lof)j+2NrFDf->1boZUWIX+Sem+rq zqaCW-o=R=CVOc?@2- zr}q#T^7vp#&VZ^ct5oUcYwbb8O~jWFDc?K0Mcz(NkH+)m0z0t!U?P{m=Q5V;HZLD@ zJR1#XD-FRs`X18)(f#gHunT%so=oFKbND#49xlaPjH?pxf+4PtbA5DLuUS&$6=-A% z(|f*sx@7ci0$nPX&yFv&vM0D)r*t9>x&`rDmwV<0|Kd$s(fo0$Nz8>6k}%YX{ZNeC zWhr~X(#XECN`UuShT0mWjg$ce9gwG2CnMbP3QA>Cqe{`l0Cmp%X~*3Eto3$>EDsDfK9d4xP~b&ceEv_U*;5VwoP)wS!O)?+*NtGfZ>=2$&0 zJlF&Hu{sXa@oqLNy}W6soLdb^Z=xT?8A@!WN{lrg@dk#nz-OL;)6J7kInmn3;p*tM zf}A$N0z9`d?4X$RDCq!4P3Yx;#MTw8veqm`bE9}*9;Coy*1CO(l zg#!YdoJ0uv^w~7zq^8PIR9@4Qkv0&4Ko59Sq#R-e_l z-gW9>-aw3z6!0*VMtyfLWHNm}yd&`GCA^>#CV+?7RFXLT2Unfv)D-oWd9zlxBL&!* z4f#!`3oKF}Nk2JwcDK(uqq69gU_lGNObW`&RC5xcbbqd;I)mxi!)fGlFDgXM11?ye`zXjiFeCX08#!i*q7@k|N4J8;{JDtp6(l_~Z3B+JtiyFxhz^hqXWX_GEz)1k%~mb*#&r4K0T!EoeXAO|bR29~hCc-aAL-^ZUZYyYL|$Q}^Os zvA~CGdL+2`a(zv-ks6+ZiC56=#gPA)5@bR?f%#^(7e&jZ&`12a&KPkX1U`+|7C)uht2=GU|gzzlpt5l4yxk0w@NNkoZLQaV$XATA_Tm_^TvSmS=0}Z z@jwUElYe3*wVA7FDb!wG^IfdWUkg;H^X$?T)JX^88%G~7xvo?X8CHK`oS5#a+rv4j zCdz3_UlY^$uoaUMEeo~I2n@I1u$`}Kk z8O+7fTMZdWh6!!2pwQ>bQpjnL_hZa19;B;+BC=3|5wstF#muDhuizt7Kss9=^WVmnm=pSOL`oP3gW(khZhOj=i76L~F3N_@^cPQv`p z3d9taE%>S;QViC}jzl3czR_#hdy{0qn#LkRbt)Z{y&UJ&|Eu6i!=d`y@bAoS4931Q zp-^NkqMA{5Vk}9U#*(E!MJXiBu|^4rqAXJpNttM|#gHw8q)=pJNtUwjX5M+vhqbJkc;rph`uR(`JqFuRDiny@0W!o{Ts$6p0}PEQ304O> z<~H~aKXQlXt`j&zs+UNxXZyXfk7-Z$Re6AVG_VF}AMn;_y8Us)l&4q)B61OrcD89Vn@B@5)g#ZRYh7kKS z@`$8|*}p+KHOI8ky}KwpQr#?f>CGh+uwk`8yBFm1dD!?=<{rh`TJE*MLM~5mB0&L3 zTwkM16R0?VPlA;tNS%)SZ;;;K&bt+bIp#Q7F z7AG9ZCI9=U_+(BaB&v|uE&zVjwux$gCW&hsvaM4F7`?*kHz%>@0+Iiir^}xOjGe;(FiE*wgI1> zPoF;v5UT;}|Cn1ltC@WJ`_WldS_}#|xoa%hn~&-nsWz87Lf6kp=O<;1<44F=6%c%o zL6yKoki-URTcnAS=4>Gm7RPkXYyYjs2<~z0oV}3h3>+sM+6+~e&Ato$x^V{nS9Ull z17|o2UMSynPT=F7$#Vn0I0u$>N~$88-+4 zmdHAc8VsW{&Zgf)$hu2}LFAC1%!T4t4IJ+Sk99JYS0@a2qA13uzP|W1pW2K1($}xK zZrcB{mJi*Rn^^Gq+Rtp1uLN(639lF{ zO9Bg?(pM&?y2o~u4hdwg?<<$X**)lhTUKLG`;B;P# zrcZ#2g3$dn`=E1X8nt(XRQ&Nh|AF@5@;SBU+9$eGyI2|AI=PfiFsl}-(5vQG-f$u{ zr6#gZZT;83wzK;30+_>3xFdN}Xy#D|Kr^QFi%S_r2XmA|*W;7#^uEUHXCe077%{)A zX6Fl;KEX>#!zd)+r;AOkX)CJ5J8EFr?_Rx5-RNF(fv0{J345ry^&V5U35s zV1PBPi_m(Fp}mT<@UpzOxHtP+HILJ(d`T^p*Mhx+)G`sAVLVIz6HxZwvvOuVpXlxo zyB>In!2_KBh7UUKrfu>xt&u!}kr6+0RyA^3wgov8fN~jc6fUE#efY%#7(=V%(>lC` z*bh{pD4<~riCY30NXuKOF0P7A2^1gq!RiiZ&AO6hw7Fno$)jF-A;w?wS(e2KEaq=x zfBfugC`GVX15&(Rw^i8EAG8`X)WP*%tG{s_-xQ1 zPei<=S$?HTwqiJml}nRROdV!2c%BdhdyIoRdFDEvSu2p{$WRmHJP9OI2L%KkLqhx_ zdcg13nVtVGD`Cazxm_x77J(=Y9IPX~%q#@c{~oKCcSaK53d=}BFMOJy6B&3I$s487 zF^P4GJL(vsg1QtUO5az-T%lNEE|qZ7$B|-AHy-Q4a7W`0eO!mvHmc-2CD@sC@!BPQ!Tdk%=2~iuON`V0{BI zwyyoiHOIDvG0K=`hEP$iEI&(%!pszm6Rk4H6eiKviVb)Ky#$C9X>OOmA`+e9*J@}wYm@?p(z8eBfv0uKhh-xRXUW|y{X_@sgd^@^!sb8oulbXQC5Xy z;7I~}W+Qqb+IU;JhU}TNLdnvz0oI}DMcvGZ&1DsMZ)YfqwGB4x4O7m4kez;~w`qHj zD%Z*o(m=c1txLhN9xd>#sv_75Vhb|Q!IG?lM7k8MroHs(zVxeqz~+AlIMWLLW<*h$ z`I}xO`J&`wJnkXJV+So@yZl9j>ocX|UtoAU{CzJ;6_fGhKS3V5@Ej#laEe}D7{^Pc z(B)3rg*mMa2#&73sWGRucSM_w)Wk&Bg&O@vu_y4dzzzN3Zm27Y-+~paUoMQS)`w@` zJ>;WK!o1(`<&rHIpD_wgVyg`oFWJ16$c_RX@YidHfiaG@2EWR01}fM2;p{-trBXoh z`?gt~m5W<^d0vk7K{oUXy7fXYcN>yvLJvdR4_K=n+Bjr9JW39lB(hv#_s@JYR3O0W z{(;7>=dx?>qvNNYQum-;Eh~BA1DFKe#K801v#7xB3&$r94aFRc@_HP~nW9hMD@+fi z$*3)OT0Y++sumdu;l6@UL{AkFcRUb4lfGRXY5%>5Xk#8#e3MYrNZxA7kWLxo(>VIh z1N;8W%f2S(aqW!sRPb?P#8dn6rM^={t}9A=wd4&T*(sljkzfNSU`issrK6Du_*s7> z|LV$ZIN^Py+Enl(&fXBc;L}%*WfWjv9Z!5X?x;=M_sPfdvDvRaaFN~xQhMG(VVZEn zyk*)o@8+2ws{L;jgP)_5R4Kw-OZdx+Er(;|$(QzGD6s@a-Z`YVViuH$fO&caZ9MWL zolS8-V?>E`g~&Ip^Rc@vExBI=eNQ3QqmwB=E4DHHQ@{UFy-_i{jN)aJE$ zK8qytq_h~9__GoNT3IZVGnWEY-VHKbe>C=@%7Xbj1?2AuG(>g%KA%MVGeOq{LPFz4 zl++#Dk@wm%`!)BdVER@r9#2i-M@f)E#X^4{V#MckA@pLQx|m;Rtbmt<8tFU5{Jg;~&8k<_>*S6*Pc^P6Q1keqcGQ-I$2OQKI=_`cLR| z0XKHy-_>k0{N_naLqhrs(KJ1pzm~P@xls{api*}^a1qfP8KF)xN((lg?L_P%F+qDW zNXe2Klo?tW9QT|Duv8$Is>?bwfbl^iZAnZfcY;b9Y_;R3{N-LZgVkCKew9t{FidH4 z+eHfHH_eBfXJYLyCy3myl;*F;1!{3Qxt|BUkkfGy^x>Eb>7Nv){8jiQoG+3GjI~)f9as#>$7lBbl;kvNZ*Yy zGXBF2_dwOc5)8$!OEv-4yMQI`;xYV-2aq0fT0f$GD%ehbI<#^?{aL*PBQg5lB}_vW zr02m>^8sZDTajYah!vpo6N7*Uu#WyoDHOzFWc}?fG1NdJx@B`Uc>$&NjBW|iLujCO$fYOj32+MM zHtUjhOQk&BEB)X`{ZVC_cJAR^4c^6TtTHNN<9(sWvai7LF76}wu1LMYYXXZ_?%TqD z8Ls|zU~LHrA%AKj)rd@U*bqptj72aT#C{EJyALOq=t~knl}6-V5aXVB^^=?ka(97m z_k;4CSFGG;JEdli#X^1$c=i`0DiyX&p>6N$B}~4EdSxQ@8dzP~`lzT2LQx_~@Y*qu zaI)}pg56b%o8cy2(DhC9UfOC0A~N2IH5pL~_T%9@t9?K4x7j7G@Tf!)4~PH3sk{VA zTW2)s=npjY+5iRT?CEexQ&n9Yf z{&4Ol_imy6U`gZxe=y$%9y5)2Cz|!!L zJZO1Qc4#RCP`7hCBYIB76I1wjjp@^bHsFn-YU5e^C#s8)yh++3eJ;(rcK~m~A+v{G zQo@#ODfNPeGT;9z`{4A`?P2!IQP_14sRfimiYGD7(eCk4pLV?lQfNxGI4LPB&QWQCBK43^YFHQN#+tAdC(^?fZSbxQ+e$aU}z(TM-&JOM0 zdoRKwcpDojP6*YnV8kXvhuZ)i#S*|DhYe4mGCYM@c+KYbb`#hViXC=L&{uKO50@h$ z0o%Roy*B*WYWsUYc~Md;xhaq7Gc@GU{?R@xkF;j!n}8Aq7RL1=Huo`NWdXBbZGL?h zPtn<*A_+qISpA^K1un9?iZKu2GWYAU5BjjU1gg3q2}3msK5+p#?UapXPc-Wnrms!z zs#=!twMV4QcB;b3Cn8@aoeX{%75{Chg-C=@{c!jP@Z?RVfYd{29ZL*45hnACJq0hK zXcDx`yF@{52OgJ1KLOX@B^T^NkUC4hO8;~C+2CsX5qKjNcpo2NrfdZfhc$pcZW zO>Tcx<+mnxQoPq5owSPsUrds#?)6(Q(GeN|nH&r7vTv8>8b z367eGb#H+oT@2g>HW=c>LtDAq0t+jyoSso|n_O2Lh1ci&5~I#*z7mEyVX#QIMNIc> zjPRS3`2g&k&^mqVy*Q(3nzHyLxq;wzcv@eHo#;Tkb(o7lz^K3jr&`S?Y>}n2-B-SBca3|v|wElgODhVATZq|A_cp%yieyK%F7Xje~vLU_}R{P zREy7@`y0{tqjU$(7eF=IzNt8-TxHb_p65o(QGuC(X+ht9E{_`eg}f5?w(|P}jiN18 zypcMyA!pVz9&XbVHAgMxC~DlmNs%7$XQ_j%QSLHXk}`*Yjnvv4btyP3%2aPW+XCv5P$6a`l!A54)AA@hzie6`Nfp8Zrw6F?Fyv_QF< zvrMJLB%(7Qf61y;l@MtEe?RpG!M0tDIozz@3;C|ff!K=9ok*P&djt%RD_#P5^>1$l zQd)rCXn7HH1$ z^jmrBI%QCqF{V}+E>5K2{r>x63{7uppj-DMc;vdzqEhD`)14k}I!#^l)TpCM8Susf zFy*%pj%rgZ6n(-A@V}tVhtuG!G@=}b!EIIr3g)shAb0w-z_A}jBDeKL8Oiz8%a=ns zQ)lm_5D$Ija#!}c>`yGd!@RB8ELbVG-chPwkW!ITVyx#J<~s(ohiYu9(w!TnXk(un zKnR8AaF(?rRNe3|)G}Z29k+K_zMPj7ErA;65k?A~1C$MXr)kk&qUQHeb=j>+t^Alk zGDQVJ*e51&7dk$@!x6pb7tS^mt1rwGbXPzpE{El{nPgUr>0<`H9bH%97hd23p-2VM zjn!WPCD`W=rg3@Bl#|txaF$@7l;Y^R#)US&dhv^Sl{N&^}yaU z$}a|E$L7(c6dm8)_sv?i{^aW?Ik|s?J`0(;dg*FjQLo?)<+0CW*ikBooD%)o{|e2h5p5>DZpjkSOgVS@hJjPrFijd~uo z5Z$)>Z%2fhg_lGWlq5O(z#XN^M?_^z;~53M$o3OtPt0FypXoWjgbIVMMiW0r(-gJ( zgs7K-ggzH3<|{%uzc%f|Q!NX`uiZ2~bs^$TcwK-u3aploKg%pRJDc2>g-DUN(lu)+ z&SMGJnG7EbU(VAdkwlB=^UjA`Q*y5eznw+qrFq_m8 z)$r&jW;z}UNrl944kE0BlnGesht+_^%@WF*G?T6a4gCIkID5M=VR}Pr9<~j1u3=#b zg;t1fjXtr+=T?@Hzjjfhcx4jBF1wKN&FJ(=P;+l*(i`Td3s&pV^$qZ$H8p9b#6e0q zxEUEE<{PEVDu!PMz|XFj=RN*M`o=Nq*_LXOgFBB~N;HqyQ{7E!!J2{*$Au zZQk(uY7=o2TXs-LI3}hjsrBS6L)*fn0aP7WClQJyqBqBgIWkeNWHubrS_G;6p!_11 zh7j?Uio^+4>YzK@21e8~Qp|9Qv~1Qnt6r7+LJCM!gXyTLOkLbPu;)AUX#KKY*|NtyVaR%HyBi&l(R%HHNb=ECXTg4`EB| zryaorGkb%h)He}V_;nPuYq!Sq*g4X(vMW*iQUz`_I3!Sng9h8fzdu9vR zVLc&>s#7Mq;>%JrRg=H1ChiUd4<>(#0>jz$<;4lln`*Y6B4UeDs@RUmB`0`DYEWt}EGUr{D~s6o;;Fx^M(VLl`#>p8tOL z@s!C~^y}B$wpl?9p{um29zIK*B_1~gQM>0HyJ|Q5pgt8!=I_J?DiLMyv-b|!IzgJr z>@edllTXf`RgmTof6F^JyMrrFG*(j_weh8;Qo~3uC88fXIRjio^__-H1hm6Y51+Ds zWaM?cb87;I*J9FwBzuT#li?R_aQZA{8rkJL`~AnQJK%{ZOZ#rZ~u0pHq|G zy8kW9N_)SocBBS0-Wvr?MM3?@C^f z#5MbLY4**-eiSxXhQ_c}u+I)wi!4uvueKdXO89T#Q=7yumN^ZX2#lx1-*63cZ$&qo zG{7@g+780ftZu`{z{I=mq#ln^&=0P-P(lU3F>Z;sw$n)H1Lg68&=89IMizF~v8(j_{74LQzcEkI0<&Z`8c^8>GmnF+OtmTTp%1DMh|9iAEQ$zKraC|E4LesMy9G zGr)OwD%*w-T0^b`sZ9<#mMl?Z=4W~6t^jbMZ0w?F$}Mnhs~IO_<+O#<^|S0X#Cj|g zrI>?6q@~oTnaO#n(H&5yE&O>GCr#fqD-EV2C|3{ z8kVQ#t1iX%NV`Z~_>32G0NLTjfdSR7y@=>9C)CIPK-kJggZ_|mFG|0dghSVLdYW0< zL0F1Mt@8{Zn@>RJc>mpi6$1)aefe3N3`G$JIh*gfbR*@pZtVF#;$ln!vytQh3}TUX zu=?!g-i}#|os1db)e9HVH>;57t6?fr?QkdJuH_gd_MW#e(ojh4@z^i#mu*0cAacq5 z;k$&t@Nx;MpG-LpV`lD~2 z7)FqcZ@RFqyrHh7c30C_O=nrpy}+m1;zqC8<(0x2x~v8)q3DKX^kTJMZ%MQqH&OEC zris)!{QrS0z`aWmec0Jd=R2rGE#yz$z@`GxQbt#TUw+LB;Xh6NI=OlITgCS724Toc zLA%yVkz&VQi(H4Yu*H37BGz04ndyr3w-5kt>B1mk8!h6yu0ZXM@keWh?7KrHraLtj zVBKJEMfTf<)=r0+CXr<=euf-K*fiL*wd+9m&h~CA`1U7QSaQFTcE~(<%v!z^0sjyY z{r#(%h?M>kihZ5WG8@DHWM^jn_5K;(uN*vFHHD6c8jl3@VqLcgIq54|!!8X(&wEpX{@vsYjuX_dLdh186;1E>ECa6&$+lH!3F_H>`l#}G_k@{3Mj z68ZbC+M(OSBGga3Sb{XixXVvU-YA8x{}r`s^q*^x#uR+8v-dx5jOcwgid-H=z7@hQ z)ZWsAOU?>)!t3HhM=zv5kOkZzdGpZX=}f`yz6!UWS*J^KSUNtWmkZmBK8$J_h`deFR6 zNTNh>)%JsxkD7NV2U5N3FMWi!&Q&nf3QrJ9CmHXho5lTqLPtbr@5l!>8%gQAgKxEZ zT@wcys`Wy|%#>@m=Fcf!N`V`Nm}`Y20hI<(`UcM^?^9?ZYLk+v*fg3u?vY7sKV=eX-dH=G^@$Y3dGy z9Dd>31WvnglKS(}%fkew0F)G! zb&i^e%5bs={=b&#rkglSyGm&=yBY-H;|*H`6V@E44uYr4kGRhx4!1_ywsR`L&Ka^U z^t7z*ccMv0i>sh-G9*z|uw9R}1a)5U?h-5iBWxLlPE-?mFZEx|(=@3+#-IMi6LM2u z1xmBVs1lgts(}qxecHQ}X^zklJT(l}bOgc|`X@sqrn`)2<46{;W8|ZS&t^tsI&f?%7>Y z8n(P$c`~8mOZ~bL3m&}N28wRfFtL{WUxLx5o0uZ&oGK5&o0#>?*Evflg}Xue-BKQ` z>zB2psd;tYVC_S$E0S#|Mmf%9=a@`(cB`Hnooow+AEoO)pi1A%^4zE5a+Wn-cgI4K zsY6{~t*sNBcz)p`g4LZF=y

%S5&M`I9O)gmdJc$yQ#A0{HSiVyg{MZUif_xFq`W znBc?(iFurq1w|y|oAAV`73tbU)Rs7OG$7_;1dNN;K;rw1QX))+z5!l7seuk3V;Mp@ z4oP2dl@T#~{ok4e2hGLnkRN*A)d;EicgC66+%qd4D&7(bMWxDuJRlCGlp&P#xnGAa zVN2!Chn9WyJ7n5zb8+V>U3RQg;OmJeQ_Pxk%`a{e- z-zbq1p|AdQKPFA0{LGLs!S%h29uuKfgE*80rb5;o-lcgd)fcOaU4KnwQu4MZMj6iMu2GVm(5fZ8>@?`{x_B35HA;#lfx zaq%-s>e;eqfQ1VhtUL$S;Wd63EwVg#f7u zMar+QG^->)iNou)10Y_Zj{heGy>T3rIc~ebxlG-!qW#1+RKk;`=o#l0dC{378UKS` zRiNZ8-nmcZTjj#=*_k6)!_%+&KM8q*e){9pT(`Ao3{0imQr5;2&gW|jZztqz&m`5^ zs3UaPu@S8BSx=GOCSv#r)lzx^QX`r6TzZkoCwS=*Sv|OD<HV+LZ`d2~gftN?FmLi$0u-96N23Rv@a3BA!WzwyGHp{Rl zv%(F4O1WCsdJ*QRXoW(|6>5ewMH3!1W_m%3JDJE=A5q}S#flWYj%^I68_n(e6)skC zD9lWzxb|XJt3r=wv#}jr(=$;!P=-F9ObY z^1?34zkq2Qk|<^_r&4l%Sm4VAy$BplOZ_CxHm7}vLr;Ycw6V`6v8^^%>}b#Vs*lXv za;%66R*DZjq0RUm2FsvdS4XOdQ)U!j8<`9WT#BTR`tBsU)aWAau7MyFMS|j~oRU`9 z%qS}^7F<*F9=mL4q_dl#vXvyU^3 z=o*FlO;ry|j=fBN=+}wOqk=nIcM;BppaOB|J@@YAn=Pn?h}DudM3L@240f#W+64k_ zK@vX^x;Y{rnU+ad_V{JgEaAindNov}7LYV6)s=~u!%%_|@Tc5^5e&Nw#^f}%Mn=ca zy1SG;Zb3fC>g!}8^n+3JxSx@(ZtGJjVxRjRYlHp7PE1FSJvP2=Ww7(p6StKGJClx+1^mRxu2gh8{L&#MA0CJ-t>7w(!ydbsEsz@Qq~-oK46rvJ zWq`6)#%w_ZoS;rBhlrOz`h4yBsg{DO1DV?%&i(nallA)XC4z9IMzR!hh<@I@y+v6& z2Vo?`Jt>#zs%MG68xgJ}nQDaSu2$;(Ig`?N%QxY2b(DoMV=9OHO(^{>I{IPI#-9<1 zNSX2OZd_x~O#*1W4$Iy3Kf9$V-c_?EI!(s={E2+um$TlJdE=V-j>~FaJzm=G`!VwJ z!t;adOS_o3YDE2aawfeCqUzsUm%OoWan;%mOxD^!pV~s?#Jtfd1+c5uzw7sQ!8MIL zXpUKcb=Q<};?1@GX$?C7gXpHIjjU zA zNQH5}g_xt}_i^!D{+}tXW$VcrJoPg#+nHeoV$fZkLbp>!CK4x~iqS0KpD7tS0w_u; zsJyAp2tzbI9yVuctb8(HeW{MT`H?pBc^y8|h|Rzt49Vcc4{_qKa$hkJA+UV!?pf^V zNS<*(><|ywxu%$EA&NX<0DRFmuW=SoU>o^yhhRo`>$d#8SIx`SV@)m-hco8bIudgq_d73Q;0d%rP!(Xrhzb4`({~@j1cm~3A!G< zahrZe=2HA6iB}?~HBUpW6J-&fa=33H+i^uI{8pSybIZ&8+*tFf(O_41Y!T95i5VnC z%H?)*X7Gj*a3z$2k{`ql>kz{e6uq~gOYEG_ zAp(cjb_JIRi5bS3pgopr!!%}ci_eI_ZCrV1-`-Zs5BNRN^BLM@tR|BmgE{K6KhS^3 zlHBoC?%-y?Cb+90KIYg{uG~tlXrQgAE=lkW5$nHqvy%RsRl};= zRFo9s#LtB&KxHMLotv_dcM?T`$1mzdv~Lxns&`Acq@-%FZ9V8?GX2cf4m(QGtA^kE zQrAy_RTV|Cec;xu{!N}tUSZLsB10SAqW@v!3x->;@YaARL{JdWB(saIUjJvp?CADSr`um-F)+s4# zL`M5R=OAl__su{DhT3_aD+G6HV~Eb(+%P6@$p1-x$T@*XQM6idj(MHceSG&8`bUHH z=VE3v+}6f@7oyb~FrqCM$0yhDGdqI2GT3wPkkuQ{L3L|xZ|2)C4Vife6F6^lgS=9* zQOkYvjI7G_&)Dj{ifT|eY2N)h=(m`8_^p82x0gp~+K8H(!GA+Kxw1n|y)-Z?!1mJ9 zs`+PldUf-{Hr$k+>^(~7KE}h1p)u}BE}!epy$un6FKw3)UdcLoz7O$Rn!U|}uEiaC zFh6wSlNiG*U*zfij3eTHd2KF^F6HBMo%V-(r&gMA%(nuZlY-p11Gc+4oVLZ@j3LhbMxF0&%wz8X)^y|)|YEVwP*F3?4G3`?|;6V6#jIpdg<$=3;CY^ zb1yZ8nD`@3cZ$n)eD_0Q<~uG-NpIYKjrkdUf9B2OfZ2r=3(2lDqXYX^H`YI-{EK`+ z`8U+%ldJ477S%5$tB=TB!mwV<`Ci?){f|AaRrcLGAp6hvvzB=~lvk^7^a$%#cZupt>Lwjwv#?D{w`OQE6< zzOcfRg}xmex%!|^I_b^qnpVpb7N_OECu?WC&qf3R%trf;8d8((q+s-Z%zQ)Ff2R8i zqfTE6^*#P*q~yEXznVzO?u!}%$wn(V5&0iJc{$AMqQi^RlMOl@UhZAg{C9h6aPDft z5v`E{^3(a_I^Iunzn_T~df!=f@Zh#4_Y)u4t8>TwKA&}R0(-!5; z*b`8J%X%y9RdKb!>=?Hvcr4eDpOK-IRHl}n>F1!7C`7Tn<`ZCPws6{~?$IT;H=p0X zUbuF`b~xQuvSwlLI*WM`zGe%zOajpnaiaN09hSL{@k z(JfV3(3mBDg_cv^Wal%h!u*=>kTSk$>99PBN_P$rzmQm2{`ef`X6y^YZE*=vA>9a< z$DJVs8yUr#L7`GM=o^|8&~xCssf4?_$P-k>a|w^WNDrUqqatEac}Yh5ocz{~yQUeo zTNq~#@m)P0+#sRVHK3hbG-8Y5#HwEOF~P3N*_-%$>z2JzTdbT z-DlqDkuFHpPoeBkKQjC{wSLDY*m`H)UsF?-5I9iq0%aouD6vS6GZCwDK>nL1p+wY8irO*->vxcz$huT4}8~X z+aG5_;DMm_LbNarNZDvuL(lPcJa6d#AKxgv56X6QpfeEw*c$);?||Gl?$X0kcXY5* QSO0%%WoBpk@(?ZRenF0O-}Fk|6ds*oqIop{8{!PeQO=Yaoo0%BuUCY>po{@_a6}xz`xbH zqLml4G_MGDqy@C0gBGByE71aywS$)GhNfBpChHb}$q|6cwrN1hflWO^2bFV0<)$Tf zm7kn%Ns=r{l5DF%q@Q^H|A!~t%+=(s8j+pG5&f41Ns_BZ4>$qOux3n;*?mvwzP#PG zt*dRVZ4MyIUVf3penD0&+r`d%^8bIJ))>9_G1p>2zKtHye@Fm;#BH1?5yY;`&0)Ij zwEA6)(mfr2OOFf^3jY5OG&^-lMiN#z^GS;6KP=c*<2EucdO;F& zN!lbJ*;&5+bNp}r{9pb#yJ*x!-oFZN{ELQT`PYIQjNkRs|GaU7Ax(AgyFX*)h$C`D z!F78A*Qh^U-ko2_$?bi2{@-LVW^66l^K6EgPFs}h+*0|EQZ(@|;Cx2>07#}oBL2>% zblw6M#=El`PR?rh80pbGk66els_8;*K|&z+M8n5^fs^PPK}3Xwu1Or51VXt^^Yiw= z?7BLW3ztW!ho0MnP&jBx98d86Oh2iCKnh2fm#KfiJrk;!@c^9C6U`HzovyxpAzG{quuWO-pjL;TsO; z8C%B6Tzd-N4loNdDGTcX)E*39IXUXd4guWF%oBY%UJj|Fu4~wr8TO0mhBJ=e#G#NZ zo&mUlUX0-lgGiNwDAt7Ih zEDq@?XKP_DR>S4km9x=EIQrvpJ%a!j@Y_$v+!S*9^iUm8T?dcN2PF5@s#uKX7(%uE zjpKyKXkI`zEIPT>z5tjAc<7e0E7Qi&>O#FnX30-3J@rbgs_$NxKXD3p_sKNxCCfy-UTCipntDKG))Y)ts zwB#H$CCM(u3ucN(u^pGGL!VSpE-`69g5ccfa09J+P6^hXJs^8Pv|YMsVDR(8_j(Tb zSBl4$>>ijahw26s<3(A@6njV74TfzZF>FfK0B7Xj#P`nXr9YTk1ksQW@a`;_K{ZCB z9=zG$PNFsHs|n@|9S{eCVpp;?D$;CncGNdTt%-({$%Voe25sqhStmaZxs(h?A^Nb~ z90on_#WXKmrDDd$GdyBGK18J>IhcN4RAhZJ4x9391>YE>l#DRD9uJF4i$TdJ;=EP- z^m;Aucg1hUsYGN5 zN+b-0ejgt$`H#)S^%rtioIzD9yUq|qegW*OP`L46)^-`6_rx zKT{0fHzhgPOu{nvRR6B>Mg%vh18qwiTG9#*`menc%@!qvcr_|&wt~We25mw7VB^q_ zY~*T4?vno>(u_3FQVbwa>7mB08~q1FeEx+LRL3kjgWh)j0ayU?^$Gg9(b*K*wUg+>7JJFZ12j5q^+=wX*0{eav+ea}@9Zo&{iC_RGf?+hC6*?MV zxkm#SaZ_GVKC>?&{|mC1#eZUd1&=OCv|?*@{e#jmnGqwF9k;8rxrxo?I5Jcg+TF-y zx_(p?WLcwF0hlCa%>v|qonhTLad>QsTyg(;W60~*eS&hh35Bxne8QCZ1KfcAxfBus z8ifend3f$lM5_+!l`zO*Oj)_rlv^ULRAeJROS>;>A=!=;Qx8g(9@04MrgVf+-m3up zRhSgw0Vrxf=|#hy!szpT9uWt_NqZFW?St;%*%4_~X!`b<4}}L}HIJ76HXL$TSi^e3 z>_)sup%k_w?2vjx;mW?*%s9`(cys^0F`z}`I)=fVv+FSr>4kJiDfB8}*H_mC3QHI| z!f;u@Ze$sSbp@zI$kAIbIm5LwQ8Xvzn$mKOTb2k?xQMJ&VpRCp%Zqq^5v|#gSjd1G z4v;?q=U}{P9BT^Jq&ssi|CgHkB98DabgPIvXW_BB3yH{=0RGh&Hn=)kB?lk=T;Z4p zX-TSjz*C2jVXRT9f7&t#aE!>i!Bf^GiRM?Ei;3qPh+sU%ILa3qV%)lFbz*R?7G)~D zMwpy{W4DL~8qa!W&LB<#(87YX$TSp!j8y>WdgCCh2)CJL-Xg9{lxS!N?tr1umy1jv z7VRSYcf#HRY#ZQqE2To&W)jYnc2|GzV`23TXI1Y8vnJ}@MBX;U;Ut3r<#@qUxjpCY zB{fN(%#~~uSMBjgd9K0bVQ5Okg|IdGS#F}?HSH;Fk7@f2+rPj4*HiAAItU>{XaM>f zuTLxT#zHIuDbEdNF6R)=!B}QT@aLz;bUmZngZtUHb(!Pmug9n7mn5GRq0?}z4#TaR z6M|_BcYoq_ZV%VQfemQe6dbpZ>sub{r*#_LhrVl@=*Q)Bek7vLLQo=boLVx3qX~iN zynq&K_Jh4ngzP!4&3MX!a zX9bCJlY^RvVz5ymQJKQ{)=@(JrS>6Z3q`%9=Jh|}6q83A37+?x> zxO0g{#HwHccp1RNLd}5Z$KO&+-O6*N*0 z#H|1iDUHgs`TDjld7G!9YpqOn=u$5w0o9!YT5>QXe)g-gSdA^CQ}kYFsu8o^`Ip4PF? zZ4Mf(2LqJ@k*xR-ibu0FUDpOhs9o7IOyA^_A42WdHb>8ZNsf=8Oa~(yiIrC4OoB)A|D({ zoaUZt2ovyQKq(U*fPiFQJNJZGs{Mnoc2v*^dRp~UiF{&oL|>X3CO>BY@0LyvT>nYL5Ja-cBC4N-lv7`s~Z2eQ;^zY{va8eqz(=#n;Kn|2JkvrA5UGOs?@dnAIhCJ5Fv905d zvv;Dnl;P*|RW8;%>|W*qTxOM&ff(I{dy6@t0j6OpVk9Xhi7Gn1csYiQ&DZ&Orxs5e zc!9E?ekGCSZy53_E{(#=QYH#L{7XD?ql8a+rR- z=!FOz31c~7s~0(%_BZNx!tDf4gG0B56Z|ViShWs35(bWU%7&t+AW<&FVrX?b%yeuR z7g|<|%rI^8L0iJSw#|g7(9*6YTcek!e;*87Y85{2@3dm0O|b;A*SH}OXwA=vtyCDl zBUlf?)(+JO6NeKGljua401TkU*I(*fWY*CaKO?)r0z1l=#nNki-fVoi0i}y>W@4Vf z(%Z13h>S)Bv{|YcvOk3g%ku!PRLGh9WNInG1ric>H`pq5(&e)bHW`%-n6M5EX0L04 zu)s%XCS|oBe?DKU1&F^Yy^Ft#*f_M@t439(6jsqRnh9;%I>I(`atZRWnPBN3$btf+k_lS-2JU{9c5m zm2+T(-YW?&4QM|{9cfm-bW?{&xS@cpEhYoxdH0qJFCKnteZ{iCKj%=G$&W@Cgs~VTwu`^sj<<&h_JMl z!b%jn1NoaqByA*w3mBReB2GQP2Jj)CCCmnPIH{P9|H+e$4u*0_a7543Ehvnhq=+0; zX=38c-1nhqTOcV>5U~!$yQ6f`%}i7`TQCtm9PaeuMQ2?I8rQPW9la?NNy^(;B4@Z{ zFIN#wtT2g$O8-a$dmGH^1l z&niZejg)Y6nBf`!eE$JK+OP)ZHqFGhr=W_z4|T=C6t6i2z50r^Q4Qhd(^!OY1BWj# z4=5{~M+k?B$Wr7=6z+5u=2d0-P)#VgDw46W3_EuLyY4V-*y#n186OF4n1#Pjpj2sE zEQy$Cb~d9o6)oCx0iVWcbrN2YC0n>zcSx}c3(ucV0CF0Pa^oiRVR7ZWTcg)BqIh5f zezJ#~5e2-@kPbO4fF4jjnFge8ZefhwHM`;PF{LA>;v@p_;uK&1s6{=tw2Ze+5N@`j zOK{?lw+#@4QqTo?2{k5QSgL_I5q4+)Xn+Cy2iu|u;~uoE)<|GjWt9T zP(RE`OhiWGL@fK`h)Rfmc^}P3(n5xK>cSii+&8dR!_B{yH6}zc0u1P}5*nhycv_Cp zWFSi|&;m?bKz0kXv=X-fgBm%OUI;vQ!P@3-d$@TW$`LC%T#BhJnBZ+s*SWG8%Wv8B zW*Lni(%cws-ipvjfJ1St(E>u5h%7{%g@AS)=e2@%s*wtkYBrw=bD4m8 z$kU45&{@W23mKr3;KlI9&?X`)LZfe9)7rr-6vFCKv00xyqEFFwrT1*lqU(wghAgv; zWg&cK==KUU2_r3#*hFN5za;otx`oe5)H2)(&7dp{XcAm=enJ>^F`Wo{hGnquQu8@I z2i4RZ515yCt75_!a=8_C9c3xI89zT}5S`9rJdtdR{-AamP?sZ7;A(VQ0Gh=j+D3%o ziv6s?*?FP}%^?~OFgLHt~S(~shhQFmPYs>uuP8l!&Zc7jKUvVibeIuQ+_2fk-RztDVlcBN1ij5AP1KY zxwy0y2UpSTG>ePL(naX~6b2WW}MB&s!R;I&#lW71>>gsylHjF}CBzMr@j8 z86F_qj1HNO``Rozlt{q~Qn+|{Dt_&lJlL_b7_;JSw^!-im{LoN5r!0Xa}jz#tioZ~ zx~(ENi(w*iM&d!#zU~ZwSYuE!+WA(TYhgelp%`6;8{l<-2bES%L^jVti1^j^P-Ty` z5gfmudHx}WdVF|*CAG><_IMO=TCx!<2gI6ztv$!$5=88Q3Ed|LFT%teSB{TaLW@k@ zk{D^mZ}bc{fI`%_g?W;_Djg;pZGeMrA*O}9hbt42uUVmP2(LDMp%YgC){7;KAy=O3 zHJEOO`jm~0c)U&7v}_B-p|iei1&B5g`T1IJM2TJcamYg}31fc}pX)j-6f&wd#M0FI zCS<&gO|mZqwT|notOi#nB9lf%cw!iyrZP~C_iK!Ot1*oWi4skmqMMt$0cK4a_A)Nw z6JpJ1cEr~!uqE7Zlta`iSdGYFW!ao7wnsd_v3VUm!H>#WhPf+1Os5BecBgeho6TB? zh9h*cK}9rhmwGtywYN)PM;MoyE^Rl(7iij}7-QmJj?gilDfX6t+nSO)U5*iqtDS}@ zABJw5OC8~ijCE7c)y+kG6nvRR?*M996DaL%np!{ti+(T6bOhisxd0aGJd(DInr}RacvIKOECIYApVkCg-*!i-gk5C-x* zOe10zfhjI%@G!k|3NRlfWOlrWu>l2~shb-c2Ec|m?$r8iGCmKJ3g=Oa(ZU>-n_(qO zvr!M`Wo$#gGCnB4^41jaeGox^w=W3yL<5eP|Ly{jl0lbT6@pA~{8SWf?v~#IQ-B8| z4M+*~5WH}X-*DI53SmY3du=wiNQ~%Lqq7kTRHTOVo9Jd)3@&Zt?Q%wN97^>bgu~p> zZj>i3_K8l!`75FArW9IlMP}fKo{E&N0^Ds>VMCZS<8$hf{ z0!XmB(LOf}-hc>0F}QRA8i8*!oFskQI z!S+`A=<4P_nUj4X?sR=y-~*?O#$_*5efSg6=cZhosH-w?(k)ntI(Rh81mPf212*Gx zaOp24d^%@`0s4_ZNbAqeIZs2Ag&7ag_TY((_an=xCzOK}4&rn45q0wj_&Ng~Fk|O} zii4A2{&W>rJuqyDnpupkfc4f;#%Qx82xz#Zd_Z`U`Yn2ttO5k!6jpfUyu9-9l=}p8 z#T%HPR2vTel?HVk{w(5Kj*Z#D-N1!__gVuBl21zPWxQ+T5L}of{E} z0fuZas-c^uu5KPSh_|iO;aRnHrv!PVD)-asQgC4cdjr8NWf_Dq$r#mhkEU#W1>C!BZ)Sy*ZQ1q)` zqZm2lo32&R5m2jTTv9iyXqN}KLZF_L^5HNj2DRnbJIfI&_U{#>6^IDpXrN6h>EvZR z42l8{XV8f@O-iMAtl#=o^F?~|CD66RHKw;xZ1o5OFx<@Ga|8=y@1auC?^r8~V>GSZ zu2&|s%jm?ZBbeTj;L?`xoNPqF0}hVh^QkO!dAzwcoxM^=zu_hm*q_Zc|gcZa5%OH&;t2CSe*Gn6Lo6*5?!^c!qSypwwrZNb^L>-{%^Pw#>ow+H$QKkOU%) zL>TISkh)n*J;9W4++Ds-ArHUE-Z*$^{J|>Bp#-pw*V;XZI43+Uzm^mZS>HAadIb`8 zX5-1Kr-$Z5mtg$daRiIF@g!{-1@HQ&tS*Xxme5^={>-Z`xmlLIjD-|xu3g_~T(CDf zQ^C>ydig}d4zhtGo@M$8vt@T?QL?sZ0hHjz{HXQ$KrkM#~(a+)+oY|?rA`s z?5(PsZ8>z(~{|^{`w0D=LkMNm%&Cr&Yt%fZ-Q=o8+ypS(`AY@PiCavMpL$s9$ zsPp!GO^eoE%bhu392$3nfjf<0)U0miI8rwkFR|j=QWS4Xy(qmO$w66K4#S}5Seq;t zh;>6pnD06y#1IZHZPJnbD)E3rH7u&<`}72@!nE$(1cDCC2O4QzT?waz0e{GcJhR3a zYl|??GPdL5i?$ES8(X=^pNR~MVS(ZZ&W8se8QU>+^Q*xUZ@ctSQn2n)tsaxEQgf^S z`Ua872n6a9>gK)!gc@#+BGzDyASa>>avft=gkGBIz5qM+`f-dE2{(R#?l`^%jymF9 zT(nm0J^p%D;2N`iDP5qlRGw2j=hDv+#u;h66FldXnoWGYhos>)m$F7oFk?UD0mq^*W4_5u3c+ty>upfiX+fUon#x0IpA;L8~reg zvCXQLQz4teQW%*d*~_@BZXOj|J@Zy^$VxTpWA4Pu2m6kI$5u22KL7EJqE zV5>RV*TJ{o@wVz%V4X&Ip|X_Pm7F`4M4))PN_EPwxzq0UAnMz0)hCFKu)s;U**4Rl z1`?J|rl;m#jXRkULjYPcwqx`WYpg998woKcMz~ov^Fu+vfz0;3U?-X{j5uh|M2T5( zILAuC|0~99FAOV0Sk$ghXvpZTs?4lmrfzNkeoY~Qt}vHYF_syHu8s-&qS92|AYICZ4>8SHk5)IwD}20Cj2wkU2v-F!!(Ukh+s3kyLl4B*i4cq{$}{J%_F#7F27e51F$L2qFv z=$qBg3;aa~GReML@X&O9wyt<}EG`T9nO9CUEXuOVS67(R!p*NENKg7w z_u7}=dn2)M_y8J6;}EDAZn3(#G_}C1WLw1L+msv~(mD2;&8SB$-Rz#$rDRbaM^LBK zX6j}gA0%oNZwsjid$L_^Ld_Mjyl>yoqv{Bnl!Y5#RpLfHBAo+yesq0KVUF5l8&MpS zLydcdQ%!}DvLCb=>7d41dq40AqNjqRNr~=~0V&eaQw)ZYQ6cM7E?hXxscw;3Bo(69WVH?ccwRl^?A)_V%2iuSu z6%KenXl`U~ZgY4SgRZu!gMTD|$L{147TyndmMr3BT!LX!QJ328kPytK)*$UMkX+#^ z4E367m|5kTZI%U7Qw!?mFrr^M)PO@ZdP7eG6kw6ZnE19}hU3h<$d3_ksOw}gr~;%{ ztS!zQ=H*2Z6`E+%u^>Svm;^W$?aVoLvC!Y1UhC_oVUZX?0l^4!uZO6c4VVQi@wTW2 z81}N`&zm)=lM?z+{f^=W$#uz**@qYhO(FOb63C{gc5G9ilf8`0NI*K5r#eu zwHP9jjRLZ8xCZ8m=;F<>G+M>O>n<3zrocv^=?AA$ob0V|f_-fe`hX-U9B{#6G)i^D z8Th4Zta45ywjpK~Cu@Uc%OO_q?}w@SvB3mLz~N3`U$F`b7Gy&7vZu3bPW5HQNfm)O zx7jJYGcSDsnV7U}_EBEGrcld0dis>LN6bQ6L1x&Y{@W z(kP#l@4!urVQLkmSJchlKf@3!YH3Nh8DMi5HCSH@Djcw}9X>m_#Q*3@e9GRi*q4HB z?Y{y*zZ41Ih_FD3`808+N?>Abp~6D4EexnfQ3h0`?@KZ-#hytltDAp3mtZ=TLD;K6jl%1q0clG0&7m)kv7}k#6}Pdw z3b}O|mcY+gYq)^6WZn(7a&cqR(v;fw530feu~-n5u-5Lz1OAx@{kf@h_S-g zfTq`ibDAyPp`}T$fYnm>)d>0i8Fz=!;K!K+CK_K(3NBp$xxk#vwy?ZPE|sX+^VEnT z=IoX5qS>d2DYqmjutMZhS*@;o`F;6(1B?-%0aut*8df)pPWBuO@it`yUgqntdjS?+ zH#6<&`ER-ieBN)Iz~t+ZZWO3Nfm`SEcZw_@h8J$24L8AG%!RcK%SRIU9|7RoQw7@j zYzQ}3C)E@|7!W^4eZ!lrncyLJ?Ap{Yd``IBUZM84(N{+L7VzN*778ccbd`RbcCe<2 z#aB%5y#n4$61H%26@m>I&$gy&kL%?I>%&!vDeBi~QX16!zN-cwJ{OqjLE0lm4C%g@ zrfwEvOzcIif?3C4b2V1i#q;WE|D6-_erU!2v6!V{H@F@?ZzxcP)bhsjPF!Q)4feg4 zeY}-+&dKe3Y%ftw3AD5ZOj+Fwq(N|y7zHK))mlwTg2KFdagE!y%LK~d^g72_9%`zWJFo}S|ovxzj>%NVJf2{?d+n-LZA+HGcPO>FHN-)RNM{R?_c z4Rnn>W_W|`=0qa{0;2RrSBg8k2INBH%mo~m0toS;h zyM!KaC}6LwZh!-F)#t)rTVhc0X8bh;sny#pSNaD7z_kv>2Vto{d(HbcCj<_ph8Qvf zi+mx(8PwJ?o|9)O)8Nhw8`b&b3(__DG7Z7r4SHuC4BaCz0Zco)#A;xgX^l86GTr22 z&b9!Mmj%%dR&k762Y~bHeKh*xtQXAZ7Xnrvv{(OdOOURtNq0vB(fn`Wn%~fTaML{v z0FMt#Z|0sXqD|$ z!Y`jLjtH*)0^w#mSinKxXlO0uTH;`MU4&ilAMo?#n7)r0to@_6k`-#@X9D2ng^1u+x`}0${zqpGgImiVuMIk<_9oF=Ar4)CxNLivQ9LfL=Nw!5sg&cvb zDIEOrM!4}-luF?SL*hNjpKc26b20GSBg9a(`;}#oB^y%j1p6+T(B5JOPPL?NHUk{8 zWFv)FDcl?$CGHmX;JdXofNMP0TDxOzM0LyJL?Zvc;hzZu9QyRk2y~~a=@D})GBMoY=zC&@KZb}T}6{k zSrCK+$qU^IWaks%6q|WTz}v+rxO6Gn8RKmNhdPDZ@PIVP1f+gyc3VjXyG%`X7kd!$ zG4vXb#V1`PJtD?wN#@|)aSRyuAl%Rnst-%C`I};wCQEGsi0bC^+-cZT#H6D>hmQ{D zl^_!>N0$!DBHhhiCvLuo@{yIS-KM_FTMyeHH@qI^|3r_^FUmtux{@})J(yQ;n*ch= z$qI+0Ze}o*@g%~{I%+Zk5117WhZb&YUtV55z$G5l`U~*~TrEkv%|mKyJE`dU{AIX& zqx6S4-5C3wIv74C8#ZI>*ex3H1CrVUs+)^2Y%<$IZLN#-LUd`y|K4u2wmi!7m1a{? zvGSZ%Ea&3o>DMx}V#^BN7cR<)bkllHgetEDQ3s42iWb5U>?u@F0|eimjIFzfRJtv- zM!gW$Bk9x9M~5yQ7xg<1@MUlkK_D6Z^*-+a6~7rXC87h z%~#J~Mmud;`8p@x|1dK;DzxDdIH}4K>KA4nHYleL?d#JV0;6xF^WlRS{u))RHQq>84Xw zB0*YUf~*U>p?jD~wKlqeZ2%AYw%OW3fL&mH7&Ywb(;5^?bZO#Y)^T1PY(!f25YlPG~@R0@8gSGDOy|7Ar8M|;-PNSyCl{UgaUEC3>C;QQb9gGMrUoa z(377OMJY4shtBa0!*7R4o>l#;AkX;n{VU$LHfs1sp+Y3?W3S~*Vi0IT^{bm8i9=t> z3I{>moWaCnNY)96_1hXip3(3H4j7Vj8;^#(G=-5lxKJ?@_=Udz{#9I5DWiQ|o>E2b z)lOM%UNn!^B)sbCz{hX1^g5>@>Sp=VihfOeGeDHM?4nGv6krMmcnWJ@4F=U>EprxL zlI=d>pl#a!8>LEwD6{e$z!S<3f+f);Xm1;@6i856uQa2r?lX zva?}Gg_znEew6?#D!WGbS3kfIP)8Iwj{8+$q{(_BP{wUHz#ohDw zzloQ#2wAxZ|uH#9_z~aY{E3H48~sKP1+hiK!nY z!ke_ZGT=XdA}4$C!fD*{YoTnyrS}k5e8~j%2syy6hb3~YEbZGTSDI%R>mOf_-$tL3%Cu#yi<&XM&P zHB8{#WRdWVV{gK-A3ZV5#zP53T!`HmA*zi0_JnyKVYFQMWk5#&`Zf)^C1d8`-Y$NA zjmo26#T&^c6tB=vjnkM*&RoTc;RJhEF0ABc$=&U)O)TWMs%y;eI;+uqkVq7L|b#yAsUuh!67dUF0>Go z28sRa$gL=>0czPwP&qC;+9Y-Jy$;7&gJR?218$PB-(|bvJ4hwl+u2uwY=3fqD9>D*0#?abZgCgdcIZx21-lm z=QFAQRt;8fQW&t~aF63CmSvMS&430`NEa8ypqMw;H+k%u~gLqql(hB5x6EK29!p#*asWmje8aWU%g&d^Rgzh~#p?QD* zPiyCRv1kwnhcAgq>-)P`qKYF9wn36kqWL}*F)0%4*lXxk1#DENZVZeke6{FFf$Ftw z0Q5mH^(J&omhb1-ajPL~)hI)o`j}-Sv_%`Fh8pN{fH_1L{me>1t+Q;4gYW9$(h489 zUoY1Z%w{Vp?Nye#0}+hFM-J5$a4P~a0rn1Gk5ZD#MhgOPk^%y3T>4v+TSC&Nlm~~U z$wqNhu-Ck8gL^Um{q2X0G=|g|j}itVb2t&fPHS;7kGLa{F*shjq_R^gvLnI}3nUx$ zvZij90f(Ywqr7GnI5Z$?aP*)y^vU5h)Y^Ol@Kd@(w^fm&R#uQa(ZB|{M3B~ofK<6- zme}1|WfUTsp^`I+E_KR)o`gk(Z7krJzXQoe0>s;;-`m#lb`fAFa@>LoTm=}9Y&ghl zYHEkf1ZXD?Rvl#V<6R+*&>ST;NQJJNn`L9^N#S4&Hw&$_zRnW6icGK17y%171l2WZ z^Kx>e*;w53dv{krc)e>UCoq>bRd!l5fLWoYMfZ-MP-9H{wD7@Z85_gR`KQrqsDpjt zV5#6ZqeJ=-#f|hVxy6l3UED$J61NH9W~6hJ--WG2UXk%|8_vsc?*|(s7Y;c{xp=D*n}G1ecd$fuPzxMF@VNvtRirjAtvoNUXH!#(m%vik zS%(`kuOXc)^@CiSBEL5KTgox(wGUVn~Sg` zs0eq3+O6kjg=;w~=#-(hiIoeiG(jB-xX>nBEx1*|OFnCDq#|pe^NIy{Y|dfT&&+R= zy4ecxGZ-Wq9dXf9?6Z6U9`gI+VLw^m2yxBF7PCT{=Z|W^Oh183ub)*sWU|F!Oi?NV z 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 VTlfzU_@?jwh`RCkFaK7$IRG2MbSMA- literal 0 HcmV?d00001 From eec7197dbb9afd4f6e0ae501381a95212dcd5fa1 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 5 Jun 2025 13:26:07 +0200 Subject: [PATCH 24/25] fix: Fallback blur effect with alpha transition --- .../java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index 53b4e1d3b..63d3bd85f 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -220,6 +220,7 @@ private fun MnemonicWordsGrid( modifier = Modifier .fillMaxWidth() .blur(radius = blurRadius.dp, edgeTreatment = BlurredEdgeTreatment.Unbounded) + .alpha(1f - blurRadius * 0.075f) ) { Crossfade( targetState = showMnemonic, From 4f72dd3beeb47a5cf02f7433b1d4486120fb42ee Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 5 Jun 2025 13:29:06 +0200 Subject: [PATCH 25/25] fix: Remember selected mnemonic words on back --- .../bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt index 31817dee8..991b1df12 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ConfirmMnemonicScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -54,8 +55,12 @@ fun ConfirmMnemonicScreen( originalSeed.shuffled() } - var selectedWords by remember { mutableStateOf(arrayOfNulls(originalSeed.size)) } - var pressedStates by remember { mutableStateOf(BooleanArray(shuffledWords.size) { false }) } + var selectedWords by rememberSaveable { + mutableStateOf(arrayOfNulls(originalSeed.size)) + } + var pressedStates by rememberSaveable { + mutableStateOf(BooleanArray(shuffledWords.size) { false }) + } // Calculate if all words are correct val isComplete = selectedWords.all { it != null } &&