Merge pull request #6207 from vector-im/feature/adm/sdk-signout-all-devices

SDK - Logout all devices
This commit is contained in:
Adam Brown 2022-06-27 08:30:04 +01:00 committed by GitHub
commit ccb4f2d1dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 115 additions and 30 deletions

1
changelog.d/6191.sdk Normal file
View File

@ -0,0 +1 @@
Add support for MSC2457 - opting in or out of logging out all devices when changing password

View File

@ -21,5 +21,6 @@ data class LoginFlowResult(
val ssoIdentityProviders: List<SsoIdentityProvider>?, val ssoIdentityProviders: List<SsoIdentityProvider>?,
val isLoginAndRegistrationSupported: Boolean, val isLoginAndRegistrationSupported: Boolean,
val homeServerUrl: String, val homeServerUrl: String,
val isOutdatedHomeserver: Boolean val isOutdatedHomeserver: Boolean,
val isLogoutDevicesSupported: Boolean
) )

View File

@ -72,7 +72,9 @@ interface LoginWizard {
* Confirm the new password, once the user has checked their email * Confirm the new password, once the user has checked their email
* When this method succeed, tha account password will be effectively modified. * When this method succeed, tha account password will be effectively modified.
* *
* @param newPassword the desired new password * @param newPassword the desired new password.
* @param logoutAllDevices defaults to true, all devices will be logged out. False values will only be taken into account
* if [org.matrix.android.sdk.api.auth.data.LoginFlowResult.isLogoutDevicesSupported] is true.
*/ */
suspend fun resetPasswordMailConfirmed(newPassword: String) suspend fun resetPasswordMailConfirmed(newPassword: String, logoutAllDevices: Boolean = true)
} }

View File

@ -24,13 +24,13 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
interface AccountService { interface AccountService {
/** /**
* Ask the homeserver to change the password. * Ask the homeserver to change the password.
*
* @param password Current password. * @param password Current password.
* @param newPassword New password * @param newPassword New password
* @param logoutAllDevices defaults to true, all devices will be logged out. False values will only be taken into account
* if [org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities.canControlLogoutDevices] is true.
*/ */
suspend fun changePassword( suspend fun changePassword(password: String, newPassword: String, logoutAllDevices: Boolean = true)
password: String,
newPassword: String
)
/** /**
* Deactivate the account. * Deactivate the account.

View File

@ -54,7 +54,12 @@ data class HomeServerCapabilities(
/** /**
* True if the home server support threading. * True if the home server support threading.
*/ */
val canUseThreading: Boolean = false val canUseThreading: Boolean = false,
/**
* True if the home server supports controlling the logout of all devices when changing password.
*/
val canControlLogoutDevices: Boolean = false
) { ) {
enum class RoomCapabilitySupport { enum class RoomCapabilitySupport {

View File

@ -40,6 +40,7 @@ import org.matrix.android.sdk.internal.auth.login.DefaultLoginWizard
import org.matrix.android.sdk.internal.auth.login.DirectLoginTask import org.matrix.android.sdk.internal.auth.login.DirectLoginTask
import org.matrix.android.sdk.internal.auth.registration.DefaultRegistrationWizard import org.matrix.android.sdk.internal.auth.registration.DefaultRegistrationWizard
import org.matrix.android.sdk.internal.auth.version.Versions import org.matrix.android.sdk.internal.auth.version.Versions
import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk import org.matrix.android.sdk.internal.auth.version.isSupportedBySdk
import org.matrix.android.sdk.internal.di.Unauthenticated import org.matrix.android.sdk.internal.di.Unauthenticated
@ -292,7 +293,8 @@ internal class DefaultAuthenticationService @Inject constructor(
ssoIdentityProviders = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider, ssoIdentityProviders = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == LoginFlowTypes.SSO }?.ssoIdentityProvider,
isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(), isLoginAndRegistrationSupported = versions.isLoginAndRegistrationSupportedBySdk(),
homeServerUrl = homeServerUrl, homeServerUrl = homeServerUrl,
isOutdatedHomeserver = !versions.isSupportedBySdk() isOutdatedHomeserver = !versions.isSupportedBySdk(),
isLogoutDevicesSupported = versions.doesServerSupportLogoutDevices()
) )
} }

View File

