Skip to content

Commit

Permalink
feat: Show a dialog when current client's certificate is revoked (WPB…
Browse files Browse the repository at this point in the history
…-6145) - cherrypick (#2635)
  • Loading branch information
ohassine committed Jan 26, 2024
1 parent 847af47 commit b62d100
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 16 deletions.
25 changes: 25 additions & 0 deletions app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
Expand Up @@ -55,6 +55,7 @@ import com.wire.android.BuildConfig
import com.wire.android.appLogger
import com.wire.android.config.CustomUiConfigurationProvider
import com.wire.android.config.LocalCustomUiConfigurationProvider
import com.wire.android.datastore.UserDataStore
import com.wire.android.feature.NavigationSwitchAccountActions
import com.wire.android.navigation.BackStackMode
import com.wire.android.navigation.NavigationCommand
Expand All @@ -65,6 +66,7 @@ import com.wire.android.ui.calling.ProximitySensorManager
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.common.visbility.rememberVisibilityState
import com.wire.android.ui.destinations.ConversationScreenDestination
import com.wire.android.ui.destinations.E2EIEnrollmentScreenDestination
import com.wire.android.ui.destinations.E2eiCertificateDetailsScreenDestination
Expand All @@ -78,6 +80,7 @@ import com.wire.android.ui.destinations.OtherUserProfileScreenDestination
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.E2EICertificateRevokedDialog
import com.wire.android.ui.home.E2EIRequiredDialog
import com.wire.android.ui.home.E2EIResultDialog
import com.wire.android.ui.home.E2EISnoozeDialog
Expand All @@ -91,6 +94,8 @@ import com.wire.android.ui.legalhold.dialog.requested.LegalHoldRequestedState
import com.wire.android.ui.legalhold.dialog.requested.LegalHoldRequestedViewModel
import com.wire.android.ui.theme.ThemeOption
import com.wire.android.ui.theme.WireTheme
import com.wire.android.ui.userprofile.self.dialog.LogoutOptionsDialog
import com.wire.android.ui.userprofile.self.dialog.LogoutOptionsDialogState
import com.wire.android.util.CurrentScreenManager
import com.wire.android.util.LocalSyncStateObserver
import com.wire.android.util.SyncStateObserver
Expand Down Expand Up @@ -340,6 +345,26 @@ class WireActivity : AppCompatActivity() {
hideDialogStatus = featureFlagNotificationViewModel::dismissSelfDeletingMessagesDialog
)
}
val logoutOptionsDialogState = rememberVisibilityState<LogoutOptionsDialogState>()

LogoutOptionsDialog(
dialogState = logoutOptionsDialogState,
checkboxEnabled = false,
logout = {
viewModel.doHardLogout(
{ UserDataStore(context, it) },
NavigationSwitchAccountActions(navigate)
)
logoutOptionsDialogState.dismiss()
}
)

if (shouldShowE2eiCertificateRevokedDialog) {
E2EICertificateRevokedDialog(
onLogout = { logoutOptionsDialogState.show(LogoutOptionsDialogState(shouldWipeData = true)) },
onContinue = featureFlagNotificationViewModel::dismissE2EICertificateRevokedDialog,
)
}

