diff --git a/app/src/main/java/to/bitkit/data/SettingsStore.kt b/app/src/main/java/to/bitkit/data/SettingsStore.kt index cbb617c1b..c2fcda99e 100644 --- a/app/src/main/java/to/bitkit/data/SettingsStore.kt +++ b/app/src/main/java/to/bitkit/data/SettingsStore.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.map import to.bitkit.ext.enumValueOfOrNull import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.PrimaryDisplay +import to.bitkit.utils.Logger import javax.inject.Inject import javax.inject.Singleton @@ -76,6 +77,11 @@ class SettingsStore @Inject constructor( val isBiometricEnabled: Flow = store.data.map { it[IS_BIOMETRIC_ENABLED] == true } suspend fun setIsBiometricEnabled(value: Boolean) { store.edit { it[IS_BIOMETRIC_ENABLED] = value } } + suspend fun wipe() { + store.edit { it.clear() } + Logger.info("Deleted all user settings data.") + } + private companion object { private val PRIMARY_DISPLAY_UNIT_KEY = stringPreferencesKey("primary_display_unit") private val BTC_DISPLAY_UNIT_KEY = stringPreferencesKey("btc_display_unit") diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index f60018dd3..607b122aa 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -3,6 +3,9 @@ package to.bitkit.ui import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.viewModels +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen @@ -16,6 +19,7 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import to.bitkit.ui.components.AuthCheckView +import to.bitkit.ui.components.ForgotPinSheet import to.bitkit.ui.components.ToastOverlay import to.bitkit.ui.onboarding.CreateWalletWithPassphraseScreen import to.bitkit.ui.onboarding.IntroScreen @@ -97,11 +101,12 @@ class MainActivity : FragmentActivity() { onCreateClick = { scope.launch { try { + appViewModel.resetIsAuthenticatedState() walletViewModel.setInitNodeLifecycleState() walletViewModel.createWallet(bip39Passphrase = null) walletViewModel.setWalletExistsState() appViewModel.setShowEmptyState(true) - } catch (e: Exception) { + } catch (e: Throwable) { appViewModel.toast(e) } } @@ -135,12 +140,13 @@ class MainActivity : FragmentActivity() { onRestoreClick = { mnemonic, passphrase -> scope.launch { try { + appViewModel.resetIsAuthenticatedState() walletViewModel.setInitNodeLifecycleState() walletViewModel.isRestoringWallet = true walletViewModel.restoreWallet(mnemonic, passphrase) walletViewModel.setWalletExistsState() appViewModel.setShowEmptyState(false) - } catch (e: Exception) { + } catch (e: Throwable) { appViewModel.toast(e) } } @@ -158,11 +164,12 @@ class MainActivity : FragmentActivity() { onCreateClick = { passphrase -> scope.launch { try { + appViewModel.resetIsAuthenticatedState() walletViewModel.setInitNodeLifecycleState() walletViewModel.createWallet(bip39Passphrase = passphrase) walletViewModel.setWalletExistsState() appViewModel.setShowEmptyState(true) - } catch (e: Exception) { + } catch (e: Throwable) { appViewModel.toast(e) } } @@ -171,22 +178,33 @@ class MainActivity : FragmentActivity() { } } } else { - val isAuthenticated by appViewModel.isAuthenticated.collectAsStateWithLifecycle() + ContentView( + appViewModel = appViewModel, + walletViewModel = walletViewModel, + blocktankViewModel = blocktankViewModel, + currencyViewModel = currencyViewModel, + activityListViewModel = activityListViewModel, + transferViewModel = transferViewModel, + ) - if (!isAuthenticated) { + val isAuthenticated by appViewModel.isAuthenticated.collectAsStateWithLifecycle() + AnimatedVisibility( + visible = !isAuthenticated, + enter = fadeIn(), + exit = fadeOut(), + ) { AuthCheckView( showLogoOnPin = true, appViewModel = appViewModel, onSuccess = { appViewModel.setIsAuthenticated(true) }, ) - } else { - ContentView( - appViewModel = appViewModel, - walletViewModel = walletViewModel, - blocktankViewModel = blocktankViewModel, - currencyViewModel = currencyViewModel, - activityListViewModel = activityListViewModel, - transferViewModel = transferViewModel, + } + + val showForgotPinSheet by appViewModel.showForgotPinSheet.collectAsStateWithLifecycle() + if (showForgotPinSheet) { + ForgotPinSheet( + onDismiss = { appViewModel.setShowForgotPin(false) }, + onResetClick = { walletViewModel.wipeStorage() }, ) } } 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 53e99d9e6..a736b6fef 100644 --- a/app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt +++ b/app/src/main/java/to/bitkit/ui/components/AuthCheckView.kt @@ -1,6 +1,7 @@ package to.bitkit.ui.components import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -29,6 +30,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ui.scaffold.AppTopBar +import to.bitkit.ui.shared.util.blockPointerInputPassthrough import to.bitkit.ui.shared.util.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -58,7 +60,7 @@ fun AuthCheckView( validatePin = appViewModel::validatePin, onSuccess = onSuccess, onBack = onBack, - onClickForgotPin = { appViewModel.toast(Exception("TODO: Forgot PIN")) }, + onClickForgotPin = { appViewModel.setShowForgotPin(true) }, ) } @@ -85,6 +87,8 @@ private fun AuthCheckViewContent( contentAlignment = Alignment.Center, modifier = Modifier .fillMaxSize() + .background(Colors.Black) + .blockPointerInputPassthrough() .navigationBarsPadding() ) { if ((showBio && isBiometrySupported && !requirePin) || requireBiometrics) { diff --git a/app/src/main/java/to/bitkit/ui/components/ForgotPinSheet.kt b/app/src/main/java/to/bitkit/ui/components/ForgotPinSheet.kt new file mode 100644 index 000000000..802e056ce --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/components/ForgotPinSheet.kt @@ -0,0 +1,113 @@ +package to.bitkit.ui.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +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.foundation.layout.width +import androidx.compose.material3.BottomSheetDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +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 to.bitkit.R +import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.util.gradientBackground +import to.bitkit.ui.theme.AppShapes +import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.Colors +import to.bitkit.ui.theme.ModalSheetTopPadding + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ForgotPinSheet( + onDismiss: () -> Unit, + onResetClick: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + shape = AppShapes.sheet, + containerColor = Colors.Black, + dragHandle = { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .background(color = Colors.Gray6) + ) { + BottomSheetDefaults.DragHandle() + } + }, + modifier = Modifier + .fillMaxSize() + .padding(top = ModalSheetTopPadding) + ) { + ForgotPinSheetContent( + onResetClick = { + onDismiss() + onResetClick() + }, + ) + } +} + +@Composable +private fun ForgotPinSheetContent( + onResetClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + .fillMaxWidth() + .gradientBackground() + .padding(horizontal = 16.dp) + ) { + SheetTopBar(stringResource(R.string.security__pin_forgot_title)) + Spacer(modifier = Modifier.height(16.dp)) + + BodyM( + text = stringResource(R.string.security__pin_forgot_text), + color = Colors.White64, + ) + + Spacer(modifier = Modifier.weight(1f)) + Image( + painter = painterResource(R.drawable.restore), + contentDescription = null, + modifier = Modifier.width(256.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + + PrimaryButton( + text = stringResource(R.string.security__pin_forgot_reset), + onClick = onResetClick, + ) + + Spacer(modifier = Modifier.height(16.dp)) + } +} + +@Preview(showBackground = true) +@Composable +private fun Preview() { + AppThemeSurface { + ForgotPinSheetContent( + onResetClick = {}, + ) + } +} diff --git a/app/src/main/java/to/bitkit/ui/screens/SplashScreen.kt b/app/src/main/java/to/bitkit/ui/screens/SplashScreen.kt index 0025825b0..7aa804668 100644 --- a/app/src/main/java/to/bitkit/ui/screens/SplashScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/SplashScreen.kt @@ -9,13 +9,13 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.wrapContentSize 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.tooling.preview.Preview import to.bitkit.R +import to.bitkit.ui.shared.util.blockPointerInputPassthrough import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -32,16 +32,15 @@ fun SplashScreen(isVisible: Boolean = true) { ) ) { Box( + contentAlignment = Alignment.Center, modifier = Modifier .fillMaxSize() - .background(Colors.Brand), - contentAlignment = Alignment.Center, + .background(Colors.Brand) + .blockPointerInputPassthrough() ) { Image( painter = painterResource(id = R.drawable.splash_logo), contentDescription = null, - modifier = Modifier - .wrapContentSize() ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/sheets/NewTransactionSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/sheets/NewTransactionSheet.kt index f4030d157..5fe7743ec 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/sheets/NewTransactionSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/sheets/NewTransactionSheet.kt @@ -39,6 +39,7 @@ import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.theme.ModalSheetTopPadding import to.bitkit.ui.utils.localizedRandom import to.bitkit.viewmodels.AppViewModel @@ -76,7 +77,7 @@ fun NewTransactionSheet( modifier = Modifier .fillMaxSize() .gradientBackground() - .padding(top = 100.dp) + .padding(top = ModalSheetTopPadding) ) { NewTransactionSheetView( details = details, diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt index fe135ae62..e4be5e272 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/ChangePinScreen.kt @@ -68,7 +68,7 @@ fun ChangePinScreen( }, onBackClick = { navController.popBackStack() }, onCloseClick = { navController.navigateToHome() }, - onClickForgotPin = { app.toast(Exception("TODO: Forgot PIN")) }, + onClickForgotPin = { app.setShowForgotPin(true) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/pin/PinResultScreen.kt b/app/src/main/java/to/bitkit/ui/settings/pin/PinResultScreen.kt index 744b2ffb1..1ecf72ef3 100644 --- a/app/src/main/java/to/bitkit/ui/settings/pin/PinResultScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/pin/PinResultScreen.kt @@ -75,6 +75,8 @@ private fun PinResultContent( ) { SheetTopBar(stringResource(R.string.security__success_title)) + Spacer(modifier = Modifier.height(16.dp)) + Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier diff --git a/app/src/main/java/to/bitkit/ui/shared/util/Modifiers.kt b/app/src/main/java/to/bitkit/ui/shared/util/Modifiers.kt index 1d99bb466..3774912d1 100644 --- a/app/src/main/java/to/bitkit/ui/shared/util/Modifiers.kt +++ b/app/src/main/java/to/bitkit/ui/shared/util/Modifiers.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.composed import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput import to.bitkit.ui.theme.Colors /** @@ -65,3 +66,13 @@ fun Modifier.gradientBackground(): Modifier { ) ) } + +fun Modifier.blockPointerInputPassthrough(): Modifier { + return this.pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + awaitPointerEvent() + } + } + } +} diff --git a/app/src/main/java/to/bitkit/ui/theme/Defaults.kt b/app/src/main/java/to/bitkit/ui/theme/Defaults.kt index ff32f0302..1c27b11e3 100644 --- a/app/src/main/java/to/bitkit/ui/theme/Defaults.kt +++ b/app/src/main/java/to/bitkit/ui/theme/Defaults.kt @@ -11,6 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp @Immutable object AppTextFieldDefaults { @@ -112,3 +113,5 @@ object AppSwitchDefaults { uncheckedIconColor = Colors.Gray4, ) } + +val ModalSheetTopPadding = 125.dp diff --git a/app/src/main/java/to/bitkit/ui/theme/Shape.kt b/app/src/main/java/to/bitkit/ui/theme/Shape.kt index ea0525c7a..ccf0d656c 100644 --- a/app/src/main/java/to/bitkit/ui/theme/Shape.kt +++ b/app/src/main/java/to/bitkit/ui/theme/Shape.kt @@ -13,7 +13,7 @@ val Shapes = Shapes( ) object AppShapes { - val sheet = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp) + val sheet = RoundedCornerShape(topStart = 32.dp, topEnd = 32.dp) val small = RoundedCornerShape(8.dp) val smallButton = small val smallInput = small diff --git a/app/src/main/java/to/bitkit/ui/theme/Theme.kt b/app/src/main/java/to/bitkit/ui/theme/Theme.kt index d90d13201..6ac6adf87 100644 --- a/app/src/main/java/to/bitkit/ui/theme/Theme.kt +++ b/app/src/main/java/to/bitkit/ui/theme/Theme.kt @@ -11,16 +11,8 @@ import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.Color val Gray100 = Color(0xFFF4F4F4) -val Gray300 = Color(0xFFBDBDBD) -val Gray400 = Color(0xFFABABAB) val gray900 = Color(0xFF212121) -val Brand50 = Color(0xFFFFF1EE) -val Brand500 = Color(0xFFEC5428) -val Blue500 = Color(0xFF0085FF) -val Red500 = Color(0xFFF44336) val Green500 = Color(0xFF4CAF50) -val Orange500 = Color(0xFFFF9800) -val Purple500 = Color(0xFF9C27B0) val Purple700 = Color(0xFFB95CE8) val secondaryColor: Color @@ -58,6 +50,7 @@ private object ColorPalette { onPrimary = Colors.Black, onSecondary = Colors.White, outlineVariant = Colors.White10, // divider default + scrim = Colors.Black, ) } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index f6d06161e..9520ba2af 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -138,6 +138,13 @@ class AppViewModel @Inject constructor( private val _isAuthenticated = MutableStateFlow(false) val isAuthenticated = _isAuthenticated.asStateFlow() + private val _showForgotPinSheet = MutableStateFlow(false) + val showForgotPinSheet = _showForgotPinSheet.asStateFlow() + + fun setShowForgotPin(value: Boolean) { + _showForgotPinSheet.value = value + } + fun setIsAuthenticated(value: Boolean) { _isAuthenticated.value = value } @@ -171,14 +178,11 @@ class AppViewModel @Inject constructor( } } viewModelScope.launch { - delay(1500) + // Delays are required for auth check on launch functionality + delay(1000) + resetIsAuthenticatedState() + delay(500) splashVisible = false - - // Check if auth is needed after splash screen - val needsAuth = isPinEnabled.first() && isPinOnLaunchEnabled.first() - if (!needsAuth) { - _isAuthenticated.value = true - } } observeLdkNodeEvents() @@ -779,6 +783,15 @@ class AppViewModel @Inject constructor( } // region security + fun resetIsAuthenticatedState() { + viewModelScope.launch { + val needsAuth = isPinEnabled.first() && isPinOnLaunchEnabled.first() + if (!needsAuth) { + _isAuthenticated.value = true + } + } + } + fun validatePin(pin: String): Boolean { val storedPin = keychain.loadString(Keychain.Key.PIN.name) val isValid = storedPin == pin @@ -805,7 +818,10 @@ class AppViewModel @Inject constructor( return false } - fun addPin(pin: String) = editPin(pin) + fun addPin(pin: String) { + setIsPinOnLaunchEnabled(true) + editPin(pin) + } fun editPin(newPin: String) { setIsPinEnabled(true) diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 7a2dafa01..1d4fa503e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -389,6 +389,7 @@ class WalletViewModel @Inject constructor( } lightningService.wipeStorage(walletIndex = 0) appStorage.clear() + settingsStore.wipe() keychain.wipe() coreService.activity.removeAll() // todo: extract to repo & syncState after, like in removeAllActivities setWalletExistsState()