Merge pull request #3649 from element-hq/feature/bma/cryptoCopyAdjustment

Crypto copy adjustment
This commit is contained in:
Benoit Marty 2024-10-10 19:41:17 +02:00 committed by GitHub
commit 19f0be51fe
21 changed files with 127 additions and 61 deletions

View file

@ -8,6 +8,7 @@
package io.element.android.appconfig package io.element.android.appconfig
object LearnMoreConfig { object LearnMoreConfig {
const val ENCRYPTION_URL: String = "https://element.io/help#encryption"
const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5" const val SECURE_BACKUP_URL: String = "https://element.io/help#encryption5"
const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18" const val IDENTITY_CHANGE_URL: String = "https://element.io/help#encryption18"
} }

View file

@ -31,7 +31,6 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.CommonStrings
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@Composable @Composable
@ -50,7 +49,7 @@ fun ResetIdentityRootView(
buttons = { buttons = {
Button( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(id = CommonStrings.action_continue), text = stringResource(id = R.string.screen_encryption_reset_action_continue_reset),
onClick = { state.eventSink(ResetIdentityRootEvent.Continue) }, onClick = { state.eventSink(ResetIdentityRootEvent.Continue) },
destructive = true, destructive = true,
) )
@ -98,9 +97,9 @@ private fun Content() {
iconComposable = { iconComposable = {
Icon( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
imageVector = CompoundIcons.Close(), imageVector = CompoundIcons.Info(),
contentDescription = null, contentDescription = null,
tint = ElementTheme.colors.iconCriticalPrimary, tint = ElementTheme.colors.iconSecondary,
) )
}, },
), ),
@ -109,9 +108,9 @@ private fun Content() {
iconComposable = { iconComposable = {
Icon( Icon(
modifier = Modifier.size(20.dp), modifier = Modifier.size(20.dp),
imageVector = CompoundIcons.Close(), imageVector = CompoundIcons.Info(),
contentDescription = null, contentDescription = null,
tint = ElementTheme.colors.iconCriticalPrimary, tint = ElementTheme.colors.iconSecondary,
) )
}, },
), ),

View file

@ -18,7 +18,7 @@
<string name="screen_create_new_recovery_key_title">"Reset the encryption for your account using another device"</string> <string name="screen_create_new_recovery_key_title">"Reset the encryption for your account using another device"</string>
<string name="screen_encryption_reset_action_continue_reset">"Continue reset"</string> <string name="screen_encryption_reset_action_continue_reset">"Continue reset"</string>
<string name="screen_encryption_reset_bullet_1">"Your account details, contacts, preferences, and chat list will be kept"</string> <string name="screen_encryption_reset_bullet_1">"Your account details, contacts, preferences, and chat list will be kept"</string>
<string name="screen_encryption_reset_bullet_2">"You will lose your existing message history unless it is stored on another device"</string> <string name="screen_encryption_reset_bullet_2">"You will lose any message history thats stored only on the server"</string>
<string name="screen_encryption_reset_bullet_3">"You will need to verify all your existing devices and contacts again"</string> <string name="screen_encryption_reset_bullet_3">"You will need to verify all your existing devices and contacts again"</string>
<string name="screen_encryption_reset_footer">"Only reset your identity if you dont have access to another signed-in device and youve lost your recovery key."</string> <string name="screen_encryption_reset_footer">"Only reset your identity if you dont have access to another signed-in device and youve lost your recovery key."</string>
<string name="screen_encryption_reset_title">"Can\'t confirm? Youll need to reset your identity."</string> <string name="screen_encryption_reset_title">"Can\'t confirm? Youll need to reset your identity."</string>

View file

@ -60,7 +60,7 @@ class ResetIdentityRootViewTest {
ResetIdentityRootState(displayConfirmationDialog = false, eventSink = eventsRecorder), ResetIdentityRootState(displayConfirmationDialog = false, eventSink = eventsRecorder),
) )
rule.clickOn(CommonStrings.action_continue) rule.clickOn(R.string.screen_encryption_reset_action_continue_reset)
eventsRecorder.assertSingle(ResetIdentityRootEvent.Continue) eventsRecorder.assertSingle(ResetIdentityRootEvent.Continue)
} }

