Skip to content

Commit e9bac4e

Browse files
authored
Merge pull request #112 from synonymdev/feat/pin-change
Change PIN flow
2 parents 1bad750 + 83feca5 commit e9bac4e

File tree

11 files changed

+624
-18
lines changed

11 files changed

+624
-18
lines changed

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,11 @@ import to.bitkit.ui.settings.SecuritySettingsScreen
8181
import to.bitkit.ui.settings.SettingsScreen
8282
import to.bitkit.ui.settings.backups.BackupWalletScreen
8383
import to.bitkit.ui.settings.backups.RestoreWalletScreen
84+
import to.bitkit.ui.settings.pin.ChangePinConfirmScreen
85+
import to.bitkit.ui.settings.pin.ChangePinScreen
8486
import to.bitkit.ui.settings.pin.DisablePinScreen
87+
import to.bitkit.ui.settings.pin.ChangePinNewScreen
88+
import to.bitkit.ui.settings.pin.ChangePinResultScreen
8589
import to.bitkit.ui.utils.screenScaleIn
8690
import to.bitkit.ui.utils.screenScaleOut
8791
import to.bitkit.ui.utils.screenSlideIn
@@ -234,6 +238,10 @@ fun ContentView(
234238
generalSettings(navController)
235239
securitySettings(navController)
236240
disablePin(navController)
241+
changePin(navController)
242+
changePinNew(navController)
243+
changePinConfirm(navController)
244+
changePinResult(navController)
237245
defaultUnitSettings(currencyViewModel, navController)
238246
localCurrencySettings(currencyViewModel, navController)
239247
backupSettings(navController)
@@ -491,6 +499,34 @@ private fun NavGraphBuilder.disablePin(navController: NavHostController) {
491499
}
492500
}
493501

