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" }