View file

@ -23,6 +23,8 @@ android {
setupAnvil() setupAnvil()
dependencies { dependencies {
implementation(projects.appconfig)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core) implementation(projects.libraries.core)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api)

View file

@ -18,9 +18,11 @@ import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode import io.element.android.anvilannotations.ContributesNode
import io.element.android.appconfig.LearnMoreConfig
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.features.logout.api.util.onSuccessLogout import io.element.android.features.logout.api.util.onSuccessLogout
import io.element.android.features.verifysession.api.VerifySessionEntryPoint import io.element.android.features.verifysession.api.VerifySessionEntryPoint
import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab
import io.element.android.libraries.architecture.inputs import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope import io.element.android.libraries.di.SessionScope
@ -36,6 +38,10 @@ class VerifySelfSessionNode @AssistedInject constructor(
showDeviceVerifiedScreen = inputs<VerifySessionEntryPoint.Params>().showDeviceVerifiedScreen, showDeviceVerifiedScreen = inputs<VerifySessionEntryPoint.Params>().showDeviceVerifiedScreen,
) )
private fun onLearnMoreClick(activity: Activity, dark: Boolean) {
activity.openUrlInChromeCustomTab(null, dark, LearnMoreConfig.ENCRYPTION_URL)
}
@Composable @Composable
override fun View(modifier: Modifier) { override fun View(modifier: Modifier) {
val state = presenter.present() val state = presenter.present()
@ -44,6 +50,9 @@ class VerifySelfSessionNode @AssistedInject constructor(
VerifySelfSessionView( VerifySelfSessionView(
state = state, state = state,
modifier = modifier, modifier = modifier,
onLearnMoreClick = {
onLearnMoreClick(activity, isDark)
},
onEnterRecoveryKey = callback::onEnterRecoveryKey, onEnterRecoveryKey = callback::onEnterRecoveryKey,
onResetKey = callback::onResetKey, onResetKey = callback::onResetKey,
onFinish = callback::onDone, onFinish = callback::onDone,

View file

@ -23,6 +23,8 @@ data class VerifySelfSessionState(
@Stable @Stable
sealed interface VerificationStep { sealed interface VerificationStep {
data object Loading : VerificationStep data object Loading : VerificationStep
// FIXME canEnterRecoveryKey value is never read.
data class Initial(val canEnterRecoveryKey: Boolean, val isLastDevice: Boolean = false) : VerificationStep data class Initial(val canEnterRecoveryKey: Boolean, val isLastDevice: Boolean = false) : VerificationStep
data object Canceled : VerificationStep data object Canceled : VerificationStep
data object AwaitingOtherDeviceResponse : VerificationStep data object AwaitingOtherDeviceResponse : VerificationStep

View file

@ -9,13 +9,13 @@ package io.element.android.features.verifysession.impl
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
@ -64,6 +64,7 @@ import io.element.android.features.verifysession.impl.VerifySelfSessionState.Ver
@Composable @Composable
fun VerifySelfSessionView( fun VerifySelfSessionView(
state: VerifySelfSessionState, state: VerifySelfSessionState,
onLearnMoreClick: () -> Unit,
onEnterRecoveryKey: () -> Unit, onEnterRecoveryKey: () -> Unit,
onResetKey: () -> Unit, onResetKey: () -> Unit,
onFinish: () -> Unit, onFinish: () -> Unit,
@ -140,7 +141,10 @@ fun VerifySelfSessionView(
) )
} }
) { ) {
Content(flowState = verificationFlowStep) Content(
flowState = verificationFlowStep,
onLearnMoreClick = onLearnMoreClick,
)
} }
} }
@ -203,38 +207,68 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
} }
@Composable @Composable
private fun Content(flowState: FlowStep) { private fun Content(
Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.Center) { flowState: FlowStep,
if (flowState is FlowStep.Verifying) { onLearnMoreClick: () -> Unit,
) {
when (flowState) {
is VerifySelfSessionState.VerificationStep.Initial -> {
ContentInitial(onLearnMoreClick)
}
is FlowStep.Verifying -> {
ContentVerifying(flowState) ContentVerifying(flowState)
} }
else -> Unit
}
}
@Composable
private fun ContentInitial(
onLearnMoreClick: () -> Unit,
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
) {
Text(
modifier = Modifier
.clickable { onLearnMoreClick() }
.padding(vertical = 4.dp, horizontal = 16.dp),
text = stringResource(CommonStrings.action_learn_more),
style = ElementTheme.typography.fontBodyLgMedium
)
} }
} }
@Composable @Composable
private fun ContentVerifying(verificationFlowStep: FlowStep.Verifying) { private fun ContentVerifying(verificationFlowStep: FlowStep.Verifying) {
when (verificationFlowStep.data) { Box(
is SessionVerificationData.Decimals -> { modifier = Modifier.fillMaxSize(),
val text = verificationFlowStep.data.decimals.joinToString(separator = " - ") { it.toString() } contentAlignment = Alignment.Center
Text( ) {
modifier = Modifier.fillMaxWidth(), when (verificationFlowStep.data) {
text = text, is SessionVerificationData.Decimals -> {
style = ElementTheme.typography.fontHeadingLgBold, val text = verificationFlowStep.data.decimals.joinToString(separator = " - ") { it.toString() }
color = MaterialTheme.colorScheme.primary, Text(
textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(),
) text = text,
} style = ElementTheme.typography.fontHeadingLgBold,
is SessionVerificationData.Emojis -> { color = MaterialTheme.colorScheme.primary,
// We want each row to have up to 4 emojis textAlign = TextAlign.Center,
val rows = verificationFlowStep.data.emojis.chunked(4) )
Column( }
modifier = Modifier.fillMaxWidth(), is SessionVerificationData.Emojis -> {
verticalArrangement = Arrangement.spacedBy(40.dp), // We want each row to have up to 4 emojis
) { val rows = verificationFlowStep.data.emojis.chunked(4)
rows.forEach { emojis -> Column(
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { modifier = Modifier.fillMaxWidth(),
for (emoji in emojis) { verticalArrangement = Arrangement.spacedBy(40.dp),
EmojiItemView(emoji = emoji, modifier = Modifier.widthIn(max = 60.dp)) ) {
rows.forEach { emojis ->
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
for (emoji in emojis) {
EmojiItemView(emoji = emoji, modifier = Modifier.widthIn(max = 60.dp))
}
} }
} }
} }
@ -292,14 +326,14 @@ private fun BottomMenu(
text = stringResource(R.string.screen_identity_use_another_device), text = stringResource(R.string.screen_identity_use_another_device),
onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) }, onClick = { eventSink(VerifySelfSessionViewEvents.RequestVerification) },
) )
OutlinedButton( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_session_verification_enter_recovery_key), text = stringResource(R.string.screen_session_verification_enter_recovery_key),
onClick = onEnterRecoveryKey, onClick = onEnterRecoveryKey,
) )
} }
// This option should always be displayed // This option should always be displayed
TextButton( OutlinedButton(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_identity_confirmation_cannot_confirm), text = stringResource(R.string.screen_identity_confirmation_cannot_confirm),
onClick = onResetKey, onClick = onResetKey,
@ -402,6 +436,7 @@ private fun BottomMenu(
internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) = ElementPreview { internal fun VerifySelfSessionViewPreview(@PreviewParameter(VerifySelfSessionStateProvider::class) state: VerifySelfSessionState) = ElementPreview {
VerifySelfSessionView( VerifySelfSessionView(
state = state, state = state,
onLearnMoreClick = {},
onEnterRecoveryKey = {}, onEnterRecoveryKey = {},
onResetKey = {}, onResetKey = {},
onFinish = {}, onFinish = {},

View file

@ -146,6 +146,22 @@ class VerifySelfSessionViewTest {
} }
} }
@Config(qualifiers = "h1024dp")
@Test
fun `clicking on learn more invokes the expected callback`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>(expectEvents = false)
ensureCalledOnce { callback ->
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true),
eventSink = eventsRecorder
),
onLearnMoreClick = callback,
)
rule.clickOn(CommonStrings.action_learn_more)
}
}
@Test @Test
fun `clicking on they match emits the expected event`() { fun `clicking on they match emits the expected event`() {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>() val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
@ -222,6 +238,7 @@ class VerifySelfSessionViewTest {
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setVerifySelfSessionView( private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setVerifySelfSessionView(
state: VerifySelfSessionState, state: VerifySelfSessionState,
onLearnMoreClick: () -> Unit = EnsureNeverCalled(),
onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(), onEnterRecoveryKey: () -> Unit = EnsureNeverCalled(),
onFinished: () -> Unit = EnsureNeverCalled(), onFinished: () -> Unit = EnsureNeverCalled(),
onResetKey: () -> Unit = EnsureNeverCalled(), onResetKey: () -> Unit = EnsureNeverCalled(),
@ -230,6 +247,7 @@ class VerifySelfSessionViewTest {
setContent { setContent {
VerifySelfSessionView( VerifySelfSessionView(
state = state, state = state,
onLearnMoreClick = onLearnMoreClick,
onEnterRecoveryKey = onEnterRecoveryKey, onEnterRecoveryKey = onEnterRecoveryKey,
onFinish = onFinished, onFinish = onFinished,
onResetKey = onResetKey, onResetKey = onResetKey,

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:ad503075a2ff0e5fcd544c66a1a1b186abc2d05126facc397e0d812cd2206fc5 oid sha256:4a3d734e3d2a572344ab26b042cf249afc56c7eff2d1454565ddc75e54faaced
size 71628 size 69147

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:669a2f51c4276c4582e624dda9779d008a01452f0f2a1331f9fbb2b7bbbb38de oid sha256:0a41f758a358792131a03f7015710fe28c9e8fecb49cafa72fe4b0a0a21bbf9d
size 58159 size 58206

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:9a8c8d8ed03e7f85a0954bd06ac70336994dfaaec318c1d7f2c386f062e6c1e9 oid sha256:676c85998a4b76894de1cbf470e18876abc791cc56a8322db9e1d24e32adfd56
size 70136 size 67963

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:d2ffe911b13cbedf16829ed0b449f291d291f64d87917b827836a97204fe8c65 oid sha256:a3f8388e77e222255f9bcd04c81094a88d57176f588feb476146035f56c0054c
size 56167 size 56116

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e9513b48683bec56a8892bcc4f953117547bc570acd20dda760fb60904532449 oid sha256:ea4aeceb9e0d72642c9be0d8dd1abc7a3b82bd05ce05a07aa95bec4e0b2a9df0
size 31501 size 34612

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:4e5ecf84200d3c3f1e213552568bb8c1893975403363bfe9856414208ff7d84b oid sha256:4a6896e2fc99b34d37dbc7494c21990d31aa6fb99500d61764b7d04afcffca74
size 32678 size 33690

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:e9513b48683bec56a8892bcc4f953117547bc570acd20dda760fb60904532449 oid sha256:ea4aeceb9e0d72642c9be0d8dd1abc7a3b82bd05ce05a07aa95bec4e0b2a9df0
size 31501 size 34612

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:05f503995e71fd66a03efc1b675eacf3bf4f90d1b07e73e171ebb78014862ed6 oid sha256:84627a80338301f58e1fc877a587ff602c86dff848684d9ba56a49dde4bf2d9d
size 26020 size 29801

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8ed7b8e3bdda766f21419bec8f60497b3e4d18f54ec41a65160be363e165073b oid sha256:70adb44af338740cbc2f2297ca172d35563133d163bcfafbfe09295a4aa644c8
size 30567 size 33568

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:f633d654de29a28d32308039e9b78b72da948de605fcecf4b61ba3aab6068866 oid sha256:2cb5f4af03cdab9e432b790c8f5346358659d7338f1aecc6449f816b765d3b36
size 31180 size 32191

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:8ed7b8e3bdda766f21419bec8f60497b3e4d18f54ec41a65160be363e165073b oid sha256:70adb44af338740cbc2f2297ca172d35563133d163bcfafbfe09295a4aa644c8
size 30567 size 33568

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1 version https://git-lfs.github.com/spec/v1
oid sha256:72d7a54da8502a7e70d0e6b1bcf8c6e2e0d2e78d328f73b7edacec58a2bf99b2 oid sha256:65709ff3ec7a5a495370dfc58cdec1898d07e0f6eea8b41caa1ae40579fce817
size 25377 size 28896