diff --git a/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt index 4d59a2861b2..0b0098912f3 100644 --- a/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/AccountSwitchUseCase.kt @@ -18,12 +18,14 @@ package com.wire.android.feature +import com.wire.android.BuildConfig import com.wire.android.appLogger import com.wire.android.di.ApplicationScope import com.wire.android.di.AuthServerConfigProvider import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.ui.destinations.HomeScreenDestination +import com.wire.android.ui.destinations.NewWelcomeScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination import com.wire.kalium.logic.data.auth.AccountInfo import com.wire.kalium.logic.data.logout.LogoutReason @@ -212,5 +214,10 @@ interface SwitchAccountActions { class NavigationSwitchAccountActions(val navigate: (NavigationCommand) -> Unit) : SwitchAccountActions { override fun switchedToAnotherAccount() = navigate(NavigationCommand(HomeScreenDestination, BackStackMode.CLEAR_WHOLE)) - override fun noOtherAccountToSwitch() = navigate(NavigationCommand(WelcomeScreenDestination, BackStackMode.CLEAR_WHOLE)) + override fun noOtherAccountToSwitch() = navigate( + NavigationCommand( + if (BuildConfig.ENTERPRISE_LOGIN_ENABLED) NewWelcomeScreenDestination else WelcomeScreenDestination, + BackStackMode.CLEAR_WHOLE + ) + ) } 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 3846fd9ba8e..bcc7d3eccbb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -73,7 +73,6 @@ import com.wire.android.navigation.Navigator import com.wire.android.navigation.rememberNavigator import com.wire.android.navigation.style.BackgroundStyle import com.wire.android.navigation.style.BackgroundType -import com.wire.android.ui.authentication.login.LoginNavArgs import com.wire.android.ui.authentication.login.WireAuthBackgroundLayout import com.wire.android.ui.calling.getIncomingCallIntent import com.wire.android.ui.calling.getOutgoingCallIntent @@ -92,6 +91,8 @@ import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.ImportMediaScreenDestination import com.wire.android.ui.destinations.LoginScreenDestination import com.wire.android.ui.destinations.MigrationScreenDestination +import com.wire.android.ui.destinations.NewLoginScreenDestination +import com.wire.android.ui.destinations.NewWelcomeScreenDestination import com.wire.android.ui.destinations.OtherUserProfileScreenDestination import com.wire.android.ui.destinations.SelfDevicesScreenDestination import com.wire.android.ui.destinations.SelfUserProfileScreenDestination @@ -184,7 +185,10 @@ class WireActivity : AppCompatActivity() { appLogger.i("$TAG start destination") val startDestination = when (viewModel.initialAppState()) { InitialAppState.NOT_MIGRATED -> MigrationScreenDestination - InitialAppState.NOT_LOGGED_IN -> WelcomeScreenDestination + InitialAppState.NOT_LOGGED_IN -> when { + BuildConfig.ENTERPRISE_LOGIN_ENABLED -> NewWelcomeScreenDestination + else -> WelcomeScreenDestination + } InitialAppState.ENROLL_E2EI -> E2EIEnrollmentScreenDestination InitialAppState.LOGGED_IN -> HomeScreenDestination } @@ -541,7 +545,12 @@ class WireActivity : AppCompatActivity() { viewModel::dismissCustomBackendDialog, onConfirm = { viewModel.customBackendDialogProceedButtonClicked { - navigate(NavigationCommand(LoginScreenDestination(LoginNavArgs()))) + navigate( + NavigationCommand( + if (BuildConfig.ENTERPRISE_LOGIN_ENABLED) NewWelcomeScreenDestination else WelcomeScreenDestination, + BackStackMode.UPDATE_EXISTED + ) + ) } }, onTryAgain = viewModel::onCustomServerConfig @@ -650,6 +659,10 @@ class WireActivity : AppCompatActivity() { } @Suppress("ComplexCondition", "LongMethod") + /* + * This method is responsible for handling deep links from given intent + * @return true if there was any deep link in given intent to handle, false otherwise + */ private fun handleDeepLink( intent: Intent?, savedInstanceState: Bundle? = null @@ -711,7 +724,10 @@ class WireActivity : AppCompatActivity() { onMigrationLogin = { navigate( NavigationCommand( - LoginScreenDestination(it.userHandle), + when { + BuildConfig.ENTERPRISE_LOGIN_ENABLED -> NewLoginScreenDestination(userHandle = it.userHandle) + else -> LoginScreenDestination(userHandle = it.userHandle) + }, BackStackMode.UPDATE_EXISTED ) ) @@ -735,7 +751,10 @@ class WireActivity : AppCompatActivity() { onSSOLogin = { navigate( NavigationCommand( - LoginScreenDestination(ssoLoginResult = it), + when { + BuildConfig.ENTERPRISE_LOGIN_ENABLED -> NewLoginScreenDestination(ssoLoginResult = it) + else -> LoginScreenDestination(ssoLoginResult = it) + }, BackStackMode.UPDATE_EXISTED ) ) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginErrorDialog.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginErrorDialog.kt new file mode 100644 index 00000000000..5313044d4e6 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginErrorDialog.kt @@ -0,0 +1,152 @@ +/* + * 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.ui.authentication.login + +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.window.DialogProperties +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.util.deeplink.DeepLinkResult +import com.wire.android.util.dialogErrorStrings + +@Composable +fun LoginErrorDialog( + error: LoginState.Error, + onDialogDismiss: () -> Unit, + updateTheApp: () -> Unit, + ssoLoginResult: DeepLinkResult.SSOLogin? = null +) { + val dialogErrorData: LoginDialogErrorData = when (error) { + is LoginState.Error.DialogError.InvalidCredentialsError -> LoginDialogErrorData( + title = stringResource(R.string.login_error_invalid_credentials_title), + body = AnnotatedString(stringResource(R.string.login_error_invalid_credentials_message)), + onDismiss = onDialogDismiss + ) + + is LoginState.Error.DialogError.UserAlreadyExists -> LoginDialogErrorData( + title = stringResource(R.string.login_error_user_already_logged_in_title), + body = AnnotatedString(stringResource(R.string.login_error_user_already_logged_in_message)), + onDismiss = onDialogDismiss + ) + + is LoginState.Error.DialogError.ProxyError -> { + LoginDialogErrorData( + title = stringResource(R.string.error_socket_title), + body = AnnotatedString(stringResource(R.string.error_socket_message)), + onDismiss = onDialogDismiss + ) + } + + is LoginState.Error.DialogError.GenericError -> { + val strings = error.coreFailure.dialogErrorStrings(LocalContext.current.resources) + LoginDialogErrorData( + strings.title, + strings.annotatedMessage, + onDialogDismiss + ) + } + + is LoginState.Error.DialogError.InvalidSSOCodeError -> LoginDialogErrorData( + title = stringResource(R.string.login_error_invalid_credentials_title), + body = AnnotatedString(stringResource(R.string.login_error_invalid_sso_code)), + onDismiss = onDialogDismiss + ) + + is LoginState.Error.DialogError.InvalidSSOCookie -> LoginDialogErrorData( + title = stringResource(R.string.login_sso_error_invalid_cookie_title), + body = AnnotatedString(stringResource(R.string.login_sso_error_invalid_cookie_message)), + onDismiss = onDialogDismiss + ) + + is LoginState.Error.DialogError.SSOResultError -> { + with(ssoLoginResult as DeepLinkResult.SSOLogin.Failure) { + LoginDialogErrorData( + title = stringResource(R.string.sso_error_dialog_title), + body = AnnotatedString(stringResource(R.string.sso_error_dialog_message, this.ssoError.errorCode)), + onDismiss = onDialogDismiss + ) + } + } + + is LoginState.Error.DialogError.ServerVersionNotSupported -> LoginDialogErrorData( + title = stringResource(R.string.api_versioning_server_version_not_supported_title), + body = AnnotatedString(stringResource(R.string.api_versioning_server_version_not_supported_message)), + onDismiss = onDialogDismiss, + actionTextId = R.string.label_close, + dismissOnClickOutside = false + ) + + is LoginState.Error.DialogError.ClientUpdateRequired -> LoginDialogErrorData( + title = stringResource(R.string.api_versioning_client_update_required_title), + body = AnnotatedString(stringResource(R.string.api_versioning_client_update_required_message)), + onDismiss = onDialogDismiss, + actionTextId = R.string.label_update, + onAction = updateTheApp, + dismissOnClickOutside = false + ) + + LoginState.Error.DialogError.Request2FAWithHandle -> { + LoginDialogErrorData( + title = stringResource(R.string.login_error_request_2fa_with_handle_title), + body = AnnotatedString(stringResource(R.string.login_error_request_2fa_with_handle_message)), + onDismiss = onDialogDismiss + ) + } + + LoginState.Error.TextFieldError.InvalidValue, + LoginState.Error.DialogError.PasswordNeededToRegisterClient, + LoginState.Error.TooManyDevicesError -> { + LoginDialogErrorData( + title = stringResource(R.string.error_unknown_title), + body = AnnotatedString(stringResource(R.string.error_unknown_message)), + onDismiss = onDialogDismiss + ) + } + } + + WireDialog( + title = dialogErrorData.title, + text = dialogErrorData.body, + onDismiss = dialogErrorData.onDismiss, + optionButton1Properties = WireDialogButtonProperties( + text = stringResource(dialogErrorData.actionTextId), + onClick = dialogErrorData.onAction, + type = WireDialogButtonType.Primary + ), + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = dialogErrorData.dismissOnClickOutside, + usePlatformDefaultWidth = false + ) + ) +} + +data class LoginDialogErrorData( + val title: String, + val body: AnnotatedString, + val onDismiss: () -> Unit, + @StringRes val actionTextId: Int = R.string.label_ok, + val onAction: () -> Unit = onDismiss, + val dismissOnClickOutside: Boolean = true +) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt index 924fffd6005..3c52b60f3d1 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/LoginScreen.kt @@ -21,16 +21,29 @@ package com.wire.android.ui.authentication.login import androidx.annotation.StringRes import androidx.compose.animation.AnimatedContent import androidx.compose.animation.togetherWith +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.LocalOverscrollConfiguration import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.window.DialogProperties import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.RootNavGraph import com.wire.android.R @@ -38,33 +51,37 @@ import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.WireDestination -import com.wire.android.navigation.style.AuthSlideNavigationAnimation import com.wire.android.navigation.style.TransitionAnimationType import com.wire.android.ui.authentication.ServerTitle import com.wire.android.ui.authentication.login.email.LoginEmailScreen import com.wire.android.ui.authentication.login.email.LoginEmailVerificationCodeScreen import com.wire.android.ui.authentication.login.email.LoginEmailViewModel -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.authentication.login.sso.LoginSSOScreen +import com.wire.android.ui.common.TabItem +import com.wire.android.ui.common.WireTabRow +import com.wire.android.ui.common.calculateCurrentTab import com.wire.android.ui.common.dialogs.FeatureDisabledWithProxyDialogContent import com.wire.android.ui.common.dialogs.FeatureDisabledWithProxyDialogState -import com.wire.android.ui.common.spacers.VerticalSpace +import com.wire.android.ui.common.rememberTopBarElevationState +import com.wire.android.ui.common.scaffold.WireScaffold +import com.wire.android.ui.common.topappbar.NavigationIconType +import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.InitialSyncScreenDestination import com.wire.android.ui.destinations.RemoveDeviceScreenDestination import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography import com.wire.android.util.deeplink.DeepLinkResult -import com.wire.android.util.dialogErrorStrings import com.wire.android.util.ui.PreviewMultipleThemes +import com.wire.android.util.ui.UIText +import kotlinx.coroutines.launch @RootNavGraph @WireDestination( - navArgsDelegate = LoginNavArgs::class, - style = AuthSlideNavigationAnimation::class, + navArgsDelegate = LoginNavArgs::class ) @Composable fun LoginScreen( @@ -72,38 +89,33 @@ fun LoginScreen( loginNavArgs: LoginNavArgs, loginEmailViewModel: LoginEmailViewModel = hiltViewModel() ) { - NewLoginContainer( - title = stringResource(id = R.string.enterprise_login_title), - canNavigateBack = true, - onNavigateBack = navigator::navigateBack - ) { - LoginContent( - onSuccess = { initialSyncCompleted, isE2EIRequired -> - val destination = if (isE2EIRequired) E2EIEnrollmentScreenDestination - else if (initialSyncCompleted) HomeScreenDestination - else InitialSyncScreenDestination - navigator.navigate(NavigationCommand(destination, BackStackMode.CLEAR_WHOLE)) - }, - onRemoveDeviceNeeded = { - navigator.navigate(NavigationCommand(RemoveDeviceScreenDestination, BackStackMode.CLEAR_WHOLE)) - }, - loginEmailViewModel = loginEmailViewModel, - ) - } + LoginContent( + onBackPressed = navigator::navigateBack, + onSuccess = { initialSyncCompleted, isE2EIRequired -> + val destination = if (isE2EIRequired) E2EIEnrollmentScreenDestination + else if (initialSyncCompleted) HomeScreenDestination + else InitialSyncScreenDestination + + navigator.navigate(NavigationCommand(destination, BackStackMode.CLEAR_WHOLE)) + }, + onRemoveDeviceNeeded = { + navigator.navigate(NavigationCommand(RemoveDeviceScreenDestination, BackStackMode.CLEAR_WHOLE)) + }, + loginEmailViewModel = loginEmailViewModel, + ssoLoginResult = loginNavArgs.ssoLoginResult + ) } @Composable private fun LoginContent( + onBackPressed: () -> Unit, onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, onRemoveDeviceNeeded: () -> Unit, loginEmailViewModel: LoginEmailViewModel, + ssoLoginResult: DeepLinkResult.SSOLogin? ) { - Column( - modifier = Modifier - .wrapContentHeight() - .fillMaxWidth() - ) { + Column(modifier = Modifier.fillMaxSize()) { /* TODO: we can change it to be a nested navigation graph when Compose Destinations 2.0 is released, right now it's not possible to make start destination for nested graph with mandatory arguments. @@ -116,166 +128,118 @@ private fun LoginContent( if (isCodeInputNecessary) { LoginEmailVerificationCodeScreen(loginEmailViewModel) } else { - MainLoginContent(onSuccess, onRemoveDeviceNeeded, loginEmailViewModel) + MainLoginContent(onBackPressed, onSuccess, onRemoveDeviceNeeded, loginEmailViewModel, ssoLoginResult) } } } } -@Suppress("UnusedParameter") +@OptIn(ExperimentalFoundationApi::class) @Composable private fun MainLoginContent( + onBackPressed: () -> Unit, onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, onRemoveDeviceNeeded: () -> Unit, loginEmailViewModel: LoginEmailViewModel, + ssoLoginResult: DeepLinkResult.SSOLogin? ) { - val ssoDisabledWithProxyDialogState = rememberVisibilityState() - FeatureDisabledWithProxyDialogContent(dialogState = ssoDisabledWithProxyDialogState) - if (loginEmailViewModel.serverConfig.isOnPremises) { - ServerTitle( - serverLinks = loginEmailViewModel.serverConfig, - style = MaterialTheme.wireTypography.body01 - ) - VerticalSpace.x8() - } - LoginEmailScreen( - onSuccess = onSuccess, - onRemoveDeviceNeeded = onRemoveDeviceNeeded, - loginEmailViewModel = loginEmailViewModel, - fillMaxHeight = false, + val scope = rememberCoroutineScope() + val scrollState = rememberScrollState() + val initialPageIndex = if (ssoLoginResult == null) LoginTabItem.EMAIL.ordinal else LoginTabItem.SSO.ordinal + val pagerState = rememberPagerState( + initialPage = initialPageIndex, + pageCount = { LoginTabItem.values().size } ) -} - -@Composable -fun LoginErrorDialog( - error: LoginState.Error, - onDialogDismiss: () -> Unit, - updateTheApp: () -> Unit, - ssoLoginResult: DeepLinkResult.SSOLogin? = null -) { - val dialogErrorData: LoginDialogErrorData = when (error) { - is LoginState.Error.DialogError.InvalidCredentialsError -> LoginDialogErrorData( - title = stringResource(R.string.login_error_invalid_credentials_title), - body = AnnotatedString(stringResource(R.string.login_error_invalid_credentials_message)), - onDismiss = onDialogDismiss - ) - - is LoginState.Error.DialogError.UserAlreadyExists -> LoginDialogErrorData( - title = stringResource(R.string.login_error_user_already_logged_in_title), - body = AnnotatedString(stringResource(R.string.login_error_user_already_logged_in_message)), - onDismiss = onDialogDismiss - ) - - is LoginState.Error.DialogError.ProxyError -> { - LoginDialogErrorData( - title = stringResource(R.string.error_socket_title), - body = AnnotatedString(stringResource(R.string.error_socket_message)), - onDismiss = onDialogDismiss - ) - } - - is LoginState.Error.DialogError.GenericError -> { - val strings = error.coreFailure.dialogErrorStrings(LocalContext.current.resources) - LoginDialogErrorData( - strings.title, - strings.annotatedMessage, - onDialogDismiss - ) - } - is LoginState.Error.DialogError.InvalidSSOCodeError -> LoginDialogErrorData( - title = stringResource(R.string.login_error_invalid_credentials_title), - body = AnnotatedString(stringResource(R.string.login_error_invalid_sso_code)), - onDismiss = onDialogDismiss - ) - - is LoginState.Error.DialogError.InvalidSSOCookie -> LoginDialogErrorData( - title = stringResource(R.string.login_sso_error_invalid_cookie_title), - body = AnnotatedString(stringResource(R.string.login_sso_error_invalid_cookie_message)), - onDismiss = onDialogDismiss - ) + val ssoDisabledWithProxyDialogState = rememberVisibilityState() + FeatureDisabledWithProxyDialogContent(dialogState = ssoDisabledWithProxyDialogState) - is LoginState.Error.DialogError.SSOResultError -> { - with(ssoLoginResult as DeepLinkResult.SSOLogin.Failure) { - LoginDialogErrorData( - title = stringResource(R.string.sso_error_dialog_title), - body = AnnotatedString(stringResource(R.string.sso_error_dialog_message, this.ssoError.errorCode)), - onDismiss = onDialogDismiss + WireScaffold( + topBar = { + WireCenterAlignedTopAppBar( + elevation = scrollState.rememberTopBarElevationState().value, + title = stringResource(R.string.login_title), + subtitleContent = { + if (loginEmailViewModel.serverConfig.isOnPremises) { + ServerTitle( + serverLinks = loginEmailViewModel.serverConfig, + style = MaterialTheme.wireTypography.body01 + ) + } + }, + onNavigationPressed = onBackPressed, + navigationIconType = NavigationIconType.Back(R.string.content_description_login_back_btn) + ) { + WireTabRow( + tabs = LoginTabItem.values().toList(), + selectedTabIndex = pagerState.calculateCurrentTab(), + onTabChange = { + + if (loginEmailViewModel.serverConfig.isProxyEnabled) { + if (pagerState.currentPage != LoginTabItem.SSO.ordinal) { + ssoDisabledWithProxyDialogState.show( + ssoDisabledWithProxyDialogState.savedState ?: FeatureDisabledWithProxyDialogState( + R.string.sso_not_supported_dialog_description + ) + ) + } + } else { + scope.launch { pagerState.animateScrollToPage(it) } + } + }, + modifier = Modifier.padding( + start = MaterialTheme.wireDimensions.spacing16x, + end = MaterialTheme.wireDimensions.spacing16x + ), ) } - } - - is LoginState.Error.DialogError.ServerVersionNotSupported -> LoginDialogErrorData( - title = stringResource(R.string.api_versioning_server_version_not_supported_title), - body = AnnotatedString(stringResource(R.string.api_versioning_server_version_not_supported_message)), - onDismiss = onDialogDismiss, - actionTextId = R.string.label_close, - dismissOnClickOutside = false - ) - - is LoginState.Error.DialogError.ClientUpdateRequired -> LoginDialogErrorData( - title = stringResource(R.string.api_versioning_client_update_required_title), - body = AnnotatedString(stringResource(R.string.api_versioning_client_update_required_message)), - onDismiss = onDialogDismiss, - actionTextId = R.string.label_update, - onAction = updateTheApp, - dismissOnClickOutside = false - ) - - LoginState.Error.DialogError.Request2FAWithHandle -> { - LoginDialogErrorData( - title = stringResource(R.string.login_error_request_2fa_with_handle_title), - body = AnnotatedString(stringResource(R.string.login_error_request_2fa_with_handle_message)), - onDismiss = onDialogDismiss - ) - } - - LoginState.Error.TextFieldError.InvalidValue, - LoginState.Error.DialogError.PasswordNeededToRegisterClient, - LoginState.Error.TooManyDevicesError -> { - LoginDialogErrorData( - title = stringResource(R.string.error_unknown_title), - body = AnnotatedString(stringResource(R.string.error_unknown_message)), - onDismiss = onDialogDismiss - ) + }, + modifier = Modifier.fillMaxHeight(), + ) { internalPadding -> + var focusedTabIndex: Int by remember { mutableStateOf(initialPageIndex) } + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + + CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + HorizontalPager( + state = pagerState, + modifier = Modifier + .fillMaxWidth() + .padding(internalPadding) + ) { pageIndex -> + when (LoginTabItem.values()[pageIndex]) { + LoginTabItem.EMAIL -> LoginEmailScreen(onSuccess, onRemoveDeviceNeeded, loginEmailViewModel, scrollState) + LoginTabItem.SSO -> LoginSSOScreen(onSuccess, onRemoveDeviceNeeded, ssoLoginResult) + } + } + if (!pagerState.isScrollInProgress && focusedTabIndex != pagerState.currentPage) { + LaunchedEffect(Unit) { + keyboardController?.hide() + focusManager.clearFocus() + focusedTabIndex = pagerState.currentPage + } + } } } - - WireDialog( - title = dialogErrorData.title, - text = dialogErrorData.body, - onDismiss = dialogErrorData.onDismiss, - optionButton1Properties = WireDialogButtonProperties( - text = stringResource(dialogErrorData.actionTextId), - onClick = dialogErrorData.onAction, - type = WireDialogButtonType.Primary - ), - properties = DialogProperties( - dismissOnBackPress = true, - dismissOnClickOutside = dialogErrorData.dismissOnClickOutside, - usePlatformDefaultWidth = false - ) - ) } -data class LoginDialogErrorData( - val title: String, - val body: AnnotatedString, - val onDismiss: () -> Unit, - @StringRes val actionTextId: Int = R.string.label_ok, - val onAction: () -> Unit = onDismiss, - val dismissOnClickOutside: Boolean = true -) +enum class LoginTabItem(@StringRes val titleResId: Int) : TabItem { + EMAIL(R.string.login_tab_email), + SSO(R.string.login_tab_sso); + override val title: UIText = UIText.StringResource(titleResId) +} @PreviewMultipleThemes @Composable -private fun PreviewNewLoginEmailScreen() = WireTheme { +private fun PreviewLoginScreen() = WireTheme { WireTheme { MainLoginContent( + onBackPressed = {}, onSuccess = { _, _ -> }, onRemoveDeviceNeeded = {}, loginEmailViewModel = hiltViewModel(), + ssoLoginResult = null ) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/start/StartLoginScreenNavArgs.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/start/StartLoginScreenNavArgs.kt deleted file mode 100644 index 1696d8e6814..00000000000 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/start/StartLoginScreenNavArgs.kt +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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.ui.authentication.start - -data class StartLoginScreenNavArgs(val isCustomBackend: Boolean = false) 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 53b8df9d929..68710f5f901 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 @@ -20,35 +20,90 @@ package com.wire.android.ui.authentication.welcome -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize +import android.content.res.TypedArray +import androidx.annotation.ArrayRes +import androidx.annotation.DrawableRes +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.LocalOverscrollConfiguration +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.integerResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.semantics.clearAndSetSemantics +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.wire.android.R +import com.wire.android.config.LocalCustomUiConfigurationProvider import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.WireDestination -import com.wire.android.navigation.style.AuthPopUpNavigationAnimation -import com.wire.android.ui.authentication.login.WireAuthBackgroundLayout +import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.ui.authentication.ServerTitle +import com.wire.android.ui.common.button.WirePrimaryButton +import com.wire.android.ui.common.button.WireSecondaryButton import com.wire.android.ui.common.dialogs.FeatureDisabledWithProxyDialogContent import com.wire.android.ui.common.dialogs.FeatureDisabledWithProxyDialogState import com.wire.android.ui.common.dialogs.MaxAccountsReachedDialog import com.wire.android.ui.common.dialogs.MaxAccountsReachedDialogState -import com.wire.android.ui.common.preview.EdgeToEdgePreview +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.scaffold.WireScaffold +import com.wire.android.ui.common.topappbar.NavigationIconType +import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.visbility.rememberVisibilityState -import com.wire.android.ui.destinations.StartLoginScreenDestination +import com.wire.android.ui.destinations.CreatePersonalAccountOverviewScreenDestination +import com.wire.android.ui.destinations.CreateTeamAccountOverviewScreenDestination +import com.wire.android.ui.destinations.LoginScreenDestination import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireDimensions +import com.wire.android.ui.theme.wireTypography import com.wire.android.util.CustomTabsHelper -import com.wire.android.util.ui.PreviewMultipleThemes import com.wire.kalium.logic.configuration.server.ServerConfig +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.scan -@RootNavGraph(start = true) +@RootNavGraph @WireDestination( - style = AuthPopUpNavigationAnimation::class, + style = PopUpNavigationAnimation::class, ) @Composable fun WelcomeScreen( @@ -56,62 +111,293 @@ fun WelcomeScreen( viewModel: WelcomeViewModel = hiltViewModel() ) { WelcomeContent( - viewModel.state, + viewModel.state.isThereActiveSession, + viewModel.state.maxAccountsReached, + viewModel.state.links, navigator::navigateBack, navigator::navigate ) } +@OptIn(ExperimentalComposeUiApi::class) @Composable private fun WelcomeContent( - state: WelcomeScreenState, + isThereActiveSession: Boolean, + maxAccountsReached: Boolean, + state: ServerConfig.Links, navigateBack: () -> Unit, navigate: (NavigationCommand) -> Unit ) { val enterpriseDisabledWithProxyDialogState = rememberVisibilityState() val createPersonalAccountDisabledWithProxyDialogState = rememberVisibilityState() val context = LocalContext.current - val maxAccountsReachedDialogState = rememberVisibilityState() - MaxAccountsReachedDialog(dialogState = maxAccountsReachedDialogState) { navigateBack() } - if (state.maxAccountsReached) { - maxAccountsReachedDialogState.show(maxAccountsReachedDialogState.savedState ?: MaxAccountsReachedDialogState) - } - FeatureDisabledWithProxyDialogContent( - dialogState = enterpriseDisabledWithProxyDialogState, - onActionButtonClicked = { - CustomTabsHelper.launchUrl(context, state.links.teams) + WireScaffold(topBar = { + if (isThereActiveSession) { + WireCenterAlignedTopAppBar( + elevation = dimensions().spacing0x, + title = "", + navigationIconType = NavigationIconType.Close(R.string.content_description_welcome_screen_close_btn), + onNavigationPressed = navigateBack + ) + } else { + Spacer(modifier = Modifier.height(MaterialTheme.wireDimensions.welcomeVerticalPadding)) } - ) - FeatureDisabledWithProxyDialogContent(dialogState = createPersonalAccountDisabledWithProxyDialogState) - - // empty Box to keep proper bounds of the screen for transition animation to the next screen - Box(modifier = Modifier.fillMaxSize()) - - with(state.startLoginDestination) { - LaunchedEffect(this) { - if (state.maxAccountsReached.not()) { - delay(1_000) // small delay to resolve the navigation - when (state.startLoginDestination) { - StartLoginDestination.Default -> { - navigate(NavigationCommand(StartLoginScreenDestination())) + }) { internalPadding -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .padding(internalPadding) + ) { + val maxAccountsReachedDialogState = rememberVisibilityState() + MaxAccountsReachedDialog(dialogState = maxAccountsReachedDialogState) { navigateBack() } + if (maxAccountsReached) { + maxAccountsReachedDialogState.show(maxAccountsReachedDialogState.savedState ?: MaxAccountsReachedDialogState) + } + + Icon( + imageVector = ImageVector.vectorResource(id = R.drawable.ic_wire_logo), + tint = MaterialTheme.colorScheme.onBackground, + contentDescription = null + ) + + if (state.isOnPremises) { + ServerTitle(serverLinks = state, modifier = Modifier.padding(top = dimensions().spacing16x)) + } + + WelcomeCarousel(modifier = Modifier.weight(1f, true)) + + Column( + modifier = Modifier + .padding( + vertical = MaterialTheme.wireDimensions.welcomeVerticalSpacing, + horizontal = MaterialTheme.wireDimensions.welcomeButtonHorizontalPadding + ) + .semantics { + testTagsAsResourceId = true } + ) { + LoginButton(onClick = { navigate(NavigationCommand(LoginScreenDestination())) }) + FeatureDisabledWithProxyDialogContent( + dialogState = enterpriseDisabledWithProxyDialogState, + onActionButtonClicked = { + CustomTabsHelper.launchUrl(context, state.teams) + } + ) + FeatureDisabledWithProxyDialogContent(dialogState = createPersonalAccountDisabledWithProxyDialogState) - StartLoginDestination.CustomBackend -> navigate(NavigationCommand(StartLoginScreenDestination(isCustomBackend = true))) + if (LocalCustomUiConfigurationProvider.current.isAccountCreationAllowed) { + CreateEnterpriseAccountButton { + if (state.isProxyEnabled()) { + enterpriseDisabledWithProxyDialogState.show( + enterpriseDisabledWithProxyDialogState.savedState ?: FeatureDisabledWithProxyDialogState( + R.string.create_team_not_supported_dialog_description, + state.teams + ) + ) + } else { + navigate(NavigationCommand(CreateTeamAccountOverviewScreenDestination)) + } + } } } + + if (LocalCustomUiConfigurationProvider.current.isAccountCreationAllowed) { + WelcomeFooter( + modifier = Modifier.padding(horizontal = MaterialTheme.wireDimensions.welcomeTextHorizontalPadding), + onPrivateAccountClick = { + if (state.isProxyEnabled()) { + createPersonalAccountDisabledWithProxyDialogState.show( + createPersonalAccountDisabledWithProxyDialogState.savedState ?: FeatureDisabledWithProxyDialogState( + R.string.create_personal_account_not_supported_dialog_description + ) + ) + } else { + navigate(NavigationCommand(CreatePersonalAccountOverviewScreenDestination)) + } + } + ) + } + } + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun WelcomeCarousel(modifier: Modifier = Modifier) { + val delay = integerResource(id = R.integer.welcome_carousel_item_time_ms) + val icons: List = typedArrayResource(id = R.array.welcome_carousel_icons).drawableResIdList() + val texts: List = stringArrayResource(id = R.array.welcome_carousel_texts).toList() + val items: List = icons.zip(texts) { icon, text -> CarouselPageData(icon, text) } + + // adding repeated elements on both edges to have list like: [E A B C D E A] and because of that we can flip to the other side of the + // list when we reach the end while keeping swipe capability both ways and from the user side it looks like an infinite loop both ways + val circularItemsList = listOf().plus(items.last()).plus(items).plus(items.first()) + val initialPage = 1 + val pagerState = rememberPagerState(initialPage = initialPage, pageCount = { circularItemsList.size }) + + LaunchedEffect(pagerState) { + autoScrollCarousel(pagerState, initialPage, circularItemsList, delay.toLong()) + } + + CompositionLocalProvider(LocalOverscrollConfiguration provides null) { + HorizontalPager( + state = pagerState, + modifier = modifier.fillMaxWidth() + ) { page -> + val (pageIconResId, pageText) = circularItemsList[page] + WelcomeCarouselItem(pageIconResId = pageIconResId, pageText = pageText) } } } -@PreviewMultipleThemes +@OptIn(ExperimentalCoroutinesApi::class, ExperimentalFoundationApi::class) +private suspend fun autoScrollCarousel( + pageState: PagerState, + initialPage: Int, + circularItemsList: List, + delay: Long +) = snapshotFlow { pageState.currentPage }.distinctUntilChanged() + .scan(initialPage to initialPage) { (_, previousPage), currentPage -> previousPage to currentPage } + .flatMapLatest { (previousPage, currentPage) -> + when { + shouldJumpToStart(previousPage, currentPage, circularItemsList.lastIndex, initialPage) -> flow { + emit( + CarouselScrollData( + scrollToPage = initialPage, + animate = false + ) + ) + } + + shouldJumpToEnd( + previousPage, + currentPage, + circularItemsList.lastIndex + ) -> flow { emit(CarouselScrollData(scrollToPage = circularItemsList.lastIndex - 1, animate = false)) } + + else -> flow { emit(CarouselScrollData(scrollToPage = pageState.currentPage + 1, animate = true)) }.onEach { + delay( + delay + ) + } + } + }.collect { (scrollToPage, animate) -> + if (pageState.pageCount != 0) { + if (animate) pageState.animateScrollToPage(scrollToPage) + else pageState.scrollToPage(scrollToPage) + } + } + @Composable -fun PreviewWelcomeScreen() = WireTheme { - EdgeToEdgePreview(useDarkIcons = false) { - WireAuthBackgroundLayout() +private fun WelcomeCarouselItem(pageIconResId: Int, pageText: String) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier.fillMaxWidth() + ) { + Image( + painter = painterResource(id = pageIconResId), + contentDescription = null, + contentScale = ContentScale.Inside, + modifier = Modifier + .weight(1f, true) + .padding( + horizontal = MaterialTheme.wireDimensions.welcomeImageHorizontalPadding, + vertical = MaterialTheme.wireDimensions.welcomeVerticalSpacing + ) + ) + Text( + text = pageText, + style = MaterialTheme.wireTypography.title01, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = MaterialTheme.wireDimensions.welcomeTextHorizontalPadding) + .clearAndSetSemantics {} + ) + } +} + +@Composable +private fun LoginButton(onClick: () -> Unit) { + WirePrimaryButton( + onClick = onClick, + text = stringResource(R.string.label_login), + modifier = Modifier + .padding(bottom = MaterialTheme.wireDimensions.welcomeButtonVerticalPadding) + .testTag("loginButton") + ) +} + +@Composable +private fun CreateEnterpriseAccountButton(onClick: () -> Unit) { + WireSecondaryButton( + onClick = onClick, + text = stringResource(R.string.welcome_button_create_team), + modifier = Modifier.padding(bottom = MaterialTheme.wireDimensions.welcomeButtonVerticalPadding) + ) +} + +@Composable +private fun WelcomeFooter(onPrivateAccountClick: () -> Unit, modifier: Modifier = Modifier) { + Column(modifier = modifier) { + Text( + text = stringResource(R.string.welcome_footer_text), + style = MaterialTheme.wireTypography.body02, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + + Text( + text = stringResource(R.string.welcome_button_create_personal_account), + style = MaterialTheme.wireTypography.body02.copy( + textDecoration = TextDecoration.Underline, + color = MaterialTheme.colorScheme.primary + ), + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = onPrivateAccountClick, + onClickLabel = stringResource(R.string.content_description_open_link_label) + ) + ) + + Spacer(modifier = Modifier.height(MaterialTheme.wireDimensions.welcomeVerticalPadding)) + } +} + +@Composable +@ReadOnlyComposable +private fun typedArrayResource(@ArrayRes id: Int): TypedArray = LocalContext.current.resources.obtainTypedArray(id) + +private fun TypedArray.drawableResIdList(): List = (0 until this.length()).map { this.getResourceId(it, 0) } + +// having list [E A B C D E A], when moving forward we reach the last one - second "A", we want to flip to the first "A" +// to keep swipe capability both ways and the feeling of an endless loop +private fun shouldJumpToStart(previousPage: Int, currentPage: Int, lastPage: Int, initialPage: Int): Boolean = + currentPage == lastPage && previousPage < currentPage && previousPage >= initialPage + +// having list [E A B C D E A], when moving backward we reach the first one - first "E", we want to flip to the second "E" +// to keep swipe capability both ways and the feeling of an endless loop +private fun shouldJumpToEnd(previousPage: Int, currentPage: Int, lastPage: Int): Boolean = + currentPage == 0 && previousPage > currentPage && previousPage < lastPage + +@Preview +@Composable +fun PreviewWelcomeScreen() { + WireTheme { WelcomeContent( - state = WelcomeScreenState(ServerConfig.DEFAULT), + isThereActiveSession = false, + maxAccountsReached = false, + state = ServerConfig.DEFAULT, navigateBack = {}, - navigate = {} - ) + navigate = {}) } } + +private data class CarouselScrollData(val scrollToPage: Int, val animate: Boolean) +private data class CarouselPageData(@DrawableRes val icon: Int, val text: String) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreenState.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreenState.kt index 63b3541a54d..7a64380faeb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreenState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/welcome/WelcomeScreenState.kt @@ -23,10 +23,4 @@ data class WelcomeScreenState( val links: ServerConfig.Links, val isThereActiveSession: Boolean = false, val maxAccountsReached: Boolean = false, - val startLoginDestination: StartLoginDestination = StartLoginDestination.Default, ) - -enum class StartLoginDestination { - Default, - CustomBackend, -} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeScreen.kt index 6094a01fc30..02c29850c27 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/appLock/forgot/ForgotLockCodeScreen.kt @@ -47,6 +47,7 @@ import androidx.compose.ui.semantics.testTagsAsResourceId import androidx.compose.ui.text.style.TextAlign import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.wire.android.BuildConfig import com.wire.android.R import com.wire.android.navigation.BackStackMode import com.wire.android.navigation.NavigationCommand @@ -59,6 +60,7 @@ import com.wire.android.ui.common.button.WireButtonState import com.wire.android.ui.common.button.WirePrimaryButton import com.wire.android.ui.common.rememberBottomBarElevationState import com.wire.android.ui.common.scaffold.WireScaffold +import com.wire.android.ui.destinations.NewWelcomeScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireColorScheme @@ -76,7 +78,8 @@ fun ForgotLockCodeScreen( ) { with(viewModel.state) { LaunchedEffect(completed) { - if (completed) navigator.navigate(NavigationCommand(WelcomeScreenDestination, BackStackMode.CLEAR_WHOLE)) + val destination = if (BuildConfig.ENTERPRISE_LOGIN_ENABLED) NewWelcomeScreenDestination else WelcomeScreenDestination + if (completed) navigator.navigate(NavigationCommand(destination, BackStackMode.CLEAR_WHOLE)) } ForgotLockCodeScreenContent( scrollState = rememberScrollState(), diff --git a/app/src/main/kotlin/com/wire/android/ui/migration/MigrationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/migration/MigrationScreen.kt index e095bf8e6aa..68354822c6f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/migration/MigrationScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/migration/MigrationScreen.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.wire.android.BuildConfig import com.wire.android.R import com.wire.android.migration.MigrationData import com.wire.android.navigation.BackStackMode @@ -39,6 +40,8 @@ import com.wire.android.ui.common.SettingUpWireScreenContent import com.wire.android.ui.common.SettingUpWireScreenType import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.LoginScreenDestination +import com.wire.android.ui.destinations.NewLoginScreenDestination +import com.wire.android.ui.destinations.NewWelcomeScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination import com.wire.android.ui.theme.wireColorScheme import com.wire.android.ui.theme.wireTypography @@ -56,12 +59,23 @@ fun MigrationScreen( ) { when (val state = viewModel.state) { - is MigrationState.LoginRequired -> - navigator.navigate(NavigationCommand(LoginScreenDestination(state.userHandle), BackStackMode.CLEAR_WHOLE)) + is MigrationState.LoginRequired -> navigator.navigate( + NavigationCommand( + when { + BuildConfig.ENTERPRISE_LOGIN_ENABLED -> NewLoginScreenDestination(userHandle = state.userHandle) + else -> LoginScreenDestination(userHandle = state.userHandle) + }, + BackStackMode.CLEAR_WHOLE + ) + ) is MigrationState.Success -> navigator.navigate( NavigationCommand( - if (state.currentSessionAvailable) HomeScreenDestination else WelcomeScreenDestination, + when { + state.currentSessionAvailable -> HomeScreenDestination + BuildConfig.ENTERPRISE_LOGIN_ENABLED -> NewWelcomeScreenDestination + else -> WelcomeScreenDestination + }, BackStackMode.CLEAR_WHOLE ) ) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/NewLoginContainer.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginContainer.kt similarity index 98% rename from app/src/main/kotlin/com/wire/android/ui/authentication/login/NewLoginContainer.kt rename to app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginContainer.kt index d1aaaf58661..da6efd76745 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/NewLoginContainer.kt +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginContainer.kt @@ -1,6 +1,6 @@ /* * Wire - * Copyright (C) 2024 Wire Swiss GmbH + * 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 @@ -15,7 +15,7 @@ * 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.authentication.login +package com.wire.android.ui.newauthentication.login import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/start/StartLoginScreen.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginScreen.kt similarity index 91% rename from app/src/main/kotlin/com/wire/android/ui/authentication/start/StartLoginScreen.kt rename to app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginScreen.kt index 62b150bf6b2..602c8cd6fcc 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/start/StartLoginScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginScreen.kt @@ -1,6 +1,6 @@ /* * Wire - * Copyright (C) 2024 Wire Swiss GmbH + * 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 @@ -18,7 +18,7 @@ @file:Suppress("TooManyFunctions") -package com.wire.android.ui.authentication.start +package com.wire.android.ui.newauthentication.login import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -55,8 +55,8 @@ import com.wire.android.navigation.NavigationCommand import com.wire.android.navigation.Navigator import com.wire.android.navigation.WireDestination import com.wire.android.navigation.style.AuthPopUpNavigationAnimation +import com.wire.android.ui.authentication.login.LoginNavArgs import com.wire.android.ui.authentication.login.LoginState -import com.wire.android.ui.authentication.login.NewLoginContainer import com.wire.android.ui.authentication.login.WireAuthBackgroundLayout import com.wire.android.ui.authentication.login.email.LoginEmailState import com.wire.android.ui.common.button.WireButtonState @@ -68,7 +68,7 @@ import com.wire.android.ui.common.textfield.DefaultEmailNext import com.wire.android.ui.common.textfield.WireAutoFillType import com.wire.android.ui.common.textfield.WireTextField import com.wire.android.ui.common.textfield.WireTextFieldState -import com.wire.android.ui.destinations.LoginScreenDestination +import com.wire.android.ui.destinations.NewLoginPasswordScreenDestination import com.wire.android.ui.theme.WireTheme import com.wire.android.ui.theme.wireDimensions import com.wire.android.ui.theme.wireTypography @@ -77,15 +77,14 @@ import com.wire.android.util.ui.PreviewMultipleThemes @WireDestination( style = AuthPopUpNavigationAnimation::class, - navArgsDelegate = StartLoginScreenNavArgs::class, + navArgsDelegate = LoginNavArgs::class, ) @Composable -fun StartLoginScreen( +fun NewLoginScreen( navigator: Navigator, - viewModel: StartLoginViewModel = hiltViewModel() + viewModel: NewLoginViewModel = hiltViewModel() ) { StartLoginContent( - viewModel.state.isCustomBackend, viewModel.state.isThereActiveSession, viewModel.loginState, viewModel.userIdentifierTextState, @@ -97,7 +96,6 @@ fun StartLoginScreen( @Composable private fun StartLoginContent( - isCustomBackend: Boolean, isThereActiveSession: Boolean, loginEmailState: LoginEmailState, userIdentifierState: TextFieldState, @@ -109,14 +107,12 @@ private fun StartLoginContent( canNavigateBack = isThereActiveSession, onNavigateBack = navigateBack ) { - if (!isCustomBackend) { - NewWelcomeExperienceContent( - loginEmailState = loginEmailState, - userIdentifierState = userIdentifierState, - onNextClicked = onNextClicked, - navigate = navigate - ) - } + NewWelcomeExperienceContent( + loginEmailState = loginEmailState, + userIdentifierState = userIdentifierState, + onNextClicked = onNextClicked, + navigate = navigate + ) } } @@ -168,7 +164,7 @@ private fun NewWelcomeExperienceContent( enabled = loginEmailState.loginEnabled, onClick = { onNextClicked { - navigate(NavigationCommand(LoginScreenDestination(userHandle = userIdentifierState.text.toString()))) + navigate(NavigationCommand(NewLoginPasswordScreenDestination(userHandle = userIdentifierState.text.toString()))) } } ) @@ -257,11 +253,10 @@ private fun WelcomeFooter(onTermsAndConditionClick: () -> Unit, modifier: Modifi @PreviewMultipleThemes @Composable -fun PreviewStartLoginScreen() = WireTheme { +fun PreviewNewLoginScreen() = WireTheme { EdgeToEdgePreview(useDarkIcons = false) { WireAuthBackgroundLayout { StartLoginContent( - isCustomBackend = false, isThereActiveSession = false, loginEmailState = LoginEmailState(), userIdentifierState = TextFieldState(), diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/start/StartLoginScreenState.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginScreenState.kt similarity index 87% rename from app/src/main/kotlin/com/wire/android/ui/authentication/start/StartLoginScreenState.kt rename to app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginScreenState.kt index 7ee98144907..35c972f7574 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/start/StartLoginScreenState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginScreenState.kt @@ -15,12 +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.ui.authentication.start +package com.wire.android.ui.newauthentication.login import com.wire.kalium.logic.configuration.server.ServerConfig -data class StartLoginScreenState( +data class NewLoginScreenState( val links: ServerConfig.Links, val isThereActiveSession: Boolean = false, - val isCustomBackend: Boolean = false ) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/start/StartLoginViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt similarity index 77% rename from app/src/main/kotlin/com/wire/android/ui/authentication/start/StartLoginViewModel.kt rename to app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt index d5ee86b4387..24b447fd189 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/start/StartLoginViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/NewLoginViewModel.kt @@ -1,6 +1,6 @@ /* * Wire - * Copyright (C) 2024 Wire Swiss GmbH + * 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 @@ -16,9 +16,10 @@ * along with this program. If not, see http://www.gnu.org/licenses/. */ -package com.wire.android.ui.authentication.start +package com.wire.android.ui.newauthentication.login import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -26,11 +27,14 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.wire.android.di.AuthServerConfigProvider +import com.wire.android.ui.authentication.login.LoginNavArgs import com.wire.android.ui.authentication.login.LoginState +import com.wire.android.ui.authentication.login.PreFilledUserIdentifierType import com.wire.android.ui.authentication.login.email.LoginEmailState import com.wire.android.ui.authentication.login.email.LoginEmailViewModel.Companion.USER_IDENTIFIER_SAVED_STATE_KEY import com.wire.android.ui.common.textfield.textAsFlow import com.wire.android.ui.navArgs +import com.wire.android.util.EMPTY import com.wire.kalium.logic.configuration.server.ServerConfig import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.delay @@ -41,16 +45,18 @@ import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel -class StartLoginViewModel @Inject constructor( +class NewLoginViewModel @Inject constructor( private val authServerConfigProvider: AuthServerConfigProvider, private val savedStateHandle: SavedStateHandle, ) : ViewModel() { + private val loginNavArgs: LoginNavArgs = savedStateHandle.navArgs() + private val preFilledUserIdentifier: PreFilledUserIdentifierType = loginNavArgs.userHandle.let { + if (it.isNullOrEmpty()) PreFilledUserIdentifierType.None else PreFilledUserIdentifierType.PreFilled(it) + } - private val startLoginScreenNavArgs: StartLoginScreenNavArgs = savedStateHandle.navArgs() var state by mutableStateOf( - StartLoginScreenState( + NewLoginScreenState( links = ServerConfig.DEFAULT, - isCustomBackend = startLoginScreenNavArgs.isCustomBackend ) ) private set @@ -59,6 +65,13 @@ class StartLoginViewModel @Inject constructor( init { observerAuthServer() + userIdentifierTextState.setTextAndPlaceCursorAtEnd( + if (preFilledUserIdentifier is PreFilledUserIdentifierType.PreFilled) { + preFilledUserIdentifier.userIdentifier + } else { + savedStateHandle[USER_IDENTIFIER_SAVED_STATE_KEY] ?: String.EMPTY + } + ) viewModelScope.launch { userIdentifierTextState.textAsFlow().distinctUntilChanged().onEach { savedStateHandle[USER_IDENTIFIER_SAVED_STATE_KEY] = it.toString() diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/password/NewLoginPasswordScreen.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/password/NewLoginPasswordScreen.kt new file mode 100644 index 00000000000..f57f7ae6a54 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/login/password/NewLoginPasswordScreen.kt @@ -0,0 +1,151 @@ +/* + * 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.ui.newauthentication.login.password + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.wire.android.R +import com.wire.android.navigation.BackStackMode +import com.wire.android.navigation.NavigationCommand +import com.wire.android.navigation.Navigator +import com.wire.android.navigation.WireDestination +import com.wire.android.navigation.style.AuthSlideNavigationAnimation +import com.wire.android.navigation.style.TransitionAnimationType +import com.wire.android.ui.authentication.ServerTitle +import com.wire.android.ui.authentication.login.LoginNavArgs +import com.wire.android.ui.newauthentication.login.NewLoginContainer +import com.wire.android.ui.authentication.login.email.LoginEmailScreen +import com.wire.android.ui.authentication.login.email.LoginEmailVerificationCodeScreen +import com.wire.android.ui.authentication.login.email.LoginEmailViewModel +import com.wire.android.ui.common.dialogs.FeatureDisabledWithProxyDialogContent +import com.wire.android.ui.common.dialogs.FeatureDisabledWithProxyDialogState +import com.wire.android.ui.common.spacers.VerticalSpace +import com.wire.android.ui.common.visbility.rememberVisibilityState +import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination +import com.wire.android.ui.destinations.HomeScreenDestination +import com.wire.android.ui.destinations.InitialSyncScreenDestination +import com.wire.android.ui.destinations.RemoveDeviceScreenDestination +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.ui.PreviewMultipleThemes + +@RootNavGraph +@WireDestination( + navArgsDelegate = LoginNavArgs::class, + style = AuthSlideNavigationAnimation::class, +) +@Composable +fun NewLoginPasswordScreen( + navigator: Navigator, + loginEmailViewModel: LoginEmailViewModel = hiltViewModel() +) { + NewLoginContainer( + title = stringResource(id = R.string.enterprise_login_title), + canNavigateBack = true, + onNavigateBack = navigator::navigateBack + ) { + LoginContent( + onSuccess = { initialSyncCompleted, isE2EIRequired -> + val destination = if (isE2EIRequired) E2EIEnrollmentScreenDestination + else if (initialSyncCompleted) HomeScreenDestination + else InitialSyncScreenDestination + + navigator.navigate(NavigationCommand(destination, BackStackMode.CLEAR_WHOLE)) + }, + onRemoveDeviceNeeded = { + navigator.navigate(NavigationCommand(RemoveDeviceScreenDestination, BackStackMode.CLEAR_WHOLE)) + }, + loginEmailViewModel = loginEmailViewModel, + ) + } +} + +@Composable +private fun LoginContent( + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, + onRemoveDeviceNeeded: () -> Unit, + loginEmailViewModel: LoginEmailViewModel, +) { + Column( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + ) { + /* + TODO: we can change it to be a nested navigation graph when Compose Destinations 2.0 is released, + right now it's not possible to make start destination for nested graph with mandatory arguments. + More on that here: https://github.com/raamcosta/compose-destinations/issues/185 + */ + AnimatedContent( + targetState = loginEmailViewModel.secondFactorVerificationCodeState.isCodeInputNecessary, + transitionSpec = { TransitionAnimationType.SLIDE.enterTransition.togetherWith(TransitionAnimationType.SLIDE.exitTransition) } + ) { isCodeInputNecessary -> + if (isCodeInputNecessary) { + LoginEmailVerificationCodeScreen(loginEmailViewModel) + } else { + MainLoginContent(onSuccess, onRemoveDeviceNeeded, loginEmailViewModel) + } + } + } +} + +@Composable +private fun MainLoginContent( + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, + onRemoveDeviceNeeded: () -> Unit, + loginEmailViewModel: LoginEmailViewModel, +) { + val ssoDisabledWithProxyDialogState = rememberVisibilityState() + FeatureDisabledWithProxyDialogContent(dialogState = ssoDisabledWithProxyDialogState) + + if (loginEmailViewModel.serverConfig.isOnPremises) { + ServerTitle( + serverLinks = loginEmailViewModel.serverConfig, + style = MaterialTheme.wireTypography.body01 + ) + VerticalSpace.x8() + } + LoginEmailScreen( + onSuccess = onSuccess, + onRemoveDeviceNeeded = onRemoveDeviceNeeded, + loginEmailViewModel = loginEmailViewModel, + fillMaxHeight = false, + ) +} + +@PreviewMultipleThemes +@Composable +private fun PreviewNewLoginPasswordScreen() = WireTheme { + WireTheme { + MainLoginContent( + onSuccess = { _, _ -> }, + onRemoveDeviceNeeded = {}, + loginEmailViewModel = hiltViewModel(), + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/newauthentication/welcome/NewWelcomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/newauthentication/welcome/NewWelcomeScreen.kt new file mode 100644 index 00000000000..e558bce2d80 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/newauthentication/welcome/NewWelcomeScreen.kt @@ -0,0 +1,115 @@ +/* + * 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/. + */ + +@file:Suppress("TooManyFunctions") + +package com.wire.android.ui.newauthentication.welcome + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.hilt.navigation.compose.hiltViewModel +import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.wire.android.BuildConfig +import com.wire.android.navigation.BackStackMode +import com.wire.android.navigation.NavigationCommand +import com.wire.android.navigation.Navigator +import com.wire.android.navigation.WireDestination +import com.wire.android.navigation.style.AuthPopUpNavigationAnimation +import com.wire.android.ui.authentication.login.LoginNavArgs +import com.wire.android.ui.authentication.login.WireAuthBackgroundLayout +import com.wire.android.ui.authentication.welcome.WelcomeScreenState +import com.wire.android.ui.authentication.welcome.WelcomeViewModel +import com.wire.android.ui.common.dialogs.MaxAccountsReachedDialog +import com.wire.android.ui.common.dialogs.MaxAccountsReachedDialogState +import com.wire.android.ui.common.preview.EdgeToEdgePreview +import com.wire.android.ui.common.visbility.rememberVisibilityState +import com.wire.android.ui.destinations.NewLoginScreenDestination +import com.wire.android.ui.destinations.NewWelcomeScreenDestination +import com.wire.android.ui.destinations.WelcomeScreenDestination +import com.wire.android.ui.theme.WireTheme +import com.wire.android.util.ui.PreviewMultipleThemes +import com.wire.kalium.logic.configuration.server.ServerConfig +import kotlinx.coroutines.delay + +@RootNavGraph(start = true) +@WireDestination( + navArgsDelegate = LoginNavArgs::class +) +@Composable +// this is a temporary solution because annotation argument "start" must be a compile-time constant +// TODO: remove this composable as well when removing old WelcomeScreen and set start = true for NewWelcomeScreen +fun WelcomeChooserScreen(navigator: Navigator) { + LaunchedEffect(Unit) { + val destination = if (BuildConfig.ENTERPRISE_LOGIN_ENABLED) NewWelcomeScreenDestination else WelcomeScreenDestination + navigator.navigate(NavigationCommand(destination)) + } +} + +@RootNavGraph(start = false) +@WireDestination( + style = AuthPopUpNavigationAnimation::class, +) +@Composable +fun NewWelcomeScreen( + navigator: Navigator, + viewModel: WelcomeViewModel = hiltViewModel() +) { + WelcomeContent( + viewModel.state, + navigator::navigateBack, + navigator::navigate + ) +} + +@Composable +private fun WelcomeContent( + state: WelcomeScreenState, + navigateBack: () -> Unit, + navigate: (NavigationCommand) -> Unit +) { + val maxAccountsReachedDialogState = rememberVisibilityState() + MaxAccountsReachedDialog(dialogState = maxAccountsReachedDialogState) { navigateBack() } + if (state.maxAccountsReached) { + maxAccountsReachedDialogState.show(maxAccountsReachedDialogState.savedState ?: MaxAccountsReachedDialogState) + } + + Box(modifier = Modifier.fillMaxSize()) // empty Box to keep proper bounds of the screen for transition animation to the next screen + + LaunchedEffect(Unit) { + if (state.maxAccountsReached.not()) { + delay(5_000) // small delay to resolve the navigation + navigate(NavigationCommand(NewLoginScreenDestination(), BackStackMode.CLEAR_WHOLE)) + } + } +} + +@PreviewMultipleThemes +@Composable +fun PreviewNewWelcomeScreen() = WireTheme { + EdgeToEdgePreview(useDarkIcons = false) { + WireAuthBackgroundLayout() + WelcomeContent( + state = WelcomeScreenState(ServerConfig.DEFAULT), + navigateBack = {}, + navigate = {} + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt index 0d065b320a0..eb4667b81a5 100644 --- a/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/sharing/ImportMediaScreen.kt @@ -58,6 +58,7 @@ import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.paging.compose.collectAsLazyPagingItems import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.wire.android.BuildConfig import com.wire.android.R import com.wire.android.model.Clickable import com.wire.android.model.ImageAsset @@ -84,6 +85,7 @@ import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.topappbar.search.SearchBarState import com.wire.android.ui.common.topappbar.search.SearchTopBar import com.wire.android.ui.destinations.ConversationScreenDestination +import com.wire.android.ui.destinations.NewWelcomeScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination import com.wire.android.ui.home.FeatureFlagState import com.wire.android.ui.home.conversations.AssetTooLargeDialog @@ -136,7 +138,8 @@ fun ImportMediaScreen( fileSharingRestrictedState = fileSharingRestrictedState, navigateBack = navigator.finish, openWireAction = { - navigator.navigate(NavigationCommand(WelcomeScreenDestination, BackStackMode.CLEAR_WHOLE)) + val destination = if (BuildConfig.ENTERPRISE_LOGIN_ENABLED) NewWelcomeScreenDestination else WelcomeScreenDestination + navigator.navigate(NavigationCommand(destination, BackStackMode.CLEAR_WHOLE)) } ) } diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt index c562b1ba5dc..d01c8989927 100644 --- a/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/self/SelfUserProfileScreen.kt @@ -54,6 +54,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import com.ramcosta.composedestinations.annotation.RootNavGraph import com.ramcosta.composedestinations.result.NavResult import com.ramcosta.composedestinations.result.ResultRecipient +import com.wire.android.BuildConfig import com.wire.android.R import com.wire.android.appLogger import com.wire.android.feature.NavigationSwitchAccountActions @@ -80,6 +81,7 @@ import com.wire.android.ui.common.visbility.rememberVisibilityState import com.wire.android.ui.destinations.AppSettingsScreenDestination import com.wire.android.ui.destinations.AvatarPickerScreenDestination import com.wire.android.ui.destinations.MyAccountScreenDestination +import com.wire.android.ui.destinations.NewWelcomeScreenDestination import com.wire.android.ui.destinations.SelfQRCodeScreenDestination import com.wire.android.ui.destinations.TeamMigrationScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination @@ -138,7 +140,10 @@ fun SelfUserProfileScreen( }, onEditClick = { navigator.navigate(NavigationCommand(AppSettingsScreenDestination)) }, onStatusClicked = viewModelSelf::changeStatusClick, - onAddAccountClick = { navigator.navigate(NavigationCommand(WelcomeScreenDestination)) }, + onAddAccountClick = { + val destination = if (BuildConfig.ENTERPRISE_LOGIN_ENABLED) NewWelcomeScreenDestination else WelcomeScreenDestination + navigator.navigate(NavigationCommand(destination)) + }, dismissStatusDialog = viewModelSelf::dismissStatusDialog, onStatusChange = viewModelSelf::changeStatus, onNotShowRationaleAgainChange = viewModelSelf::dialogCheckBoxStateChanged, diff --git a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt index b254197dd1a..67a2aebb8dc 100644 --- a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt +++ b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt @@ -43,10 +43,14 @@ import com.wire.android.ui.destinations.ImportMediaScreenDestination import com.wire.android.ui.destinations.InitialSyncScreenDestination import com.wire.android.ui.destinations.LoginScreenDestination import com.wire.android.ui.destinations.MigrationScreenDestination +import com.wire.android.ui.destinations.NewLoginPasswordScreenDestination +import com.wire.android.ui.destinations.NewLoginScreenDestination +import com.wire.android.ui.destinations.NewWelcomeScreenDestination import com.wire.android.ui.destinations.OtherUserProfileScreenDestination import com.wire.android.ui.destinations.RegisterDeviceScreenDestination import com.wire.android.ui.destinations.RemoveDeviceScreenDestination import com.wire.android.ui.destinations.SelfDevicesScreenDestination +import com.wire.android.ui.destinations.WelcomeChooserScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination import com.wire.kalium.logger.obfuscateId import com.wire.kalium.logic.data.id.ConversationId @@ -211,7 +215,11 @@ sealed class CurrentScreen { is SelfDevicesScreenDestination -> DeviceManager is WelcomeScreenDestination, + is NewWelcomeScreenDestination, + is WelcomeChooserScreenDestination, is LoginScreenDestination, + is NewLoginScreenDestination, + is NewLoginPasswordScreenDestination, is CreatePersonalAccountOverviewScreenDestination, is CreateTeamAccountOverviewScreenDestination, is CreateAccountEmailScreenDestination, diff --git a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt index 5907a31e89a..7b171ed9b55 100644 --- a/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt +++ b/buildSrc/src/main/kotlin/customization/FeatureConfigs.kt @@ -105,6 +105,7 @@ enum class FeatureConfigs(val value: String, val configType: ConfigType) { PICTURE_IN_PICTURE_ENABLED("picture_in_picture_enabled", ConfigType.BOOLEAN), PAGINATED_CONVERSATION_LIST_ENABLED("paginated_conversation_list_enabled", ConfigType.BOOLEAN), + ENTERPRISE_LOGIN_ENABLED("enterprise_login_enabled", ConfigType.BOOLEAN), /** * Anonymous Analytics diff --git a/default.json b/default.json index 3f0e58c7457..4995e7e38d3 100644 --- a/default.json +++ b/default.json @@ -34,7 +34,8 @@ "analytics_enabled": false, "picture_in_picture_enabled": true, "analytics_app_key": "8ffae535f1836ed5f58fd5c8a11c00eca07c5438", - "analytics_server_url": "https://countly.wire.com/" + "analytics_server_url": "https://countly.wire.com/", + "enterprise_login_enabled": true }, "staging": { "application_id": "com.waz.zclient.dev", @@ -142,5 +143,6 @@ "limit_team_members_fetch_during_slow_sync": 2000, "picture_in_picture_enabled": false, "paginated_conversation_list_enabled": false, + "enterprise_login_enabled": false, "should_display_release_notes": true }