diff --git a/app/src/main/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCase.kt index b2ae0514e25..c1604192e11 100644 --- a/app/src/main/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCase.kt @@ -19,22 +19,17 @@ package com.wire.android.feature -import android.content.Context -import android.content.Intent -import android.os.Build import com.wire.android.appLogger -import com.wire.android.services.PersistentWebSocketService -import dagger.hilt.android.qualifiers.ApplicationContext +import com.wire.android.services.ServicesManager import javax.inject.Inject import javax.inject.Singleton @Singleton class StartPersistentWebsocketIfNecessaryUseCase @Inject constructor( - @ApplicationContext private val appContext: Context, + private val servicesManager: ServicesManager, private val shouldStartPersistentWebSocketService: ShouldStartPersistentWebSocketServiceUseCase ) { suspend operator fun invoke() { - val persistentWebSocketServiceIntent = PersistentWebSocketService.newIntent(appContext) shouldStartPersistentWebSocketService().let { when (it) { is ShouldStartPersistentWebSocketServiceUseCase.Result.Failure -> { @@ -43,33 +38,17 @@ class StartPersistentWebsocketIfNecessaryUseCase @Inject constructor( is ShouldStartPersistentWebSocketServiceUseCase.Result.Success -> { if (it.shouldStartPersistentWebSocketService) { - startForegroundService(persistentWebSocketServiceIntent) + appLogger.i("${TAG}: Starting PersistentWebsocketService") + servicesManager.startPersistentWebSocketService() } else { appLogger.i("${TAG}: Stopping PersistentWebsocketService, no user with persistent web socket enabled found") - appContext.stopService(persistentWebSocketServiceIntent) + servicesManager.stopPersistentWebSocketService() } } } } } - private fun startForegroundService(persistentWebSocketServiceIntent: Intent) { - when { - PersistentWebSocketService.isServiceStarted -> { - appLogger.i("${TAG}: PersistentWebsocketService already started, not starting again") - } - - else -> { - appLogger.i("${TAG}: Starting PersistentWebsocketService") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - appContext.startForegroundService(persistentWebSocketServiceIntent) - } else { - appContext.startService(persistentWebSocketServiceIntent) - } - } - } - } - companion object { const val TAG = "StartPersistentWebsocketIfNecessaryUseCase" } diff --git a/app/src/main/kotlin/com/wire/android/navigation/OtherDestinations.kt b/app/src/main/kotlin/com/wire/android/navigation/OtherDestinations.kt index 521232b688c..1ed00dd158e 100644 --- a/app/src/main/kotlin/com/wire/android/navigation/OtherDestinations.kt +++ b/app/src/main/kotlin/com/wire/android/navigation/OtherDestinations.kt @@ -100,8 +100,8 @@ object ReportBugDestination : IntentDirection { val dir = LogFileWriter.logsDirectory(context) val logsUris = context.getUrisOfFilesInDirectory(dir) val intent = context.multipleFileSharingIntent(logsUris) - intent.putExtra(Intent.EXTRA_EMAIL, arrayOf("wire-newandroid@wearezeta.zendesk.com")) - intent.putExtra(Intent.EXTRA_SUBJECT, "Bug Report - Wire Beta") + intent.putExtra(Intent.EXTRA_EMAIL, arrayOf(context.getString(R.string.send_bug_report_email))) + intent.putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.send_bug_report_subject)) intent.putExtra( Intent.EXTRA_TEXT, EmailComposer.reportBugEmailTemplate( diff --git a/app/src/main/kotlin/com/wire/android/navigation/TeamMigrationDestination.kt b/app/src/main/kotlin/com/wire/android/navigation/TeamMigrationDestination.kt new file mode 100644 index 00000000000..1ef93569367 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/navigation/TeamMigrationDestination.kt @@ -0,0 +1,46 @@ +/* + * 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.navigation + +import com.ramcosta.composedestinations.spec.Direction +import com.wire.android.ui.destinations.TeamMigrationConfirmationStepScreenDestination +import com.wire.android.ui.destinations.TeamMigrationDoneStepScreenDestination + +sealed class TeamMigrationDestination( + val direction: Direction +) { + + data object Confirmation : TeamMigrationDestination( + direction = TeamMigrationConfirmationStepScreenDestination + ) + + data object MigrationDone : TeamMigrationDestination( + direction = TeamMigrationDoneStepScreenDestination + ) + + val itemName: String get() = ITEM_NAME_PREFIX + this + + companion object { + private const val ITEM_NAME_PREFIX = "TeamMigrationNavigationItem." + fun fromRoute(fullRoute: String): TeamMigrationDestination? = + values().find { it.direction.route.getBaseRoute() == fullRoute.getBaseRoute() } + + fun values(): Array = + arrayOf(Confirmation, MigrationDone) + } +} diff --git a/app/src/main/kotlin/com/wire/android/services/PersistentWebSocketService.kt b/app/src/main/kotlin/com/wire/android/services/PersistentWebSocketService.kt index 475794c89b1..75087eaeac7 100644 --- a/app/src/main/kotlin/com/wire/android/services/PersistentWebSocketService.kt +++ b/app/src/main/kotlin/com/wire/android/services/PersistentWebSocketService.kt @@ -18,11 +18,13 @@ package com.wire.android.services +import android.app.ForegroundServiceStartNotAllowedException import android.app.Notification import android.app.Service import android.content.Context import android.content.Intent import android.content.pm.ServiceInfo +import android.os.Build import android.os.IBinder import androidx.core.app.NotificationCompat import androidx.core.app.ServiceCompat @@ -131,12 +133,29 @@ class PersistentWebSocketService : Service() { .setOngoing(true) .build() - ServiceCompat.startForeground( - this, - NotificationIds.PERSISTENT_NOTIFICATION_ID.ordinal, - notification, - ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE - ) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + ServiceCompat.startForeground( + this, + NotificationIds.PERSISTENT_NOTIFICATION_ID.ordinal, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE + ) + } catch (e: ForegroundServiceStartNotAllowedException) { + // ForegroundServiceStartNotAllowedException may be thrown on restarting service from the background. + // this is the only suggested workaround from google for now. + // https://issuetracker.google.com/issues/307329994#comment86 + appLogger.e("Failure while starting foreground: $e") + stopSelf() + } + } else { + ServiceCompat.startForeground( + this, + NotificationIds.PERSISTENT_NOTIFICATION_ID.ordinal, + notification, + ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE + ) + } } override fun onDestroy() { diff --git a/app/src/main/kotlin/com/wire/android/services/ServicesManager.kt b/app/src/main/kotlin/com/wire/android/services/ServicesManager.kt index 5a3aa8bdf13..48e00aaf977 100644 --- a/app/src/main/kotlin/com/wire/android/services/ServicesManager.kt +++ b/app/src/main/kotlin/com/wire/android/services/ServicesManager.kt @@ -18,7 +18,6 @@ package com.wire.android.services -import android.app.Service import android.content.Context import android.content.Intent import android.os.Build @@ -34,7 +33,6 @@ import kotlinx.coroutines.launch import org.jetbrains.annotations.VisibleForTesting import javax.inject.Inject import javax.inject.Singleton -import kotlin.reflect.KClass /** * This is helper class that should be used for starting/stopping any services. @@ -102,11 +100,15 @@ class ServicesManager @Inject constructor( // Persistent WebSocket fun startPersistentWebSocketService() { - startService(PersistentWebSocketService.newIntent(context)) + if (PersistentWebSocketService.isServiceStarted) { + appLogger.i("ServicesManager: PersistentWebsocketService already started, not starting again") + } else { + startService(PersistentWebSocketService.newIntent(context)) + } } fun stopPersistentWebSocketService() { - stopService(PersistentWebSocketService::class) + stopService(PersistentWebSocketService.newIntent(context)) } fun isPersistentWebSocketServiceRunning(): Boolean = @@ -121,8 +123,9 @@ class ServicesManager @Inject constructor( } } - private fun stopService(serviceClass: KClass) { - context.stopService(Intent(context, serviceClass.java)) + private fun stopService(intent: Intent) { + appLogger.i("ServicesManager: stopping service for $intent") + context.stopService(intent) } companion object { 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 c4e9082df00..5c5cf0d3d2e 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 @@ -79,6 +79,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.SelfQRCodeScreenDestination +import com.wire.android.ui.destinations.TeamMigrationScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination import com.wire.android.ui.home.conversations.search.HighlightName import com.wire.android.ui.home.conversations.search.HighlightSubtitle @@ -137,7 +138,7 @@ fun SelfUserProfileScreen( navigator.navigate(NavigationCommand(SelfQRCodeScreenDestination(viewModelSelf.userProfileState.userName))) }, onCreateAccount = { - // TODO: open screen to create a team + navigator.navigate(NavigationCommand(TeamMigrationScreenDestination)) }, isUserInCall = viewModelSelf::isUserInCall, ) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/BottomLineButtons.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/BottomLineButtons.kt new file mode 100644 index 00000000000..75213cea552 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/BottomLineButtons.kt @@ -0,0 +1,92 @@ +/* + * 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.userprofile.teammigration + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.isImeVisible +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.wire.android.R +import com.wire.android.ui.common.button.WireButtonState +import com.wire.android.ui.common.button.WirePrimaryButton +import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.preview.MultipleThemePreviews +import com.wire.android.ui.theme.WireTheme + +@OptIn(ExperimentalLayoutApi::class) +@Composable +fun BottomLineButtons( + isContinueButtonEnabled: Boolean, + modifier: Modifier = Modifier, + isBackButtonVisible: Boolean = true, + onBack: () -> Unit = { }, + onContinue: () -> Unit = { } +) { + Column( + modifier = modifier + .padding( + top = dimensions().spacing16x, + start = dimensions().spacing16x, + end = dimensions().spacing16x, + bottom = if (!WindowInsets.isImeVisible) { + dimensions().spacing32x + } else { + dimensions().spacing16x + } + ) + .imePadding() + ) { + if (isBackButtonVisible) { + WireSecondaryButton( + modifier = Modifier.fillMaxWidth(), + text = stringResource(R.string.personal_to_team_migration_back_button_label), + onClick = onBack + ) + } + + WirePrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding(top = dimensions().spacing6x), + text = stringResource(R.string.label_continue), + onClick = onContinue, + state = if (isContinueButtonEnabled) { + WireButtonState.Default + } else { + WireButtonState.Disabled + } + ) + } +} + +@MultipleThemePreviews +@Composable +private fun BottomLineButtonsPreview() { + WireTheme { + BottomLineButtons( + isContinueButtonEnabled = true + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/BulletList.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/BulletList.kt new file mode 100644 index 00000000000..71d086ad662 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/BulletList.kt @@ -0,0 +1,66 @@ +/* + * 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.userprofile.teammigration + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.ParagraphStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextIndent +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.sp +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireTypography +import com.wire.android.util.ui.PreviewMultipleThemes + +@Composable +fun BulletList(messages: List, modifier: Modifier = Modifier) { + val bullet = "\u2022" + val paragraphStyle = ParagraphStyle(textIndent = TextIndent(restLine = 12.sp)) + Text( + modifier = modifier, + text = buildAnnotatedString { + messages.forEach { + withStyle(style = paragraphStyle) { + append(bullet) + append("\t\t") + append(it) + } + } + }, + style = MaterialTheme.wireTypography.body01, + color = colorsScheme().onBackground + ) +} + +@PreviewMultipleThemes +@Composable +private fun BulletListPreview() { + WireTheme { + BulletList( + messages = listOf( + "Item 1", + "Item 2", + "Item 3" + ) + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/PersonalToTeamMigrationNavGraph.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/PersonalToTeamMigrationNavGraph.kt new file mode 100644 index 00000000000..a61c7f8cba4 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/PersonalToTeamMigrationNavGraph.kt @@ -0,0 +1,27 @@ +/* + * 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.userprofile.teammigration + +import com.ramcosta.composedestinations.annotation.NavGraph +import com.ramcosta.composedestinations.annotation.RootNavGraph + +@RootNavGraph +@NavGraph +annotation class PersonalToTeamMigrationNavGraph( + val start: Boolean = false +) diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationScreen.kt new file mode 100644 index 00000000000..9b47f33b311 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationScreen.kt @@ -0,0 +1,129 @@ +/* + * 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.userprofile.teammigration + +import android.graphics.drawable.ColorDrawable +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.hilt.navigation.compose.hiltViewModel +import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi +import com.ramcosta.composedestinations.DestinationsNavHost +import com.ramcosta.composedestinations.animations.defaults.RootNavGraphDefaultAnimations +import com.ramcosta.composedestinations.animations.rememberAnimatedNavHostEngine +import com.ramcosta.composedestinations.navigation.dependency +import com.wire.android.R +import com.wire.android.navigation.Navigator +import com.wire.android.navigation.TeamMigrationDestination +import com.wire.android.navigation.WireDestination +import com.wire.android.navigation.rememberNavigator +import com.wire.android.navigation.rememberTrackingAnimatedNavController +import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.ui.LocalActivity +import com.wire.android.ui.NavGraphs +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.preview.MultipleThemePreviews +import com.wire.android.ui.theme.WireTheme + +@OptIn(ExperimentalMaterialNavigationApi::class, ExperimentalAnimationApi::class) +@WireDestination(style = PopUpNavigationAnimation::class) +@Composable +fun TeamMigrationScreen( + navigator: Navigator, + modifier: Modifier = Modifier +) { + val navHostEngine = rememberAnimatedNavHostEngine( + rootDefaultAnimations = RootNavGraphDefaultAnimations.ACCOMPANIST_FADING + ) + val navController = rememberTrackingAnimatedNavController { + TeamMigrationDestination.fromRoute(it)?.itemName + } + + val isRunInPreview = LocalInspectionMode.current + + if (!isRunInPreview) { + val activity = LocalActivity.current + activity.window.setBackgroundDrawable( + ColorDrawable(colorsScheme().windowPersonalToTeamMigration.toArgb()) + ) + } + + Column( + modifier = modifier + .padding(top = dimensions().spacing32x) + .clip( + shape = RoundedCornerShape( + dimensions().corner16x, + dimensions().corner16x + ) + ) + .fillMaxSize() + .background(color = colorsScheme().surface) + ) { + IconButton( + modifier = Modifier.align(alignment = Alignment.End), + onClick = { + // TODO(next PR): show dialog to confirm exit before navigating back + navigator.navigateBack() + } + ) { + Icon( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = stringResource(R.string.personal_to_team_migration_close_icon_content_description) + ) + } + + DestinationsNavHost( + navGraph = NavGraphs.personalToTeamMigration, + engine = navHostEngine, + navController = navController, + dependenciesContainerBuilder = { + dependency(navigator) + dependency(NavGraphs.personalToTeamMigration) { + val parentEntry = remember(navBackStackEntry) { + navController.getBackStackEntry(NavGraphs.personalToTeamMigration.route) + } + hiltViewModel(parentEntry) + } + } + ) + } +} + +@MultipleThemePreviews +@Composable +private fun TeamMigrationScreenPreview() { + WireTheme { + TeamMigrationScreen(navigator = rememberNavigator { }) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt new file mode 100644 index 00000000000..4681531d628 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/TeamMigrationViewModel.kt @@ -0,0 +1,29 @@ +/* + * 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.userprofile.teammigration + +import androidx.compose.foundation.text.input.TextFieldState +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class TeamMigrationViewModel @Inject constructor() : ViewModel() { + val teamNameTextState: TextFieldState = TextFieldState() + val passwordTextState: TextFieldState = TextFieldState() +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step1/TeamMigrationTeamPlanStepScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step1/TeamMigrationTeamPlanStepScreen.kt new file mode 100644 index 00000000000..b7f99376995 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step1/TeamMigrationTeamPlanStepScreen.kt @@ -0,0 +1,242 @@ +/* + * 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.userprofile.teammigration.step1 + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Divider +import androidx.compose.material.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withLink +import androidx.compose.ui.text.withStyle +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.wire.android.R +import com.wire.android.navigation.WireDestination +import com.wire.android.navigation.style.SlideNavigationAnimation +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.destinations.TeamMigrationTeamNameStepScreenDestination +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireTypography +import com.wire.android.ui.userprofile.teammigration.BottomLineButtons +import com.wire.android.ui.userprofile.teammigration.PersonalToTeamMigrationNavGraph +import com.wire.android.util.CustomTabsHelper +import com.wire.android.util.ui.PreviewMultipleThemes + +@PersonalToTeamMigrationNavGraph(start = true) +@WireDestination( + style = SlideNavigationAnimation::class +) +@Composable +fun TeamMigrationTeamPlanStepScreen( + navigator: DestinationsNavigator +) { + TeamMigrationTeamPlanStepScreenContent( + onContinueButtonClicked = { + navigator.navigate(TeamMigrationTeamNameStepScreenDestination) + } + ) +} + +@Composable +private fun TeamMigrationTeamPlanStepScreenContent( + modifier: Modifier = Modifier, + onContinueButtonClicked: () -> Unit = { }, +) { + + Column( + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = modifier + .fillMaxWidth() + .weight(1f) + .padding( + start = dimensions().spacing16x, + end = dimensions().spacing16x + ) + .verticalScroll(rememberScrollState()) + ) { + Text( + modifier = Modifier + .padding(top = dimensions().spacing24x) + .align(alignment = Alignment.CenterHorizontally), + text = stringResource(R.string.personal_to_team_migration_step_label, 1), + style = MaterialTheme.wireTypography.subline01, + color = colorsScheme().secondaryText, + ) + Text( + modifier = Modifier + .padding( + top = dimensions().spacing8x, + bottom = dimensions().spacing56x + ) + .align(alignment = Alignment.CenterHorizontally), + text = stringResource(R.string.personal_to_team_migration_team_plan_step), + style = MaterialTheme.wireTypography.title01, + color = colorsScheme().onBackground + ) + + Text( + text = stringResource(R.string.personal_to_team_migration_team_plan_description), + style = MaterialTheme.wireTypography.body01, + color = colorsScheme().onBackground + ) + AdvantagesList() + LearnMoreWirePlans() + } + BottomLineButtons( + isContinueButtonEnabled = true, + isBackButtonVisible = false, + onContinue = onContinueButtonClicked + ) + } +} + +@Composable +private fun LearnMoreWirePlans( + modifier: Modifier = Modifier +) { + val context = LocalContext.current + val annotatedString = buildAnnotatedString { + val wirePlansLink = + stringResource(R.string.url_wire_plans) + withLink( + link = LinkAnnotation.Clickable( + tag = "plans", + styles = TextLinkStyles( + SpanStyle( + color = colorsScheme().onBackground, + textDecoration = TextDecoration.Underline + ), + ), + linkInteractionListener = { + CustomTabsHelper.launchUrl(context, wirePlansLink) + }, + ), + ) { + append(stringResource(R.string.personal_to_team_migration_team_plan_learn_more_about_wire_plan)) + } + } + Text( + modifier = modifier.padding(top = dimensions().spacing24x), + text = annotatedString, + style = MaterialTheme.wireTypography.body01, + color = colorsScheme().onBackground + ) +} + +@Composable +private fun AdvantagesList( + modifier: Modifier = Modifier +) { + val texts = listOf( + Pair( + stringResource(R.string.personal_to_team_migration_team_plan_admin_console_label), + stringResource(R.string.personal_to_team_migration_team_plan_admin_console_details), + ), + Pair( + stringResource(R.string.personal_to_team_migration_team_plan_effortless_collaboration_label), + stringResource(R.string.personal_to_team_migration_team_plan_effortless_collaboration_details), + ), + Pair( + stringResource(R.string.personal_to_team_migration_team_plan_larger_meetings_label), + stringResource(R.string.personal_to_team_migration_team_plan_larger_meetings_details), + ), + Pair( + stringResource(R.string.personal_to_team_migration_team_plan_availability_status_label), + stringResource(R.string.personal_to_team_migration_team_plan_availability_status_details), + ), + Pair( + stringResource(R.string.personal_to_team_migration_team_plan_upgrade_to_enterprise_label), + stringResource(R.string.personal_to_team_migration_team_plan_upgrade_to_enterprise_description), + ), + ) + Column(modifier = modifier) { + texts.forEach { (title, description) -> + val annotatedString = buildAnnotatedString { + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(title) + } + append(" ") + append(description) + } + AdvantageRow(annotatedString) + } + } +} + +@Composable +private fun AdvantageRow( + annotatedString: AnnotatedString, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Row( + modifier = Modifier.padding( + top = dimensions().spacing24x, + bottom = dimensions().spacing12x + ) + ) { + Icon( + painter = painterResource(id = R.drawable.ic_check), + contentDescription = null, + tint = Color.Unspecified, + modifier = Modifier.align(Alignment.CenterVertically) + ) + Text( + text = annotatedString, + style = MaterialTheme.wireTypography.body01, + modifier = Modifier.padding( + start = dimensions().spacing12x + ) + ) + } + Divider( + color = colorsScheme().dividerPersonalToTeamMigration + ) + } +} + +@PreviewMultipleThemes +@Composable +private fun TeamMigrationTeamPlanStepScreenPreview() { + WireTheme { + TeamMigrationTeamPlanStepScreenContent() + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step2/TeamMigrationTeamNameStepScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step2/TeamMigrationTeamNameStepScreen.kt new file mode 100644 index 00000000000..f9d91d44a4d --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step2/TeamMigrationTeamNameStepScreen.kt @@ -0,0 +1,162 @@ +/* + * 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.userprofile.teammigration.step2 + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.wire.android.R +import com.wire.android.navigation.WireDestination +import com.wire.android.navigation.style.SlideNavigationAnimation +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.textfield.WireTextField +import com.wire.android.ui.destinations.TeamMigrationConfirmationStepScreenDestination +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireTypography +import com.wire.android.ui.userprofile.teammigration.BottomLineButtons +import com.wire.android.ui.userprofile.teammigration.PersonalToTeamMigrationNavGraph +import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModel +import com.wire.android.util.ui.PreviewMultipleThemes + +@PersonalToTeamMigrationNavGraph +@WireDestination( + style = SlideNavigationAnimation::class +) +@Composable +fun TeamMigrationTeamNameStepScreen( + navigator: DestinationsNavigator, + teamMigrationViewModel: TeamMigrationViewModel +) { + TeamMigrationTeamNameStepScreenContent( + onContinueButtonClicked = { + navigator.navigate(TeamMigrationConfirmationStepScreenDestination) + }, + onBackButtonClicked = { + navigator.popBackStack() + }, + teamNameTextFieldState = teamMigrationViewModel.teamNameTextState + ) +} + +@Composable +private fun TeamMigrationTeamNameStepScreenContent( + teamNameTextFieldState: TextFieldState, + modifier: Modifier = Modifier, + onContinueButtonClicked: () -> Unit = { }, + onBackButtonClicked: () -> Unit = { } +) { + + Column( + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = modifier + .fillMaxWidth() + .weight(1f) + .padding( + start = dimensions().spacing16x, + end = dimensions().spacing16x + ) + .verticalScroll(rememberScrollState()) + ) { + Text( + modifier = Modifier + .padding(top = dimensions().spacing24x) + .align(alignment = Alignment.CenterHorizontally), + text = stringResource(R.string.personal_to_team_migration_step_label, 2), + style = MaterialTheme.wireTypography.subline01, + color = colorsScheme().secondaryText, + ) + Text( + modifier = Modifier + .padding( + top = dimensions().spacing8x, + bottom = dimensions().spacing56x + ) + .align(alignment = Alignment.CenterHorizontally), + text = stringResource(R.string.personal_to_team_migration_team_name_step), + style = MaterialTheme.wireTypography.title01, + color = colorsScheme().onBackground + ) + + Text( + text = stringResource(R.string.personal_to_team_migration_team_name_description), + style = MaterialTheme.wireTypography.body01, + color = colorsScheme().onBackground + ) + + TeamNameInput( + modifier = Modifier + .fillMaxWidth() + .padding(top = dimensions().spacing24x), + textFieldState = teamNameTextFieldState, + ) + } + val isContinueButtonEnabled = teamNameTextFieldState.text.isNotEmpty() + BottomLineButtons( + isContinueButtonEnabled = isContinueButtonEnabled, + onContinue = onContinueButtonClicked, + onBack = onBackButtonClicked + ) + } +} + +@Composable +private fun TeamNameInput( + textFieldState: TextFieldState, + modifier: Modifier = Modifier +) { + val keyboardController = LocalSoftwareKeyboardController.current + + WireTextField( + textState = textFieldState, + labelText = stringResource(R.string.personal_to_team_migration_team_name_step), + onKeyboardAction = { + keyboardController?.hide() + }, + placeholderText = stringResource(R.string.personal_to_team_migration_team_name_text_field_placeholder), + modifier = modifier + .testTag("teamNameFieldTeamMigration"), + testTag = "teamNameFieldTeamMigration" + ) +} + +@PreviewMultipleThemes +@Composable +private fun TeamMigrationTeamNameStepScreenPreview() { + WireTheme { + TeamMigrationTeamNameStepScreenContent( + teamNameTextFieldState = rememberTextFieldState("Your Team") + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt new file mode 100644 index 00000000000..463cf6d571b --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step3/TeamMigrationConfirmationStepScreen.kt @@ -0,0 +1,238 @@ +/* + * 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.userprofile.teammigration.step3 + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withLink +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.wire.android.R +import com.wire.android.navigation.WireDestination +import com.wire.android.navigation.style.SlideNavigationAnimation +import com.wire.android.ui.common.WireCheckbox +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.textfield.DefaultPassword +import com.wire.android.ui.common.textfield.WirePasswordTextField +import com.wire.android.ui.destinations.TeamMigrationDoneStepScreenDestination +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireTypography +import com.wire.android.ui.userprofile.teammigration.BottomLineButtons +import com.wire.android.ui.userprofile.teammigration.BulletList +import com.wire.android.ui.userprofile.teammigration.PersonalToTeamMigrationNavGraph +import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModel +import com.wire.android.util.CustomTabsHelper +import com.wire.android.util.ui.PreviewMultipleThemes + +@PersonalToTeamMigrationNavGraph +@WireDestination( + style = SlideNavigationAnimation::class +) +@Composable +fun TeamMigrationConfirmationStepScreen( + navigator: DestinationsNavigator, + teamMigrationViewModel: TeamMigrationViewModel +) { + TeamMigrationConfirmationStepScreenContent( + onContinueButtonClicked = { + // TODO: call the API to migrate the user to the team, if successful navigate to next screen + navigator.navigate(TeamMigrationDoneStepScreenDestination) + }, + onBackPressed = { + navigator.popBackStack() + }, + passwordTextState = teamMigrationViewModel.passwordTextState + ) +} + +@Composable +private fun TeamMigrationConfirmationStepScreenContent( + passwordTextState: TextFieldState, + modifier: Modifier = Modifier, + onContinueButtonClicked: () -> Unit = { }, + onBackPressed: () -> Unit = { } +) { + + val agreedToMigrationTerms = remember { + mutableStateOf(false) + } + + val acceptedWireTermsOfUse = remember { + mutableStateOf(false) + } + + Column( + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = modifier + .fillMaxWidth() + .weight(1f) + .padding( + start = dimensions().spacing16x, + end = dimensions().spacing16x + ) + .verticalScroll(rememberScrollState()) + ) { + Text( + modifier = Modifier + .padding(top = dimensions().spacing24x) + .align(alignment = Alignment.CenterHorizontally), + text = stringResource(R.string.personal_to_team_migration_step_label, 3), + style = MaterialTheme.wireTypography.subline01, + color = colorsScheme().secondaryText, + ) + Text( + modifier = Modifier + .padding( + top = dimensions().spacing8x, + bottom = dimensions().spacing56x + ) + .align(alignment = Alignment.CenterHorizontally), + text = stringResource(R.string.personal_to_team_migration_confirmation_step), + style = MaterialTheme.wireTypography.title01, + color = colorsScheme().onBackground + ) + val messages = listOf( + stringResource(R.string.personal_to_team_migration_confirmation_step_bullet_list_first_item), + stringResource(R.string.personal_to_team_migration_confirmation_step_bullet_list_second_item), + stringResource(R.string.personal_to_team_migration_confirmation_step_bullet_list_third_item), + ) + BulletList(messages) + + PasswordInput( + modifier = Modifier + .fillMaxWidth() + .padding( + top = dimensions().spacing56x, + bottom = dimensions().spacing56x + ), + passwordState = passwordTextState, + ) + Row { + WireCheckbox( + checked = agreedToMigrationTerms.value, + onCheckedChange = { agreedToMigrationTerms.value = it } + ) + + Text( + modifier = Modifier.align(Alignment.CenterVertically), + text = stringResource(R.string.personal_to_team_migration_confirmation_step_terms), + style = MaterialTheme.wireTypography.subline01, + ) + } + Row { + WireCheckbox( + checked = acceptedWireTermsOfUse.value, + onCheckedChange = { acceptedWireTermsOfUse.value = it } + ) + WireTermsOfUseWithLink() + } + } + val isContinueButtonEnabled = + passwordTextState.text.isNotEmpty() && agreedToMigrationTerms.value && acceptedWireTermsOfUse.value + BottomLineButtons( + isContinueButtonEnabled = isContinueButtonEnabled, + onContinue = onContinueButtonClicked, + onBack = onBackPressed + ) + } +} + +@Composable +private fun RowScope.WireTermsOfUseWithLink() { + val context = LocalContext.current + val annotatedString = buildAnnotatedString { + + append(stringResource(R.string.personal_to_team_migration_confirmation_step_wire_terms_of_use_accept_label)) + append(" ") + val urlTermsOfUse = stringResource(R.string.url_terms_of_use_legal) + withLink( + link = LinkAnnotation.Clickable( + tag = "terms", + styles = TextLinkStyles(SpanStyle(color = colorsScheme().primary)), + linkInteractionListener = { + CustomTabsHelper.launchUrl(context, urlTermsOfUse) + }, + ), + ) { + append(stringResource(R.string.personal_to_team_migration_confirmation_step_wire_terms_of_use_label)) + } + } + + Text( + modifier = Modifier.align(Alignment.CenterVertically), + text = annotatedString, + style = MaterialTheme.wireTypography.subline01, + ) +} + +@Composable +private fun PasswordInput( + passwordState: TextFieldState, + modifier: Modifier = Modifier +) { + val keyboardController = LocalSoftwareKeyboardController.current + + WirePasswordTextField( + textState = passwordState, + labelText = stringResource(R.string.personal_to_team_migration_confirmation_step_password_field_label), + keyboardOptions = KeyboardOptions.DefaultPassword, + placeholderText = stringResource(R.string.personal_to_team_migration_confirmation_step_password_field_placeholder), + onKeyboardAction = { + keyboardController?.hide() + }, + modifier = modifier + .testTag("passwordFieldTeamMigration"), + testTag = "passwordFieldTeamMigration" + ) +} + +@PreviewMultipleThemes +@Composable +private fun TeamMigrationConfirmationStepPreview() { + WireTheme { + TeamMigrationConfirmationStepScreenContent( + passwordTextState = rememberTextFieldState() + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt new file mode 100644 index 00000000000..c661e360813 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/userprofile/teammigration/step4/TeamMigrationDoneStepScreen.kt @@ -0,0 +1,179 @@ +/* + * 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.userprofile.teammigration.step4 + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +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.SlideNavigationAnimation +import com.wire.android.ui.common.button.WirePrimaryButton +import com.wire.android.ui.common.button.WireSecondaryButton +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dimensions +import com.wire.android.ui.common.spacers.VerticalSpace.x32 +import com.wire.android.ui.destinations.HomeScreenDestination +import com.wire.android.ui.theme.WireTheme +import com.wire.android.ui.theme.wireTypography +import com.wire.android.ui.userprofile.teammigration.BulletList +import com.wire.android.ui.userprofile.teammigration.PersonalToTeamMigrationNavGraph +import com.wire.android.ui.userprofile.teammigration.TeamMigrationViewModel +import com.wire.android.util.CustomTabsHelper +import com.wire.android.util.ui.PreviewMultipleThemes + +@PersonalToTeamMigrationNavGraph +@WireDestination( + style = SlideNavigationAnimation::class +) +@Composable +fun TeamMigrationDoneStepScreen( + navigator: Navigator, + teamMigrationViewModel: TeamMigrationViewModel +) { + val context = LocalContext.current + + val teamManagementUrl = stringResource(R.string.url_team_management) + TeamMigrationDoneStepContent( + onBackToWireClicked = { + navigator.navigate( + NavigationCommand( + HomeScreenDestination, + BackStackMode.CLEAR_WHOLE + ) + ) + }, + onOpenTeamManagementClicked = { + CustomTabsHelper.launchUrl(context, teamManagementUrl) + }, + teamName = teamMigrationViewModel.teamNameTextState.text.toString() + ) + + BackHandler { } +} + +@Composable +private fun TeamMigrationDoneStepContent( + onBackToWireClicked: () -> Unit, + onOpenTeamManagementClicked: () -> Unit, + teamName: String, + modifier: Modifier = Modifier +) { + Column( + modifier = Modifier + .fillMaxSize() + ) { + Column( + modifier = modifier + .fillMaxWidth() + .weight(1f) + .padding( + start = dimensions().spacing16x, + end = dimensions().spacing16x + ) + .verticalScroll(rememberScrollState()) + ) { + Text( + modifier = Modifier + .padding(top = dimensions().spacing24x) + .align(alignment = Alignment.CenterHorizontally), + text = stringResource(R.string.personal_to_team_migration_step_label, 4), + style = MaterialTheme.wireTypography.subline01, + color = colorsScheme().secondaryText, + ) + Text( + modifier = Modifier + .padding( + top = dimensions().spacing8x, + bottom = dimensions().spacing56x + ) + .align(alignment = Alignment.CenterHorizontally), + text = stringResource(R.string.personal_to_team_migration_done_step, teamName), + style = MaterialTheme.wireTypography.title01, + color = colorsScheme().onBackground + ) + + Text( + text = stringResource( + R.string.personal_to_team_migration_done_step_now_team_member_label, + teamName + ), + style = MaterialTheme.wireTypography.body01, + color = colorsScheme().onBackground + ) + + x32() + + Text( + text = stringResource(R.string.personal_to_team_migration_done_step_go_to_team_label), + style = MaterialTheme.wireTypography.body01, + color = colorsScheme().onBackground + ) + + val messages = listOf( + stringResource(R.string.personal_to_team_migration_done_step_bullet_list_first_item), + stringResource(R.string.personal_to_team_migration_done_step_bullet_list_second_item) + ) + BulletList(messages) + } + WireSecondaryButton( + modifier = Modifier + .fillMaxWidth() + .padding( + start = dimensions().spacing16x, + end = dimensions().spacing16x + ), + text = stringResource(R.string.personal_to_team_migration_back_to_wire_button), + onClick = onBackToWireClicked + ) + WirePrimaryButton( + modifier = Modifier + .fillMaxWidth() + .padding( + top = dimensions().spacing6x, + start = dimensions().spacing16x, + end = dimensions().spacing16x, + bottom = dimensions().spacing32x + ), + text = stringResource(R.string.to_team_management_action), + onClick = onOpenTeamManagementClicked + ) + } +} + +@PreviewMultipleThemes +@Composable +private fun TeamMigrationDoneStepScreenPreview() { + WireTheme { + TeamMigrationDoneStepContent({}, {}, teamName = "teamName") + } +} diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 00000000000..d0a2612a8ab --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7c1e29a1170..7019ad4656c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -221,6 +221,8 @@ https://support.wire.com/hc/articles/12434725011485 https://support.wire.com/hc/articles/6655706999581 https://support.wire.com/hc/articles/207859815 + https://wire.com/pricing + https://teams.wire.com/ Vault Archive @@ -1158,7 +1160,12 @@ In group conversations, the group admin can overwrite this setting. Choose Backup File Enter password This backup is password protected. + + \"Select email provider:\" + Bug Report + wire-newandroid@wearezeta.zendesk.com + Save File Share File Incompatible backup @@ -1509,4 +1516,45 @@ In group conversations, the group admin can overwrite this setting. Force API versioning update ⚠️ Break Session Update + + + Step %1$d of 4 + Back to Wire + Back + Close team migration flow + + Team Account + Transform your personal account into a team account to get more out of your collaboration. + Admin Console: + Invite team members and manage settings. + Effortless Collaboration: + Communicate with guests and external parties. + Larger Meetings: + Join video conferences up to 150 participants. + Availability status: + Let your team know if you’re available, busy or away. + Upgrade to Enterprise: + Get additional features and premium support. + Learn more about Wire\’s plans + + Team Name + Select a name for your team. You can change it at any time. + Your Team + + Confirmation + You create a team and transfer your personal account into a team account + As the team owner you can invite and remove team members and manage team settings + This change is permanent and irrevocable + I agree to the migration terms and understand that this change is irreversible. + I accept + Wire\’s Terms of Use. + Password of your personal account + Enter password + + Congratulations %1$s! + You\’re now the owner of the team %1$s. + Go to Team Management to: + Invite your first team members, and start working together + Customize your team settings + diff --git a/app/src/test/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCaseTest.kt b/app/src/test/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCaseTest.kt index 0288e4bae7e..80e3333bc92 100644 --- a/app/src/test/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCaseTest.kt +++ b/app/src/test/kotlin/com/wire/android/feature/StartPersistentWebsocketIfNecessaryUseCaseTest.kt @@ -17,8 +17,7 @@ */ package com.wire.android.feature -import android.content.ComponentName -import android.content.Context +import com.wire.android.services.ServicesManager import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.every @@ -41,7 +40,7 @@ class StartPersistentWebsocketIfNecessaryUseCaseTest { sut.invoke() // then - verify(exactly = 1) { arrangement.applicationContext.startService(any()) } + verify(exactly = 1) { arrangement.servicesManager.startPersistentWebSocketService() } } @Test @@ -56,7 +55,7 @@ class StartPersistentWebsocketIfNecessaryUseCaseTest { sut.invoke() // then - verify(exactly = 0) { arrangement.applicationContext.startService(any()) } + verify(exactly = 0) { arrangement.servicesManager.startPersistentWebSocketService() } } inner class Arrangement { @@ -65,16 +64,16 @@ class StartPersistentWebsocketIfNecessaryUseCaseTest { lateinit var shouldStartPersistentWebSocketServiceUseCase: ShouldStartPersistentWebSocketServiceUseCase @MockK - lateinit var applicationContext: Context + lateinit var servicesManager: ServicesManager init { MockKAnnotations.init(this, relaxUnitFun = true) - every { applicationContext.startService(any()) } returns ComponentName.createRelative("dummy", "class") - every { applicationContext.stopService(any()) } returns true + every { servicesManager.startPersistentWebSocketService() } returns Unit + every { servicesManager.stopPersistentWebSocketService() } returns Unit } fun arrange() = this to StartPersistentWebsocketIfNecessaryUseCase( - applicationContext, + servicesManager, shouldStartPersistentWebSocketServiceUseCase ) diff --git a/core/navigation/src/main/kotlin/com/wire/android/navigation/style/DefaultNavGraphAnimations.kt b/core/navigation/src/main/kotlin/com/wire/android/navigation/style/DefaultNavGraphAnimations.kt index f02221405dc..07b294c6c82 100644 --- a/core/navigation/src/main/kotlin/com/wire/android/navigation/style/DefaultNavGraphAnimations.kt +++ b/core/navigation/src/main/kotlin/com/wire/android/navigation/style/DefaultNavGraphAnimations.kt @@ -17,11 +17,9 @@ */ package com.wire.android.navigation.style -import androidx.compose.animation.ExperimentalAnimationApi import com.ramcosta.composedestinations.animations.defaults.NestedNavGraphDefaultAnimations import com.ramcosta.composedestinations.animations.defaults.RootNavGraphDefaultAnimations -@OptIn(ExperimentalAnimationApi::class) val DefaultRootNavGraphAnimations = RootNavGraphDefaultAnimations( enterTransition = { with(DefaultNavigationAnimation) { enterTransition() } }, exitTransition = { with(DefaultNavigationAnimation) { exitTransition() } }, @@ -29,7 +27,6 @@ val DefaultRootNavGraphAnimations = RootNavGraphDefaultAnimations( popExitTransition = { with(DefaultNavigationAnimation) { popExitTransition() } }, ) -@OptIn(ExperimentalAnimationApi::class) val DefaultNestedNavGraphAnimations = NestedNavGraphDefaultAnimations( enterTransition = { with(DefaultNavigationAnimation) { enterTransition() } }, exitTransition = { with(DefaultNavigationAnimation) { exitTransition() } }, diff --git a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt index 5f02a8daf2a..31bd3f8d98c 100644 --- a/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt +++ b/core/ui-common/src/main/kotlin/com/wire/android/ui/theme/WireColorScheme.kt @@ -61,6 +61,8 @@ data class WireColorScheme( val switchEnabledChecked: Color, val switchDisabledChecked: Color, val switchEnabledUnchecked: Color, val switchDisabledUnchecked: Color, val divider: Color, + val dividerPersonalToTeamMigration: Color, + val windowPersonalToTeamMigration: Color, val secondaryText: Color, val outline: Color, val outlineVariant: Color, @@ -175,6 +177,8 @@ private val LightWireColorScheme = WireColorScheme( switchEnabledChecked = WireColorPalette.LightGreen500, switchDisabledChecked = WireColorPalette.LightGreen200, switchEnabledUnchecked = WireColorPalette.Gray70, switchDisabledUnchecked = WireColorPalette.Gray50, divider = WireColorPalette.Gray20, + dividerPersonalToTeamMigration = WireColorPalette.Gray40, + windowPersonalToTeamMigration = WireColorPalette.Gray100, secondaryText = WireColorPalette.Gray70, outline = WireColorPalette.Gray40, outlineVariant = WireColorPalette.Gray20, @@ -321,6 +325,8 @@ private val DarkWireColorScheme = WireColorScheme( switchEnabledChecked = WireColorPalette.DarkGreen500, switchDisabledChecked = WireColorPalette.DarkGreen200, switchEnabledUnchecked = WireColorPalette.Gray40, switchDisabledUnchecked = WireColorPalette.Gray60, divider = WireColorPalette.Gray100, + dividerPersonalToTeamMigration = WireColorPalette.Gray90, + windowPersonalToTeamMigration = WireColorPalette.Gray100, secondaryText = WireColorPalette.Gray60, outline = WireColorPalette.Gray90, outlineVariant = WireColorPalette.Gray100,