mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 11:18:45 +03:00
[Pm 10616] create account start design (#3751)
This commit is contained in:
parent
55b57a605e
commit
57c2e7ee4e
11 changed files with 689 additions and 143 deletions
|
@ -6,6 +6,8 @@ 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.UserState
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
|
@ -34,6 +36,7 @@ class LandingViewModel @Inject constructor(
|
|||
private val authRepository: AuthRepository,
|
||||
private val vaultRepository: VaultRepository,
|
||||
private val environmentRepository: EnvironmentRepository,
|
||||
private val featureFlagManager: FeatureFlagManager,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<LandingState, LandingEvent, LandingAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
|
@ -191,8 +194,13 @@ class LandingViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
private fun handleCreateAccountClicked() {
|
||||
// TODO PM-9401: ADD FEATURE FLAG email-verification and navigation to StartRegistration
|
||||
sendEvent(LandingEvent.NavigateToCreateAccount)
|
||||
@Suppress("MaxLineLength")
|
||||
val navigationEvent = if (featureFlagManager.getFeatureFlag(key = FlagKey.EmailVerification)) {
|
||||
LandingEvent.NavigateToStartRegistration
|
||||
} else {
|
||||
LandingEvent.NavigateToCreateAccount
|
||||
}
|
||||
sendEvent(navigationEvent)
|
||||
}
|
||||
|
||||
private fun handleDialogDismiss() {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.startregistration
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.Image
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
|
@ -13,13 +13,17 @@ 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.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
|
@ -33,6 +37,8 @@ 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.CustomAccessibilityAction
|
||||
import androidx.compose.ui.semantics.customActions
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.semantics.testTag
|
||||
import androidx.compose.ui.semantics.toggleableState
|
||||
|
@ -42,27 +48,24 @@ import androidx.compose.ui.text.SpanStyle
|
|||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||
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.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.CloseClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.ContinueClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.EmailInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.EnvironmentTypeSelect
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.ErrorDialogDismiss
|
||||
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.ReceiveMarketingEmailsToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.TermsClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.UnsubscribeMarketingEmailsClick
|
||||
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.auth.feature.startregistration.handlers.StartRegistrationHandler
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.handlers.rememberStartRegistrationHandler
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.createAnnotatedString
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
|
||||
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenFilledButton
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BitwardenBasicDialog
|
||||
|
@ -74,6 +77,7 @@ import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
|
|||
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
|
||||
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
|
||||
|
||||
/**
|
||||
* Constant string to be used in string annotation tag field
|
||||
|
@ -98,6 +102,7 @@ fun StartRegistrationScreen(
|
|||
viewModel: StartRegistrationViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
val handler = rememberStartRegistrationHandler(viewModel = viewModel)
|
||||
val context = LocalContext.current
|
||||
EventsEffect(viewModel) { event ->
|
||||
when (event) {
|
||||
|
@ -113,6 +118,12 @@ fun StartRegistrationScreen(
|
|||
intentManager.launchUri("https://bitwarden.com/email-preferences/".toUri())
|
||||
}
|
||||
|
||||
is StartRegistrationEvent.NavigateToServerSelectionInfo -> {
|
||||
intentManager.launchUri(
|
||||
uri = "https://bitwarden.com/help/server-geographies/".toUri(),
|
||||
)
|
||||
}
|
||||
|
||||
is StartRegistrationEvent.NavigateBack -> onNavigateBack.invoke()
|
||||
is StartRegistrationEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.text, Toast.LENGTH_SHORT).show()
|
||||
|
@ -164,11 +175,9 @@ fun StartRegistrationScreen(
|
|||
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) }
|
||||
},
|
||||
navigationIcon = rememberVectorPainter(id = R.drawable.ic_back),
|
||||
navigationIconContentDescription = stringResource(id = R.string.back),
|
||||
onNavigationIconClick = handler.onBackClick,
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
|
@ -179,102 +188,138 @@ fun StartRegistrationScreen(
|
|||
.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(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))
|
||||
|
||||
if (state.selectedEnvironmentType != Environment.Type.SELF_HOSTED) {
|
||||
ReceiveMarketingEmailsSwitch(
|
||||
isChecked = state.isReceiveMarketingEmailsToggled,
|
||||
onCheckedChange = remember(viewModel) {
|
||||
{
|
||||
viewModel.trySendAction(
|
||||
ReceiveMarketingEmailsToggle(
|
||||
it,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
onUnsubscribeClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(UnsubscribeMarketingEmailsClick) }
|
||||
},
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.continue_text),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ContinueClick) }
|
||||
},
|
||||
isEnabled = state.isContinueButtonEnabled,
|
||||
modifier = Modifier
|
||||
.testTag("ContinueButton")
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TermsAndPrivacyText(
|
||||
onTermsClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(TermsClick) }
|
||||
},
|
||||
onPrivacyPolicyClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(PrivacyPolicyClick) }
|
||||
},
|
||||
StartRegistrationContent(
|
||||
emailInput = state.emailInput,
|
||||
selectedEnvironmentType = state.selectedEnvironmentType,
|
||||
nameInput = state.nameInput,
|
||||
isReceiveMarketingEmailsToggled = state.isReceiveMarketingEmailsToggled,
|
||||
isContinueButtonEnabled = state.isContinueButtonEnabled,
|
||||
handler = handler,
|
||||
)
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun StartRegistrationContent(
|
||||
emailInput: String,
|
||||
selectedEnvironmentType: Environment.Type,
|
||||
nameInput: String,
|
||||
isReceiveMarketingEmailsToggled: Boolean,
|
||||
isContinueButtonEnabled: Boolean,
|
||||
handler: StartRegistrationHandler,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Image(
|
||||
painter = rememberVectorPainter(id = R.drawable.vault),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(132.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(48.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.name),
|
||||
value = nameInput,
|
||||
onValueChange = handler.onNameInputChange,
|
||||
modifier = Modifier
|
||||
.testTag("NameEntry")
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenTextField(
|
||||
label = if (emailInput.isEmpty()) {
|
||||
stringResource(R.string.email_address_required)
|
||||
} else {
|
||||
stringResource(
|
||||
id = R.string.email_address,
|
||||
)
|
||||
},
|
||||
placeholder = stringResource(R.string.email_address_required),
|
||||
value = emailInput,
|
||||
onValueChange = handler.onEmailInputChange,
|
||||
modifier = Modifier
|
||||
.testTag("EmailAddressEntry")
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
keyboardType = KeyboardType.Email,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.standardHorizontalMargin(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
EnvironmentSelector(
|
||||
labelText = stringResource(id = R.string.creating_on),
|
||||
selectedOption = selectedEnvironmentType,
|
||||
onOptionSelected = handler.onEnvironmentTypeSelect,
|
||||
modifier = Modifier
|
||||
.testTag("RegionSelectorDropdown"),
|
||||
)
|
||||
IconButton(
|
||||
onClick = handler.onServerGeologyHelpClick,
|
||||
// Align with design but keep accessible touch target of IconButton.
|
||||
modifier = Modifier.offset(y = (-8f).dp, x = 16.dp),
|
||||
) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(id = R.drawable.ic_tooltip_small),
|
||||
contentDescription = stringResource(R.string.help_with_server_geolocations),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
if (selectedEnvironmentType != Environment.Type.SELF_HOSTED) {
|
||||
ReceiveMarketingEmailsSwitch(
|
||||
isChecked = isReceiveMarketingEmailsToggled,
|
||||
onCheckedChange = handler.onReceiveMarketingEmailsToggle,
|
||||
onUnsubscribeClick = handler.onUnsubscribeMarketingEmailsClick,
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.continue_text),
|
||||
onClick = handler.onContinueClick,
|
||||
isEnabled = isContinueButtonEnabled,
|
||||
modifier = Modifier
|
||||
.testTag("ContinueButton")
|
||||
.standardHorizontalMargin()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
TermsAndPrivacyText(
|
||||
onTermsClick = handler.onTermsClick,
|
||||
onPrivacyPolicyClick = handler.onPrivacyPolicyClick,
|
||||
modifier = Modifier.standardHorizontalMargin(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun TermsAndPrivacyText(
|
||||
onTermsClick: () -> Unit,
|
||||
onPrivacyPolicyClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val strTerms = stringResource(id = R.string.terms_of_service)
|
||||
val strPrivacy = stringResource(id = R.string.privacy_policy)
|
||||
val annotatedLinkString: AnnotatedString = buildAnnotatedString {
|
||||
val strTermsAndPrivacy = stringResource(
|
||||
id = R.string.by_continuing_you_agree_to_the_terms_of_service_and_privacy_policy,
|
||||
)
|
||||
val strTerms = stringResource(id = R.string.terms_of_service)
|
||||
val strPrivacy = stringResource(id = R.string.privacy_policy)
|
||||
val startIndexTerms = strTermsAndPrivacy.indexOf(strTerms)
|
||||
val endIndexTerms = startIndexTerms + strTerms.length
|
||||
val startIndexPrivacy = strTermsAndPrivacy.indexOf(strPrivacy)
|
||||
|
@ -322,30 +367,47 @@ private fun TermsAndPrivacyText(
|
|||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.semantics(mergeDescendants = true) {
|
||||
testTag = "DisclaimerText"
|
||||
customActions = listOf(
|
||||
CustomAccessibilityAction(
|
||||
label = strTerms,
|
||||
action = {
|
||||
onTermsClick()
|
||||
true
|
||||
},
|
||||
),
|
||||
CustomAccessibilityAction(
|
||||
label = strPrivacy,
|
||||
action = {
|
||||
onPrivacyPolicyClick()
|
||||
true
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
val termsUrl = stringResource(id = R.string.terms_of_service)
|
||||
Column(Modifier.padding(start = 16.dp, top = 4.dp, bottom = 4.dp)) {
|
||||
ClickableText(
|
||||
text = annotatedLinkString,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
onClick = {
|
||||
annotatedLinkString
|
||||
.getStringAnnotations(TAG_URL, it, it)
|
||||
.firstOrNull()?.let { stringAnnotation ->
|
||||
if (stringAnnotation.item == termsUrl) {
|
||||
onTermsClick()
|
||||
} else {
|
||||
onPrivacyPolicyClick()
|
||||
}
|
||||
ClickableText(
|
||||
text = annotatedLinkString,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
textAlign = TextAlign.Center,
|
||||
),
|
||||
onClick = {
|
||||
annotatedLinkString
|
||||
.getStringAnnotations(TAG_URL, it, it)
|
||||
.firstOrNull()?.let { stringAnnotation ->
|
||||
if (stringAnnotation.item == termsUrl) {
|
||||
onTermsClick()
|
||||
} else {
|
||||
onPrivacyPolicyClick()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -355,27 +417,37 @@ private fun ReceiveMarketingEmailsSwitch(
|
|||
isChecked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
onUnsubscribeClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val unsubscribeString = stringResource(id = R.string.unsubscribe)
|
||||
@Suppress("MaxLineLength")
|
||||
val annotatedLinkString = createAnnotatedString(
|
||||
mainString = stringResource(id = R.string.get_emails_from_bitwarden_for_announcements_advices_and_research_opportunities_unsubscribe_any_time),
|
||||
highlights = listOf(stringResource(id = R.string.unsubscribe)),
|
||||
highlights = listOf(unsubscribeString),
|
||||
tag = TAG_URL,
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
modifier = modifier
|
||||
.semantics(mergeDescendants = true) {
|
||||
testTag = "ReceiveMarketingEmailsToggle"
|
||||
toggleableState = ToggleableState(isChecked)
|
||||
customActions = listOf(
|
||||
CustomAccessibilityAction(
|
||||
label = unsubscribeString,
|
||||
action = {
|
||||
onUnsubscribeClick()
|
||||
true
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
|
||||
onClick = { onCheckedChange.invoke(!isChecked) },
|
||||
)
|
||||
.padding(start = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Switch(
|
||||
|
@ -385,18 +457,69 @@ private fun ReceiveMarketingEmailsSwitch(
|
|||
checked = isChecked,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
Column(Modifier.padding(start = 16.dp, top = 4.dp, bottom = 4.dp)) {
|
||||
ClickableText(
|
||||
text = annotatedLinkString,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
onClick = {
|
||||
annotatedLinkString
|
||||
.getStringAnnotations(TAG_URL, it, it)
|
||||
.firstOrNull()?.let {
|
||||
onUnsubscribeClick()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
ClickableText(
|
||||
text = annotatedLinkString,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
onClick = {
|
||||
annotatedLinkString
|
||||
.getStringAnnotations(TAG_URL, it, it)
|
||||
.firstOrNull()?.let {
|
||||
onUnsubscribeClick()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewScreenSizes
|
||||
@Composable
|
||||
private fun StartRegistrationContentPreview_filledout() {
|
||||
BitwardenTheme {
|
||||
StartRegistrationContent(
|
||||
emailInput = "e@mail.com",
|
||||
selectedEnvironmentType = Environment.Type.US,
|
||||
nameInput = "Test User",
|
||||
isReceiveMarketingEmailsToggled = true,
|
||||
isContinueButtonEnabled = true,
|
||||
handler = StartRegistrationHandler(
|
||||
onEmailInputChange = {},
|
||||
onNameInputChange = {},
|
||||
onEnvironmentTypeSelect = {},
|
||||
onContinueClick = {},
|
||||
onTermsClick = {},
|
||||
onPrivacyPolicyClick = {},
|
||||
onReceiveMarketingEmailsToggle = {},
|
||||
onUnsubscribeMarketingEmailsClick = {},
|
||||
onServerGeologyHelpClick = {},
|
||||
onBackClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun StartRegistrationContentPreview_empty() {
|
||||
BitwardenTheme {
|
||||
StartRegistrationContent(
|
||||
emailInput = "",
|
||||
selectedEnvironmentType = Environment.Type.US,
|
||||
nameInput = "",
|
||||
isReceiveMarketingEmailsToggled = false,
|
||||
isContinueButtonEnabled = false,
|
||||
handler = StartRegistrationHandler(
|
||||
onEmailInputChange = {},
|
||||
onNameInputChange = {},
|
||||
onEnvironmentTypeSelect = {},
|
||||
onContinueClick = {},
|
||||
onTermsClick = {},
|
||||
onPrivacyPolicyClick = {},
|
||||
onReceiveMarketingEmailsToggle = {},
|
||||
onUnsubscribeMarketingEmailsClick = {},
|
||||
onServerGeologyHelpClick = {},
|
||||
onBackClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResul
|
|||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment.Type
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.CloseClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.BackClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.ContinueClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.EmailInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.EnvironmentTypeSelect
|
||||
|
@ -20,6 +20,7 @@ import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAc
|
|||
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.ReceiveMarketingEmailsToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.ServerGeologyHelpClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.TermsClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.UnsubscribeMarketingEmailsClick
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
|
@ -79,7 +80,7 @@ class StartRegistrationViewModel @Inject constructor(
|
|||
is ContinueClick -> handleContinueClick()
|
||||
is EmailInputChange -> handleEmailInputChanged(action)
|
||||
is NameInputChange -> handleNameInputChanged(action)
|
||||
is CloseClick -> handleCloseClick()
|
||||
is BackClick -> handleBackClick()
|
||||
is ErrorDialogDismiss -> handleDialogDismiss()
|
||||
is ReceiveMarketingEmailsToggle -> handleReceiveMarketingEmailsToggle(
|
||||
action,
|
||||
|
@ -96,9 +97,15 @@ class StartRegistrationViewModel @Inject constructor(
|
|||
is UpdatedEnvironmentReceive -> {
|
||||
handleUpdatedEnvironmentReceive(action)
|
||||
}
|
||||
|
||||
ServerGeologyHelpClick -> handleServerGeologyHelpClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleServerGeologyHelpClick() {
|
||||
sendEvent(StartRegistrationEvent.NavigateToServerSelectionInfo)
|
||||
}
|
||||
|
||||
private fun handleEnvironmentTypeSelect(action: EnvironmentTypeSelect) {
|
||||
val environment = when (action.environmentType) {
|
||||
Type.US -> Environment.Us
|
||||
|
@ -145,7 +152,7 @@ class StartRegistrationViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleCloseClick() {
|
||||
private fun handleBackClick() {
|
||||
sendEvent(StartRegistrationEvent.NavigateBack)
|
||||
}
|
||||
|
||||
|
@ -330,6 +337,11 @@ sealed class StartRegistrationEvent {
|
|||
* Navigates to the self-hosted/custom environment screen.
|
||||
*/
|
||||
data object NavigateToEnvironment : StartRegistrationEvent()
|
||||
|
||||
/**
|
||||
* Navigates to the server selection info.
|
||||
*/
|
||||
data object NavigateToServerSelectionInfo : StartRegistrationEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -342,9 +354,9 @@ sealed class StartRegistrationAction {
|
|||
data object ContinueClick : StartRegistrationAction()
|
||||
|
||||
/**
|
||||
* User clicked close.
|
||||
* User clicked back.
|
||||
*/
|
||||
data object CloseClick : StartRegistrationAction()
|
||||
data object BackClick : StartRegistrationAction()
|
||||
|
||||
/**
|
||||
* Email input changed.
|
||||
|
@ -388,6 +400,11 @@ sealed class StartRegistrationAction {
|
|||
*/
|
||||
data object UnsubscribeMarketingEmailsClick : StartRegistrationAction()
|
||||
|
||||
/**
|
||||
* User has tapped the tooltip for the server environment
|
||||
*/
|
||||
data object ServerGeologyHelpClick : StartRegistrationAction()
|
||||
|
||||
/**
|
||||
* Models actions that the [StartRegistrationViewModel] itself might send.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.startregistration.handlers
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationViewModel
|
||||
|
||||
/**
|
||||
* A collection of handler functions for managing actions within the context of the
|
||||
* [StartRegistrationScreen].
|
||||
*/
|
||||
data class StartRegistrationHandler(
|
||||
val onEmailInputChange: (String) -> Unit,
|
||||
val onNameInputChange: (String) -> Unit,
|
||||
val onEnvironmentTypeSelect: (Environment.Type) -> Unit,
|
||||
val onContinueClick: () -> Unit,
|
||||
val onTermsClick: () -> Unit,
|
||||
val onPrivacyPolicyClick: () -> Unit,
|
||||
val onReceiveMarketingEmailsToggle: (Boolean) -> Unit,
|
||||
val onUnsubscribeMarketingEmailsClick: () -> Unit,
|
||||
val onServerGeologyHelpClick: () -> Unit,
|
||||
val onBackClick: () -> Unit,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Creates an instance of [StartRegistrationHandler] by binding actions to the provided
|
||||
* [StartRegistrationViewModel].
|
||||
*/
|
||||
fun create(viewModel: StartRegistrationViewModel): StartRegistrationHandler {
|
||||
return StartRegistrationHandler(
|
||||
onEmailInputChange = {
|
||||
viewModel.trySendAction(
|
||||
StartRegistrationAction.EmailInputChange(
|
||||
it,
|
||||
),
|
||||
)
|
||||
},
|
||||
onNameInputChange = {
|
||||
viewModel.trySendAction(
|
||||
StartRegistrationAction.NameInputChange(
|
||||
it,
|
||||
),
|
||||
)
|
||||
},
|
||||
onEnvironmentTypeSelect = {
|
||||
viewModel.trySendAction(
|
||||
StartRegistrationAction.EnvironmentTypeSelect(
|
||||
it,
|
||||
),
|
||||
)
|
||||
},
|
||||
onContinueClick = {
|
||||
viewModel.trySendAction(StartRegistrationAction.ContinueClick)
|
||||
},
|
||||
onTermsClick = { viewModel.trySendAction(StartRegistrationAction.TermsClick) },
|
||||
onPrivacyPolicyClick = {
|
||||
viewModel.trySendAction(StartRegistrationAction.PrivacyPolicyClick)
|
||||
},
|
||||
onReceiveMarketingEmailsToggle = {
|
||||
viewModel.trySendAction(
|
||||
StartRegistrationAction.ReceiveMarketingEmailsToggle(it),
|
||||
)
|
||||
},
|
||||
onUnsubscribeMarketingEmailsClick = {
|
||||
viewModel.trySendAction(
|
||||
StartRegistrationAction.UnsubscribeMarketingEmailsClick,
|
||||
)
|
||||
},
|
||||
onServerGeologyHelpClick = {
|
||||
viewModel.trySendAction(StartRegistrationAction.ServerGeologyHelpClick)
|
||||
},
|
||||
onBackClick = { viewModel.trySendAction(StartRegistrationAction.BackClick) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function for creating a [StartRegistrationHandler] instance in a [Composable]
|
||||
* context.
|
||||
*/
|
||||
@Composable
|
||||
fun rememberStartRegistrationHandler(viewModel: StartRegistrationViewModel) = remember(viewModel) {
|
||||
StartRegistrationHandler.create(viewModel)
|
||||
}
|
74
app/src/main/res/drawable-night/vault.xml
Normal file
74
app/src/main/res/drawable-night/vault.xml
Normal file
|
@ -0,0 +1,74 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:viewportWidth="200"
|
||||
android:viewportHeight="200">
|
||||
<path
|
||||
android:pathData="M9.61,36C9.61,26.06 17.66,18 27.61,18H172.39C182.33,18 190.39,26.06 190.39,36V148.89C190.39,158.83 182.33,166.89 172.39,166.89H27.61C17.66,166.89 9.61,158.83 9.61,148.89V36ZM27.61,22C19.87,22 13.61,28.27 13.61,36V148.89C13.61,156.62 19.87,162.89 27.61,162.89H172.39C180.12,162.89 186.39,156.62 186.39,148.89V36C186.39,28.27 180.12,22 172.39,22H27.61Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M83.07,115.51C84.18,115.51 85.07,116.4 85.07,117.51L85.07,132.09C85.07,133.19 84.18,134.09 83.07,134.09C81.97,134.09 81.07,133.19 81.07,132.09L81.07,117.51C81.07,116.4 81.97,115.51 83.07,115.51Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M83.07,50.81C84.18,50.81 85.07,51.7 85.07,52.81L85.07,67.39C85.07,68.49 84.18,69.39 83.07,69.39C81.97,69.39 81.07,68.49 81.07,67.39L81.07,52.81C81.07,51.7 81.97,50.81 83.07,50.81Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M66.77,108.75C67.55,109.53 67.55,110.8 66.77,111.58L56.46,121.89C55.68,122.67 54.41,122.67 53.63,121.89C52.85,121.11 52.85,119.84 53.63,119.06L63.94,108.75C64.72,107.97 65.99,107.97 66.77,108.75Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M112.52,63C113.3,63.78 113.3,65.05 112.52,65.83L102.21,76.14C101.43,76.92 100.16,76.92 99.38,76.14C98.6,75.36 98.6,74.09 99.38,73.31L109.69,63C110.47,62.22 111.74,62.22 112.52,63Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M66.77,76.14C65.99,76.92 64.72,76.92 63.94,76.14L53.63,65.83C52.85,65.05 52.85,63.78 53.63,63C54.41,62.22 55.68,62.22 56.46,63L66.77,73.31C67.55,74.09 67.55,75.36 66.77,76.14Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M112.52,121.89C111.74,122.67 110.47,122.67 109.69,121.89L99.38,111.58C98.6,110.8 98.6,109.53 99.38,108.75C100.16,107.97 101.43,107.97 102.21,108.75L112.52,119.06C113.3,119.84 113.3,121.11 112.52,121.89Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M125.04,92.41C125.04,93.51 124.15,94.41 123.04,94.41L108.46,94.41C107.36,94.41 106.46,93.51 106.46,92.41C106.46,91.3 107.36,90.41 108.46,90.41L123.04,90.41C124.15,90.41 125.04,91.3 125.04,92.41Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M59.22,92.41C59.22,93.51 58.32,94.41 57.22,94.41L42.64,94.41C41.53,94.41 40.64,93.51 40.64,92.41C40.64,91.3 41.53,90.41 42.64,90.41L57.22,90.41C58.32,90.41 59.22,91.3 59.22,92.41Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M83.07,69.96C70.67,69.96 60.62,80.01 60.62,92.41C60.62,104.81 70.67,114.86 83.07,114.86C95.47,114.86 105.52,104.81 105.52,92.41C105.52,80.01 95.47,69.96 83.07,69.96ZM56.62,92.41C56.62,77.8 68.47,65.96 83.07,65.96C97.68,65.96 109.52,77.8 109.52,92.41C109.52,107.01 97.68,118.86 83.07,118.86C68.47,118.86 56.62,107.01 56.62,92.41Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M83.07,76.48C74.28,76.48 67.15,83.61 67.15,92.41C67.15,101.2 74.28,108.33 83.07,108.33C91.87,108.33 99,101.2 99,92.41C99,83.61 91.87,76.48 83.07,76.48ZM65.15,92.41C65.15,82.51 73.17,74.48 83.07,74.48C92.97,74.48 101,82.51 101,92.41C101,102.31 92.97,110.33 83.07,110.33C73.17,110.33 65.15,102.31 65.15,92.41Z"
|
||||
android:fillColor="#6FD9E2"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M27.27,164.89V171.65C27.27,173.86 29.07,175.65 31.27,175.65H47.52C49.73,175.65 51.52,173.86 51.52,171.65V164.89H55.52V171.65C55.52,176.07 51.94,179.65 47.52,179.65H31.27C26.86,179.65 23.27,176.07 23.27,171.65V164.89H27.27Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M148.47,164.89V171.65C148.47,173.86 150.26,175.65 152.47,175.65H168.72C170.93,175.65 172.72,173.86 172.72,171.65V164.89H176.72V171.65C176.72,176.07 173.14,179.65 168.72,179.65H152.47C148.05,179.65 144.47,176.07 144.47,171.65V164.89H148.47Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M174.72,69.12L177.1,69.12C178.76,69.12 180.1,67.78 180.1,66.12L180.1,45.87C180.1,44.21 178.76,42.87 177.1,42.87L174.72,42.87L174.72,40.87L177.1,40.87C179.86,40.87 182.1,43.11 182.1,45.87L182.1,66.12C182.1,68.88 179.86,71.12 177.1,71.12L174.72,71.12L174.72,69.12Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M174.72,142.02L177.1,142.02C178.76,142.02 180.1,140.68 180.1,139.02L180.1,118.77C180.1,117.11 178.76,115.77 177.1,115.77L174.72,115.77L174.72,113.77L177.1,113.77C179.86,113.77 182.1,116.01 182.1,118.77L182.1,139.02C182.1,141.78 179.86,144.02 177.1,144.02L174.72,144.02L174.72,142.02Z"
|
||||
android:fillColor="#E2E3E4"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M24.27,41.67C24.27,36.7 28.3,32.67 33.27,32.67H166.72C171.69,32.67 175.72,36.7 175.72,41.67V144.13C175.72,149.1 171.69,153.13 166.72,153.13H33.27C28.3,153.13 24.27,149.1 24.27,144.13V41.67ZM33.27,34.67C29.41,34.67 26.27,37.8 26.27,41.67V144.13C26.27,148 29.41,151.13 33.27,151.13H166.72C170.59,151.13 173.72,148 173.72,144.13V41.67C173.72,37.8 170.59,34.67 166.72,34.67H33.27Z"
|
||||
android:fillColor="#6FD9E2"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M38.17,42.63C36.52,42.63 35.17,43.97 35.17,45.63V64.36C35.17,64.92 34.73,65.36 34.17,65.36C33.62,65.36 33.17,64.92 33.17,64.36V45.63C33.17,42.87 35.41,40.63 38.17,40.63H123.65C124.2,40.63 124.65,41.08 124.65,41.63C124.65,42.18 124.2,42.63 123.65,42.63H38.17ZM166.76,117.02C167.32,117.02 167.76,117.46 167.76,118.02V140.13C167.76,142.89 165.52,145.13 162.76,145.13H77.05C76.5,145.13 76.05,144.68 76.05,144.13C76.05,143.57 76.5,143.13 77.05,143.13H162.76C164.42,143.13 165.76,141.78 165.76,140.13V118.02C165.76,117.46 166.21,117.02 166.76,117.02Z"
|
||||
android:fillColor="#6FD9E2"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
74
app/src/main/res/drawable/vault.xml
Normal file
74
app/src/main/res/drawable/vault.xml
Normal file
|
@ -0,0 +1,74 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="200dp"
|
||||
android:height="200dp"
|
||||
android:viewportWidth="200"
|
||||
android:viewportHeight="200">
|
||||
<path
|
||||
android:pathData="M9.61,36C9.61,26.06 17.66,18 27.61,18H172.39C182.33,18 190.39,26.06 190.39,36V148.89C190.39,158.83 182.33,166.89 172.39,166.89H27.61C17.66,166.89 9.61,158.83 9.61,148.89V36ZM27.61,22C19.87,22 13.61,28.27 13.61,36V148.89C13.61,156.62 19.87,162.89 27.61,162.89H172.39C180.12,162.89 186.39,156.62 186.39,148.89V36C186.39,28.27 180.12,22 172.39,22H27.61Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M83.07,115.51C84.18,115.51 85.07,116.4 85.07,117.51L85.07,132.09C85.07,133.19 84.18,134.09 83.07,134.09C81.97,134.09 81.07,133.19 81.07,132.09L81.07,117.51C81.07,116.4 81.97,115.51 83.07,115.51Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M83.07,50.81C84.18,50.81 85.07,51.7 85.07,52.81L85.07,67.39C85.07,68.49 84.18,69.39 83.07,69.39C81.97,69.39 81.07,68.49 81.07,67.39L81.07,52.81C81.07,51.7 81.97,50.81 83.07,50.81Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M66.77,108.75C67.55,109.53 67.55,110.8 66.77,111.58L56.46,121.89C55.68,122.67 54.41,122.67 53.63,121.89C52.85,121.11 52.85,119.84 53.63,119.06L63.94,108.75C64.72,107.97 65.99,107.97 66.77,108.75Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M112.52,63C113.3,63.78 113.3,65.05 112.52,65.83L102.21,76.14C101.43,76.92 100.16,76.92 99.38,76.14C98.6,75.36 98.6,74.09 99.38,73.31L109.69,63C110.47,62.22 111.74,62.22 112.52,63Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M66.77,76.14C65.99,76.92 64.72,76.92 63.94,76.14L53.63,65.83C52.85,65.05 52.85,63.78 53.63,63C54.41,62.22 55.68,62.22 56.46,63L66.77,73.31C67.55,74.09 67.55,75.36 66.77,76.14Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M112.52,121.89C111.74,122.67 110.47,122.67 109.69,121.89L99.38,111.58C98.6,110.8 98.6,109.53 99.38,108.75C100.16,107.97 101.43,107.97 102.21,108.75L112.52,119.06C113.3,119.84 113.3,121.11 112.52,121.89Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M125.04,92.41C125.04,93.51 124.15,94.41 123.04,94.41L108.46,94.41C107.36,94.41 106.46,93.51 106.46,92.41C106.46,91.3 107.36,90.41 108.46,90.41L123.04,90.41C124.15,90.41 125.04,91.3 125.04,92.41Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M59.22,92.41C59.22,93.51 58.32,94.41 57.22,94.41L42.64,94.41C41.53,94.41 40.64,93.51 40.64,92.41C40.64,91.3 41.53,90.41 42.64,90.41L57.22,90.41C58.32,90.41 59.22,91.3 59.22,92.41Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M83.07,69.96C70.67,69.96 60.62,80.01 60.62,92.41C60.62,104.81 70.67,114.86 83.07,114.86C95.47,114.86 105.52,104.81 105.52,92.41C105.52,80.01 95.47,69.96 83.07,69.96ZM56.62,92.41C56.62,77.8 68.47,65.96 83.07,65.96C97.68,65.96 109.52,77.8 109.52,92.41C109.52,107.01 97.68,118.86 83.07,118.86C68.47,118.86 56.62,107.01 56.62,92.41Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M83.07,76.48C74.28,76.48 67.15,83.61 67.15,92.41C67.15,101.2 74.28,108.33 83.07,108.33C91.87,108.33 99,101.2 99,92.41C99,83.61 91.87,76.48 83.07,76.48ZM65.15,92.41C65.15,82.51 73.17,74.48 83.07,74.48C92.97,74.48 101,82.51 101,92.41C101,102.31 92.97,110.33 83.07,110.33C73.17,110.33 65.15,102.31 65.15,92.41Z"
|
||||
android:fillColor="#10949D"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M27.27,164.89V171.65C27.27,173.86 29.07,175.65 31.27,175.65H47.52C49.73,175.65 51.52,173.86 51.52,171.65V164.89H55.52V171.65C55.52,176.07 51.94,179.65 47.52,179.65H31.27C26.86,179.65 23.27,176.07 23.27,171.65V164.89H27.27Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M148.47,164.89V171.65C148.47,173.86 150.26,175.65 152.47,175.65H168.72C170.93,175.65 172.72,173.86 172.72,171.65V164.89H176.72V171.65C176.72,176.07 173.14,179.65 168.72,179.65H152.47C148.05,179.65 144.47,176.07 144.47,171.65V164.89H148.47Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M174.72,69.12L177.1,69.12C178.76,69.12 180.1,67.78 180.1,66.12L180.1,45.87C180.1,44.21 178.76,42.87 177.1,42.87L174.72,42.87L174.72,40.87L177.1,40.87C179.86,40.87 182.1,43.11 182.1,45.87L182.1,66.12C182.1,68.88 179.86,71.12 177.1,71.12L174.72,71.12L174.72,69.12Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M174.72,142.02L177.1,142.02C178.76,142.02 180.1,140.68 180.1,139.02L180.1,118.77C180.1,117.11 178.76,115.77 177.1,115.77L174.72,115.77L174.72,113.77L177.1,113.77C179.86,113.77 182.1,116.01 182.1,118.77L182.1,139.02C182.1,141.78 179.86,144.02 177.1,144.02L174.72,144.02L174.72,142.02Z"
|
||||
android:fillColor="#020F66"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M24.27,41.67C24.27,36.7 28.3,32.67 33.27,32.67H166.72C171.69,32.67 175.72,36.7 175.72,41.67V144.13C175.72,149.1 171.69,153.13 166.72,153.13H33.27C28.3,153.13 24.27,149.1 24.27,144.13V41.67ZM33.27,34.67C29.41,34.67 26.27,37.8 26.27,41.67V144.13C26.27,148 29.41,151.13 33.27,151.13H166.72C170.59,151.13 173.72,148 173.72,144.13V41.67C173.72,37.8 170.59,34.67 166.72,34.67H33.27Z"
|
||||
android:fillColor="#10949D"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M38.17,42.63C36.52,42.63 35.17,43.97 35.17,45.63V64.36C35.17,64.92 34.73,65.36 34.17,65.36C33.62,65.36 33.17,64.92 33.17,64.36V45.63C33.17,42.87 35.41,40.63 38.17,40.63H123.65C124.2,40.63 124.65,41.08 124.65,41.63C124.65,42.18 124.2,42.63 123.65,42.63H38.17ZM166.76,117.02C167.32,117.02 167.76,117.46 167.76,118.02V140.13C167.76,142.89 165.52,145.13 162.76,145.13H77.05C76.5,145.13 76.05,144.68 76.05,144.13C76.05,143.57 76.5,143.13 77.05,143.13H162.76C164.42,143.13 165.76,141.78 165.76,140.13V118.02C165.76,117.46 166.21,117.02 166.76,117.02Z"
|
||||
android:fillColor="#10949D"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -975,4 +975,6 @@ Do you want to switch to this account?</string>
|
|||
<string name="generate_button_label">Generate</string>
|
||||
<string name="write_this_password_down_and_keep_it_somewhere_safe">Write this password down and keep it somewhere safe.</string>
|
||||
<string name="learn_about_other_ways_to_prevent_account_lockout">Learn about other ways to prevent account lockout</string>
|
||||
<string name="help_with_server_geolocations">Help with server geolocations.</string>
|
||||
<string name="email_address_required">Email address (required)</string>
|
||||
</resources>
|
||||
|
|
|
@ -6,6 +6,8 @@ import com.x8bit.bitwarden.R
|
|||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.VaultUnlockType
|
||||
import com.x8bit.bitwarden.data.platform.manager.FeatureFlagManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.model.FlagKey
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
|
||||
|
@ -35,6 +37,10 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
private val fakeEnvironmentRepository = FakeEnvironmentRepository()
|
||||
|
||||
private val featureFlagManager: FeatureFlagManager = mockk(relaxed = true) {
|
||||
every { getFeatureFlag(FlagKey.EmailVerification) } returns false
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should be correct when there is no remembered email`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
|
@ -372,6 +378,20 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `When feature is enabled CreateAccountClick should emit NavigateToStartRegistration`() =
|
||||
runTest {
|
||||
every { featureFlagManager.getFeatureFlag(FlagKey.EmailVerification) } returns true
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(LandingAction.CreateAccountClick)
|
||||
assertEquals(
|
||||
LandingEvent.NavigateToStartRegistration,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `DialogDismiss should clear the active dialog`() {
|
||||
val initialState = DEFAULT_STATE.copy(
|
||||
|
@ -549,6 +569,7 @@ class LandingViewModelTest : BaseViewModelTest() {
|
|||
},
|
||||
vaultRepository = vaultRepository,
|
||||
environmentRepository = fakeEnvironmentRepository,
|
||||
featureFlagManager = featureFlagManager,
|
||||
savedStateHandle = savedStateHandle,
|
||||
)
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.startregistration
|
||||
|
||||
import androidx.compose.ui.test.assert
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
|
@ -8,16 +9,18 @@ import androidx.compose.ui.test.onAllNodesWithText
|
|||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
import androidx.compose.ui.test.performTextInput
|
||||
import androidx.core.net.toUri
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.CloseClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.BackClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.EmailInputChange
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.dialog.BasicDialogState
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import com.x8bit.bitwarden.ui.util.performCustomAccessibilityAction
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
|
@ -39,6 +42,7 @@ class StartRegistrationScreenTest : BaseComposeTest() {
|
|||
private val intentManager = mockk<IntentManager>(relaxed = true) {
|
||||
every { startCustomTabsActivity(any()) } just runs
|
||||
every { startActivity(any()) } just runs
|
||||
every { launchUri(any()) } just runs
|
||||
}
|
||||
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
|
@ -66,9 +70,9 @@ class StartRegistrationScreenTest : BaseComposeTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `close click should send CloseClick action`() {
|
||||
composeTestRule.onNodeWithContentDescription("Close").performClick()
|
||||
verify { viewModel.trySendAction(CloseClick) }
|
||||
fun `close click should send BackClick action`() {
|
||||
composeTestRule.onNodeWithContentDescription("Back").performClick()
|
||||
verify { viewModel.trySendAction(BackClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -130,7 +134,7 @@ class StartRegistrationScreenTest : BaseComposeTest() {
|
|||
|
||||
@Test
|
||||
fun `email input change should send EmailInputChange action`() {
|
||||
composeTestRule.onNodeWithText("Email address").performTextInput(TEST_INPUT)
|
||||
composeTestRule.onNodeWithText("Email address (required)").performTextInput(TEST_INPUT)
|
||||
verify { viewModel.trySendAction(EmailInputChange(TEST_INPUT)) }
|
||||
}
|
||||
|
||||
|
@ -174,6 +178,98 @@ class StartRegistrationScreenTest : BaseComposeTest() {
|
|||
composeTestRule.onNode(isDialog()).assertIsDisplayed()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clicking the server tool tip should send ServerGeologyHelpClickAction`() {
|
||||
composeTestRule
|
||||
.onNodeWithContentDescription("Help with server geolocations.")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify { viewModel.trySendAction(StartRegistrationAction.ServerGeologyHelpClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when NavigateToServerSelectionInfo is observed event should invoke intent manager`() {
|
||||
mutableEventFlow.tryEmit(StartRegistrationEvent.NavigateToServerSelectionInfo)
|
||||
|
||||
verify {
|
||||
intentManager.launchUri(
|
||||
uri = "https://bitwarden.com/help/server-geographies/".toUri(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when environment selected in dialog should send EnvironmentTypeSelect action`() {
|
||||
val selectedEnvironment = Environment.Eu
|
||||
|
||||
// Clicking to open dialog
|
||||
composeTestRule
|
||||
.onNodeWithText(Environment.Us.label)
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
// Clicking item on dialog
|
||||
composeTestRule
|
||||
.onNodeWithText(selectedEnvironment.label)
|
||||
.assert(hasAnyAncestor(isDialog()))
|
||||
.performClick()
|
||||
|
||||
verify {
|
||||
viewModel.trySendAction(
|
||||
StartRegistrationAction.EnvironmentTypeSelect(selectedEnvironment.type),
|
||||
)
|
||||
}
|
||||
|
||||
// Make sure dialog is hidden:
|
||||
composeTestRule
|
||||
.onNode(isDialog())
|
||||
.assertDoesNotExist()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when continue button clicked should send ContinueClick action`() {
|
||||
mutableStateFlow.update {
|
||||
it.copy(
|
||||
isContinueButtonEnabled = true,
|
||||
)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Continue")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
|
||||
verify { viewModel.trySendAction(StartRegistrationAction.ContinueClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when unsubscribe custom action invoked should send UnsubscribeMarketingEmailsClick`() {
|
||||
@Suppress("MaxLineLength")
|
||||
composeTestRule
|
||||
.onNodeWithText("Get emails from Bitwarden for announcements, advice, and research opportunities. Unsubscribe at any time.")
|
||||
.performCustomAccessibilityAction("Unsubscribe")
|
||||
|
||||
verify { viewModel.trySendAction(StartRegistrationAction.UnsubscribeMarketingEmailsClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when terms and conditions custom action invoked should send TermsClick`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("By continuing, you agree to the Terms of Service and Privacy Policy")
|
||||
.performCustomAccessibilityAction("Terms of Service")
|
||||
|
||||
verify { viewModel.trySendAction(StartRegistrationAction.TermsClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `when privacy policy custom action invoked should send TermsClick`() {
|
||||
composeTestRule
|
||||
.onNodeWithText("By continuing, you agree to the Terms of Service and Privacy Policy")
|
||||
.performCustomAccessibilityAction("Privacy Policy")
|
||||
|
||||
verify { viewModel.trySendAction(StartRegistrationAction.PrivacyPolicyClick) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TEST_INPUT = "input"
|
||||
private val DEFAULT_STATE = StartRegistrationState(
|
||||
|
|
|
@ -10,7 +10,7 @@ import com.x8bit.bitwarden.data.auth.repository.model.SendVerificationEmailResul
|
|||
import com.x8bit.bitwarden.data.auth.repository.util.generateUriForCaptcha
|
||||
import com.x8bit.bitwarden.data.platform.repository.model.Environment
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.CloseClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.BackClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.ContinueClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.EmailInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.startregistration.StartRegistrationAction.EnvironmentTypeSelect
|
||||
|
@ -293,14 +293,14 @@ class StartRegistrationViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun `CloseClick should emit NavigateBack`() = runTest {
|
||||
fun `BackClick should emit NavigateBack`() = runTest {
|
||||
val viewModel = StartRegistrationViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockAuthRepository,
|
||||
environmentRepository = fakeEnvironmentRepository,
|
||||
)
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(CloseClick)
|
||||
viewModel.trySendAction(BackClick)
|
||||
assertEquals(NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
@ -432,6 +432,20 @@ class StartRegistrationViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ServerGeologyHelpClickAction should emit NavigateToServerSelectionInfo`() = runTest {
|
||||
val viewModel = StartRegistrationViewModel(
|
||||
savedStateHandle = SavedStateHandle(),
|
||||
authRepository = mockAuthRepository,
|
||||
environmentRepository = fakeEnvironmentRepository,
|
||||
)
|
||||
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(StartRegistrationAction.ServerGeologyHelpClick)
|
||||
assertEquals(StartRegistrationEvent.NavigateToServerSelectionInfo, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EMAIL = "test@test.com"
|
||||
private const val NAME = "name"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.x8bit.bitwarden.ui.util
|
||||
|
||||
import androidx.compose.ui.semantics.SemanticsActions
|
||||
import androidx.compose.ui.semantics.SemanticsProperties
|
||||
import androidx.compose.ui.semantics.getOrNull
|
||||
import androidx.compose.ui.test.SemanticsMatcher
|
||||
|
@ -19,6 +20,7 @@ import androidx.compose.ui.test.onFirst
|
|||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performScrollToNode
|
||||
import androidx.compose.ui.test.printToString
|
||||
import org.junit.jupiter.api.assertThrows
|
||||
|
||||
/**
|
||||
|
@ -143,3 +145,31 @@ fun ComposeContentTestRule.onFirstNodeWithTextAfterScroll(
|
|||
text: String,
|
||||
): SemanticsNodeInteraction =
|
||||
onAllNodesWithTextAfterScroll(text).onFirst()
|
||||
|
||||
/**
|
||||
* A helper used to perform a custom accessibility action on a node with a given [label].
|
||||
*
|
||||
* @throws AssertionError if no action with the given [label] is found.
|
||||
*/
|
||||
fun SemanticsNodeInteraction.performCustomAccessibilityAction(label: String) {
|
||||
val tree = printToString()
|
||||
fetchSemanticsNode()
|
||||
.let {
|
||||
val customActions = it.config[SemanticsActions.CustomActions]
|
||||
customActions
|
||||
.find { action ->
|
||||
action.label == label
|
||||
}
|
||||
?.action
|
||||
?.invoke()
|
||||
?: throw AssertionError(
|
||||
"""
|
||||
No action with label $label
|
||||
|
||||
Available actions: $customActions
|
||||
in
|
||||
$tree
|
||||
""".trimMargin(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue