diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index 63e1c3f9a8..737b5248ec 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -18,14 +18,17 @@ package io.element.android.appnav.loggedin import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import im.vector.app.features.analytics.plan.CryptoSessionStateChange import im.vector.app.features.analytics.plan.UserProperties import io.element.android.features.networkmonitor.api.NetworkMonitor import io.element.android.features.networkmonitor.api.NetworkStatus +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.MatrixClient @@ -55,13 +58,16 @@ class LoggedInPresenter @Inject constructor( val isVerified by remember { sessionVerificationService.sessionVerifiedStatus.map { it == SessionVerifiedStatus.Verified } }.collectAsState(initial = false) - + val pusherRegistrationState = remember>> { mutableStateOf(AsyncData.Uninitialized) } if (isVerified) { LaunchedEffect(Unit) { - ensurePusherIsRegistered() + ensurePusherIsRegistered(pusherRegistrationState) + } + } else { + LaunchedEffect(Unit) { + pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.AccountNotVerified()) } } - val syncIndicator by matrixClient.roomListService.syncIndicator.collectAsState() val networkStatus by networkMonitor.connectivity.collectAsState() val showSyncSpinner by remember { @@ -77,25 +83,32 @@ class LoggedInPresenter @Inject constructor( return LoggedInState( showSyncSpinner = showSyncSpinner, + pusherRegistrationState = pusherRegistrationState.value, ) } - private suspend fun ensurePusherIsRegistered() { + private suspend fun ensurePusherIsRegistered(pusherRegistrationState: MutableState>) { Timber.tag(pusherTag.value).d("Ensure pusher is registered") val currentPushProvider = pushService.getCurrentPushProvider() val result = if (currentPushProvider == null) { Timber.tag(pusherTag.value).d("Register with the first available push provider") val pushProvider = pushService.getAvailablePushProviders().firstOrNull() - ?: return Unit.also { Timber.tag(pusherTag.value).w("No push provider available") } + ?: return Unit + .also { Timber.tag(pusherTag.value).w("No push providers available") } + .also { pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.NoProvidersAvailable()) } val distributor = pushProvider.getDistributors().firstOrNull() - ?: return Unit.also { Timber.tag(pusherTag.value).w("No distributor available") } + ?: return Unit + .also { Timber.tag(pusherTag.value).w("No distributors available") } + .also { pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable()) } pushService.registerWith(matrixClient, pushProvider, distributor) } else { val currentPushDistributor = currentPushProvider.getCurrentDistributor(matrixClient) if (currentPushDistributor == null) { Timber.tag(pusherTag.value).d("Register with the first available distributor") val distributor = currentPushProvider.getDistributors().firstOrNull() - ?: return Unit.also { Timber.tag(pusherTag.value).w("No distributor available") } + ?: return Unit + .also { Timber.tag(pusherTag.value).w("No distributors available") } + .also { pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable()) } pushService.registerWith(matrixClient, currentPushProvider, distributor) } else { Timber.tag(pusherTag.value).d("Re-register with the current distributor") @@ -105,9 +118,11 @@ class LoggedInPresenter @Inject constructor( result.fold( onSuccess = { Timber.tag(pusherTag.value).d("Pusher registered") + pusherRegistrationState.value = AsyncData.Success(Unit) }, onFailure = { Timber.tag(pusherTag.value).e(it, "Failed to register pusher") + pusherRegistrationState.value = AsyncData.Failure(PusherRegistrationFailure.RegistrationFailure(it)) } ) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt index 4196277698..2e3db40c9a 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt @@ -16,6 +16,9 @@ package io.element.android.appnav.loggedin +import io.element.android.libraries.architecture.AsyncData + data class LoggedInState( val showSyncSpinner: Boolean, + val pusherRegistrationState: AsyncData, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt index 0e8fdef8d8..89f71f384a 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt @@ -17,6 +17,7 @@ package io.element.android.appnav.loggedin import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.AsyncData open class LoggedInStateProvider : PreviewParameterProvider { override val values: Sequence @@ -31,4 +32,5 @@ fun aLoggedInState( showSyncSpinner: Boolean = true, ) = LoggedInState( showSyncSpinner = showSyncSpinner, + pusherRegistrationState = AsyncData.Uninitialized, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/PusherRegistrationFailure.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/PusherRegistrationFailure.kt new file mode 100644 index 0000000000..791043c6e2 --- /dev/null +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/PusherRegistrationFailure.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2024 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.appnav.loggedin + +sealed class PusherRegistrationFailure : Exception() { + class AccountNotVerified : PusherRegistrationFailure() + class NoProvidersAvailable : PusherRegistrationFailure() + class NoDistributorsAvailable : PusherRegistrationFailure() + class RegistrationFailure(val failure: Throwable) : PusherRegistrationFailure() +} diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt index 8d4248df27..27f44eaeef 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -47,13 +47,10 @@ import io.element.android.tests.testutils.consumeItemsUntilPredicate import io.element.android.tests.testutils.lambda.any import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test -@OptIn(ExperimentalCoroutinesApi::class) class LoggedInPresenterTest { @get:Rule val warmUpRule = WarmUpRule() @@ -66,6 +63,8 @@ class LoggedInPresenterTest { }.test { val initialState = awaitItem() assertThat(initialState.showSyncSpinner).isFalse() + assertThat(initialState.pusherRegistrationState.isUninitialized()).isTrue() + skipItems(1) } } @@ -106,7 +105,7 @@ class LoggedInPresenterTest { encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE) verificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified) - skipItems(4) + skipItems(6) assertThat(analyticsService.capturedEvents.size).isEqualTo(1) assertThat(analyticsService.capturedEvents[0]).isInstanceOf(CryptoSessionStateChange::class.java) @@ -133,6 +132,9 @@ class LoggedInPresenterTest { presenter.present() }.test { skipItems(1) + val finalState = awaitItem() + assertThat(finalState.pusherRegistrationState.errorOrNull()) + .isInstanceOf(PusherRegistrationFailure.AccountNotVerified::class.java) lambda.assertions() .isNeverCalled() } @@ -156,7 +158,8 @@ class LoggedInPresenterTest { presenter.present() }.test { skipItems(2) - advanceUntilIdle() + val finalState = awaitItem() + assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue() lambda.assertions() .isCalledOnce() .with( @@ -188,7 +191,8 @@ class LoggedInPresenterTest { presenter.present() }.test { skipItems(2) - advanceUntilIdle() + val finalState = awaitItem() + assertThat(finalState.pusherRegistrationState.isFailure()).isTrue() lambda.assertions() .isCalledOnce() .with( @@ -233,7 +237,8 @@ class LoggedInPresenterTest { presenter.present() }.test { skipItems(2) - advanceUntilIdle() + val finalState = awaitItem() + assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue() lambda.assertions() .isCalledOnce() .with( @@ -277,7 +282,8 @@ class LoggedInPresenterTest { presenter.present() }.test { skipItems(2) - advanceUntilIdle() + val finalState = awaitItem() + assertThat(finalState.pusherRegistrationState.isSuccess()).isTrue() lambda.assertions() .isCalledOnce() .with( @@ -317,7 +323,9 @@ class LoggedInPresenterTest { presenter.present() }.test { skipItems(2) - advanceUntilIdle() + val finalState = awaitItem() + assertThat(finalState.pusherRegistrationState.errorOrNull()) + .isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java) lambda.assertions() .isNeverCalled() } @@ -343,7 +351,9 @@ class LoggedInPresenterTest { presenter.present() }.test { skipItems(2) - advanceUntilIdle() + val finalState = awaitItem() + assertThat(finalState.pusherRegistrationState.errorOrNull()) + .isInstanceOf(PusherRegistrationFailure.NoProvidersAvailable::class.java) lambda.assertions() .isNeverCalled() } @@ -374,7 +384,9 @@ class LoggedInPresenterTest { presenter.present() }.test { skipItems(2) - advanceUntilIdle() + val finalState = awaitItem() + assertThat(finalState.pusherRegistrationState.errorOrNull()) + .isInstanceOf(PusherRegistrationFailure.NoDistributorsAvailable::class.java) lambda.assertions() .isNeverCalled() }