generated from bitwarden/template
-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7e93c1a
commit 4c710f0
Showing
12 changed files
with
483 additions
and
276 deletions.
There are no files selected for viewing
27 changes: 27 additions & 0 deletions
27
...in/kotlin/com/bitwarden/authenticator/data/authenticator/repository/model/UnlockResult.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package com.bitwarden.authenticator.data.authenticator.repository.model | ||
|
||
/** | ||
* Models result of unlocking the vault. | ||
*/ | ||
sealed class UnlockResult { | ||
|
||
/** | ||
* Vault successfully unlocked. | ||
*/ | ||
data object Success : UnlockResult() | ||
|
||
/** | ||
* Incorrect password provided. | ||
*/ | ||
data object AuthenticationError : UnlockResult() | ||
|
||
/** | ||
* Unable to access user state information. | ||
*/ | ||
data object InvalidStateError : UnlockResult() | ||
|
||
/** | ||
* Generic error thrown by Bitwarden SDK. | ||
*/ | ||
data object GenericError : UnlockResult() | ||
} |
22 changes: 22 additions & 0 deletions
22
app/src/main/kotlin/com/bitwarden/authenticator/ui/auth/unlock/UnlockNavigation.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package com.bitwarden.authenticator.ui.auth.unlock | ||
|
||
import androidx.navigation.NavController | ||
import androidx.navigation.NavGraphBuilder | ||
import androidx.navigation.NavOptions | ||
import androidx.navigation.compose.composable | ||
|
||
const val UNLOCK_ROUTE: String = "unlock" | ||
|
||
fun NavController.navigateToUnlock( | ||
navOptions: NavOptions? = null, | ||
) { | ||
navigate(route = UNLOCK_ROUTE, navOptions = navOptions) | ||
} | ||
|
||
fun NavGraphBuilder.unlockDestination( | ||
onUnlocked: () -> Unit, | ||
) { | ||
composable(route = UNLOCK_ROUTE) { | ||
UnlockScreen(onUnlocked = onUnlocked) | ||
} | ||
} |
146 changes: 146 additions & 0 deletions
146
app/src/main/kotlin/com/bitwarden/authenticator/ui/auth/unlock/UnlockScreen.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
package com.bitwarden.authenticator.ui.auth.unlock | ||
|
||
import android.widget.Toast | ||
import androidx.compose.foundation.Image | ||
import androidx.compose.foundation.layout.Arrangement | ||
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.material3.ExperimentalMaterial3Api | ||
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.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.platform.LocalContext | ||
import androidx.compose.ui.res.painterResource | ||
import androidx.compose.ui.res.stringResource | ||
import androidx.compose.ui.unit.dp | ||
import androidx.hilt.navigation.compose.hiltViewModel | ||
import androidx.lifecycle.compose.collectAsStateWithLifecycle | ||
import com.bitwarden.authenticator.R | ||
import com.bitwarden.authenticator.ui.platform.base.util.EventsEffect | ||
import com.bitwarden.authenticator.ui.platform.base.util.asText | ||
import com.bitwarden.authenticator.ui.platform.components.button.BitwardenOutlinedButton | ||
import com.bitwarden.authenticator.ui.platform.components.dialog.BasicDialogState | ||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenBasicDialog | ||
import com.bitwarden.authenticator.ui.platform.components.dialog.BitwardenLoadingDialog | ||
import com.bitwarden.authenticator.ui.platform.components.dialog.LoadingDialogState | ||
import com.bitwarden.authenticator.ui.platform.components.scaffold.BitwardenScaffold | ||
import com.bitwarden.authenticator.ui.platform.manager.biometrics.BiometricsManager | ||
import com.bitwarden.authenticator.ui.platform.theme.LocalBiometricsManager | ||
|
||
@OptIn(ExperimentalMaterial3Api::class) | ||
@Composable | ||
fun UnlockScreen( | ||
viewModel: UnlockViewModel = hiltViewModel(), | ||
biometricsManager: BiometricsManager = LocalBiometricsManager.current, | ||
onUnlocked: () -> Unit, | ||
) { | ||
|
||
val state by viewModel.stateFlow.collectAsStateWithLifecycle() | ||
val context = LocalContext.current | ||
val resources = context.resources | ||
var showBiometricsPrompt by remember { mutableStateOf(true) } | ||
|
||
EventsEffect(viewModel = viewModel) { event -> | ||
when (event) { | ||
is UnlockEvent.ShowToast -> { | ||
Toast.makeText(context, event.message(resources), Toast.LENGTH_SHORT).show() | ||
} | ||
|
||
UnlockEvent.BiometricUnlock -> onUnlocked() | ||
} | ||
} | ||
|
||
when (val dialog = state.dialog) { | ||
is UnlockState.Dialog.Error -> BitwardenBasicDialog( | ||
visibilityState = BasicDialogState.Shown( | ||
title = R.string.an_error_has_occurred.asText(), | ||
message = dialog.message | ||
), | ||
onDismissRequest = remember(viewModel) { | ||
{ | ||
viewModel.trySendAction(UnlockAction.DismissDialog) | ||
} | ||
}, | ||
) | ||
|
||
UnlockState.Dialog.Loading -> BitwardenLoadingDialog( | ||
visibilityState = LoadingDialogState.Shown(R.string.loading.asText()) | ||
) | ||
|
||
null -> Unit | ||
} | ||
|
||
val onBiometricsUnlock: () -> Unit = remember(viewModel) { | ||
{ viewModel.trySendAction(UnlockAction.BiometricsUnlock) } | ||
} | ||
val onBiometricsLockOut: () -> Unit = remember(viewModel) { | ||
{ viewModel.trySendAction(UnlockAction.BiometricsLockout) } | ||
} | ||
|
||
if (showBiometricsPrompt) { | ||
biometricsManager.promptBiometrics( | ||
onSuccess = { | ||
showBiometricsPrompt = false | ||
onBiometricsUnlock() | ||
}, | ||
onCancel = { | ||
showBiometricsPrompt = false | ||
}, | ||
onError = { | ||
showBiometricsPrompt = false | ||
}, | ||
onLockOut = { | ||
showBiometricsPrompt = false | ||
onBiometricsLockOut() | ||
}, | ||
) | ||
} | ||
|
||
BitwardenScaffold( | ||
modifier = Modifier | ||
.fillMaxSize() | ||
) { innerPadding -> | ||
Box { | ||
Column( | ||
modifier = Modifier | ||
.padding(innerPadding) | ||
.fillMaxSize(), | ||
verticalArrangement = Arrangement.Center, | ||
horizontalAlignment = Alignment.CenterHorizontally | ||
) { | ||
Image( | ||
painter = painterResource(id = R.drawable.ic_logo_horizontal), | ||
contentDescription = stringResource(R.string.bitwarden_authenticator) | ||
) | ||
Spacer(modifier = Modifier.height(12.dp)) | ||
BitwardenOutlinedButton( | ||
label = stringResource(id = R.string.use_biometrics_to_unlock), | ||
onClick = { | ||
biometricsManager.promptBiometrics( | ||
onSuccess = onBiometricsUnlock, | ||
onCancel = { | ||
// no-op | ||
}, | ||
onError = { | ||
// no-op | ||
}, | ||
onLockOut = onBiometricsLockOut, | ||
) | ||
}, | ||
modifier = Modifier | ||
.padding(horizontal = 16.dp) | ||
.fillMaxWidth(), | ||
) | ||
} | ||
} | ||
} | ||
} |
120 changes: 120 additions & 0 deletions
120
app/src/main/kotlin/com/bitwarden/authenticator/ui/auth/unlock/UnlockViewModel.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
package com.bitwarden.authenticator.ui.auth.unlock | ||
|
||
import android.os.Parcelable | ||
import androidx.lifecycle.SavedStateHandle | ||
import androidx.lifecycle.viewModelScope | ||
import com.bitwarden.authenticator.R | ||
import com.bitwarden.authenticator.data.authenticator.repository.model.UnlockResult | ||
import com.bitwarden.authenticator.data.platform.manager.BiometricsEncryptionManager | ||
import com.bitwarden.authenticator.data.platform.repository.SettingsRepository | ||
import com.bitwarden.authenticator.ui.platform.base.BaseViewModel | ||
import com.bitwarden.authenticator.ui.platform.base.util.Text | ||
import com.bitwarden.authenticator.ui.platform.base.util.asText | ||
import dagger.hilt.android.lifecycle.HiltViewModel | ||
import kotlinx.coroutines.flow.launchIn | ||
import kotlinx.coroutines.flow.onEach | ||
import kotlinx.coroutines.flow.update | ||
import kotlinx.parcelize.Parcelize | ||
import javax.inject.Inject | ||
|
||
private const val KEY_STATE = "state" | ||
|
||
@HiltViewModel | ||
class UnlockViewModel @Inject constructor( | ||
savedStateHandle: SavedStateHandle, | ||
private val settingsRepository: SettingsRepository, | ||
private val biometricsEncryptionManager: BiometricsEncryptionManager, | ||
) : BaseViewModel<UnlockState, UnlockEvent, UnlockAction>( | ||
initialState = savedStateHandle[KEY_STATE] ?: run { | ||
UnlockState( | ||
isBiometricsEnabled = settingsRepository.isUnlockWithBiometricsEnabled, | ||
isBiometricsValid = biometricsEncryptionManager.isBiometricIntegrityValid(), | ||
dialog = null, | ||
) | ||
} | ||
) { | ||
|
||
init { | ||
stateFlow | ||
.onEach { savedStateHandle[KEY_STATE] = it } | ||
.launchIn(viewModelScope) | ||
} | ||
|
||
override fun handleAction(action: UnlockAction) { | ||
when (action) { | ||
UnlockAction.BiometricsUnlock -> { | ||
handleBiometricsUnlock() | ||
} | ||
|
||
UnlockAction.DismissDialog -> { | ||
handleDismissDialog() | ||
} | ||
|
||
UnlockAction.BiometricsLockout -> { | ||
handleBiometricsLockout() | ||
} | ||
} | ||
} | ||
|
||
private fun handleBiometricsUnlock() { | ||
if (state.isBiometricsEnabled && !state.isBiometricsValid) { | ||
biometricsEncryptionManager.setupBiometrics() | ||
} | ||
sendEvent(UnlockEvent.BiometricUnlock) | ||
} | ||
|
||
private fun handleDismissDialog() { | ||
mutableStateFlow.update { it.copy(dialog = null) } | ||
} | ||
|
||
private fun handleBiometricsLockout() { | ||
mutableStateFlow.update { | ||
it.copy( | ||
dialog = UnlockState.Dialog.Error( | ||
message = R.string.too_many_failed_biometric_attempts.asText(), | ||
) | ||
) | ||
} | ||
} | ||
} | ||
|
||
@Parcelize | ||
data class UnlockState( | ||
val isBiometricsEnabled: Boolean, | ||
val isBiometricsValid: Boolean, | ||
val dialog: Dialog?, | ||
) : Parcelable { | ||
|
||
@Parcelize | ||
sealed class Dialog : Parcelable { | ||
data class Error( | ||
val message: Text, | ||
) : Dialog() | ||
|
||
data object Loading : Dialog() | ||
} | ||
} | ||
|
||
sealed class UnlockEvent { | ||
|
||
data object BiometricUnlock : UnlockEvent() | ||
|
||
data class ShowToast( | ||
val message: Text, | ||
) : UnlockEvent() | ||
|
||
} | ||
|
||
sealed class UnlockAction { | ||
data object DismissDialog : UnlockAction() | ||
|
||
data object BiometricsLockout : UnlockAction() | ||
|
||
data object BiometricsUnlock : UnlockAction() | ||
|
||
sealed class Internal { | ||
data class ReceiveUnlockResult( | ||
val unlockResult: UnlockResult, | ||
) : Internal() | ||
} | ||
} |
65 changes: 65 additions & 0 deletions
65
...tlin/com/bitwarden/authenticator/ui/platform/components/button/BitwardenOutlinedButton.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package com.bitwarden.authenticator.ui.platform.components.button | ||
|
||
import androidx.compose.foundation.layout.PaddingValues | ||
import androidx.compose.material3.ButtonDefaults | ||
import androidx.compose.material3.MaterialTheme | ||
import androidx.compose.material3.OutlinedButton | ||
import androidx.compose.material3.Text | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.semantics.semantics | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import androidx.compose.ui.unit.dp | ||
|
||
/** | ||
* Represents a Bitwarden-styled filled [OutlinedButton]. | ||
* | ||
* @param label The label for the button. | ||
* @param onClick The callback when the button is clicked. | ||
* @param modifier The [Modifier] to be applied to the button. | ||
* @param isEnabled Whether or not the button is enabled. | ||
*/ | ||
@Composable | ||
fun BitwardenOutlinedButton( | ||
label: String, | ||
onClick: () -> Unit, | ||
modifier: Modifier = Modifier, | ||
isEnabled: Boolean = true, | ||
) { | ||
OutlinedButton( | ||
onClick = onClick, | ||
modifier = modifier | ||
.semantics(mergeDescendants = true) { }, | ||
enabled = isEnabled, | ||
contentPadding = PaddingValues( | ||
vertical = 10.dp, | ||
horizontal = 24.dp, | ||
), | ||
colors = ButtonDefaults.outlinedButtonColors(), | ||
) { | ||
Text( | ||
text = label, | ||
style = MaterialTheme.typography.labelLarge, | ||
) | ||
} | ||
} | ||
|
||
@Preview | ||
@Composable | ||
private fun BitwardenOutlinedButton_preview_isEnabled() { | ||
BitwardenOutlinedButton( | ||
label = "Label", | ||
onClick = {}, | ||
isEnabled = true, | ||
) | ||
} | ||
|
||
@Preview | ||
@Composable | ||
private fun BitwardenOutlinedButton_preview_isNotEnabled() { | ||
BitwardenOutlinedButton( | ||
label = "Label", | ||
onClick = {}, | ||
isEnabled = false, | ||
) | ||
} |
Oops, something went wrong.