BIT-817: Update storage of Environment on Landing Screen (#203)

This commit is contained in:
Brian Yencho 2023-11-03 11:09:11 -05:00 committed by Álison Fernandes
parent a8de4b10aa
commit 9a4e3af27c
6 changed files with 103 additions and 24 deletions

View file

@ -1,18 +1,15 @@
package com.x8bit.bitwarden.data.platform.repository.model
import android.os.Parcelable
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.model.EnvironmentUrlDataJson
import com.x8bit.bitwarden.ui.platform.base.util.Text
import com.x8bit.bitwarden.ui.platform.base.util.asText
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
/**
* A higher-level wrapper around [EnvironmentUrlDataJson] that provides type-safety, enumerability,
* and human-readable labels.
*/
sealed class Environment : Parcelable {
sealed class Environment {
/**
* The [Type] of the environment.
*/
@ -31,7 +28,6 @@ sealed class Environment : Parcelable {
/**
* The default US environment.
*/
@Parcelize
data object Us : Environment() {
override val type: Type get() = Type.US
override val environmentUrlData: EnvironmentUrlDataJson
@ -41,7 +37,6 @@ sealed class Environment : Parcelable {
/**
* The default EU environment.
*/
@Parcelize
data object Eu : Environment() {
override val type: Type get() = Type.EU
override val environmentUrlData: EnvironmentUrlDataJson
@ -51,9 +46,8 @@ sealed class Environment : Parcelable {
/**
* A custom self-hosted environment with a fully configurable [environmentUrlData].
*/
@Parcelize
data class SelfHosted(
override val environmentUrlData: @RawValue EnvironmentUrlDataJson,
override val environmentUrlData: EnvironmentUrlDataJson,
) : Environment() {
override val type: Type get() = Type.SELF_HOSTED
}

View file

@ -142,7 +142,7 @@ fun LandingScreen(
Spacer(modifier = Modifier.height(10.dp))
EnvironmentSelector(
selectedOption = state.selectedEnvironment.type,
selectedOption = state.selectedEnvironmentType,
onOptionSelected = remember(viewModel) {
{ viewModel.trySendAction(LandingAction.EnvironmentTypeSelect(it)) }
},

View file

@ -34,7 +34,7 @@ class LandingViewModel @Inject constructor(
emailInput = authRepository.rememberedEmailAddress.orEmpty(),
isContinueButtonEnabled = authRepository.rememberedEmailAddress != null,
isRememberMeEnabled = authRepository.rememberedEmailAddress != null,
selectedEnvironment = environmentRepository.environment,
selectedEnvironmentType = environmentRepository.environment.type,
errorDialogState = BasicDialogState.Hidden,
),
) {
@ -42,11 +42,19 @@ class LandingViewModel @Inject constructor(
init {
// As state updates:
// - write to saved state handle
// - updated selected environment
stateFlow
.onEach {
savedStateHandle[KEY_STATE] = it
environmentRepository.environment = it.selectedEnvironment
}
.launchIn(viewModelScope)
// Listen for changes in environment triggered both by this VM and externally.
environmentRepository
.environmentStateFlow
.onEach { environment ->
sendAction(
LandingAction.Internal.UpdatedEnvironmentReceive(environment = environment),
)
}
.launchIn(viewModelScope)
}
@ -59,6 +67,9 @@ class LandingViewModel @Inject constructor(
is LandingAction.RememberMeToggle -> handleRememberMeToggled(action)
is LandingAction.EmailInputChanged -> handleEmailInputUpdated(action)
is LandingAction.EnvironmentTypeSelect -> handleEnvironmentTypeSelect(action)
is LandingAction.Internal.UpdatedEnvironmentReceive -> {
handleUpdatedEnvironmentReceive(action)
}
}
}
@ -119,9 +130,17 @@ class LandingViewModel @Inject constructor(
}
}
// Update the environment in the repo; the VM state will update accordingly because it is
// listening for changes.
environmentRepository.environment = environment
}
private fun handleUpdatedEnvironmentReceive(
action: LandingAction.Internal.UpdatedEnvironmentReceive,
) {
mutableStateFlow.update {
it.copy(
selectedEnvironment = environment,
selectedEnvironmentType = action.environment.type,
)
}
}
@ -135,7 +154,7 @@ data class LandingState(
val emailInput: String,
val isContinueButtonEnabled: Boolean,
val isRememberMeEnabled: Boolean,
val selectedEnvironment: Environment,
val selectedEnvironmentType: Environment.Type,
val errorDialogState: BasicDialogState,
) : Parcelable
@ -200,4 +219,17 @@ sealed class LandingAction {
data class EnvironmentTypeSelect(
val environmentType: Environment.Type,
) : LandingAction()
/**
* Actions for internal use by the ViewModel.
*/
sealed class Internal : LandingAction() {
/**
* Indicates that there has been a change in [environment].
*/
data class UpdatedEnvironmentReceive(
val environment: Environment,
) : Internal()
}
}

View file

@ -0,0 +1,22 @@
package com.x8bit.bitwarden.data.platform.repository.util
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
/**
* A faked implementation of [EnvironmentRepository] based on in-memory caching.
*/
class FakeEnvironmentRepository : EnvironmentRepository {
override var environment: Environment
get() = mutableEnvironmentStateFlow.value
set(value) {
mutableEnvironmentStateFlow.value = value
}
override val environmentStateFlow: StateFlow<Environment>
get() = mutableEnvironmentStateFlow.asStateFlow()
private val mutableEnvironmentStateFlow = MutableStateFlow<Environment>(Environment.Us)
}

View file

@ -366,7 +366,7 @@ class LandingScreenTest : BaseComposeTest() {
emailInput = "",
isContinueButtonEnabled = true,
isRememberMeEnabled = false,
selectedEnvironment = Environment.Us,
selectedEnvironmentType = Environment.Type.US,
errorDialogState = BasicDialogState.Hidden,
)
}

View file

@ -4,6 +4,7 @@ import androidx.lifecycle.SavedStateHandle
import app.cash.turbine.test
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.repository.model.Environment
import com.x8bit.bitwarden.data.platform.repository.util.FakeEnvironmentRepository
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
@ -14,6 +15,9 @@ import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
class LandingViewModelTest : BaseViewModelTest() {
private val fakeEnvironmentRepository = FakeEnvironmentRepository()
@Test
fun `initial state should be correct when there is no remembered email`() = runTest {
val viewModel = createViewModel()
@ -146,14 +150,44 @@ class LandingViewModelTest : BaseViewModelTest() {
}
@Test
fun `EnvironmentTypeSelect should update value of selected region`() = runTest {
val inputEnvironment = Environment.Eu
fun `external environment updates should update the selected environment type`() = runTest {
val viewModel = createViewModel()
viewModel.stateFlow.test {
assertEquals(DEFAULT_STATE, awaitItem())
fakeEnvironmentRepository.environment = Environment.Eu
assertEquals(
DEFAULT_STATE.copy(
selectedEnvironmentType = Environment.Type.EU,
),
awaitItem(),
)
}
}
@Test
fun `EnvironmentTypeSelect should update value of selected region for US or EU`() = runTest {
val inputEnvironmentType = Environment.Type.EU
val viewModel = createViewModel()
viewModel.stateFlow.test {
awaitItem()
viewModel.trySendAction(LandingAction.EnvironmentTypeSelect(inputEnvironment.type))
viewModel.trySendAction(LandingAction.EnvironmentTypeSelect(inputEnvironmentType))
assertEquals(
DEFAULT_STATE.copy(selectedEnvironment = Environment.Eu),
DEFAULT_STATE.copy(selectedEnvironmentType = Environment.Type.EU),
awaitItem(),
)
}
}
@Test
fun `EnvironmentTypeSelect should emit NavigateToEnvironment for self-hosted`() = runTest {
val inputEnvironmentType = Environment.Type.SELF_HOSTED
val viewModel = createViewModel()
viewModel.eventFlow.test {
viewModel.trySendAction(LandingAction.EnvironmentTypeSelect(inputEnvironmentType))
assertEquals(
LandingEvent.NavigateToEnvironment,
awaitItem(),
)
}
@ -163,15 +197,12 @@ class LandingViewModelTest : BaseViewModelTest() {
private fun createViewModel(
rememberedEmail: String? = null,
environment: Environment = Environment.Us,
savedStateHandle: SavedStateHandle = SavedStateHandle(),
): LandingViewModel = LandingViewModel(
authRepository = mockk(relaxed = true) {
every { rememberedEmailAddress } returns rememberedEmail
},
environmentRepository = mockk(relaxed = true) {
every { this@mockk.environment } returns environment
},
environmentRepository = fakeEnvironmentRepository,
savedStateHandle = savedStateHandle,
)
@ -182,7 +213,7 @@ class LandingViewModelTest : BaseViewModelTest() {
emailInput = "",
isContinueButtonEnabled = false,
isRememberMeEnabled = false,
selectedEnvironment = Environment.Us,
selectedEnvironmentType = Environment.Type.US,
errorDialogState = BasicDialogState.Hidden,
)
}