mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 19:28:44 +03:00
BIT-188 Add switches and links for check password and terms and privacy (#106)
This commit is contained in:
parent
bb9c260160
commit
2cda9db9a2
8 changed files with 435 additions and 47 deletions
|
@ -2,33 +2,63 @@ package com.x8bit.bitwarden.ui.auth.feature.createaccount
|
|||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement.spacedBy
|
||||
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.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.padding
|
||||
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.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
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.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ErrorDialogDismiss
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PrivacyPolicyClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.TermsClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToPrivacyPolicy
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.NavigateToTerms
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenBasicDialog
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenPasswordField
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenSwitch
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextButtonTopAppBar
|
||||
import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
||||
import com.x8bit.bitwarden.ui.platform.theme.clickableSpanStyle
|
||||
|
||||
/**
|
||||
* Top level composable for the create account screen.
|
||||
|
@ -37,12 +67,21 @@ import com.x8bit.bitwarden.ui.platform.components.BitwardenTextField
|
|||
@Composable
|
||||
fun CreateAccountScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
intentHandler: IntentHandler = IntentHandler(context = LocalContext.current),
|
||||
viewModel: CreateAccountViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsState()
|
||||
val context = LocalContext.current
|
||||
EventsEffect(viewModel) { event ->
|
||||
when (event) {
|
||||
is NavigateToPrivacyPolicy -> {
|
||||
intentHandler.launchUri("https://bitwarden.com/privacy/".toUri())
|
||||
}
|
||||
|
||||
is NavigateToTerms -> {
|
||||
intentHandler.launchUri("https://bitwarden.com/terms/".toUri())
|
||||
}
|
||||
|
||||
is CreateAccountEvent.NavigateBack -> onNavigateBack.invoke()
|
||||
is CreateAccountEvent.ShowToast -> {
|
||||
Toast.makeText(context, event.text, Toast.LENGTH_SHORT).show()
|
||||
|
@ -57,54 +96,183 @@ fun CreateAccountScreen(
|
|||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.surface),
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
BitwardenTextButtonTopAppBar(
|
||||
title = stringResource(id = R.string.create_account),
|
||||
navigationIcon = painterResource(id = R.drawable.ic_close),
|
||||
navigationIconContentDescription = stringResource(id = R.string.close),
|
||||
onNavigationIconClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(CreateAccountAction.CloseClick) }
|
||||
{ viewModel.trySendAction(CloseClick) }
|
||||
},
|
||||
buttonText = stringResource(id = R.string.submit),
|
||||
onButtonClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(CreateAccountAction.SubmitClick) }
|
||||
{ viewModel.trySendAction(SubmitClick) }
|
||||
},
|
||||
isButtonEnabled = true,
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalArrangement = spacedBy(16.dp),
|
||||
) {
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.email_address),
|
||||
value = state.emailInput,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(EmailInputChange(it)) }
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
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
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.master_password),
|
||||
value = state.passwordInput,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(PasswordInputChange(it)) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.retype_master_password),
|
||||
value = state.confirmPasswordInput,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(ConfirmPasswordInputChange(it)) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.master_password_hint),
|
||||
value = state.passwordHintInput,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(PasswordHintChange(it)) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.master_password_hint_description),
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 32.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
BitwardenSwitch(
|
||||
label = stringResource(id = R.string.check_known_data_breaches_for_this_password),
|
||||
isChecked = state.isCheckDataBreachesToggled,
|
||||
onCheckedChange = remember(viewModel) {
|
||||
{ newState ->
|
||||
viewModel.trySendAction(CheckDataBreachesToggle(newState = newState))
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
TermsAndPrivacySwitch(
|
||||
isChecked = state.isAcceptPoliciesToggled,
|
||||
onCheckedChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(AcceptPoliciesToggle(it)) }
|
||||
},
|
||||
onTermsClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(TermsClick) }
|
||||
},
|
||||
onPrivacyPolicyClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(PrivacyPolicyClick) }
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
private fun TermsAndPrivacySwitch(
|
||||
isChecked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
onTermsClick: () -> Unit,
|
||||
onPrivacyPolicyClick: () -> Unit,
|
||||
) {
|
||||
val clickableStyle = clickableSpanStyle()
|
||||
val termsString = stringResource(id = R.string.terms_of_service)
|
||||
val privacyString = stringResource(id = R.string.privacy_policy)
|
||||
val annotatedString = remember {
|
||||
buildAnnotatedString {
|
||||
withStyle(style = clickableStyle) {
|
||||
pushStringAnnotation(tag = termsString, annotation = termsString)
|
||||
append("$termsString,")
|
||||
}
|
||||
append(" ")
|
||||
withStyle(style = clickableStyle) {
|
||||
pushStringAnnotation(tag = privacyString, annotation = privacyString)
|
||||
append(privacyString)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.semantics(mergeDescendants = true) {
|
||||
customActions = listOf(
|
||||
CustomAccessibilityAction(
|
||||
label = termsString,
|
||||
action = {
|
||||
onTermsClick.invoke()
|
||||
true
|
||||
},
|
||||
),
|
||||
CustomAccessibilityAction(
|
||||
label = privacyString,
|
||||
action = {
|
||||
onPrivacyPolicyClick.invoke()
|
||||
true
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
|
||||
onClick = { onCheckedChange.invoke(!isChecked) },
|
||||
)
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.master_password),
|
||||
value = state.passwordInput,
|
||||
onValueChange = remember(viewModel) {
|
||||
{ viewModel.trySendAction(PasswordInputChange(it)) }
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
.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,
|
||||
)
|
||||
BitwardenPasswordField(
|
||||
label = stringResource(id = R.string.retype_master_password),
|
||||
value = state.confirmPasswordInput,
|
||||
onValueChange = remember {
|
||||
{ viewModel.trySendAction(ConfirmPasswordInputChange(it)) }
|
||||
ClickableText(
|
||||
text = annotatedString,
|
||||
onClick = { offset ->
|
||||
annotatedString
|
||||
.getStringAnnotations(offset, offset)
|
||||
.firstOrNull()
|
||||
?.let { span ->
|
||||
when (span.tag) {
|
||||
termsString -> onTermsClick.invoke()
|
||||
privacyString -> onPrivacyPolicyClick.invoke()
|
||||
else -> onCheckedChange.invoke(!isChecked)
|
||||
}
|
||||
} ?: onCheckedChange.invoke(!isChecked)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
BitwardenTextField(
|
||||
label = stringResource(id = R.string.master_password_hint),
|
||||
value = state.passwordHintInput,
|
||||
onValueChange = remember { { viewModel.trySendAction(PasswordHintChange(it)) } },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,11 +4,15 @@ import android.os.Parcelable
|
|||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PrivacyPolicyClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.TermsClick
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
|
@ -25,6 +29,7 @@ private const val MIN_PASSWORD_LENGTH = 12
|
|||
/**
|
||||
* Models logic for the create account screen.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
@HiltViewModel
|
||||
class CreateAccountViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
|
@ -35,6 +40,8 @@ class CreateAccountViewModel @Inject constructor(
|
|||
passwordInput = "",
|
||||
confirmPasswordInput = "",
|
||||
passwordHintInput = "",
|
||||
isAcceptPoliciesToggled = false,
|
||||
isCheckDataBreachesToggled = false,
|
||||
errorDialogState = BasicDialogState.Hidden,
|
||||
),
|
||||
) {
|
||||
|
@ -55,6 +62,26 @@ class CreateAccountViewModel @Inject constructor(
|
|||
is PasswordInputChange -> handlePasswordInputChanged(action)
|
||||
is CreateAccountAction.CloseClick -> handleCloseClick()
|
||||
is CreateAccountAction.ErrorDialogDismiss -> handleDialogDismiss()
|
||||
is AcceptPoliciesToggle -> handleAcceptPoliciesToggle(action)
|
||||
is CheckDataBreachesToggle -> handleCheckDataBreachesToggle(action)
|
||||
is PrivacyPolicyClick -> handlePrivacyPolicyClick()
|
||||
is TermsClick -> handleTermsClick()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePrivacyPolicyClick() = sendEvent(CreateAccountEvent.NavigateToPrivacyPolicy)
|
||||
|
||||
private fun handleTermsClick() = sendEvent(CreateAccountEvent.NavigateToTerms)
|
||||
|
||||
private fun handleAcceptPoliciesToggle(action: AcceptPoliciesToggle) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isAcceptPoliciesToggled = action.newState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCheckDataBreachesToggle(action: CheckDataBreachesToggle) {
|
||||
mutableStateFlow.update {
|
||||
it.copy(isCheckDataBreachesToggled = action.newState)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,6 +135,8 @@ data class CreateAccountState(
|
|||
val passwordInput: String,
|
||||
val confirmPasswordInput: String,
|
||||
val passwordHintInput: String,
|
||||
val isCheckDataBreachesToggled: Boolean,
|
||||
val isAcceptPoliciesToggled: Boolean,
|
||||
val errorDialogState: BasicDialogState,
|
||||
) : Parcelable
|
||||
|
||||
|
@ -125,6 +154,16 @@ sealed class CreateAccountEvent {
|
|||
* Placeholder event for showing a toast. Can be removed once there are real events.
|
||||
*/
|
||||
data class ShowToast(val text: String) : CreateAccountEvent()
|
||||
|
||||
/**
|
||||
* Navigate to terms and conditions.
|
||||
*/
|
||||
data object NavigateToTerms : CreateAccountEvent()
|
||||
|
||||
/**
|
||||
* Navigate to privacy policy.
|
||||
*/
|
||||
data object NavigateToPrivacyPolicy : CreateAccountEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -165,4 +204,24 @@ sealed class CreateAccountAction {
|
|||
* User dismissed the error dialog.
|
||||
*/
|
||||
data object ErrorDialogDismiss : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* User tapped check data breaches toggle.
|
||||
*/
|
||||
data class CheckDataBreachesToggle(val newState: Boolean) : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* User tapped accept policies toggle.
|
||||
*/
|
||||
data class AcceptPoliciesToggle(val newState: Boolean) : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* User tapped privacy policy link.
|
||||
*/
|
||||
data object PrivacyPolicyClick : CreateAccountAction()
|
||||
|
||||
/**
|
||||
* User tapped terms link.
|
||||
*/
|
||||
data object TermsClick : CreateAccountAction()
|
||||
}
|
||||
|
|
|
@ -26,4 +26,11 @@ class IntentHandler(private val context: Context) {
|
|||
.build()
|
||||
.launchUrl(context, uri)
|
||||
}
|
||||
|
||||
/**
|
||||
* Start an activity to view the given [uri] in an external browser.
|
||||
*/
|
||||
fun launchUri(uri: Uri) {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, uri))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,18 @@
|
|||
package com.x8bit.bitwarden.ui.platform.components
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.wrapContentHeight
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
|
@ -34,23 +37,29 @@ fun BitwardenSwitch(
|
|||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(color = MaterialTheme.colorScheme.primary),
|
||||
onClick = { onCheckedChange?.invoke(!isChecked) },
|
||||
)
|
||||
.semantics(mergeDescendants = true) { }
|
||||
.wrapContentHeight(),
|
||||
.then(modifier),
|
||||
) {
|
||||
Switch(
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
.height(32.dp)
|
||||
.width(52.dp),
|
||||
checked = isChecked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
onCheckedChange = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier.padding(start = 16.dp),
|
||||
modifier = Modifier.padding(start = 16.dp, top = 4.dp, bottom = 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package com.x8bit.bitwarden.ui.platform.theme
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
|
||||
/**
|
||||
* Defines a span style for clickable span texts. Useful because spans require a
|
||||
* [SpanStyle] instead of the typical [TextStyle].
|
||||
*/
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun clickableSpanStyle(): SpanStyle = SpanStyle(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||
fontWeight = MaterialTheme.typography.bodyMedium.fontWeight,
|
||||
fontStyle = MaterialTheme.typography.bodyMedium.fontStyle,
|
||||
fontFamily = MaterialTheme.typography.bodyMedium.fontFamily,
|
||||
)
|
|
@ -1,5 +1,7 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.createaccount
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.ui.test.assertIsDisplayed
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasAnyAncestor
|
||||
|
@ -8,7 +10,11 @@ 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.ui.auth.feature.createaccount.CreateAccountAction.AcceptPoliciesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CheckDataBreachesToggle
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.CloseClick
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.ConfirmPasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
|
||||
|
@ -16,6 +22,7 @@ import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Pas
|
|||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.SubmitClick
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.IntentHandler
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
import io.mockk.every
|
||||
|
@ -56,6 +63,40 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
verify { viewModel.trySendAction(CloseClick) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check data breaches click should send CheckDataBreachesToggle action`() {
|
||||
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
|
||||
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(CheckDataBreachesToggle(true)) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Check known data breaches for this password")
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(CheckDataBreachesToggle(true)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `accept policies click should send AcceptPoliciesToggle action`() {
|
||||
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
|
||||
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
|
||||
every { eventFlow } returns emptyFlow()
|
||||
every { trySendAction(AcceptPoliciesToggle(true)) } returns Unit
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(onNavigateBack = {}, viewModel = viewModel)
|
||||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("By activating this switch you agree", substring = true)
|
||||
.performScrollTo()
|
||||
.performClick()
|
||||
verify { viewModel.trySendAction(AcceptPoliciesToggle(true)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateBack event should invoke navigate back lambda`() {
|
||||
var onNavigateBackCalled = false
|
||||
|
@ -70,6 +111,52 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
assert(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToPrivacyPolicy event should invoke intent handler`() {
|
||||
val expectedIntent =
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse("https://bitwarden.com/privacy/"))
|
||||
val intentHandler = mockk<IntentHandler>(relaxed = true) {
|
||||
every { startActivity(expectedIntent) } returns Unit
|
||||
}
|
||||
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
|
||||
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
|
||||
every { eventFlow } returns flowOf(CreateAccountEvent.NavigateToPrivacyPolicy)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
viewModel = viewModel,
|
||||
intentHandler = intentHandler,
|
||||
)
|
||||
}
|
||||
verify {
|
||||
intentHandler.launchUri("https://bitwarden.com/privacy/".toUri())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToTerms event should invoke intent handler`() {
|
||||
val expectedIntent =
|
||||
Intent(Intent.ACTION_VIEW, Uri.parse("https://bitwarden.com/terms/"))
|
||||
val intentHandler = mockk<IntentHandler>(relaxed = true) {
|
||||
every { startActivity(expectedIntent) } returns Unit
|
||||
}
|
||||
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
|
||||
every { stateFlow } returns MutableStateFlow(DEFAULT_STATE)
|
||||
every { eventFlow } returns flowOf(CreateAccountEvent.NavigateToTerms)
|
||||
}
|
||||
composeTestRule.setContent {
|
||||
CreateAccountScreen(
|
||||
onNavigateBack = {},
|
||||
viewModel = viewModel,
|
||||
intentHandler = intentHandler,
|
||||
)
|
||||
}
|
||||
verify {
|
||||
intentHandler.launchUri("https://bitwarden.com/terms/".toUri())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `email input change should send EmailInputChange action`() {
|
||||
val viewModel = mockk<CreateAccountViewModel>(relaxed = true) {
|
||||
|
@ -179,6 +266,8 @@ class CreateAccountScreenTest : BaseComposeTest() {
|
|||
passwordInput = "",
|
||||
confirmPasswordInput = "",
|
||||
passwordHintInput = "",
|
||||
isCheckDataBreachesToggled = false,
|
||||
isAcceptPoliciesToggled = false,
|
||||
errorDialogState = BasicDialogState.Hidden,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.Con
|
|||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.EmailInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordHintChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountAction.PasswordInputChange
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.CreateAccountEvent.ShowToast
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.asText
|
||||
import com.x8bit.bitwarden.ui.platform.components.BasicDialogState
|
||||
|
@ -30,6 +31,8 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
passwordInput = "password",
|
||||
confirmPasswordInput = "confirmPassword",
|
||||
passwordHintInput = "hint",
|
||||
isCheckDataBreachesToggled = false,
|
||||
isAcceptPoliciesToggled = false,
|
||||
errorDialogState = BasicDialogState.Hidden,
|
||||
)
|
||||
val handle = SavedStateHandle(mapOf("state" to savedState))
|
||||
|
@ -61,7 +64,7 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
viewModel.trySendAction(PasswordInputChange("longenoughpassword"))
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(CreateAccountAction.SubmitClick)
|
||||
assert(awaitItem() is CreateAccountEvent.ShowToast)
|
||||
assertEquals(ShowToast("TODO: Handle Submit Click"), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,7 +73,25 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(CloseClick)
|
||||
assert(awaitItem() is CreateAccountEvent.NavigateBack)
|
||||
assertEquals(CreateAccountEvent.NavigateBack, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `PrivacyPolicyClick should emit NavigatePrivacyPolicy`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(CreateAccountAction.PrivacyPolicyClick)
|
||||
assertEquals(CreateAccountEvent.NavigateToPrivacyPolicy, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `TermsClick should emit NavigateToTerms`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.actionChannel.trySend(CreateAccountAction.TermsClick)
|
||||
assertEquals(CreateAccountEvent.NavigateToTerms, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,12 +131,32 @@ class CreateAccountViewModelTest : BaseViewModelTest() {
|
|||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CheckDataBreachesToggle should change isCheckDataBreachesToggled`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
viewModel.trySendAction(CreateAccountAction.CheckDataBreachesToggle(true))
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE.copy(isCheckDataBreachesToggled = true), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `AcceptPoliciesToggle should change isAcceptPoliciesToggled`() = runTest {
|
||||
val viewModel = CreateAccountViewModel(SavedStateHandle())
|
||||
viewModel.trySendAction(CreateAccountAction.AcceptPoliciesToggle(true))
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE.copy(isAcceptPoliciesToggled = true), awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val DEFAULT_STATE = CreateAccountState(
|
||||
passwordInput = "",
|
||||
emailInput = "",
|
||||
confirmPasswordInput = "",
|
||||
passwordHintInput = "",
|
||||
isCheckDataBreachesToggled = false,
|
||||
isAcceptPoliciesToggled = false,
|
||||
errorDialogState = BasicDialogState.Hidden,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,9 +2,6 @@ package com.x8bit.bitwarden.ui.auth.feature.landing
|
|||
|
||||
import androidx.compose.ui.test.assertIsEnabled
|
||||
import androidx.compose.ui.test.assertIsNotEnabled
|
||||
import androidx.compose.ui.test.filterToOne
|
||||
import androidx.compose.ui.test.hasClickAction
|
||||
import androidx.compose.ui.test.onChildren
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import androidx.compose.ui.test.performScrollTo
|
||||
|
@ -69,7 +66,7 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
viewModel = viewModel,
|
||||
)
|
||||
}
|
||||
composeTestRule.onNodeWithText("Continue").performClick()
|
||||
composeTestRule.onNodeWithText("Continue").performScrollTo().performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(LandingAction.ContinueButtonClick)
|
||||
}
|
||||
|
@ -97,8 +94,6 @@ class LandingScreenTest : BaseComposeTest() {
|
|||
}
|
||||
composeTestRule
|
||||
.onNodeWithText("Remember me")
|
||||
.onChildren()
|
||||
.filterToOne(hasClickAction())
|
||||
.performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(LandingAction.RememberMeToggle(true))
|
||||
|
|
Loading…
Add table
Reference in a new issue