diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 1d21c6bfd..39bfe1f6b 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -25,6 +25,7 @@ import androidx.navigation.NavController import androidx.navigation.NavDeepLink import androidx.navigation.NavGraphBuilder import androidx.navigation.NavHostController +import androidx.navigation.NavOptions import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable @@ -80,6 +81,7 @@ 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.RestoreWalletScreen +import to.bitkit.ui.settings.pin.DisablePinScreen import to.bitkit.ui.utils.screenScaleIn import to.bitkit.ui.utils.screenScaleOut import to.bitkit.ui.utils.screenSlideIn @@ -231,6 +233,7 @@ fun ContentView( nodeState(walletViewModel, navController) generalSettings(navController) securitySettings(navController) + disablePin(navController) defaultUnitSettings(currencyViewModel, navController) localCurrencySettings(currencyViewModel, navController) backupSettings(navController) @@ -475,14 +478,19 @@ private fun NavGraphBuilder.generalSettings(navController: NavHostController) { } private fun NavGraphBuilder.securitySettings(navController: NavHostController) { - composableWithDefaultTransitions { backStackEntry -> + composableWithDefaultTransitions { SecuritySettingsScreen( navController = navController, - savedStateHandle = backStackEntry.savedStateHandle, ) } } +private fun NavGraphBuilder.disablePin(navController: NavHostController) { + composableWithDefaultTransitions { + DisablePinScreen(navController) + } +} + private fun NavGraphBuilder.defaultUnitSettings( currencyViewModel: CurrencyViewModel, navController: NavHostController, @@ -688,11 +696,16 @@ fun NavController.navigateToSecuritySettings() = navigate( route = Routes.SecuritySettings, ) +fun NavController.navigateToDisablePin() = navigate( + route = Routes.DisablePin, +) + fun NavController.navigateToAuthCheck( showLogoOnPin: Boolean = false, requirePin: Boolean = false, requireBiometrics: Boolean = false, onSuccessActionId: String, + navOptions: NavOptions? = null, ) = navigate( route = Routes.AuthCheck( showLogoOnPin = showLogoOnPin, @@ -700,6 +713,7 @@ fun NavController.navigateToAuthCheck( requireBiometrics = requireBiometrics, onSuccessActionId = onSuccessActionId, ), + navOptions = navOptions, ) fun NavController.navigateToDefaultUnitSettings() = navigate( @@ -795,6 +809,9 @@ object Routes { @Serializable data object SecuritySettings + @Serializable + data object DisablePin + @Serializable data class AuthCheck( val showLogoOnPin: Boolean = false, diff --git a/app/src/main/java/to/bitkit/ui/components/AuthCheckScreen.kt b/app/src/main/java/to/bitkit/ui/components/AuthCheckScreen.kt index f515999b0..879f1e947 100644 --- a/app/src/main/java/to/bitkit/ui/components/AuthCheckScreen.kt +++ b/app/src/main/java/to/bitkit/ui/components/AuthCheckScreen.kt @@ -1,6 +1,8 @@ package to.bitkit.ui.components import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel @@ -12,26 +14,37 @@ fun AuthCheckScreen( ) { val app = appViewModel ?: return + val isPinOnLaunchEnabled by app.isPinOnLaunchEnabled.collectAsStateWithLifecycle() + val isBiometricEnabled by app.isBiometricEnabled.collectAsStateWithLifecycle() + AuthCheckView( showLogoOnPin = route.showLogoOnPin, appViewModel = app, requireBiometrics = route.requireBiometrics, requirePin = route.requirePin, onSuccess = { - navController.previousBackStackEntry - ?.savedStateHandle - ?.set(AuthCheckAction.KEY, route.onSuccessActionId) + when (route.onSuccessActionId) { + AuthCheckAction.TOGGLE_BIOMETRICS -> { + app.setIsBiometricEnabled(!isBiometricEnabled) + } + + AuthCheckAction.TOGGLE_PIN_ON_LAUNCH -> { + app.setIsPinOnLaunchEnabled(!isPinOnLaunchEnabled) + } + + AuthCheckAction.DISABLE_PIN -> { + app.removePin() + } + } navController.popBackStack() }, + onBack = { navController.popBackStack() }, ) } object AuthCheckAction { - const val KEY = "auth_check_action_key" - - object Id { - const val TOGGLE_PIN_ON_LAUNCH = "toggle_pin_on_launch" - const val TOGGLE_BIOMETRICS = "toggle_biometrics" - } + const val TOGGLE_PIN_ON_LAUNCH = "toggle_pin_on_launch" + const val TOGGLE_BIOMETRICS = "toggle_biometrics" + const val DISABLE_PIN = "disable_pin" } diff --git a/app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt b/app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt index c87366c55..21335201f 100644 --- a/app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt +++ b/app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.env.Env +import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.rememberBiometricAuthSupported @@ -41,6 +42,7 @@ fun AuthCheckView( requireBiometrics: Boolean = false, requirePin: Boolean = false, onSuccess: (() -> Unit)? = null, + onBack: (() -> Unit)? = null, ) { val isBiometricsEnabled by appViewModel.isBiometricEnabled.collectAsStateWithLifecycle() val attemptsRemaining by appViewModel.pinAttemptsRemaining.collectAsStateWithLifecycle() @@ -54,6 +56,7 @@ fun AuthCheckView( requirePin = requirePin, validatePin = appViewModel::validatePin, onSuccess = onSuccess, + onBack = onBack, ) } @@ -67,6 +70,7 @@ private fun AuthCheckViewContent( requirePin: Boolean = false, validatePin: (String) -> Boolean, onSuccess: (() -> Unit)? = null, + onBack: (() -> Unit)?, ) { var showBio by rememberSaveable { mutableStateOf(isBiometricsEnabled) } @@ -89,10 +93,11 @@ private fun AuthCheckViewContent( PinPad( showLogo = showLogoOnPin, validatePin = validatePin, - onSuccess = onSuccess, attemptsRemaining = attemptsRemaining, allowBiometrics = isBiometricsEnabled && isBiometrySupported && !requirePin, onShowBiometrics = { showBio = true }, + onSuccess = onSuccess, + onBack = onBack, ) } } @@ -102,10 +107,11 @@ private fun AuthCheckViewContent( private fun PinPad( showLogo: Boolean = false, validatePin: (String) -> Boolean, - onSuccess: (() -> Unit)?, attemptsRemaining: Int, allowBiometrics: Boolean, onShowBiometrics: () -> Unit, + onSuccess: (() -> Unit)?, + onBack: (() -> Unit)? = null, ) { var pin by remember { mutableStateOf("") } val isLastAttempt = attemptsRemaining == 1 @@ -122,6 +128,7 @@ private fun PinPad( Column( horizontalAlignment = Alignment.CenterHorizontally, ) { + AppTopBar(titleText = " ", onBackClick = onBack) Box( contentAlignment = Alignment.BottomCenter, modifier = Modifier.weight(1f) @@ -200,6 +207,7 @@ private fun PreviewBio() { AppThemeSurface { AuthCheckViewContent( onSuccess = {}, + onBack = {}, isBiometricsEnabled = true, isBiometrySupported = true, showLogoOnPin = true, @@ -215,6 +223,7 @@ private fun PreviewPin() { AppThemeSurface { AuthCheckViewContent( onSuccess = {}, + onBack = {}, isBiometricsEnabled = false, isBiometrySupported = true, showLogoOnPin = true, @@ -230,6 +239,7 @@ private fun PreviewPinAttempts() { AppThemeSurface { AuthCheckViewContent( onSuccess = {}, + onBack = null, isBiometricsEnabled = false, isBiometrySupported = true, showLogoOnPin = false, @@ -245,6 +255,7 @@ private fun PreviewPinAttemptLast() { AppThemeSurface { AuthCheckViewContent( onSuccess = {}, + onBack = {}, isBiometricsEnabled = false, isBiometrySupported = true, showLogoOnPin = true, diff --git a/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt b/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt index d3b2bdeba..eb12ffa88 100644 --- a/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt +++ b/app/src/main/java/to/bitkit/ui/scaffold/AppTopBar.kt @@ -27,13 +27,16 @@ import to.bitkit.ui.components.Title @OptIn(ExperimentalMaterial3Api::class) fun AppTopBar( titleText: String, - onBackClick: () -> Unit, + onBackClick: (() -> Unit)?, icon: Painter? = null, - navigationIcon: @Composable () -> Unit = backNavIcon(onBackClick), actions: @Composable (RowScope.() -> Unit) = {}, ) { CenterAlignedTopAppBar( - navigationIcon = navigationIcon, + navigationIcon = { + if (onBackClick != null) { + BackNavIcon(onBackClick) + } + }, title = { Row( verticalAlignment = Alignment.CenterVertically, @@ -60,8 +63,10 @@ fun AppTopBar( ) } -private fun backNavIcon(onBackClick: () -> Unit) = @Composable { - IconButton(onClick = onBackClick) { +// TODO use everywhere +@Composable +fun BackNavIcon(onClick: () -> Unit) { + IconButton(onClick = onClick) { Icon( imageVector = Icons.AutoMirrored.Default.ArrowBack, contentDescription = stringResource(R.string.common__back), diff --git a/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt index 5ac31da12..77bc51629 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -14,7 +13,6 @@ 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.lifecycle.SavedStateHandle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import to.bitkit.R @@ -24,19 +22,19 @@ import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsSwitchRow import to.bitkit.ui.navigateToAuthCheck +import to.bitkit.ui.navigateToDisablePin import to.bitkit.ui.navigateToHome import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.CloseNavIcon import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.settings.pin.PinNavigationSheet import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.rememberBiometricAuthSupported -import to.bitkit.ui.settings.pin.PinNavigationSheet @Composable fun SecuritySettingsScreen( navController: NavController, - savedStateHandle: SavedStateHandle, ) { val app = appViewModel ?: return @@ -45,24 +43,6 @@ fun SecuritySettingsScreen( val isPinOnLaunchEnabled by app.isPinOnLaunchEnabled.collectAsStateWithLifecycle() val isBiometricEnabled by app.isBiometricEnabled.collectAsStateWithLifecycle() - LaunchedEffect(savedStateHandle) { - savedStateHandle.getStateFlow(AuthCheckAction.KEY, null) - .collect { actionId -> - if (actionId != null) { - when (actionId) { - AuthCheckAction.Id.TOGGLE_BIOMETRICS -> { - app.setIsBiometricEnabled(!isBiometricEnabled) - } - AuthCheckAction.Id.TOGGLE_PIN_ON_LAUNCH -> { - app.setIsPinOnLaunchEnabled(!isPinOnLaunchEnabled) - } - } - // cleanup - savedStateHandle.remove(AuthCheckAction.KEY) - } - } - } - PinNavigationSheet( showSheet = showPinSheet, showLaterButton = false, @@ -77,19 +57,18 @@ fun SecuritySettingsScreen( if (!isPinEnabled) { showPinSheet = true } else { - // TODO show Disable Pin screen - app.removePin() + navController.navigateToDisablePin() } }, onPinOnLaunchClick = { navController.navigateToAuthCheck( - onSuccessActionId = AuthCheckAction.Id.TOGGLE_PIN_ON_LAUNCH, + onSuccessActionId = AuthCheckAction.TOGGLE_PIN_ON_LAUNCH, ) }, onUseBiometricsClick = { navController.navigateToAuthCheck( requireBiometrics = true, - onSuccessActionId = AuthCheckAction.Id.TOGGLE_BIOMETRICS, + onSuccessActionId = AuthCheckAction.TOGGLE_BIOMETRICS, ) }, onBackClick = { navController.popBackStack() }, diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/DisablePinScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/DisablePinScreen.kt new file mode 100644 index 000000000..8d4c842b8 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/settings/pin/DisablePinScreen.kt @@ -0,0 +1,99 @@ +package to.bitkit.ui.settings.pin + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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 androidx.navigation.NavController +import androidx.navigation.navOptions +import to.bitkit.R +import to.bitkit.ui.Routes +import to.bitkit.ui.components.AuthCheckAction +import to.bitkit.ui.components.BodyM +import to.bitkit.ui.components.PrimaryButton +import to.bitkit.ui.navigateToAuthCheck +import to.bitkit.ui.navigateToHome +import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.scaffold.CloseNavIcon +import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors + +@Composable +fun DisablePinScreen( + navController: NavController, +) { + DisablePinContent( + onDisableClick = { + navController.navigateToAuthCheck( + requirePin = true, + onSuccessActionId = AuthCheckAction.DISABLE_PIN, + navOptions = navOptions { popUpTo(Routes.SecuritySettings) }, + ) + }, + onBackClick = { navController.popBackStack() }, + onCloseClick = { navController.navigateToHome() }, + ) +} + +@Composable +private fun DisablePinContent( + onDisableClick: () -> Unit = {}, + onBackClick: () -> Unit = {}, + onCloseClick: () -> Unit = {}, +) { + ScreenColumn { + AppTopBar( + titleText = stringResource(R.string.security__pin_disable_title), + onBackClick = onBackClick, + actions = { CloseNavIcon(onClick = onCloseClick) }, + ) + Column( + modifier = Modifier.padding(horizontal = 16.dp), + ) { + + BodyM( + text = stringResource(R.string.security__pin_disable_text), + color = Colors.White64, + ) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + Image( + painter = painterResource(R.drawable.shield), + contentDescription = null, + modifier = Modifier.size(256.dp) + ) + } + + PrimaryButton( + text = stringResource(R.string.security__pin_disable_button), + onClick = onDisableClick, + ) + Spacer(modifier = Modifier.height(16.dp)) + } + } +} + +@Preview +@Composable +private fun Preview() { + AppThemeSurface { + DisablePinContent() + } +}