Merge pull request #8620 from vector-im/feature/bma/oidcSessionEnd

Feature/bma/OIDC session end
This commit is contained in:
Benoit Marty 2023-09-12 16:25:46 +02:00 committed by GitHub
commit ec9a066900
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 148 additions and 20 deletions

1
changelog.d/8616.misc Normal file
View File

@ -0,0 +1 @@
If an external account manager is configured on the server, use it to delete other sessions and hide the multi session deletion.

View File

@ -85,6 +85,11 @@ data class HomeServerCapabilities(
* External account management url for use with MSC3824 delegated OIDC, provided in Wellknown. * External account management url for use with MSC3824 delegated OIDC, provided in Wellknown.
*/ */
val externalAccountManagementUrl: String? = null, val externalAccountManagementUrl: String? = null,
/**
* Authentication issuer for use with MSC3824 delegated OIDC, provided in Wellknown.
*/
val authenticationIssuer: String? = null,
) { ) {
enum class RoomCapabilitySupport { enum class RoomCapabilitySupport {
@ -141,6 +146,8 @@ data class HomeServerCapabilities(
return cap?.preferred ?: cap?.support?.lastOrNull() return cap?.preferred ?: cap?.support?.lastOrNull()
} }
val delegatedOidcAuthEnabled: Boolean = authenticationIssuer != null
companion object { companion object {
const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L const val MAX_UPLOAD_FILE_SIZE_UNKNOWN = -1L
const val ROOM_CAP_KNOCK = "knock" const val ROOM_CAP_KNOCK = "knock"

View File

@ -298,7 +298,7 @@ internal class DefaultAuthenticationService @Inject constructor(
} }
// If an m.login.sso flow is present that is flagged as being for MSC3824 OIDC compatibility then we only return that flow // If an m.login.sso flow is present that is flagged as being for MSC3824 OIDC compatibility then we only return that flow
val oidcCompatibilityFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.sso" && it.delegatedOidcCompatibilty == true } val oidcCompatibilityFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.sso" && it.delegatedOidcCompatibility == true }
val flows = if (oidcCompatibilityFlow != null) listOf(oidcCompatibilityFlow) else loginFlowResponse.flows val flows = if (oidcCompatibilityFlow != null) listOf(oidcCompatibilityFlow) else loginFlowResponse.flows
val supportsGetLoginTokenFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.token" && it.getLoginToken == true } != null val supportsGetLoginTokenFlow = loginFlowResponse.flows.orEmpty().firstOrNull { it.type == "m.login.token" && it.getLoginToken == true } != null

View File

@ -51,7 +51,7 @@ internal data class LoginFlow(
* See [MSC3824](https://github.com/matrix-org/matrix-spec-proposals/pull/3824) * See [MSC3824](https://github.com/matrix-org/matrix-spec-proposals/pull/3824)
*/ */
@Json(name = "org.matrix.msc3824.delegated_oidc_compatibility") @Json(name = "org.matrix.msc3824.delegated_oidc_compatibility")
val delegatedOidcCompatibilty: Boolean? = null, val delegatedOidcCompatibility: Boolean? = null,
/** /**
* Whether a login flow of type m.login.token could accept a token issued using /login/get_token. * Whether a login flow of type m.login.token could accept a token issued using /login/get_token.

View File

@ -69,6 +69,7 @@ import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo049
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo050 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo050
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo051 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo051
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo052 import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo052
import org.matrix.android.sdk.internal.database.migration.MigrateSessionTo053
import org.matrix.android.sdk.internal.util.Normalizer import org.matrix.android.sdk.internal.util.Normalizer
import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration import org.matrix.android.sdk.internal.util.database.MatrixRealmMigration
import javax.inject.Inject import javax.inject.Inject
@ -77,7 +78,7 @@ internal class RealmSessionStoreMigration @Inject constructor(
private val normalizer: Normalizer private val normalizer: Normalizer
) : MatrixRealmMigration( ) : MatrixRealmMigration(
dbName = "Session", dbName = "Session",
schemaVersion = 52L, schemaVersion = 53L,
) { ) {
/** /**
* Forces all RealmSessionStoreMigration instances to be equal. * Forces all RealmSessionStoreMigration instances to be equal.
@ -139,5 +140,6 @@ internal class RealmSessionStoreMigration @Inject constructor(
if (oldVersion < 50) MigrateSessionTo050(realm).perform() if (oldVersion < 50) MigrateSessionTo050(realm).perform()
if (oldVersion < 51) MigrateSessionTo051(realm).perform() if (oldVersion < 51) MigrateSessionTo051(realm).perform()
if (oldVersion < 52) MigrateSessionTo052(realm).perform() if (oldVersion < 52) MigrateSessionTo052(realm).perform()
if (oldVersion < 53) MigrateSessionTo053(realm).perform()
} }
} }

View File

@ -49,6 +49,7 @@ internal object HomeServerCapabilitiesMapper {
canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices, canRemotelyTogglePushNotificationsOfDevices = entity.canRemotelyTogglePushNotificationsOfDevices,
canRedactRelatedEvents = entity.canRedactEventWithRelations, canRedactRelatedEvents = entity.canRedactEventWithRelations,
externalAccountManagementUrl = entity.externalAccountManagementUrl, externalAccountManagementUrl = entity.externalAccountManagementUrl,
authenticationIssuer = entity.authenticationIssuer,
) )
} }

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) 2023 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 MigrateSessionTo053(realm: DynamicRealm) : RealmMigrator(realm, 53) {
override fun doMigrate(realm: DynamicRealm) {
realm.schema.get("HomeServerCapabilitiesEntity")
?.addField(HomeServerCapabilitiesEntityFields.AUTHENTICATION_ISSUER, String::class.java)
?.forceRefreshOfHomeServerCapabilities()
}
}

View File

@ -36,6 +36,7 @@ internal open class HomeServerCapabilitiesEntity(
var canRemotelyTogglePushNotificationsOfDevices: Boolean = false, var canRemotelyTogglePushNotificationsOfDevices: Boolean = false,
var canRedactEventWithRelations: Boolean = false, var canRedactEventWithRelations: Boolean = false,
var externalAccountManagementUrl: String? = null, var externalAccountManagementUrl: String? = null,
var authenticationIssuer: String? = null,
) : RealmObject() { ) : RealmObject() {
companion object companion object

View File

@ -165,6 +165,7 @@ internal class DefaultGetHomeServerCapabilitiesTask @Inject constructor(
Timber.v("Extracted integration config : $config") Timber.v("Extracted integration config : $config")
realm.insertOrUpdate(config) realm.insertOrUpdate(config)
} }
homeServerCapabilitiesEntity.authenticationIssuer = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.issuer
homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl homeServerCapabilitiesEntity.externalAccountManagementUrl = getWellknownResult.wellKnown.unstableDelegatedAuthConfig?.accountManagementUrl
} }

View File

@ -50,4 +50,6 @@ sealed class DevicesViewEvents : VectorViewEvents {
data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvents() data class ShowManuallyVerify(val cryptoDeviceInfo: CryptoDeviceInfo) : DevicesViewEvents()
object PromptResetSecrets : DevicesViewEvents() object PromptResetSecrets : DevicesViewEvents()
data class OpenBrowser(val url: String) : DevicesViewEvents()
} }

View File

@ -346,6 +346,20 @@ class DevicesViewModel @AssistedInject constructor(
private fun handleDelete(action: DevicesAction.Delete) { private fun handleDelete(action: DevicesAction.Delete) {
val deviceId = action.deviceId val deviceId = action.deviceId
val accountManagementUrl = session.homeServerCapabilitiesService().getHomeServerCapabilities().externalAccountManagementUrl
if (accountManagementUrl != null) {
// Open external browser to delete this session
_viewEvents.post(
DevicesViewEvents.OpenBrowser(
url = accountManagementUrl.removeSuffix("/") + "?action=session_end&device_id=$deviceId"
)
)
} else {
doDelete(deviceId)
}
}
private fun doDelete(deviceId: String) {
setState { setState {
copy( copy(
request = Loading() request = Loading()

View File

@ -35,6 +35,7 @@ import im.vector.app.core.extensions.cleanup
import im.vector.app.core.extensions.configureWith import im.vector.app.core.extensions.configureWith
import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.databinding.DialogBaseEditTextBinding import im.vector.app.databinding.DialogBaseEditTextBinding
import im.vector.app.databinding.FragmentGenericRecyclerBinding import im.vector.app.databinding.FragmentGenericRecyclerBinding
import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.auth.ReAuthActivity
@ -95,6 +96,9 @@ class VectorSettingsDevicesFragment :
is DevicesViewEvents.PromptResetSecrets -> { is DevicesViewEvents.PromptResetSecrets -> {
navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET) navigator.open4SSetup(requireActivity(), SetupMode.PASSPHRASE_AND_NEEDED_SECRETS_RESET)
} }
is DevicesViewEvents.OpenBrowser -> {
openUrlInChromeCustomTab(requireContext(), null, it.url)
}
} }
} }
} }

View File

@ -35,12 +35,13 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
import timber.log.Timber import timber.log.Timber
class DevicesViewModel @AssistedInject constructor( class DevicesViewModel @AssistedInject constructor(
@Assisted initialState: DevicesViewState, @Assisted initialState: DevicesViewState,
activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase, private val getCurrentSessionCrossSigningInfoUseCase: GetCurrentSessionCrossSigningInfoUseCase,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase, private val refreshDevicesOnCryptoDevicesChangeUseCase: RefreshDevicesOnCryptoDevicesChangeUseCase,
@ -69,6 +70,19 @@ class DevicesViewModel @AssistedInject constructor(
refreshDeviceList() refreshDeviceList()
refreshIpAddressVisibility() refreshIpAddressVisibility()
observePreferences() observePreferences()
initDelegatedOidcAuthEnabled()
}
private fun initDelegatedOidcAuthEnabled() {
setState {
copy(
delegatedOidcAuthEnabled = activeSessionHolder.getSafeActiveSession()
?.homeServerCapabilitiesService()
?.getHomeServerCapabilities()
?.delegatedOidcAuthEnabled
.orFalse()
)
}
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {

View File

@ -26,6 +26,7 @@ data class DevicesViewState(
val devices: Async<DeviceFullInfoList> = Uninitialized, val devices: Async<DeviceFullInfoList> = Uninitialized,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val isShowingIpAddress: Boolean = false, val isShowingIpAddress: Boolean = false,
val delegatedOidcAuthEnabled: Boolean = false,
) : MavericksState ) : MavericksState
data class DeviceFullInfoList( data class DeviceFullInfoList(

View File

@ -290,8 +290,8 @@ class VectorSettingsDevicesFragment :
val unverifiedSessionsCount = deviceFullInfoList?.unverifiedSessionsCount ?: 0 val unverifiedSessionsCount = deviceFullInfoList?.unverifiedSessionsCount ?: 0
renderSecurityRecommendations(inactiveSessionsCount, unverifiedSessionsCount) renderSecurityRecommendations(inactiveSessionsCount, unverifiedSessionsCount)
renderCurrentSessionView(currentDeviceInfo, hasOtherDevices = otherDevices?.isNotEmpty().orFalse()) renderCurrentSessionView(currentDeviceInfo, hasOtherDevices = otherDevices?.isNotEmpty().orFalse(), state)
renderOtherSessionsView(otherDevices, state.isShowingIpAddress) renderOtherSessionsView(otherDevices, state)
} else { } else {
hideSecurityRecommendations() hideSecurityRecommendations()
hideCurrentSessionView() hideCurrentSessionView()
@ -347,13 +347,16 @@ class VectorSettingsDevicesFragment :
hideInactiveSessionsRecommendation() hideInactiveSessionsRecommendation()
} }
private fun renderOtherSessionsView(otherDevices: List<DeviceFullInfo>?, isShowingIpAddress: Boolean) { private fun renderOtherSessionsView(otherDevices: List<DeviceFullInfo>?, state: DevicesViewState) {
val isShowingIpAddress = state.isShowingIpAddress
if (otherDevices.isNullOrEmpty()) { if (otherDevices.isNullOrEmpty()) {
hideOtherSessionsView() hideOtherSessionsView()
} else { } else {
views.deviceListHeaderOtherSessions.isVisible = true views.deviceListHeaderOtherSessions.isVisible = true
val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError) val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError)
val multiSignoutItem = views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout) val multiSignoutItem = views.deviceListHeaderOtherSessions.menu.findItem(R.id.otherSessionsHeaderMultiSignout)
// Hide multi signout if the homeserver delegates the account management
multiSignoutItem.isVisible = state.delegatedOidcAuthEnabled.not()
val nbDevices = otherDevices.size val nbDevices = otherDevices.size
multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices) multiSignoutItem.title = stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices)
multiSignoutItem.setTextColor(colorDestructive) multiSignoutItem.setTextColor(colorDestructive)
@ -377,23 +380,24 @@ class VectorSettingsDevicesFragment :
views.deviceListOtherSessions.isVisible = false views.deviceListOtherSessions.isVisible = false
} }
private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?, hasOtherDevices: Boolean) { private fun renderCurrentSessionView(currentDeviceInfo: DeviceFullInfo?, hasOtherDevices: Boolean, state: DevicesViewState) {
currentDeviceInfo?.let { currentDeviceInfo?.let {
renderCurrentSessionHeaderView(hasOtherDevices) renderCurrentSessionHeaderView(hasOtherDevices, state)
renderCurrentSessionListView(it) renderCurrentSessionListView(it)
} ?: run { } ?: run {
hideCurrentSessionView() hideCurrentSessionView()
} }
} }
private fun renderCurrentSessionHeaderView(hasOtherDevices: Boolean) { private fun renderCurrentSessionHeaderView(hasOtherDevices: Boolean, state: DevicesViewState) {
views.deviceListHeaderCurrentSession.isVisible = true views.deviceListHeaderCurrentSession.isVisible = true
val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError) val colorDestructive = colorProvider.getColorFromAttribute(R.attr.colorError)
val signoutSessionItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignout) val signoutSessionItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignout)
signoutSessionItem.setTextColor(colorDestructive) signoutSessionItem.setTextColor(colorDestructive)
val signoutOtherSessionsItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignoutOtherSessions) val signoutOtherSessionsItem = views.deviceListHeaderCurrentSession.menu.findItem(R.id.currentSessionHeaderSignoutOtherSessions)
signoutOtherSessionsItem.setTextColor(colorDestructive) signoutOtherSessionsItem.setTextColor(colorDestructive)
signoutOtherSessionsItem.isVisible = hasOtherDevices // Hide signout other sessions if the homeserver delegates the account management
signoutOtherSessionsItem.isVisible = hasOtherDevices && state.delegatedOidcAuthEnabled.not()
} }
private fun renderCurrentSessionListView(currentDeviceInfo: DeviceFullInfo) { private fun renderCurrentSessionListView(currentDeviceInfo: DeviceFullInfo) {

View File

@ -103,10 +103,15 @@ class OtherSessionsFragment :
val nbDevices = viewState.devices()?.size ?: 0 val nbDevices = viewState.devices()?.size ?: 0
stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices) stringProvider.getQuantityString(R.plurals.device_manager_other_sessions_multi_signout_all, nbDevices, nbDevices)
} }
multiSignoutItem.isVisible = if (viewState.isSelectModeEnabled) { multiSignoutItem.isVisible = if (viewState.delegatedOidcAuthEnabled) {
viewState.devices.invoke()?.any { it.isSelected }.orFalse() // Hide multi signout if the homeserver delegates the account management
false
} else { } else {
viewState.devices.invoke()?.isNotEmpty().orFalse() if (viewState.isSelectModeEnabled) {
viewState.devices.invoke()?.any { it.isSelected }.orFalse()
} else {
viewState.devices.invoke()?.isNotEmpty().orFalse()
}
} }
val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER val showAsActionFlag = if (viewState.isSelectModeEnabled) MenuItem.SHOW_AS_ACTION_IF_ROOM else MenuItem.SHOW_AS_ACTION_NEVER
multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT) multiSignoutItem.setShowAsAction(showAsActionFlag or MenuItem.SHOW_AS_ACTION_WITH_TEXT)
@ -308,7 +313,10 @@ class OtherSessionsFragment :
) )
) )
views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_inactive_sessions_found) views.otherSessionsNotFoundTextView.text = getString(R.string.device_manager_other_sessions_no_inactive_sessions_found)
updateSecurityLearnMoreButton(R.string.device_manager_learn_more_sessions_inactive_title, R.string.device_manager_learn_more_sessions_inactive) updateSecurityLearnMoreButton(
R.string.device_manager_learn_more_sessions_inactive_title,
R.string.device_manager_learn_more_sessions_inactive
)
} }
DeviceManagerFilterType.ALL_SESSIONS -> { /* NOOP. View is not visible */ DeviceManagerFilterType.ALL_SESSIONS -> { /* NOOP. View is not visible */
} }

View File

@ -36,12 +36,13 @@ import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsReAuthN
import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase import im.vector.app.features.settings.devices.v2.signout.SignoutSessionsUseCase
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth import org.matrix.android.sdk.api.session.uia.DefaultBaseAuth
import timber.log.Timber import timber.log.Timber
class OtherSessionsViewModel @AssistedInject constructor( class OtherSessionsViewModel @AssistedInject constructor(
@Assisted private val initialState: OtherSessionsViewState, @Assisted private val initialState: OtherSessionsViewState,
activeSessionHolder: ActiveSessionHolder, private val activeSessionHolder: ActiveSessionHolder,
private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase, private val getDeviceFullInfoListUseCase: GetDeviceFullInfoListUseCase,
private val signoutSessionsUseCase: SignoutSessionsUseCase, private val signoutSessionsUseCase: SignoutSessionsUseCase,
private val pendingAuthHandler: PendingAuthHandler, private val pendingAuthHandler: PendingAuthHandler,
@ -65,6 +66,19 @@ class OtherSessionsViewModel @AssistedInject constructor(
observeDevices(initialState.currentFilter) observeDevices(initialState.currentFilter)
refreshIpAddressVisibility() refreshIpAddressVisibility()
observePreferences() observePreferences()
initDelegatedOidcAuthEnabled()
}
private fun initDelegatedOidcAuthEnabled() {
setState {
copy(
delegatedOidcAuthEnabled = activeSessionHolder.getSafeActiveSession()
?.homeServerCapabilitiesService()
?.getHomeServerCapabilities()
?.delegatedOidcAuthEnabled
.orFalse()
)
}
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {

View File

@ -29,6 +29,7 @@ data class OtherSessionsViewState(
val isSelectModeEnabled: Boolean = false, val isSelectModeEnabled: Boolean = false,
val isLoading: Boolean = false, val isLoading: Boolean = false,
val isShowingIpAddress: Boolean = false, val isShowingIpAddress: Boolean = false,
val delegatedOidcAuthEnabled: Boolean = false,
) : MavericksState { ) : MavericksState {
constructor(args: OtherSessionsArgs) : this(excludeCurrentDevice = args.excludeCurrentDevice) constructor(args: OtherSessionsArgs) : this(excludeCurrentDevice = args.excludeCurrentDevice)

View File

@ -39,6 +39,7 @@ import im.vector.app.core.platform.VectorMenuProvider
import im.vector.app.core.resources.ColorProvider import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.resources.DrawableProvider import im.vector.app.core.resources.DrawableProvider
import im.vector.app.core.resources.StringProvider import im.vector.app.core.resources.StringProvider
import im.vector.app.core.utils.openUrlInChromeCustomTab
import im.vector.app.databinding.FragmentSessionOverviewBinding import im.vector.app.databinding.FragmentSessionOverviewBinding
import im.vector.app.features.auth.ReAuthActivity import im.vector.app.features.auth.ReAuthActivity
import im.vector.app.features.crypto.recover.SetupMode import im.vector.app.features.crypto.recover.SetupMode
@ -135,10 +136,19 @@ class SessionOverviewFragment :
activity?.let { SignOutUiWorker(it).perform() } activity?.let { SignOutUiWorker(it).perform() }
} }
private fun confirmSignoutOtherSession() { private fun confirmSignoutOtherSession() = withState(viewModel) { state ->
activity?.let { if (state.externalAccountManagementUrl != null) {
buildConfirmSignoutDialogUseCase.execute(it, this::signoutSession) // Manage in external account manager
.show() openUrlInChromeCustomTab(
requireContext(),
null,
state.externalAccountManagementUrl.removeSuffix("/") + "?action=session_end&device_id=${state.deviceId}"
)
} else {
activity?.let {
buildConfirmSignoutDialogUseCase.execute(it, this::signoutSession)
.show()
}
} }
} }

View File

@ -75,6 +75,18 @@ class SessionOverviewViewModel @AssistedInject constructor(
observeNotificationsStatus(initialState.deviceId) observeNotificationsStatus(initialState.deviceId)
refreshIpAddressVisibility() refreshIpAddressVisibility()
observePreferences() observePreferences()
initExternalAccountManagementUrl()
}
private fun initExternalAccountManagementUrl() {
setState {
copy(
externalAccountManagementUrl = activeSessionHolder.getSafeActiveSession()
?.homeServerCapabilitiesService()
?.getHomeServerCapabilities()
?.externalAccountManagementUrl
)
}
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {

View File

@ -29,6 +29,7 @@ data class SessionOverviewViewState(
val isLoading: Boolean = false, val isLoading: Boolean = false,
val notificationsStatus: NotificationsStatus = NotificationsStatus.NOT_SUPPORTED, val notificationsStatus: NotificationsStatus = NotificationsStatus.NOT_SUPPORTED,
val isShowingIpAddress: Boolean = false, val isShowingIpAddress: Boolean = false,
val externalAccountManagementUrl: String? = null,
) : MavericksState { ) : MavericksState {
constructor(args: SessionOverviewArgs) : this( constructor(args: SessionOverviewArgs) : this(
deviceId = args.deviceId deviceId = args.deviceId