Make sure we know the session verification state before showing the option to verify the session. #5521
This commit is contained in:
@@ -15,7 +15,9 @@ import androidx.compose.runtime.remember
|
||||
import dev.zacsweers.metro.Inject
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.core.coroutine.mapState
|
||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||
|
||||
@@ -27,8 +29,33 @@ class ChooseSelfVerificationModePresenter(
|
||||
@Composable
|
||||
override fun present(): ChooseSelfVerificationModeState {
|
||||
val hasDevicesToVerifyAgainst by encryptionService.hasDevicesToVerifyAgainst.collectAsState()
|
||||
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
||||
val canEnterRecoveryKey by remember { derivedStateOf { recoveryState == RecoveryState.INCOMPLETE } }
|
||||
val canEnterRecoveryKey by encryptionService.recoveryStateStateFlow
|
||||
.mapState { recoveryState ->
|
||||
when (recoveryState) {
|
||||
RecoveryState.WAITING_FOR_SYNC,
|
||||
RecoveryState.UNKNOWN -> AsyncData.Loading()
|
||||
RecoveryState.INCOMPLETE -> AsyncData.Success(true)
|
||||
RecoveryState.ENABLED,
|
||||
RecoveryState.DISABLED -> AsyncData.Success(false)
|
||||
}
|
||||
}
|
||||
.collectAsState()
|
||||
val buttonsState by remember {
|
||||
derivedStateOf {
|
||||
val canUseAnotherDevice = hasDevicesToVerifyAgainst.dataOrNull()
|
||||
val canEnterRecoveryKey = canEnterRecoveryKey.dataOrNull()
|
||||
if (canUseAnotherDevice == null || canEnterRecoveryKey == null) {
|
||||
AsyncData.Loading()
|
||||
} else {
|
||||
AsyncData.Success(
|
||||
ChooseSelfVerificationModeState.ButtonsState(
|
||||
canUseAnotherDevice = canUseAnotherDevice,
|
||||
canEnterRecoveryKey = canEnterRecoveryKey,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val directLogoutState = directLogoutPresenter.present()
|
||||
|
||||
@@ -39,8 +66,7 @@ class ChooseSelfVerificationModePresenter(
|
||||
}
|
||||
|
||||
return ChooseSelfVerificationModeState(
|
||||
canUseAnotherDevice = hasDevicesToVerifyAgainst,
|
||||
canEnterRecoveryKey = canEnterRecoveryKey,
|
||||
buttonsState = buttonsState,
|
||||
directLogoutState = directLogoutState,
|
||||
eventSink = ::eventHandler,
|
||||
)
|
||||
|
||||
@@ -8,10 +8,15 @@
|
||||
package io.element.android.features.ftue.impl.sessionverification.choosemode
|
||||
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
data class ChooseSelfVerificationModeState(
|
||||
val canUseAnotherDevice: Boolean,
|
||||
val canEnterRecoveryKey: Boolean,
|
||||
val buttonsState: AsyncData<ButtonsState>,
|
||||
val directLogoutState: DirectLogoutState,
|
||||
val eventSink: (ChooseSelfVerificationModeEvent) -> Unit,
|
||||
)
|
||||
) {
|
||||
data class ButtonsState(
|
||||
val canUseAnotherDevice: Boolean,
|
||||
val canEnterRecoveryKey: Boolean,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,23 +9,49 @@ package io.element.android.features.ftue.impl.sessionverification.choosemode
|
||||
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
|
||||
class ChooseSelfVerificationModeStateProvider :
|
||||
PreviewParameterProvider<ChooseSelfVerificationModeState> {
|
||||
override val values = sequenceOf(
|
||||
aChooseSelfVerificationModeState(canUseAnotherDevice = false, canEnterRecoveryKey = true),
|
||||
aChooseSelfVerificationModeState(canUseAnotherDevice = false, canEnterRecoveryKey = false),
|
||||
aChooseSelfVerificationModeState(canUseAnotherDevice = true, canEnterRecoveryKey = true),
|
||||
aChooseSelfVerificationModeState(canUseAnotherDevice = true, canEnterRecoveryKey = false),
|
||||
aChooseSelfVerificationModeState(
|
||||
buttonsState = AsyncData.Success(
|
||||
aButtonsState(canUseAnotherDevice = false, canEnterRecoveryKey = true),
|
||||
),
|
||||
),
|
||||
aChooseSelfVerificationModeState(
|
||||
buttonsState = AsyncData.Success(
|
||||
aButtonsState(canUseAnotherDevice = false, canEnterRecoveryKey = false),
|
||||
),
|
||||
),
|
||||
aChooseSelfVerificationModeState(
|
||||
buttonsState = AsyncData.Success(
|
||||
aButtonsState(canUseAnotherDevice = true, canEnterRecoveryKey = true),
|
||||
),
|
||||
),
|
||||
aChooseSelfVerificationModeState(
|
||||
buttonsState = AsyncData.Success(
|
||||
aButtonsState(canUseAnotherDevice = true, canEnterRecoveryKey = false),
|
||||
),
|
||||
),
|
||||
aChooseSelfVerificationModeState(
|
||||
buttonsState = AsyncData.Loading(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun aChooseSelfVerificationModeState(
|
||||
canUseAnotherDevice: Boolean = true,
|
||||
canEnterRecoveryKey: Boolean = true,
|
||||
buttonsState: AsyncData<ChooseSelfVerificationModeState.ButtonsState> = AsyncData.Success(aButtonsState()),
|
||||
) = ChooseSelfVerificationModeState(
|
||||
canUseAnotherDevice = canUseAnotherDevice,
|
||||
canEnterRecoveryKey = canEnterRecoveryKey,
|
||||
buttonsState = buttonsState,
|
||||
directLogoutState = aDirectLogoutState(),
|
||||
eventSink = {},
|
||||
)
|
||||
|
||||
fun aButtonsState(
|
||||
canUseAnotherDevice: Boolean = true,
|
||||
canEnterRecoveryKey: Boolean = true,
|
||||
) = ChooseSelfVerificationModeState.ButtonsState(
|
||||
canUseAnotherDevice = canUseAnotherDevice,
|
||||
canEnterRecoveryKey = canEnterRecoveryKey,
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.compound.tokens.generated.CompoundIcons
|
||||
import io.element.android.features.ftue.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
@@ -50,7 +51,6 @@ fun ChooseSelfVerificationModeView(
|
||||
BackHandler {
|
||||
activity?.finish()
|
||||
}
|
||||
|
||||
HeaderFooterPage(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
@@ -73,29 +73,12 @@ fun ChooseSelfVerificationModeView(
|
||||
)
|
||||
},
|
||||
footer = {
|
||||
ButtonColumnMolecule(
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
if (state.canUseAnotherDevice) {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.screen_identity_use_another_device),
|
||||
onClick = onUseAnotherDevice,
|
||||
)
|
||||
}
|
||||
if (state.canEnterRecoveryKey) {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.screen_session_verification_enter_recovery_key),
|
||||
onClick = onUseRecoveryKey,
|
||||
)
|
||||
}
|
||||
OutlinedButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.screen_identity_confirmation_cannot_confirm),
|
||||
onClick = onResetKey,
|
||||
)
|
||||
}
|
||||
ChooseSelfVerificationModeButtons(
|
||||
state = state,
|
||||
onUseAnotherDevice = onUseAnotherDevice,
|
||||
onUseRecoveryKey = onUseRecoveryKey,
|
||||
onResetKey = onResetKey,
|
||||
)
|
||||
}
|
||||
) {
|
||||
Row(
|
||||
@@ -113,6 +96,53 @@ fun ChooseSelfVerificationModeView(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChooseSelfVerificationModeButtons(
|
||||
state: ChooseSelfVerificationModeState,
|
||||
onUseAnotherDevice: () -> Unit,
|
||||
onUseRecoveryKey: () -> Unit,
|
||||
onResetKey: () -> Unit,
|
||||
) {
|
||||
ButtonColumnMolecule(
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
when (state.buttonsState) {
|
||||
AsyncData.Uninitialized,
|
||||
is AsyncData.Failure,
|
||||
is AsyncData.Loading -> {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = false,
|
||||
showProgress = true,
|
||||
text = stringResource(CommonStrings.common_loading),
|
||||
onClick = {},
|
||||
)
|
||||
}
|
||||
is AsyncData.Success -> {
|
||||
if (state.buttonsState.data.canUseAnotherDevice) {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.screen_identity_use_another_device),
|
||||
onClick = onUseAnotherDevice,
|
||||
)
|
||||
}
|
||||
if (state.buttonsState.data.canEnterRecoveryKey) {
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.screen_session_verification_enter_recovery_key),
|
||||
onClick = onUseRecoveryKey,
|
||||
)
|
||||
}
|
||||
OutlinedButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(R.string.screen_identity_confirmation_cannot_confirm),
|
||||
onClick = onResetKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun ChooseSelfVerificationModeViewPreview(
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.google.common.truth.Truth.assertThat
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.architecture.Presenter
|
||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||
@@ -22,23 +23,92 @@ import org.junit.Test
|
||||
|
||||
class ChooseSessionVerificationModePresenterTest {
|
||||
@Test
|
||||
fun `initial state - is relayed from EncryptionService`() = runTest {
|
||||
val encryptionService = FakeEncryptionService().apply {
|
||||
// Has device to verify against
|
||||
emitHasDevicesToVerifyAgainst(false)
|
||||
// Can enter recovery key
|
||||
emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||
}
|
||||
val presenter = createPresenter(encryptionService = encryptionService)
|
||||
fun `present - initial state`() = runTest {
|
||||
val presenter = createPresenter()
|
||||
presenter.test {
|
||||
awaitItem().run {
|
||||
assertThat(canUseAnotherDevice).isFalse()
|
||||
assertThat(canEnterRecoveryKey).isTrue()
|
||||
assertThat(buttonsState.isLoading()).isTrue()
|
||||
assertThat(directLogoutState.logoutAction.isUninitialized()).isTrue()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - state is relayed from EncryptionService, order 1`() = runTest {
|
||||
val encryptionService = FakeEncryptionService()
|
||||
val presenter = createPresenter(encryptionService = encryptionService)
|
||||
presenter.test {
|
||||
assertThat(awaitItem().buttonsState.isLoading()).isTrue()
|
||||
// Has device to verify against
|
||||
encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false))
|
||||
// Can enter recovery key
|
||||
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
|
||||
assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
|
||||
ChooseSelfVerificationModeState.ButtonsState(
|
||||
canUseAnotherDevice = false,
|
||||
canEnterRecoveryKey = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - state is relayed from EncryptionService, order 2`() = runTest {
|
||||
val encryptionService = FakeEncryptionService()
|
||||
val presenter = createPresenter(encryptionService = encryptionService)
|
||||
presenter.test {
|
||||
assertThat(awaitItem().buttonsState.isLoading()).isTrue()
|
||||
// Can enter recovery key
|
||||
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
|
||||
// Has device to verify against
|
||||
encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false))
|
||||
assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
|
||||
ChooseSelfVerificationModeState.ButtonsState(
|
||||
canUseAnotherDevice = false,
|
||||
canEnterRecoveryKey = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - can use another device`() = runTest {
|
||||
val encryptionService = FakeEncryptionService()
|
||||
val presenter = createPresenter(encryptionService = encryptionService)
|
||||
presenter.test {
|
||||
assertThat(awaitItem().buttonsState.isLoading()).isTrue()
|
||||
// Can enter recovery key
|
||||
encryptionService.emitRecoveryState(RecoveryState.DISABLED)
|
||||
// Has device to verify against
|
||||
encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(true))
|
||||
assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
|
||||
ChooseSelfVerificationModeState.ButtonsState(
|
||||
canUseAnotherDevice = true,
|
||||
canEnterRecoveryKey = false,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - can enter recovery key`() = runTest {
|
||||
val encryptionService = FakeEncryptionService()
|
||||
val presenter = createPresenter(encryptionService = encryptionService)
|
||||
presenter.test {
|
||||
assertThat(awaitItem().buttonsState.isLoading()).isTrue()
|
||||
// Can enter recovery key
|
||||
encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE)
|
||||
// Has device to verify against
|
||||
encryptionService.emitHasDevicesToVerifyAgainst(AsyncData.Success(false))
|
||||
assertThat(awaitItem().buttonsState.dataOrNull()).isEqualTo(
|
||||
ChooseSelfVerificationModeState.ButtonsState(
|
||||
canUseAnotherDevice = false,
|
||||
canEnterRecoveryKey = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `sing out action triggers a direct logout`() = runTest {
|
||||
val logoutEventRecorder = lambdaRecorder<DirectLogoutEvents, Unit> {}
|
||||
@@ -49,8 +119,8 @@ class ChooseSessionVerificationModePresenterTest {
|
||||
presenter.test {
|
||||
val initial = awaitItem()
|
||||
initial.eventSink(ChooseSelfVerificationModeEvent.SignOut)
|
||||
|
||||
logoutEventRecorder.assertions().isCalledOnce().with(value(DirectLogoutEvents.Logout(ignoreSdkError = false)))
|
||||
logoutEventRecorder.assertions().isCalledOnce()
|
||||
.with(value(DirectLogoutEvents.Logout(ignoreSdkError = false)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import androidx.compose.ui.test.junit4.AndroidComposeTestRule
|
||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.ftue.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
@@ -43,7 +44,7 @@ class ChooseSessionVerificationModeViewTest {
|
||||
fun `clicking on use another device calls the callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setChooseSelfVerificationModeView(
|
||||
aChooseSelfVerificationModeState(canUseAnotherDevice = true),
|
||||
aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canUseAnotherDevice = true))),
|
||||
onUseAnotherDevice = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_identity_use_another_device)
|
||||
@@ -55,7 +56,7 @@ class ChooseSessionVerificationModeViewTest {
|
||||
fun `clicking on enter recovery key calls the callback`() {
|
||||
ensureCalledOnce { callback ->
|
||||
rule.setChooseSelfVerificationModeView(
|
||||
aChooseSelfVerificationModeState(canEnterRecoveryKey = true),
|
||||
aChooseSelfVerificationModeState(AsyncData.Success(aButtonsState(canEnterRecoveryKey = true))),
|
||||
onEnterRecoveryKey = callback,
|
||||
)
|
||||
rule.clickOn(R.string.screen_session_verification_enter_recovery_key)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
package io.element.android.libraries.matrix.api.encryption
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.encryption.identity.IdentityState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -17,7 +18,7 @@ interface EncryptionService {
|
||||
val recoveryStateStateFlow: StateFlow<RecoveryState>
|
||||
val enableRecoveryProgressStateFlow: StateFlow<EnableRecoveryProgress>
|
||||
val isLastDevice: StateFlow<Boolean>
|
||||
val hasDevicesToVerifyAgainst: StateFlow<Boolean>
|
||||
val hasDevicesToVerifyAgainst: StateFlow<AsyncData<Boolean>>
|
||||
|
||||
suspend fun enableBackups(): Result<Unit>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
package io.element.android.libraries.matrix.impl.encryption
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||
import io.element.android.libraries.core.extensions.flatMap
|
||||
import io.element.android.libraries.core.extensions.mapFailure
|
||||
@@ -42,6 +43,7 @@ import org.matrix.rustcomponents.sdk.Client
|
||||
import org.matrix.rustcomponents.sdk.EnableRecoveryProgressListener
|
||||
import org.matrix.rustcomponents.sdk.Encryption
|
||||
import org.matrix.rustcomponents.sdk.UserIdentity
|
||||
import timber.log.Timber
|
||||
import org.matrix.rustcomponents.sdk.BackupUploadState as RustBackupUploadState
|
||||
import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecoveryProgress
|
||||
import org.matrix.rustcomponents.sdk.RecoveryException as RustRecoveryException
|
||||
@@ -103,14 +105,20 @@ class RustEncryptionService(
|
||||
* TODO This is a temporary workaround, when we will have a way to observe
|
||||
* the sessions, this code will have to be updated.
|
||||
*/
|
||||
override val hasDevicesToVerifyAgainst: StateFlow<Boolean> = flow {
|
||||
override val hasDevicesToVerifyAgainst: StateFlow<AsyncData<Boolean>> = flow {
|
||||
while (currentCoroutineContext().isActive) {
|
||||
val result = hasDevicesToVerifyAgainst().getOrDefault(false)
|
||||
emit(result)
|
||||
val result = hasDevicesToVerifyAgainst()
|
||||
result
|
||||
.onSuccess {
|
||||
emit(AsyncData.Success(it))
|
||||
}
|
||||
.onFailure {
|
||||
Timber.e(it, "Failed to get hasDevicesToVerifyAgainst, retrying in 5s...")
|
||||
}
|
||||
delay(5_000)
|
||||
}
|
||||
}
|
||||
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
|
||||
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, AsyncData.Uninitialized)
|
||||
|
||||
override suspend fun enableBackups(): Result<Unit> = withContext(dispatchers.io) {
|
||||
runCatchingExceptions {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
package io.element.android.libraries.matrix.test.encryption
|
||||
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupState
|
||||
import io.element.android.libraries.matrix.api.encryption.BackupUploadState
|
||||
@@ -34,7 +35,7 @@ class FakeEncryptionService(
|
||||
override val recoveryStateStateFlow: MutableStateFlow<RecoveryState> = MutableStateFlow(RecoveryState.UNKNOWN)
|
||||
override val enableRecoveryProgressStateFlow: MutableStateFlow<EnableRecoveryProgress> = MutableStateFlow(EnableRecoveryProgress.Starting)
|
||||
override val isLastDevice: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
override val hasDevicesToVerifyAgainst: MutableStateFlow<Boolean> = MutableStateFlow(true)
|
||||
override val hasDevicesToVerifyAgainst: MutableStateFlow<AsyncData<Boolean>> = MutableStateFlow(AsyncData.Uninitialized)
|
||||
private var waitForBackupUploadSteadyStateFlow: Flow<BackupUploadState> = flowOf()
|
||||
|
||||
private var recoverFailure: Exception? = null
|
||||
@@ -84,7 +85,7 @@ class FakeEncryptionService(
|
||||
this.isLastDevice.value = isLastDevice
|
||||
}
|
||||
|
||||
fun emitHasDevicesToVerifyAgainst(hasDevicesToVerifyAgainst: Boolean) {
|
||||
fun emitHasDevicesToVerifyAgainst(hasDevicesToVerifyAgainst: AsyncData<Boolean>) {
|
||||
this.hasDevicesToVerifyAgainst.value = hasDevicesToVerifyAgainst
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user