mirror of
https://github.com/bitwarden/android.git
synced 2026-05-11 02:15:43 -05:00
[PM-6701] Add start registration screen and all the navigation logic necessary in landing screen
This commit is contained in:
@@ -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() },
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user