[PM-6701] Add start registration screen and all the navigation logic necessary in landing screen

This commit is contained in:
André Bispo
2024-05-30 22:14:43 +01:00
parent a417e5cc11
commit bc44043bfc
8 changed files with 774 additions and 1 deletions

View File

@@ -23,6 +23,8 @@ import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.masterPasswordHint
import com.x8bit.bitwarden.ui.auth.feature.masterpasswordhint.navigateToMasterPasswordHint
import com.x8bit.bitwarden.ui.auth.feature.setpassword.navigateToSetPassword
import com.x8bit.bitwarden.ui.auth.feature.setpassword.setPasswordDestination
import com.x8bit.bitwarden.ui.auth.feature.startregistration.navigateToStartRegistration
import com.x8bit.bitwarden.ui.auth.feature.startregistration.startRegistrationDestination
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.navigateToTwoFactorLogin
import com.x8bit.bitwarden.ui.auth.feature.twofactorlogin.twoFactorLoginDestination
@@ -49,6 +51,14 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
)
},
)
startRegistrationDestination(
onNavigateBack = { navController.popBackStack() },
// TODO check necessary parameters
onNavigateToCompleteRegistration = { emailAddress, verificationToken, captchaToken ->
navController.popBackStack()
},
onNavigateToEnvironment = { navController.navigateToEnvironment() }
)
enterpriseSignOnDestination(
onNavigateBack = { navController.popBackStack() },
onNavigateToSetPassword = { navController.navigateToSetPassword() },
@@ -71,6 +81,7 @@ fun NavGraphBuilder.authGraph(navController: NavHostController) {
onNavigateToEnvironment = {
navController.navigateToEnvironment()
},
onNavigateToStartRegistration = { navController.navigateToStartRegistration()}
)
loginDestination(
onNavigateBack = { navController.popBackStack() },

View File

@@ -21,6 +21,7 @@ fun NavGraphBuilder.landingDestination(
onNavigateToCreateAccount: () -> Unit,
onNavigateToLogin: (emailAddress: String) -> Unit,
onNavigateToEnvironment: () -> Unit,
onNavigateToStartRegistration: () -> Unit
) {
composableWithStayTransitions(
route = LANDING_ROUTE,
@@ -29,6 +30,7 @@ fun NavGraphBuilder.landingDestination(
onNavigateToCreateAccount = onNavigateToCreateAccount,
onNavigateToLogin = onNavigateToLogin,
onNavigateToEnvironment = onNavigateToEnvironment,
onNavigateToStartRegistration = onNavigateToStartRegistration
)
}
}

View File

@@ -76,6 +76,7 @@ fun LandingScreen(
onNavigateToCreateAccount: () -> Unit,
onNavigateToLogin: (emailAddress: String) -> Unit,
onNavigateToEnvironment: () -> Unit,
onNavigateToStartRegistration: () -> Unit,
viewModel: LandingViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
@@ -87,6 +88,7 @@ fun LandingScreen(
)
LandingEvent.NavigateToEnvironment -> onNavigateToEnvironment()
LandingEvent.NavigateToStartRegistration -> onNavigateToStartRegistration()
}
}

View File

@@ -160,7 +160,11 @@ class LandingViewModel @Inject constructor(
}
private fun handleCreateAccountClicked() {
sendEvent(LandingEvent.NavigateToCreateAccount)
// TODO ADD FEATURE FLAG email-verification
if (true)
sendEvent(LandingEvent.NavigateToStartRegistration)
else
sendEvent(LandingEvent.NavigateToCreateAccount)
}
private fun handleDialogDismiss() {
@@ -245,6 +249,11 @@ sealed class LandingEvent {
*/
data object NavigateToCreateAccount : LandingEvent()
/**
* Navigates to the Start Registration screen.
*/
data object NavigateToStartRegistration : LandingEvent()
/**
* Navigates to the Login screen with the given email address and region label.
*/

View File

@@ -0,0 +1,38 @@
package com.x8bit.bitwarden.ui.auth.feature.startregistration
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
private const val START_REGISTRATION_ROUTE = "start_registration"
/**
* Navigate to the start registration screen.
*/
fun NavController.navigateToStartRegistration(navOptions: NavOptions? = null) {
this.navigate(START_REGISTRATION_ROUTE, navOptions)
}
/**
* Add the start registration screen to the nav graph.
*/
fun NavGraphBuilder.startRegistrationDestination(
onNavigateBack: () -> Unit,
onNavigateToCompleteRegistration: (
emailAddress: String,
verificationToken: String,
captchaToken: String
) -> Unit,
onNavigateToEnvironment: () -> Unit,
) {
composableWithSlideTransitions(
route = START_REGISTRATION_ROUTE,
) {
StartRegistrationScreen(
onNavigateBack = onNavigateBack,
onNavigateToCompleteRegistration = onNavigateToCompleteRegistration,
onNavigateToEnvironment = onNavigateToEnvironment,
)
}
}

View File

@@ -0,0 +1,301 @@
package com.x8bit.bitwarden.ui.auth.feature.startregistration
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
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.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTag
import androidx.compose.ui.semantics.toggleableState
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.AcceptPoliciesToggle
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.CloseClick
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.EmailInputChange
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.ErrorDialogDismiss
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.PrivacyPolicyClick
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.TermsClick
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.NameInputChange
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationEvent.NavigateToPrivacyPolicy
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationEvent.NavigateToTerms
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenLoadingDialog
import com.x8bit.bitwarden.ui.platform.components.dialog.LoadingDialogState
import com.x8bit.bitwarden.ui.platform.components.dropdown.EnvironmentSelector
import com.x8bit.bitwarden.ui.platform.components.field.BitwardenTextField
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenClickableText
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
/**
* Top level composable for the create account screen.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Suppress("LongMethod")
@Composable
fun StartRegistrationScreen(
onNavigateBack: () -> Unit,
onNavigateToCompleteRegistration: (
emailAddress: String,
verificationToken: String,
captchaToken: String) -> Unit,
onNavigateToEnvironment: () -> Unit,
intentManager: IntentManager = LocalIntentManager.current,
viewModel: StartRegistrationViewModel = hiltViewModel(),
) {
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
val context = LocalContext.current
EventsEffect(viewModel) { event ->
when (event) {
is NavigateToPrivacyPolicy -> {
intentManager.launchUri("https://bitwarden.com/privacy/".toUri())
}
is NavigateToTerms -> {
intentManager.launchUri("https://bitwarden.com/terms/".toUri())
}
is StartRegistrationEvent.NavigateBack -> onNavigateBack.invoke()
is StartRegistrationEvent.ShowToast -> {
Toast.makeText(context, event.text, Toast.LENGTH_SHORT).show()
}
is StartRegistrationEvent.NavigateToCaptcha -> {
intentManager.startCustomTabsActivity(uri = event.uri)
}
is StartRegistrationEvent.NavigateToCompleteRegistration -> {
onNavigateToCompleteRegistration(
event.email,
event.verificationToken,
event.captchaToken,
)
}
StartRegistrationEvent.NavigateToEnvironment -> onNavigateToEnvironment()
}
}
// Show dialog if needed:
when (val dialog = state.dialog) {
is StartRegistrationDialog.Error -> {
BitwardenBasicDialog(
visibilityState = dialog.state,
onDismissRequest = remember(viewModel) {
{ viewModel.trySendAction(ErrorDialogDismiss) }
},
)
}
StartRegistrationDialog.Loading -> {
BitwardenLoadingDialog(
visibilityState = LoadingDialogState.Shown(R.string.create_account.asText()),
)
}
null -> Unit
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
BitwardenScaffold(
modifier = Modifier
.fillMaxSize()
.nestedScroll(scrollBehavior.nestedScrollConnection),
topBar = {
BitwardenTopAppBar(
title = stringResource(id = R.string.create_account),
scrollBehavior = scrollBehavior,
navigationIcon = rememberVectorPainter(id = R.drawable.ic_close),
navigationIconContentDescription = stringResource(id = R.string.close),
onNavigationIconClick = remember(viewModel) {
{ viewModel.trySendAction(CloseClick) }
},
actions = {
BitwardenTextButton(
label = stringResource(id = R.string.continue_text),
onClick = remember(viewModel) {
{ viewModel.trySendAction(StartRegistrationAction.ContinueClick) }
},
modifier = Modifier.testTag("ContinueButton"),
)
},
)
},
) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.imePadding()
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
Spacer(modifier = Modifier.height(16.dp))
BitwardenTextField(
label = stringResource(id = R.string.email_address),
value = state.emailInput,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(EmailInputChange(it)) }
},
modifier = Modifier
.testTag("EmailAddressEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
keyboardType = KeyboardType.Email,
)
Spacer(modifier = Modifier.height(2.dp))
EnvironmentSelector(
labelText = stringResource(id = R.string.creating_on),
selectedOption = state.selectedEnvironmentType,
onOptionSelected = remember(viewModel) {
{ viewModel.trySendAction(StartRegistrationAction.EnvironmentTypeSelect(it)) }
},
modifier = Modifier
.testTag("RegionSelectorDropdown")
.padding(horizontal = 16.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(16.dp))
BitwardenTextField(
label = stringResource(id = R.string.name),
value = state.nameInput,
onValueChange = remember(viewModel) {
{ viewModel.trySendAction(NameInputChange(it)) }
},
modifier = Modifier
.testTag("NameEntry")
.fillMaxWidth()
.padding(horizontal = 16.dp),
)
Spacer(modifier = Modifier.height(16.dp))
TermsAndPrivacySwitch(
isChecked = state.isAcceptPoliciesToggled,
onCheckedChange = remember(viewModel) {
{ viewModel.trySendAction(AcceptPoliciesToggle(it)) }
},
onTermsClick = remember(viewModel) {
{ viewModel.trySendAction(TermsClick) }
},
onPrivacyPolicyClick = remember(viewModel) {
{ viewModel.trySendAction(PrivacyPolicyClick) }
},
)
Spacer(modifier = Modifier.navigationBarsPadding())
}
}
}
@OptIn(ExperimentalLayoutApi::class)
@Suppress("LongMethod")
@Composable
private fun TermsAndPrivacySwitch(
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onTermsClick: () -> Unit,
onPrivacyPolicyClick: () -> Unit,
) {
Row(
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.semantics(mergeDescendants = true) {
testTag = "AcceptPoliciesToggle"
toggleableState = ToggleableState(isChecked)
}
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
onClick = { onCheckedChange.invoke(!isChecked) },
)
.padding(start = 16.dp)
.fillMaxWidth(),
) {
Switch(
modifier = Modifier
.height(32.dp)
.width(52.dp),
checked = isChecked,
onCheckedChange = null,
)
Column(Modifier.padding(start = 16.dp, top = 4.dp, bottom = 4.dp)) {
Text(
text = stringResource(id = R.string.accept_policies),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurface,
)
FlowRow(
horizontalArrangement = Arrangement.Start,
modifier = Modifier
.padding(end = 16.dp)
.fillMaxWidth()
.wrapContentHeight(),
) {
BitwardenClickableText(
label = stringResource(id = R.string.terms_of_service),
onClick = onTermsClick,
style = MaterialTheme.typography.bodyMedium,
innerPadding = PaddingValues(vertical = 4.dp, horizontal = 0.dp),
color = MaterialTheme.colorScheme.primary,
)
Text(
text = ",",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 4.dp),
)
Spacer(modifier = Modifier.width(4.dp))
BitwardenClickableText(
label = stringResource(id = R.string.privacy_policy),
onClick = onPrivacyPolicyClick,
style = MaterialTheme.typography.bodyMedium,
innerPadding = PaddingValues(vertical = 4.dp, horizontal = 0.dp),
color = MaterialTheme.colorScheme.primary,
)
}
}
}
}

View File

@@ -0,0 +1,409 @@
package com.x8bit.bitwarden.ui.auth.feature.startregistration
import android.net.Uri
import android.os.Parcelable
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.auth.repository.model.RegisterResult
import com.x8bit.bitwarden.data.auth.repository.util.CaptchaCallbackTokenResult
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.AcceptPoliciesToggle
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.EmailInputChange
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.NameInputChange
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.PrivacyPolicyClick
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.CloseClick
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.TermsClick
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.ErrorDialogDismiss
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
import com.x8bit.bitwarden.ui.platform.base.util.asText
import com.x8bit.bitwarden.ui.platform.base.util.isValidEmail
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import javax.inject.Inject
private const val KEY_STATE = "state"
/**
* Models logic for the create account screen.
*/
@Suppress("TooManyFunctions")
@HiltViewModel
class StartRegistrationViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val authRepository: AuthRepository,
private val environmentRepository: EnvironmentRepository,
) : BaseViewModel<StartRegistrationState, StartRegistrationEvent, StartRegistrationAction>(
initialState = savedStateHandle[KEY_STATE]
?: StartRegistrationState(
emailInput = "",
nameInput = "",
isAcceptPoliciesToggled = false,
selectedEnvironmentType = environmentRepository.environment.type,
dialog = null,
),
) {
init {
// As state updates, write to saved state handle:
stateFlow
.onEach { savedStateHandle[KEY_STATE] = it }
.launchIn(viewModelScope)
authRepository
.captchaTokenResultFlow
.onEach {
sendAction(
StartRegistrationAction.Internal.ReceiveCaptchaToken(
tokenResult = it,
),
)
}
.launchIn(viewModelScope)
// Listen for changes in environment triggered both by this VM and externally.
environmentRepository
.environmentStateFlow
.onEach { environment ->
sendAction(
StartRegistrationAction.Internal.UpdatedEnvironmentReceive(environment = environment),
)
}
.launchIn(viewModelScope)
}
override fun handleAction(action: StartRegistrationAction) {
when (action) {
is StartRegistrationAction.ContinueClick -> handleContinueClick()
is EmailInputChange -> handleEmailInputChanged(action)
is NameInputChange -> handleNameInputChanged(action)
is CloseClick -> handleCloseClick()
is ErrorDialogDismiss -> handleDialogDismiss()
is AcceptPoliciesToggle -> handleAcceptPoliciesToggle(action)
is PrivacyPolicyClick -> handlePrivacyPolicyClick()
is TermsClick -> handleTermsClick()
is StartRegistrationAction.Internal.ReceiveRegisterResult -> {
// handleReceiveRegisterAccountResult(action)
}
is StartRegistrationAction.Internal.ReceiveCaptchaToken -> {
handleReceiveCaptchaToken(action)
}
is StartRegistrationAction.EnvironmentTypeSelect -> handleEnvironmentTypeSelect(action)
is StartRegistrationAction.Internal.UpdatedEnvironmentReceive -> {
handleUpdatedEnvironmentReceive(action)
}
}
}
private fun handleReceiveCaptchaToken(
action: StartRegistrationAction.Internal.ReceiveCaptchaToken,
) {
when (val result = action.tokenResult) {
is CaptchaCallbackTokenResult.MissingToken -> {
mutableStateFlow.update {
it.copy(
dialog = StartRegistrationDialog.Error(
BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(),
message = R.string.captcha_failed.asText(),
),
),
)
}
}
is CaptchaCallbackTokenResult.Success -> {
submitRegisterAccountRequest(
shouldCheckForDataBreaches = false,
shouldIgnorePasswordStrength = true,
captchaToken = result.token,
)
}
}
}
private fun handleEnvironmentTypeSelect(action: StartRegistrationAction.EnvironmentTypeSelect) {
val environment = when (action.environmentType) {
Environment.Type.US -> Environment.Us
Environment.Type.EU -> Environment.Eu
Environment.Type.SELF_HOSTED -> {
// Launch the self-hosted screen and select the full environment details there.
sendEvent(StartRegistrationEvent.NavigateToEnvironment)
return
}
}
// Update the environment in the repo; the VM state will update accordingly because it is
// listening for changes.
environmentRepository.environment = environment
}
private fun handleUpdatedEnvironmentReceive(
action: StartRegistrationAction.Internal.UpdatedEnvironmentReceive,
) {
mutableStateFlow.update {
it.copy(
selectedEnvironmentType = action.environment.type,
)
}
}
private fun handlePrivacyPolicyClick() = sendEvent(StartRegistrationEvent.NavigateToPrivacyPolicy)
private fun handleTermsClick() = sendEvent(StartRegistrationEvent.NavigateToTerms)
private fun handleAcceptPoliciesToggle(action: AcceptPoliciesToggle) {
mutableStateFlow.update {
it.copy(isAcceptPoliciesToggled = action.newState)
}
}
private fun handleDialogDismiss() {
mutableStateFlow.update {
it.copy(dialog = null)
}
}
private fun handleCloseClick() {
sendEvent(StartRegistrationEvent.NavigateBack)
}
private fun handleEmailInputChanged(action: EmailInputChange) {
mutableStateFlow.update { it.copy(emailInput = action.input) }
}
private fun handleNameInputChanged(action: NameInputChange) {
mutableStateFlow.update { it.copy(nameInput = action.input) }
}
private fun handleContinueClick() = when {
state.emailInput.isBlank() -> {
val dialog = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(),
message = R.string.validation_field_required
.asText(R.string.email_address.asText()),
)
mutableStateFlow.update { it.copy(dialog = StartRegistrationDialog.Error(dialog)) }
}
!state.emailInput.isValidEmail() -> {
val dialog = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(),
message = R.string.invalid_email.asText(),
)
mutableStateFlow.update { it.copy(dialog = StartRegistrationDialog.Error(dialog)) }
}
state.nameInput.isBlank() -> {
val dialog = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(),
message = R.string.validation_field_required
.asText(R.string.name.asText()),
)
mutableStateFlow.update { it.copy(dialog = StartRegistrationDialog.Error(dialog)) }
}
!state.isAcceptPoliciesToggled -> {
val dialog = BasicDialogState.Shown(
title = R.string.an_error_has_occurred.asText(),
message = R.string.accept_policies_error.asText(),
)
mutableStateFlow.update { it.copy(dialog = StartRegistrationDialog.Error(dialog)) }
}
else -> {
// TODO Call to send verification email
/*
submitRegisterAccountRequest(
shouldCheckForDataBreaches = state.isCheckDataBreachesToggled,
shouldIgnorePasswordStrength = false,
captchaToken = null,
)
*/
}
}
private fun submitRegisterAccountRequest(
shouldCheckForDataBreaches: Boolean,
shouldIgnorePasswordStrength: Boolean,
captchaToken: String?,
) {
mutableStateFlow.update {
it.copy(dialog = StartRegistrationDialog.Loading)
}
viewModelScope.launch {
// TODO change to send email service call
/*
val result = authRepository.register(
email = state.emailInput,
captchaToken = captchaToken,
)
sendAction(
StartRegistrationAction.Internal.ReceiveRegisterResult(
registerResult = result,
),
)*/
}
}
}
/**
* UI state for the create account screen.
*/
@Parcelize
data class StartRegistrationState(
val emailInput: String,
val nameInput: String,
val isAcceptPoliciesToggled: Boolean,
val selectedEnvironmentType: Environment.Type,
val dialog: StartRegistrationDialog?
) : Parcelable {
}
/**
* Models dialogs that can be displayed on the create account screen.
*/
sealed class StartRegistrationDialog : Parcelable {
/**
* Loading dialog.
*/
@Parcelize
data object Loading : StartRegistrationDialog()
/**
* General error dialog with an OK button.
*/
@Parcelize
data class Error(val state: BasicDialogState.Shown) : StartRegistrationDialog()
}
/**
* Models events for the create account screen.
*/
sealed class StartRegistrationEvent {
/**
* Navigate back to previous screen.
*/
data object NavigateBack : StartRegistrationEvent()
/**
* Placeholder event for showing a toast. Can be removed once there are real events.
*/
data class ShowToast(val text: String) : StartRegistrationEvent()
/**
* Navigates to the captcha verification screen.
*/
data class NavigateToCaptcha(val uri: Uri) : StartRegistrationEvent()
/**
* Navigates to the complete registration screen.
*/
data class NavigateToCompleteRegistration(
val email: String,
val verificationToken: String,
val captchaToken: String,
) : StartRegistrationEvent()
/**
* Navigate to terms and conditions.
*/
data object NavigateToTerms : StartRegistrationEvent()
/**
* Navigate to privacy policy.
*/
data object NavigateToPrivacyPolicy : StartRegistrationEvent()
/**
* Navigates to the self-hosted/custom environment screen.
*/
data object NavigateToEnvironment : StartRegistrationEvent()
}
/**
* Models actions for the create account screen.
*/
sealed class StartRegistrationAction {
/**
* User clicked submit.
*/
data object ContinueClick : StartRegistrationAction()
/**
* User clicked close.
*/
data object CloseClick : StartRegistrationAction()
/**
* Email input changed.
*/
data class EmailInputChange(val input: String) : StartRegistrationAction()
/**
* Name input changed.
*/
data class NameInputChange(val input: String) : StartRegistrationAction()
/**
* Indicates that the selection from the region drop down has changed.
*/
data class EnvironmentTypeSelect(
val environmentType: Environment.Type,
) : StartRegistrationAction()
/**
* User dismissed the error dialog.
*/
data object ErrorDialogDismiss : StartRegistrationAction()
/**
* User tapped accept policies toggle.
*/
data class AcceptPoliciesToggle(val newState: Boolean) : StartRegistrationAction()
/**
* User tapped privacy policy link.
*/
data object PrivacyPolicyClick : StartRegistrationAction()
/**
* User tapped terms link.
*/
data object TermsClick : StartRegistrationAction()
/**
* Models actions that the [StartRegistrationViewModel] itself might send.
*/
sealed class Internal : StartRegistrationAction() {
/**
* Indicates a captcha callback token has been received.
*/
data class ReceiveCaptchaToken(
val tokenResult: CaptchaCallbackTokenResult,
) : Internal()
/**
* Indicates a [RegisterResult] has been received.
*/
data class ReceiveRegisterResult(
val registerResult: RegisterResult,
) : Internal()
/**
* Indicates that there has been a change in [environment].
*/
data class UpdatedEnvironmentReceive(
val environment: Environment,
) : Internal()
}
}

View File

@@ -913,4 +913,5 @@ Do you want to switch to this account?</string>
<string name="passkey_operation_failed_because_of_missing_asset_links">Passkey operation failed because of missing asset links</string>
<string name="passkey_operation_failed_because_app_not_found_in_asset_links">Passkey operation failed because app not found in asset links</string>
<string name="passkey_operation_failed_because_app_could_not_be_verified">Passkey operation failed because app could not be verified</string>
<string name="creating_on">Creating on</string>
</resources>