[Pm-6702] Pass arguments from AppLink to Complete registration. Compute region from url domain

This commit is contained in:
André Bispo
2024-06-25 23:34:11 +01:00
parent ec8c1e36b6
commit 75c2f0e97e
11 changed files with 119 additions and 26 deletions

View File

@@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.util
import android.content.Intent
import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
import com.x8bit.bitwarden.data.platform.repository.model.Environment
/**
* Checks if the given [Intent] contains data to complete registration.
@@ -9,11 +10,16 @@ import com.x8bit.bitwarden.data.platform.manager.model.CompleteRegistrationData
*/
fun Intent.getCompleteRegistrationDataIntentOrNull(): CompleteRegistrationData? {
val uri = data ?: return null
val host = uri.host ?: return null
val email = uri?.getQueryParameter("email") ?: return null
val verificationToken = uri.getQueryParameter("verificationtoken") ?: return null
if (!host.contains("bitwarden.eu") && !host.contains("bitwarden.com")) return null
val region = if (host.contains("bitwarden.eu")) Environment.Type.EU else Environment.Type.US
return CompleteRegistrationData(
email = email,
verificationToken = verificationToken
verificationToken = verificationToken,
region = region
)
}

View File

@@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.platform.manager.model
import android.os.Parcelable
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import kotlinx.parcelize.Parcelize
/**
@@ -13,4 +14,5 @@ import kotlinx.parcelize.Parcelize
data class CompleteRegistrationData(
val email: String,
val verificationToken: String,
val region: Environment.Type
) : Parcelable

View File

@@ -58,8 +58,9 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
startRegistrationDestination(
onNavigateBack = { navController.popBackStack() },
// TODO check necessary parameters
onNavigateToCompleteRegistration = { emailAddress, verificationToken, captchaToken ->
navController.navigateToCompleteRegistration()
onNavigateToCompleteRegistration = { emailAddress, verificationToken ->
navController.navigateToCompleteRegistration(emailAddress= emailAddress,
verificationToken = verificationToken )
},
onNavigateToCheckEmail = {emailAddress ->
navController.navigateToCheckEmail(emailAddress)

View File

@@ -4,16 +4,43 @@ import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.NavType
import androidx.navigation.navArgument
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val COMPLETE_REGISTRATION_ROUTE = "complete_registration"
private const val EMAIL_ADDRESS: String = "email_address"
private const val VERIFICATION_TOKEN: String = "verification_token"
private const val REGION: String = "region"
private const val COMPLETE_REGISTRATION_PREFIX = "complete_registration"
private const val COMPLETE_REGISTRATION_ROUTE = "$COMPLETE_REGISTRATION_PREFIX/{$EMAIL_ADDRESS}/{$VERIFICATION_TOKEN}/{$REGION}"
/**
* Class to retrieve login with device arguments from the [SavedStateHandle].
*/
@OmitFromCoverage
data class CompleteRegistrationArgs(
val emailAddress: String,
val verificationToken: String,
val region: Environment.Type
) {
constructor(savedStateHandle: SavedStateHandle) : this(
emailAddress = checkNotNull(savedStateHandle.get<String>(EMAIL_ADDRESS)),
verificationToken = checkNotNull(savedStateHandle.get<String>(VERIFICATION_TOKEN)),
region = checkNotNull(savedStateHandle.get<Environment.Type>(REGION))
)
}
/**
* Navigate to the complete registration screen.
*/
fun NavController.navigateToCompleteRegistration(navOptions: NavOptions? = null) {
this.navigate(COMPLETE_REGISTRATION_ROUTE, navOptions)
fun NavController.navigateToCompleteRegistration(
emailAddress: String,
verificationToken: String,
region: Environment.Type? = null,
navOptions: NavOptions? = null) {
this.navigate("$COMPLETE_REGISTRATION_PREFIX/$emailAddress/$verificationToken/$region", navOptions)
}
/**
@@ -25,6 +52,11 @@ fun NavGraphBuilder.completeRegistrationDestination(
) {
composableWithSlideTransitions(
route = COMPLETE_REGISTRATION_ROUTE,
arguments = listOf(
navArgument(EMAIL_ADDRESS) { type = NavType.StringType },
navArgument(VERIFICATION_TOKEN) { type = NavType.StringType },
navArgument(REGION) { type = NavType.EnumType(Environment.Type::class.java) },
),
) {
CompleteRegistrationScreen(
onNavigateBack = onNavigateBack,

View File

@@ -73,7 +73,7 @@ fun CompleteRegistrationScreen(
when (event) {
is CompleteRegistrationEvent.NavigateBack -> onNavigateBack.invoke()
is CompleteRegistrationEvent.ShowToast -> {
Toast.makeText(context, event.text, Toast.LENGTH_SHORT).show()
Toast.makeText(context, event.message(context.resources), Toast.LENGTH_SHORT).show()
}
is CompleteRegistrationEvent.NavigateToCaptcha -> {

View File

@@ -15,18 +15,22 @@ import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.manager.util.toFido2RequestOrNull
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
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
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.Internal.ReceivePasswordStrengthResult
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.PasswordHintChange
import com.x8bit.bitwarden.ui.auth.feature.completeregistration.CompleteRegistrationAction.PasswordInputChange
import com.x8bit.bitwarden.ui.auth.feature.landing.LandingEvent
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.concat
import com.x8bit.bitwarden.ui.platform.base.util.isValidEmail
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
import com.x8bit.bitwarden.ui.tools.feature.send.SendEvent
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
@@ -47,19 +51,24 @@ private const val MIN_PASSWORD_LENGTH = 12
class CompleteRegistrationViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val authRepository: AuthRepository,
private val environmentRepository: EnvironmentRepository,
private val specialCircumstance: SpecialCircumstanceManager
) : BaseViewModel<CompleteRegistrationState, CompleteRegistrationEvent, CompleteRegistrationAction>(
initialState = savedStateHandle[KEY_STATE]
?: CompleteRegistrationState(
userEmail = "",
emailVerificationToken = "",
passwordInput = "",
confirmPasswordInput = "",
passwordHintInput = "",
isCheckDataBreachesToggled = true,
dialog = null,
passwordStrengthState = PasswordStrengthState.NONE,
),
?: run {
val args = CompleteRegistrationArgs(savedStateHandle)
CompleteRegistrationState(
userEmail = args.emailAddress,
emailVerificationToken = args.verificationToken,
region = args.region,
passwordInput = "",
confirmPasswordInput = "",
passwordHintInput = "",
isCheckDataBreachesToggled = true,
dialog = null,
passwordStrengthState = PasswordStrengthState.NONE,
)
},
) {
/**
@@ -69,6 +78,8 @@ class CompleteRegistrationViewModel @Inject constructor(
private var passwordStrengthJob: Job = Job().apply { complete() }
init {
updateCurrentRegion()
verifyEmailAddress()
// As state updates, write to saved state handle:
stateFlow
.onEach { savedStateHandle[KEY_STATE] = it }
@@ -85,6 +96,38 @@ class CompleteRegistrationViewModel @Inject constructor(
.launchIn(viewModelScope)
}
// Update region when coming from an AppLink to complete the registration
private fun updateCurrentRegion() {
if (state.region == null) {
return
}
val environment = when (state.region) {
Environment.Type.US -> Environment.Us
Environment.Type.EU -> Environment.Eu
Environment.Type.SELF_HOSTED -> {
// Self-host does not have email confirmation
return
}
}
// Update the environment in the repo; the VM state will update accordingly because it is
// listening for changes.
environmentRepository.environment = environment
}
private fun verifyEmailAddress() {
mutableStateFlow.update {
it.copy(dialog = CompleteRegistrationDialog.Loading)
}
// TODO Add call to service to verify email
sendEvent(CompleteRegistrationEvent.ShowToast(message = R.string.email_verified.asText()))
mutableStateFlow.update {
it.copy(dialog = null)
}
}
override fun onCleared() {
// clean the specialCircumstance after being handled
specialCircumstance.specialCircumstance = null
@@ -365,6 +408,7 @@ class CompleteRegistrationViewModel @Inject constructor(
data class CompleteRegistrationState(
val userEmail: String,
val emailVerificationToken: String,
val region: Environment.Type,
val passwordInput: String,
val confirmPasswordInput: String,
val passwordHintInput: String,
@@ -442,9 +486,11 @@ sealed class CompleteRegistrationEvent {
data object NavigateBack : CompleteRegistrationEvent()
/**
* Placeholder event for showing a toast. Can be removed once there are real events.
* Show a toast with the given message.
*/
data class ShowToast(val text: String) : CompleteRegistrationEvent()
data class ShowToast(
val message: Text,
) : CompleteRegistrationEvent()
/**
* Navigates to the captcha verification screen.

View File

@@ -21,8 +21,7 @@ fun NavGraphBuilder.startRegistrationDestination(
onNavigateBack: () -> Unit,
onNavigateToCompleteRegistration: (
emailAddress: String,
verificationToken: String,
captchaToken: String
verificationToken: String
) -> Unit,
onNavigateToCheckEmail: (email: String) -> Unit,
onNavigateToEnvironment: () -> Unit,

View File

@@ -79,8 +79,7 @@ fun StartRegistrationScreen(
onNavigateBack: () -> Unit,
onNavigateToCompleteRegistration: (
emailAddress: String,
verificationToken: String,
captchaToken: String) -> Unit,
verificationToken: String) -> Unit,
onNavigateToCheckEmail: (email: String) -> Unit,
onNavigateToEnvironment: () -> Unit,
intentManager: IntentManager = LocalIntentManager.current,
@@ -114,8 +113,7 @@ fun StartRegistrationScreen(
is StartRegistrationEvent.NavigateToCompleteRegistration -> {
onNavigateToCompleteRegistration(
event.email,
event.verificationToken,
event.captchaToken,
event.verificationToken
)
}

View File

@@ -197,7 +197,11 @@ fun RootNavScreen(
is RootNavState.CompleteOngoingRegistration -> {
navController.navigateToAuthGraph(rootNavOptions)
navController.navigateToCompleteRegistration()
navController.navigateToCompleteRegistration(
emailAddress = currentState.email,
verificationToken = currentState.verificationToken,
region = currentState.region
)
}
}
}

View File

@@ -10,6 +10,7 @@ import com.x8bit.bitwarden.data.autofill.model.AutofillSaveItem
import com.x8bit.bitwarden.data.autofill.model.AutofillSelectionData
import com.x8bit.bitwarden.data.platform.manager.SpecialCircumstanceManager
import com.x8bit.bitwarden.data.platform.manager.model.SpecialCircumstance
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.combine
@@ -80,6 +81,7 @@ class RootNavViewModel @Inject constructor(
RootNavState.CompleteOngoingRegistration(
email = specialCircumstance.completeRegistrationData.email,
verificationToken = specialCircumstance.completeRegistrationData.verificationToken,
region = specialCircumstance.completeRegistrationData.region,
timestamp = specialCircumstance.timestamp
)
}
@@ -125,6 +127,7 @@ class RootNavViewModel @Inject constructor(
RootNavState.CompleteOngoingRegistration (
email = specialCircumstance.completeRegistrationData.email,
verificationToken = specialCircumstance.completeRegistrationData.verificationToken,
region = specialCircumstance.completeRegistrationData.region,
timestamp = specialCircumstance.timestamp
)
}
@@ -229,6 +232,7 @@ sealed class RootNavState : Parcelable {
data class CompleteOngoingRegistration (
val email: String,
val verificationToken: String,
val region: Environment.Type,
val timestamp: Timestamp
) : RootNavState()

View File

@@ -921,6 +921,7 @@ Do you want to switch to this account?</string>
<string name="check_your_email">Check your email</string>
<string name="open_email_app">Open email app</string>
<string name="go_back">Go back</string>
<string name="email_verified">Email verified</string>
<string name="no_email_go_back_to_edit_your_email_address">No email? Go back to edit your email address.</string>
<string name="or_log_in_you_may_already_have_an_account">Or log in, you may already have an account.</string>
<string name="get_emails_from_bitwarden_for_announcements_advices_and_research_opportunities_unsubscribe_any_time">Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time.</string>