@ -121,12 +121,13 @@ internal class DefaultLoginWizard(
.also { pendingSessionStore.savePendingSessionData(it) } .also { pendingSessionStore.savePendingSessionData(it) }
} }
override suspend fun resetPasswordMailConfirmed(newPassword: String) { override suspend fun resetPasswordMailConfirmed(newPassword: String, logoutAllDevices: Boolean) {
val resetPasswordData = pendingSessionData.resetPasswordData ?: throw IllegalStateException("Developer error - Must call resetPassword first") val resetPasswordData = pendingSessionData.resetPasswordData ?: throw IllegalStateException("Developer error - Must call resetPassword first")
val param = ResetPasswordMailConfirmed.create( val param = ResetPasswordMailConfirmed.create(
pendingSessionData.clientSecret, pendingSessionData.clientSecret,
resetPasswordData.addThreePidRegistrationResponse.sid, resetPasswordData.addThreePidRegistrationResponse.sid,
newPassword newPassword,
logoutAllDevices
) )
executeRequest(null) { executeRequest(null) {

View File

@ -30,13 +30,17 @@ internal data class ResetPasswordMailConfirmed(
// the new password // the new password
@Json(name = "new_password") @Json(name = "new_password")
val newPassword: String? = null val newPassword: String? = null,
@Json(name = "logout_devices")
val logoutDevices: Boolean? = null
) { ) {
companion object { companion object {
fun create(clientSecret: String, sid: String, newPassword: String): ResetPasswordMailConfirmed { fun create(clientSecret: String, sid: String, newPassword: String, logoutDevices: Boolean?): ResetPasswordMailConfirmed {
return ResetPasswordMailConfirmed( return ResetPasswordMailConfirmed(
auth = AuthParams.createForResetPassword(clientSecret, sid), auth = AuthParams.createForResetPassword(clientSecret, sid),
newPassword = newPassword newPassword = newPassword,
logoutDevices = logoutDevices
) )
} }
} }

View File

@ -58,6 +58,7 @@ internal data class HomeServerVersion(
val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0) val r0_4_0 = HomeServerVersion(major = 0, minor = 4, patch = 0)
val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0) val r0_5_0 = HomeServerVersion(major = 0, minor = 5, patch = 0)
val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0) val r0_6_0 = HomeServerVersion(major = 0, minor = 6, patch = 0)
val r0_6_1 = HomeServerVersion(major = 0, minor = 6, patch = 1)
val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0) val v1_3_0 = HomeServerVersion(major = 1, minor = 3, patch = 0)
} }
} }

View File

@ -111,6 +111,15 @@ private fun Versions.doesServerSeparatesAddAndBind(): Boolean {
unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false unstableFeatures?.get(FEATURE_SEPARATE_ADD_AND_BIND) ?: false
} }
/**
* Indicate if the server supports MSC2457 `logout_devices` parameter when setting a new password.
*
* @return true if logout_devices is supported
*/
internal fun Versions.doesServerSupportLogoutDevices(): Boolean {
return getMaxVersion() >= HomeServerVersion.r0_6_1
}
private fun Versions.getMaxVersion(): HomeServerVersion { private fun Versions.getMaxVersion(): HomeServerVersion {
return supportedVersions return supportedVersions
?.mapNotNull { HomeServerVersion.parse(it) } ?.mapNotNull { HomeServerVersion.parse(it) }

View File

@ -48,6 +48,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo027
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo028 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo028
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo029
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo030 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo030
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo031
import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.Normalizer
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -62,7 +63,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
override fun equals(other: Any?) = other is RealmSessionStoreMigration override fun equals(other: Any?) = other is RealmSessionStoreMigration
override fun hashCode() = 1000 override fun hashCode() = 1000
val schemaVersion = 30L val schemaVersion = 31L
override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) { override fun migrate(realm: DynamicRealm, oldVersion: Long, newVersion: Long) {
Timber.d("Migrating Realm Session from $oldVersion to $newVersion") Timber.d("Migrating Realm Session from $oldVersion to $newVersion")
@ -97,5 +98,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 28) MigrateSessionTo028(realm).perform() if (oldVersion < 28) MigrateSessionTo028(realm).perform()
if (oldVersion < 29) MigrateSessionTo029(realm).perform() if (oldVersion < 29) MigrateSessionTo029(realm).perform()
if (oldVersion < 30) MigrateSessionTo030(realm).perform() if (oldVersion < 30) MigrateSessionTo030(realm).perform()
if (oldVersion < 31) MigrateSessionTo031(realm).perform()
} }
} }

View File

