diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d1bf963ea..4a27a2dcf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,6 +55,17 @@ + + + + + + + + + + + { specialCircumstanceManager.specialCircumstance = @@ -185,6 +188,14 @@ class MainViewModel @Inject constructor( ) } + completeRegistrationData != null -> { + specialCircumstanceManager.specialCircumstance = + SpecialCircumstance.CompleteRegistration( + completeRegistrationData = completeRegistrationData, + timestamp = Timestamp.now() + ) + } + autofillSaveItem != null -> { specialCircumstanceManager.specialCircumstance = SpecialCircumstance.AutofillSave( diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/util/CompleteRegistrationDataUtils.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/util/CompleteRegistrationDataUtils.kt new file mode 100644 index 000000000..315267ccf --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/util/CompleteRegistrationDataUtils.kt @@ -0,0 +1,19 @@ +package com.x8bit.bitwarden.data.auth.util + +import android.content.Intent +import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData + +/** + * Checks if the given [Intent] contains data to complete registration. + * The [CompleteRegistrationData] will be returned when present. + */ +fun Intent.getCompleteRegistrationDataIntentOrNull(): CompleteRegistrationData? { + val uri = data ?: return null + val email = uri?.getQueryParameter("email") ?: return null + val verificationToken = uri.getQueryParameter("verificationtoken") ?: return null + return CompleteRegistrationData( + email = email, + verificationToken = verificationToken + ) +} + diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/CompleteRegistrationData.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/CompleteRegistrationData.kt new file mode 100644 index 000000000..f1207f729 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/CompleteRegistrationData.kt @@ -0,0 +1,16 @@ +package com.x8bit.bitwarden.data.platform.manager.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +/** + * Required data to complete ongoing registration process. + * + * @property email The email of the user creating the account. + * @property verificationToken The token required to finish the registration process. + */ +@Parcelize +data class CompleteRegistrationData( + val email: String, + val verificationToken: String, +) : Parcelable diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt index a112956b6..709ec46e9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/model/SpecialCircumstance.kt @@ -1,6 +1,7 @@ package com.x8bit.bitwarden.data.platform.manager.model import android.os.Parcelable +import com.google.firebase.Timestamp import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData @@ -48,6 +49,15 @@ sealed class SpecialCircumstance : Parcelable { val shouldFinishWhenComplete: Boolean, ) : SpecialCircumstance() + /** + * The app was launched via AppLink in order to allow the user complete an ongoing registration. + */ + @Parcelize + data class CompleteRegistration( + val completeRegistrationData: CompleteRegistrationData, + val timestamp: Timestamp + ) : SpecialCircumstance() + /** * The app was launched via the credential manager framework in order to allow the user to * manually save a passkey to their vault. diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt index 0588fcca3..c8fee015e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/manager/util/SpecialCircumstanceExtensions.kt @@ -17,6 +17,7 @@ fun SpecialCircumstance.toAutofillSaveItemOrNull(): AutofillSaveItem? = SpecialCircumstance.GeneratorShortcut -> null SpecialCircumstance.VaultShortcut -> null is SpecialCircumstance.Fido2Save -> null + is SpecialCircumstance.CompleteRegistration -> null } /** @@ -31,6 +32,7 @@ fun SpecialCircumstance.toAutofillSelectionDataOrNull(): AutofillSelectionData? SpecialCircumstance.GeneratorShortcut -> null SpecialCircumstance.VaultShortcut -> null is SpecialCircumstance.Fido2Save -> null + is SpecialCircumstance.CompleteRegistration -> null } /** diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailNavigation.kt index 1f154a7f4..2a92accf9 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/checkemail/CheckEmailNavigation.kt @@ -18,6 +18,7 @@ private const val CHECK_EMAIL_ROUTE: String = "check_email/{$EMAIL_ADDRESS}" fun NavController.navigateToCheckEmail(emailAddress: String, navOptions: NavOptions? = null) { this.navigate("check_email/$emailAddress", navOptions) } + /** * Class to retrieve check email arguments from the [SavedStateHandle]. */ diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationNavigation.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationNavigation.kt index 604a37c16..9bda514d0 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationNavigation.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationNavigation.kt @@ -1,8 +1,10 @@ package com.x8bit.bitwarden.ui.auth.feature.completeregistration +import androidx.lifecycle.SavedStateHandle import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions +import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions private const val COMPLETE_REGISTRATION_ROUTE = "complete_registration" diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt index 5f43a6378..6e6c264a4 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/completeregistration/CompleteRegistrationViewModel.kt @@ -12,6 +12,8 @@ import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha +import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance +import com.x8bit.bitwarden.data.platform.manager.util.toFido2RequestOrNull import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.CheckDataBreachesToggle import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ConfirmPasswordInputChange import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.ContinueWithBreachedPasswordClick @@ -43,7 +45,7 @@ private const val MIN_PASSWORD_LENGTH = 12 @HiltViewModel class CompleteRegistrationViewModel @Inject constructor( savedStateHandle: SavedStateHandle, - private val authRepository: AuthRepository, + private val authRepository: AuthRepository ) : BaseViewModel( initialState = savedStateHandle[KEY_STATE] ?: CompleteRegistrationState( diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/PasswordStrengthIndicator.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/PasswordStrengthIndicator.kt deleted file mode 100644 index a9ca846d2..000000000 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/PasswordStrengthIndicator.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.x8bit.bitwarden.ui.auth.feature.createaccount - -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.animateFloatAsState -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.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.graphics.TransformOrigin -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.unit.dp -import com.x8bit.bitwarden.R -import com.x8bit.bitwarden.ui.platform.base.util.asText -import com.x8bit.bitwarden.ui.platform.theme.LocalNonMaterialColors - -/** - * Draws a password indicator that displays password strength based on the given [state]. - */ -@Suppress("LongMethod", "CyclomaticComplexMethod", "MagicNumber") -@Composable -fun PasswordStrengthIndicator( - modifier: Modifier = Modifier, - state: PasswordStrengthState, -) { - val widthPercent by animateFloatAsState( - targetValue = when (state) { - PasswordStrengthState.NONE -> 0f - PasswordStrengthState.WEAK_1 -> .25f - PasswordStrengthState.WEAK_2 -> .5f - PasswordStrengthState.WEAK_3 -> .66f - PasswordStrengthState.GOOD -> .82f - PasswordStrengthState.STRONG -> 1f - }, - label = "Width Percent State", - ) - val indicatorColor = when (state) { - PasswordStrengthState.NONE -> MaterialTheme.colorScheme.error - PasswordStrengthState.WEAK_1 -> MaterialTheme.colorScheme.error - PasswordStrengthState.WEAK_2 -> MaterialTheme.colorScheme.error - PasswordStrengthState.WEAK_3 -> LocalNonMaterialColors.current.passwordWeak - PasswordStrengthState.GOOD -> MaterialTheme.colorScheme.primary - PasswordStrengthState.STRONG -> LocalNonMaterialColors.current.passwordStrong - } - val animatedIndicatorColor by animateColorAsState( - targetValue = indicatorColor, - label = "Indicator Color State", - ) - val label = when (state) { - PasswordStrengthState.NONE -> "".asText() - PasswordStrengthState.WEAK_1 -> R.string.weak.asText() - PasswordStrengthState.WEAK_2 -> R.string.weak.asText() - PasswordStrengthState.WEAK_3 -> R.string.weak.asText() - PasswordStrengthState.GOOD -> R.string.good.asText() - PasswordStrengthState.STRONG -> R.string.strong.asText() - } - Column( - modifier = modifier, - ) { - Box( - Modifier - .fillMaxWidth() - .height(4.dp) - .background(MaterialTheme.colorScheme.surfaceContainerHigh), - ) { - Box( - modifier = Modifier - .fillMaxHeight() - .fillMaxWidth() - .graphicsLayer { - transformOrigin = TransformOrigin(pivotFractionX = 0f, pivotFractionY = 0f) - scaleX = widthPercent - } - .drawBehind { - drawRect(animatedIndicatorColor) - }, - ) - } - Spacer(Modifier.height(4.dp)) - Text( - text = label(), - style = MaterialTheme.typography.labelSmall, - color = indicatorColor, - ) - } -} - -/** - * Models various levels of password strength that can be displayed by [PasswordStrengthIndicator]. - */ -enum class PasswordStrengthState { - NONE, - WEAK_1, - WEAK_2, - WEAK_3, - GOOD, - STRONG, -} diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt index 923752acd..1866b2882 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/auth/feature/startregistration/StartRegistrationScreen.kt @@ -306,7 +306,7 @@ private fun TermsAndPrivacyText( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .semantics(mergeDescendants = true) { - testTag = "AcceptPoliciesText" + testTag = "DisclaimerText" } .fillMaxWidth(), ) { diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt index 23d4c11d4..3f8a2cd25 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavScreen.kt @@ -18,6 +18,7 @@ import androidx.navigation.navOptions import com.x8bit.bitwarden.ui.auth.feature.auth.AUTH_GRAPH_ROUTE import com.x8bit.bitwarden.ui.auth.feature.auth.authGraph import com.x8bit.bitwarden.ui.auth.feature.auth.navigateToAuthGraph +import com.x8bit.bitwarden.ui.auth.feature.completeregistration.navigateToCompleteRegistration import com.x8bit.bitwarden.ui.auth.feature.resetpassword.RESET_PASSWORD_ROUTE import com.x8bit.bitwarden.ui.auth.feature.resetpassword.navigateToResetPasswordGraph import com.x8bit.bitwarden.ui.auth.feature.resetpassword.resetPasswordDestination @@ -95,7 +96,8 @@ fun RootNavScreen( } val targetRoute = when (state) { - RootNavState.Auth -> AUTH_GRAPH_ROUTE + RootNavState.Auth, + is RootNavState.CompleteOngoingRegistration-> AUTH_GRAPH_ROUTE RootNavState.ResetPassword -> RESET_PASSWORD_ROUTE RootNavState.SetPassword -> SET_PASSWORD_ROUTE RootNavState.Splash -> SPLASH_ROUTE @@ -192,6 +194,11 @@ fun RootNavScreen( navOptions = rootNavOptions, ) } + + is RootNavState.CompleteOngoingRegistration -> { + navController.navigateToAuthGraph(rootNavOptions) + navController.navigateToCompleteRegistration() + } } } } diff --git a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt index 8fccd4220..01e03fb22 100644 --- a/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt +++ b/app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/rootnav/RootNavViewModel.kt @@ -2,6 +2,7 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav import android.os.Parcelable import androidx.lifecycle.viewModelScope +import com.google.firebase.Timestamp import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.UserState import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialRequest @@ -60,6 +61,7 @@ class RootNavViewModel @Inject constructor( action: RootNavAction.Internal.UserStateUpdateReceive, ) { val userState = action.userState + val specialCircumstance = action.specialCircumstance val updatedRootNavState = when { userState?.activeAccount?.trustedDevice?.isDeviceTrusted == false && !userState.activeAccount.isVaultUnlocked && @@ -69,12 +71,21 @@ class RootNavViewModel @Inject constructor( userState?.activeAccount?.needsPasswordReset == true -> RootNavState.ResetPassword + specialCircumstance is SpecialCircumstance.CompleteRegistration -> { + RootNavState.CompleteOngoingRegistration( + email = specialCircumstance.completeRegistrationData.email, + verificationToken = specialCircumstance.completeRegistrationData.verificationToken, + timestamp = specialCircumstance.timestamp + ) + } + + userState == null || !userState.activeAccount.isLoggedIn || userState.hasPendingAccountAddition -> RootNavState.Auth userState.activeAccount.isVaultUnlocked -> { - when (val specialCircumstance = action.specialCircumstance) { + when (specialCircumstance) { is SpecialCircumstance.AutofillSave -> { RootNavState.VaultUnlockedForAutofillSave( autofillSaveItem = specialCircumstance.autofillSaveItem, @@ -105,6 +116,13 @@ class RootNavViewModel @Inject constructor( SpecialCircumstance.VaultShortcut, null, -> RootNavState.VaultUnlocked(activeUserId = userState.activeAccount.userId) + + is SpecialCircumstance.CompleteRegistration -> + RootNavState.CompleteOngoingRegistration ( + email = specialCircumstance.completeRegistrationData.email, + verificationToken = specialCircumstance.completeRegistrationData.verificationToken, + timestamp = specialCircumstance.timestamp + ) } } @@ -200,6 +218,16 @@ sealed class RootNavState : Parcelable { @Parcelize data object VaultUnlockedForNewSend : RootNavState() + /** + * App should show the screen to complete an ongoing registration process. + */ + @Parcelize + data class CompleteOngoingRegistration ( + val email: String, + val verificationToken: String, + val timestamp: Timestamp + ) : RootNavState() + /** * App should show the auth confirmation screen for an unlocked user. */ diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreenTest.kt index 0c4e57ed5..72bc69be4 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountScreenTest.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextInput import androidx.core.net.toUri import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow +import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountViewModelTest.kt index bf09b1ae8..0801b442e 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/auth/feature/createaccount/CreateAccountViewModelTest.kt @@ -14,6 +14,7 @@ import com.x8bit.bitwarden.data.auth.repository.AuthRepository import com.x8bit.bitwarden.data.auth.repository.model.PasswordStrengthResult import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha +import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreenTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreenTest.kt index 585d2e165..e421c20ae 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreenTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultScreenTest.kt @@ -15,7 +15,7 @@ import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performScrollTo import androidx.compose.ui.test.performTextInput import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow -import com.x8bit.bitwarden.ui.auth.feature.createaccount.PasswordStrengthState +import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.feature.settings.exportvault.model.ExportVaultFormat diff --git a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModelTest.kt b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModelTest.kt index b9e1507cd..0adc9e588 100644 --- a/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModelTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/ui/platform/feature/settings/exportvault/ExportVaultViewModelTest.kt @@ -19,7 +19,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy import com.x8bit.bitwarden.data.vault.manager.FileManager import com.x8bit.bitwarden.data.vault.repository.VaultRepository import com.x8bit.bitwarden.data.vault.repository.model.ExportVaultDataResult -import com.x8bit.bitwarden.ui.auth.feature.createaccount.PasswordStrengthState +import com.x8bit.bitwarden.ui.auth.feature.completeregistration.PasswordStrengthState import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest import com.x8bit.bitwarden.ui.platform.base.util.asText import com.x8bit.bitwarden.ui.platform.feature.settings.exportvault.model.ExportVaultFormat