Remove SessionData.needsVerification as the source of truth for session verification status (#2748)
* Remove `SessionData.needsVerification` as the source of truth for session verification status. - Use the Rust SDK `EncryptionService.verificationState()` instead, but always waiting for the first 'known' result (either verified or not, discarding 'unknown'). - Add a workaround in the super rare case when reading this value gets stuck somehow. We'll assume the user is not verified in that case. - Make `DefaultFtueService.getNextStep` and dependent checks `suspend`. - Make the `skip` button use a value in the session preferences instead. * Log exception when the verification status can't be loaded Co-authored-by: Benoit Marty <benoit@matrix.org> * Fix review comments --------- Co-authored-by: Benoit Marty <benoit@matrix.org>
This commit is contained in:
parent
c83712ca91
commit
2cc124bda2
26 changed files with 99 additions and 106 deletions
1
changelog.d/2718.bugfix
Normal file
1
changelog.d/2718.bugfix
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Fix session verification being asked again for already verified users.
|
||||||
|
|
@ -39,6 +39,7 @@ dependencies {
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.features.analytics.api)
|
implementation(projects.features.analytics.api)
|
||||||
|
|
@ -60,6 +61,7 @@ dependencies {
|
||||||
testImplementation(projects.services.analytics.test)
|
testImplementation(projects.services.analytics.test)
|
||||||
testImplementation(projects.libraries.permissions.impl)
|
testImplementation(projects.libraries.permissions.impl)
|
||||||
testImplementation(projects.libraries.permissions.test)
|
testImplementation(projects.libraries.permissions.test)
|
||||||
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.features.lockscreen.test)
|
testImplementation(projects.features.lockscreen.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ class FtueFlowNode @AssistedInject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun moveToNextStep() {
|
private fun moveToNextStep() = lifecycleScope.launch {
|
||||||
when (ftueState.getNextStep()) {
|
when (ftueState.getNextStep()) {
|
||||||
FtueStep.SessionVerification -> {
|
FtueStep.SessionVerification -> {
|
||||||
backstack.newRoot(NavTarget.SessionVerification)
|
backstack.newRoot(NavTarget.SessionVerification)
|
||||||
|
|
|
||||||
|
|
@ -23,18 +23,26 @@ import com.squareup.anvil.annotations.ContributesBinding
|
||||||
import io.element.android.features.ftue.api.state.FtueService
|
import io.element.android.features.ftue.api.state.FtueService
|
||||||
import io.element.android.features.ftue.api.state.FtueState
|
import io.element.android.features.ftue.api.state.FtueState
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
|
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
import io.element.android.libraries.permissions.api.PermissionStateProvider
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.first
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.timeout
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@ContributesBinding(SessionScope::class)
|
@ContributesBinding(SessionScope::class)
|
||||||
class DefaultFtueService @Inject constructor(
|
class DefaultFtueService @Inject constructor(
|
||||||
|
|
@ -44,6 +52,7 @@ class DefaultFtueService @Inject constructor(
|
||||||
private val permissionStateProvider: PermissionStateProvider,
|
private val permissionStateProvider: PermissionStateProvider,
|
||||||
private val lockScreenService: LockScreenService,
|
private val lockScreenService: LockScreenService,
|
||||||
private val sessionVerificationService: SessionVerificationService,
|
private val sessionVerificationService: SessionVerificationService,
|
||||||
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
) : FtueService {
|
) : FtueService {
|
||||||
override val state = MutableStateFlow<FtueState>(FtueState.Unknown)
|
override val state = MutableStateFlow<FtueState>(FtueState.Unknown)
|
||||||
|
|
||||||
|
|
@ -55,7 +64,7 @@ class DefaultFtueService @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
sessionVerificationService.needsVerificationFlow
|
sessionVerificationService.sessionVerifiedStatus
|
||||||
.onEach { updateState() }
|
.onEach { updateState() }
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
|
|
||||||
|
|
@ -64,7 +73,7 @@ class DefaultFtueService @Inject constructor(
|
||||||
.launchIn(coroutineScope)
|
.launchIn(coroutineScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
suspend fun getNextStep(currentStep: FtueStep? = null): FtueStep? =
|
||||||
when (currentStep) {
|
when (currentStep) {
|
||||||
null -> if (isSessionNotVerified()) {
|
null -> if (isSessionNotVerified()) {
|
||||||
FtueStep.SessionVerification
|
FtueStep.SessionVerification
|
||||||
|
|
@ -89,8 +98,8 @@ class DefaultFtueService @Inject constructor(
|
||||||
FtueStep.AnalyticsOptIn -> null
|
FtueStep.AnalyticsOptIn -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isAnyStepIncomplete(): Boolean {
|
private suspend fun isAnyStepIncomplete(): Boolean {
|
||||||
return listOf(
|
return listOf<suspend () -> Boolean>(
|
||||||
{ isSessionNotVerified() },
|
{ isSessionNotVerified() },
|
||||||
{ shouldAskNotificationPermissions() },
|
{ shouldAskNotificationPermissions() },
|
||||||
{ needsAnalyticsOptIn() },
|
{ needsAnalyticsOptIn() },
|
||||||
|
|
@ -98,16 +107,28 @@ class DefaultFtueService @Inject constructor(
|
||||||
).any { it() }
|
).any { it() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun isSessionNotVerified(): Boolean {
|
@OptIn(FlowPreview::class)
|
||||||
return sessionVerificationService.needsVerificationFlow.value
|
private suspend fun isSessionNotVerified(): Boolean {
|
||||||
|
// Wait for the first known (or ready) verification status
|
||||||
|
val readyVerifiedSessionStatus = sessionVerificationService.sessionVerifiedStatus
|
||||||
|
.filter { it != SessionVerifiedStatus.Unknown }
|
||||||
|
// This is not ideal, but there are some very rare cases when reading the flow seems to get stuck
|
||||||
|
.timeout(5.seconds)
|
||||||
|
.catch {
|
||||||
|
Timber.e(it, "Failed to get session verification status, assume it's not verified")
|
||||||
|
emit(SessionVerifiedStatus.NotVerified)
|
||||||
|
}
|
||||||
|
.first()
|
||||||
|
val skipVerification = suspend { sessionPreferencesStore.isSessionVerificationSkipped().first() }
|
||||||
|
return readyVerifiedSessionStatus == SessionVerifiedStatus.NotVerified && !skipVerification()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun needsAnalyticsOptIn(): Boolean {
|
private suspend fun needsAnalyticsOptIn(): Boolean {
|
||||||
// We need this function to not be suspend, so we need to load the value through runBlocking
|
// We need this function to not be suspend, so we need to load the value through runBlocking
|
||||||
return runBlocking { analyticsService.didAskUserConsent().first().not() }
|
return analyticsService.didAskUserConsent().first().not()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldAskNotificationPermissions(): Boolean {
|
private suspend fun shouldAskNotificationPermissions(): Boolean {
|
||||||
return if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
|
return if (sdkVersionProvider.isAtLeast(Build.VERSION_CODES.TIRAMISU)) {
|
||||||
val permission = Manifest.permission.POST_NOTIFICATIONS
|
val permission = Manifest.permission.POST_NOTIFICATIONS
|
||||||
val isPermissionDenied = runBlocking { permissionStateProvider.isPermissionDenied(permission).first() }
|
val isPermissionDenied = runBlocking { permissionStateProvider.isPermissionDenied(permission).first() }
|
||||||
|
|
@ -118,14 +139,12 @@ class DefaultFtueService @Inject constructor(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldDisplayLockscreenSetup(): Boolean {
|
private suspend fun shouldDisplayLockscreenSetup(): Boolean {
|
||||||
return runBlocking {
|
return lockScreenService.isSetupRequired().first()
|
||||||
lockScreenService.isSetupRequired().first()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
||||||
internal fun updateState() {
|
internal suspend fun updateState() {
|
||||||
state.value = when {
|
state.value = when {
|
||||||
isAnyStepIncomplete() -> FtueState.Incomplete
|
isAnyStepIncomplete() -> FtueState.Incomplete
|
||||||
else -> FtueState.Complete
|
else -> FtueState.Complete
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import io.element.android.features.ftue.impl.state.DefaultFtueService
|
||||||
import io.element.android.features.ftue.impl.state.FtueStep
|
import io.element.android.features.ftue.impl.state.FtueStep
|
||||||
import io.element.android.features.lockscreen.api.LockScreenService
|
import io.element.android.features.lockscreen.api.LockScreenService
|
||||||
import io.element.android.features.lockscreen.test.FakeLockScreenService
|
import io.element.android.features.lockscreen.test.FakeLockScreenService
|
||||||
|
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
import io.element.android.libraries.permissions.impl.FakePermissionStateProvider
|
||||||
|
|
@ -90,7 +91,6 @@ class DefaultFtueServiceTests {
|
||||||
fun `traverse flow`() = runTest {
|
fun `traverse flow`() = runTest {
|
||||||
val sessionVerificationService = FakeSessionVerificationService().apply {
|
val sessionVerificationService = FakeSessionVerificationService().apply {
|
||||||
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||||
givenNeedsVerification(true)
|
|
||||||
}
|
}
|
||||||
val analyticsService = FakeAnalyticsService()
|
val analyticsService = FakeAnalyticsService()
|
||||||
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
|
||||||
|
|
@ -108,7 +108,7 @@ class DefaultFtueServiceTests {
|
||||||
|
|
||||||
// Session verification
|
// Session verification
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
sessionVerificationService.givenNeedsVerification(false)
|
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||||
|
|
||||||
// Notifications opt in
|
// Notifications opt in
|
||||||
steps.add(state.getNextStep(steps.lastOrNull()))
|
steps.add(state.getNextStep(steps.lastOrNull()))
|
||||||
|
|
@ -200,6 +200,7 @@ class DefaultFtueServiceTests {
|
||||||
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
analyticsService: AnalyticsService = FakeAnalyticsService(),
|
||||||
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
|
permissionStateProvider: FakePermissionStateProvider = FakePermissionStateProvider(permissionGranted = false),
|
||||||
lockScreenService: LockScreenService = FakeLockScreenService(),
|
lockScreenService: LockScreenService = FakeLockScreenService(),
|
||||||
|
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
// First version where notification permission is required
|
// First version where notification permission is required
|
||||||
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
sdkIntVersion: Int = Build.VERSION_CODES.TIRAMISU,
|
||||||
) = DefaultFtueService(
|
) = DefaultFtueService(
|
||||||
|
|
@ -209,5 +210,6 @@ class DefaultFtueServiceTests {
|
||||||
analyticsService = analyticsService,
|
analyticsService = analyticsService,
|
||||||
permissionStateProvider = permissionStateProvider,
|
permissionStateProvider = permissionStateProvider,
|
||||||
lockScreenService = lockScreenService,
|
lockScreenService = lockScreenService,
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ fun aSignedOutState() = SignedOutState(
|
||||||
fun aSessionData(
|
fun aSessionData(
|
||||||
sessionId: SessionId = SessionId("@alice:server.org"),
|
sessionId: SessionId = SessionId("@alice:server.org"),
|
||||||
isTokenValid: Boolean = false,
|
isTokenValid: Boolean = false,
|
||||||
needsVerification: Boolean = false,
|
|
||||||
): SessionData {
|
): SessionData {
|
||||||
return SessionData(
|
return SessionData(
|
||||||
userId = sessionId.value,
|
userId = sessionId.value,
|
||||||
|
|
@ -52,6 +51,5 @@ fun aSessionData(
|
||||||
isTokenValid = isTokenValid,
|
isTokenValid = isTokenValid,
|
||||||
loginType = LoginType.UNKNOWN,
|
loginType = LoginType.UNKNOWN,
|
||||||
passphrase = null,
|
passphrase = null,
|
||||||
needsVerification = needsVerification,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ dependencies {
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
implementation(projects.libraries.matrixui)
|
implementation(projects.libraries.matrixui)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
|
implementation(projects.libraries.preferences.api)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
api(libs.statemachine)
|
api(libs.statemachine)
|
||||||
api(projects.features.verifysession.api)
|
api(projects.features.verifysession.api)
|
||||||
|
|
@ -53,6 +54,7 @@ dependencies {
|
||||||
testImplementation(libs.test.truth)
|
testImplementation(libs.test.truth)
|
||||||
testImplementation(libs.test.turbine)
|
testImplementation(libs.test.turbine)
|
||||||
testImplementation(projects.libraries.matrix.test)
|
testImplementation(projects.libraries.matrix.test)
|
||||||
|
testImplementation(projects.libraries.preferences.test)
|
||||||
testImplementation(projects.tests.testutils)
|
testImplementation(projects.tests.testutils)
|
||||||
testImplementation(libs.androidx.compose.ui.test.junit)
|
testImplementation(libs.androidx.compose.ui.test.junit)
|
||||||
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
testReleaseImplementation(libs.androidx.compose.ui.test.manifest)
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,11 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import com.freeletics.flowredux.compose.rememberStateAndDispatch
|
import com.freeletics.flowredux.compose.rememberStateAndDispatch
|
||||||
|
import io.element.android.features.preferences.api.store.SessionPreferencesStore
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
|
@ -49,6 +49,7 @@ class VerifySelfSessionPresenter @Inject constructor(
|
||||||
private val encryptionService: EncryptionService,
|
private val encryptionService: EncryptionService,
|
||||||
private val stateMachine: VerifySelfSessionStateMachine,
|
private val stateMachine: VerifySelfSessionStateMachine,
|
||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
|
private val sessionPreferencesStore: SessionPreferencesStore,
|
||||||
) : Presenter<VerifySelfSessionState> {
|
) : Presenter<VerifySelfSessionState> {
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): VerifySelfSessionState {
|
override fun present(): VerifySelfSessionState {
|
||||||
|
|
@ -59,8 +60,8 @@ class VerifySelfSessionPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
|
||||||
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
|
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
|
||||||
var skipVerification by remember { mutableStateOf(false) }
|
val skipVerification by sessionPreferencesStore.isSessionVerificationSkipped().collectAsState(initial = false)
|
||||||
val needsVerification by sessionVerificationService.needsVerificationFlow.collectAsState()
|
val needsVerification by sessionVerificationService.canVerifySessionFlow.collectAsState(initial = true)
|
||||||
val verificationFlowStep by remember {
|
val verificationFlowStep by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
when {
|
when {
|
||||||
|
|
@ -86,8 +87,7 @@ class VerifySelfSessionPresenter @Inject constructor(
|
||||||
VerifySelfSessionViewEvents.Cancel -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel)
|
VerifySelfSessionViewEvents.Cancel -> stateAndDispatch.dispatchAction(StateMachineEvent.Cancel)
|
||||||
VerifySelfSessionViewEvents.Reset -> stateAndDispatch.dispatchAction(StateMachineEvent.Reset)
|
VerifySelfSessionViewEvents.Reset -> stateAndDispatch.dispatchAction(StateMachineEvent.Reset)
|
||||||
VerifySelfSessionViewEvents.SkipVerification -> coroutineScope.launch {
|
VerifySelfSessionViewEvents.SkipVerification -> coroutineScope.launch {
|
||||||
sessionVerificationService.saveVerifiedState(true)
|
sessionPreferencesStore.setSkipSessionVerification(true)
|
||||||
skipVerification = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ import com.google.common.truth.Truth.assertThat
|
||||||
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep
|
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
import io.element.android.libraries.featureflag.test.InMemorySessionPreferencesStore
|
||||||
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
||||||
|
|
@ -35,7 +36,6 @@ import io.element.android.libraries.matrix.test.core.aBuildMeta
|
||||||
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
|
||||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||||
import io.element.android.tests.testutils.WarmUpRule
|
import io.element.android.tests.testutils.WarmUpRule
|
||||||
import io.element.android.tests.testutils.lambda.value
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
|
|
@ -289,7 +289,6 @@ class VerifySelfSessionPresenterTests {
|
||||||
}.test {
|
}.test {
|
||||||
val state = requestVerificationAndAwaitVerifyingState(service)
|
val state = requestVerificationAndAwaitVerifyingState(service)
|
||||||
state.eventSink(VerifySelfSessionViewEvents.SkipVerification)
|
state.eventSink(VerifySelfSessionViewEvents.SkipVerification)
|
||||||
service.saveVerifiedStateResult.assertions().isCalledOnce().with(value(true))
|
|
||||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Skipped)
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Skipped)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -297,12 +296,16 @@ class VerifySelfSessionPresenterTests {
|
||||||
@Test
|
@Test
|
||||||
fun `present - When verification is not needed, the flow is completed`() = runTest {
|
fun `present - When verification is not needed, the flow is completed`() = runTest {
|
||||||
val service = FakeSessionVerificationService().apply {
|
val service = FakeSessionVerificationService().apply {
|
||||||
givenNeedsVerification(false)
|
givenCanVerifySession(false)
|
||||||
|
givenIsReady(true)
|
||||||
|
givenVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||||
|
givenVerificationFlowState(VerificationFlowState.Finished)
|
||||||
}
|
}
|
||||||
val presenter = createVerifySelfSessionPresenter(service)
|
val presenter = createVerifySelfSessionPresenter(service)
|
||||||
moleculeFlow(RecompositionMode.Immediate) {
|
moleculeFlow(RecompositionMode.Immediate) {
|
||||||
presenter.present()
|
presenter.present()
|
||||||
}.test {
|
}.test {
|
||||||
|
skipItems(1)
|
||||||
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed)
|
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -334,7 +337,6 @@ class VerifySelfSessionPresenterTests {
|
||||||
private fun unverifiedSessionService(): FakeSessionVerificationService {
|
private fun unverifiedSessionService(): FakeSessionVerificationService {
|
||||||
return FakeSessionVerificationService().apply {
|
return FakeSessionVerificationService().apply {
|
||||||
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
|
||||||
givenNeedsVerification(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -342,12 +344,14 @@ class VerifySelfSessionPresenterTests {
|
||||||
service: SessionVerificationService = unverifiedSessionService(),
|
service: SessionVerificationService = unverifiedSessionService(),
|
||||||
encryptionService: EncryptionService = FakeEncryptionService(),
|
encryptionService: EncryptionService = FakeEncryptionService(),
|
||||||
buildMeta: BuildMeta = aBuildMeta(),
|
buildMeta: BuildMeta = aBuildMeta(),
|
||||||
|
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),
|
||||||
): VerifySelfSessionPresenter {
|
): VerifySelfSessionPresenter {
|
||||||
return VerifySelfSessionPresenter(
|
return VerifySelfSessionPresenter(
|
||||||
sessionVerificationService = service,
|
sessionVerificationService = service,
|
||||||
encryptionService = encryptionService,
|
encryptionService = encryptionService,
|
||||||
stateMachine = VerifySelfSessionStateMachine(service, encryptionService),
|
stateMachine = VerifySelfSessionStateMachine(service, encryptionService),
|
||||||
buildMeta = buildMeta,
|
buildMeta = buildMeta,
|
||||||
|
sessionPreferencesStore = sessionPreferencesStore,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,17 +21,6 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
interface SessionVerificationService {
|
interface SessionVerificationService {
|
||||||
/**
|
|
||||||
* This flow stores the local verification status of the current session.
|
|
||||||
*
|
|
||||||
* We should ideally base the verified status in the Rust SDK info, but there are several issues with that approach:
|
|
||||||
*
|
|
||||||
* - The SDK takes a while to report this value, resulting in a delay of 1-2s in displaying the UI.
|
|
||||||
* - We need to add a 'Skip' option for testing purposes, which would not be possible if we relied only on the SDK.
|
|
||||||
* - The SDK sometimes doesn't report the verification state if there is no network connection when the app boots.
|
|
||||||
*/
|
|
||||||
val needsVerificationFlow: StateFlow<Boolean>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State of the current verification flow ([VerificationFlowState.Initial] if not started).
|
* State of the current verification flow ([VerificationFlowState.Initial] if not started).
|
||||||
*/
|
*/
|
||||||
|
|
@ -83,11 +72,6 @@ interface SessionVerificationService {
|
||||||
* Returns the verification service state to the initial step.
|
* Returns the verification service state to the initial step.
|
||||||
*/
|
*/
|
||||||
suspend fun reset()
|
suspend fun reset()
|
||||||
|
|
||||||
/**
|
|
||||||
* Saves the current session state as [verified].
|
|
||||||
*/
|
|
||||||
suspend fun saveVerifiedState(verified: Boolean)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Verification status of the current session. */
|
/** Verification status of the current session. */
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,6 @@ class RustMatrixClient(
|
||||||
syncService = rustSyncService,
|
syncService = rustSyncService,
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
sessionCoroutineScope = sessionCoroutineScope,
|
||||||
dispatchers = dispatchers,
|
dispatchers = dispatchers,
|
||||||
sessionStore = sessionStore,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private val roomDirectoryService = RustRoomDirectoryService(
|
private val roomDirectoryService = RustRoomDirectoryService(
|
||||||
|
|
@ -188,7 +187,6 @@ class RustMatrixClient(
|
||||||
isTokenValid = false,
|
isTokenValid = false,
|
||||||
loginType = existingData.loginType,
|
loginType = existingData.loginType,
|
||||||
passphrase = existingData.passphrase,
|
passphrase = existingData.passphrase,
|
||||||
needsVerification = existingData.needsVerification,
|
|
||||||
)
|
)
|
||||||
sessionStore.updateData(newData)
|
sessionStore.updateData(newData)
|
||||||
Timber.d("Removed session data with token: '...$anonymizedToken'.")
|
Timber.d("Removed session data with token: '...$anonymizedToken'.")
|
||||||
|
|
@ -216,7 +214,6 @@ class RustMatrixClient(
|
||||||
isTokenValid = true,
|
isTokenValid = true,
|
||||||
loginType = existingData.loginType,
|
loginType = existingData.loginType,
|
||||||
passphrase = existingData.passphrase,
|
passphrase = existingData.passphrase,
|
||||||
needsVerification = existingData.needsVerification,
|
|
||||||
)
|
)
|
||||||
sessionStore.updateData(newData)
|
sessionStore.updateData(newData)
|
||||||
Timber.d("Saved new session data with token: '...$anonymizedToken'.")
|
Timber.d("Saved new session data with token: '...$anonymizedToken'.")
|
||||||
|
|
@ -242,7 +239,6 @@ class RustMatrixClient(
|
||||||
client = client,
|
client = client,
|
||||||
isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running },
|
isSyncServiceReady = rustSyncService.syncState.map { it == SyncState.Running },
|
||||||
sessionCoroutineScope = sessionCoroutineScope,
|
sessionCoroutineScope = sessionCoroutineScope,
|
||||||
sessionStore = sessionStore,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
private val eventFilters = TimelineConfig.excludedEvents
|
private val eventFilters = TimelineConfig.excludedEvents
|
||||||
|
|
|
||||||
|
|
@ -138,7 +138,6 @@ class RustMatrixAuthenticationService @Inject constructor(
|
||||||
isTokenValid = true,
|
isTokenValid = true,
|
||||||
loginType = LoginType.PASSWORD,
|
loginType = LoginType.PASSWORD,
|
||||||
passphrase = pendingPassphrase,
|
passphrase = pendingPassphrase,
|
||||||
needsVerification = true,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sessionStore.storeData(sessionData)
|
sessionStore.storeData(sessionData)
|
||||||
|
|
@ -187,7 +186,6 @@ class RustMatrixAuthenticationService @Inject constructor(
|
||||||
isTokenValid = true,
|
isTokenValid = true,
|
||||||
loginType = LoginType.OIDC,
|
loginType = LoginType.OIDC,
|
||||||
passphrase = pendingPassphrase,
|
passphrase = pendingPassphrase,
|
||||||
needsVerification = true,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
pendingOidcAuthenticationData?.close()
|
pendingOidcAuthenticationData?.close()
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import io.element.android.libraries.matrix.api.encryption.EncryptionService
|
||||||
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
import io.element.android.libraries.matrix.api.encryption.RecoveryState
|
||||||
import io.element.android.libraries.matrix.api.sync.SyncState
|
import io.element.android.libraries.matrix.api.sync.SyncState
|
||||||
import io.element.android.libraries.matrix.impl.sync.RustSyncService
|
import io.element.android.libraries.matrix.impl.sync.RustSyncService
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.channels.awaitClose
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
|
@ -49,11 +48,10 @@ import org.matrix.rustcomponents.sdk.EnableRecoveryProgress as RustEnableRecover
|
||||||
import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException
|
import org.matrix.rustcomponents.sdk.SteadyStateException as RustSteadyStateException
|
||||||
|
|
||||||
internal class RustEncryptionService(
|
internal class RustEncryptionService(
|
||||||
private val client: Client,
|
client: Client,
|
||||||
syncService: RustSyncService,
|
syncService: RustSyncService,
|
||||||
sessionCoroutineScope: CoroutineScope,
|
sessionCoroutineScope: CoroutineScope,
|
||||||
private val dispatchers: CoroutineDispatchers,
|
private val dispatchers: CoroutineDispatchers,
|
||||||
private val sessionStore: SessionStore,
|
|
||||||
) : EncryptionService {
|
) : EncryptionService {
|
||||||
private val service: Encryption = client.encryption()
|
private val service: Encryption = client.encryption()
|
||||||
|
|
||||||
|
|
@ -188,9 +186,6 @@ internal class RustEncryptionService(
|
||||||
override suspend fun recover(recoveryKey: String): Result<Unit> = withContext(dispatchers.io) {
|
override suspend fun recover(recoveryKey: String): Result<Unit> = withContext(dispatchers.io) {
|
||||||
runCatching {
|
runCatching {
|
||||||
service.recover(recoveryKey)
|
service.recover(recoveryKey)
|
||||||
val existingSession = sessionStore.getSession(client.userId())
|
|
||||||
?: error("Failed to save verification state. No session with id ${client.userId()}")
|
|
||||||
sessionStore.updateData(existingSession.copy(needsVerification = false))
|
|
||||||
}.mapFailure {
|
}.mapFailure {
|
||||||
it.mapRecoveryException()
|
it.mapRecoveryException()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ internal fun Session.toSessionData(
|
||||||
isTokenValid: Boolean,
|
isTokenValid: Boolean,
|
||||||
loginType: LoginType,
|
loginType: LoginType,
|
||||||
passphrase: String?,
|
passphrase: String?,
|
||||||
needsVerification: Boolean,
|
|
||||||
) = SessionData(
|
) = SessionData(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
deviceId = deviceId,
|
deviceId = deviceId,
|
||||||
|
|
@ -38,5 +37,4 @@ internal fun Session.toSessionData(
|
||||||
isTokenValid = isTokenValid,
|
isTokenValid = isTokenValid,
|
||||||
loginType = loginType,
|
loginType = loginType,
|
||||||
passphrase = passphrase,
|
passphrase = passphrase,
|
||||||
needsVerification = needsVerification,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
|
|
||||||
package io.element.android.libraries.matrix.impl.verification
|
package io.element.android.libraries.matrix.impl.verification
|
||||||
|
|
||||||
import io.element.android.libraries.core.bool.orFalse
|
|
||||||
import io.element.android.libraries.core.data.tryOrNull
|
import io.element.android.libraries.core.data.tryOrNull
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
|
|
@ -24,7 +23,6 @@ import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatu
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||||
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
|
||||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
@ -33,10 +31,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -58,7 +53,6 @@ class RustSessionVerificationService(
|
||||||
private val client: Client,
|
private val client: Client,
|
||||||
isSyncServiceReady: Flow<Boolean>,
|
isSyncServiceReady: Flow<Boolean>,
|
||||||
private val sessionCoroutineScope: CoroutineScope,
|
private val sessionCoroutineScope: CoroutineScope,
|
||||||
private val sessionStore: SessionStore,
|
|
||||||
) : SessionVerificationService, SessionVerificationControllerDelegate {
|
) : SessionVerificationService, SessionVerificationControllerDelegate {
|
||||||
private val encryptionService: Encryption = client.encryption()
|
private val encryptionService: Encryption = client.encryption()
|
||||||
private lateinit var verificationController: SessionVerificationController
|
private lateinit var verificationController: SessionVerificationController
|
||||||
|
|
@ -80,11 +74,6 @@ class RustSessionVerificationService(
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
override val needsVerificationFlow: StateFlow<Boolean> = sessionStore.sessionsFlow()
|
|
||||||
.map { sessions -> sessions.firstOrNull { it.userId == client.userId() }?.needsVerification.orFalse() }
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.stateIn(sessionCoroutineScope, SharingStarted.Eagerly, false)
|
|
||||||
|
|
||||||
private val _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
|
private val _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
|
||||||
override val verificationFlowState = _verificationFlowState.asStateFlow()
|
override val verificationFlowState = _verificationFlowState.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -98,6 +87,9 @@ class RustSessionVerificationService(
|
||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
// Update initial state in case sliding sync isn't ready
|
||||||
|
updateVerificationStatus(encryptionService.verificationState())
|
||||||
|
|
||||||
isReady.onEach { isReady ->
|
isReady.onEach { isReady ->
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
Timber.d("Starting verification service")
|
Timber.d("Starting verification service")
|
||||||
|
|
@ -165,7 +157,6 @@ class RustSessionVerificationService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onSuccess {
|
.onSuccess {
|
||||||
saveVerifiedState(true)
|
|
||||||
updateVerificationStatus(VerificationState.VERIFIED)
|
updateVerificationStatus(VerificationState.VERIFIED)
|
||||||
_verificationFlowState.value = VerificationFlowState.Finished
|
_verificationFlowState.value = VerificationFlowState.Finished
|
||||||
}
|
}
|
||||||
|
|
@ -195,14 +186,6 @@ class RustSessionVerificationService(
|
||||||
_verificationFlowState.value = VerificationFlowState.Initial
|
_verificationFlowState.value = VerificationFlowState.Initial
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun saveVerifiedState(verified: Boolean) = tryOrFail {
|
|
||||||
val existingSession = sessionStore.getSession(client.userId())
|
|
||||||
?: error("Failed to save verification state. No session with id ${client.userId()}")
|
|
||||||
sessionStore.updateData(existingSession.copy(needsVerification = !verified))
|
|
||||||
// Wait until the new state is saved
|
|
||||||
needsVerificationFlow.first { needsVerification -> !needsVerification }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun destroy() {
|
fun destroy() {
|
||||||
Timber.d("Destroying RustSessionVerificationService")
|
Timber.d("Destroying RustSessionVerificationService")
|
||||||
verificationStateListenerTaskHandle.cancelAndDestroy()
|
verificationStateListenerTaskHandle.cancelAndDestroy()
|
||||||
|
|
|
||||||
|
|
@ -20,22 +20,17 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
|
||||||
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
|
||||||
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
|
||||||
import io.element.android.tests.testutils.lambda.LambdaOneParamRecorder
|
|
||||||
import io.element.android.tests.testutils.lambda.lambdaRecorder
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
class FakeSessionVerificationService(
|
class FakeSessionVerificationService : SessionVerificationService {
|
||||||
var saveVerifiedStateResult: LambdaOneParamRecorder<Boolean, Unit> = lambdaRecorder<Boolean, Unit> {}
|
|
||||||
) : SessionVerificationService {
|
|
||||||
private val _isReady = MutableStateFlow(false)
|
private val _isReady = MutableStateFlow(false)
|
||||||
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
|
private val _sessionVerifiedStatus = MutableStateFlow<SessionVerifiedStatus>(SessionVerifiedStatus.Unknown)
|
||||||
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
|
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
|
||||||
private var _canVerifySessionFlow = MutableStateFlow(true)
|
private var _canVerifySessionFlow = MutableStateFlow(true)
|
||||||
var shouldFail = false
|
var shouldFail = false
|
||||||
|
|
||||||
override val needsVerificationFlow: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
|
||||||
override val verificationFlowState: StateFlow<VerificationFlowState> = _verificationFlowState
|
override val verificationFlowState: StateFlow<VerificationFlowState> = _verificationFlowState
|
||||||
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
|
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
|
||||||
override val canVerifySessionFlow: Flow<Boolean> = _canVerifySessionFlow
|
override val canVerifySessionFlow: Flow<Boolean> = _canVerifySessionFlow
|
||||||
|
|
@ -94,15 +89,7 @@ class FakeSessionVerificationService(
|
||||||
_isReady.value = value
|
_isReady.value = value
|
||||||
}
|
}
|
||||||
|
|
||||||
fun givenNeedsVerification(value: Boolean) {
|
|
||||||
needsVerificationFlow.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun reset() {
|
override suspend fun reset() {
|
||||||
_verificationFlowState.value = VerificationFlowState.Initial
|
_verificationFlowState.value = VerificationFlowState.Initial
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun saveVerifiedState(verified: Boolean) {
|
|
||||||
saveVerifiedStateResult(verified)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,5 +34,8 @@ interface SessionPreferencesStore {
|
||||||
suspend fun setRenderTypingNotifications(enabled: Boolean)
|
suspend fun setRenderTypingNotifications(enabled: Boolean)
|
||||||
fun isRenderTypingNotificationsEnabled(): Flow<Boolean>
|
fun isRenderTypingNotificationsEnabled(): Flow<Boolean>
|
||||||
|
|
||||||
|
suspend fun setSkipSessionVerification(skip: Boolean)
|
||||||
|
fun isSessionVerificationSkipped(): Flow<Boolean>
|
||||||
|
|
||||||
suspend fun clear()
|
suspend fun clear()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ class DefaultSessionPreferencesStore(
|
||||||
private val renderReadReceiptsKey = booleanPreferencesKey("renderReadReceipts")
|
private val renderReadReceiptsKey = booleanPreferencesKey("renderReadReceipts")
|
||||||
private val sendTypingNotificationsKey = booleanPreferencesKey("sendTypingNotifications")
|
private val sendTypingNotificationsKey = booleanPreferencesKey("sendTypingNotifications")
|
||||||
private val renderTypingNotificationsKey = booleanPreferencesKey("renderTypingNotifications")
|
private val renderTypingNotificationsKey = booleanPreferencesKey("renderTypingNotifications")
|
||||||
|
private val skipSessionVerification = booleanPreferencesKey("skipSessionVerification")
|
||||||
|
|
||||||
private val dataStoreFile = storeFile(context, sessionId)
|
private val dataStoreFile = storeFile(context, sessionId)
|
||||||
private val store = PreferenceDataStoreFactory.create(
|
private val store = PreferenceDataStoreFactory.create(
|
||||||
|
|
@ -86,6 +87,9 @@ class DefaultSessionPreferencesStore(
|
||||||
override suspend fun setRenderTypingNotifications(enabled: Boolean) = update(renderTypingNotificationsKey, enabled)
|
override suspend fun setRenderTypingNotifications(enabled: Boolean) = update(renderTypingNotificationsKey, enabled)
|
||||||
override fun isRenderTypingNotificationsEnabled(): Flow<Boolean> = get(renderTypingNotificationsKey) { true }
|
override fun isRenderTypingNotificationsEnabled(): Flow<Boolean> = get(renderTypingNotificationsKey) { true }
|
||||||
|
|
||||||
|
override suspend fun setSkipSessionVerification(skip: Boolean) = update(skipSessionVerification, skip)
|
||||||
|
override fun isSessionVerificationSkipped(): Flow<Boolean> = get(skipSessionVerification) { false }
|
||||||
|
|
||||||
override suspend fun clear() {
|
override suspend fun clear() {
|
||||||
dataStoreFile.safeDelete()
|
dataStoreFile.safeDelete()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,12 +26,14 @@ class InMemorySessionPreferencesStore(
|
||||||
isRenderReadReceiptsEnabled: Boolean = true,
|
isRenderReadReceiptsEnabled: Boolean = true,
|
||||||
isSendTypingNotificationsEnabled: Boolean = true,
|
isSendTypingNotificationsEnabled: Boolean = true,
|
||||||
isRenderTypingNotificationsEnabled: Boolean = true,
|
isRenderTypingNotificationsEnabled: Boolean = true,
|
||||||
|
isSessionVerificationSkipped: Boolean = false,
|
||||||
) : SessionPreferencesStore {
|
) : SessionPreferencesStore {
|
||||||
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
|
private val isSharePresenceEnabled = MutableStateFlow(isSharePresenceEnabled)
|
||||||
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
|
private val isSendPublicReadReceiptsEnabled = MutableStateFlow(isSendPublicReadReceiptsEnabled)
|
||||||
private val isRenderReadReceiptsEnabled = MutableStateFlow(isRenderReadReceiptsEnabled)
|
private val isRenderReadReceiptsEnabled = MutableStateFlow(isRenderReadReceiptsEnabled)
|
||||||
private val isSendTypingNotificationsEnabled = MutableStateFlow(isSendTypingNotificationsEnabled)
|
private val isSendTypingNotificationsEnabled = MutableStateFlow(isSendTypingNotificationsEnabled)
|
||||||
private val isRenderTypingNotificationsEnabled = MutableStateFlow(isRenderTypingNotificationsEnabled)
|
private val isRenderTypingNotificationsEnabled = MutableStateFlow(isRenderTypingNotificationsEnabled)
|
||||||
|
private val isSessionVerificationSkipped = MutableStateFlow(isSessionVerificationSkipped)
|
||||||
var clearCallCount = 0
|
var clearCallCount = 0
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
|
@ -65,6 +67,14 @@ class InMemorySessionPreferencesStore(
|
||||||
|
|
||||||
override fun isRenderTypingNotificationsEnabled(): Flow<Boolean> = isRenderTypingNotificationsEnabled
|
override fun isRenderTypingNotificationsEnabled(): Flow<Boolean> = isRenderTypingNotificationsEnabled
|
||||||
|
|
||||||
|
override suspend fun setSkipSessionVerification(skip: Boolean) {
|
||||||
|
isSessionVerificationSkipped.tryEmit(skip)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isSessionVerificationSkipped(): Flow<Boolean> {
|
||||||
|
return isSessionVerificationSkipped
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun clear() {
|
override suspend fun clear() {
|
||||||
clearCallCount++
|
clearCallCount++
|
||||||
isSendPublicReadReceiptsEnabled.tryEmit(true)
|
isSendPublicReadReceiptsEnabled.tryEmit(true)
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,4 @@ data class SessionData(
|
||||||
val loginType: LoginType,
|
val loginType: LoginType,
|
||||||
/** The optional passphrase used to encrypt data in the SDK local store. */
|
/** The optional passphrase used to encrypt data in the SDK local store. */
|
||||||
val passphrase: String?,
|
val passphrase: String?,
|
||||||
/** Whether the session needs verification. */
|
|
||||||
val needsVerification: Boolean,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ internal fun SessionData.toDbModel(): DbSessionData {
|
||||||
isTokenValid = if (isTokenValid) 1L else 0L,
|
isTokenValid = if (isTokenValid) 1L else 0L,
|
||||||
loginType = loginType.name,
|
loginType = loginType.name,
|
||||||
passphrase = passphrase,
|
passphrase = passphrase,
|
||||||
needsVerification = if (needsVerification) 1L else 0L,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -51,6 +50,5 @@ internal fun DbSessionData.toApiModel(): SessionData {
|
||||||
isTokenValid = isTokenValid == 1L,
|
isTokenValid = isTokenValid == 1L,
|
||||||
loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name),
|
loginType = LoginType.fromName(loginType ?: LoginType.UNKNOWN.name),
|
||||||
passphrase = passphrase,
|
passphrase = passphrase,
|
||||||
needsVerification = needsVerification == 1L,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -23,9 +23,7 @@ CREATE TABLE SessionData (
|
||||||
isTokenValid INTEGER NOT NULL DEFAULT 1,
|
isTokenValid INTEGER NOT NULL DEFAULT 1,
|
||||||
loginType TEXT,
|
loginType TEXT,
|
||||||
-- added in version 5
|
-- added in version 5
|
||||||
passphrase TEXT,
|
passphrase TEXT
|
||||||
-- added in version 6
|
|
||||||
needsVerification INTEGER NOT NULL DEFAULT 0
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
-- Migrate DB from version 6
|
||||||
|
-- Remove DB value for verified status, we're back to using the Rust SDK as a source of truth
|
||||||
|
|
||||||
|
CREATE TABLE SessionData_bak (
|
||||||
|
userId TEXT NOT NULL PRIMARY KEY,
|
||||||
|
deviceId TEXT NOT NULL,
|
||||||
|
accessToken TEXT NOT NULL,
|
||||||
|
refreshToken TEXT,
|
||||||
|
homeserverUrl TEXT NOT NULL,
|
||||||
|
slidingSyncProxy TEXT,
|
||||||
|
loginTimestamp INTEGER,
|
||||||
|
oidcData TEXT,
|
||||||
|
isTokenValid INTEGER NOT NULL DEFAULT 1,
|
||||||
|
loginType TEXT,
|
||||||
|
passphrase TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO SessionData_bak SELECT userId, deviceId, accessToken, refreshToken, homeserverUrl, slidingSyncProxy, loginTimestamp, oidcData, isTokenValid, loginType, passphrase FROM SessionData;
|
||||||
|
DROP TABLE SessionData;
|
||||||
|
ALTER TABLE SessionData_bak RENAME TO SessionData;
|
||||||
|
|
@ -144,7 +144,6 @@ class DatabaseSessionStoreTests {
|
||||||
isTokenValid = 1,
|
isTokenValid = 1,
|
||||||
loginType = null,
|
loginType = null,
|
||||||
passphrase = "aPassphrase",
|
passphrase = "aPassphrase",
|
||||||
needsVerification = 1L,
|
|
||||||
)
|
)
|
||||||
val secondSessionData = SessionData(
|
val secondSessionData = SessionData(
|
||||||
userId = "userId",
|
userId = "userId",
|
||||||
|
|
@ -158,7 +157,6 @@ class DatabaseSessionStoreTests {
|
||||||
isTokenValid = 1,
|
isTokenValid = 1,
|
||||||
loginType = null,
|
loginType = null,
|
||||||
passphrase = "aPassphraseAltered",
|
passphrase = "aPassphraseAltered",
|
||||||
needsVerification = 0L,
|
|
||||||
)
|
)
|
||||||
assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId)
|
assertThat(firstSessionData.userId).isEqualTo(secondSessionData.userId)
|
||||||
assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp)
|
assertThat(firstSessionData.loginTimestamp).isNotEqualTo(secondSessionData.loginTimestamp)
|
||||||
|
|
@ -179,7 +177,6 @@ class DatabaseSessionStoreTests {
|
||||||
assertThat(alteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp)
|
assertThat(alteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp)
|
||||||
assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData)
|
assertThat(alteredSession.oidcData).isEqualTo(secondSessionData.oidcData)
|
||||||
assertThat(alteredSession.passphrase).isEqualTo(secondSessionData.passphrase)
|
assertThat(alteredSession.passphrase).isEqualTo(secondSessionData.passphrase)
|
||||||
assertThat(alteredSession.needsVerification).isEqualTo(secondSessionData.needsVerification)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -196,7 +193,6 @@ class DatabaseSessionStoreTests {
|
||||||
isTokenValid = 1,
|
isTokenValid = 1,
|
||||||
loginType = null,
|
loginType = null,
|
||||||
passphrase = "aPassphrase",
|
passphrase = "aPassphrase",
|
||||||
needsVerification = 1L,
|
|
||||||
)
|
)
|
||||||
val secondSessionData = SessionData(
|
val secondSessionData = SessionData(
|
||||||
userId = "userIdUnknown",
|
userId = "userIdUnknown",
|
||||||
|
|
@ -210,7 +206,6 @@ class DatabaseSessionStoreTests {
|
||||||
isTokenValid = 1,
|
isTokenValid = 1,
|
||||||
loginType = null,
|
loginType = null,
|
||||||
passphrase = "aPassphraseAltered",
|
passphrase = "aPassphraseAltered",
|
||||||
needsVerification = 0L,
|
|
||||||
)
|
)
|
||||||
assertThat(firstSessionData.userId).isNotEqualTo(secondSessionData.userId)
|
assertThat(firstSessionData.userId).isNotEqualTo(secondSessionData.userId)
|
||||||
|
|
||||||
|
|
@ -229,6 +224,5 @@ class DatabaseSessionStoreTests {
|
||||||
assertThat(notAlteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp)
|
assertThat(notAlteredSession.loginTimestamp).isEqualTo(firstSessionData.loginTimestamp)
|
||||||
assertThat(notAlteredSession.oidcData).isEqualTo(firstSessionData.oidcData)
|
assertThat(notAlteredSession.oidcData).isEqualTo(firstSessionData.oidcData)
|
||||||
assertThat(notAlteredSession.passphrase).isEqualTo(firstSessionData.passphrase)
|
assertThat(notAlteredSession.passphrase).isEqualTo(firstSessionData.passphrase)
|
||||||
assertThat(notAlteredSession.needsVerification).isEqualTo(firstSessionData.needsVerification)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,5 +31,4 @@ internal fun aSessionData() = SessionData(
|
||||||
isTokenValid = 1,
|
isTokenValid = 1,
|
||||||
loginType = LoginType.UNKNOWN.name,
|
loginType = LoginType.UNKNOWN.name,
|
||||||
passphrase = null,
|
passphrase = null,
|
||||||
needsVerification = 0L,
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue