diff --git a/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt index 04522629cd..35946ea82d 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/verification/IncomingVerificationRequestObserverTest.kt @@ -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, ) } diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt index cb0b97a54f..71a28d0dbc 100644 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt +++ b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt @@ -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, ) diff --git a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt b/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt deleted file mode 100644 index c398d72ddd..0000000000 --- a/features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationUserProfileContent.kt +++ /dev/null @@ -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", - ) - ) -} diff --git a/features/verifysession/impl/src/main/res/values/localazy.xml b/features/verifysession/impl/src/main/res/values/localazy.xml index 4aa672cdeb..012437792e 100644 --- a/features/verifysession/impl/src/main/res/values/localazy.xml +++ b/features/verifysession/impl/src/main/res/values/localazy.xml @@ -44,6 +44,7 @@ "Open the app on another verified device" "For extra security, verify this user by comparing a set of emojis on your devices. Do this by using a trusted way to communicate." "Verify this user?" + "User requesting verification:" "For extra security, another user wants to verify your digital identity. You’ll be shown a set of emojis to compare." "You should see a popup on the other device. Start the verification from there now." "Start verification on the other device" diff --git a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt index dc65924d46..3a969938a1 100644 --- a/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt +++ b/features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt @@ -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() }, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt index bbf6d94d9b..ed1b0b2e6f 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarSize.kt @@ -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), diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt index 38de05999f..2af66d7356 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCard.kt @@ -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, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt index 7077fa7c0b..22697217fb 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardState.kt @@ -22,6 +22,7 @@ sealed interface AccountInfoCardMode { data class UserVerification( val otherUser: MatrixUser, + val hint: String, ) : AccountInfoCardMode data class DeviceVerification( diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardStatePreviewParam.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardStatePreviewParam.kt index d383648154..0dac3c46b7 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardStatePreviewParam.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/accountinfo/AccountInfoCardStatePreviewParam.kt @@ -20,6 +20,7 @@ open class AccountInfoCardStatePreviewParam : PreviewParameterProvider