diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f76f5432ac..f3f51dcd5f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -238,6 +238,15 @@ android:authorities="${applicationId}.firebaseinitprovider" tools:node="remove" /> + + + ) -> Unit - operator fun invoke(context: Context, enrollmentResultHandler: (Either) -> Unit) { + operator fun invoke( + context: Context, + isNewClient: Boolean, + enrollmentResultHandler: (Either) -> Unit + ) { this.enrollmentResultHandler = enrollmentResultHandler scope.launch { - enrollE2EI.initialEnrollment().fold({ + enrollE2EI.initialEnrollment(isNewClientRegistration = isNewClient).fold({ enrollmentResultHandler(Either.Left(it)) }, { if (it is E2EIEnrollmentResult.Initialized) { initialEnrollmentResult = it - OAuthUseCase(context, it.target, it.oAuthState).launch( + OAuthUseCase(context, it.target, it.oAuthClaims, it.oAuthState).launch( context.getActivity()!!.activityResultRegistry, ::oAuthResultHandler ) diff --git a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt index 6e0476116e..12c2885903 100644 --- a/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/e2ei/OAuthUseCase.kt @@ -28,6 +28,8 @@ import androidx.activity.result.ActivityResultRegistry import androidx.activity.result.contract.ActivityResultContracts import com.wire.android.appLogger import com.wire.android.util.deeplink.DeepLinkProcessor +import com.wire.android.util.removeQueryParams +import kotlinx.serialization.json.JsonObject import net.openid.appauth.AppAuthConfiguration import net.openid.appauth.AuthState import net.openid.appauth.AuthorizationException @@ -41,7 +43,9 @@ import net.openid.appauth.ResponseTypeValues import net.openid.appauth.browser.BrowserAllowList import net.openid.appauth.browser.VersionedBrowserMatcher import net.openid.appauth.connectivity.ConnectionBuilder +import org.json.JSONObject import java.net.HttpURLConnection +import java.net.URI import java.net.URL import java.security.MessageDigest import java.security.SecureRandom @@ -52,7 +56,7 @@ import javax.net.ssl.SSLContext import javax.net.ssl.TrustManager import javax.net.ssl.X509TrustManager -class OAuthUseCase(context: Context, private val authUrl: String, oAuthState: String?) { +class OAuthUseCase(context: Context, private val authUrl: String, private val claims: JsonObject, oAuthState: String?) { private var authState: AuthState = oAuthState?.let { AuthState.jsonDeserialize(it) } ?: AuthState() @@ -117,7 +121,7 @@ class OAuthUseCase(context: Context, private val authUrl: String, oAuthState: St handleActivityResult(result, resultHandler) } AuthorizationServiceConfiguration.fetchFromUrl( - Uri.parse(authUrl.plus(IDP_CONFIGURATION_PATH)), + Uri.parse(URI(authUrl).removeQueryParams().toString().plus(IDP_CONFIGURATION_PATH)), { configuration, ex -> if (ex == null) { authServiceConfig = configuration!! @@ -177,7 +181,8 @@ class OAuthUseCase(context: Context, private val authUrl: String, oAuthState: St AuthorizationRequest.Scope.EMAIL, AuthorizationRequest.Scope.PROFILE, AuthorizationRequest.Scope.OFFLINE_ACCESS - ).build() + ).setClaims(JSONObject(claims.toString())) + .build() private fun AuthorizationRequest.Builder.setCodeVerifier(): AuthorizationRequest.Builder { val codeVerifier = getCodeVerifier() diff --git a/app/src/main/kotlin/com/wire/android/migration/feature/MigrateClientsDataUseCase.kt b/app/src/main/kotlin/com/wire/android/migration/feature/MigrateClientsDataUseCase.kt index b4f2378bf8..eb2a9cd3cb 100644 --- a/app/src/main/kotlin/com/wire/android/migration/feature/MigrateClientsDataUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/migration/feature/MigrateClientsDataUseCase.kt @@ -47,7 +47,7 @@ class MigrateClientsDataUseCase @Inject constructor( private val scalaUserDBProvider: ScalaUserDatabaseProvider, private val userDataStoreProvider: UserDataStoreProvider ) { - @Suppress("ReturnCount") + @Suppress("ReturnCount", "ComplexMethod") suspend operator fun invoke(userId: UserId, isFederated: Boolean): Either = scalaUserDBProvider.clientDAO(userId.value).flatMap { clientDAO -> val clientId = clientDAO.clientInfo()?.clientId?.let { ClientId(it) } @@ -103,6 +103,19 @@ class MigrateClientsDataUseCase @Inject constructor( userDataStoreProvider.getOrCreate(userId).setInitialSyncCompleted() } } + + is RegisterClientResult.E2EICertificateRequired -> + withTimeoutOrNull(SYNC_START_TIMEOUT) { + syncManager.waitUntilStartedOrFailure() + }.let { + it ?: Either.Left(NetworkFailure.NoNetworkConnection(null)) + }.flatMap { + syncManager.waitUntilLiveOrFailure() + .onSuccess { + userDataStoreProvider.getOrCreate(userId).setInitialSyncCompleted() + TODO() // TODO: ask question about this! + } + } } } } 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 e1b4e05ae2..b3bf8103ea 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -66,6 +66,7 @@ import com.wire.android.ui.common.snackbar.LocalSnackbarHostState import com.wire.android.ui.common.topappbar.CommonTopAppBar import com.wire.android.ui.common.topappbar.CommonTopAppBarViewModel import com.wire.android.ui.destinations.ConversationScreenDestination +import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.ImportMediaScreenDestination @@ -158,9 +159,9 @@ class WireActivity : AppCompatActivity() { val startDestination = when (viewModel.initialAppState) { InitialAppState.NOT_MIGRATED -> MigrationScreenDestination InitialAppState.NOT_LOGGED_IN -> WelcomeScreenDestination - InitialAppState.LOGGED_IN -> HomeScreenDestination - } - + InitialAppState.ENROLL_E2EI -> E2EIEnrollmentScreenDestination + InitialAppState.LOGGED_IN -> HomeScreenDestination + } appLogger.i("$TAG composable content") setComposableContent(startDestination) { appLogger.i("$TAG splash hide") diff --git a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt index 5d934eb6b9..84574392d9 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt @@ -29,6 +29,7 @@ import com.wire.android.appLogger import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.AuthServerConfigProvider import com.wire.android.di.KaliumCoreLogic +import com.wire.android.di.ObserveIfE2EIRequiredDuringLoginUseCaseProvider import com.wire.android.di.ObserveScreenshotCensoringConfigUseCaseProvider import com.wire.android.di.ObserveSyncStateUseCaseProvider import com.wire.android.feature.AccountSwitchUseCase @@ -110,6 +111,7 @@ class WireActivityViewModel @Inject constructor( private val currentScreenManager: CurrentScreenManager, private val observeScreenshotCensoringConfigUseCaseProviderFactory: ObserveScreenshotCensoringConfigUseCaseProvider.Factory, private val globalDataStore: GlobalDataStore, + private val observeIfE2EIRequiredDuringLoginUseCaseProviderFactory: ObserveIfE2EIRequiredDuringLoginUseCaseProvider.Factory ) : ViewModel() { var globalAppState: GlobalAppState by mutableStateOf(GlobalAppState()) @@ -142,12 +144,16 @@ class WireActivityViewModel @Inject constructor( private val _observeSyncFlowState: MutableStateFlow = MutableStateFlow(null) val observeSyncFlowState: StateFlow = _observeSyncFlowState + private val _observeE2EIState: MutableStateFlow = MutableStateFlow(null) + private val observeE2EIState: StateFlow = _observeE2EIState + init { observeSyncState() observeUpdateAppState() observeNewClientState() observeScreenshotCensoringConfigState() observeAppThemeState() + observerE2EIState() } private fun observeAppThemeState() { @@ -160,6 +166,18 @@ class WireActivityViewModel @Inject constructor( } } + fun observerE2EIState() { + viewModelScope.launch(dispatchers.io()) { + observeUserId + .flatMapLatest { + it?.let { observeIfE2EIRequiredDuringLoginUseCaseProviderFactory.create(it).observeIfE2EIIsRequiredDuringLogin() } + ?: flowOf(null) + } + .distinctUntilChanged() + .collect { _observeE2EIState.emit(it) } + } + } + private fun observeSyncState() { viewModelScope.launch(dispatchers.io()) { observeUserId @@ -233,6 +251,7 @@ class WireActivityViewModel @Inject constructor( get() = when { shouldMigrate() -> InitialAppState.NOT_MIGRATED shouldLogIn() -> InitialAppState.NOT_LOGGED_IN + blockedByE2EI() -> InitialAppState.ENROLL_E2EI else -> InitialAppState.LOGGED_IN } @@ -263,8 +282,10 @@ class WireActivityViewModel @Inject constructor( // to handle the deepLinks above user needs to be Logged in // do nothing, already handled by initialAppState } + result is DeepLinkResult.JoinConversation -> onConversationInviteDeepLink(result.code, result.key, result.domain, onOpenConversation) + result != null -> onResult(result) result is DeepLinkResult.Unknown -> appLogger.e("unknown deeplink result $result") } @@ -391,6 +412,10 @@ class WireActivityViewModel @Inject constructor( fun shouldLogIn(): Boolean = !hasValidCurrentSession() + fun blockedByE2EI(): Boolean { + return observeE2EIState.value == true + } + private fun hasValidCurrentSession(): Boolean = runBlocking { // TODO: the usage of currentSessionFlow is a temporary solution, it should be replaced with a proper solution currentSessionFlow().first().let { @@ -510,5 +535,5 @@ data class GlobalAppState( ) enum class InitialAppState { - NOT_MIGRATED, NOT_LOGGED_IN, LOGGED_IN + NOT_MIGRATED, NOT_LOGGED_IN, LOGGED_IN, ENROLL_E2EI } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt index d8b8fda010..c7a075c102 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/create/code/CreateAccountCodeViewModel.kt @@ -187,6 +187,11 @@ class CreateAccountCodeViewModel @Inject constructor( is RegisterClientResult.Success -> { onSuccess() } + + is RegisterClientResult.E2EICertificateRequired -> { + // TODO + onSuccess() + } } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt index 1e8e7c3122..4f5e3911eb 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/model/Device.kt @@ -51,6 +51,20 @@ data class Device( mlsPublicKeys = client.mlsPublicKeys, e2eiCertificateStatus = e2eiCertificateStatus ) + + fun updateFromClient(client: Client): Device = copy( + name = client.displayName(), + clientId = client.id, + registrationTime = client.registrationTime?.toIsoDateTimeString(), + lastActiveInWholeWeeks = client.lastActiveInWholeWeeks(), + isValid = client.isValid, + isVerifiedProteus = client.isVerified, + mlsPublicKeys = client.mlsPublicKeys, + ) + + fun updateE2EICertificateStatus(e2eiCertificateStatus: CertificateStatus): Device = copy( + e2eiCertificateStatus = e2eiCertificateStatus + ) } /** diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt index 72c89dbeaf..ba16c4bb82 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceScreen.kt @@ -61,6 +61,7 @@ import com.wire.android.ui.common.textfield.clearAutofillTree 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 @@ -81,11 +82,14 @@ fun RegisterDeviceScreen(navigator: Navigator) { is RegisterDeviceFlowState.Success -> { navigator.navigate( NavigationCommand( - destination = if (flowState.initialSyncCompleted) HomeScreenDestination else InitialSyncScreenDestination, + destination = if (flowState.isE2EIRequired) E2EIEnrollmentScreenDestination + else if (flowState.initialSyncCompleted) HomeScreenDestination + else InitialSyncScreenDestination, backStackMode = BackStackMode.CLEAR_WHOLE ) ) } + is RegisterDeviceFlowState.TooManyDevices -> navigator.navigate(NavigationCommand(RemoveDeviceScreenDestination)) else -> RegisterDeviceContent( @@ -189,6 +193,7 @@ private fun PasswordTextField(state: RegisterDeviceState, onPasswordChange: (Tex state = when (state.flowState) { is RegisterDeviceFlowState.Error.InvalidCredentialsError -> WireTextFieldState.Error(stringResource(id = R.string.remove_device_invalid_password)) + else -> WireTextFieldState.Default }, imeAction = ImeAction.Done, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt index c0169ccdc8..766959596d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceState.kt @@ -20,17 +20,26 @@ package com.wire.android.ui.authentication.devices.register import androidx.compose.ui.text.input.TextFieldValue import com.wire.kalium.logic.CoreFailure +import com.wire.kalium.logic.data.conversation.ClientId +import com.wire.kalium.logic.data.user.UserId data class RegisterDeviceState( val password: TextFieldValue = TextFieldValue(""), val continueEnabled: Boolean = false, val flowState: RegisterDeviceFlowState = RegisterDeviceFlowState.Default ) + sealed class RegisterDeviceFlowState { object Default : RegisterDeviceFlowState() object Loading : RegisterDeviceFlowState() object TooManyDevices : RegisterDeviceFlowState() - data class Success(val initialSyncCompleted: Boolean) : RegisterDeviceFlowState() + data class Success( + val initialSyncCompleted: Boolean, + val isE2EIRequired: Boolean, + val clientId: ClientId, + val userId: UserId? = null + ) : RegisterDeviceFlowState() + sealed class Error : RegisterDeviceFlowState() { object InvalidCredentialsError : Error() data class GenericError(val coreFailure: CoreFailure) : Error() diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt index bc9ac87c01..b7d0354d7f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/register/RegisterDeviceViewModel.kt @@ -83,8 +83,25 @@ class RegisterDeviceViewModel @Inject constructor( )) { is RegisterClientResult.Failure.TooManyClients -> updateFlowState(RegisterDeviceFlowState.TooManyDevices) + is RegisterClientResult.Success -> - updateFlowState(RegisterDeviceFlowState.Success(userDataStore.initialSyncCompleted.first())) + updateFlowState( + RegisterDeviceFlowState.Success( + userDataStore.initialSyncCompleted.first(), + false, + registerDeviceResult.client.id + ) + ) + + is RegisterClientResult.E2EICertificateRequired -> + updateFlowState( + RegisterDeviceFlowState.Success( + userDataStore.initialSyncCompleted.first(), + true, + registerDeviceResult.client.id, + registerDeviceResult.userId + ) + ) is RegisterClientResult.Failure.Generic -> state = state.copy( continueEnabled = true, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt index 1e8f40bdbf..a0c36cdc40 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceScreen.kt @@ -58,6 +58,7 @@ import com.wire.android.ui.common.divider.WireDivider import com.wire.android.ui.common.rememberTopBarElevationState import com.wire.android.ui.common.textfield.clearAutofillTree 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.util.dialogErrorStrings @@ -73,9 +74,11 @@ fun RemoveDeviceScreen(navigator: Navigator) { val state: RemoveDeviceState = viewModel.state val clearSessionState: ClearSessionState = clearSessionViewModel.state - fun navigateAfterSuccess(initialSyncCompleted: Boolean) = navigator.navigate( + fun navigateAfterSuccess(initialSyncCompleted: Boolean, isE2EIRequired: Boolean) = navigator.navigate( NavigationCommand( - destination = if (initialSyncCompleted) HomeScreenDestination else InitialSyncScreenDestination, + destination = if (isE2EIRequired) E2EIEnrollmentScreenDestination + else if (initialSyncCompleted) HomeScreenDestination + else InitialSyncScreenDestination, backStackMode = BackStackMode.CLEAR_WHOLE ) ) @@ -84,9 +87,9 @@ fun RemoveDeviceScreen(navigator: Navigator) { RemoveDeviceContent( state = state, clearSessionState = clearSessionState, - onItemClicked = { viewModel.onItemClicked(it) { navigateAfterSuccess(it) } }, + onItemClicked = { viewModel.onItemClicked(it, ::navigateAfterSuccess) }, onPasswordChange = viewModel::onPasswordChange, - onRemoveConfirm = { viewModel.onRemoveConfirmed { navigateAfterSuccess(it) } }, + onRemoveConfirm = { viewModel.onRemoveConfirmed(::navigateAfterSuccess) }, onDialogDismiss = viewModel::onDialogDismissed, onErrorDialogDismiss = viewModel::clearDeleteClientError, onBackButtonClicked = clearSessionViewModel::onBackButtonClicked, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt index 44422ce461..ceb0f3b653 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/devices/remove/RemoveDeviceViewModel.kt @@ -92,7 +92,7 @@ class RemoveDeviceViewModel @Inject constructor( updateStateIfDialogVisible { state.copy(error = RemoveDeviceError.None) } } - fun onItemClicked(device: Device, onCompleted: (initialSyncCompleted: Boolean) -> Unit) { + fun onItemClicked(device: Device, onCompleted: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) { viewModelScope.launch { val isPasswordRequired: Boolean = when (val passwordRequiredResult = isPasswordRequired()) { is IsPasswordRequiredUseCase.Result.Failure -> { @@ -113,7 +113,7 @@ class RemoveDeviceViewModel @Inject constructor( } } - private suspend fun registerClient(password: String?, onCompleted: (initialSyncCompleted: Boolean) -> Unit) { + private suspend fun registerClient(password: String?, onCompleted: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) { registerClientUseCase( RegisterClientUseCase.RegisterClientParam(password, null) ).also { result -> @@ -125,12 +125,17 @@ class RemoveDeviceViewModel @Inject constructor( is RegisterClientResult.Failure.Generic -> state = state.copy(error = RemoveDeviceError.GenericError(result.genericFailure)) is RegisterClientResult.Failure.InvalidCredentials -> state = state.copy(error = RemoveDeviceError.InvalidCredentialsError) is RegisterClientResult.Failure.TooManyClients -> loadClientsList() - is RegisterClientResult.Success -> onCompleted(userDataStore.initialSyncCompleted.first()) + is RegisterClientResult.Success -> onCompleted(userDataStore.initialSyncCompleted.first(), false) + is RegisterClientResult.E2EICertificateRequired -> onCompleted(userDataStore.initialSyncCompleted.first(), true) } } } - private suspend fun deleteClient(password: String?, device: Device, onCompleted: (initialSyncCompleted: Boolean) -> Unit) { + private suspend fun deleteClient( + password: String?, + device: Device, + onCompleted: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit + ) { when (val deleteResult = deleteClientUseCase(DeleteClientParam(password, device.clientId))) { is DeleteClientResult.Failure.Generic -> { state = state.copy(error = RemoveDeviceError.GenericError(deleteResult.genericFailure)) @@ -147,7 +152,7 @@ class RemoveDeviceViewModel @Inject constructor( } } - fun onRemoveConfirmed(onCompleted: (initialSyncCompleted: Boolean) -> Unit) { + fun onRemoveConfirmed(onCompleted: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) { (state.removeDeviceDialogState as? RemoveDeviceDialogState.Visible)?.let { dialogStateVisible -> updateStateIfDialogVisible { state.copy(removeDeviceDialogState = it.copy(loading = true, removeEnabled = false)) } viewModelScope.launch { 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 55a656be33..acad24526f 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 @@ -74,6 +74,7 @@ import com.wire.android.ui.common.dialogs.FeatureDisabledWithProxyDialogState import com.wire.android.ui.common.rememberTopBarElevationState 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 @@ -98,13 +99,12 @@ fun LoginScreen( LoginContent( navigator::navigateBack, - { initialSyncCompleted -> - navigator.navigate( - NavigationCommand( - if (initialSyncCompleted) HomeScreenDestination else InitialSyncScreenDestination, - BackStackMode.CLEAR_WHOLE - ) - ) + { initialSyncCompleted, isE2EIRequired -> + val destination = if (isE2EIRequired) E2EIEnrollmentScreenDestination + else if (initialSyncCompleted) HomeScreenDestination + else InitialSyncScreenDestination + + navigator.navigate(NavigationCommand(destination, BackStackMode.CLEAR_WHOLE)) }, { navigator.navigate(NavigationCommand(RemoveDeviceScreenDestination, BackStackMode.CLEAR_WHOLE)) }, loginViewModel, @@ -117,7 +117,7 @@ fun LoginScreen( @Composable private fun LoginContent( onBackPressed: () -> Unit, - onSuccess: (initialSyncCompleted: Boolean) -> Unit, + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, onRemoveDeviceNeeded: () -> Unit, viewModel: LoginViewModel, loginEmailViewModel: LoginEmailViewModel, @@ -146,7 +146,7 @@ private fun LoginContent( @Composable private fun MainLoginContent( onBackPressed: () -> Unit, - onSuccess: (initialSyncCompleted: Boolean) -> Unit, + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, onRemoveDeviceNeeded: () -> Unit, viewModel: LoginViewModel, loginEmailViewModel: LoginEmailViewModel, @@ -353,6 +353,6 @@ enum class LoginTabItem(@StringRes override val titleResId: Int) : TabItem { @Composable private fun PreviewLoginScreen() { WireTheme { - MainLoginContent({}, {}, {}, hiltViewModel(), hiltViewModel(), ssoLoginResult = null) + MainLoginContent({}, { _, _ -> }, {}, hiltViewModel(), hiltViewModel(), ssoLoginResult = null) } } diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt index a3a326cd82..f03624ae09 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailScreen.kt @@ -77,7 +77,7 @@ import kotlinx.coroutines.launch @Composable fun LoginEmailScreen( - onSuccess: (initialSyncCompleted: Boolean) -> Unit, + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, onRemoveDeviceNeeded: () -> Unit, loginEmailViewModel: LoginEmailViewModel, scrollState: ScrollState = rememberScrollState() diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt index f2dcd0f71d..43fb16021c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailVerificationCodeScreen.kt @@ -46,7 +46,7 @@ import com.wire.android.util.ui.UIText @Composable fun LoginEmailVerificationCodeScreen( - onSuccess: (initialSyncCompleted: Boolean) -> Unit, + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, viewModel: LoginEmailViewModel = hiltViewModel() ) = LoginEmailVerificationCodeContent( viewModel.secondFactorVerificationCodeState, diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt index 52f2e6a9e3..85361e510b 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModel.kt @@ -72,7 +72,7 @@ class LoginEmailViewModel @Inject constructor( ) @Suppress("LongMethod") - fun login(onSuccess: (initialSyncCompleted: Boolean) -> Unit) { + fun login(onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) { loginState = loginState.copy(emailLoginLoading = true, loginError = LoginError.None).updateEmailLoginEnabled() viewModelScope.launch { val authScope = withContext(dispatchers.io()) { resolveCurrentAuthScope() } ?: return@launch @@ -123,7 +123,12 @@ class LoginEmailViewModel @Inject constructor( } is RegisterClientResult.Success -> { - onSuccess(isInitialSyncCompleted(storedUserId)) + onSuccess(isInitialSyncCompleted(storedUserId), false) + } + + is RegisterClientResult.E2EICertificateRequired -> { + onSuccess(isInitialSyncCompleted(storedUserId), true) + return@launch } } } @@ -215,7 +220,7 @@ class LoginEmailViewModel @Inject constructor( loginState = loginState.copy(proxyPassword = newText).updateEmailLoginEnabled() } - fun onCodeChange(newValue: CodeFieldValue, onSuccess: (initialSyncCompleted: Boolean) -> Unit) { + fun onCodeChange(newValue: CodeFieldValue, onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit) { secondFactorVerificationCodeState = secondFactorVerificationCodeState.copy(codeInput = newValue, isCurrentCodeInvalid = false) if (newValue.isFullyFilled) { login(onSuccess) diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt index 211e26273c..a7ceb4f44f 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOScreen.kt @@ -61,7 +61,7 @@ import kotlinx.coroutines.flow.onEach @Composable fun LoginSSOScreen( - onSuccess: (initialSyncCompleted: Boolean) -> Unit, + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit, onRemoveDeviceNeeded: () -> Unit, ssoLoginResult: DeepLinkResult.SSOLogin?, scrollState: ScrollState = rememberScrollState() diff --git a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt index 263f1f25b9..af0e111012 100644 --- a/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModel.kt @@ -197,9 +197,13 @@ class LoginSSOViewModel @Inject constructor( } } - @Suppress("ComplexMethod") + @Suppress("ComplexMethod", "LongMethod") @VisibleForTesting - fun establishSSOSession(cookie: String, serverConfigId: String, onSuccess: (initialSyncCompleted: Boolean) -> Unit) { + fun establishSSOSession( + cookie: String, + serverConfigId: String, + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit + ) { loginState = loginState.copy(ssoLoginLoading = true, loginError = LoginError.None).updateSSOLoginEnabled() viewModelScope.launch { val authScope = @@ -251,13 +255,17 @@ class LoginSSOViewModel @Inject constructor( registerClient(storedUserId, null).let { when (it) { is RegisterClientResult.Success -> { - onSuccess(isInitialSyncCompleted(storedUserId)) + onSuccess(isInitialSyncCompleted(storedUserId), false) } is RegisterClientResult.Failure -> { updateSSOLoginError(it.toLoginError()) return@launch } + + is RegisterClientResult.E2EICertificateRequired -> { + onSuccess(isInitialSyncCompleted(storedUserId), true) + } } } } @@ -272,15 +280,18 @@ class LoginSSOViewModel @Inject constructor( savedStateHandle.set(SSO_CODE_SAVED_STATE_KEY, newText.text) } - fun handleSSOResult(ssoLoginResult: DeepLinkResult.SSOLogin?, onSuccess: (initialSyncCompleted: Boolean) -> Unit) = + fun handleSSOResult( + ssoLoginResult: DeepLinkResult.SSOLogin?, + onSuccess: (initialSyncCompleted: Boolean, isE2EIRequired: Boolean) -> Unit + ) = when (ssoLoginResult) { - is DeepLinkResult.SSOLogin.Success -> { - establishSSOSession(ssoLoginResult.cookie, ssoLoginResult.serverConfigId, onSuccess) - } + is DeepLinkResult.SSOLogin.Success -> { + establishSSOSession(ssoLoginResult.cookie, ssoLoginResult.serverConfigId, onSuccess) + } - is DeepLinkResult.SSOLogin.Failure -> updateSSOLoginError(LoginError.DialogError.SSOResultError(ssoLoginResult.ssoError)) - null -> {} - } + is DeepLinkResult.SSOLogin.Failure -> updateSSOLoginError(LoginError.DialogError.SSOResultError(ssoLoginResult.ssoError)) + null -> {} + } private fun openWebUrl(url: String) { viewModelScope.launch { diff --git a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt index e492e64a7d..8978e3ed04 100644 --- a/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt +++ b/app/src/main/kotlin/com/wire/android/ui/debug/DebugDataOptions.kt @@ -128,7 +128,7 @@ class DebugDataOptionsViewModel } fun enrollE2EICertificate(context: Context) { - e2eiCertificateUseCase(context) { result -> + e2eiCertificateUseCase(context, false) { result -> result.fold({ state = state.copy( certificate = (it as E2EIFailure.FailedOAuth).reason, showCertificate = true diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt new file mode 100644 index 0000000000..e341e844c9 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentScreen.kt @@ -0,0 +1,231 @@ +/* + * 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.e2eiEnrollment + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.MaterialTheme +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.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootNavGraph +import com.wire.android.R +import com.wire.android.feature.NavigationSwitchAccountActions +import com.wire.android.navigation.BackStackMode +import com.wire.android.navigation.NavigationCommand +import com.wire.android.navigation.Navigator +import com.wire.android.navigation.style.PopUpNavigationAnimation +import com.wire.android.ui.common.ClickableText +import com.wire.android.ui.common.button.WireButtonState +import com.wire.android.ui.common.button.WirePrimaryButton +import com.wire.android.ui.common.colorsScheme +import com.wire.android.ui.common.dialogs.CancelLoginDialogContent +import com.wire.android.ui.common.dialogs.CancelLoginDialogState +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.E2eiCertificateDetailsScreenDestination +import com.wire.android.ui.destinations.InitialSyncScreenDestination +import com.wire.android.ui.home.E2EIErrorWithDismissDialog +import com.wire.android.ui.home.E2EISuccessDialog +import com.wire.android.ui.markdown.MarkdownConstants +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.ui.PreviewMultipleThemes + +@RootNavGraph +@Destination( + style = PopUpNavigationAnimation::class +) +@Composable +fun E2EIEnrollmentScreen( + navigator: Navigator, + viewModel: E2EIEnrollmentViewModel = hiltViewModel(), +) { + val state = viewModel.state + val context = LocalContext.current + + E2EIEnrollmentScreenContent( + state = state, + dismissSuccess = { + navigator.navigate(NavigationCommand(InitialSyncScreenDestination, BackStackMode.CLEAR_WHOLE)) + viewModel.finalizeMLSClient() + }, + dismissErrorDialog = viewModel::dismissErrorDialog, + enrollE2EICertificate = { viewModel.enrollE2EICertificate(context) }, + openCertificateDetails = { + navigator.navigate(NavigationCommand(E2eiCertificateDetailsScreenDestination(state.certificate))) + }, + onBackButtonClicked = viewModel::onBackButtonClicked, + onCancelEnrollmentClicked = { viewModel.onCancelEnrollmentClicked(NavigationSwitchAccountActions(navigator::navigate)) }, + onProceedEnrollmentClicked = viewModel::onProceedEnrollmentClicked + ) +} + +@Composable +private fun E2EIEnrollmentScreenContent( + state: E2EIEnrollmentState, + dismissSuccess: () -> Unit, + dismissErrorDialog: () -> Unit, + enrollE2EICertificate: () -> Unit, + openCertificateDetails: () -> Unit, + onBackButtonClicked: () -> Unit, + onCancelEnrollmentClicked: () -> Unit, + onProceedEnrollmentClicked: () -> Unit +) { + val uriHandler = LocalUriHandler.current + BackHandler { + onBackButtonClicked() + } + val cancelLoginDialogState = rememberVisibilityState() + CancelLoginDialogContent( + dialogState = cancelLoginDialogState, + onActionButtonClicked = { + onCancelEnrollmentClicked() + }, + onProceedButtonClicked = { + onProceedEnrollmentClicked() + } + ) + if (state.showCancelLoginDialog) { + cancelLoginDialogState.show( + cancelLoginDialogState.savedState ?: CancelLoginDialogState + ) + } else { + cancelLoginDialogState.dismiss() + } + WireScaffold( + topBar = { + WireCenterAlignedTopAppBar( + elevation = 0.dp, + title = stringResource(id = R.string.end_to_end_identity_required_dialog_title), + navigationIconType = NavigationIconType.Close, + onNavigationPressed = onBackButtonClicked + ) + }, + bottomBar = { + Column( + Modifier + .wrapContentWidth(Alignment.CenterHorizontally) + ) { + WirePrimaryButton( + onClick = enrollE2EICertificate, + text = stringResource(id = R.string.end_to_end_identity_required_dialog_positive_button), + state = WireButtonState.Default, + loading = state.isLoading, + modifier = Modifier.padding( + top = dimensions().spacing16x, + start = dimensions().spacing16x, + end = dimensions().spacing16x, + bottom = dimensions().spacing16x + ) + ) + } + } + ) { internalPadding -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Top, + modifier = Modifier.padding(PaddingValues(MaterialTheme.wireDimensions.dialogContentPadding)) + ) { + Spacer(modifier = Modifier.height(internalPadding.calculateTopPadding())) + val text = buildAnnotatedString { + val style = SpanStyle( + color = colorsScheme().onBackground, + fontWeight = MaterialTheme.wireTypography.body01.fontWeight, + fontSize = MaterialTheme.wireTypography.body01.fontSize, + fontFamily = MaterialTheme.wireTypography.body01.fontFamily, + fontStyle = MaterialTheme.wireTypography.body01.fontStyle + ) + withStyle(style) { append(stringResource(id = R.string.end_to_end_identity_required_dialog_text_no_snooze)) } + } + ClickableText( + text = text, + style = MaterialTheme.wireTypography.body01, + modifier = Modifier.padding( + top = MaterialTheme.wireDimensions.dialogTextsSpacing, + bottom = MaterialTheme.wireDimensions.dialogTextsSpacing, + ), + onClick = { offset -> + text.getStringAnnotations( + tag = MarkdownConstants.TAG_URL, + start = offset, + end = offset, + ).firstOrNull()?.let { result -> uriHandler.openUri(result.item) } + } + ) + } + + if (state.isCertificateEnrollError) { + E2EIErrorWithDismissDialog( + isE2EILoading = state.isLoading, + updateCertificate = enrollE2EICertificate, + onDismiss = dismissErrorDialog + ) + } + + if (state.isCertificateEnrollSuccess) { + E2EISuccessDialog( + openCertificateDetails = openCertificateDetails, + dismissDialog = dismissSuccess + ) + } + } +} + +@PreviewMultipleThemes +@Composable +fun previewE2EIEnrollmentScreenContent() { + WireTheme { + E2EIEnrollmentScreenContent(E2EIEnrollmentState(), {}, {}, {}, {}, {}, {}) { } + } +} + +@PreviewMultipleThemes +@Composable +fun previewE2EIEnrollmentScreenContentWithSuccess() { + WireTheme { + E2EIEnrollmentScreenContent(E2EIEnrollmentState(isCertificateEnrollSuccess = true), {}, {}, {}, {}, {}, {}) { } + } +} + +@PreviewMultipleThemes +@Composable +fun previewE2EIEnrollmentScreenContentWithError() { + WireTheme { + E2EIEnrollmentScreenContent(E2EIEnrollmentState(isCertificateEnrollError = true), {}, {}, {}, {}, {}, {}) { } + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt new file mode 100644 index 0000000000..514ca785d3 --- /dev/null +++ b/app/src/main/kotlin/com/wire/android/ui/e2eiEnrollment/E2EIEnrollmentViewModel.kt @@ -0,0 +1,123 @@ +/* + * 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.e2eiEnrollment + +import android.content.Context +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.wire.android.appLogger +import com.wire.android.feature.AccountSwitchUseCase +import com.wire.android.feature.SwitchAccountActions +import com.wire.android.feature.SwitchAccountParam +import com.wire.android.feature.e2ei.GetE2EICertificateUseCase +import com.wire.kalium.logic.feature.client.FinalizeMLSClientAfterE2EIEnrollment +import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult +import com.wire.kalium.logic.feature.session.CurrentSessionResult +import com.wire.kalium.logic.feature.session.CurrentSessionUseCase +import com.wire.kalium.logic.feature.session.DeleteSessionUseCase +import com.wire.kalium.logic.functional.fold +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +data class E2EIEnrollmentState( + val certificate: String = "null", + val showCertificate: Boolean = false, + val isLoading: Boolean = false, + val isCertificateEnrollError: Boolean = false, + val isCertificateEnrollSuccess: Boolean = false, + val showCancelLoginDialog: Boolean = false +) + +@HiltViewModel +class E2EIEnrollmentViewModel @Inject constructor( + private val e2eiCertificateUseCase: GetE2EICertificateUseCase, + private val finalizeMLSClientAfterE2EIEnrollment: FinalizeMLSClientAfterE2EIEnrollment, + private val currentSession: CurrentSessionUseCase, + private val deleteSession: DeleteSessionUseCase, + private val switchAccount: AccountSwitchUseCase +) : ViewModel() { + var state by mutableStateOf(E2EIEnrollmentState()) + + fun finalizeMLSClient() { + viewModelScope.launch { + finalizeMLSClientAfterE2EIEnrollment.invoke() + } + } + + fun onBackButtonClicked() { + state = state.copy(showCancelLoginDialog = true) + } + + fun onProceedEnrollmentClicked() { + state = state.copy(showCancelLoginDialog = false) + } + + fun onCancelEnrollmentClicked(switchAccountActions: SwitchAccountActions) { + state = state.copy(showCancelLoginDialog = false) + viewModelScope.launch { + currentSession().let { + when (it) { + is CurrentSessionResult.Success -> { + deleteSession(it.accountInfo.userId) + } + is CurrentSessionResult.Failure.Generic -> { + appLogger.e("failed to delete session") + } + CurrentSessionResult.Failure.SessionNotFound -> { + appLogger.e("session not found") + } + } + } + }.invokeOnCompletion { + viewModelScope.launch { + switchAccount(SwitchAccountParam.TryToSwitchToNextAccount) + .callAction(switchAccountActions) + } + } + } + fun enrollE2EICertificate(context: Context) { + state = state.copy(isLoading = true) + e2eiCertificateUseCase(context, true) { result -> + result.fold({ + state = state.copy( + isLoading = false, + isCertificateEnrollError = true + ) + }, { + if (it is E2EIEnrollmentResult.Finalized) { + state = state.copy( + certificate = it.certificate, + isCertificateEnrollSuccess = true, + isCertificateEnrollError = false, + isLoading = false + ) + } + }) + } + } + + fun dismissErrorDialog() { + state = state.copy( + isCertificateEnrollError = false, + ) + } +} diff --git a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt index 39f0e77dc0..70007e52c2 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModel.kt @@ -275,7 +275,10 @@ class FeatureFlagNotificationViewModel @Inject constructor( fun getE2EICertificate(e2eiRequired: FeatureFlagState.E2EIRequired, context: Context) { featureFlagState = featureFlagState.copy(isE2EILoading = true) currentUserId?.let { userId -> - GetE2EICertificateUseCase(coreLogic.getSessionScope(userId).enrollE2EI, dispatcherProvider).invoke(context) { result -> + GetE2EICertificateUseCase(coreLogic.getSessionScope(userId).enrollE2EI, dispatcherProvider).invoke( + context, + isNewClient = false + ) { result -> result.fold({ featureFlagState = featureFlagState.copy( isE2EILoading = false, diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt index c9366037c4..ae7a9ec28d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModel.kt @@ -112,7 +112,8 @@ class DeviceDetailsViewModel @Inject constructor( state.copy( isE2eiCertificateActivated = true, e2eiCertificate = certificate.certificate, - isLoadingCertificate = false + isLoadingCertificate = false, + device = state.device.updateE2EICertificateStatus(certificate.certificate.status) ) } else { state.copy(isE2eiCertificateActivated = false, isLoadingCertificate = false) @@ -122,7 +123,7 @@ class DeviceDetailsViewModel @Inject constructor( fun enrollE2eiCertificate(context: Context) { state = state.copy(isLoadingCertificate = true) - enrolE2EICertificateUseCase(context) { result -> + enrolE2EICertificateUseCase(context, false) { result -> result.fold({ state = state.copy( isLoadingCertificate = false, @@ -162,7 +163,7 @@ class DeviceDetailsViewModel @Inject constructor( is GetClientDetailsResult.Success -> { state.copy( - device = Device(result.client), + device = state.device.updateFromClient(result.client), isCurrentDevice = result.isCurrentClient, removeDeviceDialogState = RemoveDeviceDialogState.Hidden, canBeRemoved = !result.isCurrentClient && isSelfClient && result.client.type == ClientType.Permanent, diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt index 18a0acf72c..dd9af69f75 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/SelfDevicesViewModel.kt @@ -67,7 +67,8 @@ class SelfDevicesViewModel @Inject constructor( state.copy( isLoadingClientsList = false, currentDevice = result.clients - .firstOrNull { it.id == currentClientId }?.let { Device(it, e2eiCertificates[it.id.value]?.status) }, + .firstOrNull { it.id == currentClientId } + ?.let { Device(it, e2eiCertificates[it.id.value]?.status) }, deviceList = result.clients .filter { it.id != currentClientId } .map { Device(it, e2eiCertificates[it.id.value]?.status) } diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt index 70f3093c9f..b883aff5c7 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/e2ei/E2eiCertificateDetailsScreen.kt @@ -154,4 +154,4 @@ fun E2eiCertificateDetailsContent( ) } -const val CERTIFICATE_FILE_NAME = "certificate.pem" +const val CERTIFICATE_FILE_NAME = "certificate.txt" 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 542e38e0fc..ecc3afd296 100644 --- a/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt +++ b/app/src/main/kotlin/com/wire/android/util/CurrentScreenManager.kt @@ -34,6 +34,7 @@ import com.wire.android.ui.destinations.CreateAccountSummaryScreenDestination import com.wire.android.ui.destinations.CreatePersonalAccountOverviewScreenDestination import com.wire.android.ui.destinations.CreateTeamAccountOverviewScreenDestination import com.wire.android.ui.destinations.Destination +import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.ImportMediaScreenDestination import com.wire.android.ui.destinations.IncomingCallScreenDestination @@ -43,6 +44,7 @@ import com.wire.android.ui.destinations.LoginScreenDestination import com.wire.android.ui.destinations.MigrationScreenDestination import com.wire.android.ui.destinations.OngoingCallScreenDestination 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.WelcomeScreenDestination @@ -213,6 +215,8 @@ sealed class CurrentScreen { is CreateAccountSummaryScreenDestination, is MigrationScreenDestination, is InitialSyncScreenDestination, + is E2EIEnrollmentScreenDestination, + is RegisterDeviceScreenDestination, is RemoveDeviceScreenDestination -> AuthRelated else -> SomeOther diff --git a/app/src/main/kotlin/com/wire/android/util/UriUtil.kt b/app/src/main/kotlin/com/wire/android/util/UriUtil.kt index ca24a25971..92795d61b3 100644 --- a/app/src/main/kotlin/com/wire/android/util/UriUtil.kt +++ b/app/src/main/kotlin/com/wire/android/util/UriUtil.kt @@ -61,3 +61,8 @@ fun sanitizeUrl(url: String): String { return url // Return the original URL if any errors occur } } + +fun URI.removeQueryParams(): URI { + val regex = Regex("[?&][^=]+=[^&]*") + return URI(this.toString().replace(regex, "")) +} diff --git a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt index 7c16353127..7b61cf6769 100644 --- a/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/WireActivityViewModelTest.kt @@ -26,6 +26,7 @@ import com.wire.android.config.TestDispatcherProvider import com.wire.android.config.mockUri import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.AuthServerConfigProvider +import com.wire.android.di.ObserveIfE2EIRequiredDuringLoginUseCaseProvider import com.wire.android.di.ObserveScreenshotCensoringConfigUseCaseProvider import com.wire.android.di.ObserveSyncStateUseCaseProvider import com.wire.android.feature.AccountSwitchUseCase @@ -588,6 +589,10 @@ class WireActivityViewModelTest { } private class Arrangement { + + // TODO add tests for cases when observeIfE2EIIsRequiredDuringLogin emits semothing + private val observeIfE2EIIsRequiredDuringLogin = MutableSharedFlow() + init { // Tests setup MockKAnnotations.init(this, relaxUnitFun = true) @@ -608,6 +613,8 @@ class WireActivityViewModelTest { coEvery { observeScreenshotCensoringConfigUseCase() } returns flowOf(ObserveScreenshotCensoringConfigResult.Disabled) coEvery { currentScreenManager.observeCurrentScreen(any()) } returns MutableStateFlow(CurrentScreen.SomeOther) coEvery { globalDataStore.selectedThemeOptionFlow() } returns flowOf(ThemeOption.LIGHT) + coEvery { observeIfE2EIRequiredDuringLoginUseCaseProviderFactory.create(any()).observeIfE2EIIsRequiredDuringLogin() } returns + observeIfE2EIIsRequiredDuringLogin } @MockK @@ -663,6 +670,9 @@ class WireActivityViewModelTest { @MockK private lateinit var observeScreenshotCensoringConfigUseCaseProviderFactory: ObserveScreenshotCensoringConfigUseCaseProvider.Factory + @MockK + private lateinit var observeIfE2EIRequiredDuringLoginUseCaseProviderFactory: ObserveIfE2EIRequiredDuringLoginUseCaseProvider.Factory + @MockK lateinit var globalDataStore: GlobalDataStore @@ -691,7 +701,8 @@ class WireActivityViewModelTest { clearNewClientsForUser = clearNewClientsForUser, currentScreenManager = currentScreenManager, observeScreenshotCensoringConfigUseCaseProviderFactory = observeScreenshotCensoringConfigUseCaseProviderFactory, - globalDataStore = globalDataStore + globalDataStore = globalDataStore, + observeIfE2EIRequiredDuringLoginUseCaseProviderFactory = observeIfE2EIRequiredDuringLoginUseCaseProviderFactory ) } diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt index 71d5c8d9db..c4c079e269 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/login/email/LoginEmailViewModelTest.kt @@ -116,7 +116,7 @@ class LoginEmailViewModelTest { private lateinit var authenticationScope: AuthenticationScope @MockK(relaxed = true) - private lateinit var onSuccess: (Boolean) -> Unit + private lateinit var onSuccess: (Boolean, Boolean) -> Unit private lateinit var loginViewModel: LoginEmailViewModel @@ -210,7 +210,7 @@ class LoginEmailViewModelTest { advanceUntilIdle() coVerify(exactly = 1) { loginUseCase(any(), any(), any(), any(), any()) } coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) } - coVerify(exactly = 1) { onSuccess(true) } + coVerify(exactly = 1) { onSuccess(true, false) } } @Test @@ -233,7 +233,7 @@ class LoginEmailViewModelTest { advanceUntilIdle() coVerify(exactly = 1) { loginUseCase(any(), any(), any(), any(), any()) } coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) } - coVerify(exactly = 1) { onSuccess(false) } + coVerify(exactly = 1) { onSuccess(false, false) } } @Test @@ -440,7 +440,7 @@ class LoginEmailViewModelTest { advanceUntilIdle() coVerify(exactly = 1) { loginUseCase(email, any(), any(), any(), code) } coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) } - coVerify(exactly = 1) { onSuccess(any()) } + coVerify(exactly = 1) { onSuccess(any(), any()) } } @Test diff --git a/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt index f99282e6a3..e6e33468d5 100644 --- a/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/authentication/login/sso/LoginSSOViewModelTest.kt @@ -121,7 +121,7 @@ class LoginSSOViewModelTest { private lateinit var fetchSSOSettings: FetchSSOSettingsUseCase @MockK(relaxed = true) - private lateinit var onSuccess: (Boolean) -> Unit + private lateinit var onSuccess: (Boolean, Boolean) -> Unit private lateinit var loginViewModel: LoginSSOViewModel @@ -262,7 +262,7 @@ class LoginSSOViewModelTest { coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) } coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) } coVerify(exactly = 1) { addAuthenticatedUserUseCase(any(), any(), any(), any()) } - coVerify(exactly = 1) { onSuccess(false) } + coVerify(exactly = 1) { onSuccess(false, false) } } @Test @@ -288,7 +288,7 @@ class LoginSSOViewModelTest { coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) } coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) } coVerify(exactly = 1) { addAuthenticatedUserUseCase(any(), any(), any(), any()) } - coVerify(exactly = 1) { onSuccess(true) } + coVerify(exactly = 1) { onSuccess(true, false) } } @Test @@ -312,7 +312,7 @@ class LoginSSOViewModelTest { coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) } coVerify(exactly = 0) { loginViewModel.registerClient(any(), null) } coVerify(exactly = 0) { addAuthenticatedUserUseCase(any(), any(), any(), any()) } - verify(exactly = 0) { onSuccess(any()) } + verify(exactly = 0) { onSuccess(any(), any()) } } @Test @@ -353,7 +353,7 @@ class LoginSSOViewModelTest { loginViewModel.handleSSOResult(DeepLinkResult.SSOLogin.Success("", ""), onSuccess) advanceUntilIdle() - verify(exactly = 1) { onSuccess(any()) } + verify(exactly = 1) { onSuccess(any(), any()) } } @Test @@ -376,7 +376,7 @@ class LoginSSOViewModelTest { coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) } coVerify(exactly = 0) { loginViewModel.registerClient(any(), null) } coVerify(exactly = 1) { addAuthenticatedUserUseCase(any(), any(), any(), any()) } - verify(exactly = 0) { onSuccess(any()) } + verify(exactly = 0) { onSuccess(any(), any()) } } @Test @@ -405,7 +405,7 @@ class LoginSSOViewModelTest { coVerify(exactly = 1) { getOrRegisterClientUseCase(any()) } coVerify(exactly = 1) { getSSOLoginSessionUseCase(any()) } coVerify(exactly = 1) { addAuthenticatedUserUseCase(any(), any(), any(), any()) } - verify(exactly = 0) { onSuccess(any()) } + verify(exactly = 0) { onSuccess(any(), any()) } } @Test diff --git a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt index 9029b72f58..845c3a2f72 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/conversations/search/SearchUserViewModelTest.kt @@ -38,7 +38,6 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.impl.annotations.MockK -import io.mockk.verify import kotlinx.coroutines.test.runTest import org.amshove.kluent.internal.assertEquals import org.junit.jupiter.api.Test @@ -77,7 +76,7 @@ class SearchUserViewModelTest { ) } - verify(exactly = 1) { + coVerify(exactly = 1) { arrangement.federatedSearchParser(any()) } } @@ -114,7 +113,7 @@ class SearchUserViewModelTest { ) } - verify(exactly = 1) { + coVerify(exactly = 1) { arrangement.federatedSearchParser(any()) } } @@ -170,7 +169,7 @@ class SearchUserViewModelTest { ) } - verify(exactly = 1) { + coVerify(exactly = 1) { arrangement.federatedSearchParser(any()) } diff --git a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt index 428f868d68..12af6ab551 100644 --- a/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsViewModelTest.kt @@ -279,7 +279,7 @@ class DeviceDetailsViewModelTest { viewModel.enrollE2eiCertificate(arrangement.context) coVerify { - arrangement.enrolE2EICertificateUseCase(any(), any()) + arrangement.enrolE2EICertificateUseCase(any(), any(), any()) } assertTrue(viewModel.state.isLoadingCertificate) } diff --git a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt index 8786a24986..0088a7ea5a 100644 --- a/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt +++ b/build-logic/plugins/src/main/kotlin/AndroidCoordinates.kt @@ -25,6 +25,6 @@ object AndroidSdk { object AndroidApp { const val id = "com.wire.android" - const val versionName = "4.6.0" + const val versionName = "4.7.0" val versionCode = Versionizer().versionCode } diff --git a/kalium b/kalium index aa8d9077dc..0773557532 160000 --- a/kalium +++ b/kalium @@ -1 +1 @@ -Subproject commit aa8d9077dcf4be11b174de6f78f3ed5869765edf +Subproject commit 07735575323400ca9c43e6259ec2f264e24669ad