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 805c3c46450..2da990a7e31 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
@@ -21,6 +21,7 @@
package com.wire.android.ui.home
import com.wire.android.ui.home.messagecomposer.state.SelfDeletionDuration
+import kotlin.time.Duration
data class FeatureFlagState(
val showFileSharingDialog: Boolean = false,
@@ -30,9 +31,18 @@ data class FeatureFlagState(
val isGuestRoomLinkEnabled: Boolean = true,
val shouldShowSelfDeletingMessagesDialog: Boolean = false,
val enforcedTimeoutDuration: SelfDeletionDuration = SelfDeletionDuration.None,
- val areSelfDeletedMessagesEnabled: Boolean = true
+ val areSelfDeletedMessagesEnabled: Boolean = true,
+ val e2EIRequired: E2EIRequired? = null,
+ val e2EISnoozeInfo: E2EISnooze? = null
) {
enum class SharingRestrictedState {
NONE, NO_USER, RESTRICTED_IN_TEAM
}
+
+ data class E2EISnooze(val timeLeft: Duration)
+
+ sealed class E2EIRequired {
+ data class WithGracePeriod(val timeLeft: Duration) : E2EIRequired()
+ object NoGracePeriod : E2EIRequired()
+ }
}
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeDialogs.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeDialogs.kt
index 3421cfa8350..1b630908f39 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/HomeDialogs.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeDialogs.kt
@@ -18,12 +18,14 @@
*
*/
+@file:Suppress("TooManyFunctions")
package com.wire.android.ui.home
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.window.DialogProperties
import com.wire.android.R
import com.wire.android.ui.common.WireDialog
import com.wire.android.ui.common.WireDialogButtonProperties
@@ -31,7 +33,9 @@ import com.wire.android.ui.common.WireDialogButtonType
import com.wire.android.ui.home.messagecomposer.state.SelfDeletionDuration
import com.wire.android.ui.theme.WireTheme
import com.wire.android.util.CustomTabsHelper
+import com.wire.android.util.toTimeLongLabelUiText
import com.wire.android.util.ui.PreviewMultipleThemes
+import kotlin.time.Duration.Companion.seconds
@Composable
fun FileRestrictionDialog(
@@ -131,6 +135,113 @@ fun WelcomeNewUserDialog(
)
}
+@Composable
+fun E2EIRequiredDialog(
+ result: FeatureFlagState.E2EIRequired,
+ getCertificate: () -> Unit,
+ snoozeDialog: (FeatureFlagState.E2EIRequired.WithGracePeriod) -> Unit,
+) {
+ when (result) {
+ FeatureFlagState.E2EIRequired.NoGracePeriod -> E2EIdRequiredNoSnoozeDialog(getCertificate = getCertificate)
+ is FeatureFlagState.E2EIRequired.WithGracePeriod -> E2EIdRequiredWithSnoozeDialog(
+ result = result,
+ getCertificate = getCertificate,
+ snoozeDialog = snoozeDialog
+ )
+ }
+}
+
+@Composable
+fun E2EIdRequiredWithSnoozeDialog(
+ result: FeatureFlagState.E2EIRequired.WithGracePeriod,
+ getCertificate: () -> Unit,
+ snoozeDialog: (FeatureFlagState.E2EIRequired.WithGracePeriod) -> 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),
+ onDismiss = { snoozeDialog(result) },
+ optionButton1Properties = WireDialogButtonProperties(
+ onClick = getCertificate,
+ text = stringResource(id = R.string.end_to_end_identity_required_dialog_positive_button),
+ type = WireDialogButtonType.Primary,
+ ),
+ optionButton2Properties = WireDialogButtonProperties(
+ onClick = { snoozeDialog(result) },
+ text = stringResource(id = R.string.end_to_end_identity_required_dialog_snooze_button),
+ type = WireDialogButtonType.Secondary,
+ ),
+ buttonsHorizontalAlignment = false,
+ properties = DialogProperties(
+ usePlatformDefaultWidth = false,
+ dismissOnBackPress = false,
+ dismissOnClickOutside = false
+ )
+ )
+}
+
+@Composable
+fun E2EIdRequiredNoSnoozeDialog(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),
+ onDismiss = getCertificate,
+ optionButton1Properties = WireDialogButtonProperties(
+ onClick = getCertificate,
+ text = stringResource(id = R.string.end_to_end_identity_required_dialog_positive_button),
+ type = WireDialogButtonType.Primary,
+ ),
+ buttonsHorizontalAlignment = false,
+ properties = DialogProperties(
+ usePlatformDefaultWidth = false,
+ dismissOnBackPress = false,
+ dismissOnClickOutside = false
+ )
+ )
+}
+
+@Composable
+fun E2EIdSnoozeDialog(
+ state: FeatureFlagState.E2EISnooze,
+ dismissDialog: () -> Unit
+) {
+ val timeText = state.timeLeft.toTimeLongLabelUiText().asString()
+ WireDialog(
+ title = stringResource(id = R.string.end_to_end_identity_required_dialog_title),
+ text = stringResource(id = R.string.end_to_end_identity_snooze_dialog_text, timeText),
+ onDismiss = dismissDialog,
+ optionButton1Properties = WireDialogButtonProperties(
+ onClick = dismissDialog,
+ text = stringResource(id = R.string.label_ok),
+ type = WireDialogButtonType.Primary,
+ )
+ )
+}
+
+@PreviewMultipleThemes
+@Composable
+fun previewE2EIdRequiredWithSnoozeDialog() {
+ WireTheme {
+ E2EIdRequiredWithSnoozeDialog(FeatureFlagState.E2EIRequired.WithGracePeriod(2.seconds), {}) {}
+ }
+}
+
+@PreviewMultipleThemes
+@Composable
+fun previewE2EIdRequiredNoSnoozeDialog() {
+ WireTheme {
+ E2EIdRequiredNoSnoozeDialog {}
+ }
+}
+
+@PreviewMultipleThemes
+@Composable
+fun previewE2EIdSnoozeDialog() {
+ WireTheme {
+ E2EIdSnoozeDialog(FeatureFlagState.E2EISnooze(2.seconds)) {}
+ }
+}
+
@PreviewMultipleThemes
@Composable
fun previewFileRestrictionDialog() {
diff --git a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt
index 8fa147ec05d..479dd2f6921 100644
--- a/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt
+++ b/app/src/main/kotlin/com/wire/android/ui/home/HomeScreen.kt
@@ -108,7 +108,7 @@ fun HomeScreen(
with(featureFlagNotificationViewModel.featureFlagState) {
if (showFileSharingDialog) {
FileRestrictionDialog(
- isFileSharingEnabled = featureFlagNotificationViewModel.featureFlagState.showFileSharingDialog,
+ isFileSharingEnabled = showFileSharingDialog,
hideDialogStatus = featureFlagNotificationViewModel::dismissFileSharingDialog
)
}
@@ -127,6 +127,21 @@ fun HomeScreen(
hideDialogStatus = featureFlagNotificationViewModel::dismissSelfDeletingMessagesDialog
)
}
+
+ e2EIRequired?.let {
+ E2EIRequiredDialog(
+ result = e2EIRequired,
+ getCertificate = featureFlagNotificationViewModel::getE2EICertificate,
+ snoozeDialog = featureFlagNotificationViewModel::snoozeE2EIdRequiredDialog
+ )
+ }
+
+ e2EISnoozeInfo?.let {
+ E2EIdSnoozeDialog(
+ state = e2EISnoozeInfo,
+ dismissDialog = featureFlagNotificationViewModel::dismissSnoozeE2EIdRequiredDialog
+ )
+ }
}
HomeContent(
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 8e94ca0f551..f5a2721b1b4 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
@@ -37,6 +37,7 @@ import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.selfDeletingMessages.TeamSelfDeleteTimer
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 dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.launch
@@ -82,6 +83,7 @@ class FeatureFlagNotificationViewModel @Inject constructor(
setFileSharingState(userId)
observeTeamSettingsSelfDeletionStatus(userId)
setGuestRoomLinkFeatureFlag(userId)
+ setE2EIRequiredState(userId)
}
}
}
@@ -147,6 +149,16 @@ class FeatureFlagNotificationViewModel @Inject constructor(
}
}
+ private fun setE2EIRequiredState(userId: UserId) = viewModelScope.launch {
+ coreLogic.getSessionScope(userId).observeE2EIRequired().collect { result ->
+ val state = when (result) {
+ is E2EIRequiredResult.WithGracePeriod -> FeatureFlagState.E2EIRequired.WithGracePeriod(result.gracePeriod)
+ E2EIRequiredResult.NoGracePeriod -> FeatureFlagState.E2EIRequired.NoGracePeriod
+ }
+ featureFlagState = featureFlagState.copy(e2EIRequired = state)
+ }
+ }
+
fun dismissSelfDeletingMessagesDialog() {
featureFlagState = featureFlagState.copy(shouldShowSelfDeletingMessagesDialog = false)
viewModelScope.launch {
@@ -167,4 +179,25 @@ class FeatureFlagNotificationViewModel @Inject constructor(
}
featureFlagState = featureFlagState.copy(shouldShowGuestRoomLinkDialog = false)
}
+
+ fun getE2EICertificate() {
+ // TODO do the magic
+ featureFlagState = featureFlagState.copy(e2EIRequired = null)
+ }
+
+ fun snoozeE2EIdRequiredDialog(result: FeatureFlagState.E2EIRequired.WithGracePeriod) {
+ featureFlagState = featureFlagState.copy(
+ e2EIRequired = null,
+ e2EISnoozeInfo = FeatureFlagState.E2EISnooze(result.timeLeft)
+ )
+ currentUserId?.let { userId ->
+ viewModelScope.launch {
+ coreLogic.getSessionScope(userId).markE2EIRequiredAsNotified()
+ }
+ }
+ }
+
+ fun dismissSnoozeE2EIdRequiredDialog() {
+ featureFlagState = featureFlagState.copy(e2EISnoozeInfo = null)
+ }
}
diff --git a/app/src/main/kotlin/com/wire/android/util/DurationUtil.kt b/app/src/main/kotlin/com/wire/android/util/DurationUtil.kt
new file mode 100644
index 00000000000..011d2032ae1
--- /dev/null
+++ b/app/src/main/kotlin/com/wire/android/util/DurationUtil.kt
@@ -0,0 +1,36 @@
+/*
+ * Wire
+ * Copyright (C) 2023 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.util
+
+import com.wire.android.R
+import com.wire.android.util.ui.UIText
+import kotlin.time.Duration
+
+fun Duration.toTimeLongLabelUiText(): UIText.PluralResource = when {
+ inWholeDays >= DAYS_IN_WEEK -> {
+ val weeks = inWholeDays.toInt() / DAYS_IN_WEEK
+ UIText.PluralResource(R.plurals.weeks_long_label, weeks, weeks)
+ }
+
+ inWholeDays >= 1 -> UIText.PluralResource(R.plurals.days_long_label, inWholeDays.toInt(), inWholeDays)
+ inWholeHours >= 1 -> UIText.PluralResource(R.plurals.hours_long_label, inWholeHours.toInt(), inWholeHours)
+ inWholeMinutes >= 1 -> UIText.PluralResource(R.plurals.minutes_long_label, inWholeMinutes.toInt(), inWholeMinutes)
+ else -> UIText.PluralResource(R.plurals.seconds_long_label, inWholeSeconds.toInt(), inWholeSeconds)
+}
+
+private const val DAYS_IN_WEEK = 7
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 0eb37fc1210..38ca1c3f3e9 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1091,4 +1091,11 @@
No Application has been found to do Action
Delete Account
If you continue, will send a message via email. Follow the link to permanetly delete your account.
+
+ End-to-end identity certificate
+ As of today, your team uses end-to-end identity to make Wire’s usage more secure and practicable. The device verification takes place automatically using a certificate and replaces the previous manual process. This way, you communicate with the highest security standard.\n\nEnter the credentials of your identity provider in the next step to automatically get a verification certificate for this device.
+ Your team now uses end-to-end identity to make Wire’s usage more secure. The device verification takes place automatically using a certificate.\n\nEnter the credentials of your identity provider in the next step to automatically get a verification certificate for this device.
+ Get certificate
+ Remind me later
+ You can get the certificate in your Wire settings during the next %1$s. Open Devices and select Get Certificate for your current device.\nTo continue using Wire without interruption, retrieve it in time – it doesn’t take long.
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 ca7b08f2024..9368093f5fc 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
@@ -11,6 +11,8 @@ import com.wire.kalium.logic.data.user.UserId
import com.wire.kalium.logic.feature.auth.AccountInfo
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.feature.user.MarkEnablingE2EIAsNotifiedUseCase
import com.wire.kalium.logic.feature.user.MarkSelfDeletionStatusAsNotifiedUseCase
import com.wire.kalium.logic.feature.user.guestroomlink.MarkGuestLinkFeatureFlagAsNotChangedUseCase
import io.mockk.MockKAnnotations
@@ -32,6 +34,7 @@ import org.amshove.kluent.internal.assertEquals
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
+import kotlin.time.Duration.Companion.days
@OptIn(ExperimentalCoroutinesApi::class)
class FeatureFlagNotificationViewModelTest {
@@ -124,6 +127,48 @@ class FeatureFlagNotificationViewModelTest {
assertEquals(false, viewModel.featureFlagState.shouldShowSelfDeletingMessagesDialog)
}
+ @Test
+ fun givenE2EIRequired_thenShowDialog() = runTest(mainThreadSurrogate) {
+ val (arrangement, viewModel) = Arrangement()
+ .withE2EIRequiredSettings(E2EIRequiredResult.NoGracePeriod)
+ .arrange()
+ viewModel.initialSync()
+ advanceUntilIdle()
+
+ assertEquals(FeatureFlagState.E2EIRequired.NoGracePeriod, viewModel.featureFlagState.e2EIRequired)
+ }
+
+ @Test
+ fun givenE2EIRequiredDialogShown_whenSnoozeCalled_thenItSnoozedAndDialogShown() = runTest(mainThreadSurrogate) {
+ val gracePeriod = 1.days
+ val (arrangement, viewModel) = Arrangement()
+ .withE2EIRequiredSettings(E2EIRequiredResult.WithGracePeriod(gracePeriod))
+ .arrange()
+ viewModel.initialSync()
+ advanceUntilIdle()
+
+ viewModel.snoozeE2EIdRequiredDialog(FeatureFlagState.E2EIRequired.WithGracePeriod(gracePeriod))
+ advanceUntilIdle()
+
+ assertEquals(null, viewModel.featureFlagState.e2EIRequired)
+ assertEquals(FeatureFlagState.E2EISnooze(gracePeriod), viewModel.featureFlagState.e2EISnoozeInfo)
+ coVerify(exactly = 1) { arrangement.markE2EIRequiredAsNotified() }
+ }
+
+ @Test
+ fun givenSnoozeE2EIRequiredDialogShown_whenDismissCalled_thenItSnoozedAndDialogHidden() = runTest(mainThreadSurrogate) {
+ val gracePeriod = 1.days
+ val (arrangement, viewModel) = Arrangement()
+ .withE2EIRequiredSettings(E2EIRequiredResult.WithGracePeriod(gracePeriod))
+ .arrange()
+ viewModel.snoozeE2EIdRequiredDialog(FeatureFlagState.E2EIRequired.WithGracePeriod(gracePeriod))
+ advanceUntilIdle()
+
+ viewModel.dismissSnoozeE2EIdRequiredDialog()
+
+ assertEquals(null, viewModel.featureFlagState.e2EISnoozeInfo)
+ }
+
private inner class Arrangement {
init {
MockKAnnotations.init(this, relaxUnitFun = true)
@@ -145,6 +190,9 @@ class FeatureFlagNotificationViewModelTest {
@MockK
lateinit var markSelfDeletingStatusAsNotified: MarkSelfDeletionStatusAsNotifiedUseCase
+ @MockK
+ lateinit var markE2EIRequiredAsNotified: MarkEnablingE2EIAsNotifiedUseCase
+
@MockK
lateinit var navigationManager: NavigationManager
@@ -156,8 +204,10 @@ class FeatureFlagNotificationViewModelTest {
init {
every { coreLogic.getSessionScope(any()).markGuestLinkFeatureFlagAsNotChanged } returns markGuestLinkFeatureFlagAsNotChanged
every { coreLogic.getSessionScope(any()).markSelfDeletingMessagesAsNotified } returns markSelfDeletingStatusAsNotified
+ every { coreLogic.getSessionScope(any()).markE2EIRequiredAsNotified } returns markE2EIRequiredAsNotified
coEvery { coreLogic.getSessionScope(any()).observeFileSharingStatus.invoke() } returns flowOf()
coEvery { coreLogic.getSessionScope(any()).observeGuestRoomLinkFeatureFlag.invoke() } returns flowOf()
+ coEvery { coreLogic.getSessionScope(any()).observeE2EIRequired.invoke() } returns flowOf()
}
fun withCurrentSessions(result: CurrentSessionResult) = apply {
@@ -176,6 +226,10 @@ class FeatureFlagNotificationViewModelTest {
coEvery { coreLogic.getSessionScope(any()).observeGuestRoomLinkFeatureFlag() } returns stateFlow
}
+ fun withE2EIRequiredSettings(result: E2EIRequiredResult) = apply {
+ coEvery { coreLogic.getSessionScope(any()).observeE2EIRequired() } returns flowOf(result)
+ }
+
fun arrange() = this to viewModel
}
}
diff --git a/kalium b/kalium
index 1d2b66fc447..c6605869425 160000
--- a/kalium
+++ b/kalium
@@ -1 +1 @@
-Subproject commit 1d2b66fc4476789574ffd7344ee5a7d635fd0758
+Subproject commit c66058694256e6782aefe67474e31f0628b9850e