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:
Jorge Martin Espinosa
2025-02-26 10:04:49 +01:00
committed by GitHub
parent 3c30bec1c2
commit 274d9dc7c1
40 changed files with 46 additions and 230 deletions

View File

@@ -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()
}
}

View File

@@ -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

View File

@@ -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
)
}

View File

@@ -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)

View File

@@ -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?,

View File

@@ -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,

View File

@@ -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 = {},
)
}
}

View File

@@ -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) },
)
}
}

View File

@@ -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) {

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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,

View File

@@ -11,8 +11,5 @@ import androidx.compose.runtime.Composable
interface DirectLogoutView {
@Composable
fun Render(
state: DirectLogoutState,
onSuccessLogout: (logoutUrlResult: String?) -> Unit
)
fun Render(state: DirectLogoutState)
}

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -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 {

View File

@@ -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,
)

View File

@@ -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,

View File

@@ -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 = {},
)
}

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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
}
}

View File

@@ -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,
)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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 {

View File

@@ -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)
}
}

View File

@@ -111,6 +111,6 @@ class RoomListNode @AssistedInject constructor(
)
}
directLogoutView.Render(state.directLogoutState) {}
directLogoutView.Render(state.directLogoutState)
}
}

View File

@@ -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) },
)
}
}

View File

@@ -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)

View File

@@ -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,
) {

View File

@@ -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(

View File

@@ -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 = {},
)
}

View File

@@ -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))
}
}

View File

@@ -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,
)
}
}

View File

@@ -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" }

View File

@@ -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].

View File

@@ -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 {

View File

@@ -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()