Skip to content

Commit 89a4a92

Browse files
authored
Merge pull request #107 from synonymdev/feat/sec-settings-auth-check
feat: Ask reauth to change security settings
2 parents 39aacc2 + a08fdbf commit 89a4a92

File tree

6 files changed

+178
-28
lines changed

6 files changed

+178
-28
lines changed

app/src/main/java/to/bitkit/ui/ContentView.kt

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import kotlinx.coroutines.launch
3737
import kotlinx.serialization.Serializable
3838
import to.bitkit.models.NewTransactionSheetDetails
3939
import to.bitkit.models.NodeLifecycleState
40+
import to.bitkit.ui.components.AuthCheckScreen
4041
import to.bitkit.ui.components.BottomSheetType
4142
import to.bitkit.ui.onboarding.InitializingWalletView
4243
import to.bitkit.ui.onboarding.WalletInitResult
@@ -244,6 +245,7 @@ fun ContentView(
244245
allActivity(activityListViewModel, navController)
245246
activityItem(activityListViewModel, navController)
246247
qrScanner(appViewModel, navController)
248+
authCheck(navController)
247249

248250
// TODO extract transferNavigation
249251
navigation<Routes.TransferRoot>(
@@ -473,8 +475,11 @@ private fun NavGraphBuilder.generalSettings(navController: NavHostController) {
473475
}
474476

475477
private fun NavGraphBuilder.securitySettings(navController: NavHostController) {
476-
composableWithDefaultTransitions<Routes.SecuritySettings> {
477-
SecuritySettingsScreen(navController)
478+
composableWithDefaultTransitions<Routes.SecuritySettings> { backStackEntry ->
479+
SecuritySettingsScreen(
480+
navController = navController,
481+
savedStateHandle = backStackEntry.savedStateHandle,
482+
)
478483
}
479484
}
480485

@@ -624,6 +629,18 @@ private fun NavGraphBuilder.qrScanner(
624629
}
625630
}
626631
}
632+
633+
private fun NavGraphBuilder.authCheck(
634+
navController: NavHostController,
635+
) {
636+
composable<Routes.AuthCheck> { navBackEntry ->
637+
val route = navBackEntry.toRoute<Routes.AuthCheck>()
638+
AuthCheckScreen(
639+
route = route,
640+
navController = navController,
641+
)
642+
}
643+
}
627644
// endregion
628645

629646
/**
@@ -671,6 +688,20 @@ fun NavController.navigateToSecuritySettings() = navigate(
671688
route = Routes.SecuritySettings,
672689
)
673690

691+
fun NavController.navigateToAuthCheck(
692+
showLogoOnPin: Boolean = false,
693+
requirePin: Boolean = false,
694+
requireBiometrics: Boolean = false,
695+
onSuccessActionId: String,
696+
) = navigate(
697+
route = Routes.AuthCheck(
698+
showLogoOnPin = showLogoOnPin,
699+
requirePin = requirePin,
700+
requireBiometrics = requireBiometrics,
701+
onSuccessActionId = onSuccessActionId,
702+
),
703+
)
704+
674705
fun NavController.navigateToDefaultUnitSettings() = navigate(
675706
route = Routes.DefaultUnitSettings,
676707
)
@@ -764,6 +795,14 @@ object Routes {
764795
@Serializable
765796
data object SecuritySettings
766797

798+
@Serializable
799+
data class AuthCheck(
800+
val showLogoOnPin: Boolean = false,
801+
val requirePin: Boolean = false,
802+
val requireBiometrics: Boolean = false,
803+
val onSuccessActionId: String,
804+
)
805+
767806
@Serializable
768807
data object DefaultUnitSettings
769808

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package to.bitkit.ui.components
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.navigation.NavController
5+
import to.bitkit.ui.Routes
6+
import to.bitkit.ui.appViewModel
7+
8+
@Composable
9+
fun AuthCheckScreen(
10+
navController: NavController,
11+
route: Routes.AuthCheck,
12+
) {
13+
val app = appViewModel ?: return
14+
15+
AuthCheckView(
16+
showLogoOnPin = route.showLogoOnPin,
17+
appViewModel = app,
18+
requireBiometrics = route.requireBiometrics,
19+
requirePin = route.requirePin,
20+
onSuccess = {
21+
navController.previousBackStackEntry
22+
?.savedStateHandle
23+
?.set(AuthCheckAction.KEY, route.onSuccessActionId)
24+
25+
navController.popBackStack()
26+
},
27+
)
28+
}
29+
30+
object AuthCheckAction {
31+
const val KEY = "auth_check_action_key"
32+
33+
object Id {
34+
const val TOGGLE_PIN_ON_LAUNCH = "toggle_pin_on_launch"
35+
const val TOGGLE_BIOMETRICS = "toggle_biometrics"
36+
}
37+
}

app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,11 @@ import to.bitkit.viewmodels.AppViewModel
3535

3636
@Composable
3737
fun AuthCheckView(
38-
showLogoOnPin: Boolean,
38+
showLogoOnPin: Boolean = false,
3939
appViewModel: AppViewModel,
4040
isBiometrySupported: Boolean = rememberBiometricAuthSupported(),
41+
requireBiometrics: Boolean = false,
42+
requirePin: Boolean = false,
4143
onSuccess: (() -> Unit)? = null,
4244
) {
4345
val isBiometricsEnabled by appViewModel.isBiometricEnabled.collectAsStateWithLifecycle()
@@ -48,6 +50,8 @@ fun AuthCheckView(
4850
isBiometrySupported = isBiometrySupported,
4951
showLogoOnPin = showLogoOnPin,
5052
attemptsRemaining = attemptsRemaining,
53+
requireBiometrics = requireBiometrics,
54+
requirePin = requirePin,
5155
validatePin = appViewModel::validatePin,
5256
onSuccess = onSuccess,
5357
)
@@ -59,6 +63,8 @@ private fun AuthCheckViewContent(
5963
isBiometrySupported: Boolean,
6064
showLogoOnPin: Boolean,
6165
attemptsRemaining: Int,
66+
requireBiometrics: Boolean = false,
67+
requirePin: Boolean = false,
6268
validatePin: (String) -> Boolean,
6369
onSuccess: (() -> Unit)? = null,
6470
) {
@@ -74,22 +80,20 @@ private fun AuthCheckViewContent(
7480
.fillMaxSize()
7581
.navigationBarsPadding()
7682
) {
77-
Column {
78-
if (showBio && isBiometrySupported) {
79-
BiometricsView(
80-
onSuccess = { onSuccess?.invoke() },
81-
onFailure = { showBio = false },
82-
)
83-
} else {
84-
PinPad(
85-
showLogo = showLogoOnPin,
86-
validatePin = validatePin,
87-
onSuccess = onSuccess,
88-
attemptsRemaining = attemptsRemaining,
89-
allowBiometrics = isBiometricsEnabled && isBiometrySupported,
90-
onShowBiometrics = { showBio = true },
91-
)
92-
}
83+
if ((showBio && isBiometrySupported && !requirePin) || requireBiometrics) {
84+
BiometricsView(
85+
onSuccess = { onSuccess?.invoke() },
86+
onFailure = { showBio = false },
87+
)
88+
} else {
89+
PinPad(
90+
showLogo = showLogoOnPin,
91+
validatePin = validatePin,
92+
onSuccess = onSuccess,
93+
attemptsRemaining = attemptsRemaining,
94+
allowBiometrics = isBiometricsEnabled && isBiometrySupported && !requirePin,
95+
onShowBiometrics = { showBio = true },
96+
)
9397
}
9498
}
9599
}
@@ -228,7 +232,7 @@ private fun PreviewPinAttempts() {
228232
onSuccess = {},
229233
isBiometricsEnabled = false,
230234
isBiometrySupported = true,
231-
showLogoOnPin = true,
235+
showLogoOnPin = false,
232236
validatePin = { true },
233237
attemptsRemaining = 6,
234238
)

app/src/main/java/to/bitkit/ui/components/BiometricsView.kt

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,59 @@
11
package to.bitkit.ui.components
22

3+
import androidx.compose.foundation.layout.Arrangement
34
import androidx.compose.foundation.layout.Column
45
import androidx.compose.foundation.layout.Spacer
6+
import androidx.compose.foundation.layout.fillMaxSize
57
import androidx.compose.foundation.layout.height
68
import androidx.compose.foundation.layout.size
79
import androidx.compose.material3.Icon
810
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.getValue
12+
import androidx.compose.runtime.mutableStateOf
13+
import androidx.compose.runtime.remember
14+
import androidx.compose.runtime.rememberCoroutineScope
15+
import androidx.compose.runtime.setValue
916
import androidx.compose.ui.Alignment
1017
import androidx.compose.ui.Modifier
1118
import androidx.compose.ui.res.painterResource
1219
import androidx.compose.ui.res.stringResource
20+
import androidx.compose.ui.tooling.preview.Preview
1321
import androidx.compose.ui.unit.dp
22+
import kotlinx.coroutines.delay
23+
import kotlinx.coroutines.launch
1424
import to.bitkit.R
25+
import to.bitkit.ui.shared.util.clickableAlpha
26+
import to.bitkit.ui.theme.AppThemeSurface
1527
import to.bitkit.ui.utils.BiometricPrompt
1628

1729
@Composable
1830
fun BiometricsView(
1931
onSuccess: (() -> Unit)? = null,
2032
onFailure: (() -> Unit)? = null,
2133
) {
34+
var shouldShowPrompt by remember { mutableStateOf(true) }
35+
val scope = rememberCoroutineScope()
36+
2237
Column(
2338
horizontalAlignment = Alignment.CenterHorizontally,
39+
verticalArrangement = Arrangement.Center,
40+
modifier = Modifier
41+
.fillMaxSize()
42+
.clickableAlpha {
43+
// trick to show biometric prompt again on UI click
44+
scope.launch {
45+
shouldShowPrompt = false
46+
delay(5)
47+
shouldShowPrompt = true
48+
}
49+
}
2450
) {
25-
BiometricPrompt(
26-
onSuccess = { onSuccess?.invoke() },
27-
onError = { onFailure?.invoke() },
28-
)
51+
if (shouldShowPrompt) {
52+
BiometricPrompt(
53+
onSuccess = { onSuccess?.invoke() },
54+
onError = { onFailure?.invoke() },
55+
)
56+
}
2957
Icon(
3058
painter = painterResource(R.drawable.ic_fingerprint),
3159
contentDescription = null,
@@ -40,3 +68,11 @@ fun BiometricsView(
4068
)
4169
}
4270
}
71+
72+
@Preview(showBackground = true)
73+
@Composable
74+
private fun Preview() {
75+
AppThemeSurface {
76+
BiometricsView()
77+
}
78+
}

app/src/main/java/to/bitkit/ui/settings/SecuritySettingsScreen.kt

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.padding
55
import androidx.compose.foundation.rememberScrollState
66
import androidx.compose.foundation.verticalScroll
77
import androidx.compose.runtime.Composable
8+
import androidx.compose.runtime.LaunchedEffect
89
import androidx.compose.runtime.getValue
910
import androidx.compose.runtime.mutableStateOf
1011
import androidx.compose.runtime.remember
@@ -13,13 +14,16 @@ import androidx.compose.ui.Modifier
1314
import androidx.compose.ui.res.stringResource
1415
import androidx.compose.ui.tooling.preview.Preview
1516
import androidx.compose.ui.unit.dp
17+
import androidx.lifecycle.SavedStateHandle
1618
import androidx.lifecycle.compose.collectAsStateWithLifecycle
1719
import androidx.navigation.NavController
1820
import to.bitkit.R
1921
import to.bitkit.ui.appViewModel
22+
import to.bitkit.ui.components.AuthCheckAction
2023
import to.bitkit.ui.components.BodyS
2124
import to.bitkit.ui.components.settings.SettingsButtonRow
2225
import to.bitkit.ui.components.settings.SettingsSwitchRow
26+
import to.bitkit.ui.navigateToAuthCheck
2327
import to.bitkit.ui.navigateToHome
2428
import to.bitkit.ui.scaffold.AppTopBar
2529
import to.bitkit.ui.scaffold.CloseNavIcon
@@ -32,6 +36,7 @@ import to.bitkit.ui.settings.pin.PinNavigationSheet
3236
@Composable
3337
fun SecuritySettingsScreen(
3438
navController: NavController,
39+
savedStateHandle: SavedStateHandle,
3540
) {
3641
val app = appViewModel ?: return
3742

@@ -40,6 +45,24 @@ fun SecuritySettingsScreen(
4045
val isPinOnLaunchEnabled by app.isPinOnLaunchEnabled.collectAsStateWithLifecycle()
4146
val isBiometricEnabled by app.isBiometricEnabled.collectAsStateWithLifecycle()
4247

48+
LaunchedEffect(savedStateHandle) {
49+
savedStateHandle.getStateFlow<String?>(AuthCheckAction.KEY, null)
50+
.collect { actionId ->
51+
if (actionId != null) {
52+
when (actionId) {
53+
AuthCheckAction.Id.TOGGLE_BIOMETRICS -> {
54+
app.setIsBiometricEnabled(!isBiometricEnabled)
55+
}
56+
AuthCheckAction.Id.TOGGLE_PIN_ON_LAUNCH -> {
57+
app.setIsPinOnLaunchEnabled(!isPinOnLaunchEnabled)
58+
}
59+
}
60+
// cleanup
61+
savedStateHandle.remove<String>(AuthCheckAction.KEY)
62+
}
63+
}
64+
}
65+
4366
PinNavigationSheet(
4467
showSheet = showPinSheet,
4568
showLaterButton = false,
@@ -58,8 +81,17 @@ fun SecuritySettingsScreen(
5881
app.removePin()
5982
}
6083
},
61-
onPinOnLaunchClick = { app.setIsPinOnLaunchEnabled(!isPinOnLaunchEnabled) }, // TODO auth check
62-
onUseBiometricsClick = { app.setIsBiometricEnabled(!isBiometricEnabled) }, // TODO auth check
84+
onPinOnLaunchClick = {
85+
navController.navigateToAuthCheck(
86+
onSuccessActionId = AuthCheckAction.Id.TOGGLE_PIN_ON_LAUNCH,
87+
)
88+
},
89+
onUseBiometricsClick = {
90+
navController.navigateToAuthCheck(
91+
requireBiometrics = true,
92+
onSuccessActionId = AuthCheckAction.Id.TOGGLE_BIOMETRICS,
93+
)
94+
},
6395
onBackClick = { navController.popBackStack() },
6496
onCloseClick = { navController.navigateToHome() },
6597
)

app/src/main/java/to/bitkit/ui/utils/BiometricPrompt.kt

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,14 @@ import androidx.compose.runtime.Composable
77
import androidx.compose.runtime.LaunchedEffect
88
import androidx.compose.runtime.remember
99
import androidx.compose.ui.platform.LocalContext
10+
import androidx.compose.ui.platform.LocalInspectionMode
1011
import androidx.compose.ui.res.stringResource
1112
import androidx.core.content.ContextCompat
1213
import androidx.fragment.app.FragmentActivity
1314
import to.bitkit.R
1415
import to.bitkit.utils.BiometricCrypto
1516
import to.bitkit.utils.Logger
1617

17-
private val biometricCrypto = BiometricCrypto()
18-
1918
@Composable
2019
fun BiometricPrompt(
2120
onSuccess: () -> Unit,
@@ -25,6 +24,8 @@ fun BiometricPrompt(
2524
cancelButtonText: String = stringResource(R.string.security__use_pin),
2625
) {
2726
val context = LocalContext.current
27+
val isPreview = LocalInspectionMode.current
28+
if (isPreview) return // no UI to preview here, it's all system UI
2829

2930
val title = run {
3031
val name = stringResource(R.string.security__bio)
@@ -100,6 +101,7 @@ private fun launchBiometricPrompt(
100101
onAuthFailed: (() -> Unit),
101102
onAuthError: ((errorCode: Int, errString: CharSequence) -> Unit),
102103
) {
104+
val biometricCrypto = BiometricCrypto()
103105
val executor = ContextCompat.getMainExecutor(activity)
104106

105107
val promptInfo = BiometricPrompt.PromptInfo.Builder()

0 commit comments

Comments
 (0)