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.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.theme.ElementTheme
import timber.log.Timber
@ContributesNode(SessionScope::class)
@@ -67,9 +68,17 @@ class PreferencesRootNode @AssistedInject constructor(
plugins<Callback>().forEach { it.onOpenAbout() }
}
private fun onManageAccountClicked(activity: Activity, accountManagementUrl: String?) {
accountManagementUrl?.let {
activity.openUrlInChromeCustomTab(null, false, it)
private fun onManageAccountClicked(
activity: Activity,
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) {
val state = presenter.present()
val activity = LocalContext.current as Activity
val isDark = ElementTheme.isLightTheme.not()
PreferencesRootView(
state = state,
modifier = modifier,
@@ -91,7 +101,7 @@ class PreferencesRootNode @AssistedInject constructor(
onVerifyClicked = this::onVerifyClicked,
onOpenDeveloperSettings = this::onOpenDeveloperSettings,
onSuccessLogout = { onSuccessLogout(activity, it) },
onManageAccountClicked = { onManageAccountClicked(activity, state.accountManagementUrl) },
onManageAccountClicked = { onManageAccountClicked(activity, it, isDark) },
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.FeatureFlags
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.getCurrentUser
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
@@ -74,9 +75,12 @@ class PreferencesRootPresenter @Inject constructor(
val accountManagementUrl: MutableState<String?> = remember {
mutableStateOf(null)
}
val devicesManagementUrl: MutableState<String?> = remember {
mutableStateOf(null)
}
LaunchedEffect(Unit) {
initAccountManagementUrl(accountManagementUrl)
initAccountManagementUrl(accountManagementUrl, devicesManagementUrl)
}
val logoutState = logoutPresenter.present()
@@ -87,6 +91,7 @@ class PreferencesRootPresenter @Inject constructor(
version = versionFormatter.get(),
showCompleteVerification = showCompleteVerification,
accountManagementUrl = accountManagementUrl.value,
devicesManagementUrl = devicesManagementUrl.value,
showAnalyticsSettings = hasAnalyticsProviders,
showDeveloperSettings = showDeveloperSettings,
showNotificationSettings = showNotificationSettings.value,
@@ -98,7 +103,11 @@ class PreferencesRootPresenter @Inject constructor(
matrixUser.value = matrixClient.getCurrentUser()
}
private fun CoroutineScope.initAccountManagementUrl(accountManagementUrl: MutableState<String?>) = launch {
accountManagementUrl.value = matrixClient.getAccountManagementUrl().getOrNull()
private fun CoroutineScope.initAccountManagementUrl(
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 showCompleteVerification: Boolean,
val accountManagementUrl: String?,
val devicesManagementUrl: String?,
val showAnalyticsSettings: Boolean,
val showDeveloperSettings: Boolean,
val showNotificationSettings: Boolean,

View File

@@ -26,6 +26,7 @@ fun aPreferencesRootState() = PreferencesRootState(
version = "Version 1.1 (1)",
showCompleteVerification = true,
accountManagementUrl = "aUrl",
devicesManagementUrl = "anOtherUrl",
showAnalyticsSettings = true,
showDeveloperSettings = 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.Help
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.OpenInNew
import androidx.compose.material.icons.outlined.VerifiedUser
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -53,7 +53,7 @@ fun PreferencesRootView(
state: PreferencesRootState,
onBackPressed: () -> Unit,
onVerifyClicked: () -> Unit,
onManageAccountClicked: () -> Unit,
onManageAccountClicked: (url: String) -> Unit,
onOpenAnalytics: () -> Unit,
onOpenRageShake: () -> Unit,
onOpenAbout: () -> Unit,
@@ -82,10 +82,11 @@ fun PreferencesRootView(
}
if (state.accountManagementUrl != null) {
PreferenceText(
title = stringResource(id = CommonStrings.screen_settings_oidc_account),
icon = Icons.Outlined.ManageAccounts,
onClick = onManageAccountClicked,
title = stringResource(id = CommonStrings.action_manage_account),
icon = Icons.Outlined.OpenInNew,
onClick = { onManageAccountClicked(state.accountManagementUrl) },
)
HorizontalDivider()
}
if (state.showAnalyticsSettings) {
PreferenceText(
@@ -94,7 +95,7 @@ fun PreferencesRootView(
onClick = onOpenAnalytics,
)
}
if(state.showNotificationSettings) {
if (state.showNotificationSettings) {
PreferenceText(
title = stringResource(id = CommonStrings.screen_notification_settings_title),
icon = Icons.Outlined.Notifications,
@@ -111,10 +112,19 @@ fun PreferencesRootView(
icon = Icons.Outlined.Help,
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) {
DeveloperPreferencesView(onOpenDeveloperSettings)
HorizontalDivider()
}
HorizontalDivider()
LogoutPreferenceView(
state = state.logoutState,
onSuccessLogout = onSuccessLogout,

View File

@@ -73,6 +73,7 @@ class PreferencesRootPresenterTest {
assertThat(loadedState.showDeveloperSettings).isEqualTo(true)
assertThat(loadedState.showAnalyticsSettings).isEqualTo(false)
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.notification.NotificationService
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.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
@@ -66,7 +67,7 @@ interface MatrixClient : Closeable {
suspend fun logout(): String?
suspend fun loadUserDisplayName(): 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>
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.notification.NotificationService
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.room.MatrixRoom
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.notification.RustNotificationService
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.room.RoomContentForwarder
import io.element.android.libraries.matrix.impl.room.RustMatrixRoom
@@ -326,9 +328,10 @@ class RustMatrixClient constructor(
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 {
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.notification.NotificationService
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.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.RoomMembershipObserver
@@ -129,7 +130,7 @@ class FakeMatrixClient(
return userAvatarURLString
}
override suspend fun getAccountManagementUrl(): Result<String?> {
override suspend fun getAccountManagementUrl(action: AccountManagementAction?): Result<String?> {
return accountManagementUrlString
}
override suspend fun uploadMedia(