Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Show a dialog when current client's certificate is revoked (WPB-6145) - cherrypick #2635

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/src/main/kotlin/com/wire/android/ui/WireActivity.kt
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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