@ -42,7 +42,8 @@ internal object HomeServerCapabilitiesMapper {
lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported, lastVersionIdentityServerSupported = entity.lastVersionIdentityServerSupported,
defaultIdentityServerUrl = entity.defaultIdentityServerUrl, defaultIdentityServerUrl = entity.defaultIdentityServerUrl,
roomVersions = mapRoomVersion(entity.roomVersionsJson), roomVersions = mapRoomVersion(entity.roomVersionsJson),
canUseThreading = entity.canUseThreading canUseThreading = entity.canUseThreading,
canControlLogoutDevices = entity.canControlLogoutDevices
) )
} }

View File

@ -25,7 +25,7 @@ import org.matrix.android.sdk.internal.util.database.RealmMigrator
* Migrating to: * Migrating to:
* Live location sharing aggregated summary: adding new field userId. * Live location sharing aggregated summary: adding new field userId.
*/ */
internal class MigrateSessionTo029(realm: DynamicRealm) : RealmMigrator(realm, 28) { internal class MigrateSessionTo029(realm: DynamicRealm) : RealmMigrator(realm, 29) {
override fun doMigrate(realm: DynamicRealm) { override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("LiveLocationShareAggregatedSummaryEntity") realm.schema.get("LiveLocationShareAggregatedSummaryEntity")

View File

@ -0,0 +1,31 @@
/*
* Copyright (c) 2022 The Matrix.org Foundation C.I.C.
*
* 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 org.matrix.android.sdk.internal.database.migration
import io.realm.DynamicRealm
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntityFields
import org.matrix.android.sdk.internal.extensions.forceRefreshOfHomeServerCapabilities
import org.matrix.android.sdk.internal.util.database.RealmMigrator
internal class MigrateSessionTo031(realm: DynamicRealm) : RealmMigrator(realm, 31) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("HomeServerCapabilitiesEntity")
?.addField(HomeServerCapabilitiesEntityFields.CAN_CONTROL_LOGOUT_DEVICES, Boolean::class.java)
?.forceRefreshOfHomeServerCapabilities()
}
}

View File

@ -29,7 +29,8 @@ internal open class HomeServerCapabilitiesEntity(
var lastVersionIdentityServerSupported: Boolean = false, var lastVersionIdentityServerSupported: Boolean = false,
var defaultIdentityServerUrl: String? = null, var defaultIdentityServerUrl: String? = null,
var lastUpdatedTimestamp: Long = 0L, var lastUpdatedTimestamp: Long = 0L,
var canUseThreading: Boolean = false var canUseThreading: Boolean = false,
var canControlLogoutDevices: Boolean = false
) : RealmObject() { ) : RealmObject() {
companion object companion object

View File

@ -29,13 +29,17 @@ internal data class ChangePasswordParams(
val auth: UserPasswordAuth? = null, val auth: UserPasswordAuth? = null,
@Json(name = "new_password") @Json(name = "new_password")
val newPassword: String? = null val newPassword: String? = null,
@Json(name = "logout_devices")
val logoutDevices: Boolean = true
) { ) {
companion object { companion object {
fun create(userId: String, oldPassword: String, newPassword: String): ChangePasswordParams { fun create(userId: String, oldPassword: String, newPassword: String, logoutAllDevices: Boolean): ChangePasswordParams {
return ChangePasswordParams( return ChangePasswordParams(
auth = UserPasswordAuth(user = userId, password = oldPassword), auth = UserPasswordAuth(user = userId, password = oldPassword),
newPassword = newPassword newPassword = newPassword,
logoutDevices = logoutAllDevices
) )
} }
} }

View File

@ -26,7 +26,8 @@ import javax.inject.Inject
internal interface ChangePasswordTask : Task<ChangePasswordTask.Params, Unit> { internal interface ChangePasswordTask : Task<ChangePasswordTask.Params, Unit> {
data class Params( data class Params(
val password: String, val password: String,
val newPassword: String val newPassword: String,
val logoutAllDevices: Boolean
) )
} }
@ -37,7 +38,7 @@ internal class DefaultChangePasswordTask @Inject constructor(
) : ChangePasswordTask { ) : ChangePasswordTask {
override suspend fun execute(params: ChangePasswordTask.Params) { override suspend fun execute(params: ChangePasswordTask.Params) {
val changePasswordParams = ChangePasswordParams.create(userId, params.password, params.newPassword) val changePasswordParams = ChangePasswordParams.create(userId, params.password, params.newPassword, params.logoutAllDevices)
try { try {
executeRequest(globalErrorReceiver) { executeRequest(globalErrorReceiver) {
accountAPI.changePassword(changePasswordParams) accountAPI.changePassword(changePasswordParams)

View File

@ -25,8 +25,8 @@ internal class DefaultAccountService @Inject constructor(
private val deactivateAccountTask: DeactivateAccountTask private val deactivateAccountTask: DeactivateAccountTask
) : AccountService { ) : AccountService {
override suspend fun changePassword(password: String, newPassword: String) { override suspend fun changePassword(password: String, newPassword: String, logoutAllDevices: Boolean) {
changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword)) changePasswordTask.execute(ChangePasswordTask.Params(password, newPassword, logoutAllDevices))
} }
override suspend fun deactivateAccount(eraseAllData: Boolean, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) { override suspend fun deactivateAccount(eraseAllData: Boolean, userInteractiveAuthInterceptor: UserInteractiveAuthInterceptor) {

View File

@ -24,6 +24,7 @@ import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.orTrue import org.matrix.android.sdk.api.extensions.orTrue
import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities import org.matrix.android.sdk.api.session.homeserver.HomeServerCapabilities
import org.matrix.android.sdk.internal.auth.version.Versions import org.matrix.android.sdk.internal.auth.version.Versions
import org.matrix.android.sdk.internal.auth.version.doesServerSupportLogoutDevices
import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads import org.matrix.android.sdk.internal.auth.version.doesServerSupportThreads
import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk import org.matrix.android.sdk.internal.auth.version.isLoginAndRegistrationSupportedBySdk
import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity import org.matrix.android.sdk.internal.database.model.HomeServerCapabilitiesEntity
@ -142,6 +143,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
if (getVersionResult != null) { if (getVersionResult != null) {
homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk() homeServerCapabilitiesEntity.lastVersionIdentityServerSupported = getVersionResult.isLoginAndRegistrationSupportedBySdk()
homeServerCapabilitiesEntity.canControlLogoutDevices = getVersionResult.doesServerSupportLogoutDevices()
} }
if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) { if (getWellknownResult != null && getWellknownResult is WellknownResult.Prompt) {

View File

@ -444,10 +444,11 @@ class OnboardingViewModel @AssistedInject constructor(
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {
runCatching { safeLoginWizard.resetPassword(action.email) }.fold( runCatching { safeLoginWizard.resetPassword(action.email) }.fold(
onSuccess = { onSuccess = {
val state = awaitState()
setState { setState {
copy( copy(
isLoading = false, isLoading = false,
resetState = ResetState(email = action.email, newPassword = action.newPassword) resetState = createResetState(action, state.selectedHomeserver)
) )
} }
_viewEvents.post(OnboardingViewEvents.OnResetPasswordSendThreePidDone) _viewEvents.post(OnboardingViewEvents.OnResetPasswordSendThreePidDone)
@ -460,6 +461,12 @@ class OnboardingViewModel @AssistedInject constructor(
} }
} }
private fun createResetState(action: OnboardingAction.ResetPassword, selectedHomeserverState: SelectedHomeserverState) = ResetState(
email = action.email,
newPassword = action.newPassword,
supportsLogoutAllDevices = selectedHomeserverState.isLogoutDevicesSupported
)
private fun handleResetPasswordMailConfirmed() { private fun handleResetPasswordMailConfirmed() {
setState { copy(isLoading = true) } setState { copy(isLoading = true) }
currentJob = viewModelScope.launch { currentJob = viewModelScope.launch {

View File

@ -71,6 +71,7 @@ data class SelectedHomeserverState(
val upstreamUrl: String? = null, val upstreamUrl: String? = null,
val preferredLoginMode: LoginMode = LoginMode.Unknown, val preferredLoginMode: LoginMode = LoginMode.Unknown,
val supportedLoginTypes: List<String> = emptyList(), val supportedLoginTypes: List<String> = emptyList(),
val isLogoutDevicesSupported: Boolean = false,
) : Parcelable ) : Parcelable
@Parcelize @Parcelize
@ -88,6 +89,7 @@ data class PersonalizationState(
data class ResetState( data class ResetState(
val email: String? = null, val email: String? = null,
val newPassword: String? = null, val newPassword: String? = null,
val supportsLogoutAllDevices: Boolean = false
) : Parcelable ) : Parcelable
@Parcelize @Parcelize

View File

@ -53,7 +53,8 @@ class StartAuthenticationFlowUseCase @Inject constructor(
userFacingUrl = config.homeServerUri.toString(), userFacingUrl = config.homeServerUri.toString(),
upstreamUrl = authFlow.homeServerUrl, upstreamUrl = authFlow.homeServerUrl,
preferredLoginMode = preferredLoginMode, preferredLoginMode = preferredLoginMode,
supportedLoginTypes = authFlow.supportedLoginTypes supportedLoginTypes = authFlow.supportedLoginTypes,
isLogoutDevicesSupported = authFlow.isLogoutDevicesSupported
) )
private fun matrixOrgUrl() = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash() private fun matrixOrgUrl() = stringProvider.getString(R.string.matrix_org_server_url).ensureTrailingSlash()

View File

@ -178,9 +178,10 @@ class VectorSettingsGeneralFragment @Inject constructor(
} }
} }
val homeServerCapabilities = session.homeServerCapabilitiesService().getHomeServerCapabilities()
// Password // Password
// Hide the preference if password can not be updated // Hide the preference if password can not be updated
if (session.homeServerCapabilitiesService().getHomeServerCapabilities().canChangePassword) { if (homeServerCapabilities.canChangePassword) {
mPasswordPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener { mPasswordPreference.onPreferenceClickListener = Preference.OnPreferenceClickListener {
onPasswordUpdateClick() onPasswordUpdateClick()
false false

View File

@ -65,6 +65,7 @@ private val A_DIRECT_LOGIN = OnboardingAction.AuthenticateAction.LoginDirect("@a
private const val A_HOMESERVER_URL = "https://edited-homeserver.org" private const val A_HOMESERVER_URL = "https://edited-homeserver.org"
private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance) private val A_HOMESERVER_CONFIG = HomeServerConnectionConfig(FakeUri().instance)
private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password) private val SELECTED_HOMESERVER_STATE = SelectedHomeserverState(preferredLoginMode = LoginMode.Password)
private val SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES = SelectedHomeserverState(isLogoutDevicesSupported = true)
private const val AN_EMAIL = "hello@example.com" private const val AN_EMAIL = "hello@example.com"
private const val A_PASSWORD = "a-password" private const val A_PASSWORD = "a-password"
@ -478,6 +479,7 @@ class OnboardingViewModelTest {
@Test @Test
fun `given can successfully reset password, when resetting password, then emits reset done event`() = runTest { fun `given can successfully reset password, when resetting password, then emits reset done event`() = runTest {
viewModelWith(initialState.copy(selectedHomeserver = SELECTED_HOMESERVER_STATE_SUPPORTED_LOGOUT_DEVICES))
val test = viewModel.test() val test = viewModel.test()
fakeLoginWizard.givenResetPasswordSuccess(AN_EMAIL) fakeLoginWizard.givenResetPasswordSuccess(AN_EMAIL)
fakeAuthenticationService.givenLoginWizard(fakeLoginWizard) fakeAuthenticationService.givenLoginWizard(fakeLoginWizard)
@ -488,7 +490,10 @@ class OnboardingViewModelTest {
.assertStatesChanges( .assertStatesChanges(
initialState, initialState,
{ copy(isLoading = true) }, { copy(isLoading = true) },
{ copy(isLoading = false, resetState = ResetState(AN_EMAIL, A_PASSWORD)) } {
val resetState = ResetState(AN_EMAIL, A_PASSWORD, supportsLogoutAllDevices = true)
copy(isLoading = false, resetState = resetState)
}
) )
.assertEvents(OnboardingViewEvents.OnResetPasswordSendThreePidDone) .assertEvents(OnboardingViewEvents.OnResetPasswordSendThreePidDone)
.finish() .finish()

View File

@ -128,7 +128,8 @@ class StartAuthenticationFlowUseCaseTest {
ssoIdentityProviders = SSO_IDENTITY_PROVIDERS, ssoIdentityProviders = SSO_IDENTITY_PROVIDERS,
isLoginAndRegistrationSupported = true, isLoginAndRegistrationSupported = true,
homeServerUrl = A_DECLARED_HOMESERVER_URL, homeServerUrl = A_DECLARED_HOMESERVER_URL,
isOutdatedHomeserver = false isOutdatedHomeserver = false,
isLogoutDevicesSupported = false
) )
private fun expectedResult( private fun expectedResult(