e2EIRequired?.let {
E2EIRequiredDialog(
Expand Down
22 changes: 22 additions & 0 deletions app/src/main/kotlin/com/wire/android/ui/WireActivityViewModel.kt
Expand Up @@ -35,6 +35,7 @@ import com.wire.android.di.ObserveSyncStateUseCaseProvider
import com.wire.android.feature.AccountSwitchUseCase
import com.wire.android.feature.SwitchAccountActions
import com.wire.android.feature.SwitchAccountParam
import com.wire.android.feature.SwitchAccountResult
import com.wire.android.migration.MigrationManager
import com.wire.android.services.ServicesManager
import com.wire.android.ui.authentication.devices.model.displayName
Expand Down Expand Up @@ -310,6 +311,27 @@ class WireActivityViewModel @Inject constructor(
}
}

// TODO: needs to be covered with test once hard logout is validated to be used
fun doHardLogout(
clearUserData: (userId: UserId) -> Unit,
switchAccountActions: SwitchAccountActions
) {
viewModelScope.launch {
coreLogic.getGlobalScope().session.currentSession().takeIf {
it is CurrentSessionResult.Success
}?.let {
val currentUserId = (it as CurrentSessionResult.Success).accountInfo.userId
coreLogic.getSessionScope(currentUserId).logout(LogoutReason.SELF_HARD_LOGOUT)
clearUserData(currentUserId)
}
accountSwitch(SwitchAccountParam.TryToSwitchToNextAccount).also {
if (it == SwitchAccountResult.NoOtherAccountToSwitch) {
globalDataStore.clearAppLockPasscode()
}
}.callAction(switchAccountActions)
}
}

fun dismissNewClientsDialog(userId: UserId) {
globalAppState = globalAppState.copy(newClientDialog = null)
viewModelScope.launch {
Expand Down
Expand Up @@ -46,6 +46,7 @@ fun WireLabelledCheckbox(
overflow: TextOverflow = TextOverflow.Visible,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
contentPadding: PaddingValues = PaddingValues(dimensions().spacing0x),
checkboxEnabled: Boolean = true,
modifier: Modifier = Modifier
) {
Row(
Expand All @@ -55,12 +56,17 @@ fun WireLabelledCheckbox(
.toggleable(
value = checked,
role = Role.Checkbox,
onValueChange = { onCheckClicked(!checked) }
onValueChange = {
if (checkboxEnabled) {
onCheckClicked(!checked)
}
}
)
.padding(contentPadding)
) {
Checkbox(
checked = checked,
enabled = checkboxEnabled,
onCheckedChange = null // null since we are handling the click on parent
)

Expand Down
50 changes: 40 additions & 10 deletions app/src/main/kotlin/com/wire/android/ui/home/E2EIDialogs.kt
Expand Up @@ -363,81 +363,111 @@ private fun E2EIRenewNoSnoozeDialog(isLoading: Boolean, updateCertificate: () ->
)
}

@Composable
fun E2EICertificateRevokedDialog(
onLogout: () -> Unit,
onContinue: () -> Unit
) {
WireDialog(
title = stringResource(id = R.string.end_to_end_identity_certificate_revoked_dialog_title),
text = stringResource(id = R.string.end_to_end_identity_certificate_revoked_dialog_description),
onDismiss = onContinue,
optionButton1Properties = WireDialogButtonProperties(
onClick = onLogout,
text = stringResource(id = R.string.end_to_end_identity_certificate_revoked_dialog_button_logout),
type = WireDialogButtonType.Primary
),
optionButton2Properties = WireDialogButtonProperties(
onClick = onContinue,
text = stringResource(id = R.string.end_to_end_identity_certificate_revoked_dialog_button_continue),
type = WireDialogButtonType.Secondary,
),
buttonsHorizontalAlignment = false,
properties = DialogProperties(usePlatformDefaultWidth = false)
)
}

@PreviewMultipleThemes
@Composable
fun PreviewE2EICertificateRevokedDialog() {
E2EICertificateRevokedDialog({}, {})
}

@PreviewMultipleThemes
@Composable
fun previewE2EIdRequiredWithSnoozeDialog() {
fun PreviewE2EIdRequiredWithSnoozeDialog() {
WireTheme {
E2EIRequiredWithSnoozeDialog(false, {}) {}
}
}

@PreviewMultipleThemes
@Composable
fun previewE2EIdRequiredNoSnoozeDialog() {
fun PreviewE2EIdRequiredNoSnoozeDialog() {
WireTheme {
E2EIRequiredNoSnoozeDialog(false) {}
}
}

@PreviewMultipleThemes
@Composable
fun previewE2EIdRenewRequiredWithSnoozeDialog() {
fun PreviewE2EIdRenewRequiredWithSnoozeDialog() {
WireTheme {
E2EIRenewWithSnoozeDialog(false, {}) {}
}
}

@PreviewMultipleThemes
@Composable
fun previewE2EIdRenewRequiredNoSnoozeDialog() {
fun PreviewE2EIdRenewRequiredNoSnoozeDialog() {
WireTheme {
E2EIRenewNoSnoozeDialog(false) {}
}
}

@PreviewMultipleThemes
@Composable
fun previewE2EIdSnoozeDialog() {
fun PreviewE2EIdSnoozeDialog() {
WireTheme {
E2EISnoozeDialog(2.seconds) {}
}
}

@PreviewMultipleThemes
@Composable
fun previewE2EIRenewErrorDialogNoGracePeriod() {
fun PreviewE2EIRenewErrorDialogNoGracePeriod() {
WireTheme {
E2EIRenewErrorDialog(FeatureFlagState.E2EIRequired.NoGracePeriod.Renew, false, { }) {}
}
}

@PreviewMultipleThemes
@Composable
fun previewE2EIRenewErrorDialogWithGracePeriod() {
fun PreviewE2EIRenewErrorDialogWithGracePeriod() {
WireTheme {
E2EIRenewErrorDialog(FeatureFlagState.E2EIRequired.WithGracePeriod.Renew(2.days), false, { }) {}
}
}

@PreviewMultipleThemes
@Composable
fun previewE2EISuccessDialog() {
fun PreviewE2EISuccessDialog() {
WireTheme {
E2EISuccessDialog({ }) {}
}
}

@PreviewMultipleThemes
@Composable
fun previewE2EIRenewErrorNoSnoozeDialog() {
fun PreviewE2EIRenewErrorNoSnoozeDialog() {
WireTheme {
E2EIErrorNoSnoozeDialog(false) { }
}
}

@PreviewMultipleThemes
@Composable
fun previewE2EIRenewErrorWithSnoozeDialog() {
fun PreviewE2EIRenewErrorWithSnoozeDialog() {
WireTheme {
E2EIErrorWithSnoozeDialog(isE2EILoading = false, updateCertificate = {}) { }
}
Expand Down
Expand Up @@ -26,6 +26,7 @@ data class FeatureFlagState(
val isFileSharingEnabledState: Boolean = true,
val fileSharingRestrictedState: SharingRestrictedState? = null,
val shouldShowGuestRoomLinkDialog: Boolean = false,
val shouldShowE2eiCertificateRevokedDialog: Boolean = false,
val shouldShowTeamAppLockDialog: Boolean = false,
val isTeamAppLockEnabled: Boolean = false,
val isGuestRoomLinkEnabled: Boolean = true,
Expand Down
Expand Up @@ -120,6 +120,13 @@ class FeatureFlagNotificationViewModel @Inject constructor(
launch { setE2EIRequiredState(userId) }
launch { setTeamAppLockFeatureFlag(userId) }
launch { observeCallEndedBecauseOfConversationDegraded(userId) }
launch { observeShouldNotifyForRevokedCertificate(userId) }
}
}

private suspend fun observeShouldNotifyForRevokedCertificate(userId: UserId) {
coreLogic.getSessionScope(userId).observeShouldNotifyForRevokedCertificate().collect {
featureFlagState = featureFlagState.copy(shouldShowE2eiCertificateRevokedDialog = it)
}
}

Expand Down Expand Up @@ -232,6 +239,15 @@ class FeatureFlagNotificationViewModel @Inject constructor(
}
}

fun dismissE2EICertificateRevokedDialog() {
featureFlagState = featureFlagState.copy(shouldShowE2eiCertificateRevokedDialog = false)
currentUserId?.let {
viewModelScope.launch {
coreLogic.getSessionScope(it).markNotifyForRevokedCertificateAsNotified()
}
}
}

fun dismissFileSharingDialog() {
featureFlagState = featureFlagState.copy(showFileSharingDialog = false)
viewModelScope.launch {
Expand Down
Expand Up @@ -41,7 +41,8 @@ import com.wire.android.ui.common.visbility.VisibilityState
@Composable
fun LogoutOptionsDialog(
dialogState: VisibilityState<LogoutOptionsDialogState>,
logout: (Boolean) -> Unit
logout: (Boolean) -> Unit,
checkboxEnabled: Boolean = true
) {
VisibilityState(dialogState) { state ->
WireDialog(
Expand All @@ -61,15 +62,16 @@ fun LogoutOptionsDialog(
)
) {
WireLabelledCheckbox(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = dimensions().spacing16x)
.clip(RoundedCornerShape(size = dimensions().spacing4x)),
label = stringResource(R.string.dialog_logout_wipe_data_checkbox),
checked = state.shouldWipeData,
onCheckClicked = remember { { dialogState.show(state.copy(shouldWipeData = it)) } },
horizontalArrangement = Arrangement.Center,
contentPadding = PaddingValues(vertical = dimensions().spacing4x),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = dimensions().spacing16x)
.clip(RoundedCornerShape(size = dimensions().spacing4x))
checkboxEnabled = checkboxEnabled
)
}
}
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Expand Up @@ -1274,6 +1274,10 @@
<string name="end_to_end_identity_renew_success_dialog_text">The certificate is updated and your device is verified.</string>
<string name="end_to_end_identity_renew_success_dialog_second_button">Certificate Details</string>
<string name="end_to_end_identity_ceritifcate">Certificate Details</string>
<string name="end_to_end_identity_certificate_revoked_dialog_title">End-to-end certificate revoked</string>
<string name="end_to_end_identity_certificate_revoked_dialog_description">Log out to reduce security risks. Then log in again, get a new certificate, and reset your password.\n\nIf you keep using this device, your conversations are no longer verified.</string>
<string name="end_to_end_identity_certificate_revoked_dialog_button_logout">Log out</string>
<string name="end_to_end_identity_certificate_revoked_dialog_button_continue">Continue Using This Device</string>
<!-- Record Audio -->
<string name="record_audio_start_label">Start Recording</string>
<string name="record_audio_recording_label">Recording Audio…</string>
Expand Down
Expand Up @@ -37,6 +37,7 @@ import com.wire.kalium.logic.feature.session.CurrentSessionResult
import com.wire.kalium.logic.feature.user.E2EIRequiredResult
import com.wire.kalium.logic.feature.user.MarkEnablingE2EIAsNotifiedUseCase
import com.wire.kalium.logic.feature.user.MarkSelfDeletionStatusAsNotifiedUseCase
import com.wire.kalium.logic.feature.user.e2ei.MarkNotifyForRevokedCertificateAsNotifiedUseCase
import com.wire.kalium.logic.feature.user.guestroomlink.MarkGuestLinkFeatureFlagAsNotChangedUseCase
import io.mockk.MockKAnnotations
import io.mockk.coEvery
Expand Down Expand Up @@ -284,6 +285,19 @@ class FeatureFlagNotificationViewModelTest {
assertEquals(null, viewModel.featureFlagState.e2EIRequired)
}

@Test
fun givenADisplayedDialog_whenDismissingIt_thenInvokeMarkFileSharingStatusAsNotifiedUseCaseOnce() = runTest {
val (arrangement, viewModel) = Arrangement()
.withCurrentSessionsFlow(flowOf(CurrentSessionResult.Success(AccountInfo.Valid(UserId("value", "domain")))))
.arrange()
coEvery { arrangement.markNotifyForRevokedCertificateAsNotified() } returns Unit

viewModel.dismissE2EICertificateRevokedDialog()

assertEquals(false, viewModel.featureFlagState.shouldShowE2eiCertificateRevokedDialog)
coVerify(exactly = 1) { arrangement.markNotifyForRevokedCertificateAsNotified() }
}

private inner class Arrangement {

@MockK
Expand All @@ -310,6 +324,9 @@ class FeatureFlagNotificationViewModelTest {
@MockK
lateinit var globalDataStore: GlobalDataStore

@MockK
lateinit var markNotifyForRevokedCertificateAsNotified: MarkNotifyForRevokedCertificateAsNotifiedUseCase

val viewModel: FeatureFlagNotificationViewModel by lazy {
FeatureFlagNotificationViewModel(
coreLogic = coreLogic,
Expand All @@ -332,6 +349,9 @@ class FeatureFlagNotificationViewModelTest {
coEvery { coreLogic.getSessionScope(any()).observeGuestRoomLinkFeatureFlag.invoke() } returns flowOf()
coEvery { coreLogic.getSessionScope(any()).observeE2EIRequired.invoke() } returns flowOf()
coEvery { coreLogic.getSessionScope(any()).calls.observeEndCallDialog() } returns flowOf()
coEvery { coreLogic.getSessionScope(any()).observeShouldNotifyForRevokedCertificate() } returns flowOf()
every { coreLogic.getSessionScope(any()).markNotifyForRevokedCertificateAsNotified } returns
markNotifyForRevokedCertificateAsNotified
coEvery { ppLockTeamFeatureConfigObserver() } returns flowOf(null)
}

Expand Down

0 comments on commit b62d100

Please sign in to comment.