502+
private fun NavGraphBuilder.changePin(navController: NavHostController) {
503+
composableWithDefaultTransitions<Routes.ChangePin> {
504+
ChangePinScreen(navController)
505+
}
506+
}
507+
508+
private fun NavGraphBuilder.changePinNew(navController: NavHostController) {
509+
composableWithDefaultTransitions<Routes.ChangePinNew> {
510+
ChangePinNewScreen(navController)
511+
}
512+
}
513+
514+
private fun NavGraphBuilder.changePinConfirm(navController: NavHostController) {
515+
composableWithDefaultTransitions<Routes.ChangePinConfirm> { navBackEntry ->
516+
val route = navBackEntry.toRoute<Routes.ChangePinConfirm>()
517+
ChangePinConfirmScreen(
518+
newPin = route.newPin,
519+
navController = navController,
520+
)
521+
}
522+
}
523+
524+
private fun NavGraphBuilder.changePinResult(navController: NavHostController) {
525+
composableWithDefaultTransitions<Routes.ChangePinResult> {
526+
ChangePinResultScreen(navController)
527+
}
528+
}
529+
494530
private fun NavGraphBuilder.defaultUnitSettings(
495531
currencyViewModel: CurrencyViewModel,
496532
navController: NavHostController,
@@ -700,6 +736,22 @@ fun NavController.navigateToDisablePin() = navigate(
700736
route = Routes.DisablePin,
701737
)
702738

739+
fun NavController.navigateToChangePin() = navigate(
740+
route = Routes.ChangePin,
741+
)
742+
743+
fun NavController.navigateToChangePinNew() = navigate(
744+
route = Routes.ChangePinNew,
745+
)
746+
747+
fun NavController.navigateToChangePinConfirm(newPin: String) = navigate(
748+
route = Routes.ChangePinConfirm(newPin),
749+
)
750+
751+
fun NavController.navigateToChangePinResult() = navigate(
752+
route = Routes.ChangePinResult,
753+
)
754+
703755
fun NavController.navigateToAuthCheck(
704756
showLogoOnPin: Boolean = false,
705757
requirePin: Boolean = false,
@@ -812,6 +864,18 @@ object Routes {
812864
@Serializable
813865
data object DisablePin
814866

867+
@Serializable
868+
data object ChangePin
869+
870+
@Serializable
871+
data object ChangePinNew
872+
873+
@Serializable
874+
data class ChangePinConfirm(val newPin: String)
875+
876+
@Serializable
877+
data object ChangePinResult
878+
815879
@Serializable
816880
data class AuthCheck(
817881
val showLogoOnPin: Boolean = false,

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
2929
import to.bitkit.R
3030
import to.bitkit.env.Env
3131
import to.bitkit.ui.scaffold.AppTopBar
32+
import to.bitkit.ui.shared.util.clickableAlpha
3233
import to.bitkit.ui.theme.AppThemeSurface
3334
import to.bitkit.ui.theme.Colors
3435
import to.bitkit.ui.utils.rememberBiometricAuthSupported
@@ -57,6 +58,7 @@ fun AuthCheckView(
5758
validatePin = appViewModel::validatePin,
5859
onSuccess = onSuccess,
5960
onBack = onBack,
61+
onClickForgotPin = { appViewModel.toast(Exception("TODO: Forgot PIN")) },
6062
)
6163
}
6264

@@ -71,6 +73,7 @@ private fun AuthCheckViewContent(
7173
validatePin: (String) -> Boolean,
7274
onSuccess: (() -> Unit)? = null,
7375
onBack: (() -> Unit)?,
76+
onClickForgotPin: () -> Unit,
7477
) {
7578
var showBio by rememberSaveable { mutableStateOf(isBiometricsEnabled) }
7679

@@ -98,6 +101,7 @@ private fun AuthCheckViewContent(
98101
onShowBiometrics = { showBio = true },
99102
onSuccess = onSuccess,
100103
onBack = onBack,
104+
onClickForgotPin = onClickForgotPin,
101105
)
102106
}
103107
}
@@ -112,6 +116,7 @@ private fun PinPad(
112116
onShowBiometrics: () -> Unit,
113117
onSuccess: (() -> Unit)?,
114118
onBack: (() -> Unit)? = null,
119+
onClickForgotPin: () -> Unit = {},
115120
) {
116121
var pin by remember { mutableStateOf("") }
117122
val isLastAttempt = attemptsRemaining == 1
@@ -153,11 +158,11 @@ private fun PinPad(
153158
modifier = Modifier.padding(horizontal = 16.dp)
154159
)
155160
} else {
156-
// TODO: onClick: show forgotPin sheet
157161
BodyS(
158162
text = stringResource(R.string.security__pin_attempts).replace("{attemptsRemaining}", "$attemptsRemaining"),
159163
color = Colors.Brand,
160164
textAlign = TextAlign.Center,
165+
modifier = Modifier.clickableAlpha { onClickForgotPin() }
161166
)
162167
}
163168
Spacer(modifier = Modifier.height(16.dp))
@@ -213,6 +218,7 @@ private fun PreviewBio() {
213218
showLogoOnPin = true,
214219
validatePin = { true },
215220
attemptsRemaining = 8,
221+
onClickForgotPin = {},
216222
)
217223
}
218224
}
@@ -229,6 +235,7 @@ private fun PreviewPin() {
229235
showLogoOnPin = true,
230236
validatePin = { true },
231237
attemptsRemaining = 8,
238+
onClickForgotPin = {},
232239
)
233240
}
234241
}
@@ -245,6 +252,7 @@ private fun PreviewPinAttempts() {
245252
showLogoOnPin = false,
246253
validatePin = { true },
247254
attemptsRemaining = 6,
255+
onClickForgotPin = {},
248256
)
249257
}
250258
}
@@ -261,6 +269,7 @@ private fun PreviewPinAttemptLast() {
261269
showLogoOnPin = true,
262270
validatePin = { true },
263271
attemptsRemaining = 1,
272+
onClickForgotPin = {},
264273
)
265274
}
266275
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import to.bitkit.ui.components.BodyS
2222
import to.bitkit.ui.components.settings.SettingsButtonRow
2323
import to.bitkit.ui.components.settings.SettingsSwitchRow
2424
import to.bitkit.ui.navigateToAuthCheck
25+
import to.bitkit.ui.navigateToChangePin
2526
import to.bitkit.ui.navigateToDisablePin
2627
import to.bitkit.ui.navigateToHome
2728
import to.bitkit.ui.scaffold.AppTopBar
@@ -60,6 +61,9 @@ fun SecuritySettingsScreen(
6061
navController.navigateToDisablePin()
6162
}
6263
},
64+
onChangePinClick = {
65+
navController.navigateToChangePin()
66+
},
6367
onPinOnLaunchClick = {
6468
navController.navigateToAuthCheck(
6569
onSuccessActionId = AuthCheckAction.TOGGLE_PIN_ON_LAUNCH,
@@ -84,6 +88,7 @@ private fun SecuritySettingsContent(
8488
isBiometricEnabled: Boolean,
8589
isBiometrySupported: Boolean,
8690
onPinClick: () -> Unit = {},
91+
onChangePinClick: () -> Unit = {},
8792
onPinOnLaunchClick: () -> Unit = {},
8893
onUseBiometricsClick: () -> Unit = {},
8994
onBackClick: () -> Unit = {},
@@ -108,6 +113,10 @@ private fun SecuritySettingsContent(
108113
onClick = onPinClick,
109114
)
110115
if (isPinEnabled) {
116+
SettingsButtonRow(
117+
title = stringResource(R.string.settings__security__pin_change),
118+
onClick = onChangePinClick,
119+
)
111120
SettingsSwitchRow(
112121
title = stringResource(R.string.settings__security__pin_launch),
113122
isChecked = isPinOnLaunchEnabled,
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package to.bitkit.ui.settings.pin
2+
3+
import androidx.compose.animation.AnimatedVisibility
4+
import androidx.compose.foundation.layout.Column
5+
import androidx.compose.foundation.layout.Spacer
6+
import androidx.compose.foundation.layout.fillMaxWidth
7+
import androidx.compose.foundation.layout.height
8+
import androidx.compose.foundation.layout.padding
9+
import androidx.compose.runtime.Composable
10+
import androidx.compose.runtime.LaunchedEffect
11+
import androidx.compose.runtime.getValue
12+
import androidx.compose.runtime.mutableStateOf
13+
import androidx.compose.runtime.remember
14+
import androidx.compose.runtime.setValue
15+
import androidx.compose.ui.Alignment
16+
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.res.stringResource
18+
import androidx.compose.ui.text.style.TextAlign
19+
import androidx.compose.ui.tooling.preview.Preview
20+
import androidx.compose.ui.unit.dp
21+
import androidx.navigation.NavController
22+
import kotlinx.coroutines.delay
23+
import to.bitkit.R
24+
import to.bitkit.env.Env
25+
import to.bitkit.ui.appViewModel
26+
import to.bitkit.ui.components.BodyM
27+
import to.bitkit.ui.components.BodyS
28+
import to.bitkit.ui.components.KEY_DELETE
29+
import to.bitkit.ui.components.PinDots
30+
import to.bitkit.ui.components.PinNumberPad
31+
import to.bitkit.ui.navigateToHome
32+
import to.bitkit.ui.navigateToChangePinResult
33+
import to.bitkit.ui.scaffold.AppTopBar
34+
import to.bitkit.ui.scaffold.CloseNavIcon
35+
import to.bitkit.ui.scaffold.ScreenColumn
36+
import to.bitkit.ui.theme.AppThemeSurface
37+
import to.bitkit.ui.theme.Colors
38+
39+
@Composable
40+
fun ChangePinConfirmScreen(
41+
newPin: String,
42+
navController: NavController,
43+
) {
44+
val app = appViewModel ?: return
45+
var pin by remember { mutableStateOf("") }
46+
var showError by remember { mutableStateOf(false) }
47+
48+
LaunchedEffect(pin) {
49+
if (pin.length == Env.PIN_LENGTH) {
50+
if (pin == newPin) {
51+
app.editPin(newPin)
52+
navController.navigateToChangePinResult()
53+
} else {
54+
showError = true
55+
delay(500)
56+
pin = ""
57+
}
58+
}
59+
}
60+
61+
ChangePinConfirmContent(
62+
pin = pin,
63+
showError = showError,
64+
onKeyPress = { key ->
65+
if (key == KEY_DELETE) {
66+
if (pin.isNotEmpty()) {
67+
pin = pin.dropLast(1)
68+
}
69+
} else if (pin.length < Env.PIN_LENGTH) {
70+
pin += key
71+
}
72+
},
73+
onBackClick = { navController.popBackStack() },
74+
onCloseClick = { navController.navigateToHome() },
75+
)
76+
}
77+
78+
@Composable
79+
private fun ChangePinConfirmContent(
80+
pin: String,
81+
showError: Boolean,
82+
onKeyPress: (String) -> Unit,
83+
onBackClick: () -> Unit,
84+
onCloseClick: () -> Unit,
85+
) {
86+
ScreenColumn {
87+
AppTopBar(
88+
titleText = stringResource(R.string.security__cp_retype_title),
89+
onBackClick = onBackClick,
90+
actions = { CloseNavIcon(onClick = onCloseClick) },
91+
)
92+
93+
Column(
94+
horizontalAlignment = Alignment.CenterHorizontally,
95+
modifier = Modifier.padding(horizontal = 16.dp)
96+
) {
97+
BodyM(
98+
text = stringResource(R.string.security__cp_retype_text),
99+
color = Colors.White64,
100+
)
101+
102+
Spacer(modifier = Modifier.height(32.dp))
103+
104+
AnimatedVisibility(visible = showError) {
105+
BodyS(
106+
text = stringResource(R.string.security__pin_not_match),
107+
textAlign = TextAlign.Center,
108+
color = Colors.Brand,
109+
modifier = Modifier.fillMaxWidth()
110+
)
111+
}
112+
113+
PinDots(
114+
pin = pin,
115+
modifier = Modifier.padding(vertical = 16.dp),
116+
)
117+
118+
Spacer(modifier = Modifier.weight(1f))
119+
120+
PinNumberPad(
121+
modifier = Modifier.height(350.dp),
122+
onPress = onKeyPress,
123+
)
124+
}
125+
}
126+
}
127+
128+
@Preview(showBackground = true)
129+
@Composable
130+
private fun Preview() {
131+
AppThemeSurface {
132+
ChangePinConfirmContent(
133+
pin = "12",
134+
showError = false,
135+
onKeyPress = {},
136+
onBackClick = {},
137+
onCloseClick = {},
138+
)
139+
}
140+
}
141+
142+
@Preview(showBackground = true)
143+
@Composable
144+
private fun PreviewRetry() {
145+
AppThemeSurface {
146+
ChangePinConfirmContent(
147+
pin = "",
148+
showError = true,
149+
onKeyPress = {},
150+
onBackClick = {},
151+
onCloseClick = {},
152+
)
153+
}
154+
}

0 commit comments

Comments
 (0)