Account management with OIDC: split account and session management. #1303

This commit is contained in:
Benoit Marty 2023-09-14 16:34:58 +02:00 committed by Benoit Marty
parent aa22e731f9
commit 51e663ffdc
11 changed files with 108 additions and 18 deletions

View file

@ -29,6 +29,7 @@ import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.theme.ElementTheme
import timber.log.Timber import timber.log.Timber
@ContributesNode(SessionScope::class) @ContributesNode(SessionScope::class)
@ -67,9 +68,17 @@ class PreferencesRootNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onOpenAbout() } plugins<Callback>().forEach { it.onOpenAbout() }
} }
private fun onManageAccountClicked(activity: Activity, accountManagementUrl: String?) { private fun onManageAccountClicked(
accountManagementUrl?.let { activity: Activity,
activity.openUrlInChromeCustomTab(null, false, it) url: String?,
isDark: Boolean,
) {
url?.let {
activity.openUrlInChromeCustomTab(
null,
darkTheme = isDark,
url = it
)
} }
} }
@ -81,6 +90,7 @@ class PreferencesRootNode @AssistedInject constructor(
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
val activity = LocalContext.current as Activity val activity = LocalContext.current as Activity
val isDark = ElementTheme.isLightTheme.not()
PreferencesRootView( PreferencesRootView(
state = state, state = state,
modifier = modifier, modifier = modifier,
@ -91,7 +101,7 @@ class PreferencesRootNode @AssistedInject constructor(
onVerifyClicked = this::onVerifyClicked, onVerifyClicked = this::onVerifyClicked,
onOpenDeveloperSettings = this::onOpenDeveloperSettings, onOpenDeveloperSettings = this::onOpenDeveloperSettings,
onSuccessLogout = { onSuccessLogout(activity, it) }, onSuccessLogout = { onSuccessLogout(activity, it) },
onManageAccountClicked = { onManageAccountClicked(activity, state.accountManagementUrl) }, onManageAccountClicked = { onManageAccountClicked(activity, it, isDark) },
onOpenNotificationSettings = this::onOpenNotificationSettings onOpenNotificationSettings = this::onOpenNotificationSettings
) )
} }

View file

@ -32,6 +32,7 @@ import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsS
import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlagService
import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.api.FeatureFlags
import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.MatrixClient
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.user.MatrixUser
import io.element.android.libraries.matrix.api.user.getCurrentUser import io.element.android.libraries.matrix.api.user.getCurrentUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@ -74,9 +75,12 @@ class PreferencesRootPresenter @Inject constructor(
val accountManagementUrl: MutableState<String?> = remember { val accountManagementUrl: MutableState<String?> = remember {
mutableStateOf(null) mutableStateOf(null)
} }
val devicesManagementUrl: MutableState<String?> = remember {
mutableStateOf(null)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
initAccountManagementUrl(accountManagementUrl) initAccountManagementUrl(accountManagementUrl, devicesManagementUrl)
} }
val logoutState = logoutPresenter.present() val logoutState = logoutPresenter.present()
@ -87,6 +91,7 @@ class PreferencesRootPresenter @Inject constructor(
version = versionFormatter.get(), version = versionFormatter.get(),
showCompleteVerification = showCompleteVerification, showCompleteVerification = showCompleteVerification,
accountManagementUrl = accountManagementUrl.value, accountManagementUrl = accountManagementUrl.value,
devicesManagementUrl = devicesManagementUrl.value,
showAnalyticsSettings = hasAnalyticsProviders, showAnalyticsSettings = hasAnalyticsProviders,
showDeveloperSettings = showDeveloperSettings, showDeveloperSettings = showDeveloperSettings,
showNotificationSettings = showNotificationSettings.value, showNotificationSettings = showNotificationSettings.value,
@ -98,7 +103,11 @@ class PreferencesRootPresenter @Inject constructor(
matrixUser.value = matrixClient.getCurrentUser() matrixUser.value = matrixClient.getCurrentUser()
} }
private fun CoroutineScope.initAccountManagementUrl(accountManagementUrl: MutableState<String?>) = launch { private fun CoroutineScope.initAccountManagementUrl(
accountManagementUrl.value = matrixClient.getAccountManagementUrl().getOrNull() accountManagementUrl: MutableState<String?>,
devicesManagementUrl: MutableState<String?>,
) = launch {
accountManagementUrl.value = matrixClient.getAccountManagementUrl(AccountManagementAction.Profile).getOrNull()
devicesManagementUrl.value = matrixClient.getAccountManagementUrl(AccountManagementAction.SessionsList).getOrNull()
} }
} }

