mirror of
https://github.com/bitwarden/android.git
synced 2025-03-16 11:18:45 +03:00
[PM-6702] 5# Check your email screen (#3621)
This commit is contained in:
parent
eab94dde79
commit
244d259804
10 changed files with 626 additions and 6 deletions
|
@ -6,6 +6,8 @@ import androidx.navigation.NavHostController
|
|||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.navOptions
|
||||
import androidx.navigation.navigation
|
||||
import com.x8bit.bitwarden.ui.auth.feature.checkemail.checkEmailDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.checkemail.navigateToCheckEmail
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.createAccountDestination
|
||||
import com.x8bit.bitwarden.ui.auth.feature.createaccount.navigateToCreateAccount
|
||||
import com.x8bit.bitwarden.ui.auth.feature.enterprisesignon.enterpriseSignOnDestination
|
||||
|
@ -66,10 +68,15 @@ fun NavGraphBuilder.authGraph(
|
|||
// TODO PR-3622 ADD NAVIGATION TO COMPLETE REGISTRATION
|
||||
},
|
||||
onNavigateToCheckEmail = { emailAddress ->
|
||||
// TODO PR-3621 ADD NAVIGATION TO CHECK EMAIL
|
||||
navController.navigateToCheckEmail(emailAddress)
|
||||
},
|
||||
onNavigateToEnvironment = { navController.navigateToEnvironment() },
|
||||
)
|
||||
checkEmailDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateBackToLanding = {
|
||||
navController.popBackStack(route = LANDING_ROUTE, inclusive = false)
|
||||
},)
|
||||
enterpriseSignOnDestination(
|
||||
onNavigateBack = { navController.popBackStack() },
|
||||
onNavigateToSetPassword = { navController.navigateToSetPassword() },
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.checkemail
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.NavOptions
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.composableWithSlideTransitions
|
||||
|
||||
private const val EMAIL: String = "email"
|
||||
private const val CHECK_EMAIL_ROUTE: String = "check_email/{$EMAIL}"
|
||||
|
||||
/**
|
||||
* Navigate to the check email screen.
|
||||
*/
|
||||
fun NavController.navigateToCheckEmail(emailAddress: String, navOptions: NavOptions? = null) {
|
||||
this.navigate("check_email/$emailAddress", navOptions)
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to retrieve check email arguments from the [SavedStateHandle].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
data class CheckEmailArgs(
|
||||
val emailAddress: String,
|
||||
) {
|
||||
constructor(savedStateHandle: SavedStateHandle) : this(
|
||||
emailAddress = checkNotNull(savedStateHandle.get<String>(EMAIL)),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the check email screen to the nav graph.
|
||||
*/
|
||||
fun NavGraphBuilder.checkEmailDestination(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateBackToLanding: () -> Unit,
|
||||
) {
|
||||
composableWithSlideTransitions(
|
||||
route = CHECK_EMAIL_ROUTE,
|
||||
arguments = listOf(
|
||||
navArgument(EMAIL) { type = NavType.StringType },
|
||||
),
|
||||
) {
|
||||
CheckEmailScreen(
|
||||
onNavigateBack = onNavigateBack,
|
||||
onNavigateBackToLanding = onNavigateBackToLanding,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.checkemail
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.wrapContentHeight
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.ClickableText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.graphics.ColorFilter
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.x8bit.bitwarden.R
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
|
||||
import com.x8bit.bitwarden.ui.platform.base.util.createAnnotatedString
|
||||
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.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
|
||||
|
||||
/**
|
||||
* Constant string to be used in string annotation tag field
|
||||
*/
|
||||
private const val TAG_URL = "URL"
|
||||
|
||||
/**
|
||||
* Top level composable for the check email screen.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun CheckEmailScreen(
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateBackToLanding: () -> Unit,
|
||||
intentManager: IntentManager = LocalIntentManager.current,
|
||||
viewModel: CheckEmailViewModel = hiltViewModel(),
|
||||
) {
|
||||
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
|
||||
EventsEffect(viewModel) { event ->
|
||||
when (event) {
|
||||
is CheckEmailEvent.NavigateBack -> {
|
||||
onNavigateBack.invoke()
|
||||
}
|
||||
|
||||
is CheckEmailEvent.NavigateToEmailApp -> {
|
||||
intentManager.startDefaultEmailApplication()
|
||||
}
|
||||
|
||||
is CheckEmailEvent.NavigateBackToLanding -> {
|
||||
onNavigateBackToLanding.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(CheckEmailAction.CloseClick) }
|
||||
},
|
||||
)
|
||||
},
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(innerPadding)
|
||||
.imePadding()
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Image(
|
||||
painter = rememberVectorPainter(id = R.drawable.email_check),
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillHeight,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.height(112.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Text(
|
||||
text = stringResource(id = R.string.check_your_email),
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 24.dp)
|
||||
.wrapContentHeight()
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
val descriptionAnnotatedString = createAnnotatedString(
|
||||
mainString = stringResource(
|
||||
id = R.string.follow_the_instructions_in_the_email_sent_to_x_to_continue_creating_your_account,
|
||||
state.email,
|
||||
),
|
||||
highlights = listOf(state.email),
|
||||
highlightStyle = SpanStyle(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
tag = "EMAIL",
|
||||
)
|
||||
Text(
|
||||
text = descriptionAnnotatedString,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 24.dp)
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
BitwardenFilledButton(
|
||||
label = stringResource(id = R.string.open_email_app),
|
||||
onClick = remember(viewModel) {
|
||||
{ viewModel.trySendAction(CheckEmailAction.OpenEmailClick) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.testTag("OpenEmailApp")
|
||||
.padding(horizontal = 16.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
val goBackAnnotatedString = createAnnotatedString(
|
||||
mainString = stringResource(
|
||||
id = R.string.no_email_go_back_to_edit_your_email_address,
|
||||
),
|
||||
highlights = listOf(stringResource(id = R.string.go_back)),
|
||||
tag = TAG_URL,
|
||||
)
|
||||
ClickableText(
|
||||
text = goBackAnnotatedString,
|
||||
onClick = {
|
||||
goBackAnnotatedString
|
||||
.getStringAnnotations(TAG_URL, it, it)
|
||||
.firstOrNull()?.let {
|
||||
viewModel.trySendAction(CheckEmailAction.CloseClick)
|
||||
}
|
||||
},
|
||||
)
|
||||
Spacer(modifier = Modifier.height(32.dp))
|
||||
val logInAnnotatedString = createAnnotatedString(
|
||||
mainString = stringResource(
|
||||
id = R.string.or_log_in_you_may_already_have_an_account,
|
||||
),
|
||||
highlights = listOf(stringResource(id = R.string.log_in)),
|
||||
tag = TAG_URL,
|
||||
)
|
||||
ClickableText(
|
||||
text = logInAnnotatedString,
|
||||
onClick = {
|
||||
logInAnnotatedString
|
||||
.getStringAnnotations(TAG_URL, it, it)
|
||||
.firstOrNull()?.let {
|
||||
viewModel.trySendAction(CheckEmailAction.LoginClick)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.navigationBarsPadding())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.checkemail
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import javax.inject.Inject
|
||||
|
||||
private const val KEY_STATE = "state"
|
||||
|
||||
/**
|
||||
* Models logic for the check email screen.
|
||||
*/
|
||||
@HiltViewModel
|
||||
class CheckEmailViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
) : BaseViewModel<CheckEmailState, CheckEmailEvent, CheckEmailAction>(
|
||||
initialState = savedStateHandle[KEY_STATE]
|
||||
?: CheckEmailState(
|
||||
email = CheckEmailArgs(savedStateHandle).emailAddress,
|
||||
),
|
||||
) {
|
||||
init {
|
||||
// As state updates, write to saved state handle:
|
||||
stateFlow
|
||||
.onEach { savedStateHandle[KEY_STATE] = it }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
|
||||
override fun handleAction(action: CheckEmailAction) {
|
||||
when (action) {
|
||||
CheckEmailAction.CloseClick -> sendEvent(CheckEmailEvent.NavigateBack)
|
||||
CheckEmailAction.LoginClick -> sendEvent(CheckEmailEvent.NavigateBackToLanding)
|
||||
CheckEmailAction.OpenEmailClick -> sendEvent(CheckEmailEvent.NavigateToEmailApp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* UI state for the check email screen.
|
||||
*/
|
||||
@Parcelize
|
||||
data class CheckEmailState(
|
||||
val email: String,
|
||||
) : Parcelable
|
||||
|
||||
/**
|
||||
* Models events for the check email screen.
|
||||
*/
|
||||
sealed class CheckEmailEvent {
|
||||
|
||||
/**
|
||||
* Navigate back to previous screen.
|
||||
*/
|
||||
data object NavigateBack : CheckEmailEvent()
|
||||
|
||||
/**
|
||||
* Navigate to email app.
|
||||
*/
|
||||
data object NavigateToEmailApp : CheckEmailEvent()
|
||||
|
||||
/**
|
||||
* Navigate to landing screen.
|
||||
*/
|
||||
data object NavigateBackToLanding : CheckEmailEvent()
|
||||
}
|
||||
|
||||
/**
|
||||
* Models actions for the check email screen.
|
||||
*/
|
||||
sealed class CheckEmailAction {
|
||||
/**
|
||||
* User clicked close.
|
||||
*/
|
||||
data object CloseClick : CheckEmailAction()
|
||||
|
||||
/**
|
||||
* User clicked log in.
|
||||
*/
|
||||
data object LoginClick : CheckEmailAction()
|
||||
|
||||
/**
|
||||
* User clicked open email.
|
||||
*/
|
||||
data object OpenEmailClick : CheckEmailAction()
|
||||
}
|
|
@ -133,6 +133,11 @@ fun @receiver:StringRes Int.asText(vararg args: Any): Text = ResArgsText(this, a
|
|||
fun createAnnotatedString(
|
||||
mainString: String,
|
||||
highlights: List<String>,
|
||||
highlightStyle: SpanStyle = SpanStyle(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
tag: String,
|
||||
): AnnotatedString {
|
||||
return buildAnnotatedString {
|
||||
|
@ -149,11 +154,7 @@ fun createAnnotatedString(
|
|||
val startIndexUnsubscribe = mainString.indexOf(highlightString, ignoreCase = true)
|
||||
val endIndexUnsubscribe = startIndexUnsubscribe + highlightString.length
|
||||
addStyle(
|
||||
style = SpanStyle(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = MaterialTheme.typography.bodyMedium.fontSize,
|
||||
fontWeight = FontWeight.Bold,
|
||||
),
|
||||
style = highlightStyle,
|
||||
start = startIndexUnsubscribe,
|
||||
end = endIndexUnsubscribe,
|
||||
)
|
||||
|
|
|
@ -117,6 +117,11 @@ interface IntentManager {
|
|||
requestCode: Int,
|
||||
): PendingIntent
|
||||
|
||||
/**
|
||||
* Open the default email app on device.
|
||||
*/
|
||||
fun startDefaultEmailApplication()
|
||||
|
||||
/**
|
||||
* Represents file information.
|
||||
*/
|
||||
|
|
|
@ -265,6 +265,13 @@ class IntentManagerImpl(
|
|||
)
|
||||
}
|
||||
|
||||
override fun startDefaultEmailApplication() {
|
||||
val intent = Intent(Intent.ACTION_MAIN)
|
||||
intent.addCategory(Intent.CATEGORY_APP_EMAIL)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
private fun getCameraFileData(): IntentManager.FileData {
|
||||
val tmpDir = File(context.filesDir, TEMP_CAMERA_IMAGE_DIR)
|
||||
val file = File(tmpDir, TEMP_CAMERA_IMAGE_NAME)
|
||||
|
|
84
app/src/main/res/drawable/email_check.xml
Normal file
84
app/src/main/res/drawable/email_check.xml
Normal file
|
@ -0,0 +1,84 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="413dp"
|
||||
android:height="114dp"
|
||||
android:viewportWidth="413"
|
||||
android:viewportHeight="114">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M134.84,0.57h143.82v112.71h-143.82z"/>
|
||||
<path
|
||||
android:pathData="M260.14,44.62V59.04M192.86,14.74L201.04,8.64C204.02,6.42 208.12,6.45 211.07,8.73L212.84,10.09M164.61,35.82L156.15,42.14C154.05,43.7 152.82,46.17 152.82,48.79V100.48C152.82,105.07 156.53,108.78 161.11,108.78H251.85C256.43,108.78 260.14,105.07 260.14,100.48V71.05"
|
||||
android:strokeWidth="2.76577"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#175DDC"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M165.38,54.52V17.83C165.38,16.3 166.61,15.07 168.14,15.07H206.8"
|
||||
android:strokeWidth="2.76577"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#175DDC"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M175.32,27.21H194.11"
|
||||
android:strokeWidth="2.76577"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#175DDC"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M175.32,38.39H193.56"
|
||||
android:strokeWidth="2.76577"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#175DDC"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M175.32,49.57H196.57"
|
||||
android:strokeWidth="2.76577"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#175DDC"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M175.32,60.75H203.13"
|
||||
android:strokeWidth="2.76577"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#175DDC"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M257.86,106.5L223.49,74.77C220.93,72.41 217.59,71.1 214.11,71.1H197.24C193.65,71.1 190.2,72.5 187.62,75L155.1,106.5"
|
||||
android:strokeWidth="2.76577"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#175DDC"/>
|
||||
<path
|
||||
android:pathData="M220.36,71.58L231.04,65.46"
|
||||
android:strokeWidth="2.76577"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#175DDC"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M153.86,48.45L192.59,71.58"
|
||||
android:strokeWidth="2.77"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#175DDC"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M260.35,35.09C260.35,51.78 246.82,65.3 230.13,65.3C213.45,65.3 199.92,51.78 199.92,35.09C199.92,18.4 213.45,4.88 230.13,4.88C246.82,4.88 260.35,18.4 260.35,35.09Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2.76577"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#175DDC"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M256.3,35.09C256.3,49.44 244.53,61.07 230.02,61.07M230.02,9.11C215.5,9.11 203.73,20.74 203.73,35.09"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.38289"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#175DDC"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M254.25,53.55L258.87,58.17L276.21,75.51C277.56,76.86 277.56,79.05 276.21,80.4L275.49,81.12C274.14,82.47 271.95,82.47 270.6,81.12L253.26,63.78L248.64,59.16"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2.76577"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#175DDC"
|
||||
android:strokeLineCap="round"/>
|
||||
</group>
|
||||
</vector>
|
|
@ -0,0 +1,88 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.checkemail
|
||||
|
||||
import androidx.compose.ui.test.onNodeWithContentDescription
|
||||
import androidx.compose.ui.test.onNodeWithText
|
||||
import androidx.compose.ui.test.performClick
|
||||
import com.x8bit.bitwarden.data.platform.repository.util.bufferedMutableSharedFlow
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseComposeTest
|
||||
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.verify
|
||||
import junit.framework.TestCase
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
||||
class CheckEmailScreenTest : BaseComposeTest() {
|
||||
private val intentManager = mockk<IntentManager>(relaxed = true) {
|
||||
every { startDefaultEmailApplication() } just runs
|
||||
}
|
||||
private var onNavigateBackCalled = false
|
||||
private var onNavigateBackToLandingCalled = false
|
||||
private var onNavigateToEmailAppCalled = false
|
||||
|
||||
private val mutableStateFlow = MutableStateFlow(DEFAULT_STATE)
|
||||
private val mutableEventFlow = bufferedMutableSharedFlow<CheckEmailEvent>()
|
||||
private val viewModel = mockk<CheckEmailViewModel>(relaxed = true) {
|
||||
every { stateFlow } returns mutableStateFlow
|
||||
every { eventFlow } returns mutableEventFlow
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
composeTestRule.setContent {
|
||||
CheckEmailScreen(
|
||||
onNavigateBack = { onNavigateBackCalled = true },
|
||||
onNavigateBackToLanding = { onNavigateBackToLandingCalled = true },
|
||||
viewModel = viewModel,
|
||||
intentManager = intentManager,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `close button click should send CloseTap action`() {
|
||||
composeTestRule.onNodeWithContentDescription("Close").performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(CheckEmailAction.CloseClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `open email app button click should send OpenEmailTap action`() {
|
||||
composeTestRule.onNodeWithText("Open email app").performClick()
|
||||
verify {
|
||||
viewModel.trySendAction(CheckEmailAction.OpenEmailClick)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login button click should send LoginTap action`() {
|
||||
mutableEventFlow.tryEmit(CheckEmailEvent.NavigateBackToLanding)
|
||||
TestCase.assertTrue(onNavigateBackToLandingCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateBack should call onNavigateBack`() {
|
||||
mutableEventFlow.tryEmit(CheckEmailEvent.NavigateBack)
|
||||
TestCase.assertTrue(onNavigateBackCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `NavigateToEmailApp should call openEmailApp`() {
|
||||
mutableEventFlow.tryEmit(CheckEmailEvent.NavigateToEmailApp)
|
||||
verify {
|
||||
intentManager.startDefaultEmailApplication()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EMAIL = "test@gmail.com"
|
||||
private val DEFAULT_STATE = CheckEmailState(
|
||||
email = EMAIL,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
package com.x8bit.bitwarden.ui.auth.feature.checkemail
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import app.cash.turbine.test
|
||||
import com.x8bit.bitwarden.ui.platform.base.BaseViewModelTest
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class CheckEmailViewModelTest : BaseViewModelTest() {
|
||||
@Test
|
||||
fun `initial state should be correct`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(DEFAULT_STATE, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `initial state should pull from handle when present`() = runTest {
|
||||
val expectedState = DEFAULT_STATE.copy(
|
||||
email = "another@email.com",
|
||||
)
|
||||
val viewModel = createViewModel(expectedState)
|
||||
viewModel.stateFlow.test {
|
||||
assertEquals(expectedState, awaitItem())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CloseTap should emit NavigateBack`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(CheckEmailAction.CloseClick)
|
||||
assertEquals(
|
||||
CheckEmailEvent.NavigateBack,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `LoginTap should emit NavigateBackToLanding`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(CheckEmailAction.LoginClick)
|
||||
assertEquals(
|
||||
CheckEmailEvent.NavigateBackToLanding,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `OpenEmailTap should emit NavigateToEmailApp`() = runTest {
|
||||
val viewModel = createViewModel()
|
||||
viewModel.eventFlow.test {
|
||||
viewModel.trySendAction(CheckEmailAction.OpenEmailClick)
|
||||
assertEquals(
|
||||
CheckEmailEvent.NavigateToEmailApp,
|
||||
awaitItem(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createViewModel(state: CheckEmailState? = null): CheckEmailViewModel =
|
||||
CheckEmailViewModel(
|
||||
savedStateHandle = SavedStateHandle().also {
|
||||
it["email"] = EMAIL
|
||||
it["state"] = state
|
||||
},
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val EMAIL = "test@gmail.com"
|
||||
private val DEFAULT_STATE = CheckEmailState(
|
||||
email = EMAIL,
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue