Upgrade SDK version to 25.02.26 (#4305)
* Upgrade SDK version to 25.02.26 * Remove OIDC URL result from logout, the SDK no longer provides it * Handle room creation and destruction in a better way * Remove `onSuccessLogout`
This commit is contained in:
committed by
GitHub
parent
3c30bec1c2
commit
274d9dc7c1
@@ -9,9 +9,7 @@ package io.element.android.appnav.room.joined
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.bumble.appyx.core.lifecycle.subscribe
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
@@ -56,7 +54,7 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||
roomComponentFactory: RoomComponentFactory,
|
||||
) : BaseFlowNode<JoinedRoomLoadedFlowNode.NavTarget>(
|
||||
backstack = BackStack(
|
||||
initialElement = when (val input = plugins.filterIsInstance(Inputs::class.java).first().initialElement) {
|
||||
initialElement = when (val input = plugins.filterIsInstance<Inputs>().first().initialElement) {
|
||||
is RoomNavigationTarget.Messages -> NavTarget.Messages(input.focusedEventId)
|
||||
RoomNavigationTarget.Details -> NavTarget.RoomDetails
|
||||
RoomNavigationTarget.NotificationSettings -> NavTarget.RoomNotificationSettings
|
||||
@@ -197,16 +195,6 @@ class JoinedRoomLoadedFlowNode @AssistedInject constructor(
|
||||
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
// Rely on the View Lifecycle in addition to the Node Lifecycle,
|
||||
// because this node enters 'onDestroy' before his children, so it can leads to
|
||||
// using the room in a child node where it's already closed.
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
|
||||
inputs.room.destroy()
|
||||
}
|
||||
}
|
||||
}
|
||||
BackstackView()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,10 +521,9 @@ class LoggedInPresenterTest {
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
@Test
|
||||
fun `present - LogoutAndMigrateToNativeSlidingSync logs out the user`() = runTest {
|
||||
val logoutLambda = lambdaRecorder<Boolean, Boolean, String?> { userInitiated, ignoreSdkError ->
|
||||
val logoutLambda = lambdaRecorder<Boolean, Boolean, Unit> { userInitiated, ignoreSdkError ->
|
||||
assertThat(userInitiated).isTrue()
|
||||
assertThat(ignoreSdkError).isTrue()
|
||||
null
|
||||
}
|
||||
val matrixClient = FakeMatrixClient().apply {
|
||||
this.logoutLambda = logoutLambda
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
package io.element.android.features.lockscreen.impl.unlock
|
||||
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
@@ -18,8 +17,6 @@ import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.logout.api.util.onSuccessLogout
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@@ -41,8 +38,6 @@ class PinUnlockNode @AssistedInject constructor(
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
val isDark = ElementTheme.isLightTheme.not()
|
||||
LaunchedEffect(state.isUnlocked) {
|
||||
if (state.isUnlocked) {
|
||||
onUnlock()
|
||||
@@ -53,7 +48,6 @@ class PinUnlockNode @AssistedInject constructor(
|
||||
// UnlockNode is only used for in-app unlock, so we can safely set isInAppUnlock to true.
|
||||
// It's set to false in PinUnlockActivity.
|
||||
isInAppUnlock = true,
|
||||
onSuccessLogout = { onSuccessLogout(activity, isDark, it) },
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ class PinUnlockPresenter @Inject constructor(
|
||||
mutableStateOf(false)
|
||||
}
|
||||
val signOutAction = remember {
|
||||
mutableStateOf<AsyncAction<String?>>(AsyncAction.Uninitialized)
|
||||
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||
}
|
||||
var biometricUnlockResult by remember {
|
||||
mutableStateOf<BiometricAuthenticator.AuthenticationResult?>(null)
|
||||
@@ -169,7 +169,7 @@ class PinUnlockPresenter @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncAction<String?>>) = launch {
|
||||
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncAction<Unit>>) = launch {
|
||||
suspend {
|
||||
logoutUseCase.logout(ignoreSdkError = true)
|
||||
}.runCatchingUpdatingState(signOutAction)
|
||||
|
||||
@@ -18,7 +18,7 @@ data class PinUnlockState(
|
||||
val showWrongPinTitle: Boolean,
|
||||
val remainingAttempts: AsyncData<Int>,
|
||||
val showSignOutPrompt: Boolean,
|
||||
val signOutAction: AsyncAction<String?>,
|
||||
val signOutAction: AsyncAction<Unit>,
|
||||
val showBiometricUnlock: Boolean,
|
||||
val isUnlocked: Boolean,
|
||||
val biometricUnlockResult: BiometricAuthenticator.AuthenticationResult?,
|
||||
|
||||
@@ -41,7 +41,7 @@ fun aPinUnlockState(
|
||||
showBiometricUnlock: Boolean = true,
|
||||
biometricUnlockResult: BiometricAuthenticator.AuthenticationResult? = null,
|
||||
isUnlocked: Boolean = false,
|
||||
signOutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
|
||||
signOutAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
) = PinUnlockState(
|
||||
pinEntry = AsyncData.Success(pinEntry),
|
||||
showWrongPinTitle = showWrongPinTitle,
|
||||
|
||||
@@ -29,9 +29,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
@@ -67,7 +65,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
fun PinUnlockView(
|
||||
state: PinUnlockState,
|
||||
isInAppUnlock: Boolean,
|
||||
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
OnLifecycleEvent { _, event ->
|
||||
@@ -89,12 +86,7 @@ fun PinUnlockView(
|
||||
AsyncAction.Loading -> {
|
||||
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
|
||||
}
|
||||
is AsyncAction.Success -> {
|
||||
val latestOnSuccessLogout by rememberUpdatedState(onSuccessLogout)
|
||||
LaunchedEffect(state) {
|
||||
latestOnSuccessLogout(state.signOutAction.data)
|
||||
}
|
||||
}
|
||||
is AsyncAction.Success,
|
||||
is AsyncAction.Confirming,
|
||||
is AsyncAction.Failure,
|
||||
AsyncAction.Uninitialized -> Unit
|
||||
@@ -369,7 +361,6 @@ internal fun PinUnlockViewInAppPreview(@PreviewParameter(PinUnlockStateProvider:
|
||||
PinUnlockView(
|
||||
state = state,
|
||||
isInAppUnlock = true,
|
||||
onSuccessLogout = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -381,7 +372,6 @@ internal fun PinUnlockViewPreview(@PreviewParameter(PinUnlockStateProvider::clas
|
||||
PinUnlockView(
|
||||
state = state,
|
||||
isInAppUnlock = false,
|
||||
onSuccessLogout = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,14 +15,12 @@ import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.enterprise.api.EnterpriseService
|
||||
import io.element.android.features.lockscreen.api.LockScreenLockState
|
||||
import io.element.android.features.lockscreen.api.LockScreenService
|
||||
import io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter
|
||||
import io.element.android.features.lockscreen.impl.unlock.PinUnlockView
|
||||
import io.element.android.features.lockscreen.impl.unlock.di.PinUnlockBindings
|
||||
import io.element.android.features.logout.api.util.onSuccessLogout
|
||||
import io.element.android.libraries.architecture.bindings
|
||||
import io.element.android.libraries.designsystem.theme.ElementThemeApp
|
||||
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
|
||||
@@ -51,11 +49,9 @@ class PinUnlockActivity : AppCompatActivity() {
|
||||
enterpriseService = enterpriseService,
|
||||
) {
|
||||
val state = presenter.present()
|
||||
val isDark = ElementTheme.isLightTheme.not()
|
||||
PinUnlockView(
|
||||
state = state,
|
||||
isInAppUnlock = false,
|
||||
onSuccessLogout = { onSuccessLogout(this, isDark, it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ class PinUnlockPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - forgot pin flow`() = runTest {
|
||||
val signOutLambda = lambdaRecorder<Boolean, String?> { "" }
|
||||
val signOutLambda = lambdaRecorder<Boolean, Unit> {}
|
||||
val signOut = FakeLogoutUseCase(signOutLambda)
|
||||
val presenter = createPinUnlockPresenter(this, logoutUseCase = signOut)
|
||||
moleculeFlow(RecompositionMode.Immediate) {
|
||||
|
||||
@@ -14,10 +14,8 @@ interface LogoutUseCase {
|
||||
/**
|
||||
* Log out the current user and then perform any needed cleanup tasks.
|
||||
* @param ignoreSdkError if true, the SDK error will be ignored and the user will be logged out anyway.
|
||||
* @return an optional URL. When the URL is there, it should be presented to the user after logout for
|
||||
* Relying Party (RP) initiated logout on their account page.
|
||||
*/
|
||||
suspend fun logout(ignoreSdkError: Boolean): String?
|
||||
suspend fun logout(ignoreSdkError: Boolean)
|
||||
|
||||
interface Factory {
|
||||
fun create(sessionId: String): LogoutUseCase
|
||||
|
||||
@@ -11,6 +11,6 @@ import io.element.android.libraries.architecture.AsyncAction
|
||||
|
||||
data class DirectLogoutState(
|
||||
val canDoDirectSignOut: Boolean,
|
||||
val logoutAction: AsyncAction<String?>,
|
||||
val logoutAction: AsyncAction<Unit>,
|
||||
val eventSink: (DirectLogoutEvents) -> Unit,
|
||||
)
|
||||
|
||||
@@ -17,13 +17,13 @@ open class DirectLogoutStateProvider : PreviewParameterProvider<DirectLogoutStat
|
||||
aDirectLogoutState(logoutAction = AsyncAction.ConfirmingNoParams),
|
||||
aDirectLogoutState(logoutAction = AsyncAction.Loading),
|
||||
aDirectLogoutState(logoutAction = AsyncAction.Failure(Exception("Error"))),
|
||||
aDirectLogoutState(logoutAction = AsyncAction.Success("success")),
|
||||
aDirectLogoutState(logoutAction = AsyncAction.Success(Unit)),
|
||||
)
|
||||
}
|
||||
|
||||
fun aDirectLogoutState(
|
||||
canDoDirectSignOut: Boolean = true,
|
||||
logoutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
|
||||
logoutAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (DirectLogoutEvents) -> Unit = {},
|
||||
) = DirectLogoutState(
|
||||
canDoDirectSignOut = canDoDirectSignOut,
|
||||
|
||||
@@ -11,8 +11,5 @@ import androidx.compose.runtime.Composable
|
||||
|
||||
interface DirectLogoutView {
|
||||
@Composable
|
||||
fun Render(
|
||||
state: DirectLogoutState,
|
||||
onSuccessLogout: (logoutUrlResult: String?) -> Unit
|
||||
)
|
||||
fun Render(state: DirectLogoutState)
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.logout.api.util
|
||||
|
||||
import android.app.Activity
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import timber.log.Timber
|
||||
|
||||
fun onSuccessLogout(
|
||||
activity: Activity,
|
||||
darkTheme: Boolean,
|
||||
url: String?,
|
||||
) {
|
||||
Timber.d("Success logout with result url: $url")
|
||||
url?.let {
|
||||
activity.openUrlInChromeCustomTab(null, darkTheme, it)
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,9 @@ class DefaultLogoutUseCase @Inject constructor(
|
||||
private val authenticationService: MatrixAuthenticationService,
|
||||
private val matrixClientProvider: MatrixClientProvider,
|
||||
) : LogoutUseCase {
|
||||
override suspend fun logout(ignoreSdkError: Boolean): String? {
|
||||
override suspend fun logout(ignoreSdkError: Boolean) {
|
||||
val currentSession = authenticationService.getLatestSessionId()
|
||||
return if (currentSession != null) {
|
||||
if (currentSession != null) {
|
||||
matrixClientProvider.getOrRestore(currentSession)
|
||||
.getOrThrow()
|
||||
.logout(userInitiated = true, ignoreSdkError = true)
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
package io.element.android.features.logout.impl
|
||||
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.bumble.appyx.core.modality.BuildContext
|
||||
@@ -17,9 +16,7 @@ import com.bumble.appyx.core.plugin.plugins
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.logout.api.LogoutEntryPoint
|
||||
import io.element.android.features.logout.api.util.onSuccessLogout
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
|
||||
@ContributesNode(SessionScope::class)
|
||||
@@ -35,14 +32,9 @@ class LogoutNode @AssistedInject constructor(
|
||||
@Composable
|
||||
override fun View(modifier: Modifier) {
|
||||
val state = presenter.present()
|
||||
val activity = requireNotNull(LocalActivity.current)
|
||||
val isDark = ElementTheme.isLightTheme.not()
|
||||
LogoutView(
|
||||
state = state,
|
||||
onChangeRecoveryKeyClick = ::onChangeRecoveryKeyClick,
|
||||
onSuccessLogout = {
|
||||
onSuccessLogout(activity, isDark, it)
|
||||
},
|
||||
onBackClick = ::navigateUp,
|
||||
modifier = modifier,
|
||||
)
|
||||
|
||||
@@ -35,7 +35,7 @@ class LogoutPresenter @Inject constructor(
|
||||
@Composable
|
||||
override fun present(): LogoutState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
val logoutAction: MutableState<AsyncAction<String?>> = remember {
|
||||
val logoutAction: MutableState<AsyncAction<Unit>> = remember {
|
||||
mutableStateOf(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ class LogoutPresenter @Inject constructor(
|
||||
}
|
||||
|
||||
private fun CoroutineScope.logout(
|
||||
logoutAction: MutableState<AsyncAction<String?>>,
|
||||
logoutAction: MutableState<AsyncAction<Unit>>,
|
||||
ignoreSdkError: Boolean,
|
||||
) = launch {
|
||||
suspend {
|
||||
|
||||
@@ -18,6 +18,6 @@ data class LogoutState(
|
||||
val doesBackupExistOnServer: Boolean,
|
||||
val recoveryState: RecoveryState,
|
||||
val backupUploadState: BackupUploadState,
|
||||
val logoutAction: AsyncAction<String?>,
|
||||
val logoutAction: AsyncAction<Unit>,
|
||||
val eventSink: (LogoutEvents) -> Unit,
|
||||
)
|
||||
|
||||
@@ -38,7 +38,7 @@ fun aLogoutState(
|
||||
doesBackupExistOnServer: Boolean = true,
|
||||
recoveryState: RecoveryState = RecoveryState.ENABLED,
|
||||
backupUploadState: BackupUploadState = BackupUploadState.Unknown,
|
||||
logoutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
|
||||
logoutAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
eventSink: (LogoutEvents) -> Unit = {},
|
||||
) = LogoutState(
|
||||
isLastDevice = isLastDevice,
|
||||
|
||||
@@ -45,7 +45,6 @@ fun LogoutView(
|
||||
state: LogoutState,
|
||||
onChangeRecoveryKeyClick: () -> Unit,
|
||||
onBackClick: () -> Unit,
|
||||
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val eventSink = state.eventSink
|
||||
@@ -80,9 +79,6 @@ fun LogoutView(
|
||||
onDismissDialog = {
|
||||
eventSink(LogoutEvents.CloseDialogs)
|
||||
},
|
||||
onSuccessLogout = {
|
||||
onSuccessLogout(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -177,7 +173,6 @@ internal fun LogoutViewPreview(
|
||||
LogoutView(
|
||||
state,
|
||||
onChangeRecoveryKeyClick = {},
|
||||
onSuccessLogout = {},
|
||||
onBackClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,10 +23,7 @@ import javax.inject.Inject
|
||||
@ContributesBinding(SessionScope::class)
|
||||
class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView {
|
||||
@Composable
|
||||
override fun Render(
|
||||
state: DirectLogoutState,
|
||||
onSuccessLogout: (logoutUrlResult: String?) -> Unit,
|
||||
) {
|
||||
override fun Render(state: DirectLogoutState) {
|
||||
val eventSink = state.eventSink
|
||||
LogoutActionDialog(
|
||||
state.logoutAction,
|
||||
@@ -39,9 +36,6 @@ class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView {
|
||||
onDismissDialog = {
|
||||
eventSink(DirectLogoutEvents.CloseDialogs)
|
||||
},
|
||||
onSuccessLogout = {
|
||||
onSuccessLogout(it)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -51,8 +45,5 @@ class DefaultDirectLogoutView @Inject constructor() : DirectLogoutView {
|
||||
internal fun DefaultDirectLogoutViewPreview(
|
||||
@PreviewParameter(DirectLogoutStateProvider::class) state: DirectLogoutState,
|
||||
) = ElementPreview {
|
||||
DefaultDirectLogoutView().Render(
|
||||
state = state,
|
||||
onSuccessLogout = {},
|
||||
)
|
||||
DefaultDirectLogoutView().Render(state = state)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ class DirectLogoutPresenter @Inject constructor(
|
||||
override fun present(): DirectLogoutState {
|
||||
val localCoroutineScope = rememberCoroutineScope()
|
||||
|
||||
val logoutAction: MutableState<AsyncAction<String?>> = remember {
|
||||
val logoutAction: MutableState<AsyncAction<Unit>> = remember {
|
||||
mutableStateOf(AsyncAction.Uninitialized)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class DirectLogoutPresenter @Inject constructor(
|
||||
}
|
||||
|
||||
private fun CoroutineScope.logout(
|
||||
logoutAction: MutableState<AsyncAction<String?>>,
|
||||
logoutAction: MutableState<AsyncAction<Unit>>,
|
||||
ignoreSdkError: Boolean,
|
||||
) = launch {
|
||||
suspend {
|
||||
|
||||
@@ -8,9 +8,7 @@
|
||||
package io.element.android.features.logout.impl.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import io.element.android.features.logout.impl.R
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
@@ -20,11 +18,10 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||
|
||||
@Composable
|
||||
fun LogoutActionDialog(
|
||||
state: AsyncAction<String?>,
|
||||
state: AsyncAction<Unit>,
|
||||
onConfirmClick: () -> Unit,
|
||||
onForceLogoutClick: () -> Unit,
|
||||
onDismissDialog: () -> Unit,
|
||||
onSuccessLogout: (String?) -> Unit,
|
||||
) {
|
||||
when (state) {
|
||||
AsyncAction.Uninitialized ->
|
||||
@@ -44,11 +41,6 @@ fun LogoutActionDialog(
|
||||
onRetry = onForceLogoutClick,
|
||||
onDismiss = onDismissDialog,
|
||||
)
|
||||
is AsyncAction.Success -> {
|
||||
val latestOnSuccessLogout by rememberUpdatedState(onSuccessLogout)
|
||||
LaunchedEffect(state) {
|
||||
latestOnSuccessLogout(state.data)
|
||||
}
|
||||
}
|
||||
is AsyncAction.Success -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,9 @@ import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.testtags.TestTags
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.pressBack
|
||||
import io.element.android.tests.testutils.pressTag
|
||||
import org.junit.Rule
|
||||
@@ -96,21 +94,6 @@ class LogoutViewTest {
|
||||
eventsRecorder.assertSingle(LogoutEvents.CloseDialogs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `success logout invoke onSuccessLogout`() {
|
||||
val data = "data"
|
||||
val eventsRecorder = EventsRecorder<LogoutEvents>(expectEvents = false)
|
||||
ensureCalledOnceWithParam<String?>(data) { callback ->
|
||||
rule.setLogoutView(
|
||||
aLogoutState(
|
||||
logoutAction = AsyncAction.Success(data),
|
||||
eventSink = eventsRecorder
|
||||
),
|
||||
onSuccessLogout = callback,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `last session setting button invoke onChangeRecoveryKeyClicked`() {
|
||||
val eventsRecorder = EventsRecorder<LogoutEvents>(expectEvents = false)
|
||||
@@ -131,14 +114,12 @@ private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setLogou
|
||||
state: LogoutState,
|
||||
onChangeRecoveryKeyClick: () -> Unit = EnsureNeverCalled(),
|
||||
onBackClick: () -> Unit = EnsureNeverCalled(),
|
||||
onSuccessLogout: (logoutUrlResult: String?) -> Unit = EnsureNeverCalledWithParam()
|
||||
) {
|
||||
setContent {
|
||||
LogoutView(
|
||||
state = state,
|
||||
onChangeRecoveryKeyClick = onChangeRecoveryKeyClick,
|
||||
onBackClick = onBackClick,
|
||||
onSuccessLogout = onSuccessLogout,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,10 +16,8 @@ import io.element.android.features.logout.api.direct.DirectLogoutState
|
||||
import io.element.android.features.logout.api.direct.aDirectLogoutState
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.pressBackKey
|
||||
import org.junit.Ignore
|
||||
import org.junit.Rule
|
||||
@@ -96,45 +94,12 @@ class DefaultDirectLogoutViewTest {
|
||||
rule.clickOn(CommonStrings.action_cancel)
|
||||
eventsRecorder.assertSingle(DirectLogoutEvents.CloseDialogs)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `success logout invoke expected callback and sends expected Event`() {
|
||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>(expectEvents = false)
|
||||
ensureCalledOnceWithParam<String?>(null) { callback ->
|
||||
rule.setDefaultDirectLogoutView(
|
||||
state = aDirectLogoutState(
|
||||
logoutAction = AsyncAction.Success(null),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onSuccessLogout = callback
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `success logout invoke expected callback and sends expected Event with data`() {
|
||||
val eventsRecorder = EventsRecorder<DirectLogoutEvents>(expectEvents = false)
|
||||
val data = "data"
|
||||
ensureCalledOnceWithParam<String?>(data) { callback ->
|
||||
rule.setDefaultDirectLogoutView(
|
||||
state = aDirectLogoutState(
|
||||
logoutAction = AsyncAction.Success(data),
|
||||
eventSink = eventsRecorder,
|
||||
),
|
||||
onSuccessLogout = callback
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setDefaultDirectLogoutView(
|
||||
state: DirectLogoutState,
|
||||
onSuccessLogout: (String?) -> Unit = EnsureNeverCalledWithParam(),
|
||||
) {
|
||||
setContent {
|
||||
DefaultDirectLogoutView().Render(
|
||||
state,
|
||||
onSuccessLogout = onSuccessLogout,
|
||||
)
|
||||
DefaultDirectLogoutView().Render(state)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,9 @@ import io.element.android.tests.testutils.lambda.lambdaError
|
||||
import io.element.android.tests.testutils.simulateLongTask
|
||||
|
||||
class FakeLogoutUseCase(
|
||||
var logoutLambda: (Boolean) -> String? = { lambdaError() }
|
||||
var logoutLambda: (Boolean) -> Unit = { lambdaError() }
|
||||
) : LogoutUseCase {
|
||||
override suspend fun logout(ignoreSdkError: Boolean): String? = simulateLongTask {
|
||||
override suspend fun logout(ignoreSdkError: Boolean) = simulateLongTask {
|
||||
logoutLambda(ignoreSdkError)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class MatrixComposerDraftStore @Inject constructor(
|
||||
private val client: MatrixClient,
|
||||
) : ComposerDraftStore {
|
||||
override suspend fun loadDraft(roomId: RoomId): ComposerDraft? {
|
||||
return client.getRoom(roomId)?.use { room ->
|
||||
return client.getRoom(roomId)?.let { room ->
|
||||
room.loadComposerDraft()
|
||||
.onFailure {
|
||||
Timber.e(it, "Failed to load composer draft for room $roomId")
|
||||
@@ -35,7 +35,7 @@ class MatrixComposerDraftStore @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun updateDraft(roomId: RoomId, draft: ComposerDraft?) {
|
||||
client.getRoom(roomId)?.use { room ->
|
||||
client.getRoom(roomId)?.let { room ->
|
||||
val updateDraftResult = if (draft == null) {
|
||||
room.clearComposerDraft()
|
||||
} else {
|
||||
|
||||
@@ -21,7 +21,6 @@ import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutEvents
|
||||
import io.element.android.features.logout.api.direct.DirectLogoutView
|
||||
import io.element.android.features.logout.api.util.onSuccessLogout
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import io.element.android.libraries.di.SessionScope
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
@@ -140,11 +139,6 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||
onDeactivateClick = this::onOpenAccountDeactivation
|
||||
)
|
||||
|
||||
directLogoutView.Render(
|
||||
state = state.directLogoutState,
|
||||
onSuccessLogout = {
|
||||
onSuccessLogout(activity, isDark, it)
|
||||
}
|
||||
)
|
||||
directLogoutView.Render(state = state.directLogoutState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +111,6 @@ class RoomListNode @AssistedInject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
directLogoutView.Render(state.directLogoutState) {}
|
||||
directLogoutView.Render(state.directLogoutState)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import dagger.assisted.AssistedInject
|
||||
import io.element.android.anvilannotations.ContributesNode
|
||||
import io.element.android.appconfig.LearnMoreConfig
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.features.logout.api.util.onSuccessLogout
|
||||
import io.element.android.features.verifysession.api.VerifySessionEntryPoint
|
||||
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
|
||||
import io.element.android.libraries.architecture.inputs
|
||||
@@ -56,7 +55,6 @@ class VerifySelfSessionNode @AssistedInject constructor(
|
||||
onEnterRecoveryKey = callback::onEnterRecoveryKey,
|
||||
onResetKey = callback::onResetKey,
|
||||
onFinish = callback::onDone,
|
||||
onSuccessLogout = { onSuccessLogout(activity, isDark, it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
|
||||
val skipVerification by sessionPreferencesStore.isSessionVerificationSkipped().collectAsState(initial = false)
|
||||
val sessionVerifiedStatus by sessionVerificationService.sessionVerifiedStatus.collectAsState()
|
||||
val signOutAction = remember {
|
||||
mutableStateOf<AsyncAction<String?>>(AsyncAction.Uninitialized)
|
||||
mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized)
|
||||
}
|
||||
val step by remember {
|
||||
derivedStateOf {
|
||||
@@ -195,7 +195,7 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
|
||||
.launchIn(this)
|
||||
}
|
||||
|
||||
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncAction<String?>>) = launch {
|
||||
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncAction<Unit>>) = launch {
|
||||
suspend {
|
||||
logoutUseCase.logout(ignoreSdkError = true)
|
||||
}.runCatchingUpdatingState(signOutAction)
|
||||
|
||||
@@ -16,7 +16,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD
|
||||
@Immutable
|
||||
data class VerifySelfSessionState(
|
||||
val step: Step,
|
||||
val signOutAction: AsyncAction<String?>,
|
||||
val signOutAction: AsyncAction<Unit>,
|
||||
val displaySkipButton: Boolean,
|
||||
val eventSink: (VerifySelfSessionViewEvents) -> Unit,
|
||||
) {
|
||||
|
||||
@@ -65,7 +65,7 @@ open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfS
|
||||
|
||||
internal fun aVerifySelfSessionState(
|
||||
step: Step = Step.Initial(canEnterRecoveryKey = false),
|
||||
signOutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
|
||||
signOutAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||
displaySkipButton: Boolean = false,
|
||||
eventSink: (VerifySelfSessionViewEvents) -> Unit = {},
|
||||
) = VerifySelfSessionState(
|
||||
|
||||
@@ -58,7 +58,6 @@ fun VerifySelfSessionView(
|
||||
onEnterRecoveryKey: () -> Unit,
|
||||
onResetKey: () -> Unit,
|
||||
onFinish: () -> Unit,
|
||||
onSuccessLogout: (String?) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val step = state.step
|
||||
@@ -144,12 +143,7 @@ fun VerifySelfSessionView(
|
||||
AsyncAction.Loading -> {
|
||||
ProgressDialog(text = stringResource(id = R.string.screen_signout_in_progress_dialog_content))
|
||||
}
|
||||
is AsyncAction.Success -> {
|
||||
val latestOnSuccessLogout by rememberUpdatedState(onSuccessLogout)
|
||||
LaunchedEffect(state) {
|
||||
latestOnSuccessLogout(state.signOutAction.data)
|
||||
}
|
||||
}
|
||||
is AsyncAction.Success,
|
||||
is AsyncAction.Confirming,
|
||||
is AsyncAction.Failure,
|
||||
AsyncAction.Uninitialized -> Unit
|
||||
@@ -372,6 +366,5 @@ internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionSta
|
||||
onEnterRecoveryKey = {},
|
||||
onResetKey = {},
|
||||
onFinish = {},
|
||||
onSuccessLogout = {},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -314,7 +314,7 @@ class VerifySelfSessionPresenterTest {
|
||||
emitVerifiedStatus(SessionVerifiedStatus.Verified)
|
||||
emitVerificationFlowState(VerificationFlowState.DidFinish)
|
||||
}
|
||||
val signOutLambda = lambdaRecorder<Boolean, String?> { "aUrl" }
|
||||
val signOutLambda = lambdaRecorder<Boolean, Unit> {}
|
||||
val presenter = createVerifySelfSessionPresenter(
|
||||
service,
|
||||
logoutUseCase = FakeLogoutUseCase(signOutLambda)
|
||||
@@ -326,7 +326,6 @@ class VerifySelfSessionPresenterTest {
|
||||
assertThat(awaitItem().signOutAction.isLoading()).isTrue()
|
||||
val finalItem = awaitItem()
|
||||
assertThat(finalItem.signOutAction.isSuccess()).isTrue()
|
||||
assertThat(finalItem.signOutAction.dataOrNull()).isEqualTo("aUrl")
|
||||
signOutLambda.assertions().isCalledOnce().with(value(true))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import io.element.android.features.verifysession.impl.R
|
||||
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
|
||||
import io.element.android.libraries.architecture.AsyncAction
|
||||
import io.element.android.libraries.architecture.AsyncData
|
||||
import io.element.android.libraries.ui.strings.CommonStrings
|
||||
import io.element.android.tests.testutils.EnsureNeverCalled
|
||||
@@ -21,7 +20,6 @@ import io.element.android.tests.testutils.EnsureNeverCalledWithParam
|
||||
import io.element.android.tests.testutils.EventsRecorder
|
||||
import io.element.android.tests.testutils.clickOn
|
||||
import io.element.android.tests.testutils.ensureCalledOnce
|
||||
import io.element.android.tests.testutils.ensureCalledOnceWithParam
|
||||
import io.element.android.tests.testutils.pressBackKey
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
@@ -224,27 +222,12 @@ class VerifySelfSessionViewTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `on success logout - onFinished callback is called immediately`() {
|
||||
val aUrl = "aUrl"
|
||||
ensureCalledOnceWithParam<String?>(aUrl) { callback ->
|
||||
rule.setVerifySelfSessionView(
|
||||
aVerifySelfSessionState(
|
||||
signOutAction = AsyncAction.Success(aUrl),
|
||||
eventSink = EnsureNeverCalledWithParam(),
|
||||
),
|
||||
onSuccessLogout = callback,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setVerifySelfSessionView(
|
||||
state: VerifySelfSessionState,
|
||||
onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
|
||||
onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(),
|
||||
onFinished: () -> Unit = EnsureNeverCalled(),
|
||||
onResetKey: () -> Unit = EnsureNeverCalled(),
|
||||
onSuccessLogout: (String?) -> Unit = EnsureNeverCalledWithParam(),
|
||||
) {
|
||||
setContent {
|
||||
VerifySelfSessionView(
|
||||
@@ -253,7 +236,6 @@ class VerifySelfSessionViewTest {
|
||||
onEnterRecoveryKey = onEnterRecoveryKey,
|
||||
onFinish = onFinished,
|
||||
onResetKey = onResetKey,
|
||||
onSuccessLogout = onSuccessLogout,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ jsoup = "org.jsoup:jsoup:1.18.3"
|
||||
appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" }
|
||||
molecule-runtime = "app.cash.molecule:molecule-runtime:2.0.0"
|
||||
timber = "com.jakewharton.timber:timber:5.0.1"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.2.17"
|
||||
matrix_sdk = "org.matrix.rustcomponents:sdk-android:25.2.26"
|
||||
matrix_richtexteditor = { module = "io.element.android:wysiwyg", version.ref = "wysiwyg" }
|
||||
matrix_richtexteditor_compose = { module = "io.element.android:wysiwyg-compose", version.ref = "wysiwyg" }
|
||||
sqldelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" }
|
||||
|
||||
@@ -84,12 +84,11 @@ interface MatrixClient : Closeable {
|
||||
|
||||
/**
|
||||
* Logout the user.
|
||||
* Returns an optional URL. When the URL is there, it should be presented to the user after logout for
|
||||
* Relying Party (RP) initiated logout on their account page.
|
||||
*
|
||||
* @param userInitiated if false, the logout came from the HS, no request will be made and the session entry will be kept in the store.
|
||||
* @param ignoreSdkError if true, the SDK will ignore any error and delete the session data anyway.
|
||||
*/
|
||||
suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean): String?
|
||||
suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean)
|
||||
|
||||
/**
|
||||
* Retrieve the user profile, will also eventually emit a new value to [userProfile].
|
||||
|
||||
@@ -493,8 +493,7 @@ class RustMatrixClient(
|
||||
deleteSessionDirectory(deleteCryptoDb = false)
|
||||
}
|
||||
|
||||
override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean): String? {
|
||||
var result: String? = null
|
||||
override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean) {
|
||||
sessionCoroutineScope.cancel()
|
||||
// Remove current delegate so we don't receive an auth error
|
||||
clientDelegateTaskHandle?.cancelAndDestroy()
|
||||
@@ -502,7 +501,7 @@ class RustMatrixClient(
|
||||
withContext(sessionDispatcher) {
|
||||
if (userInitiated) {
|
||||
try {
|
||||
result = innerClient.logout()
|
||||
innerClient.logout()
|
||||
} catch (failure: Throwable) {
|
||||
if (ignoreSdkError) {
|
||||
Timber.e(failure, "Fail to call logout on HS. Still delete local files.")
|
||||
@@ -521,7 +520,6 @@ class RustMatrixClient(
|
||||
sessionStore.removeSession(sessionId.value)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun canDeactivateAccount(): Boolean {
|
||||
|
||||
@@ -122,9 +122,7 @@ class FakeMatrixClient(
|
||||
var getRoomSummaryFlowLambda = { _: RoomIdOrAlias ->
|
||||
flowOf<Optional<RoomSummary>>(Optional.empty())
|
||||
}
|
||||
var logoutLambda: (Boolean, Boolean) -> String? = { _, _ ->
|
||||
null
|
||||
}
|
||||
var logoutLambda: (Boolean, Boolean) -> Unit = { _, _ -> }
|
||||
|
||||
override suspend fun getRoom(roomId: RoomId): MatrixRoom? {
|
||||
return getRoomResults[roomId]
|
||||
@@ -174,8 +172,8 @@ class FakeMatrixClient(
|
||||
clearCacheLambda()
|
||||
}
|
||||
|
||||
override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean): String? = simulateLongTask {
|
||||
return logoutLambda(ignoreSdkError, userInitiated)
|
||||
override suspend fun logout(userInitiated: Boolean, ignoreSdkError: Boolean) = simulateLongTask {
|
||||
logoutLambda(ignoreSdkError, userInitiated)
|
||||
}
|
||||
|
||||
override fun canDeactivateAccount() = canDeactivateAccountResult()
|
||||
|
||||
Reference in New Issue
Block a user