Skip to content

Commit

Permalink
feat(applock): forgot passcode - reset device [WPB-5094] (#2392)
Browse files Browse the repository at this point in the history
  • Loading branch information
saleniuk committed Nov 7, 2023
1 parent 9f141b4 commit 162eded
Show file tree
Hide file tree
Showing 19 changed files with 925 additions and 70 deletions.
Expand Up @@ -68,6 +68,7 @@ class AccountSwitchUseCase @Inject constructor(
return when (params) {
is SwitchAccountParam.SwitchToAccount -> switch(params.userId, current.await())
SwitchAccountParam.TryToSwitchToNextAccount -> getNextAccountIfPossibleAndSwitch(current.await())
SwitchAccountParam.Clear -> switch(null, current.await())
}
}

Expand Down Expand Up @@ -143,6 +144,7 @@ class AccountSwitchUseCase @Inject constructor(
sealed class SwitchAccountParam {
object TryToSwitchToNextAccount : SwitchAccountParam()
data class SwitchToAccount(val userId: UserId) : SwitchAccountParam()
data object Clear : SwitchAccountParam()
}

sealed class SwitchAccountResult {
Expand Down
44 changes: 27 additions & 17 deletions app/src/main/kotlin/com/wire/android/ui/AppLockActivity.kt
Expand Up @@ -21,10 +21,14 @@ import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.core.view.WindowCompat
import com.wire.android.appLogger
import com.wire.android.navigation.NavigationGraph
import com.wire.android.navigation.rememberNavigator
import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
import com.wire.android.ui.destinations.AppUnlockWithBiometricsScreenDestination
import com.wire.android.ui.destinations.EnterLockCodeScreenDestination
import com.wire.android.ui.theme.WireTheme
Expand All @@ -36,26 +40,32 @@ class AppLockActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
setContent {
WireTheme {
val canAuthenticateWithBiometrics = BiometricManager
.from(this)
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
val snackbarHostState = remember { SnackbarHostState() }
CompositionLocalProvider(
LocalSnackbarHostState provides snackbarHostState,
LocalActivity provides this
) {
WireTheme {
val canAuthenticateWithBiometrics = BiometricManager
.from(this)
.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)

val navigator = rememberNavigator(this@AppLockActivity::finish)
val navigator = rememberNavigator(this@AppLockActivity::finish)

val startDestination =
if (canAuthenticateWithBiometrics == BiometricManager.BIOMETRIC_SUCCESS) {
appLogger.i("appLock: requesting app Unlock with biometrics")
AppUnlockWithBiometricsScreenDestination
} else {
appLogger.i("appLock: requesting app Unlock with passcode")
EnterLockCodeScreenDestination
}
val startDestination =
if (canAuthenticateWithBiometrics == BiometricManager.BIOMETRIC_SUCCESS) {
appLogger.i("appLock: requesting app Unlock with biometrics")
AppUnlockWithBiometricsScreenDestination
} else {
appLogger.i("appLock: requesting app Unlock with passcode")
EnterLockCodeScreenDestination
}

NavigationGraph(
navigator = navigator,
startDestination = startDestination
)
NavigationGraph(
navigator = navigator,
startDestination = startDestination
)
}
}
}
}
Expand Down
47 changes: 32 additions & 15 deletions app/src/main/kotlin/com/wire/android/ui/common/WireDialog.kt
Expand Up @@ -57,9 +57,11 @@ import com.wire.android.ui.common.button.WireButtonState
import com.wire.android.ui.common.button.WirePrimaryButton
import com.wire.android.ui.common.button.WireSecondaryButton
import com.wire.android.ui.common.button.WireTertiaryButton
import com.wire.android.ui.common.progress.WireCircularProgressIndicator
import com.wire.android.ui.common.textfield.WirePasswordTextField
import com.wire.android.ui.markdown.MarkdownConstants
import com.wire.android.ui.theme.WireTheme
import com.wire.android.ui.theme.wireColorScheme
import com.wire.android.ui.theme.wireDimensions
import com.wire.android.ui.theme.wireTypography

Expand All @@ -79,7 +81,7 @@ fun WireDialog(
title: String,
text: String,
onDismiss: () -> Unit,
optionButton1Properties: WireDialogButtonProperties,
optionButton1Properties: WireDialogButtonProperties? = null,
optionButton2Properties: WireDialogButtonProperties? = null,
dismissButtonProperties: WireDialogButtonProperties? = null,
buttonsHorizontalAlignment: Boolean = true,
Expand All @@ -88,6 +90,7 @@ fun WireDialog(
contentPadding: PaddingValues = PaddingValues(MaterialTheme.wireDimensions.dialogContentPadding),
properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
centerContent: Boolean = false,
titleLoading: Boolean = false,
content: @Composable (() -> Unit)? = null
) {
WireDialog(
Expand All @@ -101,6 +104,7 @@ fun WireDialog(
shape = shape,
contentPadding = contentPadding,
title = title,
titleLoading = titleLoading,
text = buildAnnotatedString {
val style = SpanStyle(
color = colorsScheme().onBackground,
Expand All @@ -122,7 +126,7 @@ fun WireDialog(
title: String,
text: AnnotatedString? = null,
onDismiss: () -> Unit,
optionButton1Properties: WireDialogButtonProperties,
optionButton1Properties: WireDialogButtonProperties? = null,
optionButton2Properties: WireDialogButtonProperties? = null,
dismissButtonProperties: WireDialogButtonProperties? = null,
buttonsHorizontalAlignment: Boolean = true,
Expand All @@ -131,6 +135,7 @@ fun WireDialog(
contentPadding: PaddingValues = PaddingValues(MaterialTheme.wireDimensions.dialogContentPadding),
properties: DialogProperties = DialogProperties(usePlatformDefaultWidth = false),
centerContent: Boolean = false,
titleLoading: Boolean = false,
content: @Composable (() -> Unit)? = null
) {
Dialog(
Expand All @@ -146,6 +151,7 @@ fun WireDialog(
shape = shape,
contentPadding = contentPadding,
title = title,
titleLoading = titleLoading,
text = text,
centerContent = centerContent,
content = content
Expand All @@ -156,8 +162,9 @@ fun WireDialog(
@Composable
private fun WireDialogContent(
title: String,
titleLoading: Boolean = false,
text: AnnotatedString? = null,
optionButton1Properties: WireDialogButtonProperties,
optionButton1Properties: WireDialogButtonProperties? = null,
optionButton2Properties: WireDialogButtonProperties? = null,
dismissButtonProperties: WireDialogButtonProperties? = null,
buttonsHorizontalAlignment: Boolean = true,
Expand All @@ -181,16 +188,24 @@ private fun WireDialogContent(
.padding(contentPadding),
horizontalAlignment = if (centerContent) Alignment.CenterHorizontally else Alignment.Start
) {
Text(
text = title,
style = MaterialTheme.wireTypography.title02,
modifier = Modifier.padding(bottom = MaterialTheme.wireDimensions.dialogTextsSpacing)
)
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = title,
style = MaterialTheme.wireTypography.title02,
modifier = Modifier.weight(1f)
)
if (titleLoading) {
WireCircularProgressIndicator(progressColor = MaterialTheme.wireColorScheme.onBackground)
}
}
text?.let {
ClickableText(
text = text,
style = MaterialTheme.wireTypography.body01,
modifier = Modifier.padding(bottom = MaterialTheme.wireDimensions.dialogTextsSpacing),
modifier = Modifier.padding(
top = MaterialTheme.wireDimensions.dialogTextsSpacing,
bottom = MaterialTheme.wireDimensions.dialogTextsSpacing,
),
onClick = { offset ->
text.getStringAnnotations(
tag = MarkdownConstants.TAG_URL,
Expand All @@ -206,29 +221,31 @@ private fun WireDialogContent(
}
}

val containsAnyButton = dismissButtonProperties != null || optionButton1Properties != null || optionButton2Properties != null
val dialogButtonsSpacing = if (containsAnyButton) dimensions().dialogButtonsSpacing else dimensions().spacing0x
if (buttonsHorizontalAlignment) {
Row(Modifier.padding(top = MaterialTheme.wireDimensions.dialogButtonsSpacing)) {
Row(Modifier.padding(top = dialogButtonsSpacing)) {
dismissButtonProperties.getButton(Modifier.weight(1f))
if (dismissButtonProperties != null) {
Spacer(Modifier.width(MaterialTheme.wireDimensions.dialogButtonsSpacing))
Spacer(Modifier.width(dialogButtonsSpacing))
}
optionButton1Properties.getButton(Modifier.weight(1f))
if (optionButton2Properties != null) {
Spacer(Modifier.width(MaterialTheme.wireDimensions.dialogButtonsSpacing))
Spacer(Modifier.width(dialogButtonsSpacing))
}
optionButton2Properties.getButton(Modifier.weight(1f))
}
} else {
Column(Modifier.padding(top = MaterialTheme.wireDimensions.dialogButtonsSpacing)) {
Column(Modifier.padding(top = dialogButtonsSpacing)) {
optionButton1Properties.getButton()

if (optionButton2Properties != null) {
Spacer(Modifier.height(MaterialTheme.wireDimensions.dialogButtonsSpacing))
Spacer(Modifier.height(dialogButtonsSpacing))
}
optionButton2Properties.getButton()

if (dismissButtonProperties != null) {
Spacer(Modifier.height(MaterialTheme.wireDimensions.dialogButtonsSpacing))
Spacer(Modifier.height(dialogButtonsSpacing))
}
dismissButtonProperties.getButton()
}
Expand Down
@@ -0,0 +1,137 @@
/*
* Wire
* Copyright (C) 2023 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
package com.wire.android.ui.home.appLock.forgot

import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.window.DialogProperties
import com.wire.android.R
import com.wire.android.ui.common.WireDialog
import com.wire.android.ui.common.WireDialogButtonProperties
import com.wire.android.ui.common.WireDialogButtonType
import com.wire.android.ui.common.button.WireButtonState
import com.wire.android.ui.common.colorsScheme
import com.wire.android.ui.common.dimensions
import com.wire.android.ui.common.textfield.WirePasswordTextField
import com.wire.android.ui.common.textfield.WireTextFieldState
import com.wire.android.ui.theme.WireTheme
import com.wire.android.ui.theme.wireTypography
import com.wire.android.util.ui.PreviewMultipleThemes
import com.wire.android.util.ui.stringWithStyledArgs

@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ForgotLockCodeResetDeviceDialog(
username: String,
isPasswordValid: Boolean,
isResetDeviceEnabled: Boolean,
onPasswordChanged: (TextFieldValue) -> Unit,
onResetDeviceClicked: () -> Unit,
onDialogDismissed: () -> Unit
) {
var backupPassword by remember { mutableStateOf(TextFieldValue("")) }
var keyboardController: SoftwareKeyboardController? = null
val onDialogDismissHideKeyboard: () -> Unit = {
keyboardController?.hide()
onDialogDismissed()
}
WireDialog(
title = stringResource(R.string.settings_forgot_lock_screen_reset_device),
text = LocalContext.current.resources.stringWithStyledArgs(
R.string.settings_forgot_lock_screen_reset_device_description,
MaterialTheme.wireTypography.body01,
MaterialTheme.wireTypography.body02,
colorsScheme().onBackground,
colorsScheme().onBackground,
username
),
onDismiss = onDialogDismissHideKeyboard,
buttonsHorizontalAlignment = false,
dismissButtonProperties = WireDialogButtonProperties(
onClick = onDialogDismissHideKeyboard,
text = stringResource(id = R.string.label_cancel),
state = WireButtonState.Default
),
optionButton1Properties = WireDialogButtonProperties(
onClick = {
keyboardController?.hide()
onResetDeviceClicked()
},
text = stringResource(id = R.string.settings_forgot_lock_screen_reset_device),
type = WireDialogButtonType.Primary,
state = if (!isResetDeviceEnabled) WireButtonState.Disabled else WireButtonState.Error
)
) {
// keyboard controller from outside the Dialog doesn't work inside its content so we have to pass the state
// to the dialog's content and use keyboard controller from there
keyboardController = LocalSoftwareKeyboardController.current
WirePasswordTextField(
state = when {
!isPasswordValid -> WireTextFieldState.Error(stringResource(id = R.string.remove_device_invalid_password))
else -> WireTextFieldState.Default
},
value = backupPassword,
onValueChange = {
backupPassword = it
onPasswordChanged(it)
},
autofill = false,
keyboardActions = KeyboardActions(onDone = { keyboardController?.hide() }),
modifier = Modifier.padding(bottom = dimensions().spacing16x)
)
}
}

@Composable
fun ForgotLockCodeResettingDeviceDialog() {
WireDialog(
title = stringResource(R.string.settings_forgot_lock_screen_please_wait_label),
titleLoading = true,
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
onDismiss = {},
)
}

@PreviewMultipleThemes
@Composable
fun PreviewForgotLockCodeResetDeviceDialog() {
WireTheme(isPreview = true) {
ForgotLockCodeResetDeviceDialog("Username", true, true, {}, {}, {})
}
}

@PreviewMultipleThemes
@Composable
fun PreviewForgotLockCodeResettingDeviceDialog() {
WireTheme(isPreview = true) {
ForgotLockCodeResettingDeviceDialog()
}
}

0 comments on commit 162eded

Please sign in to comment.