diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt index d1aa189674e..8b8ee6e66b3 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityDialogs.kt @@ -55,8 +55,10 @@ import com.wire.android.ui.common.bottomsheet.WireSheetValue import com.wire.android.ui.common.bottomsheet.rememberWireModalSheetState import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WireSecondaryButton -import com.wire.android.ui.common.dialogs.CustomServerDialog -import com.wire.android.ui.common.dialogs.CustomServerDialogState +import com.wire.android.ui.common.dialogs.CustomServerDetailsDialog +import com.wire.android.ui.common.dialogs.CustomServerDetailsDialogState +import com.wire.android.ui.common.dialogs.CustomServerInvalidJsonDialog +import com.wire.android.ui.common.dialogs.CustomServerInvalidJsonDialogState import com.wire.android.ui.common.dialogs.MaxAccountAllowedDialogContent import com.wire.android.ui.common.dimensions import com.wire.android.ui.common.wireDialogPropertiesBuilder @@ -244,12 +246,24 @@ fun CustomBackendDialog( onDismiss: () -> Unit, onConfirm: () -> Unit ) { - if (globalAppState.customBackendDialog != null) { - CustomServerDialog( - serverLinks = globalAppState.customBackendDialog.serverLinks, - onDismiss = onDismiss, - onConfirm = onConfirm - ) + when (globalAppState.customBackendDialog) { + is CustomServerDetailsDialogState -> { + CustomServerDetailsDialog( + serverLinks = globalAppState.customBackendDialog.serverLinks, + onDismiss = onDismiss, + onConfirm = onConfirm + ) + } + + is CustomServerInvalidJsonDialogState -> { + CustomServerInvalidJsonDialog( + onDismiss = onDismiss + ) + } + + else -> { + // nop + } } } @@ -562,10 +576,13 @@ fun PreviewCustomBackendDialog() { WireTheme { CustomBackendDialog( GlobalAppState( - customBackendDialog = CustomServerDialogState( + customBackendDialog = CustomServerDetailsDialogState( ServerConfig.STAGING ) - ), {}, {}) + ), + {}, + {} + ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index 1f3073d4a9e..4fefdb9e261 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -40,7 +40,9 @@ import com.wire.android.feature.SwitchAccountResult import com.wire.android.migration.MigrationManager import com.wire.android.services.ServicesManager import com.wire.android.ui.authentication.devices.model.displayName +import com.wire.android.ui.common.dialogs.CustomServerDetailsDialogState import com.wire.android.ui.common.dialogs.CustomServerDialogState +import com.wire.android.ui.common.dialogs.CustomServerInvalidJsonDialogState import com.wire.android.ui.joinConversation.JoinConversationViaCodeState import com.wire.android.ui.theme.ThemeOption import com.wire.android.util.CurrentScreen @@ -378,10 +380,11 @@ class WireActivityViewModel @Inject constructor( } fun customBackendDialogProceedButtonClicked(onProceed: () -> Unit) { - if (globalAppState.customBackendDialog != null) { + val backendDialogState = globalAppState.customBackendDialog + if (backendDialogState is CustomServerDetailsDialogState) { viewModelScope.launch { authServerConfigProvider.get() - .updateAuthServer(globalAppState.customBackendDialog!!.serverLinks) + .updateAuthServer(backendDialogState.serverLinks) dismissCustomBackendDialog() if (checkNumberOfSessions() >= BuildConfig.MAX_ACCOUNTS) { globalAppState = globalAppState.copy(maxAccountDialog = true) @@ -456,7 +459,6 @@ class WireActivityViewModel @Inject constructor( private suspend fun loadServerConfig(url: String): ServerConfig.Links? = when (val result = getServerConfigUseCase.get().invoke(url)) { is GetServerConfigResult.Success -> result.serverConfigLinks - // TODO: show error message on failure is GetServerConfigResult.Failure.Generic -> { appLogger.e("something went wrong during handling the custom server deep link: ${result.genericFailure}") null @@ -464,13 +466,12 @@ class WireActivityViewModel @Inject constructor( } private suspend fun onCustomServerConfig(result: DeepLinkResult.CustomServerConfig) { - loadServerConfig(result.url)?.let { serverLinks -> - globalAppState = globalAppState.copy( - customBackendDialog = CustomServerDialogState( - serverLinks = serverLinks - ) - ) - } + val customBackendDialogData = loadServerConfig(result.url)?.let { serverLinks -> + CustomServerDetailsDialogState(serverLinks = serverLinks) + } ?: CustomServerInvalidJsonDialogState + globalAppState = globalAppState.copy( + customBackendDialog = customBackendDialogData + ) } private suspend fun onConversationInviteDeepLink( diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt index b91861b35fa..ddc8ece1858 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt @@ -47,7 +47,7 @@ import com.wire.android.ui.authentication.login.LoginErrorDialog import com.wire.android.ui.authentication.login.LoginState import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WirePrimaryButton -import com.wire.android.ui.common.dialogs.CustomServerDialog +import com.wire.android.ui.common.dialogs.CustomServerDetailsDialog import com.wire.android.ui.common.textfield.WireTextField import com.wire.android.ui.common.textfield.WireTextFieldState import com.wire.android.ui.theme.WireTheme @@ -142,7 +142,7 @@ private fun LoginSSOContent( } if (loginSSOState.customServerDialogState != null) { - CustomServerDialog( + CustomServerDetailsDialog( serverLinks = loginSSOState.customServerDialogState.serverLinks, onDismiss = onCustomServerDialogDismiss, onConfirm = onCustomServerDialogConfirm diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOState.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOState.kt index 9b0de0a845d..43e6f626f84 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOState.kt @@ -18,10 +18,10 @@ package com.wire.android.ui.authentication.login.sso import com.wire.android.ui.authentication.login.LoginState -import com.wire.android.ui.common.dialogs.CustomServerDialogState +import com.wire.android.ui.common.dialogs.CustomServerDetailsDialogState data class LoginSSOState( val loginEnabled: Boolean = false, val flowState: LoginState = LoginState.Default, - val customServerDialogState: CustomServerDialogState? = null, + val customServerDialogState: CustomServerDetailsDialogState? = null, ) 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 e8b1c513476..e3d1be22f99 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 @@ -33,7 +33,7 @@ import com.wire.android.di.KaliumCoreLogic import com.wire.android.ui.authentication.login.LoginState import com.wire.android.ui.authentication.login.LoginViewModel import com.wire.android.ui.authentication.login.toLoginError -import com.wire.android.ui.common.dialogs.CustomServerDialogState +import com.wire.android.ui.common.dialogs.CustomServerDetailsDialogState import com.wire.android.ui.common.textfield.textAsFlow import com.wire.android.util.EMPTY import com.wire.android.util.deeplink.DeepLinkResult @@ -190,7 +190,7 @@ class LoginSSOViewModel @Inject constructor( } is DomainLookupUseCase.Result.Success -> { - loginState = loginState.copy(customServerDialogState = CustomServerDialogState(it.serverLinks)) + loginState = loginState.copy(customServerDialogState = CustomServerDetailsDialogState(it.serverLinks)) updateSSOFlowState(LoginState.Default) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerDialog.kt index aa130cbc332..04205ae6d1c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerDialog.kt +++ b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerDialog.kt @@ -49,7 +49,7 @@ import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.configuration.server.ServerConfig @Composable -internal fun CustomServerDialog( +internal fun CustomServerDetailsDialog( serverLinks: ServerConfig.Links, onDismiss: () -> Unit, onConfirm: () -> Unit @@ -163,12 +163,14 @@ private fun CustomServerPropertyInfo( VerticalSpace.x16() } -data class CustomServerDialogState(val serverLinks: ServerConfig.Links) +sealed class CustomServerDialogState + +data class CustomServerDetailsDialogState(val serverLinks: ServerConfig.Links) : CustomServerDialogState() @PreviewMultipleThemes @Composable fun PreviewCustomServerDialog() = WireTheme { - CustomServerDialog( + CustomServerDetailsDialog( serverLinks = ServerConfig.DEFAULT, onConfirm = { }, onDismiss = { } diff --git a/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerInvalidJsonDialog.kt b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerInvalidJsonDialog.kt new file mode 100644 index 00000000000..356d9b12f17 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/common/dialogs/CustomServerInvalidJsonDialog.kt @@ -0,0 +1,56 @@ +/* + * Wire + * Copyright (C) 2024 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.ui.common.dialogs + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import com.wire.android.R +import com.wire.android.ui.common.WireDialog +import com.wire.android.ui.common.WireDialogButtonProperties +import com.wire.android.ui.common.WireDialogButtonType +import com.wire.android.ui.common.button.WireButtonState +import com.wire.android.ui.theme.WireTheme +import com.wire.android.util.ui.PreviewMultipleThemes + +@Composable +internal fun CustomServerInvalidJsonDialog( + onDismiss: () -> Unit +) { + WireDialog( + title = stringResource(R.string.custom_backend_invalid_deeplink_data_title), + text = stringResource(R.string.custom_backend_invalid_deeplink_data_body), + onDismiss = onDismiss, + optionButton1Properties = WireDialogButtonProperties( + onClick = onDismiss, + text = stringResource(id = R.string.label_ok), + type = WireDialogButtonType.Primary, + state = + WireButtonState.Default + ), + ) +} + +data object CustomServerInvalidJsonDialogState : CustomServerDialogState() + +@PreviewMultipleThemes +@Composable +fun PreviewCustomServerInvalidJsonDialog() = WireTheme { + CustomServerInvalidJsonDialog( + onDismiss = { } + ) +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b20e3344f83..39fe5dfc118 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1083,6 +1083,8 @@ In group conversations, the group admin can overwrite this setting. Accounts URL: Website URL: Backend WSURL: + An error occurred + Redirecting to an on-premises backend was not possible, as there was an invalid configuration in the JSON file.\n\nContact your admin or check the deeplink that brought you here. Receiving new messages Text copied to clipboard Logs diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt index 8585edc2ee4..4b492a219b3 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -36,6 +36,8 @@ import com.wire.android.framework.TestClient import com.wire.android.framework.TestUser import com.wire.android.migration.MigrationManager import com.wire.android.services.ServicesManager +import com.wire.android.ui.common.dialogs.CustomServerDetailsDialogState +import com.wire.android.ui.common.dialogs.CustomServerInvalidJsonDialogState import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModelTest import com.wire.android.ui.joinConversation.JoinConversationViaCodeState import com.wire.android.ui.theme.ThemeOption @@ -45,6 +47,7 @@ import com.wire.android.util.deeplink.DeepLinkProcessor import com.wire.android.util.deeplink.DeepLinkResult import com.wire.android.util.newServerConfig import com.wire.kalium.logic.CoreLogic +import com.wire.kalium.logic.NetworkFailure import com.wire.kalium.logic.data.auth.AccountInfo import com.wire.kalium.logic.data.auth.PersistentWebSocketStatus import com.wire.kalium.logic.data.call.Call @@ -89,6 +92,7 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.`should be equal to` +import org.junit.jupiter.api.Assertions.assertInstanceOf import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -133,6 +137,42 @@ class WireActivityViewModelTest { verify(exactly = 1) { arrangement.onDeepLinkResult(result) } } + @Test + fun `given Intent with malformed ServerConfig json, when currentSessions is present, then initialAppState is LOGGED_IN and customBackEndInvalidJson dialog is shown`() = + runTest { + val result = DeepLinkResult.CustomServerConfig("url") + val (arrangement, viewModel) = Arrangement() + .withSomeCurrentSession() + .withDeepLinkResult(result) + .withMalformedServerJson() + .withNoOngoingCall() + .arrange() + + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}, {}, {}, {}) + + assertEquals(InitialAppState.LOGGED_IN, viewModel.initialAppState()) + verify(exactly = 0) { arrangement.onDeepLinkResult(any()) } + assertInstanceOf(CustomServerInvalidJsonDialogState::class.java, viewModel.globalAppState.customBackendDialog) + } + + @Test + fun `given Intent with malformed ServerConfig json, when currentSessions is present, then initialAppState is NOT_LOGGED_IN and customBackEndInvalidJson dialog is shown`() = + runTest { + val result = DeepLinkResult.CustomServerConfig("url") + val (arrangement, viewModel) = Arrangement() + .withNoCurrentSession() + .withDeepLinkResult(result) + .withMalformedServerJson() + .withNoOngoingCall() + .arrange() + + viewModel.handleDeepLink(mockedIntent(), {}, {}, arrangement.onDeepLinkResult, {}, {}, {}, {}) + + assertEquals(InitialAppState.NOT_LOGGED_IN, viewModel.initialAppState()) + verify(exactly = 0) { arrangement.onDeepLinkResult(any()) } + assertInstanceOf(CustomServerInvalidJsonDialogState::class.java, viewModel.globalAppState.customBackendDialog) + } + @Test fun `given Intent with ServerConfig, when currentSession is present, then initialAppState is LOGGED_IN and customBackEnd dialog is shown`() = runTest { @@ -147,7 +187,11 @@ class WireActivityViewModelTest { assertEquals(InitialAppState.LOGGED_IN, viewModel.initialAppState()) verify(exactly = 0) { arrangement.onDeepLinkResult(any()) } - assertEquals(newServerConfig(1).links, viewModel.globalAppState.customBackendDialog!!.serverLinks) + assertInstanceOf(CustomServerDetailsDialogState::class.java, viewModel.globalAppState.customBackendDialog) + assertEquals( + newServerConfig(1).links, + (viewModel.globalAppState.customBackendDialog as CustomServerDetailsDialogState).serverLinks + ) } @Test @@ -163,7 +207,11 @@ class WireActivityViewModelTest { assertEquals(InitialAppState.NOT_LOGGED_IN, viewModel.initialAppState()) verify(exactly = 0) { arrangement.onDeepLinkResult(any()) } - assertEquals(newServerConfig(1).links, viewModel.globalAppState.customBackendDialog!!.serverLinks) + assertInstanceOf(CustomServerDetailsDialogState::class.java, viewModel.globalAppState.customBackendDialog) + assertEquals( + newServerConfig(1).links, + (viewModel.globalAppState.customBackendDialog as CustomServerDetailsDialogState).serverLinks + ) } @Test @@ -820,6 +868,11 @@ class WireActivityViewModelTest { coEvery { coreLogic.getSessionScope(TEST_ACCOUNT_INFO.userId).observeIfE2EIRequiredDuringLogin() } returns flowOf(false) } + fun withMalformedServerJson() = apply { + coEvery { getServerConfigUseCase(any()) } returns + GetServerConfigResult.Failure.Generic(NetworkFailure.NoNetworkConnection(null)) + } + suspend fun withScreenshotCensoringConfig(result: ObserveScreenshotCensoringConfigResult) = apply { coEvery { observeScreenshotCensoringConfigUseCase() } returns flowOf(result) } 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 db95f56468c..c0895b990ec 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 @@ -28,7 +28,7 @@ import com.wire.android.di.AuthServerConfigProvider import com.wire.android.di.ClientScopeProvider import com.wire.android.framework.TestClient import com.wire.android.ui.authentication.login.LoginState -import com.wire.android.ui.common.dialogs.CustomServerDialogState +import com.wire.android.ui.common.dialogs.CustomServerDetailsDialogState import com.wire.android.util.EMPTY import com.wire.android.util.deeplink.DeepLinkResult import com.wire.android.util.deeplink.SSOFailureCodes @@ -423,7 +423,7 @@ class LoginSSOViewModelTest { @Test fun `given backend switch confirmed, then auth server provider is updated`() = runTest { val expected = newServerConfig(2).links - loginViewModel.loginState = loginViewModel.loginState.copy(customServerDialogState = CustomServerDialogState(expected)) + loginViewModel.loginState = loginViewModel.loginState.copy(customServerDialogState = CustomServerDetailsDialogState(expected)) coEvery { fetchSSOSettings.invoke() } returns FetchSSOSettingsUseCase.Result.Success("ssoCode") coEvery { ssoInitiateLoginUseCase(any()) } returns SSOInitiateLoginResult.Success("url") @@ -440,7 +440,7 @@ class LoginSSOViewModelTest { coEvery { fetchSSOSettings.invoke() } returns FetchSSOSettingsUseCase.Result.Success("ssoCode") coEvery { ssoInitiateLoginUseCase(any()) } returns SSOInitiateLoginResult.Success("url") - loginViewModel.loginState = loginViewModel.loginState.copy(customServerDialogState = CustomServerDialogState(expected)) + loginViewModel.loginState = loginViewModel.loginState.copy(customServerDialogState = CustomServerDetailsDialogState(expected)) loginViewModel.onCustomServerDialogConfirm() @@ -457,7 +457,7 @@ class LoginSSOViewModelTest { val expected = newServerConfig(2).links every { validateEmailUseCase(any()) } returns true coEvery { fetchSSOSettings.invoke() } returns FetchSSOSettingsUseCase.Result.Success(null) - loginViewModel.loginState = loginViewModel.loginState.copy(customServerDialogState = CustomServerDialogState(expected)) + loginViewModel.loginState = loginViewModel.loginState.copy(customServerDialogState = CustomServerDetailsDialogState(expected)) loginViewModel.onCustomServerDialogConfirm() @@ -473,7 +473,7 @@ class LoginSSOViewModelTest { val expected = newServerConfig(2).links every { validateEmailUseCase(any()) } returns true coEvery { fetchSSOSettings.invoke() } returns FetchSSOSettingsUseCase.Result.Failure(CoreFailure.Unknown(IOException())) - loginViewModel.loginState = loginViewModel.loginState.copy(customServerDialogState = CustomServerDialogState(expected)) + loginViewModel.loginState = loginViewModel.loginState.copy(customServerDialogState = CustomServerDetailsDialogState(expected)) loginViewModel.onCustomServerDialogConfirm()