Add a way to tweak MAS url.

This commit is contained in:
Benoit Marty 2026-04-28 17:55:56 +02:00 committed by Benoit Marty
parent 79eb28d10a
commit eafd94e4a6
19 changed files with 60 additions and 7 deletions

@ -1 +1 @@
Subproject commit cdde60c158ecd0987a3ba6fd79a4617551aff463 Subproject commit 1ed9a7e8b4406a5e6d12f603cfc2083e84f8576f

View file

@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.Flow
interface EnterpriseService { interface EnterpriseService {
val isEnterpriseBuild: Boolean val isEnterpriseBuild: Boolean
suspend fun isEnterpriseUser(sessionId: SessionId): Boolean suspend fun isEnterpriseUser(sessionId: SessionId): Boolean
suspend fun tweakMasUrl(url: String, homeserver: String): String
fun defaultHomeserverList(): List<String> fun defaultHomeserverList(): List<String>
suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean

View file

@ -10,6 +10,7 @@ package io.element.android.features.enterprise.api
interface SessionEnterpriseService { interface SessionEnterpriseService {
suspend fun isElementCallAvailable(): Boolean suspend fun isElementCallAvailable(): Boolean
suspend fun tweakMasUrl(url: String): String
suspend fun init() suspend fun init()
} }

View file

@ -23,7 +23,7 @@ class DefaultEnterpriseService : EnterpriseService {
override val isEnterpriseBuild = false override val isEnterpriseBuild = false
override suspend fun isEnterpriseUser(sessionId: SessionId) = false override suspend fun isEnterpriseUser(sessionId: SessionId) = false
override suspend fun tweakMasUrl(url: String, homeserver: String) = url
override fun defaultHomeserverList(): List<String> = emptyList() override fun defaultHomeserverList(): List<String> = emptyList()
override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true

View file

@ -15,5 +15,6 @@ import io.element.android.libraries.di.SessionScope
@ContributesBinding(SessionScope::class) @ContributesBinding(SessionScope::class)
class DefaultSessionEnterpriseService : SessionEnterpriseService { class DefaultSessionEnterpriseService : SessionEnterpriseService {
override suspend fun init() = Unit override suspend fun init() = Unit
override suspend fun tweakMasUrl(url: String): String = url
override suspend fun isElementCallAvailable(): Boolean = true override suspend fun isElementCallAvailable(): Boolean = true
} }

View file

@ -30,6 +30,7 @@ class FakeEnterpriseService(
private val firebasePushGatewayResult: () -> String? = { lambdaError() }, private val firebasePushGatewayResult: () -> String? = { lambdaError() },
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() }, private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() }, private val getNoisyNotificationChannelIdResult: (SessionId?) -> String? = { lambdaError() },
private val tweakMasUrlResult: (String, String) -> String = { _, _ -> lambdaError() },
) : EnterpriseService { ) : EnterpriseService {
private val brandColorState = MutableStateFlow(initialBrandColor) private val brandColorState = MutableStateFlow(initialBrandColor)
private val semanticColorsState = MutableStateFlow(initialSemanticColors) private val semanticColorsState = MutableStateFlow(initialSemanticColors)
@ -38,6 +39,10 @@ class FakeEnterpriseService(
isEnterpriseUserResult(sessionId) isEnterpriseUserResult(sessionId)
} }
override suspend fun tweakMasUrl(url: String, homeserver: String): String = simulateLongTask {
tweakMasUrlResult(url, homeserver)
}
override fun defaultHomeserverList(): List<String> { override fun defaultHomeserverList(): List<String> {
return defaultHomeserverListResult() return defaultHomeserverListResult()
} }

View file

@ -14,10 +14,15 @@ import io.element.android.tests.testutils.simulateLongTask
class FakeSessionEnterpriseService( class FakeSessionEnterpriseService(
private val isElementCallAvailableResult: () -> Boolean = { lambdaError() }, private val isElementCallAvailableResult: () -> Boolean = { lambdaError() },
private val tweakMasUrlResult: (String) -> String = { lambdaError() },
) : SessionEnterpriseService { ) : SessionEnterpriseService {
override suspend fun init() { override suspend fun init() {
} }
override suspend fun tweakMasUrl(url: String): String = simulateLongTask {
tweakMasUrlResult(url)
}
override suspend fun isElementCallAvailable(): Boolean = simulateLongTask { override suspend fun isElementCallAvailable(): Boolean = simulateLongTask {
isElementCallAvailableResult() isElementCallAvailableResult()
} }

View file

@ -26,6 +26,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
import io.element.android.features.linknewdevice.impl.screens.confirmation.CodeConfirmationNode import io.element.android.features.linknewdevice.impl.screens.confirmation.CodeConfirmationNode
import io.element.android.features.linknewdevice.impl.screens.desktop.DesktopNoticeNode import io.element.android.features.linknewdevice.impl.screens.desktop.DesktopNoticeNode
@ -65,6 +66,7 @@ class LinkNewDeviceFlowNode(
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
private val linkNewMobileHandler: LinkNewMobileHandler, private val linkNewMobileHandler: LinkNewMobileHandler,
private val linkNewDesktopHandler: LinkNewDesktopHandler, private val linkNewDesktopHandler: LinkNewDesktopHandler,
private val sessionEnterpriseService: SessionEnterpriseService,
) : BaseFlowNode<LinkNewDeviceFlowNode.NavTarget>( ) : BaseFlowNode<LinkNewDeviceFlowNode.NavTarget>(
backstack = BackStack( backstack = BackStack(
initialElement = NavTarget.Root, initialElement = NavTarget.Root,
@ -301,8 +303,12 @@ class LinkNewDeviceFlowNode(
} }
} }
private fun navigateToBrowser(url: String) { private suspend fun navigateToBrowser(url: String) {
activity?.openUrlInChromeCustomTab(null, darkTheme, url) activity?.openUrlInChromeCustomTab(
session = null,
darkTheme = darkTheme,
url = sessionEnterpriseService.tweakMasUrl(url),
)
} }
@Composable @Composable

View file

@ -11,6 +11,7 @@ import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.testing.junit4.util.MainDispatcherRule import com.bumble.appyx.testing.junit4.util.MainDispatcherRule
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint import io.element.android.features.linknewdevice.api.LinkNewDeviceEntryPoint
import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.test.FakeMatrixClient
import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.lambda.lambdaError
@ -37,6 +38,7 @@ class DefaultLinkNewDeviceEntryPointTest {
sessionCoroutineScope = backgroundScope, sessionCoroutineScope = backgroundScope,
linkNewMobileHandler = LinkNewMobileHandler(client), linkNewMobileHandler = LinkNewMobileHandler(client),
linkNewDesktopHandler = LinkNewDesktopHandler(client), linkNewDesktopHandler = LinkNewDesktopHandler(client),
sessionEnterpriseService = FakeSessionEnterpriseService(),
) )
} }
val callback: LinkNewDeviceEntryPoint.Callback = object : LinkNewDeviceEntryPoint.Callback { val callback: LinkNewDeviceEntryPoint.Callback = object : LinkNewDeviceEntryPoint.Callback {

View file

@ -19,6 +19,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.logout.api.direct.DirectLogoutState import io.element.android.features.logout.api.direct.DirectLogoutState
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
@ -55,6 +56,7 @@ class PreferencesRootPresenter(
private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability,
private val featureFlagService: FeatureFlagService, private val featureFlagService: FeatureFlagService,
private val sessionStore: SessionStore, private val sessionStore: SessionStore,
private val sessionEnterpriseService: SessionEnterpriseService,
) : Presenter<PreferencesRootState> { ) : Presenter<PreferencesRootState> {
@Composable @Composable
override fun present(): PreferencesRootState { override fun present(): PreferencesRootState {
@ -158,6 +160,10 @@ class PreferencesRootPresenter(
private fun CoroutineScope.initAccountManagementUrl( private fun CoroutineScope.initAccountManagementUrl(
accountManagementUrl: MutableState<String?>, accountManagementUrl: MutableState<String?>,
) = launch { ) = launch {
accountManagementUrl.value = matrixClient.getAccountManagementUrl(null).getOrNull() accountManagementUrl.value = matrixClient.getAccountManagementUrl(null)
.getOrNull()
?.let {
sessionEnterpriseService.tweakMasUrl(it)
}
} }
} }

View file

@ -12,6 +12,8 @@ package io.element.android.features.preferences.impl.root
import app.cash.turbine.ReceiveTurbine import app.cash.turbine.ReceiveTurbine
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.enterprise.test.FakeSessionEnterpriseService
import io.element.android.features.logout.api.direct.aDirectLogoutState import io.element.android.features.logout.api.direct.aDirectLogoutState
import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider import io.element.android.features.preferences.impl.utils.ShowDeveloperSettingsProvider
import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.features.rageshake.api.RageshakeFeatureAvailability
@ -65,6 +67,9 @@ class PreferencesRootPresenterTest {
) )
createPresenter( createPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
sessionEnterpriseService = FakeSessionEnterpriseService(
tweakMasUrlResult = { "tweaked $it" },
),
).test { ).test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState.myUser).isEqualTo( assertThat(initialState.myUser).isEqualTo(
@ -100,7 +105,7 @@ class PreferencesRootPresenterTest {
val finalState = awaitItem() val finalState = awaitItem()
accountManagementUrlResult.assertions().isCalledOnce() accountManagementUrlResult.assertions().isCalledOnce()
.with(value(null)) .with(value(null))
assertThat(finalState.accountManagementUrl).isEqualTo("null url") assertThat(finalState.accountManagementUrl).isEqualTo("tweaked null url")
} }
} }
@ -327,6 +332,7 @@ class PreferencesRootPresenterTest {
indicatorService: IndicatorService = FakeIndicatorService(), indicatorService: IndicatorService = FakeIndicatorService(),
featureFlagService: FeatureFlagService = FakeFeatureFlagService(), featureFlagService: FeatureFlagService = FakeFeatureFlagService(),
sessionStore: SessionStore = InMemorySessionStore(), sessionStore: SessionStore = InMemorySessionStore(),
sessionEnterpriseService: SessionEnterpriseService = FakeSessionEnterpriseService(),
) = PreferencesRootPresenter( ) = PreferencesRootPresenter(
matrixClient = matrixClient, matrixClient = matrixClient,
sessionVerificationService = sessionVerificationService, sessionVerificationService = sessionVerificationService,
@ -339,5 +345,6 @@ class PreferencesRootPresenterTest {
rageshakeFeatureAvailability = rageshakeFeatureAvailability, rageshakeFeatureAvailability = rageshakeFeatureAvailability,
featureFlagService = featureFlagService, featureFlagService = featureFlagService,
sessionStore = sessionStore, sessionStore = sessionStore,
sessionEnterpriseService = sessionEnterpriseService,
) )
} }

