diff --git a/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt b/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt index 4354058cfc..0f57f04997 100644 --- a/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt +++ b/app/src/main/kotlin/com/wire/android/feature/e2ei/GetE2EICertificateUseCase.kt @@ -26,7 +26,6 @@ import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult import com.wire.kalium.logic.feature.e2ei.usecase.EnrollE2EIUseCase import com.wire.kalium.logic.functional.Either import com.wire.kalium.logic.functional.fold -import com.wire.kalium.logic.functional.map import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -62,10 +61,10 @@ class GetE2EICertificateUseCase @Inject constructor( scope.launch { when (oAuthResult) { is OAuthUseCase.OAuthResult.Success -> { - enrollE2EI.finalizeEnrollment( + enrollmentResultHandler(enrollE2EI.finalizeEnrollment( oAuthResult.idToken, initialEnrollmentResult - ).map { enrollmentResultHandler(Either.Right(it)) } + )) } is OAuthUseCase.OAuthResult.Failed -> { 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 65cb1a988a..4a688ead22 100644 --- a/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt +++ b/app/src/main/kotlin/com/wire/android/ui/WireActivity.kt @@ -45,6 +45,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat @@ -70,6 +71,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.E2eiCertificateDetailsScreenDestination import com.wire.android.ui.destinations.HomeScreenDestination import com.wire.android.ui.destinations.ImportMediaScreenDestination import com.wire.android.ui.destinations.IncomingCallScreenDestination @@ -81,6 +83,7 @@ import com.wire.android.ui.destinations.SelfDevicesScreenDestination import com.wire.android.ui.destinations.SelfUserProfileScreenDestination import com.wire.android.ui.destinations.WelcomeScreenDestination import com.wire.android.ui.home.E2EIRequiredDialog +import com.wire.android.ui.home.E2EIResultDialog import com.wire.android.ui.home.E2EISnoozeDialog import com.wire.android.ui.home.appLock.LockCodeTimeManager import com.wire.android.ui.home.sync.FeatureFlagNotificationViewModel @@ -273,6 +276,7 @@ class WireActivity : AppCompatActivity() { LaunchedEffect(userId) { featureFlagNotificationViewModel.loadInitialSync() } + val context = LocalContext.current with(featureFlagNotificationViewModel.featureFlagState) { if (shouldShowTeamAppLockDialog) { TeamAppLockFeatureFlagDialog( @@ -339,8 +343,9 @@ class WireActivity : AppCompatActivity() { e2EIRequired?.let { E2EIRequiredDialog( - result = e2EIRequired, - getCertificate = featureFlagNotificationViewModel::getE2EICertificate, + e2EIRequired = e2EIRequired, + isE2EILoading = isE2EILoading, + getCertificate = { featureFlagNotificationViewModel.getE2EICertificate(it, context) }, snoozeDialog = featureFlagNotificationViewModel::snoozeE2EIdRequiredDialog ) } @@ -352,6 +357,17 @@ class WireActivity : AppCompatActivity() { ) } + e2EIResult?.let { + E2EIResultDialog( + result = e2EIResult, + updateCertificate = { featureFlagNotificationViewModel.getE2EICertificate(it, context) }, + snoozeDialog = featureFlagNotificationViewModel::snoozeE2EIdRequiredDialog, + openCertificateDetails = { navigate(NavigationCommand(E2eiCertificateDetailsScreenDestination(it))) }, + dismissSuccessDialog = featureFlagNotificationViewModel::dismissSuccessE2EIdDialog, + isE2EILoading = isE2EILoading + ) + } + UpdateAppDialog(viewModel.globalAppState.updateAppDialog, ::updateTheApp) JoinConversationDialog( viewModel.globalAppState.conversationJoinedDialog, diff --git a/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt index fb247a4008..4317dfb312 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt @@ -50,36 +50,77 @@ import kotlin.time.Duration.Companion.seconds @Composable fun E2EIRequiredDialog( - result: FeatureFlagState.E2EIRequired, - getCertificate: () -> Unit, + e2EIRequired: FeatureFlagState.E2EIRequired, + isE2EILoading: Boolean, + getCertificate: (FeatureFlagState.E2EIRequired) -> Unit, snoozeDialog: (FeatureFlagState.E2EIRequired.WithGracePeriod) -> Unit, ) { - when (result) { - FeatureFlagState.E2EIRequired.NoGracePeriod.Create -> E2EIRequiredNoSnoozeDialog(getCertificate = getCertificate) - FeatureFlagState.E2EIRequired.NoGracePeriod.Renew -> E2EIRenewNoSnoozeDialog(updateCertificate = getCertificate) + when (e2EIRequired) { + FeatureFlagState.E2EIRequired.NoGracePeriod.Create -> E2EIRequiredNoSnoozeDialog( + isLoading = isE2EILoading, + getCertificate = { getCertificate(e2EIRequired) } + ) + + FeatureFlagState.E2EIRequired.NoGracePeriod.Renew -> E2EIRenewNoSnoozeDialog( + isLoading = isE2EILoading, + updateCertificate = { getCertificate(e2EIRequired) } + ) + is FeatureFlagState.E2EIRequired.WithGracePeriod.Create -> E2EIRequiredWithSnoozeDialog( - getCertificate = getCertificate, - snoozeDialog = { snoozeDialog(result) } + isLoading = isE2EILoading, + getCertificate = { getCertificate(e2EIRequired) }, + snoozeDialog = { snoozeDialog(e2EIRequired) } ) is FeatureFlagState.E2EIRequired.WithGracePeriod.Renew -> E2EIRenewWithSnoozeDialog( - updateCertificate = getCertificate, - snoozeDialog = { snoozeDialog(result) } + isLoading = isE2EILoading, + updateCertificate = { getCertificate(e2EIRequired) }, + snoozeDialog = { snoozeDialog(e2EIRequired) } + ) + } +} + +@Composable +fun E2EIResultDialog( + result: FeatureFlagState.E2EIResult, + isE2EILoading: Boolean, + updateCertificate: (FeatureFlagState.E2EIRequired) -> Unit, + snoozeDialog: (FeatureFlagState.E2EIRequired.WithGracePeriod) -> Unit, + openCertificateDetails: (String) -> Unit, + dismissSuccessDialog: () -> Unit +) { + when (result) { + is FeatureFlagState.E2EIResult.Failure -> E2EIRenewErrorDialog( + e2EIRequired = result.e2EIRequired, + isE2EILoading = isE2EILoading, + updateCertificate = { updateCertificate(result.e2EIRequired) }, + snoozeDialog = snoozeDialog + ) + + is FeatureFlagState.E2EIResult.Success -> E2EISuccessDialog( + openCertificateDetails = { openCertificateDetails(result.certificate) }, + dismissDialog = dismissSuccessDialog ) } } @Composable fun E2EIRenewErrorDialog( - result: FeatureFlagState.E2EIRequired, + e2EIRequired: FeatureFlagState.E2EIRequired, + isE2EILoading: Boolean, updateCertificate: () -> Unit, snoozeDialog: (FeatureFlagState.E2EIRequired.WithGracePeriod) -> Unit, ) { - when (result) { - is FeatureFlagState.E2EIRequired.NoGracePeriod -> E2EIErrorNoSnoozeDialog(updateCertificate = updateCertificate) + when (e2EIRequired) { + is FeatureFlagState.E2EIRequired.NoGracePeriod -> E2EIErrorNoSnoozeDialog( + isE2EILoading = isE2EILoading, + updateCertificate = updateCertificate + ) + is FeatureFlagState.E2EIRequired.WithGracePeriod -> E2EIErrorWithSnoozeDialog( updateCertificate = updateCertificate, - snoozeDialog = { snoozeDialog(result) } + isE2EILoading = isE2EILoading, + snoozeDialog = { snoozeDialog(e2EIRequired) } ) } } @@ -151,19 +192,47 @@ fun E2EISuccessDialog( ) } +@Composable +fun E2EIErrorWithDismissDialog( + isE2EILoading: Boolean, + updateCertificate: () -> Unit, + onDismiss: () -> Unit +) { + WireDialog( + title = stringResource(id = R.string.end_to_end_identity_renew_error_dialog_title), + text = stringResource(id = R.string.end_to_end_identity_renew_error_dialog_text), + onDismiss = onDismiss, + optionButton1Properties = WireDialogButtonProperties( + onClick = updateCertificate, + text = stringResource(id = R.string.label_retry), + type = WireDialogButtonType.Primary, + loading = isE2EILoading + ), + optionButton2Properties = WireDialogButtonProperties( + onClick = onDismiss, + text = stringResource(id = R.string.label_cancel), + type = WireDialogButtonType.Secondary, + ), + buttonsHorizontalAlignment = false, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) +} + @Composable private fun E2EIErrorWithSnoozeDialog( + isE2EILoading: Boolean, updateCertificate: () -> Unit, snoozeDialog: () -> Unit ) { WireDialog( title = stringResource(id = R.string.end_to_end_identity_renew_error_dialog_title), text = stringResource(id = R.string.end_to_end_identity_renew_error_dialog_text), - onDismiss = updateCertificate, + onDismiss = snoozeDialog, optionButton1Properties = WireDialogButtonProperties( onClick = updateCertificate, text = stringResource(id = R.string.label_retry), type = WireDialogButtonType.Primary, + loading = isE2EILoading ), optionButton2Properties = WireDialogButtonProperties( onClick = snoozeDialog, @@ -177,6 +246,7 @@ private fun E2EIErrorWithSnoozeDialog( @Composable private fun E2EIErrorNoSnoozeDialog( + isE2EILoading: Boolean, updateCertificate: () -> Unit ) { WireDialog( @@ -187,6 +257,7 @@ private fun E2EIErrorNoSnoozeDialog( onClick = updateCertificate, text = stringResource(id = R.string.label_retry), type = WireDialogButtonType.Primary, + loading = isE2EILoading ), properties = DialogProperties( usePlatformDefaultWidth = false, @@ -198,6 +269,7 @@ private fun E2EIErrorNoSnoozeDialog( @Composable private fun E2EIRequiredWithSnoozeDialog( + isLoading: Boolean, getCertificate: () -> Unit, snoozeDialog: () -> Unit ) { @@ -209,6 +281,7 @@ private fun E2EIRequiredWithSnoozeDialog( onClick = getCertificate, text = stringResource(id = R.string.end_to_end_identity_required_dialog_positive_button), type = WireDialogButtonType.Primary, + loading = isLoading ), optionButton2Properties = WireDialogButtonProperties( onClick = snoozeDialog, @@ -221,7 +294,7 @@ private fun E2EIRequiredWithSnoozeDialog( } @Composable -private fun E2EIRequiredNoSnoozeDialog(getCertificate: () -> Unit) { +private fun E2EIRequiredNoSnoozeDialog(isLoading: Boolean, getCertificate: () -> Unit) { WireDialog( title = stringResource(id = R.string.end_to_end_identity_required_dialog_title), text = stringResource(id = R.string.end_to_end_identity_required_dialog_text_no_snooze), @@ -230,6 +303,7 @@ private fun E2EIRequiredNoSnoozeDialog(getCertificate: () -> Unit) { onClick = getCertificate, text = stringResource(id = R.string.end_to_end_identity_required_dialog_positive_button), type = WireDialogButtonType.Primary, + loading = isLoading ), buttonsHorizontalAlignment = false, properties = DialogProperties( @@ -242,6 +316,7 @@ private fun E2EIRequiredNoSnoozeDialog(getCertificate: () -> Unit) { @Composable private fun E2EIRenewWithSnoozeDialog( + isLoading: Boolean, updateCertificate: () -> Unit, snoozeDialog: () -> Unit ) { @@ -253,6 +328,7 @@ private fun E2EIRenewWithSnoozeDialog( onClick = updateCertificate, text = stringResource(id = R.string.end_to_end_identity_renew_dialog_positive_button), type = WireDialogButtonType.Primary, + loading = isLoading ), optionButton2Properties = WireDialogButtonProperties( onClick = snoozeDialog, @@ -265,7 +341,7 @@ private fun E2EIRenewWithSnoozeDialog( } @Composable -private fun E2EIRenewNoSnoozeDialog(updateCertificate: () -> Unit) { +private fun E2EIRenewNoSnoozeDialog(isLoading: Boolean, updateCertificate: () -> Unit) { WireDialog( title = stringResource(id = R.string.end_to_end_identity_renew_dialog_title), text = stringResource(id = R.string.end_to_end_identity_renew_dialog_text_no_snooze), @@ -274,6 +350,7 @@ private fun E2EIRenewNoSnoozeDialog(updateCertificate: () -> Unit) { onClick = updateCertificate, text = stringResource(id = R.string.end_to_end_identity_renew_dialog_positive_button), type = WireDialogButtonType.Primary, + loading = isLoading ), buttonsHorizontalAlignment = false, properties = DialogProperties( @@ -288,7 +365,7 @@ private fun E2EIRenewNoSnoozeDialog(updateCertificate: () -> Unit) { @Composable fun previewE2EIdRequiredWithSnoozeDialog() { WireTheme { - E2EIRequiredWithSnoozeDialog({}) {} + E2EIRequiredWithSnoozeDialog(false, {}) {} } } @@ -296,7 +373,7 @@ fun previewE2EIdRequiredWithSnoozeDialog() { @Composable fun previewE2EIdRequiredNoSnoozeDialog() { WireTheme { - E2EIRequiredNoSnoozeDialog {} + E2EIRequiredNoSnoozeDialog(false) {} } } @@ -304,7 +381,7 @@ fun previewE2EIdRequiredNoSnoozeDialog() { @Composable fun previewE2EIdRenewRequiredWithSnoozeDialog() { WireTheme { - E2EIRenewWithSnoozeDialog({}) {} + E2EIRenewWithSnoozeDialog(false, {}) {} } } @@ -312,7 +389,7 @@ fun previewE2EIdRenewRequiredWithSnoozeDialog() { @Composable fun previewE2EIdRenewRequiredNoSnoozeDialog() { WireTheme { - E2EIRenewNoSnoozeDialog {} + E2EIRenewNoSnoozeDialog(false) {} } } @@ -328,7 +405,7 @@ fun previewE2EIdSnoozeDialog() { @Composable fun previewE2EIRenewErrorDialogNoGracePeriod() { WireTheme { - E2EIRenewErrorDialog(FeatureFlagState.E2EIRequired.NoGracePeriod.Renew, { }) {} + E2EIRenewErrorDialog(FeatureFlagState.E2EIRequired.NoGracePeriod.Renew, false, { }) {} } } @@ -336,7 +413,7 @@ fun previewE2EIRenewErrorDialogNoGracePeriod() { @Composable fun previewE2EIRenewErrorDialogWithGracePeriod() { WireTheme { - E2EIRenewErrorDialog(FeatureFlagState.E2EIRequired.WithGracePeriod.Renew(2.days), { }) {} + E2EIRenewErrorDialog(FeatureFlagState.E2EIRequired.WithGracePeriod.Renew(2.days), false, { }) {} } } @@ -352,7 +429,7 @@ fun previewE2EISuccessDialog() { @Composable fun previewE2EIRenewErrorNoSnoozeDialog() { WireTheme { - E2EIErrorNoSnoozeDialog { } + E2EIErrorNoSnoozeDialog(false) { } } } @@ -360,6 +437,6 @@ fun previewE2EIRenewErrorNoSnoozeDialog() { @Composable fun previewE2EIRenewErrorWithSnoozeDialog() { WireTheme { - E2EIErrorWithSnoozeDialog(updateCertificate = {}) { } + E2EIErrorWithSnoozeDialog(isE2EILoading = false, updateCertificate = {}) { } } } diff --git a/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt b/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt index 9f877f6017..4a9bd612df 100644 --- a/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/home/FeatureFlagState.kt @@ -36,6 +36,8 @@ data class FeatureFlagState( val areSelfDeletedMessagesEnabled: Boolean = true, val e2EIRequired: E2EIRequired? = null, val e2EISnoozeInfo: E2EISnooze? = null, + val e2EIResult: E2EIResult? = null, + val isE2EILoading: Boolean = false, val showCallEndedBecauseOfConversationDegraded: Boolean = false ) { enum class SharingRestrictedState { @@ -56,4 +58,9 @@ data class FeatureFlagState( data object Renew : NoGracePeriod() } } + + sealed class E2EIResult { + data class Failure(val e2EIRequired: E2EIRequired) : E2EIResult() + data class Success(val certificate: String) : E2EIResult() + } } 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 82d481e547..453abab837 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 @@ -20,6 +20,7 @@ package com.wire.android.ui.home.sync +import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -30,17 +31,21 @@ import com.wire.android.datastore.GlobalDataStore import com.wire.android.di.KaliumCoreLogic import com.wire.android.feature.AppLockSource import com.wire.android.feature.DisableAppLockUseCase +import com.wire.android.feature.e2ei.GetE2EICertificateUseCase import com.wire.android.ui.home.FeatureFlagState import com.wire.android.ui.home.conversations.selfdeletion.SelfDeletionMapper.toSelfDeletionDuration import com.wire.android.ui.home.messagecomposer.SelfDeletionDuration +import com.wire.android.util.dispatchers.DispatcherProvider import com.wire.kalium.logic.CoreLogic import com.wire.kalium.logic.configuration.FileSharingStatus import com.wire.kalium.logic.data.message.TeamSelfDeleteTimer import com.wire.kalium.logic.data.sync.SyncState import com.wire.kalium.logic.data.user.UserId +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.user.E2EIRequiredResult +import com.wire.kalium.logic.functional.fold import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.distinctUntilChanged @@ -54,7 +59,8 @@ class FeatureFlagNotificationViewModel @Inject constructor( @KaliumCoreLogic private val coreLogic: CoreLogic, private val currentSessionUseCase: CurrentSessionUseCase, private val globalDataStore: GlobalDataStore, - private val disableAppLockUseCase: DisableAppLockUseCase + private val disableAppLockUseCase: DisableAppLockUseCase, + private val dispatcherProvider: DispatcherProvider ) : ViewModel() { var featureFlagState by mutableStateOf(FeatureFlagState()) @@ -260,9 +266,33 @@ class FeatureFlagNotificationViewModel @Inject constructor( fun isUserAppLockSet() = globalDataStore.isAppLockPasscodeSet() - fun getE2EICertificate() { - // TODO do the magic - featureFlagState = featureFlagState.copy(e2EIRequired = null) + fun getE2EICertificate(e2eiRequired: FeatureFlagState.E2EIRequired, context: Context) { + featureFlagState = featureFlagState.copy(isE2EILoading = true) + currentUserId?.let { userId -> + GetE2EICertificateUseCase(coreLogic.getSessionScope(userId).enrollE2EI, dispatcherProvider).invoke(context) { result -> + result.fold({ + featureFlagState = featureFlagState.copy( + isE2EILoading = false, + e2EIRequired = null, + e2EIResult = FeatureFlagState.E2EIResult.Failure(e2eiRequired) + ) + }, { + if (it is E2EIEnrollmentResult.Finalized) { + featureFlagState = featureFlagState.copy( + isE2EILoading = false, + e2EIRequired = null, + e2EIResult = FeatureFlagState.E2EIResult.Success(it.certificate) + ) + } else if (it is E2EIEnrollmentResult.Failed) { + featureFlagState = featureFlagState.copy( + isE2EILoading = false, + e2EIRequired = null, + e2EIResult = FeatureFlagState.E2EIResult.Failure(e2eiRequired) + ) + } + }) + } + } } fun snoozeE2EIdRequiredDialog(result: FeatureFlagState.E2EIRequired.WithGracePeriod) { @@ -284,4 +314,8 @@ class FeatureFlagNotificationViewModel @Inject constructor( fun dismissCallEndedBecauseOfConversationDegraded() { featureFlagState = featureFlagState.copy(showCallEndedBecauseOfConversationDegraded = false) } + + fun dismissSuccessE2EIdDialog() { + featureFlagState = featureFlagState.copy(e2EIResult = null) + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt index 5f881f3aae..ac9926f97c 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/DeviceDetailsScreen.kt @@ -1,5 +1,6 @@ package com.wire.android.ui.settings.devices +import android.content.Context import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box @@ -56,6 +57,8 @@ import com.wire.android.ui.common.scaffold.WireScaffold import com.wire.android.ui.common.topappbar.WireCenterAlignedTopAppBar import com.wire.android.ui.common.topappbar.WireTopAppBarTitle import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination +import com.wire.android.ui.home.E2EIErrorWithDismissDialog +import com.wire.android.ui.home.E2EISuccessDialog import com.wire.android.ui.home.conversationslist.common.FolderHeader import com.wire.android.ui.settings.devices.model.DeviceDetailsState import com.wire.android.ui.theme.wireColorScheme @@ -94,7 +97,9 @@ fun DeviceDetailsScreen( navigator.navigate( NavigationCommand(E2eiCertificateDetailsScreenDestination(it)) ) - } + }, + onEnrollE2EIErrorDismiss = viewModel::hideEnrollE2EICertificateError, + onEnrollE2EISuccessDismiss = viewModel::hideEnrollE2EICertificateSuccess ) } } @@ -109,8 +114,10 @@ fun DeviceDetailsContent( onRemoveConfirm: () -> Unit = {}, onDialogDismiss: () -> Unit = {}, onErrorDialogDismiss: () -> Unit = {}, - enrollE2eiCertificate: () -> Unit = {}, - onUpdateClientVerification: (Boolean) -> Unit = {} + enrollE2eiCertificate: (Context) -> Unit = {}, + onUpdateClientVerification: (Boolean) -> Unit = {}, + onEnrollE2EIErrorDismiss: () -> Unit = {}, + onEnrollE2EISuccessDismiss: () -> Unit = {} ) { val screenState = rememberConversationScreenState() WireScaffold( @@ -148,6 +155,7 @@ fun DeviceDetailsContent( } } ) { internalPadding -> + val context = LocalContext.current LazyColumn( modifier = Modifier .fillMaxSize() @@ -161,18 +169,17 @@ fun DeviceDetailsContent( Divider(color = MaterialTheme.wireColorScheme.background) } } - if (BuildConfig.DEBUG) { - item { - EndToEndIdentityCertificateItem( - isE2eiCertificateActivated = state.isE2eiCertificateActivated, - certificate = state.e2eiCertificate, - isSelfClient = state.isSelfClient, - enrollE2eiCertificate = enrollE2eiCertificate, - updateE2eiCertificate = {}, - showCertificate = onNavigateToE2eiCertificateDetailsScreen - ) - Divider(color = colorsScheme().background) - } + item { + EndToEndIdentityCertificateItem( + isE2eiCertificateActivated = state.isE2eiCertificateActivated, + certificate = state.e2eiCertificate, + isCurrentDevice = state.isCurrentDevice, + isLoadingCertificate = state.isLoadingCertificate, + enrollE2eiCertificate = { enrollE2eiCertificate(context) }, + updateE2eiCertificate = {}, + showCertificate = onNavigateToE2eiCertificateDetailsScreen + ) + Divider(color = colorsScheme().background) } item { FolderHeader( @@ -246,6 +253,21 @@ fun DeviceDetailsContent( ) } } + + if (state.isE2EICertificateEnrollError) { + E2EIErrorWithDismissDialog( + isE2EILoading = state.isLoadingCertificate, + updateCertificate = { enrollE2eiCertificate(context) }, + onDismiss = onEnrollE2EIErrorDismiss + ) + } + + if (state.isE2EICertificateEnrollSuccess) { + E2EISuccessDialog( + openCertificateDetails = { onNavigateToE2eiCertificateDetailsScreen(state.e2eiCertificate.certificateDetail) }, + dismissDialog = onEnrollE2EISuccessDismiss + ) + } } } 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 964c160da9..be334dff08 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 @@ -1,14 +1,15 @@ package com.wire.android.ui.settings.devices +import android.content.Context import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.text.input.TextFieldValue import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import com.wire.android.BuildConfig import com.wire.android.appLogger import com.wire.android.di.CurrentAccount +import com.wire.android.feature.e2ei.GetE2EICertificateUseCase import com.wire.android.navigation.SavedStateViewModel import com.wire.android.ui.authentication.devices.model.Device import com.wire.android.ui.authentication.devices.remove.RemoveDeviceDialogState @@ -26,11 +27,13 @@ import com.wire.kalium.logic.feature.client.GetClientDetailsResult import com.wire.kalium.logic.feature.client.ObserveClientDetailsUseCase import com.wire.kalium.logic.feature.client.Result import com.wire.kalium.logic.feature.client.UpdateClientVerificationStatusUseCase +import com.wire.kalium.logic.feature.e2ei.usecase.E2EIEnrollmentResult import com.wire.kalium.logic.feature.e2ei.usecase.GetE2EICertificateUseCaseResult import com.wire.kalium.logic.feature.e2ei.usecase.GetE2eiCertificateUseCase import com.wire.kalium.logic.feature.user.GetUserInfoResult import com.wire.kalium.logic.feature.user.IsPasswordRequiredUseCase import com.wire.kalium.logic.feature.user.ObserveUserInfoUseCase +import com.wire.kalium.logic.functional.fold import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @@ -47,7 +50,8 @@ class DeviceDetailsViewModel @Inject constructor( private val fingerprintUseCase: ClientFingerprintUseCase, private val updateClientVerificationStatus: UpdateClientVerificationStatusUseCase, private val observeUserInfo: ObserveUserInfoUseCase, - private val e2eiCertificate: GetE2eiCertificateUseCase + private val e2eiCertificate: GetE2eiCertificateUseCase, + private val enrolE2EICertificateUseCase: GetE2EICertificateUseCase ) : SavedStateViewModel(savedStateHandle) { private val deviceDetailsNavArgs: DeviceDetailsNavArgs = savedStateHandle.navArgs() @@ -61,9 +65,7 @@ class DeviceDetailsViewModel @Inject constructor( observeDeviceDetails() getClientFingerPrint() observeUserName() - if (BuildConfig.DEBUG) { - getE2eiCertificate() - } + getE2eiCertificate() } private val isSelfClient: Boolean @@ -92,16 +94,35 @@ class DeviceDetailsViewModel @Inject constructor( state = if (certificate is GetE2EICertificateUseCaseResult.Success) { state.copy( isE2eiCertificateActivated = true, - e2eiCertificate = certificate.certificate + e2eiCertificate = certificate.certificate, + isLoadingCertificate = false ) } else { - state.copy(isE2eiCertificateActivated = false) + state.copy(isE2eiCertificateActivated = false, isLoadingCertificate = false) } } } - fun enrollE2eiCertificate() { - // TODO invoke correspondent use case + fun enrollE2eiCertificate(context: Context) { + state = state.copy(isLoadingCertificate = true) + enrolE2EICertificateUseCase(context) { result -> + result.fold({ + state = state.copy( + isLoadingCertificate = false, + isE2EICertificateEnrollError = true + ) + }, { + if (it is E2EIEnrollmentResult.Finalized) { + getE2eiCertificate() + state = state.copy(isE2EICertificateEnrollSuccess = true) + } else if (it is E2EIEnrollmentResult.Failed) { + state = state.copy( + isLoadingCertificate = false, + isE2EICertificateEnrollError = true + ) + } + }) + } } private fun getClientFingerPrint() { @@ -225,4 +246,12 @@ class DeviceDetailsViewModel @Inject constructor( fun clearDeleteClientError() { state = state.copy(error = RemoveDeviceError.None) } + + fun hideEnrollE2EICertificateError() { + state = state.copy(isE2EICertificateEnrollError = false) + } + + fun hideEnrollE2EICertificateSuccess() { + state = state.copy(isE2EICertificateEnrollSuccess = false) + } } diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt index cf9870c8df..cf562fb350 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/EndToEndIdentityCertificateItem.kt @@ -45,7 +45,8 @@ import com.wire.kalium.logic.feature.e2ei.E2eiCertificate fun EndToEndIdentityCertificateItem( isE2eiCertificateActivated: Boolean, certificate: E2eiCertificate, - isSelfClient: Boolean, + isCurrentDevice: Boolean, + isLoadingCertificate: Boolean, enrollE2eiCertificate: () -> Unit, updateE2eiCertificate: () -> Unit, showCertificate: (String) -> Unit @@ -83,13 +84,6 @@ fun EndToEndIdentityCertificateItem( icon = R.drawable.ic_certificate_revoked_mls ) SerialNumberBlock(certificate.serialNumber) - ShowE2eiCertificateButton( - enabled = true, - isLoading = false, - onShowCertificateClicked = { - showCertificate(certificate.certificateDetail) - } - ) } CertificateStatus.EXPIRED -> { @@ -99,18 +93,13 @@ fun EndToEndIdentityCertificateItem( icon = R.drawable.ic_certificate_not_activated_mls ) SerialNumberBlock(certificate.serialNumber) - UpdateE2eiCertificateButton( - enabled = true, - isLoading = false, - updateE2eiCertificate - ) - ShowE2eiCertificateButton( - enabled = true, - isLoading = false, - onShowCertificateClicked = { - showCertificate(certificate.certificateDetail) - } - ) + if (isCurrentDevice) { + UpdateE2eiCertificateButton( + enabled = true, + isLoading = isLoadingCertificate, + onUpdateCertificateClicked = enrollE2eiCertificate + ) + } } CertificateStatus.VALID -> { @@ -120,23 +109,34 @@ fun EndToEndIdentityCertificateItem( icon = R.drawable.ic_certificate_valid_mls ) SerialNumberBlock(certificate.serialNumber) - ShowE2eiCertificateButton( - enabled = true, - isLoading = false, - onShowCertificateClicked = { - showCertificate(certificate.certificateDetail) - } - ) + if (isCurrentDevice) { + UpdateE2eiCertificateButton( + enabled = true, + isLoading = isLoadingCertificate, + onUpdateCertificateClicked = enrollE2eiCertificate + ) + } } } + ShowE2eiCertificateButton( + enabled = true, + isLoading = false, + onShowCertificateClicked = { + showCertificate(certificate.certificateDetail) + } + ) } else { E2EIStatusRow( label = stringResource(id = R.string.e2ei_certificat_status_not_activated), labelColor = colorsScheme().error, icon = R.drawable.ic_certificate_not_activated_mls ) - if (isSelfClient) { - GetE2eiCertificateButton(enabled = true, isLoading = false) { } + if (isCurrentDevice) { + GetE2eiCertificateButton( + enabled = true, + isLoading = isLoadingCertificate, + onGetCertificateClicked = enrollE2eiCertificate + ) } } } @@ -193,13 +193,33 @@ private fun E2EIStatusRow( fun PreviewEndToEndIdentityCertificateItem() { EndToEndIdentityCertificateItem( isE2eiCertificateActivated = true, - isSelfClient = false, + isCurrentDevice = false, + certificate = E2eiCertificate( + issuer = "Wire", + status = CertificateStatus.VALID, + serialNumber = "e5:d5:e6:75:7e:04:86:07:14:3c:a0:ed:9a:8d:e4:fd", + certificateDetail = "" + ), + isLoadingCertificate = false, + enrollE2eiCertificate = {}, + updateE2eiCertificate = {}, + showCertificate = {} + ) +} + +@PreviewMultipleThemes +@Composable +fun PreviewEndToEndIdentityCertificateSelfItem() { + EndToEndIdentityCertificateItem( + isE2eiCertificateActivated = true, + isCurrentDevice = true, certificate = E2eiCertificate( issuer = "Wire", status = CertificateStatus.VALID, serialNumber = "e5:d5:e6:75:7e:04:86:07:14:3c:a0:ed:9a:8d:e4:fd", certificateDetail = "" ), + isLoadingCertificate = false, enrollE2eiCertificate = {}, updateE2eiCertificate = {}, showCertificate = {} diff --git a/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt b/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt index cb4af1ccf9..078dd9595d 100644 --- a/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt +++ b/app/src/main/kotlin/com/wire/android/ui/settings/devices/model/DeviceDetailsState.kt @@ -16,4 +16,7 @@ data class DeviceDetailsState( val isE2eiCertificateActivated: Boolean = false, val e2eiCertificate: E2eiCertificate = E2eiCertificate(), val canBeRemoved: Boolean = false, + val isLoadingCertificate: Boolean = false, + val isE2EICertificateEnrollSuccess: Boolean = false, + val isE2EICertificateEnrollError: Boolean = false, ) diff --git a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt index 8ac17b40cd..189e86f80d 100644 --- a/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt +++ b/app/src/test/kotlin/com/wire/android/ui/home/sync/FeatureFlagNotificationViewModelTest.kt @@ -1,6 +1,7 @@ package com.wire.android.ui.home.sync import com.wire.android.config.CoroutineTestExtension +import com.wire.android.config.TestDispatcherProvider import com.wire.android.datastore.GlobalDataStore import com.wire.android.feature.AppLockSource import com.wire.android.feature.DisableAppLockUseCase @@ -295,7 +296,8 @@ class FeatureFlagNotificationViewModelTest { coreLogic = coreLogic, currentSessionUseCase = currentSession, globalDataStore = globalDataStore, - disableAppLockUseCase = disableAppLockUseCase + disableAppLockUseCase = disableAppLockUseCase, + dispatcherProvider = TestDispatcherProvider() ) init { 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 57ac9e7a77..33ec8f9596 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 @@ -1,8 +1,10 @@ package com.wire.android.ui.settings.devices +import android.content.Context import androidx.lifecycle.SavedStateHandle import com.wire.android.config.CoroutineTestExtension import com.wire.android.config.NavigationTestExtension +import com.wire.android.feature.e2ei.GetE2EICertificateUseCase import com.wire.android.framework.TestClient import com.wire.android.framework.TestUser import com.wire.android.ui.authentication.devices.remove.RemoveDeviceDialogState @@ -204,6 +206,7 @@ class DeviceDetailsViewModelTest { // then assertFalse(viewModel.state.canBeRemoved) } + @Test fun `given self temporary client, when fetching state, then canBeRemoved is true`() = runTest { // given @@ -214,6 +217,7 @@ class DeviceDetailsViewModelTest { // then assertFalse(viewModel.state.canBeRemoved) } + @Test fun `given self permanent client, when fetching state, then canBeRemoved is true`() = runTest { // given @@ -224,6 +228,7 @@ class DeviceDetailsViewModelTest { // then assertTrue(viewModel.state.canBeRemoved) } + @Test fun `given self permanent current client, when fetching state, then canBeRemoved is false`() = runTest { // given @@ -234,6 +239,7 @@ class DeviceDetailsViewModelTest { // then assertFalse(viewModel.state.canBeRemoved) } + @Test fun `given other user permanent client, when fetching state, then canBeRemoved is false`() = runTest { // given @@ -245,8 +251,27 @@ class DeviceDetailsViewModelTest { assertFalse(viewModel.state.canBeRemoved) } + @Test + fun `given get certificate clicked, then should call GetE2EICertificate`() = + runTest { + val (arrangement, viewModel) = Arrangement() + .withRequiredMockSetup() + .withClientDetailsResult(GetClientDetailsResult.Success(TestClient.CLIENT, true)) + .arrange() + + viewModel.enrollE2eiCertificate(arrangement.context) + + coVerify { + arrangement.enrolE2EICertificateUseCase(any(), any()) + } + assertTrue(viewModel.state.isLoadingCertificate) + } + private class Arrangement { + @MockK + lateinit var context: Context + @MockK lateinit var savedStateHandle: SavedStateHandle @@ -271,6 +296,9 @@ class DeviceDetailsViewModelTest { @MockK lateinit var getE2eiCertificate: GetE2eiCertificateUseCase + @MockK + lateinit var enrolE2EICertificateUseCase: GetE2EICertificateUseCase + @MockK(relaxed = true) lateinit var onSuccess: () -> Unit @@ -286,7 +314,8 @@ class DeviceDetailsViewModelTest { updateClientVerificationStatus = updateClientVerificationStatus, currentUserId = currentUserId, observeUserInfo = observeUserInfo, - e2eiCertificate = getE2eiCertificate + e2eiCertificate = getE2eiCertificate, + enrolE2EICertificateUseCase = enrolE2EICertificateUseCase ) }