Skip to content

Commit 53f58bd

Browse files
committed
feat: Forgot PIN
1 parent 9692276 commit 53f58bd

File tree

8 files changed

+163
-13
lines changed

8 files changed

+163
-13
lines changed

app/src/main/java/to/bitkit/data/SettingsStore.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.map
1212
import to.bitkit.ext.enumValueOfOrNull
1313
import to.bitkit.models.BitcoinDisplayUnit
1414
import to.bitkit.models.PrimaryDisplay
15+
import to.bitkit.utils.Logger
1516
import javax.inject.Inject
1617
import javax.inject.Singleton
1718

@@ -76,6 +77,11 @@ class SettingsStore @Inject constructor(
7677
val isBiometricEnabled: Flow<Boolean> = store.data.map { it[IS_BIOMETRIC_ENABLED] == true }
7778
suspend fun setIsBiometricEnabled(value: Boolean) { store.edit { it[IS_BIOMETRIC_ENABLED] = value } }
7879

80+
suspend fun wipe() {
81+
store.edit { it.clear() }
82+
Logger.info("Deleted all user settings data.")
83+
}
84+
7985
private companion object {
8086
private val PRIMARY_DISPLAY_UNIT_KEY = stringPreferencesKey("primary_display_unit")
8187
private val BTC_DISPLAY_UNIT_KEY = stringPreferencesKey("btc_display_unit")

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import dagger.hilt.android.AndroidEntryPoint
1919
import kotlinx.coroutines.launch
2020
import kotlinx.serialization.Serializable
2121
import to.bitkit.ui.components.AuthCheckView
22+
import to.bitkit.ui.components.ForgotPinSheet
2223
import to.bitkit.ui.components.ToastOverlay
2324
import to.bitkit.ui.onboarding.CreateWalletWithPassphraseScreen
2425
import to.bitkit.ui.onboarding.IntroScreen
@@ -100,11 +101,12 @@ class MainActivity : FragmentActivity() {
100101
onCreateClick = {
101102
scope.launch {
102103
try {
104+
appViewModel.resetIsAuthenticatedState()
103105
walletViewModel.setInitNodeLifecycleState()
104106
walletViewModel.createWallet(bip39Passphrase = null)
105107
walletViewModel.setWalletExistsState()
106108
appViewModel.setShowEmptyState(true)
107-
} catch (e: Exception) {
109+
} catch (e: Throwable) {
108110
appViewModel.toast(e)
109111
}
110112
}
@@ -138,12 +140,13 @@ class MainActivity : FragmentActivity() {
138140
onRestoreClick = { mnemonic, passphrase ->
139141
scope.launch {
140142
try {
143+
appViewModel.resetIsAuthenticatedState()
141144
walletViewModel.setInitNodeLifecycleState()
142145
walletViewModel.isRestoringWallet = true
143146
walletViewModel.restoreWallet(mnemonic, passphrase)
144147
walletViewModel.setWalletExistsState()
145148
appViewModel.setShowEmptyState(false)
146-
} catch (e: Exception) {
149+
} catch (e: Throwable) {
147150
appViewModel.toast(e)
148151
}
149152
}
@@ -161,11 +164,12 @@ class MainActivity : FragmentActivity() {
161164
onCreateClick = { passphrase ->
162165
scope.launch {
163166
try {
167+
appViewModel.resetIsAuthenticatedState()
164168
walletViewModel.setInitNodeLifecycleState()
165169
walletViewModel.createWallet(bip39Passphrase = passphrase)
166170
walletViewModel.setWalletExistsState()
167171
appViewModel.setShowEmptyState(true)
168-
} catch (e: Exception) {
172+
} catch (e: Throwable) {
169173
appViewModel.toast(e)
170174
}
171175
}
@@ -195,6 +199,14 @@ class MainActivity : FragmentActivity() {
195199
onSuccess = { appViewModel.setIsAuthenticated(true) },
196200
)
197201
}
202+
203+
val showForgotPinSheet by appViewModel.showForgotPinSheet.collectAsStateWithLifecycle()
204+
if (showForgotPinSheet) {
205+
ForgotPinSheet(
206+
onDismiss = { appViewModel.setShowForgotPin(false) },
207+
onResetClick = { walletViewModel.wipeStorage() },
208+
)
209+
}
198210
}
199211

200212
ToastOverlay(

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ fun AuthCheckView(
6060
validatePin = appViewModel::validatePin,
6161
onSuccess = onSuccess,
6262
onBack = onBack,
63-
onClickForgotPin = { appViewModel.toast(Exception("TODO: Forgot PIN")) },
63+
onClickForgotPin = { appViewModel.setShowForgotPin(true) },
6464
)
6565
}
6666

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package to.bitkit.ui.components
2+
3+
import androidx.compose.foundation.Image
4+
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.layout.Box
6+
import androidx.compose.foundation.layout.Column
7+
import androidx.compose.foundation.layout.Spacer
8+
import androidx.compose.foundation.layout.fillMaxSize
9+
import androidx.compose.foundation.layout.fillMaxWidth
10+
import androidx.compose.foundation.layout.height
11+
import androidx.compose.foundation.layout.padding
12+
import androidx.compose.foundation.layout.width
13+
import androidx.compose.material3.BottomSheetDefaults
14+
import androidx.compose.material3.ExperimentalMaterial3Api
15+
import androidx.compose.material3.ModalBottomSheet
16+
import androidx.compose.material3.rememberModalBottomSheetState
17+
import androidx.compose.runtime.Composable
18+
import androidx.compose.ui.Alignment
19+
import androidx.compose.ui.Modifier
20+
import androidx.compose.ui.res.painterResource
21+
import androidx.compose.ui.res.stringResource
22+
import androidx.compose.ui.tooling.preview.Preview
23+
import androidx.compose.ui.unit.dp
24+
import to.bitkit.R
25+
import to.bitkit.ui.scaffold.SheetTopBar
26+
import to.bitkit.ui.shared.util.gradientBackground
27+
import to.bitkit.ui.theme.AppShapes
28+
import to.bitkit.ui.theme.AppThemeSurface
29+
import to.bitkit.ui.theme.Colors
30+
import to.bitkit.ui.theme.ModalSheetTopPadding
31+
32+
@OptIn(ExperimentalMaterial3Api::class)
33+
@Composable
34+
fun ForgotPinSheet(
35+
onDismiss: () -> Unit,
36+
onResetClick: () -> Unit,
37+
) {
38+
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
39+
40+
ModalBottomSheet(
41+
onDismissRequest = onDismiss,
42+
sheetState = sheetState,
43+
shape = AppShapes.sheet,
44+
containerColor = Colors.Black,
45+
dragHandle = {
46+
Box(
47+
contentAlignment = Alignment.Center,
48+
modifier = Modifier
49+
.fillMaxWidth()
50+
.background(color = Colors.Gray6)
51+
) {
52+
BottomSheetDefaults.DragHandle()
53+
}
54+
},
55+
modifier = Modifier
56+
.fillMaxSize()
57+
.padding(top = ModalSheetTopPadding)
58+
) {
59+
ForgotPinSheetContent(
60+
onResetClick = {
61+
onDismiss()
62+
onResetClick()
63+
},
64+
)
65+
}
66+
}
67+
68+
@Composable
69+
private fun ForgotPinSheetContent(
70+
onResetClick: () -> Unit,
71+
modifier: Modifier = Modifier,
72+
) {
73+
Column(
74+
horizontalAlignment = Alignment.CenterHorizontally,
75+
modifier = modifier
76+
.fillMaxWidth()
77+
.gradientBackground()
78+
.padding(horizontal = 16.dp)
79+
) {
80+
SheetTopBar(stringResource(R.string.security__pin_forgot_title))
81+
Spacer(modifier = Modifier.height(16.dp))
82+
83+
BodyM(
84+
text = stringResource(R.string.security__pin_forgot_text),
85+
color = Colors.White64,
86+
)
87+
88+
Spacer(modifier = Modifier.weight(1f))
89+
Image(
90+
painter = painterResource(R.drawable.restore),
91+
contentDescription = null,
92+
modifier = Modifier.width(256.dp)
93+
)
94+
Spacer(modifier = Modifier.weight(1f))
95+
96+
PrimaryButton(
97+
text = stringResource(R.string.security__pin_forgot_reset),
98+
onClick = onResetClick,
99+
)
100+
101+
Spacer(modifier = Modifier.height(16.dp))
102+
}
103+
}
104+
105+
@Preview(showBackground = true)
106+
@Composable
107+
private fun Preview() {
108+
AppThemeSurface {
109+
ForgotPinSheetContent(
110+
onResetClick = {},
111+
)
112+
}
113+
}

app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ fun ChangePinScreen(
6868
},
6969
onBackClick = { navController.popBackStack() },
7070
onCloseClick = { navController.navigateToHome() },
71-
onClickForgotPin = { app.toast(Exception("TODO: Forgot PIN")) },
71+
onClickForgotPin = { app.setShowForgotPin(true) },
7272
)
7373
}
7474

app/src/main/java/to/bitkit/ui/settings/pin/PinResultScreen.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ private fun PinResultContent(
7575
) {
7676
SheetTopBar(stringResource(R.string.security__success_title))
7777

78+
Spacer(modifier = Modifier.height(16.dp))
79+
7880
Column(
7981
horizontalAlignment = Alignment.CenterHorizontally,
8082
modifier = Modifier

app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,13 @@ class AppViewModel @Inject constructor(
138138
private val _isAuthenticated = MutableStateFlow(false)
139139
val isAuthenticated = _isAuthenticated.asStateFlow()
140140

141+
private val _showForgotPinSheet = MutableStateFlow(false)
142+
val showForgotPinSheet = _showForgotPinSheet.asStateFlow()
143+
144+
fun setShowForgotPin(value: Boolean) {
145+
_showForgotPinSheet.value = value
146+
}
147+
141148
fun setIsAuthenticated(value: Boolean) {
142149
_isAuthenticated.value = value
143150
}
@@ -171,14 +178,11 @@ class AppViewModel @Inject constructor(
171178
}
172179
}
173180
viewModelScope.launch {
174-
delay(1500)
181+
// Delays are required for auth check on launch functionality
182+
delay(1000)
183+
resetIsAuthenticatedState()
184+
delay(500)
175185
splashVisible = false
176-
177-
// Check if auth is needed after splash screen
178-
val needsAuth = isPinEnabled.first() && isPinOnLaunchEnabled.first()
179-
if (!needsAuth) {
180-
_isAuthenticated.value = true
181-
}
182186
}
183187

184188
observeLdkNodeEvents()
@@ -779,6 +783,15 @@ class AppViewModel @Inject constructor(
779783
}
780784

781785
// region security
786+
fun resetIsAuthenticatedState() {
787+
viewModelScope.launch {
788+
val needsAuth = isPinEnabled.first() && isPinOnLaunchEnabled.first()
789+
if (!needsAuth) {
790+
_isAuthenticated.value = true
791+
}
792+
}
793+
}
794+
782795
fun validatePin(pin: String): Boolean {
783796
val storedPin = keychain.loadString(Keychain.Key.PIN.name)
784797
val isValid = storedPin == pin
@@ -805,7 +818,10 @@ class AppViewModel @Inject constructor(
805818
return false
806819
}
807820

808-
fun addPin(pin: String) = editPin(pin)
821+
fun addPin(pin: String) {
822+
setIsPinOnLaunchEnabled(true)
823+
editPin(pin)
824+
}
809825

810826
fun editPin(newPin: String) {
811827
setIsPinEnabled(true)

app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ class WalletViewModel @Inject constructor(
389389
}
390390
lightningService.wipeStorage(walletIndex = 0)
391391
appStorage.clear()
392+
settingsStore.wipe()
392393
keychain.wipe()
393394
coreService.activity.removeAll() // todo: extract to repo & syncState after, like in removeAllActivities
394395
setWalletExistsState()

0 commit comments

Comments
 (0)