View file

@ -28,6 +28,7 @@ setupDependencyInjection()
dependencies { dependencies {
implementation(projects.appconfig) implementation(projects.appconfig)
implementation(projects.features.enterprise.api)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)

View file

@ -25,6 +25,7 @@ import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode import io.element.android.annotations.ContributesNode
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.securebackup.impl.reset.password.ResetIdentityPasswordNode import io.element.android.features.securebackup.impl.reset.password.ResetIdentityPasswordNode
import io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootNode import io.element.android.features.securebackup.impl.reset.root.ResetIdentityRootNode
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
@ -53,6 +54,7 @@ class ResetIdentityFlowNode(
private val resetIdentityFlowManager: ResetIdentityFlowManager, private val resetIdentityFlowManager: ResetIdentityFlowManager,
@SessionCoroutineScope @SessionCoroutineScope
private val sessionCoroutineScope: CoroutineScope, private val sessionCoroutineScope: CoroutineScope,
private val sessionEnterpriseService: SessionEnterpriseService,
) : BaseFlowNode<ResetIdentityFlowNode.NavTarget>( ) : BaseFlowNode<ResetIdentityFlowNode.NavTarget>(
backstack = BackStack(initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap), backstack = BackStack(initialElement = NavTarget.Root, savedStateMap = buildContext.savedStateMap),
buildContext = buildContext, buildContext = buildContext,
@ -125,7 +127,8 @@ class ResetIdentityFlowNode(
} }
is IdentityOAuthResetHandle -> { is IdentityOAuthResetHandle -> {
Timber.d("Launching reset confirmation in MAS") Timber.d("Launching reset confirmation in MAS")
activity.openUrlInChromeCustomTab(null, darkTheme, handle.url) val url = sessionEnterpriseService.tweakMasUrl(handle.url)
activity.openUrlInChromeCustomTab(null, darkTheme, url)
Timber.d("Starting resetOAuth") Timber.d("Starting resetOAuth")
resetJob = launch { handle.resetOAuth() } resetJob = launch { handle.resetOAuth() }
resetJob?.invokeOnCompletion { Timber.d("resetOAuth ended") } resetJob?.invokeOnCompletion { Timber.d("resetOAuth ended") }

View file

@ -31,6 +31,7 @@ dependencies {
implementation(projects.libraries.rustlsTls) implementation(projects.libraries.rustlsTls)
implementation(projects.appconfig) implementation(projects.appconfig)
implementation(projects.features.enterprise.api)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.di) implementation(projects.libraries.di)
implementation(projects.libraries.featureflag.api) implementation(projects.libraries.featureflag.api)

View file

@ -11,6 +11,7 @@ package io.element.android.libraries.matrix.impl.auth
import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.SingleIn import dev.zacsweers.metro.SingleIn
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.core.extensions.mapFailure import io.element.android.libraries.core.extensions.mapFailure
import io.element.android.libraries.core.extensions.runCatchingExceptions import io.element.android.libraries.core.extensions.runCatchingExceptions
@ -66,6 +67,7 @@ class RustMatrixAuthenticationService(
private val rustMatrixClientFactory: RustMatrixClientFactory, private val rustMatrixClientFactory: RustMatrixClientFactory,
private val passphraseGenerator: PassphraseGenerator, private val passphraseGenerator: PassphraseGenerator,
private val oAuthConfigurationProvider: OAuthConfigurationProvider, private val oAuthConfigurationProvider: OAuthConfigurationProvider,
private val enterpriseService: EnterpriseService,
) : MatrixAuthenticationService { ) : MatrixAuthenticationService {
// Any existing Element Classic session that we want to try to import secrets from during login. // Any existing Element Classic session that we want to try to import secrets from during login.
private var elementClassicSession: ElementClassicSession? = null private var elementClassicSession: ElementClassicSession? = null
@ -269,6 +271,12 @@ class RustMatrixAuthenticationService(
additionalScopes = emptyList(), additionalScopes = emptyList(),
) )
val url = oAuthAuthorizationData.loginUrl() val url = oAuthAuthorizationData.loginUrl()
.let {
enterpriseService.tweakMasUrl(
url = it,
homeserver = client.server() ?: client.homeserver(),
)
}
pendingOAuthAuthorizationData = oAuthAuthorizationData pendingOAuthAuthorizationData = oAuthAuthorizationData
OAuthDetails(url) OAuthDetails(url)
}.mapFailure { failure -> }.mapFailure { failure ->

View file

@ -14,4 +14,5 @@ data class ElementWellKnown(
val rageshakeUrl: String?, val rageshakeUrl: String?,
val brandColor: String?, val brandColor: String?,
val notificationSound: String?, val notificationSound: String?,
val identityProviderAppScheme: String?,
) )

View file

@ -32,4 +32,6 @@ data class InternalElementWellKnown(
val brandColor: String? = null, val brandColor: String? = null,
@SerialName("notification_sound") @SerialName("notification_sound")
val notificationSound: String? = null, val notificationSound: String? = null,
@SerialName("idp_app_scheme")
val identityProviderAppScheme: String? = null,
) )

View file

@ -16,4 +16,5 @@ internal fun InternalElementWellKnown.map() = ElementWellKnown(
rageshakeUrl = rageshakeUrl, rageshakeUrl = rageshakeUrl,
brandColor = brandColor, brandColor = brandColor,
notificationSound = notificationSound, notificationSound = notificationSound,
identityProviderAppScheme = identityProviderAppScheme,
) )

View file

@ -16,10 +16,12 @@ fun anElementWellKnown(
rageshakeUrl: String? = null, rageshakeUrl: String? = null,
brandColor: String? = null, brandColor: String? = null,
notificationSound: String? = null, notificationSound: String? = null,
identityProviderAppScheme: String? = null,
) = ElementWellKnown( ) = ElementWellKnown(
registrationHelperUrl = registrationHelperUrl, registrationHelperUrl = registrationHelperUrl,
enforceElementPro = enforceElementPro, enforceElementPro = enforceElementPro,
rageshakeUrl = rageshakeUrl, rageshakeUrl = rageshakeUrl,
brandColor = brandColor, brandColor = brandColor,
notificationSound = notificationSound, notificationSound = notificationSound,
identityProviderAppScheme = identityProviderAppScheme,
) )