View file

@ -26,6 +26,7 @@ data class PreferencesRootState(
val version: String, val version: String,
val showCompleteVerification: Boolean, val showCompleteVerification: Boolean,
val accountManagementUrl: String?, val accountManagementUrl: String?,
val devicesManagementUrl: String?,
val showAnalyticsSettings: Boolean, val showAnalyticsSettings: Boolean,
val showDeveloperSettings: Boolean, val showDeveloperSettings: Boolean,
val showNotificationSettings: Boolean, val showNotificationSettings: Boolean,

View file

@ -26,6 +26,7 @@ fun aPreferencesRootState() = PreferencesRootState(
version = "Version 1.1 (1)", version = "Version 1.1 (1)",
showCompleteVerification = true, showCompleteVerification = true,
accountManagementUrl = "aUrl", accountManagementUrl = "aUrl",
devicesManagementUrl = "anOtherUrl",
showAnalyticsSettings = true, showAnalyticsSettings = true,
showDeveloperSettings = true, showDeveloperSettings = true,
showNotificationSettings = true, showNotificationSettings = true,

View file

@ -23,8 +23,8 @@ import androidx.compose.material.icons.outlined.BugReport
import androidx.compose.material.icons.outlined.DeveloperMode import androidx.compose.material.icons.outlined.DeveloperMode
import androidx.compose.material.icons.outlined.Help import androidx.compose.material.icons.outlined.Help
import androidx.compose.material.icons.outlined.InsertChart import androidx.compose.material.icons.outlined.InsertChart
import androidx.compose.material.icons.outlined.ManageAccounts
import androidx.compose.material.icons.outlined.Notifications import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.OpenInNew
import androidx.compose.material.icons.outlined.VerifiedUser import androidx.compose.material.icons.outlined.VerifiedUser
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -53,7 +53,7 @@ fun PreferencesRootView(
state: PreferencesRootState, state: PreferencesRootState,
onBackPressed: () -> Unit, onBackPressed: () -> Unit,
onVerifyClicked: () -> Unit, onVerifyClicked: () -> Unit,
onManageAccountClicked: () -> Unit, onManageAccountClicked: (url: String) -> Unit,
onOpenAnalytics: () -> Unit, onOpenAnalytics: () -> Unit,
onOpenRageShake: () -> Unit, onOpenRageShake: () -> Unit,
onOpenAbout: () -> Unit, onOpenAbout: () -> Unit,
@ -82,10 +82,11 @@ fun PreferencesRootView(
} }
if (state.accountManagementUrl != null) { if (state.accountManagementUrl != null) {
PreferenceText( PreferenceText(
title = stringResource(id = CommonStrings.screen_settings_oidc_account), title = stringResource(id = CommonStrings.action_manage_account),
icon = Icons.Outlined.ManageAccounts, icon = Icons.Outlined.OpenInNew,
onClick = onManageAccountClicked, onClick = { onManageAccountClicked(state.accountManagementUrl) },
) )
HorizontalDivider()
} }
if (state.showAnalyticsSettings) { if (state.showAnalyticsSettings) {
PreferenceText( PreferenceText(
@ -94,7 +95,7 @@ fun PreferencesRootView(
onClick = onOpenAnalytics, onClick = onOpenAnalytics,
) )
} }
if(state.showNotificationSettings) { if (state.showNotificationSettings) {
PreferenceText( PreferenceText(
title = stringResource(id = CommonStrings.screen_notification_settings_title), title = stringResource(id = CommonStrings.screen_notification_settings_title),
icon = Icons.Outlined.Notifications, icon = Icons.Outlined.Notifications,
@ -111,10 +112,19 @@ fun PreferencesRootView(
icon = Icons.Outlined.Help, icon = Icons.Outlined.Help,
onClick = onOpenAbout, onClick = onOpenAbout,
) )
HorizontalDivider()
if (state.devicesManagementUrl != null) {
PreferenceText(
title = stringResource(id = CommonStrings.action_manage_devices),
icon = Icons.Outlined.OpenInNew,
onClick = { onManageAccountClicked(state.devicesManagementUrl) },
)
HorizontalDivider()
}
if (state.showDeveloperSettings) { if (state.showDeveloperSettings) {
DeveloperPreferencesView(onOpenDeveloperSettings) DeveloperPreferencesView(onOpenDeveloperSettings)
HorizontalDivider()
} }
HorizontalDivider()
LogoutPreferenceView( LogoutPreferenceView(
state = state.logoutState, state = state.logoutState,
onSuccessLogout = onSuccessLogout, onSuccessLogout = onSuccessLogout,

View file

@ -73,6 +73,7 @@ class PreferencesRootPresenterTest {
assertThat(loadedState.showDeveloperSettings).isEqualTo(true) assertThat(loadedState.showDeveloperSettings).isEqualTo(true)
assertThat(loadedState.showAnalyticsSettings).isEqualTo(false) assertThat(loadedState.showAnalyticsSettings).isEqualTo(false)
assertThat(loadedState.accountManagementUrl).isNull() assertThat(loadedState.accountManagementUrl).isNull()
assertThat(loadedState.devicesManagementUrl).isNull()
} }
} }
} }

View file

@ -24,6 +24,7 @@ import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
@ -66,7 +67,7 @@ interface MatrixClient : Closeable {
suspend fun logout(): String? suspend fun logout(): String?
suspend fun loadUserDisplayName(): Result<String> suspend fun loadUserDisplayName(): Result<String>
suspend fun loadUserAvatarURLString(): Result<String?> suspend fun loadUserAvatarURLString(): Result<String?>
suspend fun getAccountManagementUrl(): Result<String?> suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?>
suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String> suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result<String>
fun roomMembershipObserver(): RoomMembershipObserver fun roomMembershipObserver(): RoomMembershipObserver

View file

@ -0,0 +1,24 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.api.oidc
sealed interface AccountManagementAction {
data object Profile : AccountManagementAction
data object SessionsList : AccountManagementAction
data class SessionView(val deviceId: String) : AccountManagementAction
data class SessionEnd(val deviceId: String) : AccountManagementAction
}

View file

@ -30,6 +30,7 @@ import io.element.android.libraries.matrix.api.createroom.RoomVisibility
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
@ -44,6 +45,7 @@ import io.element.android.libraries.matrix.impl.mapper.toSessionData
import io.element.android.libraries.matrix.impl.media.RustMediaLoader import io.element.android.libraries.matrix.impl.media.RustMediaLoader
import io.element.android.libraries.matrix.impl.notification.RustNotificationService import io.element.android.libraries.matrix.impl.notification.RustNotificationService
import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService import io.element.android.libraries.matrix.impl.notificationsettings.RustNotificationSettingsService
import io.element.android.libraries.matrix.impl.oidc.toRustAction
import io.element.android.libraries.matrix.impl.pushers.RustPushersService import io.element.android.libraries.matrix.impl.pushers.RustPushersService
import io.element.android.libraries.matrix.impl.room.RoomContentForwarder import io.element.android.libraries.matrix.impl.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
@ -326,9 +328,10 @@ class RustMatrixClient constructor(
return result return result
} }
override suspend fun getAccountManagementUrl(): Result<String?> = withContext(sessionDispatcher) { override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?> = withContext(sessionDispatcher) {
val rustAction = action?.toRustAction()
runCatching { runCatching {
client.accountUrl() client.accountUrl(rustAction)
} }
} }

View file

@ -0,0 +1,29 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.matrix.impl.oidc
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import org.matrix.rustcomponents.sdk.AccountManagementAction as RustAccountManagementAction
fun AccountManagementAction.toRustAction(): RustAccountManagementAction {
return when (this) {
AccountManagementAction.Profile -> RustAccountManagementAction.Profile
is AccountManagementAction.SessionEnd -> RustAccountManagementAction.SessionEnd(deviceId)
is AccountManagementAction.SessionView -> RustAccountManagementAction.SessionView(deviceId)
AccountManagementAction.SessionsList -> RustAccountManagementAction.SessionsList
}
}

View file

@ -25,6 +25,7 @@ import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.notification.NotificationService
import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService import io.element.android.libraries.matrix.api.notificationsettings.NotificationSettingsService
import io.element.android.libraries.matrix.api.oidc.AccountManagementAction
import io.element.android.libraries.matrix.api.pusher.PushersService import io.element.android.libraries.matrix.api.pusher.PushersService
import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
@ -129,7 +130,7 @@ class FakeMatrixClient(
return userAvatarURLString return userAvatarURLString
} }
override suspend fun getAccountManagementUrl(): Result<String?> { override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?> {
return accountManagementUrlString return accountManagementUrlString
} }
override suspend fun uploadMedia( override suspend fun uploadMedia(