diff --git a/Makefile b/Makefile
new file mode 100644
index 00000000000..99c949929fd
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,21 @@
+PURPLE := \033[0;35m
+NC := \033[0m # No Color (reset)
+
+# Staging apk
+STAGING_APK_PATH := $(wildcard app/build/outputs/apk/staging/debug/com.*.apk)
+
+# Get user id for sample work profile
+WORK_PROFILE := $(shell adb shell pm list users | grep "Managed Profile")
+WORK_PROFILE_ID := $(shell echo "$(WORK_PROFILE)" | awk -F'[:{}]' '{print $$2}')
+
+assemble/staging-debug:
+ @echo "🔧️$(PURPLE)Assembling staging debug build...$(NC)"
+ ./gradlew assembleStagingDebug
+
+install/staging-debug:
+ @echo "🚀$(PURPLE)Installing staging debug build on connected device...$(NC)"
+ adb install -r $(STAGING_APK_PATH)
+
+emm/install/staging-debug:
+ @echo "🚀$(PURPLE)Installing staging debug build on connected device on work-profile...$(NC)"
+ adb install --user $(WORK_PROFILE_ID) -r $(STAGING_APK_PATH)
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 519ef573c42..0b48a3b4519 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -268,6 +268,7 @@ dependencies {
implementation(libs.aboutLibraries.compose.m3)
implementation(libs.compose.qr.code)
implementation(libs.audio.amplituda)
+ implementation(libs.enterprise.feedback)
// screenshot testing
screenshotTestImplementation(libs.compose.ui.tooling)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 248ebdbdbda..92b6c706df6 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -305,6 +305,10 @@
android:resource="@xml/provider_paths" />
+
+
diff --git a/app/src/main/kotlin/com/wire/android/config/ServerConfigProvider.kt b/app/src/main/kotlin/com/wire/android/config/ServerConfigProvider.kt
new file mode 100644
index 00000000000..53dd69a77ba
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/config/ServerConfigProvider.kt
@@ -0,0 +1,66 @@
+/*
+ * Wire
+ * Copyright (C) 2025 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.config
+
+import com.wire.android.BuildConfig
+import com.wire.android.emm.ManagedServerConfig
+import com.wire.kalium.logic.configuration.server.ServerConfig
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ServerConfigProvider @Inject constructor() {
+
+ fun getDefaultServerConfig(managedServerConfig: ManagedServerConfig? = null): ServerConfig.Links {
+ return if (managedServerConfig != null) {
+ with(managedServerConfig) {
+ ServerConfig.Links(
+ api = endpoints.backendURL,
+ accounts = endpoints.accountsURL,
+ webSocket = endpoints.backendWSURL,
+ teams = endpoints.teamsURL,
+ blackList = endpoints.blackListURL,
+ website = endpoints.websiteURL,
+ title = title,
+ isOnPremises = true, // EMM configuration always treated as on-premises
+ apiProxy = null
+ )
+ }
+ } else {
+ ServerConfig.Links(
+ api = BuildConfig.DEFAULT_BACKEND_URL_BASE_API,
+ accounts = BuildConfig.DEFAULT_BACKEND_URL_ACCOUNTS,
+ webSocket = BuildConfig.DEFAULT_BACKEND_URL_BASE_WEBSOCKET,
+ teams = BuildConfig.DEFAULT_BACKEND_URL_TEAM_MANAGEMENT,
+ blackList = BuildConfig.DEFAULT_BACKEND_URL_BLACKLIST,
+ website = BuildConfig.DEFAULT_BACKEND_URL_WEBSITE,
+ title = BuildConfig.DEFAULT_BACKEND_TITLE,
+ isOnPremises = false,
+ apiProxy = null
+ )
+ }
+ }
+}
+
+private val staticServerConfigProvider = ServerConfigProvider()
+
+fun getDefaultServerConfig(managedServerConfig: ManagedServerConfig? = null): ServerConfig.Links {
+ return staticServerConfigProvider.getDefaultServerConfig(managedServerConfig)
+}
+
+val DefaultServerConfig get() = getDefaultServerConfig()
diff --git a/app/src/main/kotlin/com/wire/android/di/ManagedConfigurationsModule.kt b/app/src/main/kotlin/com/wire/android/di/ManagedConfigurationsModule.kt
new file mode 100644
index 00000000000..47b9517bf75
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/di/ManagedConfigurationsModule.kt
@@ -0,0 +1,80 @@
+/*
+ * Wire
+ * Copyright (C) 2025 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.di
+
+import android.content.Context
+import com.wire.android.BuildConfig
+import com.wire.android.config.ServerConfigProvider
+import com.wire.android.emm.ManagedConfigurationsManager
+import com.wire.android.emm.ManagedConfigurationsManagerImpl
+import com.wire.android.util.EMPTY
+import com.wire.android.util.dispatchers.DispatcherProvider
+import com.wire.kalium.logic.configuration.server.ServerConfig
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Named
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+class ManagedConfigurationsModule {
+
+ @Provides
+ @Singleton
+ fun provideServerConfigProvider(): ServerConfigProvider = ServerConfigProvider()
+
+ @Provides
+ @Singleton
+ fun provideManagedConfigurationsRepository(
+ @ApplicationContext context: Context,
+ dispatcherProvider: DispatcherProvider,
+ serverConfigProvider: ServerConfigProvider
+ ): ManagedConfigurationsManager {
+ return ManagedConfigurationsManagerImpl(context, dispatcherProvider, serverConfigProvider)
+ }
+
+ @Provides
+ fun provideCurrentServerConfig(
+ managedConfigurationsManager: ManagedConfigurationsManager
+ ): ServerConfig.Links {
+ return if (BuildConfig.EMM_SUPPORT_ENABLED) {
+ // Returns the current resolved server configuration links, which could be either managed or default
+ managedConfigurationsManager.currentServerConfig
+ } else {
+ // If EMM support is disabled, always return the static default server configuration links
+ provideServerConfigProvider().getDefaultServerConfig(null)
+ }
+ }
+
+ @Provides
+ @Named("ssoCodeConfig")
+ fun provideCurrentSSOCodeConfig(
+ managedConfigurationsManager: ManagedConfigurationsManager
+ ): String {
+ return if (BuildConfig.EMM_SUPPORT_ENABLED) {
+ // Returns the current resolved SSO code from managed configurations, or empty if none
+ managedConfigurationsManager.currentSSOCodeConfig
+ } else {
+ // If EMM support is disabled, always return empty SSO code
+ String.EMPTY
+ }
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/config/DefaultServerConfig.kt b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsKeys.kt
similarity index 50%
rename from app/src/main/kotlin/com/wire/android/config/DefaultServerConfig.kt
rename to app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsKeys.kt
index 057250561fa..a29bec76371 100644
--- a/app/src/main/kotlin/com/wire/android/config/DefaultServerConfig.kt
+++ b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsKeys.kt
@@ -15,21 +15,11 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*/
-package com.wire.android.config
+package com.wire.android.emm
-import com.wire.android.BuildConfig
-import com.wire.kalium.logic.configuration.server.ServerConfig
+enum class ManagedConfigurationsKeys {
+ DEFAULT_SERVER_URLS,
+ SSO_CODE;
-val DefaultServerConfig = ServerConfig.Links(
- api = BuildConfig.DEFAULT_BACKEND_URL_BASE_API,
- accounts = BuildConfig.DEFAULT_BACKEND_URL_ACCOUNTS,
- webSocket = BuildConfig.DEFAULT_BACKEND_URL_BASE_WEBSOCKET,
- teams = BuildConfig.DEFAULT_BACKEND_URL_TEAM_MANAGEMENT,
- blackList = BuildConfig.DEFAULT_BACKEND_URL_BLACKLIST,
- website = BuildConfig.DEFAULT_BACKEND_URL_WEBSITE,
- title = BuildConfig.DEFAULT_BACKEND_TITLE,
- isOnPremises = false,
- apiProxy = null
-)
-
-fun ServerConfig.Links?.orDefault() = this ?: DefaultServerConfig
+ fun asKey() = name.lowercase()
+}
diff --git a/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsManager.kt b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsManager.kt
new file mode 100644
index 00000000000..dc285e16b44
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsManager.kt
@@ -0,0 +1,198 @@
+/*
+ * Wire
+ * Copyright (C) 2025 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.emm
+
+import android.content.Context
+import android.content.RestrictionsManager
+import com.wire.android.appLogger
+import com.wire.android.config.ServerConfigProvider
+import com.wire.android.util.EMPTY
+import com.wire.android.util.dispatchers.DispatcherProvider
+import com.wire.kalium.logic.configuration.server.ServerConfig
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.json.Json
+import java.util.concurrent.atomic.AtomicReference
+
+interface ManagedConfigurationsManager {
+ /**
+ * Current server config that ViewModels can access.
+ * This is thread-safe and will be updated when app resumes or broadcast receiver is triggered.
+ *
+ * @see refreshServerConfig
+ */
+ val currentServerConfig: ServerConfig.Links
+
+ /**
+ * Current SSO code if provided via managed configurations, empty string otherwise.
+ */
+
+ val currentSSOCodeConfig: String
+
+ /**
+ * Initialize the server config on first access or when explicitly called.
+ * This should be called when the app starts, resumes, or when broadcast receiver triggers.
+ *
+ * The result indicates whether a valid config was found or if there was an error.
+ * Nevertheless, the config is either updated or defaulted to [ServerConfigProvider.getDefaultServerConfig()].
+ *
+ * @return result of the update attempt, either success with the config,
+ * default [ServerConfigProvider.getDefaultServerConfig()] if no config found or cleared, or failure with reason.
+ */
+ suspend fun refreshServerConfig(): ServerConfigResult
+
+ /**
+ * Initialize the SSO code config on first access or when explicitly called.
+ * This should be called when the app starts, resumes, or when broadcast receiver triggers.
+ *
+ * The result indicates whether a valid config was found or if there was an error.
+ * Nevertheless, the config is either updated or defaulted to empty.
+ *
+ * @return result of the update attempt, either success with the config,
+ * empty if no config found or cleared, or failure with reason.
+ */
+ suspend fun refreshSSOCodeConfig(): SSOCodeConfigResult
+}
+
+internal class ManagedConfigurationsManagerImpl(
+ private val context: Context,
+ private val dispatchers: DispatcherProvider,
+ private val serverConfigProvider: ServerConfigProvider,
+) : ManagedConfigurationsManager {
+
+ private val json: Json = Json { ignoreUnknownKeys = true }
+ private val logger = appLogger.withTextTag(TAG)
+ private val restrictionsManager: RestrictionsManager by lazy {
+ context.getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager
+ }
+
+ private val _currentServerConfig = AtomicReference(null)
+ private val _currentSSOCodeConfig = AtomicReference(String.EMPTY)
+
+ override val currentServerConfig: ServerConfig.Links
+ get() = _currentServerConfig.get() ?: serverConfigProvider.getDefaultServerConfig()
+
+ override val currentSSOCodeConfig: String
+ get() = _currentSSOCodeConfig.get()
+
+ override suspend fun refreshServerConfig(): ServerConfigResult = withContext(dispatchers.io()) {
+ val managedServerConfig = getServerConfig()
+ val serverConfig: ServerConfig.Links = when (managedServerConfig) {
+ is ServerConfigResult.Empty,
+ is ServerConfigResult.Failure -> serverConfigProvider.getDefaultServerConfig(null)
+
+ is ServerConfigResult.Success -> serverConfigProvider.getDefaultServerConfig(
+ managedServerConfig.config
+ )
+ }
+ _currentServerConfig.set(serverConfig)
+ logger.i("Server config refreshed: $serverConfig")
+ managedServerConfig
+ }
+
+ override suspend fun refreshSSOCodeConfig(): SSOCodeConfigResult =
+ withContext(dispatchers.io()) {
+ val managedSSOCodeConfig = getSSOCodeConfig()
+ val ssoCode: String = when (managedSSOCodeConfig) {
+ is SSOCodeConfigResult.Empty -> String.EMPTY
+ is SSOCodeConfigResult.Failure -> String.EMPTY
+ is SSOCodeConfigResult.Success -> managedSSOCodeConfig.config.ssoCode
+ }
+
+ _currentSSOCodeConfig.set(ssoCode)
+ logger.i("SSO code config refreshed to: $ssoCode")
+ managedSSOCodeConfig
+ }
+
+ private suspend fun getSSOCodeConfig(): SSOCodeConfigResult =
+ withContext(dispatchers.io()) {
+ val restrictions = restrictionsManager.applicationRestrictions
+ if (restrictions == null || restrictions.isEmpty) {
+ logger.i("No application restrictions found")
+ return@withContext SSOCodeConfigResult.Empty
+ }
+
+ return@withContext try {
+ val ssoCode = getJsonRestrictionByKey(
+ ManagedConfigurationsKeys.SSO_CODE.asKey()
+ )
+
+ if (ssoCode?.isValid == true) {
+ logger.i("Managed SSO code found: $ssoCode")
+ SSOCodeConfigResult.Success(ssoCode)
+ } else {
+ logger.w("Managed SSO code is not valid: $ssoCode")
+ SSOCodeConfigResult.Failure("Managed SSO code is not a valid config. Check the format.")
+ }
+ } catch (e: InvalidManagedConfig) {
+ logger.w("Invalid managed SSO code config: ${e.reason}")
+ SSOCodeConfigResult.Failure(e.reason)
+ }
+ }
+
+ private suspend fun getServerConfig(): ServerConfigResult = withContext(dispatchers.io()) {
+ val restrictions = restrictionsManager.applicationRestrictions
+ if (restrictions == null || restrictions.isEmpty) {
+ logger.i("No application restrictions found")
+ return@withContext ServerConfigResult.Empty
+ }
+
+ return@withContext try {
+ val managedServerConfig = getJsonRestrictionByKey(
+ ManagedConfigurationsKeys.DEFAULT_SERVER_URLS.asKey()
+ )
+ if (managedServerConfig?.endpoints?.isValid == true) {
+ logger.i("Managed server config found: $managedServerConfig")
+ ServerConfigResult.Success(managedServerConfig)
+ } else {
+ logger.w("Managed server config is not valid: $managedServerConfig")
+ ServerConfigResult.Failure("Managed server config is not a valid config. Check the URLs and format.")
+ }
+ } catch (e: InvalidManagedConfig) {
+ logger.w("Invalid managed server config: ${e.reason}")
+ ServerConfigResult.Failure(e.reason)
+ }
+ }
+
+ @Suppress("TooGenericExceptionCaught")
+ private inline fun getJsonRestrictionByKey(key: String): T? =
+ restrictionsManager.applicationRestrictions.getString(key)?.let {
+ try {
+ json.decodeFromString(it)
+ } catch (e: Exception) {
+ throw InvalidManagedConfig("Failed to parse managed config for key $key: ${e.message}")
+ }
+ }
+
+ companion object {
+ private const val TAG = "ManagedConfigurationsManager"
+ }
+}
+
+data class InvalidManagedConfig(val reason: String) : Throwable(reason)
+
+sealed interface SSOCodeConfigResult {
+ data class Success(val config: ManagedSSOCodeConfig) : SSOCodeConfigResult
+ data object Empty : SSOCodeConfigResult
+ data class Failure(val reason: String) : SSOCodeConfigResult
+}
+
+sealed interface ServerConfigResult {
+ data class Success(val config: ManagedServerConfig) : ServerConfigResult
+ data object Empty : ServerConfigResult
+ data class Failure(val reason: String) : ServerConfigResult
+}
diff --git a/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsReceiver.kt b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsReceiver.kt
new file mode 100644
index 00000000000..f1ac6288862
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsReceiver.kt
@@ -0,0 +1,109 @@
+/*
+ * Wire
+ * Copyright (C) 2025 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.emm
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.wire.android.appLogger
+import com.wire.android.util.EMPTY
+import com.wire.android.util.dispatchers.DispatcherProvider
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ManagedConfigurationsReceiver @Inject constructor(
+ private val managedConfigurationsManager: ManagedConfigurationsManager,
+ private val managedConfigurationsReporter: ManagedConfigurationsReporter,
+ dispatcher: DispatcherProvider
+) : BroadcastReceiver() {
+
+ private val logger = appLogger.withTextTag(TAG)
+ private val scope by lazy {
+ CoroutineScope(SupervisorJob() + dispatcher.io())
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ when (intent.action) {
+ Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED -> {
+ scope.launch {
+ logger.i("Received intent to refresh managed configurations")
+ updateServerConfig()
+ updateSSOCodeConfig()
+ }
+ }
+
+ else -> logger.i("Received unexpected intent action: ${intent.action}")
+ }
+ }
+
+ private suspend fun updateServerConfig() {
+ when (val result = managedConfigurationsManager.refreshServerConfig()) {
+ is ServerConfigResult.Failure -> managedConfigurationsReporter.reportErrorState(
+ key = ManagedConfigurationsKeys.DEFAULT_SERVER_URLS.asKey(),
+ message = result.reason
+ )
+
+ is ServerConfigResult.Empty -> managedConfigurationsReporter.reportAppliedState(
+ key = ManagedConfigurationsKeys.DEFAULT_SERVER_URLS.asKey(),
+ message = "Managed configuration cleared",
+ data = String.EMPTY
+ )
+
+ // Just the title will be output, the docs state limits for these fields.
+ // See: https://developer.android.com/work/app-feedback/overview#keyed-app-state-components
+ is ServerConfigResult.Success -> {
+ managedConfigurationsReporter.reportAppliedState(
+ key = ManagedConfigurationsKeys.DEFAULT_SERVER_URLS.asKey(),
+ message = "Managed configuration applied",
+ data = result.config.title
+ )
+ }
+ }
+ }
+
+ private suspend fun updateSSOCodeConfig() {
+ when (val result = managedConfigurationsManager.refreshSSOCodeConfig()) {
+ is SSOCodeConfigResult.Failure -> managedConfigurationsReporter.reportErrorState(
+ key = ManagedConfigurationsKeys.SSO_CODE.asKey(),
+ message = result.reason
+ )
+
+ is SSOCodeConfigResult.Empty -> managedConfigurationsReporter.reportAppliedState(
+ key = ManagedConfigurationsKeys.SSO_CODE.asKey(),
+ message = "Managed configuration cleared",
+ data = String.EMPTY
+ )
+
+ is SSOCodeConfigResult.Success -> {
+ managedConfigurationsReporter.reportAppliedState(
+ key = ManagedConfigurationsKeys.SSO_CODE.asKey(),
+ message = "Managed configuration applied",
+ data = result.config.ssoCode
+ )
+ }
+ }
+ }
+
+ companion object {
+ private const val TAG = "ManagedConfigurationsReceiver"
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsReporter.kt b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsReporter.kt
new file mode 100644
index 00000000000..1e117e52295
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/emm/ManagedConfigurationsReporter.kt
@@ -0,0 +1,58 @@
+/*
+ * Wire
+ * Copyright (C) 2025 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.emm
+
+import android.content.Context
+import androidx.enterprise.feedback.KeyedAppState
+import androidx.enterprise.feedback.KeyedAppStatesReporter
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class ManagedConfigurationsReporter @Inject constructor(
+ @ApplicationContext context: Context
+) {
+ private val reporter by lazy { KeyedAppStatesReporter.create(context) }
+
+ fun reportAppliedState(key: String, message: String, data: String? = null) {
+ reporter.setStates(
+ hashSetOf(
+ KeyedAppState.builder()
+ .setKey(key)
+ .setSeverity(KeyedAppState.SEVERITY_INFO)
+ .setMessage(message)
+ .setData(data)
+ .build()
+ )
+ )
+ }
+
+ fun reportErrorState(key: String, message: String, data: String? = null) {
+ reporter.setStates(
+ hashSetOf(
+ KeyedAppState.builder()
+ .setKey(key)
+ .setSeverity(KeyedAppState.SEVERITY_ERROR)
+ .setMessage(message)
+ .setData(data)
+ .build()
+ )
+ )
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/emm/ManagedSSOCodeConfig.kt b/app/src/main/kotlin/com/wire/android/emm/ManagedSSOCodeConfig.kt
new file mode 100644
index 00000000000..0137429140b
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/emm/ManagedSSOCodeConfig.kt
@@ -0,0 +1,37 @@
+/*
+ * Wire
+ * Copyright (C) 2025 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.emm
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import java.util.UUID
+
+@Serializable
+data class ManagedSSOCodeConfig(
+ @SerialName("sso_code")
+ val ssoCode: String
+) {
+ @Transient
+ val isValid: Boolean = try {
+ UUID.fromString(ssoCode)
+ true
+ } catch (exception: IllegalArgumentException) {
+ false
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/emm/ManagedServerConfig.kt b/app/src/main/kotlin/com/wire/android/emm/ManagedServerConfig.kt
new file mode 100644
index 00000000000..458ccbeebc6
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/emm/ManagedServerConfig.kt
@@ -0,0 +1,50 @@
+/*
+ * Wire
+ * Copyright (C) 2025 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.emm
+
+import com.wire.android.util.isValidWebUrl
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+
+@Serializable
+data class ManagedServerConfig(
+ @SerialName("title")
+ val title: String,
+ @SerialName("endpoints")
+ val endpoints: ManagedServerLinks
+)
+
+@Serializable
+data class ManagedServerLinks(
+ val accountsURL: String,
+ val backendURL: String,
+ val backendWSURL: String,
+ val blackListURL: String,
+ val teamsURL: String,
+ val websiteURL: String
+) {
+
+ @Transient
+ val isValid: Boolean = accountsURL.isValidWebUrl() &&
+ backendURL.isValidWebUrl() &&
+ backendWSURL.isValidWebUrl() &&
+ blackListURL.isValidWebUrl() &&
+ teamsURL.isValidWebUrl() &&
+ websiteURL.isValidWebUrl()
+}
diff --git a/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/DynamicReceiversManager.kt b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/DynamicReceiversManager.kt
new file mode 100644
index 00000000000..9a9259c02e6
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/notification/broadcastreceivers/DynamicReceiversManager.kt
@@ -0,0 +1,56 @@
+/*
+ * Wire
+ * Copyright (C) 2025 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.notification.broadcastreceivers
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import com.wire.android.BuildConfig.EMM_SUPPORT_ENABLED
+import com.wire.android.appLogger
+import com.wire.android.emm.ManagedConfigurationsReceiver
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Manages dynamic registration and unregistration of broadcast receivers.
+ * This are receivers that are active while the app is in foreground only.
+ */
+@Singleton
+class DynamicReceiversManager @Inject constructor(
+ @ApplicationContext val context: Context,
+ private val managedConfigurationsReceiver: ManagedConfigurationsReceiver
+) {
+ fun registerAll() {
+ if (EMM_SUPPORT_ENABLED) {
+ appLogger.i("$TAG Registering Runtime ManagedConfigurations Broadcast receiver")
+ context.registerReceiver(managedConfigurationsReceiver, IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED))
+ }
+ }
+
+ fun unregisterAll() {
+ if (EMM_SUPPORT_ENABLED) {
+ appLogger.i("$TAG Unregistering Runtime ManagedConfigurations Broadcast receiver")
+ context.unregisterReceiver(managedConfigurationsReceiver)
+ }
+ }
+
+ companion object {
+ const val TAG = "DynamicReceiversManager"
+ }
+}
diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
index 9fcc757ce1f..422d67c4ec1 100644
--- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
@@ -61,6 +61,7 @@ import com.wire.android.appLogger
import com.wire.android.config.CustomUiConfigurationProvider
import com.wire.android.config.LocalCustomUiConfigurationProvider
import com.wire.android.datastore.UserDataStore
+import com.wire.android.emm.ManagedConfigurationsManager
import com.wire.android.feature.NavigationSwitchAccountActions
import com.wire.android.navigation.BackStackMode
import com.wire.android.navigation.LoginTypeSelector
@@ -72,6 +73,7 @@ import com.wire.android.navigation.rememberNavigator
import com.wire.android.navigation.startDestination
import com.wire.android.navigation.style.BackgroundStyle
import com.wire.android.navigation.style.BackgroundType
+import com.wire.android.notification.broadcastreceivers.DynamicReceiversManager
import com.wire.android.ui.authentication.login.LoginPasswordPath
import com.wire.android.ui.authentication.login.WireAuthBackgroundLayout
import com.wire.android.ui.calling.getIncomingCallIntent
@@ -148,6 +150,12 @@ class WireActivity : AppCompatActivity() {
@Inject
lateinit var loginTypeSelector: LoginTypeSelector
+ @Inject
+ lateinit var dynamicReceiversManager: DynamicReceiversManager
+
+ @Inject
+ lateinit var managedConfigurationsManager: ManagedConfigurationsManager
+
private val viewModel: WireActivityViewModel by viewModels()
private val featureFlagNotificationViewModel: FeatureFlagNotificationViewModel by viewModels()
private val callFeedbackViewModel: CallFeedbackViewModel by viewModels()
@@ -203,6 +211,22 @@ class WireActivity : AppCompatActivity() {
}
}
+ override fun onStart() {
+ super.onStart()
+ dynamicReceiversManager.registerAll()
+ if (BuildConfig.EMM_SUPPORT_ENABLED) {
+ lifecycleScope.launch(Dispatchers.IO) {
+ managedConfigurationsManager.refreshServerConfig()
+ managedConfigurationsManager.refreshSSOCodeConfig()
+ }
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ dynamicReceiversManager.unregisterAll()
+ }
+
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt
index c9565b7978b..caf0e7fba37 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt
@@ -26,7 +26,6 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.BuildConfig
-import com.wire.android.config.orDefault
import com.wire.android.di.ClientScopeProvider
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.ui.authentication.create.common.CreateAccountFlowType
@@ -59,11 +58,12 @@ class CreateAccountCodeViewModel @Inject constructor(
@KaliumCoreLogic private val coreLogic: CoreLogic,
private val addAuthenticatedUser: AddAuthenticatedUserUseCase,
private val clientScopeProviderFactory: ClientScopeProvider.Factory,
+ defaultServerConfig: ServerConfig.Links
) : ViewModel() {
val createAccountNavArgs: CreateAccountNavArgs = savedStateHandle.navArgs()
- val serverConfig: ServerConfig.Links = createAccountNavArgs.customServerConfig.orDefault()
+ val serverConfig: ServerConfig.Links = createAccountNavArgs.customServerConfig ?: defaultServerConfig
val codeTextState: TextFieldState = TextFieldState()
var codeState: CreateAccountCodeViewState by mutableStateOf(CreateAccountCodeViewState(createAccountNavArgs.flowType))
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModel.kt
index 339bed3203e..656d9d76cc0 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModel.kt
@@ -24,7 +24,6 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.wire.android.config.orDefault
import com.wire.android.ui.authentication.create.common.CreateAccountFlowType
import com.wire.android.ui.authentication.create.common.CreateAccountNavArgs
import com.wire.android.ui.common.textfield.textAsFlow
@@ -41,6 +40,7 @@ import javax.inject.Inject
class CreateAccountDetailsViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val validatePasswordUseCase: ValidatePasswordUseCase,
+ defaultServerConfig: ServerConfig.Links
) : ViewModel() {
val createAccountNavArgs: CreateAccountNavArgs = savedStateHandle.navArgs()
@@ -52,7 +52,7 @@ class CreateAccountDetailsViewModel @Inject constructor(
val teamNameTextState: TextFieldState = TextFieldState()
var detailsState: CreateAccountDetailsViewState by mutableStateOf(CreateAccountDetailsViewState(createAccountNavArgs.flowType))
- val serverConfig: ServerConfig.Links = createAccountNavArgs.customServerConfig.orDefault()
+ val serverConfig: ServerConfig.Links = createAccountNavArgs.customServerConfig ?: defaultServerConfig
init {
viewModelScope.launch {
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModel.kt
index fd8f2c75379..258f4efac16 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModel.kt
@@ -24,7 +24,6 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.wire.android.config.orDefault
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.ui.authentication.create.common.CreateAccountNavArgs
import com.wire.android.ui.common.textfield.textAsFlow
@@ -45,6 +44,7 @@ class CreateAccountEmailViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val validateEmail: ValidateEmailUseCase,
@KaliumCoreLogic private val coreLogic: CoreLogic,
+ defaultServerConfig: ServerConfig.Links
) : ViewModel() {
val createAccountNavArgs: CreateAccountNavArgs = savedStateHandle.navArgs()
@@ -53,7 +53,7 @@ class CreateAccountEmailViewModel @Inject constructor(
var emailState: CreateAccountEmailViewState by mutableStateOf(CreateAccountEmailViewState(createAccountNavArgs.flowType))
private set
- val serverConfig: ServerConfig.Links = createAccountNavArgs.customServerConfig.orDefault()
+ val serverConfig: ServerConfig.Links = createAccountNavArgs.customServerConfig ?: defaultServerConfig
fun tosUrl(): String = serverConfig.tos
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewViewModel.kt
index 09a3105cc2f..30cb1c4a655 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/overview/CreateAccountOverviewViewModel.kt
@@ -19,7 +19,6 @@ package com.wire.android.ui.authentication.create.overview
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
-import com.wire.android.config.orDefault
import com.wire.android.ui.navArgs
import com.wire.kalium.logic.configuration.server.ServerConfig
import dagger.hilt.android.lifecycle.HiltViewModel
@@ -28,8 +27,9 @@ import javax.inject.Inject
@HiltViewModel
class CreateAccountOverviewViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
+ defaultServerConfig: ServerConfig.Links
) : ViewModel() {
val navArgs: CreateAccountOverviewNavArgs = savedStateHandle.navArgs()
- val serverConfig: ServerConfig.Links = navArgs.customServerConfig.orDefault()
+ val serverConfig: ServerConfig.Links = navArgs.customServerConfig ?: defaultServerConfig
fun learnMoreUrl(): String = serverConfig.pricing
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginViewModel.kt
index 8bbd7b0a272..9f040e883d3 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginViewModel.kt
@@ -20,7 +20,6 @@ package com.wire.android.ui.authentication.login
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
-import com.wire.android.config.orDefault
import com.wire.android.datastore.UserDataStoreProvider
import com.wire.android.di.ClientScopeProvider
import com.wire.android.di.KaliumCoreLogic
@@ -43,7 +42,8 @@ open class LoginViewModel(
val clientScopeProviderFactory: ClientScopeProvider.Factory,
val userDataStoreProvider: UserDataStoreProvider,
val coreLogic: CoreLogic,
- private val loginExtension: LoginViewModelExtension
+ private val loginExtension: LoginViewModelExtension,
+ defaultServerConfig: ServerConfig.Links
) : ViewModel() {
@Inject
@@ -51,17 +51,19 @@ open class LoginViewModel(
savedStateHandle: SavedStateHandle,
clientScopeProviderFactory: ClientScopeProvider.Factory,
userDataStoreProvider: UserDataStoreProvider,
- @KaliumCoreLogic coreLogic: CoreLogic
+ @KaliumCoreLogic coreLogic: CoreLogic,
+ defaultServerConfig: ServerConfig.Links
) : this(
savedStateHandle,
clientScopeProviderFactory,
userDataStoreProvider,
coreLogic,
- LoginViewModelExtension(clientScopeProviderFactory, userDataStoreProvider)
+ LoginViewModelExtension(clientScopeProviderFactory, userDataStoreProvider),
+ defaultServerConfig
)
private val loginNavArgs: LoginNavArgs = savedStateHandle.navArgs()
- val serverConfig: ServerConfig.Links = loginNavArgs.loginPasswordPath?.customServerConfig.orDefault()
+ val serverConfig: ServerConfig.Links = loginNavArgs.loginPasswordPath?.customServerConfig ?: defaultServerConfig
suspend fun registerClient(
userId: UserId,
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt
index 8702342d3e9..1f77e62145b 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt
@@ -43,6 +43,7 @@ import com.wire.android.util.EMPTY
import com.wire.android.util.dispatchers.DispatcherProvider
import com.wire.android.util.ui.CountdownTimer
import com.wire.kalium.logic.CoreLogic
+import com.wire.kalium.logic.configuration.server.ServerConfig
import com.wire.kalium.logic.data.auth.login.ProxyCredentials
import com.wire.kalium.logic.data.auth.verification.VerifiableAction
import com.wire.kalium.logic.data.logout.LogoutReason
@@ -76,12 +77,14 @@ class LoginEmailViewModel @Inject constructor(
userDataStoreProvider: UserDataStoreProvider,
@KaliumCoreLogic coreLogic: CoreLogic,
private val resendCodeTimer: CountdownTimer,
- private val dispatchers: DispatcherProvider
+ private val dispatchers: DispatcherProvider,
+ defaultServerConfig: ServerConfig.Links,
) : LoginViewModel(
savedStateHandle,
clientScopeProviderFactory,
userDataStoreProvider,
- coreLogic
+ coreLogic,
+ defaultServerConfig
) {
val loginNavArgs: LoginNavArgs = savedStateHandle.navArgs()
private val preFilledUserIdentifier: PreFilledUserIdentifierType = loginNavArgs.userHandle ?: PreFilledUserIdentifierType.None
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt
index b83fb6777b9..a7fbab971fa 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt
@@ -64,12 +64,14 @@ class LoginSSOViewModel(
coreLogic: CoreLogic,
clientScopeProviderFactory: ClientScopeProvider.Factory,
userDataStoreProvider: UserDataStoreProvider,
- private val ssoExtension: LoginSSOViewModelExtension
+ private val ssoExtension: LoginSSOViewModelExtension,
+ serverConfig: ServerConfig.Links
) : LoginViewModel(
savedStateHandle,
clientScopeProviderFactory,
userDataStoreProvider,
- coreLogic
+ coreLogic,
+ serverConfig
) {
@Inject
@@ -80,6 +82,7 @@ class LoginSSOViewModel(
@KaliumCoreLogic coreLogic: CoreLogic,
clientScopeProviderFactory: ClientScopeProvider.Factory,
userDataStoreProvider: UserDataStoreProvider,
+ serverConfig: ServerConfig.Links
) : this(
savedStateHandle,
addAuthenticatedUser,
@@ -87,7 +90,8 @@ class LoginSSOViewModel(
coreLogic,
clientScopeProviderFactory,
userDataStoreProvider,
- LoginSSOViewModelExtension(addAuthenticatedUser, coreLogic)
+ LoginSSOViewModelExtension(addAuthenticatedUser, coreLogic),
+ serverConfig
)
var openWebUrl = MutableSharedFlow>()
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreen.kt
index ad8408a2593..1b85f51c326 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreen.kt
@@ -69,7 +69,6 @@ import androidx.hilt.navigation.compose.hiltViewModel
import com.wire.android.BuildConfig.ENABLE_NEW_REGISTRATION
import com.wire.android.R
import com.wire.android.config.LocalCustomUiConfigurationProvider
-import com.wire.android.config.orDefault
import com.wire.android.navigation.NavigationCommand
import com.wire.android.navigation.Navigator
import com.wire.android.navigation.annotation.app.WireDestination
@@ -233,7 +232,7 @@ private fun WelcomeContent(
NavigationCommand(
CreateAccountDataDetailScreenDestination(
CreateAccountDataNavArgs(
- customServerConfig = state.orDefault()
+ customServerConfig = state
)
)
)
diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModel.kt
index f24f8205f5a..2d8a1d068b9 100644
--- a/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModel.kt
@@ -25,7 +25,6 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.BuildConfig
-import com.wire.android.config.orDefault
import com.wire.android.ui.navArgs
import com.wire.kalium.logic.configuration.server.ServerConfig
import com.wire.kalium.logic.data.auth.AccountInfo
@@ -39,10 +38,11 @@ import javax.inject.Inject
class WelcomeViewModel @Inject constructor(
savedStateHandle: SavedStateHandle,
private val getSessions: GetSessionsUseCase,
+ defaultServerConfig: ServerConfig.Links
) : ViewModel() {
private val navArgs: WelcomeNavArgs = savedStateHandle.navArgs()
- var state by mutableStateOf(WelcomeScreenState(navArgs.customServerConfig.orDefault()))
+ var state by mutableStateOf(WelcomeScreenState(navArgs.customServerConfig ?: defaultServerConfig))
private set
init {
diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt
index 1c7f613218f..5fab862726b 100644
--- a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt
@@ -19,7 +19,6 @@
package com.wire.android.ui.newauthentication.login
import androidx.annotation.VisibleForTesting
-import com.wire.android.appLogger
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.compose.runtime.getValue
@@ -27,7 +26,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope
-import com.wire.android.config.orDefault
+import com.wire.android.appLogger
import com.wire.android.datastore.UserDataStoreProvider
import com.wire.android.di.ClientScopeProvider
import com.wire.android.di.KaliumCoreLogic
@@ -64,7 +63,9 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
+import javax.inject.Named
+@Suppress("LongParameterList")
@HiltViewModel
class NewLoginViewModel(
private val validateEmailOrSSOCode: ValidateEmailOrSSOCodeUseCase,
@@ -75,6 +76,8 @@ class NewLoginViewModel(
private val loginExtension: LoginViewModelExtension,
private val ssoExtension: LoginSSOViewModelExtension,
private val dispatchers: DispatcherProvider,
+ defaultServerConfig: ServerConfig.Links,
+ defaultSSOCodeConfig: String,
) : ActionsViewModel() {
@Inject
@@ -86,6 +89,8 @@ class NewLoginViewModel(
clientScopeProviderFactory: ClientScopeProvider.Factory,
userDataStoreProvider: UserDataStoreProvider,
dispatchers: DispatcherProvider,
+ defaultServerConfig: ServerConfig.Links,
+ @Named("ssoCodeConfig") defaultSSOCodeConfig: String,
) : this(
validateEmailOrSSOCode,
coreLogic,
@@ -95,11 +100,13 @@ class NewLoginViewModel(
LoginViewModelExtension(clientScopeProviderFactory, userDataStoreProvider),
LoginSSOViewModelExtension(addAuthenticatedUser, coreLogic),
dispatchers,
+ defaultServerConfig,
+ defaultSSOCodeConfig
)
private val loginNavArgs: LoginNavArgs = savedStateHandle.navArgs()
private val preFilledUserIdentifier: PreFilledUserIdentifierType = loginNavArgs.userHandle ?: PreFilledUserIdentifierType.None
- var serverConfig: ServerConfig.Links by mutableStateOf(loginNavArgs.loginPasswordPath?.customServerConfig.orDefault())
+ var serverConfig: ServerConfig.Links by mutableStateOf(loginNavArgs.loginPasswordPath?.customServerConfig ?: defaultServerConfig)
private set
var state by mutableStateOf(NewLoginScreenState())
@@ -107,9 +114,12 @@ class NewLoginViewModel(
val userIdentifierTextState: TextFieldState = TextFieldState()
init {
+ val isCustomServerDeepLink = loginNavArgs.loginPasswordPath?.customServerConfig != null
userIdentifierTextState.setTextAndPlaceCursorAtEnd(
if (preFilledUserIdentifier is PreFilledUserIdentifierType.PreFilled) {
preFilledUserIdentifier.userIdentifier
+ } else if (defaultSSOCodeConfig.isNotEmpty() && !isCustomServerDeepLink) {
+ defaultSSOCodeConfig.ssoCodeWithPrefix()
} else {
savedStateHandle[USER_IDENTIFIER_SAVED_STATE_KEY] ?: String.EMPTY
}
@@ -353,13 +363,13 @@ class NewLoginViewModel(
* Update the state based on the current state and input.
*/
private fun getAndUpdateLoginFlowState(update: (NewLoginFlowState) -> NewLoginFlowState) = viewModelScope.launch(dispatchers.main()) {
- val newState = update(state.flowState)
- val currentUserLoginInput = userIdentifierTextState.text
- state = state.copy(
- flowState = newState,
- nextEnabled = newState !is NewLoginFlowState.Loading && currentUserLoginInput.isNotEmpty()
- )
- }
+ val newState = update(state.flowState)
+ val currentUserLoginInput = userIdentifierTextState.text
+ state = state.copy(
+ flowState = newState,
+ nextEnabled = newState !is NewLoginFlowState.Loading && currentUserLoginInput.isNotEmpty()
+ )
+ }
}
private fun AutoVersionAuthScopeUseCase.Result.Failure.toLoginError() = when (this) {
diff --git a/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModel.kt
index 9e4cde5274d..dd7efdabf82 100644
--- a/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/registration/code/CreateAccountVerificationCodeViewModel.kt
@@ -27,7 +27,6 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.BuildConfig
import com.wire.android.analytics.RegistrationAnalyticsManagerUseCase
-import com.wire.android.config.orDefault
import com.wire.android.di.ClientScopeProvider
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.feature.analytics.model.AnalyticsEvent
@@ -57,11 +56,12 @@ class CreateAccountVerificationCodeViewModel @Inject constructor(
private val addAuthenticatedUser: AddAuthenticatedUserUseCase,
private val registrationAnalyticsManager: RegistrationAnalyticsManagerUseCase,
private val clientScopeProviderFactory: ClientScopeProvider.Factory,
+ defaultServerConfig: ServerConfig.Links
) : ViewModel() {
val createAccountNavArgs: CreateAccountDataNavArgs = savedStateHandle.navArgs()
- val serverConfig: ServerConfig.Links = createAccountNavArgs.customServerConfig.orDefault()
+ val serverConfig: ServerConfig.Links = createAccountNavArgs.customServerConfig ?: defaultServerConfig
val codeTextState: TextFieldState = TextFieldState()
var codeState: CreateAccountVerificationCodeViewState by mutableStateOf(
diff --git a/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModel.kt
index 48500609257..aa7eb7bd891 100644
--- a/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModel.kt
@@ -25,7 +25,6 @@ import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.wire.android.analytics.RegistrationAnalyticsManagerUseCase
-import com.wire.android.config.orDefault
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.di.KaliumCoreLogic
import com.wire.android.feature.analytics.model.AnalyticsEvent.RegistrationPersonalAccount
@@ -53,6 +52,7 @@ class CreateAccountDataDetailViewModel @Inject constructor(
private val globalDataStore: GlobalDataStore,
private val registrationAnalyticsManager: RegistrationAnalyticsManagerUseCase,
@KaliumCoreLogic private val coreLogic: CoreLogic,
+ defaultServerConfig: ServerConfig.Links
) : ViewModel() {
val createAccountNavArgs: CreateAccountDataNavArgs = savedStateHandle.navArgs()
@@ -65,7 +65,7 @@ class CreateAccountDataDetailViewModel @Inject constructor(
var detailsState: CreateAccountDataDetailViewState by mutableStateOf(CreateAccountDataDetailViewState())
- val serverConfig: ServerConfig.Links = createAccountNavArgs.customServerConfig.orDefault()
+ val serverConfig: ServerConfig.Links = createAccountNavArgs.customServerConfig ?: defaultServerConfig
fun tosUrl(): String = serverConfig.tos
fun teamCreationUrl(): String = serverConfig.teams
diff --git a/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorScreen.kt b/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorScreen.kt
index eef4dd560d5..7e12029be1a 100644
--- a/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorScreen.kt
@@ -48,7 +48,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel
import com.wire.android.R
-import com.wire.android.config.orDefault
import com.wire.android.navigation.BackStackMode
import com.wire.android.navigation.NavigationCommand
import com.wire.android.navigation.Navigator
@@ -89,7 +88,7 @@ fun CreateAccountSelectorScreen(
val context = LocalContext.current
fun navigateToEmailScreen() {
val createAccountNavArgs = CreateAccountDataNavArgs(
- customServerConfig = viewModel.serverConfig.orDefault(),
+ customServerConfig = viewModel.serverConfig,
userRegistrationInfo = UserRegistrationInfo(viewModel.email)
)
navigator.navigate(NavigationCommand(CreateAccountDataDetailScreenDestination(createAccountNavArgs)))
diff --git a/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModel.kt
index d783000a836..223393c2810 100644
--- a/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModel.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModel.kt
@@ -20,7 +20,6 @@ package com.wire.android.ui.registration.selector
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.wire.android.config.orDefault
import com.wire.android.datastore.GlobalDataStore
import com.wire.android.ui.navArgs
import com.wire.kalium.logic.configuration.server.ServerConfig
@@ -32,9 +31,10 @@ import javax.inject.Inject
class CreateAccountSelectorViewModel @Inject constructor(
private val globalDataStore: GlobalDataStore,
savedStateHandle: SavedStateHandle,
+ defaultServerConfig: ServerConfig.Links
) : ViewModel() {
val navArgs: CreateAccountSelectorNavArgs = savedStateHandle.navArgs()
- val serverConfig: ServerConfig.Links = navArgs.customServerConfig.orDefault()
+ val serverConfig: ServerConfig.Links = navArgs.customServerConfig ?: defaultServerConfig
val email: String = navArgs.email.orEmpty()
val teamAccountCreationUrl = serverConfig.teams
diff --git a/app/src/main/kotlin/com/wire/android/util/UriUtil.kt b/app/src/main/kotlin/com/wire/android/util/UriUtil.kt
index 4443a6de455..a518efc9a65 100644
--- a/app/src/main/kotlin/com/wire/android/util/UriUtil.kt
+++ b/app/src/main/kotlin/com/wire/android/util/UriUtil.kt
@@ -81,3 +81,6 @@ fun URI.findParameterValue(parameterName: String): String? {
null
}
}
+
+fun String.isValidWebUrl() = (this.startsWith("http://") || this.startsWith("https://"))
+ && android.util.Patterns.WEB_URL.matcher(this).matches()
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2396b6c91b3..c28a5b485f3 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1862,6 +1862,12 @@ In group conversations, the group admin can overwrite this setting.
When this is on, you can send messages with the Enter key on your keyboard.
Options
+
+ Server endpoints configuration
+ SSO code configuration
+ JSON value with the server endpoints configuration
+ JSON value with the default SSO code configuration
+
Channels are available for team members.
Create a team and start collaborating for free!
diff --git a/app/src/main/res/xml/app_restrictions.xml b/app/src/main/res/xml/app_restrictions.xml
new file mode 100644
index 00000000000..c43e1117b30
--- /dev/null
+++ b/app/src/main/res/xml/app_restrictions.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/test/kotlin/com/wire/android/emm/ManagedConfigurationsManagerTest.kt b/app/src/test/kotlin/com/wire/android/emm/ManagedConfigurationsManagerTest.kt
new file mode 100644
index 00000000000..328b04b553a
--- /dev/null
+++ b/app/src/test/kotlin/com/wire/android/emm/ManagedConfigurationsManagerTest.kt
@@ -0,0 +1,194 @@
+package com.wire.android.emm
+
+import android.app.Application
+import android.content.Context
+import android.content.RestrictionsManager
+import android.os.Bundle
+import androidx.test.core.app.ApplicationProvider
+import com.wire.android.config.ServerConfigProvider
+import com.wire.android.config.TestDispatcherProvider
+import com.wire.android.util.EMPTY
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertEquals
+import org.junit.Test
+import org.junit.jupiter.api.assertInstanceOf
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class)
+class ManagedConfigurationsManagerTest {
+
+ @Test
+ fun `given a server config is valid, then parse it to a corresponding ManagedServerConfig`() =
+ runTest {
+ val expected = ManagedServerConfig(
+ endpoints = ManagedServerLinks(
+ accountsURL = "https://account.anta.wire.link",
+ backendURL = "https://nginz-https.anta.wire.link",
+ backendWSURL = "https://nginz-ssl.anta.wire.link",
+ blackListURL = "https://disallowed-clients.anta.wire.link",
+ teamsURL = "https://teams.anta.wire.link",
+ websiteURL = "https://wire.com"
+ ),
+ title = "anta.wire.link"
+ )
+ val (_, manager) = Arrangement()
+ .withRestrictions(mapOf(ManagedConfigurationsKeys.DEFAULT_SERVER_URLS.asKey() to validServerConfigJson))
+ .arrange()
+
+ val result = manager.refreshServerConfig()
+ assertInstanceOf(result)
+
+ val serverConfig = manager.currentServerConfig
+ assertEquals(expected.title, serverConfig.title)
+ assertEquals(expected.endpoints.accountsURL, serverConfig.accounts)
+ assertEquals(expected.endpoints.backendURL, serverConfig.api)
+ assertEquals(expected.endpoints.backendWSURL, serverConfig.webSocket)
+ assertEquals(expected.endpoints.blackListURL, serverConfig.blackList)
+ assertEquals(expected.endpoints.teamsURL, serverConfig.teams)
+ assertEquals(expected.endpoints.websiteURL, serverConfig.website)
+ }
+
+ @Test
+ fun `given an invalid server config, then return null`() = runTest {
+ val (_, manager) = Arrangement()
+ .withRestrictions(mapOf(ManagedConfigurationsKeys.DEFAULT_SERVER_URLS.asKey() to "invalid json"))
+ .arrange()
+
+ val result = manager.refreshServerConfig()
+ assertInstanceOf(result)
+ val serverConfig = manager.currentServerConfig
+ assertEquals(ServerConfigProvider().getDefaultServerConfig(), serverConfig)
+ }
+
+ @Test
+ fun `given a server config valid, and endpoints not valid urls, then return null`() = runTest {
+ val (_, manager) = Arrangement()
+ .withRestrictions(mapOf(ManagedConfigurationsKeys.DEFAULT_SERVER_URLS.asKey() to validServerConfigJsonWithInvalidEndpoints))
+ .arrange()
+
+ val result = manager.refreshServerConfig()
+ assertInstanceOf(result)
+ val serverConfig = manager.currentServerConfig
+ assertEquals(ServerConfigProvider().getDefaultServerConfig(), serverConfig)
+ }
+
+ @Test
+ fun `given a valid SSO code, then parse it to a corresponding ManagedSSOConfig`() = runTest {
+ val expected = "fd994b20-b9af-11ec-ae36-00163e9b33ca"
+ val (_, manager) = Arrangement()
+ .withRestrictions(mapOf(ManagedConfigurationsKeys.SSO_CODE.asKey() to validSSOCodeConfigJson))
+ .arrange()
+
+ val result = manager.refreshSSOCodeConfig()
+ assertInstanceOf(result)
+ val ssoCode = manager.currentSSOCodeConfig
+
+ assertEquals(expected, ssoCode)
+ }
+
+ @Test
+ fun `given an invalid SSO code, then return empty string`() = runTest {
+ val (_, manager) = Arrangement()
+ .withRestrictions(mapOf(ManagedConfigurationsKeys.SSO_CODE.asKey() to invalidSSOCodeConfigJson))
+ .arrange()
+
+ val result = manager.refreshSSOCodeConfig()
+ assertInstanceOf(result)
+ val ssoCode = manager.currentSSOCodeConfig
+ assertEquals(String.EMPTY, ssoCode)
+ }
+
+ @Test
+ fun `given no SSO code restriction, then return empty string`() = runTest {
+ val (_, manager) = Arrangement()
+ .withRestrictions(emptyMap())
+ .arrange()
+
+ val result = manager.refreshSSOCodeConfig()
+ assertInstanceOf(result)
+ val ssoCode = manager.currentSSOCodeConfig
+ assertEquals(String.EMPTY, ssoCode)
+ }
+
+ @Test
+ fun `given no server config restriction, then return default server config`() = runTest {
+ val (_, manager) = Arrangement()
+ .withRestrictions(emptyMap())
+ .arrange()
+
+ val result = manager.refreshServerConfig()
+ assertInstanceOf(result)
+ val serverConfig = manager.currentServerConfig
+ assertEquals(ServerConfigProvider().getDefaultServerConfig(), serverConfig)
+ }
+
+ private class Arrangement {
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ fun withRestrictions(restrictions: Map) = apply {
+ val restrictionsManager =
+ context.getSystemService(Context.RESTRICTIONS_SERVICE) as RestrictionsManager
+ val shadowRestrictionsManager = Shadows.shadowOf(restrictionsManager)
+ shadowRestrictionsManager.setApplicationRestrictions(
+ Bundle().apply {
+ restrictions.forEach { (key, value) ->
+ putString(key, value)
+ }
+ }
+ )
+ }
+
+ fun arrange() = this to ManagedConfigurationsManagerImpl(
+ context = context,
+ serverConfigProvider = ServerConfigProvider(),
+ dispatchers = TestDispatcherProvider()
+ )
+ }
+
+ companion object {
+ val validServerConfigJson = """
+ {
+ "endpoints": {
+ "accountsURL": "https://account.anta.wire.link",
+ "backendURL": "https://nginz-https.anta.wire.link",
+ "backendWSURL": "https://nginz-ssl.anta.wire.link",
+ "blackListURL": "https://disallowed-clients.anta.wire.link",
+ "teamsURL": "https://teams.anta.wire.link",
+ "websiteURL": "https://wire.com"
+ },
+ "title": "anta.wire.link"
+ }
+ """.trimIndent()
+
+ val validServerConfigJsonWithInvalidEndpoints = """
+ {
+ "endpoints": {
+ "accountsURL": "account.anta.wire.link",
+ "backendURL": "nginz-https.anta.wire.link",
+ "backendWSURL": "nginz-ssl.anta.wire.",
+ "blackListURL": "https://disallowed-clients.anta.wire.link",
+ "teamsURL": "https://teams.anta.wire.link",
+ "websiteURL": "https://wire.com"
+ },
+ "title": "anta.wire.link"
+ }
+ """.trimIndent()
+
+ val validSSOCodeConfigJson = """
+ {
+ "sso_code": "fd994b20-b9af-11ec-ae36-00163e9b33ca"
+ }
+ """.trimIndent()
+
+ val invalidSSOCodeConfigJson = """
+ {
+ "sso_code": "invalid-sso-code"
+ }
+ """.trimIndent()
+ }
+}
diff --git a/app/src/test/kotlin/com/wire/android/emm/ManagedConfigurationsReceiverTest.kt b/app/src/test/kotlin/com/wire/android/emm/ManagedConfigurationsReceiverTest.kt
new file mode 100644
index 00000000000..96de544b576
--- /dev/null
+++ b/app/src/test/kotlin/com/wire/android/emm/ManagedConfigurationsReceiverTest.kt
@@ -0,0 +1,206 @@
+/*
+ * Wire
+ * Copyright (C) 2025 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.emm
+
+import android.app.Application
+import android.content.Context
+import android.content.Intent
+import androidx.test.core.app.ApplicationProvider
+import com.wire.android.config.TestDispatcherProvider
+import com.wire.android.util.EMPTY
+import io.mockk.coEvery
+import io.mockk.coVerify
+import io.mockk.mockk
+import kotlinx.coroutines.test.advanceUntilIdle
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class)
+class ManagedConfigurationsReceiverTest {
+
+ @Test
+ fun `given ACTION_APPLICATION_RESTRICTIONS_CHANGED intent, when onReceive is called, then refresh both server and SSO configs`() =
+ runTest {
+ val (arrangement, receiver) = Arrangement()
+ .withIntent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED)
+ .arrange()
+
+ receiver.onReceive(arrangement.context, arrangement.intent)
+ advanceUntilIdle()
+
+ coVerify(exactly = 1) { arrangement.managedConfigurationsManager.refreshServerConfig() }
+ coVerify(exactly = 1) {
+ arrangement.managedConfigurationsReporter.reportAppliedState(
+ eq(ManagedConfigurationsKeys.DEFAULT_SERVER_URLS.asKey()),
+ any(),
+ any()
+ )
+ }
+ coVerify(exactly = 1) { arrangement.managedConfigurationsManager.refreshSSOCodeConfig() }
+ coVerify(exactly = 1) {
+ arrangement.managedConfigurationsReporter.reportAppliedState(
+ eq(ManagedConfigurationsKeys.SSO_CODE.asKey()),
+ any(),
+ any()
+ )
+ }
+ }
+
+ @Test
+ fun `given ACTION_APPLICATION_RESTRICTIONS_CHANGED intent, when onReceive is called and refresh server returns an error, then notify`() =
+ runTest {
+ val (arrangement, receiver) = Arrangement()
+ .withIntent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED)
+ .withRefreshServerConfigResult(ServerConfigResult.Failure("Test error"))
+ .arrange()
+
+ receiver.onReceive(arrangement.context, arrangement.intent)
+ advanceUntilIdle()
+
+ coVerify(exactly = 1) { arrangement.managedConfigurationsManager.refreshServerConfig() }
+ coVerify(exactly = 1) {
+ arrangement.managedConfigurationsReporter.reportErrorState(
+ eq(ManagedConfigurationsKeys.DEFAULT_SERVER_URLS.asKey()),
+ eq("Test error"),
+ any()
+ )
+ }
+ }
+
+ @Test
+ fun `given ACTION_APPLICATION_RESTRICTIONS_CHANGED intent, when onReceive is called and refresh sso code returns an error, then notify`() =
+ runTest {
+ val (arrangement, receiver) = Arrangement()
+ .withIntent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED)
+ .withRefreshSSOConfigResult(SSOCodeConfigResult.Failure("Test error"))
+ .arrange()
+
+ receiver.onReceive(arrangement.context, arrangement.intent)
+ advanceUntilIdle()
+
+ coVerify(exactly = 1) { arrangement.managedConfigurationsManager.refreshSSOCodeConfig() }
+ coVerify(exactly = 1) {
+ arrangement.managedConfigurationsReporter.reportErrorState(
+ eq(ManagedConfigurationsKeys.SSO_CODE.asKey()),
+ eq("Test error"),
+ any()
+ )
+ }
+ }
+
+ @Test
+ fun `given ACTION_APPLICATION_RESTRICTIONS_CHANGED intent, when onReceive is called with Empty Server Config, then notify cleared`() =
+ runTest {
+ val (arrangement, receiver) = Arrangement()
+ .withIntent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED)
+ .withRefreshServerConfigResult(ServerConfigResult.Empty)
+ .arrange()
+
+ receiver.onReceive(arrangement.context, arrangement.intent)
+ advanceUntilIdle()
+
+ coVerify(exactly = 1) { arrangement.managedConfigurationsManager.refreshServerConfig() }
+ coVerify(exactly = 1) {
+ arrangement.managedConfigurationsReporter.reportAppliedState(
+ eq(ManagedConfigurationsKeys.DEFAULT_SERVER_URLS.asKey()),
+ eq("Managed configuration cleared"),
+ eq(String.EMPTY)
+ )
+ }
+ }
+
+ @Test
+ fun `given ACTION_APPLICATION_RESTRICTIONS_CHANGED intent, when onReceive is called with Empty SSO Config, then notify cleared`() =
+ runTest {
+ val (arrangement, receiver) = Arrangement()
+ .withIntent(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED)
+ .withRefreshSSOConfigResult(SSOCodeConfigResult.Empty)
+ .arrange()
+
+ receiver.onReceive(arrangement.context, arrangement.intent)
+ advanceUntilIdle()
+
+ coVerify(exactly = 1) { arrangement.managedConfigurationsManager.refreshSSOCodeConfig() }
+ coVerify(exactly = 1) {
+ arrangement.managedConfigurationsReporter.reportAppliedState(
+ eq(ManagedConfigurationsKeys.SSO_CODE.asKey()),
+ eq("Managed configuration cleared"),
+ eq(String.EMPTY)
+ )
+ }
+ }
+
+ @Test
+ fun `given unexpected intent action, when onReceive is called, then do not refresh configurations`() =
+ runTest {
+ val (arrangement, receiver) = Arrangement()
+ .withIntent("com.wire.android.UNEXPECTED_ACTION")
+ .arrange()
+
+ receiver.onReceive(arrangement.context, arrangement.intent)
+ advanceUntilIdle()
+
+ coVerify(exactly = 0) { arrangement.managedConfigurationsManager.refreshServerConfig() }
+ coVerify(exactly = 0) { arrangement.managedConfigurationsManager.refreshSSOCodeConfig() }
+ }
+
+ @Test
+ fun `given null intent action, when onReceive is called, then do not refresh configurations`() =
+ runTest {
+ val (arrangement, receiver) = Arrangement()
+ .withIntent(null)
+ .arrange()
+
+ receiver.onReceive(arrangement.context, arrangement.intent)
+ advanceUntilIdle()
+
+ coVerify(exactly = 0) { arrangement.managedConfigurationsManager.refreshServerConfig() }
+ coVerify(exactly = 0) { arrangement.managedConfigurationsManager.refreshSSOCodeConfig() }
+ }
+
+ private class Arrangement {
+
+ val context: Context = ApplicationProvider.getApplicationContext()
+ val managedConfigurationsManager: ManagedConfigurationsManager = mockk(relaxed = true)
+ val managedConfigurationsReporter: ManagedConfigurationsReporter = mockk(relaxed = true)
+ private val dispatchers = TestDispatcherProvider()
+ lateinit var intent: Intent
+
+ fun withIntent(action: String?) = apply {
+ intent = if (action != null) Intent(action) else Intent()
+ }
+
+ fun withRefreshServerConfigResult(result: ServerConfigResult) = apply {
+ coEvery { managedConfigurationsManager.refreshServerConfig() } returns result
+ }
+
+ fun withRefreshSSOConfigResult(result: SSOCodeConfigResult) = apply {
+ coEvery { managedConfigurationsManager.refreshSSOCodeConfig() } returns result
+ }
+
+ fun arrange() = this to ManagedConfigurationsReceiver(
+ managedConfigurationsManager,
+ managedConfigurationsReporter,
+ dispatchers
+ )
+ }
+}
diff --git a/app/src/test/kotlin/com/wire/android/emm/ManagedSSOCodeConfigTest.kt b/app/src/test/kotlin/com/wire/android/emm/ManagedSSOCodeConfigTest.kt
new file mode 100644
index 00000000000..8dd52c2266f
--- /dev/null
+++ b/app/src/test/kotlin/com/wire/android/emm/ManagedSSOCodeConfigTest.kt
@@ -0,0 +1,73 @@
+/*
+ * Wire
+ * Copyright (C) 2025 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.emm
+
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class ManagedSSOCodeConfigTest {
+
+ @Test
+ fun `given a valid UUID SSO code, then isValid should return true`() {
+ val validSSOCode = "fd994b20-b9af-11ec-ae36-00163e9b33ca"
+ val config = ManagedSSOCodeConfig(validSSOCode)
+
+ assertTrue(config.isValid)
+ }
+
+ @Test
+ fun `given an invalid SSO code, then isValid should return false`() {
+ val invalidSSOCode = "invalid-sso-code"
+ val config = ManagedSSOCodeConfig(invalidSSOCode)
+
+ assertFalse(config.isValid)
+ }
+
+ @Test
+ fun `given an empty SSO code, then isValid should return false`() {
+ val emptySSOCode = ""
+ val config = ManagedSSOCodeConfig(emptySSOCode)
+
+ assertFalse(config.isValid)
+ }
+
+ @Test
+ fun `given a partial UUID SSO code, then isValid should return false`() {
+ val partialUUID = "fd994b20-b9af-11ec"
+ val config = ManagedSSOCodeConfig(partialUUID)
+
+ assertFalse(config.isValid)
+ }
+
+ @Test
+ fun `given a UUID with incorrect format, then isValid should return false`() {
+ val incorrectFormat = "fd994b20b9af11ecae3600163e9b33ca"
+ val config = ManagedSSOCodeConfig(incorrectFormat)
+
+ assertFalse(config.isValid)
+ }
+
+ @Test
+ fun `given a UUID in uppercase, then isValid should return true`() {
+ val uppercaseUUID = "FD994B20-B9AF-11EC-AE36-00163E9B33CA"
+ val config = ManagedSSOCodeConfig(uppercaseUUID)
+
+ assertTrue(config.isValid)
+ }
+}
diff --git a/app/src/test/kotlin/com/wire/android/emm/ManagedServerLinksTest.kt b/app/src/test/kotlin/com/wire/android/emm/ManagedServerLinksTest.kt
new file mode 100644
index 00000000000..224725a8b65
--- /dev/null
+++ b/app/src/test/kotlin/com/wire/android/emm/ManagedServerLinksTest.kt
@@ -0,0 +1,143 @@
+/*
+ * Wire
+ * Copyright (C) 2025 Wire Swiss GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see http://www.gnu.org/licenses/.
+ */
+package com.wire.android.emm
+
+import android.app.Application
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+@RunWith(RobolectricTestRunner::class)
+@Config(application = Application::class)
+class ManagedServerLinksTest {
+
+ @Test
+ fun `given all valid URLs, then isValid should return true`() {
+ val config = ManagedServerLinks(
+ accountsURL = "https://accounts.wire.com",
+ backendURL = "https://backend.wire.com",
+ backendWSURL = "https://ws.wire.com",
+ blackListURL = "https://blacklist.wire.com",
+ teamsURL = "https://teams.wire.com",
+ websiteURL = "https://wire.com"
+ )
+
+ assertTrue(config.isValid)
+ }
+
+ @Test
+ fun `given invalid accountsURL, then isValid should return false`() {
+ val config = ManagedServerLinks(
+ accountsURL = "invalid-url",
+ backendURL = "https://backend.wire.com",
+ backendWSURL = "https://ws.wire.com",
+ blackListURL = "https://blacklist.wire.com",
+ teamsURL = "https://teams.wire.com",
+ websiteURL = "https://wire.com"
+ )
+
+ assertFalse(config.isValid)
+ }
+
+ @Test
+ fun `given invalid backendURL, then isValid should return false`() {
+ val config = ManagedServerLinks(
+ accountsURL = "https://accounts.wire.com",
+ backendURL = "not-a-url",
+ backendWSURL = "https://ws.wire.com",
+ blackListURL = "https://blacklist.wire.com",
+ teamsURL = "https://teams.wire.com",
+ websiteURL = "https://wire.com"
+ )
+
+ assertFalse(config.isValid)
+ }
+
+ @Test
+ fun `given invalid backendWSURL, then isValid should return false`() {
+ val config = ManagedServerLinks(
+ accountsURL = "https://accounts.wire.com",
+ backendURL = "https://backend.wire.com",
+ backendWSURL = "invalid",
+ blackListURL = "https://blacklist.wire.com",
+ teamsURL = "https://teams.wire.com",
+ websiteURL = "https://wire.com"
+ )
+
+ assertFalse(config.isValid)
+ }
+
+ @Test
+ fun `given invalid blackListURL, then isValid should return false`() {
+ val config = ManagedServerLinks(
+ accountsURL = "https://accounts.wire.com",
+ backendURL = "https://backend.wire.com",
+ backendWSURL = "https://ws.wire.com",
+ blackListURL = "",
+ teamsURL = "https://teams.wire.com",
+ websiteURL = "https://wire.com"
+ )
+
+ assertFalse(config.isValid)
+ }
+
+ @Test
+ fun `given invalid teamsURL, then isValid should return false`() {
+ val config = ManagedServerLinks(
+ accountsURL = "https://accounts.wire.com",
+ backendURL = "https://backend.wire.com",
+ backendWSURL = "https://ws.wire.com",
+ blackListURL = "https://blacklist.wire.com",
+ teamsURL = "ftp://teams.wire.com",
+ websiteURL = "https://wire.com"
+ )
+
+ assertFalse(config.isValid)
+ }
+
+ @Test
+ fun `given invalid websiteURL, then isValid should return false`() {
+ val config = ManagedServerLinks(
+ accountsURL = "https://accounts.wire.com",
+ backendURL = "https://backend.wire.com",
+ backendWSURL = "https://ws.wire.com",
+ blackListURL = "https://blacklist.wire.com",
+ teamsURL = "https://teams.wire.com",
+ websiteURL = "wire"
+ )
+
+ assertFalse(config.isValid)
+ }
+
+ @Test
+ fun `given all empty URLs, then isValid should return false`() {
+ val config = ManagedServerLinks(
+ accountsURL = "",
+ backendURL = "",
+ backendWSURL = "",
+ blackListURL = "",
+ teamsURL = "",
+ websiteURL = ""
+ )
+
+ assertFalse(config.isValid)
+ }
+}
diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/LoginViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/LoginViewModelTest.kt
index 81c9e73e843..70ed7b27b3e 100644
--- a/app/src/test/kotlin/com/wire/android/ui/authentication/LoginViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/authentication/LoginViewModelTest.kt
@@ -26,8 +26,8 @@ import com.wire.android.ui.authentication.login.LoginNavArgs
import com.wire.android.ui.authentication.login.LoginPasswordPath
import com.wire.android.ui.authentication.login.LoginViewModel
import com.wire.android.ui.navArgs
-import com.wire.android.util.newServerConfig
import com.wire.kalium.logic.CoreLogic
+import com.wire.kalium.logic.configuration.server.ServerConfig
import com.wire.kalium.logic.data.id.QualifiedID
import com.wire.kalium.logic.data.id.QualifiedIdMapper
import io.mockk.MockKAnnotations
@@ -63,14 +63,14 @@ class LoginViewModelTest {
MockKAnnotations.init(this)
every { qualifiedIdMapper.fromStringToQualifiedID(any()) } returns QualifiedID("", "")
every { savedStateHandle.navArgs() } returns LoginNavArgs(
- loginPasswordPath =
- LoginPasswordPath(newServerConfig(1).links)
+ loginPasswordPath = LoginPasswordPath(ServerConfig.STAGING)
)
loginViewModel = LoginViewModel(
savedStateHandle,
clientScopeProviderFactory,
userDataStoreProvider,
- coreLogic
+ coreLogic,
+ ServerConfig.STAGING
)
}
}
diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModelTest.kt
index d2257b9132e..b2291b68304 100644
--- a/app/src/test/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/authentication/create/details/CreateAccountDetailsViewModelTest.kt
@@ -20,12 +20,15 @@ package com.wire.android.ui.authentication.create.details
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
import androidx.lifecycle.SavedStateHandle
+import com.wire.android.assertions.shouldBeEqualTo
+import com.wire.android.assertions.shouldBeInstanceOf
import com.wire.android.config.CoroutineTestExtension
import com.wire.android.config.NavigationTestExtension
import com.wire.android.config.SnapshotExtension
import com.wire.android.ui.authentication.create.common.CreateAccountFlowType
import com.wire.android.ui.authentication.create.common.CreateAccountNavArgs
import com.wire.android.ui.navArgs
+import com.wire.kalium.logic.configuration.server.ServerConfig
import com.wire.kalium.logic.feature.auth.ValidatePasswordResult
import com.wire.kalium.logic.feature.auth.ValidatePasswordUseCase
import io.mockk.MockKAnnotations
@@ -35,8 +38,6 @@ import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
-import com.wire.android.assertions.shouldBeEqualTo
-import com.wire.android.assertions.shouldBeInstanceOf
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@@ -107,6 +108,6 @@ class CreateAccountDetailsViewModelTest {
coEvery { validatePasswordUseCase(any()) } returns result
}
- fun arrange() = this to CreateAccountDetailsViewModel(savedStateHandle, validatePasswordUseCase)
+ fun arrange() = this to CreateAccountDetailsViewModel(savedStateHandle, validatePasswordUseCase, ServerConfig.STAGING)
}
}
diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModelTest.kt
index 3084086e42a..df3c51878d9 100644
--- a/app/src/test/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/authentication/create/email/CreateAccountEmailViewModelTest.kt
@@ -19,6 +19,8 @@
package com.wire.android.ui.authentication.create.email
import androidx.lifecycle.SavedStateHandle
+import com.wire.android.assertions.shouldBeEqualTo
+import com.wire.android.assertions.shouldBeInstanceOf
import com.wire.android.config.CoroutineTestExtension
import com.wire.android.config.NavigationTestExtension
import com.wire.android.config.SnapshotExtension
@@ -26,6 +28,7 @@ import com.wire.android.ui.authentication.create.common.CreateAccountFlowType
import com.wire.android.ui.authentication.create.common.CreateAccountNavArgs
import com.wire.android.ui.navArgs
import com.wire.kalium.logic.CoreLogic
+import com.wire.kalium.logic.configuration.server.ServerConfig
import com.wire.kalium.logic.feature.auth.AuthenticationScope
import com.wire.kalium.logic.feature.auth.ValidateEmailUseCase
import com.wire.kalium.logic.feature.auth.autoVersioningAuth.AutoVersionAuthScopeUseCase
@@ -38,8 +41,6 @@ import io.mockk.impl.annotations.MockK
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
-import com.wire.android.assertions.shouldBeEqualTo
-import com.wire.android.assertions.shouldBeInstanceOf
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@@ -106,6 +107,6 @@ class CreateAccountEmailViewModelTest {
coEvery { requestActivationCodeUseCase(any()) } returns result
}
- fun arrange() = this to CreateAccountEmailViewModel(savedStateHandle, validateEmailUseCase, coreLogic)
+ fun arrange() = this to CreateAccountEmailViewModel(savedStateHandle, validateEmailUseCase, coreLogic, ServerConfig.STAGING)
}
}
diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt
index 5f037e26274..dcf6adb7b37 100644
--- a/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt
@@ -862,7 +862,8 @@ class LoginEmailViewModelTest {
userDataStoreProvider,
coreLogic,
countdownTimer,
- dispatcherProvider
+ dispatcherProvider,
+ ServerConfig.STAGING
).also { it.autoLoginWhenFullCodeEntered = true }
fun withLoginReturning(result: AuthenticationResult) = apply {
diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt
index 518f566bc81..d48a47f6eeb 100644
--- a/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt
@@ -798,13 +798,14 @@ class LoginSSOViewModelTest {
}
fun arrange() = this to LoginSSOViewModel(
- savedStateHandle,
- addAuthenticatedUserUseCase,
- validateEmailUseCase,
- coreLogic,
- clientScopeProviderFactory,
- userDataStoreProvider,
- ssoExtension
+ savedStateHandle = savedStateHandle,
+ addAuthenticatedUser = addAuthenticatedUserUseCase,
+ validateEmailUseCase = validateEmailUseCase,
+ coreLogic = coreLogic,
+ clientScopeProviderFactory = clientScopeProviderFactory,
+ userDataStoreProvider = userDataStoreProvider,
+ serverConfig = SERVER_CONFIG.links,
+ ssoExtension = ssoExtension,
)
}
diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModelTest.kt
index d00db70e37d..0d701877dfb 100644
--- a/app/src/test/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/authentication/welcome/WelcomeViewModelTest.kt
@@ -24,6 +24,7 @@ import com.wire.android.config.NavigationTestExtension
import com.wire.android.config.mockUri
import com.wire.android.ui.navArgs
import com.wire.android.util.newServerConfig
+import com.wire.kalium.logic.configuration.server.ServerConfig
import com.wire.kalium.logic.feature.session.GetAllSessionsResult
import com.wire.kalium.logic.feature.session.GetSessionsUseCase
import io.mockk.MockKAnnotations
@@ -53,6 +54,6 @@ class WelcomeViewModelTest {
val authServer = newServerConfig(1)
every { savedStateHandle.navArgs() } returns WelcomeNavArgs(authServer.links)
coEvery { getSessions() } returns GetAllSessionsResult.Success(listOf())
- welcomeViewModel = WelcomeViewModel(savedStateHandle, getSessions)
+ welcomeViewModel = WelcomeViewModel(savedStateHandle, getSessions, ServerConfig.STAGING)
}
}
diff --git a/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModelTest.kt
index 4e7577b853c..b24f26fb0a5 100644
--- a/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModelTest.kt
@@ -19,6 +19,7 @@ import com.wire.android.ui.authentication.login.sso.LoginSSOViewModelExtension
import com.wire.android.ui.authentication.login.sso.SSOUrlConfig
import com.wire.android.ui.navArgs
import com.wire.android.ui.newauthentication.login.ValidateEmailOrSSOCodeUseCase.Result.ValidEmail
+import com.wire.android.util.EMPTY
import com.wire.android.util.deeplink.DeepLinkResult
import com.wire.android.util.deeplink.SSOFailureCodes
import com.wire.android.util.newServerConfig
@@ -50,6 +51,7 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertInstanceOf
import org.junit.jupiter.api.extension.ExtendWith
+@Suppress("LargeClass")
@OptIn(ExperimentalCoroutinesApi::class)
@ExtendWith(CoroutineTestExtension::class, SnapshotExtension::class, NavigationTestExtension::class)
class NewLoginViewModelTest {
@@ -542,7 +544,7 @@ class NewLoginViewModelTest {
viewModel.getEnterpriseLoginFlow(email)
advanceUntilIdle()
- expectNoEvents()
+ expectNoEvents()
assertEquals(NewLoginFlowState.Error.DialogError.GenericError(failure), viewModel.state.flowState)
}
}
@@ -557,7 +559,7 @@ class NewLoginViewModelTest {
viewModel.getEnterpriseLoginFlow(email)
advanceUntilIdle()
- expectNoEvents()
+ expectNoEvents()
assertEquals(NewLoginFlowState.Error.DialogError.ServerVersionNotSupported, viewModel.state.flowState)
}
}
@@ -756,6 +758,22 @@ class NewLoginViewModelTest {
}
}
+ fun withDefaultSSOCodeConfig(ssoCode: String) = apply {
+ defaultSSOCodeConfig = ssoCode
+ }
+
+ fun withCustomServerConfigDeepLink() = apply {
+ every {
+ savedStateHandle.navArgs()
+ } returns LoginNavArgs(
+ loginPasswordPath = LoginPasswordPath(
+ customServerConfig = ServerConfig.STAGING
+ )
+ )
+ }
+
+ private var defaultSSOCodeConfig: String = String.EMPTY
+
fun arrange() = this to NewLoginViewModel(
validateEmailOrSSOCodeUseCase,
coreLogic,
@@ -764,7 +782,9 @@ class NewLoginViewModelTest {
userDataStoreProvider,
loginViewModelExtension,
loginSSOViewModelExtension,
- dispatchers
+ dispatchers,
+ ServerConfig.STAGING,
+ defaultSSOCodeConfig
)
}
@@ -883,6 +903,55 @@ class NewLoginViewModelTest {
assertEquals(userInput, viewModel.userIdentifierTextState.text.toString())
}
+ @Test
+ fun `given managed SSO code config provided, when initializing view model without prefilled user, then pre-fill SSO code`() =
+ runTest(dispatchers.main()) {
+ val managedSSOCode = "fd994b20-b9af-11ec-ae36-00163e9b33ca"
+ val (arrangement, viewModel) = Arrangement()
+ .withEmptyUserIdentifierAndNoPreFilledIdentifier()
+ .withDefaultSSOCodeConfig(managedSSOCode)
+ .arrange()
+
+ assertEquals("wire-$managedSSOCode", viewModel.userIdentifierTextState.text.toString())
+ }
+
+ @Test
+ fun `given managed SSO code config provided, when initializing with prefilled user, then use prefilled user not SSO code`() =
+ runTest(dispatchers.main()) {
+ val managedSSOCode = "fd994b20-b9af-11ec-ae36-00163e9b33ca"
+ val preFilledUser = "prefilled@user.com"
+ val (arrangement, viewModel) = Arrangement()
+ .withPreFilledUserIdentifier(preFilledUser)
+ .withDefaultSSOCodeConfig(managedSSOCode)
+ .arrange()
+
+ assertEquals(preFilledUser, viewModel.userIdentifierTextState.text.toString())
+ }
+
+ @Test
+ fun `given managed SSO code config provided, when initializing with custom server deep link, then do not use SSO code`() =
+ runTest(dispatchers.main()) {
+ val managedSSOCode = "fd994b20-b9af-11ec-ae36-00163e9b33ca"
+ val (arrangement, viewModel) = Arrangement()
+ .withEmptyUserIdentifierAndNoPreFilledIdentifier()
+ .withCustomServerConfigDeepLink()
+ .withDefaultSSOCodeConfig(managedSSOCode)
+ .arrange()
+
+ assertEquals("", viewModel.userIdentifierTextState.text.toString())
+ }
+
+ @Test
+ fun `given empty managed SSO code config, when initializing view model, then do not pre-fill SSO code`() =
+ runTest(dispatchers.main()) {
+ val (arrangement, viewModel) = Arrangement()
+ .withEmptyUserIdentifierAndNoPreFilledIdentifier()
+ .withDefaultSSOCodeConfig("")
+ .arrange()
+
+ assertEquals("", viewModel.userIdentifierTextState.text.toString())
+ }
+
@Test
fun `when onDismissDialog is called, then reset state to default`() =
runTest(dispatchers.main()) {
diff --git a/app/src/test/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModelTest.kt
index 1ae1b8c62b4..d6797d9bac3 100644
--- a/app/src/test/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/registration/details/CreateAccountDataDetailViewModelTest.kt
@@ -12,6 +12,7 @@ import com.wire.android.ui.authentication.create.common.CreateAccountDataNavArgs
import com.wire.android.ui.authentication.create.common.UserRegistrationInfo
import com.wire.android.ui.navArgs
import com.wire.kalium.logic.CoreLogic
+import com.wire.kalium.logic.configuration.server.ServerConfig
import com.wire.kalium.logic.feature.auth.AuthenticationScope
import com.wire.kalium.logic.feature.auth.ValidateEmailUseCase
import com.wire.kalium.logic.feature.auth.ValidatePasswordResult
@@ -222,7 +223,8 @@ class CreateAccountDataDetailViewModelTest {
validatePassword = validatePasswordUseCase,
coreLogic = coreLogic,
registrationAnalyticsManager = anonymousAnalyticsManager,
- globalDataStore = globalDataStore
+ globalDataStore = globalDataStore,
+ defaultServerConfig = ServerConfig.STAGING,
)
}
}
diff --git a/app/src/test/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModelTest.kt
index 2969c370914..45e360ac635 100644
--- a/app/src/test/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModelTest.kt
+++ b/app/src/test/kotlin/com/wire/android/ui/registration/selector/CreateAccountSelectorViewModelTest.kt
@@ -60,6 +60,6 @@ class CreateAccountSelectorViewModelTest {
CreateAccountSelectorNavArgs(ServerConfig.STAGING, email)
}
- fun arrange() = this to CreateAccountSelectorViewModel(globalDataStore, savedStateHandle)
+ fun arrange() = this to CreateAccountSelectorViewModel(globalDataStore, savedStateHandle, ServerConfig.STAGING)
}
}
diff --git a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt
index aebc7bbce5e..4fc5bb24de7 100644
--- a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt
+++ b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt
@@ -52,6 +52,7 @@ enum class FeatureConfigs(val value: String, val configType: ConfigType) {
ENABLE_CROSSPLATFORM_BACKUP("enable_crossplatform_backup", ConfigType.BOOLEAN),
ENABLE_NEW_REGISTRATION("enable_new_registration", ConfigType.BOOLEAN),
MLS_READ_RECEIPTS_ENABLED("mls_read_receipts_enabled", ConfigType.BOOLEAN),
+ EMM_SUPPORT_ENABLED("emm_support_enabled", ConfigType.BOOLEAN),
/**
* Security/Cryptography stuff
diff --git a/default.json b/default.json
index 9bb9bca4920..1c5b962b597 100644
--- a/default.json
+++ b/default.json
@@ -67,7 +67,8 @@
"analytics_enabled": true,
"analytics_app_key": "8ffae535f1836ed5f58fd5c8a11c00eca07c5438",
"analytics_server_url": "https://wire.count.ly/",
- "enable_new_registration": true
+ "enable_new_registration": true,
+ "emm_support_enabled": true
},
"internal": {
"application_id": "com.wire.internal",
@@ -79,7 +80,8 @@
"analytics_app_key": "8ffae535f1836ed5f58fd5c8a11c00eca07c5438",
"analytics_server_url": "https://wire.count.ly/",
"enable_new_registration": true,
- "use_strict_mls_filter": false
+ "use_strict_mls_filter": false,
+ "emm_support_enabled": true
},
"fdroid": {
"application_id": "com.wire",
@@ -148,5 +150,6 @@
"mls_read_receipts_enabled": false,
"is_mls_reset_enabled": true,
"use_strict_mls_filter": true,
- "meetings_enabled": false
+ "meetings_enabled": false,
+ "emm_support_enabled": false
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 8dc6bcb9f96..2beed8110ce 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -46,6 +46,7 @@ androidx-startup = "1.2.0"
androidx-compose-runtime = "1.7.2"
compose-qr = "1.0.1"
amplituda = "2.2.2"
+enterprise-feedback = "1.1.0"
# Compose
composeBom = "2025.09.00"
@@ -192,6 +193,7 @@ androidx-profile-installer = { group = "androidx.profileinstaller", name = "prof
androidx-biometric = { group = "androidx.biometric", name = "biometric", version.ref = "androidx-biometric" }
androidx-startup = { group = "androidx.startup", name = "startup-runtime", version.ref = "androidx-startup" }
androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime", version.ref = "androidx-compose-runtime" }
+enterprise-feedback = { group = "androidx.enterprise", name = "enterprise-feedback", version.ref = "enterprise-feedback" }
# Dependency Injection
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }