mirror of
https://github.com/Novaloop-AG/element-x-android.git
synced 2026-10-31 16:22:49 +00:00
Merge pull request #7827 from element-hq/feature/bma/multiAccountIncomingUserVerification
Multi account incoming user verification
This commit is contained in:
+31
-7
@@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationR
|
||||
import io.element.android.libraries.matrix.api.verification.VerificationRequest
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_3
|
||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||
import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
@@ -83,6 +84,23 @@ class IncomingVerificationRequestObserverTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `incoming user verification request of another session is emitted with its session id`() = runTest {
|
||||
val matrixSessionCache = createMatrixSessionCache()
|
||||
val observer = IncomingVerificationRequestObserver(
|
||||
matrixSessionCache = matrixSessionCache,
|
||||
appForegroundStateService = FakeAppForegroundStateService(),
|
||||
)
|
||||
observer.incomingVerificationRequests().test {
|
||||
matrixSessionCache.getOrRestore(A_SESSION_ID)
|
||||
matrixSessionCache.getOrRestore(A_SESSION_ID_2)
|
||||
runCurrent()
|
||||
val request = anIncomingUserVerificationRequest()
|
||||
sessionVerificationService2.listener!!.onIncomingSessionRequest(request)
|
||||
assertThat(awaitItem()).isEqualTo(IncomingVerificationRequestData(A_SESSION_ID_2, request))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `incoming verification request is emitted when the app goes to foreground`() = runTest {
|
||||
val matrixSessionCache = createMatrixSessionCache()
|
||||
@@ -156,12 +174,18 @@ class IncomingVerificationRequestObserverTest {
|
||||
}
|
||||
|
||||
private fun anIncomingVerificationRequest() = VerificationRequest.Incoming.OtherSession(
|
||||
details = SessionVerificationRequestDetails(
|
||||
senderProfile = MatrixUser(A_SESSION_ID),
|
||||
flowId = FlowId("flowId"),
|
||||
deviceId = DeviceId("deviceId"),
|
||||
deviceDisplayName = "a device name",
|
||||
firstSeenTimestamp = 0,
|
||||
)
|
||||
details = aSessionVerificationRequestDetails(senderProfile = MatrixUser(A_SESSION_ID)),
|
||||
)
|
||||
|
||||
private fun anIncomingUserVerificationRequest() = VerificationRequest.Incoming.User(
|
||||
details = aSessionVerificationRequestDetails(senderProfile = MatrixUser(A_USER_ID_3)),
|
||||
)
|
||||
|
||||
private fun aSessionVerificationRequestDetails(senderProfile: MatrixUser) = SessionVerificationRequestDetails(
|
||||
senderProfile = senderProfile,
|
||||
flowId = FlowId("flowId"),
|
||||
deviceId = DeviceId("deviceId"),
|
||||
deviceDisplayName = "a device name",
|
||||
firstSeenTimestamp = 0,
|
||||
)
|
||||
}
|
||||
|
||||
+20
-20
@@ -33,7 +33,6 @@ import io.element.android.features.verifysession.impl.R
|
||||
import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step
|
||||
import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu
|
||||
import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying
|
||||
import io.element.android.features.verifysession.impl.ui.VerificationUserProfileContent
|
||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
||||
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
|
||||
import io.element.android.libraries.designsystem.components.BigIcon
|
||||
@@ -41,6 +40,7 @@ import io.element.android.libraries.designsystem.components.button.BackButton
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
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.OutlinedButton
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||
@@ -169,12 +169,12 @@ private fun ContentInitial(
|
||||
stepInitial: Step.Initial,
|
||||
request: VerificationRequest.Incoming,
|
||||
) {
|
||||
when (request) {
|
||||
is VerificationRequest.Incoming.OtherSession -> {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
when (request) {
|
||||
is VerificationRequest.Incoming.OtherSession -> {
|
||||
AccountInfoCard(
|
||||
state = AccountInfoCardState(
|
||||
matrixUser = stepInitial.currentUser,
|
||||
@@ -194,15 +194,15 @@ private fun ContentInitial(
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
}
|
||||
is VerificationRequest.Incoming.User -> {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp),
|
||||
) {
|
||||
VerificationUserProfileContent(
|
||||
user = request.details.senderProfile,
|
||||
is VerificationRequest.Incoming.User -> {
|
||||
AccountInfoCard(
|
||||
state = AccountInfoCardState(
|
||||
matrixUser = stepInitial.currentUser,
|
||||
mode = AccountInfoCardMode.UserVerification(
|
||||
otherUser = request.details.senderProfile,
|
||||
hint = stringResource(R.string.screen_session_verification_user_requesting_verification),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -224,7 +224,7 @@ private fun IncomingVerificationBottomMenu(
|
||||
showProgress = step.isWaiting,
|
||||
onClick = { eventSink(IncomingVerificationViewEvent.StartVerification) },
|
||||
)
|
||||
TextButton(
|
||||
OutlinedButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = stringResource(CommonStrings.action_ignore),
|
||||
enabled = !step.isWaiting,
|
||||
@@ -269,9 +269,9 @@ private fun IncomingVerificationBottomMenu(
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun IncomingVerificationViewPreview(@PreviewParameter(
|
||||
IncomingVerificationStatePreviewParam::class
|
||||
) state: IncomingVerificationState) = ElementPreview {
|
||||
internal fun IncomingVerificationViewPreview(
|
||||
@PreviewParameter(IncomingVerificationStatePreviewParam::class) state: IncomingVerificationState
|
||||
) = ElementPreview {
|
||||
IncomingVerificationView(
|
||||
state = state,
|
||||
)
|
||||
|
||||
-94
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Element Creations Ltd.
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
package io.element.android.features.verifysession.impl.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.element.android.compound.theme.ElementTheme
|
||||
import io.element.android.libraries.designsystem.components.avatar.Avatar
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
|
||||
import io.element.android.libraries.designsystem.components.avatar.AvatarType
|
||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||
import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE
|
||||
import io.element.android.libraries.designsystem.theme.components.Text
|
||||
import io.element.android.libraries.designsystem.utils.CommonDrawables
|
||||
import io.element.android.libraries.matrix.api.core.UserId
|
||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||
import io.element.android.libraries.matrix.ui.model.getBestName
|
||||
|
||||
/**
|
||||
* Ref: https://www.figma.com/design/lMrKOhS8BEb75GXVq7FnNI/ER-96--User-Verification-by-Emoji?node-id=116-52049
|
||||
*/
|
||||
@Composable
|
||||
fun VerificationUserProfileContent(
|
||||
user: MatrixUser,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val avatarData = remember(user) {
|
||||
user.getAvatarData(AvatarSize.UserVerification)
|
||||
}
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(ElementTheme.colors.bgSubtleSecondary)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Avatar(
|
||||
avatarData = avatarData,
|
||||
avatarType = AvatarType.User,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
|
||||
Text(
|
||||
text = user.getBestName(),
|
||||
style = ElementTheme.typography.fontBodyLgMedium,
|
||||
color = ElementTheme.colors.textPrimary,
|
||||
)
|
||||
|
||||
if (user.displayName.isNullOrEmpty().not()) {
|
||||
Text(
|
||||
text = user.userId.value,
|
||||
style = ElementTheme.typography.fontBodyMdRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewsDayNight
|
||||
@Composable
|
||||
internal fun VerificationUserProfileContentPreview() = ElementPreview(
|
||||
drawableFallbackForImages = CommonDrawables.sample_avatar
|
||||
) {
|
||||
VerificationUserProfileContent(
|
||||
user = MatrixUser(
|
||||
userId = UserId("@alice:example.com"),
|
||||
displayName = USER_NAME_ALICE,
|
||||
avatarUrl = "https://example.com/avatar.png",
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -44,6 +44,7 @@
|
||||
<string name="screen_session_verification_use_another_device_title">"Open the app on another verified device"</string>
|
||||
<string name="screen_session_verification_user_initiator_subtitle">"For extra security, verify this user by comparing a set of emojis on your devices. Do this by using a trusted way to communicate."</string>
|
||||
<string name="screen_session_verification_user_initiator_title">"Verify this user?"</string>
|
||||
<string name="screen_session_verification_user_requesting_verification">"User requesting verification:"</string>
|
||||
<string name="screen_session_verification_user_responder_subtitle">"For extra security, another user wants to verify your digital identity. You’ll be shown a set of emojis to compare."</string>
|
||||
<string name="screen_session_verification_waiting_another_device_subtitle">"You should see a popup on the other device. Start the verification from there now."</string>
|
||||
<string name="screen_session_verification_waiting_another_device_title">"Start verification on the other device"</string>
|
||||
|
||||
+27
@@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.test.A_DEVICE_ID
|
||||
import io.element.android.libraries.matrix.test.A_SESSION_ID
|
||||
import io.element.android.libraries.matrix.test.A_TIMESTAMP
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID
|
||||
import io.element.android.libraries.matrix.test.A_USER_ID_2
|
||||
import io.element.android.libraries.matrix.test.A_USER_NAME
|
||||
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
|
||||
import io.element.android.libraries.sessionstorage.api.SessionStore
|
||||
@@ -49,7 +50,19 @@ class IncomingVerificationPresenterTest {
|
||||
|
||||
@Test
|
||||
fun `present - the current user is built from the stored session`() = runTest {
|
||||
assertCurrentUserIsBuiltFromStoredSession(anIncomingSessionVerificationRequest)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `present - incoming user verification - the current user is built from the stored session`() = runTest {
|
||||
assertCurrentUserIsBuiltFromStoredSession(anIncomingUserVerificationRequest)
|
||||
}
|
||||
|
||||
private suspend fun TestScope.assertCurrentUserIsBuiltFromStoredSession(
|
||||
verificationRequest: VerificationRequest.Incoming,
|
||||
) {
|
||||
createPresenter(
|
||||
verificationRequest = verificationRequest,
|
||||
service = FakeSessionVerificationService(
|
||||
acknowledgeVerificationRequestLambda = { },
|
||||
resetLambda = { },
|
||||
@@ -353,6 +366,20 @@ private val anIncomingSessionVerificationRequest = VerificationRequest.Incoming.
|
||||
)
|
||||
)
|
||||
|
||||
private val anIncomingUserVerificationRequest = VerificationRequest.Incoming.User(
|
||||
details = SessionVerificationRequestDetails(
|
||||
senderProfile = MatrixUser(
|
||||
userId = A_USER_ID_2,
|
||||
displayName = "another user name",
|
||||
avatarUrl = null,
|
||||
),
|
||||
flowId = FlowId("flowId"),
|
||||
deviceId = A_DEVICE_ID,
|
||||
deviceDisplayName = "a device name",
|
||||
firstSeenTimestamp = A_TIMESTAMP,
|
||||
)
|
||||
)
|
||||
|
||||
internal fun TestScope.createPresenter(
|
||||
verificationRequest: VerificationRequest.Incoming = anIncomingSessionVerificationRequest,
|
||||
navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() },
|
||||
|
||||
-2
@@ -73,8 +73,6 @@ enum class AvatarSize(val dp: Dp) {
|
||||
|
||||
DmCreationConfirmation(64.dp),
|
||||
|
||||
UserVerification(52.dp),
|
||||
|
||||
OrganizationHeader(64.dp),
|
||||
SpaceHeader(64.dp),
|
||||
RoomPreviewHeader(64.dp),
|
||||
|
||||
+6
-13
@@ -58,19 +58,6 @@ fun AccountInfoCard(
|
||||
.border(1.dp, ElementTheme.colors.borderDisabled, RoundedCornerShape(8.dp))
|
||||
.padding(20.dp),
|
||||
) {
|
||||
when (state.mode) {
|
||||
AccountInfoCardMode.Simple -> Unit
|
||||
is AccountInfoCardMode.DeviceVerification -> Unit
|
||||
is AccountInfoCardMode.UserVerification -> {
|
||||
Text(
|
||||
// TODO
|
||||
text = "User requesting verification:",
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
AccountInfoCardUser(
|
||||
matrixUser = state.matrixUser,
|
||||
)
|
||||
@@ -82,6 +69,12 @@ fun AccountInfoCard(
|
||||
}
|
||||
is AccountInfoCardMode.UserVerification -> {
|
||||
AccountInfoSeparator()
|
||||
Text(
|
||||
text = state.mode.hint,
|
||||
style = ElementTheme.typography.fontBodySmRegular,
|
||||
color = ElementTheme.colors.textSecondary,
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
AccountInfoCardUser(
|
||||
matrixUser = state.mode.otherUser,
|
||||
)
|
||||
|
||||
+1
@@ -22,6 +22,7 @@ sealed interface AccountInfoCardMode {
|
||||
|
||||
data class UserVerification(
|
||||
val otherUser: MatrixUser,
|
||||
val hint: String,
|
||||
) : AccountInfoCardMode
|
||||
|
||||
data class DeviceVerification(
|
||||
|
||||
+1
@@ -20,6 +20,7 @@ open class AccountInfoCardStatePreviewParam : PreviewParameterProvider<AccountIn
|
||||
anAccountInfoCardState(
|
||||
mode = AccountInfoCardMode.UserVerification(
|
||||
otherUser = aMatrixUser(displayName = "Other User"),
|
||||
hint = "User request verification hint",
|
||||
)
|
||||
),
|
||||
anAccountInfoCardState(
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Reference in New Issue
Block a user