Browse Source

Merge pull request #3733 from element-hq/feature/bma/incomingVerification

Incoming session verification
develop
Benoit Marty 11 hours ago committed by GitHub
parent
commit
666cedd66e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 27
      appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt
  2. 14
      features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt
  3. 4
      features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt
  4. 1
      features/verifysession/api/build.gradle.kts
  5. 33
      features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/IncomingVerificationEntryPoint.kt
  6. 2
      features/verifysession/impl/build.gradle.kts
  7. 95
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt
  8. 40
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/DefaultIncomingVerificationEntryPoint.kt
  9. 12
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNavigator.kt
  10. 47
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNode.kt
  11. 189
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt
  12. 38
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt
  13. 158
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateMachine.kt
  14. 46
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt
  15. 235
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt
  16. 16
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewEvents.kt
  17. 92
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/ui/SessionDetailsView.kt
  18. 2
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/DefaultVerifySessionEntryPoint.kt
  19. 2
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionNode.kt
  20. 81
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenter.kt
  21. 22
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionState.kt
  22. 35
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateMachine.kt
  23. 73
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateProvider.kt
  24. 212
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionView.kt
  25. 2
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewEvents.kt
  26. 33
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/Common.kt
  27. 27
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationBottomMenu.kt
  28. 94
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationContentVerifying.kt
  29. 25
      features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/util/StateMachineUtil.kt
  30. 292
      features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt
  31. 217
      features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt
  32. 227
      features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenterTest.kt
  33. 30
      features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewTest.kt
  34. 5
      libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt
  35. 34
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/TextWithLabelMolecule.kt
  36. 15
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/FlowId.kt
  37. 23
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt
  38. 34
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt
  39. 55
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt
  40. 22
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/SessionVerificationRequestDetails.kt
  41. 69
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt
  42. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png
  43. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en.png
  44. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png
  45. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png
  46. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png
  47. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png
  48. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png
  49. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png
  50. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png
  51. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png
  52. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png
  53. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png
  54. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png
  55. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png
  56. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png
  57. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png
  58. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png
  59. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png
  60. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_en.png
  61. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_en.png
  62. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_11_en.png
  63. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_12_en.png
  64. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_en.png
  65. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en.png
  66. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_en.png
  67. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_en.png
  68. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_en.png
  69. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en.png
  70. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_en.png
  71. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_en.png
  72. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_en.png
  73. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_0_en.png
  74. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_10_en.png
  75. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_11_en.png
  76. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_12_en.png
  77. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_1_en.png
  78. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en.png
  79. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_3_en.png
  80. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_4_en.png
  81. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_5_en.png
  82. 3
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en.png
  83. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_7_en.png
  84. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_8_en.png
  85. 0
      tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_9_en.png

27
appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt

@ -25,8 +25,10 @@ import com.bumble.appyx.core.node.Node @@ -25,8 +25,10 @@ import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import com.bumble.appyx.navmodel.backstack.BackStack
import com.bumble.appyx.navmodel.backstack.operation.pop
import com.bumble.appyx.navmodel.backstack.operation.push
import com.bumble.appyx.navmodel.backstack.operation.replace
import com.bumble.appyx.navmodel.backstack.operation.singleTop
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import im.vector.app.features.analytics.plan.JoinedRoom
@ -50,6 +52,7 @@ import io.element.android.features.roomlist.api.RoomListEntryPoint @@ -50,6 +52,7 @@ import io.element.android.features.roomlist.api.RoomListEntryPoint
import io.element.android.features.securebackup.api.SecureBackupEntryPoint
import io.element.android.features.share.api.ShareEntryPoint
import io.element.android.features.userprofile.api.UserProfileEntryPoint
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
@ -66,6 +69,8 @@ import io.element.android.libraries.matrix.api.core.UserId @@ -66,6 +69,8 @@ import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.sync.SyncState
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.preferences.api.store.EnableNativeSlidingSyncUseCase
import io.element.android.services.appnavstate.api.AppNavigationStateService
import kotlinx.coroutines.CoroutineScope
@ -99,6 +104,7 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -99,6 +104,7 @@ class LoggedInFlowNode @AssistedInject constructor(
private val matrixClient: MatrixClient,
private val sendingQueue: SendQueues,
private val logoutEntryPoint: LogoutEntryPoint,
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
private val enableNativeSlidingSyncUseCase: EnableNativeSlidingSyncUseCase,
snackbarDispatcher: SnackbarDispatcher,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
@ -123,6 +129,12 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -123,6 +129,12 @@ class LoggedInFlowNode @AssistedInject constructor(
matrixClient.roomMembershipObserver(),
)
private val verificationListener = object : SessionVerificationServiceListener {
override fun onIncomingSessionRequest(sessionVerificationRequestDetails: SessionVerificationRequestDetails) {
backstack.singleTop(NavTarget.IncomingVerificationRequest(sessionVerificationRequestDetails))
}
}
override fun onBuilt() {
super.onBuilt()
lifecycle.subscribe(
@ -131,6 +143,7 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -131,6 +143,7 @@ class LoggedInFlowNode @AssistedInject constructor(
// TODO We do not support Space yet, so directly navigate to main space
appNavigationStateService.onNavigateToSpace(id, MAIN_SPACE)
loggedInFlowProcessor.observeEvents(coroutineScope)
matrixClient.sessionVerificationService().setListener(verificationListener)
ftueService.state
.onEach { ftueState ->
@ -152,6 +165,7 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -152,6 +165,7 @@ class LoggedInFlowNode @AssistedInject constructor(
appNavigationStateService.onLeavingSpace(id)
appNavigationStateService.onLeavingSession(id)
loggedInFlowProcessor.stopObserving()
matrixClient.sessionVerificationService().setListener(null)
}
)
observeSyncStateAndNetworkStatus()
@ -232,6 +246,9 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -232,6 +246,9 @@ class LoggedInFlowNode @AssistedInject constructor(
@Parcelize
data object LogoutForNativeSlidingSyncMigrationNeeded : NavTarget
@Parcelize
data class IncomingVerificationRequest(val data: SessionVerificationRequestDetails) : NavTarget
}
override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node {
@ -432,6 +449,16 @@ class LoggedInFlowNode @AssistedInject constructor( @@ -432,6 +449,16 @@ class LoggedInFlowNode @AssistedInject constructor(
.callback(callback)
.build()
}
is NavTarget.IncomingVerificationRequest -> {
incomingVerificationEntryPoint.nodeBuilder(this, buildContext)
.params(IncomingVerificationEntryPoint.Params(navTarget.data))
.callback(object : IncomingVerificationEntryPoint.Callback {
override fun onDone() {
backstack.pop()
}
})
.build()
}
}
}

14
features/ftue/impl/src/test/kotlin/io/element/android/features/ftue/impl/DefaultFtueServiceTest.kt

@ -35,7 +35,7 @@ class DefaultFtueServiceTest { @@ -35,7 +35,7 @@ class DefaultFtueServiceTest {
@Test
fun `given any check being false and session verification state being loaded, FtueState is Incomplete`() = runTest {
val sessionVerificationService = FakeSessionVerificationService().apply {
givenVerifiedStatus(SessionVerifiedStatus.Unknown)
emitVerifiedStatus(SessionVerifiedStatus.Unknown)
}
val service = createDefaultFtueService(
sessionVerificationService = sessionVerificationService,
@ -46,7 +46,7 @@ class DefaultFtueServiceTest { @@ -46,7 +46,7 @@ class DefaultFtueServiceTest {
assertThat(awaitItem()).isEqualTo(FtueState.Unknown)
// Verification state is known, we should display the flow if any check is false
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
assertThat(awaitItem()).isEqualTo(FtueState.Incomplete)
}
}
@ -64,7 +64,7 @@ class DefaultFtueServiceTest { @@ -64,7 +64,7 @@ class DefaultFtueServiceTest {
lockScreenService = lockScreenService,
)
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
analyticsService.setDidAskUserConsent()
permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true)
@ -76,7 +76,7 @@ class DefaultFtueServiceTest { @@ -76,7 +76,7 @@ class DefaultFtueServiceTest {
@Test
fun `traverse flow`() = runTest {
val sessionVerificationService = FakeSessionVerificationService().apply {
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
}
val analyticsService = FakeAnalyticsService()
val permissionStateProvider = FakePermissionStateProvider(permissionGranted = false)
@ -91,7 +91,7 @@ class DefaultFtueServiceTest { @@ -91,7 +91,7 @@ class DefaultFtueServiceTest {
// Session verification
steps.add(service.getNextStep(steps.lastOrNull()))
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
// Notifications opt in
steps.add(service.getNextStep(steps.lastOrNull()))
@ -132,7 +132,7 @@ class DefaultFtueServiceTest { @@ -132,7 +132,7 @@ class DefaultFtueServiceTest {
)
// Skip first 3 steps
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
permissionStateProvider.setPermissionGranted()
lockScreenService.setIsPinSetup(true)
@ -155,7 +155,7 @@ class DefaultFtueServiceTest { @@ -155,7 +155,7 @@ class DefaultFtueServiceTest {
lockScreenService = lockScreenService,
)
sessionVerificationService.givenVerifiedStatus(SessionVerifiedStatus.Verified)
sessionVerificationService.emitVerifiedStatus(SessionVerifiedStatus.Verified)
lockScreenService.setIsPinSetup(true)
assertThat(service.getNextStep()).isEqualTo(FtueStep.AnalyticsOptIn)

4
features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTest.kt

@ -136,7 +136,7 @@ class RoomListPresenterTest { @@ -136,7 +136,7 @@ class RoomListPresenterTest {
}.test {
val initialState = awaitItem()
assertThat(initialState.showAvatarIndicator).isTrue()
sessionVerificationService.givenNeedsSessionVerification(false)
sessionVerificationService.emitNeedsSessionVerification(false)
encryptionService.emitBackupState(BackupState.ENABLED)
val finalState = awaitItem()
assertThat(finalState.showAvatarIndicator).isFalse()
@ -231,7 +231,7 @@ class RoomListPresenterTest { @@ -231,7 +231,7 @@ class RoomListPresenterTest {
roomListService = roomListService,
encryptionService = encryptionService,
sessionVerificationService = FakeSessionVerificationService().apply {
givenNeedsSessionVerification(false)
emitNeedsSessionVerification(false)
},
syncService = FakeSyncService(MutableStateFlow(SyncState.Running)),
)

1
features/verifysession/api/build.gradle.kts

@ -15,4 +15,5 @@ android { @@ -15,4 +15,5 @@ android {
dependencies {
implementation(projects.libraries.architecture)
implementation(projects.libraries.matrix.api)
}

33
features/verifysession/api/src/main/kotlin/io/element/android/features/verifysession/api/IncomingVerificationEntryPoint.kt

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.api
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import io.element.android.libraries.architecture.FeatureEntryPoint
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
interface IncomingVerificationEntryPoint : FeatureEntryPoint {
data class Params(
val sessionVerificationRequestDetails: SessionVerificationRequestDetails,
) : NodeInputs
fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder
interface NodeBuilder {
fun callback(callback: Callback): NodeBuilder
fun params(params: Params): NodeBuilder
fun build(): Node
}
interface Callback : Plugin {
fun onDone()
}
}

2
features/verifysession/impl/build.gradle.kts

@ -27,6 +27,7 @@ dependencies { @@ -27,6 +27,7 @@ dependencies {
implementation(projects.libraries.androidutils)
implementation(projects.libraries.core)
implementation(projects.libraries.architecture)
implementation(projects.libraries.dateformatter.api)
implementation(projects.libraries.matrix.api)
implementation(projects.libraries.matrixui)
implementation(projects.libraries.designsystem)
@ -43,6 +44,7 @@ dependencies { @@ -43,6 +44,7 @@ dependencies {
testImplementation(libs.test.truth)
testImplementation(libs.test.turbine)
testImplementation(projects.features.logout.test)
testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.libraries.matrix.test)
testImplementation(projects.libraries.preferences.test)
testImplementation(projects.tests.testutils)

95
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateProvider.kt

@ -1,95 +0,0 @@ @@ -1,95 +0,0 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfSessionState> {
override val values: Sequence<VerifySelfSessionState>
get() = sequenceOf(
aVerifySelfSessionState(displaySkipButton = true),
aVerifySelfSessionState(
verificationFlowStep = VerificationStep.AwaitingOtherDeviceResponse
),
aVerifySelfSessionState(
verificationFlowStep = VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized)
),
aVerifySelfSessionState(
verificationFlowStep = VerificationStep.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading())
),
aVerifySelfSessionState(
verificationFlowStep = VerificationStep.Canceled
),
aVerifySelfSessionState(
verificationFlowStep = VerificationStep.Ready
),
aVerifySelfSessionState(
verificationFlowStep = VerificationStep.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized)
),
aVerifySelfSessionState(
verificationFlowStep = VerificationStep.Initial(canEnterRecoveryKey = true)
),
aVerifySelfSessionState(
verificationFlowStep = VerificationStep.Initial(canEnterRecoveryKey = true, isLastDevice = true)
),
aVerifySelfSessionState(
verificationFlowStep = VerificationStep.Completed,
displaySkipButton = true,
),
aVerifySelfSessionState(
signOutAction = AsyncAction.Loading,
displaySkipButton = true,
),
aVerifySelfSessionState(
verificationFlowStep = VerificationStep.Loading
),
aVerifySelfSessionState(
verificationFlowStep = VerificationStep.Skipped
),
// Add other state here
)
}
internal fun aEmojisSessionVerificationData(
emojiList: List<VerificationEmoji> = aVerificationEmojiList(),
): SessionVerificationData {
return SessionVerificationData.Emojis(emojiList)
}
private fun aDecimalsSessionVerificationData(
decimals: List<Int> = listOf(123, 456, 789),
): SessionVerificationData {
return SessionVerificationData.Decimals(decimals)
}
internal fun aVerifySelfSessionState(
verificationFlowStep: VerificationStep = VerificationStep.Initial(canEnterRecoveryKey = false),
signOutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
displaySkipButton: Boolean = false,
eventSink: (VerifySelfSessionViewEvents) -> Unit = {},
) = VerifySelfSessionState(
verificationFlowStep = verificationFlowStep,
displaySkipButton = displaySkipButton,
eventSink = eventSink,
signOutAction = signOutAction,
)
private fun aVerificationEmojiList() = listOf(
VerificationEmoji(number = 27, emoji = "🍕", description = "Pizza"),
VerificationEmoji(number = 54, emoji = "🚀", description = "Rocket"),
VerificationEmoji(number = 54, emoji = "🚀", description = "Rocket"),
VerificationEmoji(number = 42, emoji = "📕", description = "Book"),
VerificationEmoji(number = 48, emoji = "🔨", description = "Hammer"),
VerificationEmoji(number = 48, emoji = "🔨", description = "Hammer"),
VerificationEmoji(number = 63, emoji = "📌", description = "Pin"),
)

40
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/DefaultIncomingVerificationEntryPoint.kt

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.incoming
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.squareup.anvil.annotations.ContributesBinding
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.di.AppScope
import javax.inject.Inject
@ContributesBinding(AppScope::class)
class DefaultIncomingVerificationEntryPoint @Inject constructor() : IncomingVerificationEntryPoint {
override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): IncomingVerificationEntryPoint.NodeBuilder {
val plugins = ArrayList<Plugin>()
return object : IncomingVerificationEntryPoint.NodeBuilder {
override fun callback(callback: IncomingVerificationEntryPoint.Callback): IncomingVerificationEntryPoint.NodeBuilder {
plugins += callback
return this
}
override fun params(params: IncomingVerificationEntryPoint.Params): IncomingVerificationEntryPoint.NodeBuilder {
plugins += params
return this
}
override fun build(): Node {
return parentNode.createNode<IncomingVerificationNode>(buildContext, plugins)
}
}
}
}

12
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNavigator.kt

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.incoming
fun interface IncomingVerificationNavigator {
fun onFinish()
}

47
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationNode.kt

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.incoming
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
import com.bumble.appyx.core.plugin.Plugin
import com.bumble.appyx.core.plugin.plugins
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import io.element.android.anvilannotations.ContributesNode
import io.element.android.features.verifysession.api.IncomingVerificationEntryPoint
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.SessionScope
@ContributesNode(SessionScope::class)
class IncomingVerificationNode @AssistedInject constructor(
@Assisted buildContext: BuildContext,
@Assisted plugins: List<Plugin>,
presenterFactory: IncomingVerificationPresenter.Factory,
) : Node(buildContext, plugins = plugins),
IncomingVerificationNavigator {
private val presenter = presenterFactory.create(
sessionVerificationRequestDetails = inputs<IncomingVerificationEntryPoint.Params>().sessionVerificationRequestDetails,
navigator = this,
)
override fun onFinish() {
plugins<IncomingVerificationEntryPoint.Callback>().forEach { it.onDone() }
}
@Composable
override fun View(modifier: Modifier) {
val state = presenter.present()
IncomingVerificationView(
state = state,
modifier = modifier,
)
}
}

189
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenter.kt

@ -0,0 +1,189 @@ @@ -0,0 +1,189 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.verifysession.impl.incoming
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import com.freeletics.flowredux.compose.rememberStateAndDispatch
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import io.element.android.features.verifysession.impl.incoming.IncomingVerificationStateMachine.Event as StateMachineEvent
import io.element.android.features.verifysession.impl.incoming.IncomingVerificationStateMachine.State as StateMachineState
class IncomingVerificationPresenter @AssistedInject constructor(
@Assisted private val sessionVerificationRequestDetails: SessionVerificationRequestDetails,
@Assisted private val navigator: IncomingVerificationNavigator,
private val sessionVerificationService: SessionVerificationService,
private val stateMachine: IncomingVerificationStateMachine,
private val dateFormatter: LastMessageTimestampFormatter,
) : Presenter<IncomingVerificationState> {
@AssistedFactory
interface Factory {
fun create(
sessionVerificationRequestDetails: SessionVerificationRequestDetails,
navigator: IncomingVerificationNavigator,
): IncomingVerificationPresenter
}
@Composable
override fun present(): IncomingVerificationState {
LaunchedEffect(Unit) {
// Force reset, just in case the service was left in a broken state
sessionVerificationService.reset(
cancelAnyPendingVerificationAttempt = false
)
// Acknowledge the request right now
sessionVerificationService.acknowledgeVerificationRequest(sessionVerificationRequestDetails)
}
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
val formattedSignInTime = remember {
dateFormatter.format(sessionVerificationRequestDetails.firstSeenTimestamp)
}
val step by remember {
derivedStateOf {
stateAndDispatch.state.value.toVerificationStep(
sessionVerificationRequestDetails = sessionVerificationRequestDetails,
formattedSignInTime = formattedSignInTime,
)
}
}
LaunchedEffect(stateAndDispatch.state.value) {
if ((stateAndDispatch.state.value as? IncomingVerificationStateMachine.State.Initial)?.isCancelled == true) {
// The verification was canceled before it was started, maybe because another session accepted it
navigator.onFinish()
}
}
// Start this after observing state machine
LaunchedEffect(Unit) {
observeVerificationService()
}
fun handleEvents(event: IncomingVerificationViewEvents) {
Timber.d("Verification user action: ${event::class.simpleName}")
when (event) {
IncomingVerificationViewEvents.StartVerification ->
stateAndDispatch.dispatchAction(StateMachineEvent.AcceptIncomingRequest)
IncomingVerificationViewEvents.IgnoreVerification ->
navigator.onFinish()
IncomingVerificationViewEvents.ConfirmVerification ->
stateAndDispatch.dispatchAction(StateMachineEvent.AcceptChallenge)
IncomingVerificationViewEvents.DeclineVerification ->
stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge)
IncomingVerificationViewEvents.GoBack -> {
when (val verificationStep = step) {
is Step.Initial -> if (verificationStep.isWaiting) {
stateAndDispatch.dispatchAction(StateMachineEvent.Cancel)
} else {
navigator.onFinish()
}
is Step.Verifying -> if (verificationStep.isWaiting) {
// What do we do in this case?
} else {
stateAndDispatch.dispatchAction(StateMachineEvent.DeclineChallenge)
}
Step.Canceled,
Step.Completed,
Step.Failure -> navigator.onFinish()
}
}
}
}
return IncomingVerificationState(
step = step,
eventSink = ::handleEvents,
)
}
private fun StateMachineState?.toVerificationStep(
sessionVerificationRequestDetails: SessionVerificationRequestDetails,
formattedSignInTime: String,
): Step =
when (val machineState = this) {
is StateMachineState.Initial,
IncomingVerificationStateMachine.State.AcceptingIncomingVerification,
IncomingVerificationStateMachine.State.RejectingIncomingVerification,
null -> {
Step.Initial(
deviceDisplayName = sessionVerificationRequestDetails.displayName ?: sessionVerificationRequestDetails.deviceId.value,
deviceId = sessionVerificationRequestDetails.deviceId,
formattedSignInTime = formattedSignInTime,
isWaiting = machineState == IncomingVerificationStateMachine.State.AcceptingIncomingVerification ||
machineState == IncomingVerificationStateMachine.State.RejectingIncomingVerification,
)
}
is IncomingVerificationStateMachine.State.ChallengeReceived ->
Step.Verifying(
data = machineState.data,
isWaiting = false,
)
IncomingVerificationStateMachine.State.Completed -> Step.Completed
IncomingVerificationStateMachine.State.Canceling,
IncomingVerificationStateMachine.State.Failure -> Step.Failure
is IncomingVerificationStateMachine.State.AcceptingChallenge ->
Step.Verifying(
data = machineState.data,
isWaiting = true,
)
is IncomingVerificationStateMachine.State.RejectingChallenge ->
Step.Verifying(
data = machineState.data,
isWaiting = true,
)
IncomingVerificationStateMachine.State.Canceled -> Step.Canceled
}
private fun CoroutineScope.observeVerificationService() {
sessionVerificationService.verificationFlowState
.onEach { Timber.d("Verification flow state: ${it::class.simpleName}") }
.onEach { verificationAttemptState ->
when (verificationAttemptState) {
VerificationFlowState.Initial,
VerificationFlowState.DidAcceptVerificationRequest,
VerificationFlowState.DidStartSasVerification -> Unit
is VerificationFlowState.DidReceiveVerificationData -> {
stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidReceiveChallenge(verificationAttemptState.data))
}
VerificationFlowState.DidFinish -> {
stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidAcceptChallenge)
}
VerificationFlowState.DidCancel -> {
// Can happen when:
// - the remote party cancel the verification (before it is started)
// - another session has accepted the incoming verification request
// - the user reject the challenge from this application (I think this is an error). In this case, the state
// machine will ignore this event and change state to Failure.
stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidCancel)
}
VerificationFlowState.DidFail -> {
stateMachine.dispatch(IncomingVerificationStateMachine.Event.DidFail)
}
}
}
.launchIn(this)
}
}

38
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationState.kt

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.incoming
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
@Immutable
data class IncomingVerificationState(
val step: Step,
val eventSink: (IncomingVerificationViewEvents) -> Unit,
) {
@Stable
sealed interface Step {
data class Initial(
val deviceDisplayName: String,
val deviceId: DeviceId,
val formattedSignInTime: String,
val isWaiting: Boolean,
) : Step
data class Verifying(
val data: SessionVerificationData,
val isWaiting: Boolean,
) : Step
data object Canceled : Step
data object Completed : Step
data object Failure : Step
}
}

158
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateMachine.kt

@ -0,0 +1,158 @@ @@ -0,0 +1,158 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.verifysession.impl.incoming
import com.freeletics.flowredux.dsl.FlowReduxStateMachine
import io.element.android.features.verifysession.impl.util.andLogStateChange
import io.element.android.features.verifysession.impl.util.logReceivedEvents
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import kotlinx.coroutines.ExperimentalCoroutinesApi
import javax.inject.Inject
import com.freeletics.flowredux.dsl.State as MachineState
class IncomingVerificationStateMachine @Inject constructor(
private val sessionVerificationService: SessionVerificationService,
) : FlowReduxStateMachine<IncomingVerificationStateMachine.State, IncomingVerificationStateMachine.Event>(
initialState = State.Initial(isCancelled = false)
) {
init {
spec {
inState<State.Initial> {
on { _: Event.AcceptIncomingRequest, state ->
state.override { State.AcceptingIncomingVerification.andLogStateChange() }
}
}
inState<State.AcceptingIncomingVerification> {
onEnterEffect {
sessionVerificationService.acceptVerificationRequest()
}
on { event: Event.DidReceiveChallenge, state ->
state.override { State.ChallengeReceived(event.data).andLogStateChange() }
}
}
inState<State.ChallengeReceived> {
on { _: Event.AcceptChallenge, state ->
state.override { State.AcceptingChallenge(state.snapshot.data).andLogStateChange() }
}
on { _: Event.DeclineChallenge, state ->
state.override { State.RejectingChallenge(state.snapshot.data).andLogStateChange() }
}
}
inState<State.AcceptingChallenge> {
onEnterEffect { _ ->
sessionVerificationService.approveVerification()
}
on { _: Event.DidAcceptChallenge, state ->
state.override { State.Completed.andLogStateChange() }
}
}
inState<State.RejectingChallenge> {
onEnterEffect { _ ->
sessionVerificationService.declineVerification()
}
}
inState<State.Canceling> {
onEnterEffect {
sessionVerificationService.cancelVerification()
}
}
inState {
logReceivedEvents()
on { _: Event.Cancel, state: MachineState<State> ->
when (state.snapshot) {
State.Completed, State.Canceled -> state.noChange()
else -> {
sessionVerificationService.cancelVerification()
state.override { State.Canceled.andLogStateChange() }
}
}
}
on { _: Event.DidCancel, state: MachineState<State> ->
when (state.snapshot) {
is State.RejectingChallenge -> {
state.override { State.Failure.andLogStateChange() }
}
is State.Initial -> state.mutate { State.Initial(isCancelled = true).andLogStateChange() }
State.AcceptingIncomingVerification,
State.RejectingIncomingVerification,
is State.ChallengeReceived,
is State.AcceptingChallenge,
State.Canceling -> state.override { State.Canceled.andLogStateChange() }
State.Canceled,
State.Completed,
State.Failure -> state.noChange()
}
}
on { _: Event.DidFail, state: MachineState<State> ->
state.override { State.Failure.andLogStateChange() }
}
}
}
}
sealed interface State {
/** The initial state, before verification started. */
data class Initial(val isCancelled: Boolean) : State
/** User is accepting the incoming verification. */
data object AcceptingIncomingVerification : State
/** User is rejecting the incoming verification. */
data object RejectingIncomingVerification : State
/** Verification accepted and emojis received. */
data class ChallengeReceived(val data: SessionVerificationData) : State
/** Accepting the verification challenge. */
data class AcceptingChallenge(val data: SessionVerificationData) : State
/** Rejecting the verification challenge. */
data class RejectingChallenge(val data: SessionVerificationData) : State
/** The verification is being canceled. */
data object Canceling : State
/** The verification has been canceled, remotely or locally. */
data object Canceled : State
/** Verification successful. */
data object Completed : State
/** Verification failure. */
data object Failure : State
}
sealed interface Event {
/** User accepts the incoming request. */
data object AcceptIncomingRequest : Event
/** Has received data. */
data class DidReceiveChallenge(val data: SessionVerificationData) : Event
/** Emojis match. */
data object AcceptChallenge : Event
/** Emojis do not match. */
data object DeclineChallenge : Event
/** Remote accepted challenge. */
data object DidAcceptChallenge : Event
/** Request cancellation. */
data object Cancel : Event
/** Verification cancelled. */
data object DidCancel : Event
/** Request failed. */
data object DidFail : Event
}
}

46
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationStateProvider.kt

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.incoming
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.verifysession.impl.incoming.IncomingVerificationState.Step
import io.element.android.features.verifysession.impl.ui.aDecimalsSessionVerificationData
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
import io.element.android.libraries.matrix.api.core.DeviceId
open class IncomingVerificationStateProvider : PreviewParameterProvider<IncomingVerificationState> {
override val values: Sequence<IncomingVerificationState>
get() = sequenceOf(
anIncomingVerificationState(),
anIncomingVerificationState(step = aStepInitial(isWaiting = true)),
anIncomingVerificationState(step = Step.Verifying(data = aEmojisSessionVerificationData(), isWaiting = false)),
anIncomingVerificationState(step = Step.Verifying(data = aEmojisSessionVerificationData(), isWaiting = true)),
anIncomingVerificationState(step = Step.Verifying(data = aDecimalsSessionVerificationData(), isWaiting = false)),
anIncomingVerificationState(step = Step.Completed),
anIncomingVerificationState(step = Step.Failure),
anIncomingVerificationState(step = Step.Canceled),
// Add other state here
)
}
internal fun aStepInitial(
isWaiting: Boolean = false,
) = Step.Initial(
deviceDisplayName = "Element X Android",
deviceId = DeviceId("ILAKNDNASDLK"),
formattedSignInTime = "12:34",
isWaiting = isWaiting,
)
internal fun anIncomingVerificationState(
step: Step = aStepInitial(),
eventSink: (IncomingVerificationViewEvents) -> Unit = {},
) = IncomingVerificationState(
step = step,
eventSink = eventSink,
)

235
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationView.kt

@ -0,0 +1,235 @@ @@ -0,0 +1,235 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.incoming
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
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.incoming.ui.SessionDetailsView
import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu
import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.PageTitle
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.Text
import io.element.android.libraries.designsystem.theme.components.TextButton
import io.element.android.libraries.designsystem.theme.components.TopAppBar
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.ui.strings.CommonStrings
/**
* [Figma](https://www.figma.com/design/pDlJZGBsri47FNTXMnEdXB/Compound-Android-Templates?node-id=819-7324).
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun IncomingVerificationView(
state: IncomingVerificationState,
modifier: Modifier = Modifier,
) {
val step = state.step
BackHandler {
state.eventSink(IncomingVerificationViewEvents.GoBack)
}
HeaderFooterPage(
modifier = modifier,
topBar = {
TopAppBar(
title = {},
)
},
header = {
IncomingVerificationHeader(step = step)
},
footer = {
IncomingVerificationBottomMenu(
state = state,
)
}
) {
IncomingVerificationContent(
step = step,
)
}
}
@Composable
private fun IncomingVerificationHeader(step: Step) {
val iconStyle = when (step) {
Step.Canceled,
is Step.Initial -> BigIcon.Style.Default(CompoundIcons.LockSolid())
is Step.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction())
Step.Completed -> BigIcon.Style.SuccessSolid
Step.Failure -> BigIcon.Style.AlertSolid
}
val titleTextId = when (step) {
Step.Canceled -> CommonStrings.common_verification_cancelled
is Step.Initial -> R.string.screen_session_verification_request_title
is Step.Verifying -> when (step.data) {
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title
}
Step.Completed -> R.string.screen_session_verification_request_success_title
Step.Failure -> R.string.screen_session_verification_request_failure_title
}
val subtitleTextId = when (step) {
Step.Canceled -> R.string.screen_session_verification_cancelled_subtitle
is Step.Initial -> R.string.screen_session_verification_request_subtitle
is Step.Verifying -> when (step.data) {
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle
}
Step.Completed -> R.string.screen_session_verification_request_success_subtitle
Step.Failure -> R.string.screen_session_verification_request_failure_subtitle
}
PageTitle(
iconStyle = iconStyle,
title = stringResource(id = titleTextId),
subtitle = stringResource(id = subtitleTextId)
)
}
@Composable
private fun IncomingVerificationContent(
step: Step,
) {
when (step) {
is Step.Initial -> ContentInitial(step)
is Step.Verifying -> VerificationContentVerifying(step.data)
else -> Unit
}
}
@Composable
private fun ContentInitial(
initialIncoming: Step.Initial,
) {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
SessionDetailsView(
deviceName = initialIncoming.deviceDisplayName,
deviceId = initialIncoming.deviceId,
signInFormattedTimestamp = initialIncoming.formattedSignInTime,
)
Text(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(bottom = 16.dp),
text = stringResource(R.string.screen_session_verification_request_footer),
style = ElementTheme.typography.fontBodyMdMedium,
textAlign = TextAlign.Center,
)
}
}
@Composable
private fun IncomingVerificationBottomMenu(
state: IncomingVerificationState,
) {
val step = state.step
val eventSink = state.eventSink
when (step) {
is Step.Initial -> {
if (step.isWaiting) {
VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_identity_waiting_on_other_device),
onClick = {},
enabled = false,
showProgress = true,
)
// Placeholder so the 1st button keeps its vertical position
Spacer(modifier = Modifier.height(40.dp))
}
} else {
VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_start),
onClick = { eventSink(IncomingVerificationViewEvents.StartVerification) },
)
TextButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_ignore),
onClick = { eventSink(IncomingVerificationViewEvents.IgnoreVerification) },
)
}
}
}
is Step.Verifying -> {
if (step.isWaiting) {
VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_session_verification_positive_button_verifying_ongoing),
onClick = {},
enabled = false,
showProgress = true,
)
// Placeholder so the 1st button keeps its vertical position
Spacer(modifier = Modifier.height(40.dp))
}
} else {
VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_session_verification_they_match),
onClick = { eventSink(IncomingVerificationViewEvents.ConfirmVerification) },
)
TextButton(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_session_verification_they_dont_match),
onClick = { eventSink(IncomingVerificationViewEvents.DeclineVerification) },
)
}
}
}
Step.Canceled,
is Step.Completed,
is Step.Failure -> {
VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_done),
onClick = { eventSink(IncomingVerificationViewEvents.GoBack) },
)
}
}
}
}
@PreviewsDayNight
@Composable
internal fun IncomingVerificationViewPreview(@PreviewParameter(IncomingVerificationStateProvider::class) state: IncomingVerificationState) = ElementPreview {
IncomingVerificationView(
state = state,
)
}

16
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewEvents.kt

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.incoming
sealed interface IncomingVerificationViewEvents {
data object GoBack : IncomingVerificationViewEvents
data object StartVerification : IncomingVerificationViewEvents
data object IgnoreVerification : IncomingVerificationViewEvents
data object ConfirmVerification : IncomingVerificationViewEvents
data object DeclineVerification : IncomingVerificationViewEvents
}

92
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/incoming/ui/SessionDetailsView.kt

@ -0,0 +1,92 @@ @@ -0,0 +1,92 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.incoming.ui
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.verifysession.impl.R
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom
import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize
import io.element.android.libraries.designsystem.atomic.molecules.TextWithLabelMolecule
import io.element.android.libraries.designsystem.icons.CompoundDrawables
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.ui.strings.CommonStrings
@Composable
fun SessionDetailsView(
deviceName: String,
deviceId: DeviceId,
signInFormattedTimestamp: String,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.border(
width = 1.dp,
color = ElementTheme.colors.borderDisabled,
shape = RoundedCornerShape(8.dp)
)
.padding(24.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
RoundedIconAtom(
modifier = Modifier,
size = RoundedIconAtomSize.Big,
resourceId = CompoundDrawables.ic_compound_devices
)
Text(
text = deviceName,
style = ElementTheme.typography.fontBodyMdMedium,
color = ElementTheme.colors.textPrimary,
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
TextWithLabelMolecule(
label = stringResource(R.string.screen_session_verification_request_details_timestamp),
text = signInFormattedTimestamp,
modifier = Modifier.weight(2f),
)
TextWithLabelMolecule(
label = stringResource(CommonStrings.common_device_id),
text = deviceId.value,
modifier = Modifier.weight(5f),
)
}
}
}
@PreviewsDayNight
@Composable
internal fun SessionDetailsViewPreview() = ElementPreview {
SessionDetailsView(
deviceName = "Element X Android",
deviceId = DeviceId("ILAKNDNASDLK"),
signInFormattedTimestamp = "12:34",
)
}

2
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/DefaultVerifySessionEntryPoint.kt → features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/DefaultVerifySessionEntryPoint.kt

@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl
package io.element.android.features.verifysession.impl.outgoing
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node

2
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionNode.kt → features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionNode.kt

@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl
package io.element.android.features.verifysession.impl.outgoing
import android.app.Activity
import androidx.compose.runtime.Composable

81
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenter.kt → features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenter.kt

@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.verifysession.impl
package io.element.android.features.verifysession.impl.outgoing
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@ -39,8 +39,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -39,8 +39,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.Event as StateMachineEvent
import io.element.android.features.verifysession.impl.VerifySelfSessionStateMachine.State as StateMachineState
import timber.log.Timber
import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionStateMachine.Event as StateMachineEvent
import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionStateMachine.State as StateMachineState
class VerifySelfSessionPresenter @AssistedInject constructor(
@Assisted private val showDeviceVerifiedScreen: Boolean,
@ -61,7 +62,7 @@ class VerifySelfSessionPresenter @AssistedInject constructor( @@ -61,7 +62,7 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
val coroutineScope = rememberCoroutineScope()
LaunchedEffect(Unit) {
// Force reset, just in case the service was left in a broken state
sessionVerificationService.reset()
sessionVerificationService.reset(true)
}
val recoveryState by encryptionService.recoveryStateStateFlow.collectAsState()
val stateAndDispatch = stateMachine.rememberStateAndDispatch()
@ -70,13 +71,13 @@ class VerifySelfSessionPresenter @AssistedInject constructor( @@ -70,13 +71,13 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
val signOutAction = remember {
mutableStateOf<AsyncAction<String?>>(AsyncAction.Uninitialized)
}
val verificationFlowStep by remember {
val step by remember {
derivedStateOf {
if (skipVerification) {
VerifySelfSessionState.VerificationStep.Skipped
VerifySelfSessionState.Step.Skipped
} else {
when (sessionVerifiedStatus) {
SessionVerifiedStatus.Unknown -> VerifySelfSessionState.VerificationStep.Loading
SessionVerifiedStatus.Unknown -> VerifySelfSessionState.Step.Loading
SessionVerifiedStatus.NotVerified -> {
stateAndDispatch.state.value.toVerificationStep(
canEnterRecoveryKey = recoveryState == RecoveryState.INCOMPLETE
@ -85,10 +86,10 @@ class VerifySelfSessionPresenter @AssistedInject constructor( @@ -85,10 +86,10 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
SessionVerifiedStatus.Verified -> {
if (stateAndDispatch.state.value != StateMachineState.Initial || showDeviceVerifiedScreen) {
// The user has verified the session, we need to show the success screen
VerifySelfSessionState.VerificationStep.Completed
VerifySelfSessionState.Step.Completed
} else {
// Automatic verification, which can happen on freshly created account, in this case, skip the screen
VerifySelfSessionState.VerificationStep.Skipped
VerifySelfSessionState.Step.Skipped
}
}
}
@ -101,6 +102,7 @@ class VerifySelfSessionPresenter @AssistedInject constructor( @@ -101,6 +102,7 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
}
fun handleEvents(event: VerifySelfSessionViewEvents) {
Timber.d("Verification user action: ${event::class.simpleName}")
when (event) {
VerifySelfSessionViewEvents.RequestVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.RequestVerification)
VerifySelfSessionViewEvents.StartSasVerification -> stateAndDispatch.dispatchAction(StateMachineEvent.StartSasVerification)
@ -115,7 +117,7 @@ class VerifySelfSessionPresenter @AssistedInject constructor( @@ -115,7 +117,7 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
}
}
return VerifySelfSessionState(
verificationFlowStep = verificationFlowStep,
step = step,
signOutAction = signOutAction.value,
displaySkipButton = buildMeta.isDebuggable,
eventSink = ::handleEvents,
@ -124,10 +126,10 @@ class VerifySelfSessionPresenter @AssistedInject constructor( @@ -124,10 +126,10 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
private fun StateMachineState?.toVerificationStep(
canEnterRecoveryKey: Boolean
): VerifySelfSessionState.VerificationStep =
): VerifySelfSessionState.Step =
when (val machineState = this) {
StateMachineState.Initial, null -> {
VerifySelfSessionState.VerificationStep.Initial(
VerifySelfSessionState.Step.Initial(
canEnterRecoveryKey = canEnterRecoveryKey,
isLastDevice = encryptionService.isLastDevice.value
)
@ -136,15 +138,15 @@ class VerifySelfSessionPresenter @AssistedInject constructor( @@ -136,15 +138,15 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
StateMachineState.StartingSasVerification,
StateMachineState.SasVerificationStarted,
StateMachineState.Canceling -> {
VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse
VerifySelfSessionState.Step.AwaitingOtherDeviceResponse
}
StateMachineState.VerificationRequestAccepted -> {
VerifySelfSessionState.VerificationStep.Ready
VerifySelfSessionState.Step.Ready
}
StateMachineState.Canceled -> {
VerifySelfSessionState.VerificationStep.Canceled
VerifySelfSessionState.Step.Canceled
}
is StateMachineState.Verifying -> {
@ -152,38 +154,41 @@ class VerifySelfSessionPresenter @AssistedInject constructor( @@ -152,38 +154,41 @@ class VerifySelfSessionPresenter @AssistedInject constructor(
is StateMachineState.Verifying.Replying -> AsyncData.Loading()
else -> AsyncData.Uninitialized
}
VerifySelfSessionState.VerificationStep.Verifying(machineState.data, async)
VerifySelfSessionState.Step.Verifying(machineState.data, async)
}
StateMachineState.Completed -> {
VerifySelfSessionState.VerificationStep.Completed
VerifySelfSessionState.Step.Completed
}
}
private fun CoroutineScope.observeVerificationService() {
sessionVerificationService.verificationFlowState.onEach { verificationAttemptState ->
when (verificationAttemptState) {
VerificationFlowState.Initial -> stateMachine.dispatch(VerifySelfSessionStateMachine.Event.Reset)
VerificationFlowState.AcceptedVerificationRequest -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptVerificationRequest)
}
VerificationFlowState.StartedSasVerification -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidStartSasVerification)
}
is VerificationFlowState.ReceivedVerificationData -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidReceiveChallenge(verificationAttemptState.data))
}
VerificationFlowState.Finished -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptChallenge)
}
VerificationFlowState.Canceled -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidCancel)
}
VerificationFlowState.Failed -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidFail)
sessionVerificationService.verificationFlowState
.onEach { Timber.d("Verification flow state: ${it::class.simpleName}") }
.onEach { verificationAttemptState ->
when (verificationAttemptState) {
VerificationFlowState.Initial -> stateMachine.dispatch(VerifySelfSessionStateMachine.Event.Reset)
VerificationFlowState.DidAcceptVerificationRequest -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptVerificationRequest)
}
VerificationFlowState.DidStartSasVerification -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidStartSasVerification)
}
is VerificationFlowState.DidReceiveVerificationData -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidReceiveChallenge(verificationAttemptState.data))
}
VerificationFlowState.DidFinish -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidAcceptChallenge)
}
VerificationFlowState.DidCancel -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidCancel)
}
VerificationFlowState.DidFail -> {
stateMachine.dispatch(VerifySelfSessionStateMachine.Event.DidFail)
}
}
}
}.launchIn(this)
.launchIn(this)
}
private fun CoroutineScope.signOut(signOutAction: MutableState<AsyncAction<String?>>) = launch {

22
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionState.kt → features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionState.kt

@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl
package io.element.android.features.verifysession.impl.outgoing
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
@ -15,22 +15,22 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD @@ -15,22 +15,22 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationD
@Immutable
data class VerifySelfSessionState(
val verificationFlowStep: VerificationStep,
val step: Step,
val signOutAction: AsyncAction<String?>,
val displaySkipButton: Boolean,
val eventSink: (VerifySelfSessionViewEvents) -> Unit,
) {
@Stable
sealed interface VerificationStep {
data object Loading : VerificationStep
sealed interface Step {
data object Loading : Step
// FIXME canEnterRecoveryKey value is never read.
data class Initial(val canEnterRecoveryKey: Boolean, val isLastDevice: Boolean = false) : VerificationStep
data object Canceled : VerificationStep
data object AwaitingOtherDeviceResponse : VerificationStep
data object Ready : VerificationStep
data class Verifying(val data: SessionVerificationData, val state: AsyncData<Unit>) : VerificationStep
data object Completed : VerificationStep
data object Skipped : VerificationStep
data class Initial(val canEnterRecoveryKey: Boolean, val isLastDevice: Boolean = false) : Step
data object Canceled : Step
data object AwaitingOtherDeviceResponse : Step
data object Ready : Step
data class Verifying(val data: SessionVerificationData, val state: AsyncData<Unit>) : Step
data object Completed : Step
data object Skipped : Step
}
}

35
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionStateMachine.kt → features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateMachine.kt

@ -8,9 +8,11 @@ @@ -8,9 +8,11 @@
@file:Suppress("WildcardImport")
@file:OptIn(ExperimentalCoroutinesApi::class)
package io.element.android.features.verifysession.impl
package io.element.android.features.verifysession.impl.outgoing
import com.freeletics.flowredux.dsl.FlowReduxStateMachine
import io.element.android.features.verifysession.impl.util.andLogStateChange
import io.element.android.features.verifysession.impl.util.logReceivedEvents
import io.element.android.libraries.core.bool.orFalse
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.encryption.EncryptionService
@ -37,10 +39,10 @@ class VerifySelfSessionStateMachine @Inject constructor( @@ -37,10 +39,10 @@ class VerifySelfSessionStateMachine @Inject constructor(
spec {
inState<State.Initial> {
on { _: Event.RequestVerification, state ->
state.override { State.RequestingVerification }
state.override { State.RequestingVerification.andLogStateChange() }
}
on { _: Event.StartSasVerification, state ->
state.override { State.StartingSasVerification }
state.override { State.StartingSasVerification.andLogStateChange() }
}
}
inState<State.RequestingVerification> {
@ -48,7 +50,7 @@ class VerifySelfSessionStateMachine @Inject constructor( @@ -48,7 +50,7 @@ class VerifySelfSessionStateMachine @Inject constructor(
sessionVerificationService.requestVerification()
}
on { _: Event.DidAcceptVerificationRequest, state ->
state.override { State.VerificationRequestAccepted }
state.override { State.VerificationRequestAccepted.andLogStateChange() }
}
}
inState<State.StartingSasVerification> {
@ -58,28 +60,28 @@ class VerifySelfSessionStateMachine @Inject constructor( @@ -58,28 +60,28 @@ class VerifySelfSessionStateMachine @Inject constructor(
}
inState<State.VerificationRequestAccepted> {
on { _: Event.StartSasVerification, state ->
state.override { State.StartingSasVerification }
state.override { State.StartingSasVerification.andLogStateChange() }
}
}
inState<State.Canceled> {
on { _: Event.RequestVerification, state ->
state.override { State.RequestingVerification }
state.override { State.RequestingVerification.andLogStateChange() }
}
on { _: Event.Reset, state ->
state.override { State.Initial }
state.override { State.Initial.andLogStateChange() }
}
}
inState<State.SasVerificationStarted> {
on { event: Event.DidReceiveChallenge, state ->
state.override { State.Verifying.ChallengeReceived(event.data) }
state.override { State.Verifying.ChallengeReceived(event.data).andLogStateChange() }
}
}
inState<State.Verifying.ChallengeReceived> {
on { _: Event.AcceptChallenge, state ->
state.override { State.Verifying.Replying(state.snapshot.data, accept = true) }
state.override { State.Verifying.Replying(state.snapshot.data, accept = true).andLogStateChange() }
}
on { _: Event.DeclineChallenge, state ->
state.override { State.Verifying.Replying(state.snapshot.data, accept = false) }
state.override { State.Verifying.Replying(state.snapshot.data, accept = false).andLogStateChange() }
}
}
inState<State.Verifying.Replying> {
@ -100,7 +102,7 @@ class VerifySelfSessionStateMachine @Inject constructor( @@ -100,7 +102,7 @@ class VerifySelfSessionStateMachine @Inject constructor(
.first()
}
}
state.override { State.Completed }
state.override { State.Completed.andLogStateChange() }
}
}
inState<State.Canceling> {
@ -110,8 +112,9 @@ class VerifySelfSessionStateMachine @Inject constructor( @@ -110,8 +112,9 @@ class VerifySelfSessionStateMachine @Inject constructor(
}
}
inState {
logReceivedEvents()
on { _: Event.DidStartSasVerification, state: MachineState<State> ->
state.override { State.SasVerificationStarted }
state.override { State.SasVerificationStarted.andLogStateChange() }
}
on { _: Event.Cancel, state: MachineState<State> ->
when (state.snapshot) {
@ -120,17 +123,17 @@ class VerifySelfSessionStateMachine @Inject constructor( @@ -120,17 +123,17 @@ class VerifySelfSessionStateMachine @Inject constructor(
// `Canceling` state to `Canceled` automatically anymore
else -> {
sessionVerificationService.cancelVerification()
state.override { State.Canceled }
state.override { State.Canceled.andLogStateChange() }
}
}
}
on { _: Event.DidCancel, state: MachineState<State> ->
state.override { State.Canceled }
state.override { State.Canceled.andLogStateChange() }
}
on { _: Event.DidFail, state: MachineState<State> ->
when (state.snapshot) {
is State.RequestingVerification -> state.override { State.Initial }
else -> state.override { State.Canceled }
is State.RequestingVerification -> state.override { State.Initial.andLogStateChange() }
else -> state.override { State.Canceled.andLogStateChange() }
}
}
}

73
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionStateProvider.kt

@ -0,0 +1,73 @@ @@ -0,0 +1,73 @@
/*
* Copyright 2023, 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.outgoing
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step
import io.element.android.features.verifysession.impl.ui.aDecimalsSessionVerificationData
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
open class VerifySelfSessionStateProvider : PreviewParameterProvider<VerifySelfSessionState> {
override val values: Sequence<VerifySelfSessionState>
get() = sequenceOf(
aVerifySelfSessionState(displaySkipButton = true),
aVerifySelfSessionState(
step = Step.AwaitingOtherDeviceResponse
),
aVerifySelfSessionState(
step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Uninitialized)
),
aVerifySelfSessionState(
step = Step.Verifying(aEmojisSessionVerificationData(), AsyncData.Loading())
),
aVerifySelfSessionState(
step = Step.Canceled
),
aVerifySelfSessionState(
step = Step.Ready
),
aVerifySelfSessionState(
step = Step.Verifying(aDecimalsSessionVerificationData(), AsyncData.Uninitialized)
),
aVerifySelfSessionState(
step = Step.Initial(canEnterRecoveryKey = true)
),
aVerifySelfSessionState(
step = Step.Initial(canEnterRecoveryKey = true, isLastDevice = true)
),
aVerifySelfSessionState(
step = Step.Completed,
displaySkipButton = true,
),
aVerifySelfSessionState(
signOutAction = AsyncAction.Loading,
displaySkipButton = true,
),
aVerifySelfSessionState(
step = Step.Loading
),
aVerifySelfSessionState(
step = Step.Skipped
),
// Add other state here
)
}
internal fun aVerifySelfSessionState(
step: Step = Step.Initial(canEnterRecoveryKey = false),
signOutAction: AsyncAction<String?> = AsyncAction.Uninitialized,
displaySkipButton: Boolean = false,
eventSink: (VerifySelfSessionViewEvents) -> Unit = {},
) = VerifySelfSessionState(
step = step,
displaySkipButton = displaySkipButton,
eventSink = eventSink,
signOutAction = signOutAction,
)

212
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionView.kt → features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionView.kt

@ -5,25 +5,19 @@ @@ -5,25 +5,19 @@
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl
package io.element.android.features.verifysession.impl.outgoing
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -31,18 +25,17 @@ import androidx.compose.runtime.rememberUpdatedState @@ -31,18 +25,17 @@ import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.features.verifysession.impl.emoji.toEmojiResource
import io.element.android.features.verifysession.impl.R
import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step
import io.element.android.features.verifysession.impl.ui.VerificationBottomMenu
import io.element.android.features.verifysession.impl.ui.VerificationContentVerifying
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.PageTitle
@ -56,9 +49,7 @@ import io.element.android.libraries.designsystem.theme.components.Text @@ -56,9 +49,7 @@ 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
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep as FlowStep
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@ -71,12 +62,13 @@ fun VerifySelfSessionView( @@ -71,12 +62,13 @@ fun VerifySelfSessionView(
onSuccessLogout: (String?) -> Unit,
modifier: Modifier = Modifier,
) {
val step = state.step
fun cancelOrResetFlow() {
when (state.verificationFlowStep) {
is FlowStep.Canceled -> state.eventSink(VerifySelfSessionViewEvents.Reset)
is FlowStep.AwaitingOtherDeviceResponse, FlowStep.Ready -> state.eventSink(VerifySelfSessionViewEvents.Cancel)
is FlowStep.Verifying -> {
if (!state.verificationFlowStep.state.isLoading()) {
when (step) {
is Step.Canceled -> state.eventSink(VerifySelfSessionViewEvents.Reset)
is Step.AwaitingOtherDeviceResponse, Step.Ready -> state.eventSink(VerifySelfSessionViewEvents.Cancel)
is Step.Verifying -> {
if (!step.state.isLoading()) {
state.eventSink(VerifySelfSessionViewEvents.DeclineVerification)
}
}
@ -85,18 +77,17 @@ fun VerifySelfSessionView( @@ -85,18 +77,17 @@ fun VerifySelfSessionView(
}
val latestOnFinish by rememberUpdatedState(newValue = onFinish)
LaunchedEffect(state.verificationFlowStep, latestOnFinish) {
if (state.verificationFlowStep is FlowStep.Skipped) {
LaunchedEffect(step, latestOnFinish) {
if (step is Step.Skipped) {
latestOnFinish()
}
}
BackHandler {
cancelOrResetFlow()
}
val verificationFlowStep = state.verificationFlowStep
if (state.verificationFlowStep is FlowStep.Loading ||
state.verificationFlowStep is FlowStep.Skipped) {
if (step is Step.Loading ||
step is Step.Skipped) {
// Just display a loader in this case, to avoid UI glitch.
Box(
modifier = Modifier.fillMaxSize(),
@ -111,7 +102,7 @@ fun VerifySelfSessionView( @@ -111,7 +102,7 @@ fun VerifySelfSessionView(
TopAppBar(
title = {},
actions = {
if (state.verificationFlowStep !is FlowStep.Completed &&
if (step !is Step.Completed &&
state.displaySkipButton &&
LocalInspectionMode.current.not()) {
TextButton(
@ -119,7 +110,7 @@ fun VerifySelfSessionView( @@ -119,7 +110,7 @@ fun VerifySelfSessionView(
onClick = { state.eventSink(VerifySelfSessionViewEvents.SkipVerification) }
)
}
if (state.verificationFlowStep is FlowStep.Initial) {
if (step is Step.Initial) {
TextButton(
text = stringResource(CommonStrings.action_signout),
onClick = { state.eventSink(VerifySelfSessionViewEvents.SignOut) }
@ -129,10 +120,10 @@ fun VerifySelfSessionView( @@ -129,10 +120,10 @@ fun VerifySelfSessionView(
)
},
header = {
HeaderContent(verificationFlowStep = verificationFlowStep)
VerifySelfSessionHeader(step = step)
},
footer = {
BottomMenu(
VerifySelfSessionBottomMenu(
screenState = state,
onCancelClick = ::cancelOrResetFlow,
onEnterRecoveryKey = onEnterRecoveryKey,
@ -141,8 +132,8 @@ fun VerifySelfSessionView( @@ -141,8 +132,8 @@ fun VerifySelfSessionView(
)
}
) {
Content(
flowState = verificationFlowStep,
VerifySelfSessionContent(
flowState = step,
onLearnMoreClick = onLearnMoreClick,
)
}
@ -165,38 +156,38 @@ fun VerifySelfSessionView( @@ -165,38 +156,38 @@ fun VerifySelfSessionView(
}
@Composable
private fun HeaderContent(verificationFlowStep: FlowStep) {
val iconStyle = when (verificationFlowStep) {
VerifySelfSessionState.VerificationStep.Loading -> error("Should not happen")
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> BigIcon.Style.Default(CompoundIcons.LockSolid())
FlowStep.Canceled -> BigIcon.Style.AlertSolid
FlowStep.Ready, is FlowStep.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction())
FlowStep.Completed -> BigIcon.Style.SuccessSolid
is FlowStep.Skipped -> return
private fun VerifySelfSessionHeader(step: Step) {
val iconStyle = when (step) {
Step.Loading -> error("Should not happen")
is Step.Initial, Step.AwaitingOtherDeviceResponse -> BigIcon.Style.Default(CompoundIcons.LockSolid())
Step.Canceled -> BigIcon.Style.AlertSolid
Step.Ready, is Step.Verifying -> BigIcon.Style.Default(CompoundIcons.Reaction())
Step.Completed -> BigIcon.Style.SuccessSolid
is Step.Skipped -> return
}
val titleTextId = when (verificationFlowStep) {
VerifySelfSessionState.VerificationStep.Loading -> error("Should not happen")
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_title
FlowStep.Canceled -> CommonStrings.common_verification_cancelled
FlowStep.Ready -> R.string.screen_session_verification_compare_emojis_title
FlowStep.Completed -> R.string.screen_identity_confirmed_title
is FlowStep.Verifying -> when (verificationFlowStep.data) {
val titleTextId = when (step) {
Step.Loading -> error("Should not happen")
is Step.Initial, Step.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_title
Step.Canceled -> CommonStrings.common_verification_cancelled
Step.Ready -> R.string.screen_session_verification_compare_emojis_title
Step.Completed -> R.string.screen_identity_confirmed_title
is Step.Verifying -> when (step.data) {
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_title
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_title
}
is FlowStep.Skipped -> return
is Step.Skipped -> return
}
val subtitleTextId = when (verificationFlowStep) {
VerifySelfSessionState.VerificationStep.Loading -> error("Should not happen")
is FlowStep.Initial, FlowStep.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_subtitle
FlowStep.Canceled -> R.string.screen_session_verification_cancelled_subtitle
FlowStep.Ready -> R.string.screen_session_verification_ready_subtitle
FlowStep.Completed -> R.string.screen_identity_confirmed_subtitle
is FlowStep.Verifying -> when (verificationFlowStep.data) {
val subtitleTextId = when (step) {
Step.Loading -> error("Should not happen")
is Step.Initial, Step.AwaitingOtherDeviceResponse -> R.string.screen_identity_confirmation_subtitle
Step.Canceled -> R.string.screen_session_verification_cancelled_subtitle
Step.Ready -> R.string.screen_session_verification_ready_subtitle
Step.Completed -> R.string.screen_identity_confirmed_subtitle
is Step.Verifying -> when (step.data) {
is SessionVerificationData.Decimals -> R.string.screen_session_verification_compare_numbers_subtitle
is SessionVerificationData.Emojis -> R.string.screen_session_verification_compare_emojis_subtitle
}
is FlowStep.Skipped -> return
is Step.Skipped -> return
}
PageTitle(
@ -207,16 +198,16 @@ private fun HeaderContent(verificationFlowStep: FlowStep) { @@ -207,16 +198,16 @@ private fun HeaderContent(verificationFlowStep: FlowStep) {
}
@Composable
private fun Content(
flowState: FlowStep,
private fun VerifySelfSessionContent(
flowState: Step,
onLearnMoreClick: () -> Unit,
) {
when (flowState) {
is VerifySelfSessionState.VerificationStep.Initial -> {
is Step.Initial -> {
ContentInitial(onLearnMoreClick)
}
is FlowStep.Verifying -> {
ContentVerifying(flowState)
is Step.Verifying -> {
VerificationContentVerifying(flowState.data)
}
else -> Unit
}
@ -241,79 +232,22 @@ private fun ContentInitial( @@ -241,79 +232,22 @@ private fun ContentInitial(
}
@Composable
private fun ContentVerifying(verificationFlowStep: FlowStep.Verifying) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
when (verificationFlowStep.data) {
is SessionVerificationData.Decimals -> {
val text = verificationFlowStep.data.decimals.joinToString(separator = " - ") { it.toString() }
Text(
modifier = Modifier.fillMaxWidth(),
text = text,
style = ElementTheme.typography.fontHeadingLgBold,
color = MaterialTheme.colorScheme.primary,
textAlign = TextAlign.Center,
)
}
is SessionVerificationData.Emojis -> {
// We want each row to have up to 4 emojis
val rows = verificationFlowStep.data.emojis.chunked(4)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(40.dp),
) {
rows.forEach { emojis ->
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
for (emoji in emojis) {
EmojiItemView(emoji = emoji, modifier = Modifier.widthIn(max = 60.dp))
}
}
}
}
}
}
}
}
@Composable
private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifier) {
val emojiResource = emoji.number.toEmojiResource()
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) {
Image(
modifier = Modifier.size(48.dp),
painter = painterResource(id = emojiResource.drawableRes),
contentDescription = null,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = emojiResource.nameRes),
style = ElementTheme.typography.fontBodyMdRegular,
color = MaterialTheme.colorScheme.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
@Composable
private fun BottomMenu(
private fun VerifySelfSessionBottomMenu(
screenState: VerifySelfSessionState,
onEnterRecoveryKey: () -> Unit,
onResetKey: () -> Unit,
onCancelClick: () -> Unit,
onContinueClick: () -> Unit,
) {
val verificationViewState = screenState.verificationFlowStep
val verificationViewState = screenState.step
val eventSink = screenState.eventSink
val isVerifying = (verificationViewState as? FlowStep.Verifying)?.state is AsyncData.Loading<Unit>
val isVerifying = (verificationViewState as? Step.Verifying)?.state is AsyncData.Loading<Unit>
when (verificationViewState) {
VerifySelfSessionState.VerificationStep.Loading -> error("Should not happen")
is FlowStep.Initial -> {
BottomMenu {
Step.Loading -> error("Should not happen")
is Step.Initial -> {
VerificationBottomMenu {
if (verificationViewState.isLastDevice) {
Button(
modifier = Modifier.fillMaxWidth(),
@ -340,8 +274,8 @@ private fun BottomMenu( @@ -340,8 +274,8 @@ private fun BottomMenu(
)
}
}
is FlowStep.Canceled -> {
BottomMenu {
is Step.Canceled -> {
VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_session_verification_positive_button_canceled),
@ -354,8 +288,8 @@ private fun BottomMenu( @@ -354,8 +288,8 @@ private fun BottomMenu(
)
}
}
is FlowStep.Ready -> {
BottomMenu {
is Step.Ready -> {
VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_start),
@ -368,8 +302,8 @@ private fun BottomMenu( @@ -368,8 +302,8 @@ private fun BottomMenu(
)
}
}
is FlowStep.AwaitingOtherDeviceResponse -> {
BottomMenu {
is Step.AwaitingOtherDeviceResponse -> {
VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(R.string.screen_identity_waiting_on_other_device),
@ -380,13 +314,13 @@ private fun BottomMenu( @@ -380,13 +314,13 @@ private fun BottomMenu(
Spacer(modifier = Modifier.height(40.dp))
}
}
is FlowStep.Verifying -> {
is Step.Verifying -> {
val positiveButtonTitle = if (isVerifying) {
stringResource(R.string.screen_session_verification_positive_button_verifying_ongoing)
} else {
stringResource(R.string.screen_session_verification_they_match)
}
BottomMenu {
VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = positiveButtonTitle,
@ -404,8 +338,8 @@ private fun BottomMenu( @@ -404,8 +338,8 @@ private fun BottomMenu(
)
}
}
is FlowStep.Completed -> {
BottomMenu {
is Step.Completed -> {
VerificationBottomMenu {
Button(
modifier = Modifier.fillMaxWidth(),
text = stringResource(CommonStrings.action_continue),
@ -415,19 +349,7 @@ private fun BottomMenu( @@ -415,19 +349,7 @@ private fun BottomMenu(
Spacer(modifier = Modifier.height(48.dp))
}
}
is FlowStep.Skipped -> return
}
}
@Composable
private fun BottomMenu(
modifier: Modifier = Modifier,
buttons: @Composable ColumnScope.() -> Unit,
) {
ButtonColumnMolecule(
modifier = modifier.padding(bottom = 16.dp)
) {
buttons()
is Step.Skipped -> return
}
}

2
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewEvents.kt → features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewEvents.kt

@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl
package io.element.android.features.verifysession.impl.outgoing
sealed interface VerifySelfSessionViewEvents {
data object RequestVerification : VerifySelfSessionViewEvents

33
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/Common.kt

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.ui
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
internal fun aEmojisSessionVerificationData(
emojiList: List<VerificationEmoji> = aVerificationEmojiList(),
): SessionVerificationData {
return SessionVerificationData.Emojis(emojiList)
}
internal fun aDecimalsSessionVerificationData(
decimals: List<Int> = listOf(123, 456, 789),
): SessionVerificationData {
return SessionVerificationData.Decimals(decimals)
}
private fun aVerificationEmojiList() = listOf(
VerificationEmoji(number = 27, emoji = "🍕", description = "Pizza"),
VerificationEmoji(number = 54, emoji = "🚀", description = "Rocket"),
VerificationEmoji(number = 54, emoji = "🚀", description = "Rocket"),
VerificationEmoji(number = 42, emoji = "📕", description = "Book"),
VerificationEmoji(number = 48, emoji = "🔨", description = "Hammer"),
VerificationEmoji(number = 48, emoji = "🔨", description = "Hammer"),
VerificationEmoji(number = 63, emoji = "📌", description = "Pin"),
)

27
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationBottomMenu.kt

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.ui
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
@Composable
internal fun VerificationBottomMenu(
modifier: Modifier = Modifier,
buttons: @Composable ColumnScope.() -> Unit,
) {
ButtonColumnMolecule(
modifier = modifier.padding(bottom = 16.dp)
) {
buttons()
}
}

94
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/ui/VerificationContentVerifying.kt

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.ui
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.verifysession.impl.emoji.toEmojiResource
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
@Composable
internal fun VerificationContentVerifying(
data: SessionVerificationData,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
when (data) {
is SessionVerificationData.Decimals -> {
val text = data.decimals.joinToString(separator = " - ") { it.toString() }
Text(
modifier = Modifier.fillMaxWidth(),
text = text,
style = ElementTheme.typography.fontHeadingLgBold,
color = MaterialTheme.colorScheme.primary,
textAlign = TextAlign.Center,
)
}
is SessionVerificationData.Emojis -> {
// We want each row to have up to 4 emojis
val rows = data.emojis.chunked(4)
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(40.dp),
) {
rows.forEach { emojis ->
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
for (emoji in emojis) {
EmojiItemView(emoji = emoji, modifier = Modifier.widthIn(max = 60.dp))
}
}
}
}
}
}
}
}
@Composable
private fun EmojiItemView(emoji: VerificationEmoji, modifier: Modifier = Modifier) {
val emojiResource = emoji.number.toEmojiResource()
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier) {
Image(
modifier = Modifier.size(48.dp),
painter = painterResource(id = emojiResource.drawableRes),
contentDescription = null,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = emojiResource.nameRes),
style = ElementTheme.typography.fontBodyMdRegular,
color = MaterialTheme.colorScheme.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}

25
features/verifysession/impl/src/main/kotlin/io/element/android/features/verifysession/impl/util/StateMachineUtil.kt

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.util
import com.freeletics.flowredux.dsl.InStateBuilderBlock
import kotlinx.coroutines.ExperimentalCoroutinesApi
import timber.log.Timber
import com.freeletics.flowredux.dsl.State as MachineState
internal fun <T : Any> T.andLogStateChange() = also {
Timber.w("Verification: state machine state moved to [${this::class.simpleName}]")
}
@OptIn(ExperimentalCoroutinesApi::class)
inline fun <State : Any, reified Event : Any> InStateBuilderBlock<State, State, Event>.logReceivedEvents() {
on { event: Event, state: MachineState<State> ->
Timber.w("Verification in state [${state.snapshot::class.simpleName}] receiving event [${event::class.simpleName}]")
state.noChange()
}
}

292
features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationPresenterTest.kt

@ -0,0 +1,292 @@ @@ -0,0 +1,292 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.incoming
import com.google.common.truth.Truth.assertThat
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormatter
import io.element.android.libraries.dateformatter.test.A_FORMATTED_DATE
import io.element.android.libraries.dateformatter.test.FakeLastMessageTimestampFormatter
import io.element.android.libraries.matrix.api.core.FlowId
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.test.A_DEVICE_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.verification.FakeSessionVerificationService
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import io.element.android.tests.testutils.test
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
@ExperimentalCoroutinesApi
class IncomingVerificationPresenterTest {
@get:Rule
val warmUpRule = WarmUpRule()
@Test
fun `present - nominal case - incoming verification successful`() = runTest {
val acknowledgeVerificationRequestLambda = lambdaRecorder<SessionVerificationRequestDetails, Unit> { _ -> }
val acceptVerificationRequestLambda = lambdaRecorder<Unit> { }
val approveVerificationLambda = lambdaRecorder<Unit> { }
val resetLambda = lambdaRecorder<Boolean, Unit> { }
val fakeSessionVerificationService = FakeSessionVerificationService(
acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda,
acceptVerificationRequestLambda = acceptVerificationRequestLambda,
approveVerificationLambda = approveVerificationLambda,
resetLambda = resetLambda,
)
createPresenter(
service = fakeSessionVerificationService,
).test {
val initialState = awaitItem()
assertThat(initialState.step).isEqualTo(
IncomingVerificationState.Step.Initial(
deviceDisplayName = "a device name",
deviceId = A_DEVICE_ID,
formattedSignInTime = A_FORMATTED_DATE,
isWaiting = false,
)
)
resetLambda.assertions().isCalledOnce().with(value(false))
acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(aSessionVerificationRequestDetails))
acceptVerificationRequestLambda.assertions().isNeverCalled()
// User accept the incoming verification
initialState.eventSink(IncomingVerificationViewEvents.StartVerification)
skipItems(1)
val initialWaitingState = awaitItem()
assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue()
advanceUntilIdle()
acceptVerificationRequestLambda.assertions().isCalledOnce()
// Remote sent the data
fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)
fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification)
fakeSessionVerificationService.emitVerificationFlowState(
VerificationFlowState.DidReceiveVerificationData(
data = aEmojisSessionVerificationData()
)
)
val emojiState = awaitItem()
assertThat(emojiState.step).isEqualTo(
IncomingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(),
isWaiting = false
)
)
// User claims that the emoji matches
emojiState.eventSink(IncomingVerificationViewEvents.ConfirmVerification)
val emojiWaitingItem = awaitItem()
assertThat((emojiWaitingItem.step as IncomingVerificationState.Step.Verifying).isWaiting).isTrue()
approveVerificationLambda.assertions().isCalledOnce()
// Remote confirm that the emojis match
fakeSessionVerificationService.emitVerificationFlowState(
VerificationFlowState.DidFinish
)
val finalItem = awaitItem()
assertThat(finalItem.step).isEqualTo(IncomingVerificationState.Step.Completed)
}
}
@Test
fun `present - emoji not matching case - incoming verification failure`() = runTest {
val acknowledgeVerificationRequestLambda = lambdaRecorder<SessionVerificationRequestDetails, Unit> { _ -> }
val acceptVerificationRequestLambda = lambdaRecorder<Unit> { }
val declineVerificationLambda = lambdaRecorder<Unit> { }
val resetLambda = lambdaRecorder<Boolean, Unit> { }
val fakeSessionVerificationService = FakeSessionVerificationService(
acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda,
acceptVerificationRequestLambda = acceptVerificationRequestLambda,
declineVerificationLambda = declineVerificationLambda,
resetLambda = resetLambda,
)
createPresenter(
service = fakeSessionVerificationService,
).test {
val initialState = awaitItem()
assertThat(initialState.step).isEqualTo(
IncomingVerificationState.Step.Initial(
deviceDisplayName = "a device name",
deviceId = A_DEVICE_ID,
formattedSignInTime = A_FORMATTED_DATE,
isWaiting = false,
)
)
resetLambda.assertions().isCalledOnce().with(value(false))
acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(aSessionVerificationRequestDetails))
acceptVerificationRequestLambda.assertions().isNeverCalled()
// User accept the incoming verification
initialState.eventSink(IncomingVerificationViewEvents.StartVerification)
skipItems(1)
val initialWaitingState = awaitItem()
assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue()
advanceUntilIdle()
acceptVerificationRequestLambda.assertions().isCalledOnce()
// Remote sent the data
fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)
fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification)
fakeSessionVerificationService.emitVerificationFlowState(
VerificationFlowState.DidReceiveVerificationData(
data = aEmojisSessionVerificationData()
)
)
val emojiState = awaitItem()
// User claims that the emojis do not match
emojiState.eventSink(IncomingVerificationViewEvents.DeclineVerification)
val emojiWaitingItem = awaitItem()
assertThat((emojiWaitingItem.step as IncomingVerificationState.Step.Verifying).isWaiting).isTrue()
declineVerificationLambda.assertions().isCalledOnce()
// Remote confirm that there is a failure
fakeSessionVerificationService.emitVerificationFlowState(
VerificationFlowState.DidFail
)
val finalItem = awaitItem()
assertThat(finalItem.step).isEqualTo(IncomingVerificationState.Step.Failure)
}
}
@Test
fun `present - incoming verification is remotely canceled`() = runTest {
val acknowledgeVerificationRequestLambda = lambdaRecorder<SessionVerificationRequestDetails, Unit> { _ -> }
val acceptVerificationRequestLambda = lambdaRecorder<Unit> { }
val declineVerificationLambda = lambdaRecorder<Unit> { }
val resetLambda = lambdaRecorder<Boolean, Unit> { }
val onFinishLambda = lambdaRecorder<Unit> { }
val fakeSessionVerificationService = FakeSessionVerificationService(
acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda,
acceptVerificationRequestLambda = acceptVerificationRequestLambda,
declineVerificationLambda = declineVerificationLambda,
resetLambda = resetLambda,
)
createPresenter(
service = fakeSessionVerificationService,
navigator = IncomingVerificationNavigator(onFinishLambda),
).test {
val initialState = awaitItem()
assertThat(initialState.step).isEqualTo(
IncomingVerificationState.Step.Initial(
deviceDisplayName = "a device name",
deviceId = A_DEVICE_ID,
formattedSignInTime = A_FORMATTED_DATE,
isWaiting = false,
)
)
// Remote cancel the verification request
fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidCancel)
// The screen is dismissed
skipItems(2)
onFinishLambda.assertions().isCalledOnce()
}
}
@Test
fun `present - user goes back when comparing emoji - incoming verification failure`() = runTest {
val acknowledgeVerificationRequestLambda = lambdaRecorder<SessionVerificationRequestDetails, Unit> { _ -> }
val acceptVerificationRequestLambda = lambdaRecorder<Unit> { }
val declineVerificationLambda = lambdaRecorder<Unit> { }
val resetLambda = lambdaRecorder<Boolean, Unit> { }
val fakeSessionVerificationService = FakeSessionVerificationService(
acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda,
acceptVerificationRequestLambda = acceptVerificationRequestLambda,
declineVerificationLambda = declineVerificationLambda,
resetLambda = resetLambda,
)
createPresenter(
service = fakeSessionVerificationService,
).test {
val initialState = awaitItem()
assertThat(initialState.step).isEqualTo(
IncomingVerificationState.Step.Initial(
deviceDisplayName = "a device name",
deviceId = A_DEVICE_ID,
formattedSignInTime = A_FORMATTED_DATE,
isWaiting = false,
)
)
resetLambda.assertions().isCalledOnce().with(value(false))
acknowledgeVerificationRequestLambda.assertions().isCalledOnce().with(value(aSessionVerificationRequestDetails))
acceptVerificationRequestLambda.assertions().isNeverCalled()
// User accept the incoming verification
initialState.eventSink(IncomingVerificationViewEvents.StartVerification)
skipItems(1)
val initialWaitingState = awaitItem()
assertThat((initialWaitingState.step as IncomingVerificationState.Step.Initial).isWaiting).isTrue()
advanceUntilIdle()
acceptVerificationRequestLambda.assertions().isCalledOnce()
// Remote sent the data
fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)
fakeSessionVerificationService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification)
fakeSessionVerificationService.emitVerificationFlowState(
VerificationFlowState.DidReceiveVerificationData(
data = aEmojisSessionVerificationData()
)
)
val emojiState = awaitItem()
// User goes back
emojiState.eventSink(IncomingVerificationViewEvents.GoBack)
val emojiWaitingItem = awaitItem()
assertThat((emojiWaitingItem.step as IncomingVerificationState.Step.Verifying).isWaiting).isTrue()
declineVerificationLambda.assertions().isCalledOnce()
// Remote confirm that there is a failure
fakeSessionVerificationService.emitVerificationFlowState(
VerificationFlowState.DidFail
)
val finalItem = awaitItem()
assertThat(finalItem.step).isEqualTo(IncomingVerificationState.Step.Failure)
}
}
@Test
fun `present - user ignores incoming request`() = runTest {
val acknowledgeVerificationRequestLambda = lambdaRecorder<SessionVerificationRequestDetails, Unit> { _ -> }
val acceptVerificationRequestLambda = lambdaRecorder<Unit> { }
val resetLambda = lambdaRecorder<Boolean, Unit> { }
val fakeSessionVerificationService = FakeSessionVerificationService(
acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda,
acceptVerificationRequestLambda = acceptVerificationRequestLambda,
resetLambda = resetLambda,
)
val navigatorLambda = lambdaRecorder<Unit> { }
createPresenter(
service = fakeSessionVerificationService,
navigator = IncomingVerificationNavigator(navigatorLambda),
).test {
val initialState = awaitItem()
initialState.eventSink(IncomingVerificationViewEvents.IgnoreVerification)
skipItems(1)
navigatorLambda.assertions().isCalledOnce()
}
}
private val aSessionVerificationRequestDetails = SessionVerificationRequestDetails(
senderId = A_USER_ID,
flowId = FlowId("flowId"),
deviceId = A_DEVICE_ID,
displayName = "a device name",
firstSeenTimestamp = A_TIMESTAMP,
)
private fun createPresenter(
sessionVerificationRequestDetails: SessionVerificationRequestDetails = aSessionVerificationRequestDetails,
navigator: IncomingVerificationNavigator = IncomingVerificationNavigator { lambdaError() },
service: SessionVerificationService = FakeSessionVerificationService(),
dateFormatter: LastMessageTimestampFormatter = FakeLastMessageTimestampFormatter(A_FORMATTED_DATE),
) = IncomingVerificationPresenter(
sessionVerificationRequestDetails = sessionVerificationRequestDetails,
navigator = navigator,
sessionVerificationService = service,
stateMachine = IncomingVerificationStateMachine(service),
dateFormatter = dateFormatter,
)
}

217
features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/incoming/IncomingVerificationViewTest.kt

@ -0,0 +1,217 @@ @@ -0,0 +1,217 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl.incoming
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.verifysession.impl.R
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
import io.element.android.libraries.ui.strings.CommonStrings
import io.element.android.tests.testutils.EventsRecorder
import io.element.android.tests.testutils.clickOn
import io.element.android.tests.testutils.pressBackKey
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class IncomingVerificationViewTest {
@get:Rule val rule = createAndroidComposeRule<ComponentActivity>()
// region step Initial
@Test
fun `back key pressed - ignore the verification`() {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView(
anIncomingVerificationState(
step = aStepInitial(),
eventSink = eventsRecorder
),
)
rule.pressBackKey()
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
}
@Test
fun `ignore incoming verification emits the expected event`() {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView(
anIncomingVerificationState(
step = aStepInitial(),
eventSink = eventsRecorder
),
)
rule.clickOn(CommonStrings.action_ignore)
eventsRecorder.assertSingle(IncomingVerificationViewEvents.IgnoreVerification)
}
@Test
fun `start incoming verification emits the expected event`() {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView(
anIncomingVerificationState(
step = aStepInitial(),
eventSink = eventsRecorder
),
)
rule.clickOn(CommonStrings.action_start)
eventsRecorder.assertSingle(IncomingVerificationViewEvents.StartVerification)
}
@Test
fun `back key pressed - when awaiting response cancels the verification`() {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView(
anIncomingVerificationState(
step = aStepInitial(
isWaiting = true,
),
eventSink = eventsRecorder
),
)
rule.pressBackKey()
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
}
// endregion step Initial
// region step Verifying
@Test
fun `back key pressed - when ready to verify cancels the verification`() {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView(
anIncomingVerificationState(
step = IncomingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(),
isWaiting = false,
),
eventSink = eventsRecorder
),
)
rule.pressBackKey()
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
}
@Test
fun `back key pressed - when verifying and loading emits the expected event`() {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView(
anIncomingVerificationState(
step = IncomingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(),
isWaiting = true,
),
eventSink = eventsRecorder
),
)
rule.pressBackKey()
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
}
@Test
fun `clicking on they do not match emits the expected event`() {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView(
anIncomingVerificationState(
step = IncomingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(),
isWaiting = false,
),
eventSink = eventsRecorder
),
)
rule.clickOn(R.string.screen_session_verification_they_dont_match)
eventsRecorder.assertSingle(IncomingVerificationViewEvents.DeclineVerification)
}
@Test
fun `clicking on they match emits the expected event`() {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView(
anIncomingVerificationState(
step = IncomingVerificationState.Step.Verifying(
data = aEmojisSessionVerificationData(),
isWaiting = false,
),
eventSink = eventsRecorder
),
)
rule.clickOn(R.string.screen_session_verification_they_match)
eventsRecorder.assertSingle(IncomingVerificationViewEvents.ConfirmVerification)
}
// endregion
// region step Failure
@Test
fun `back key pressed - when failure resets the flow`() {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView(
anIncomingVerificationState(
step = IncomingVerificationState.Step.Failure,
eventSink = eventsRecorder
),
)
rule.pressBackKey()
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
}
@Test
fun `click on done - when failure resets the flow`() {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView(
anIncomingVerificationState(
step = IncomingVerificationState.Step.Failure,
eventSink = eventsRecorder
),
)
rule.clickOn(CommonStrings.action_done)
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
}
// endregion
// region step Completed
@Test
fun `back key pressed - on Completed step emits the expected event`() {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView(
anIncomingVerificationState(
step = IncomingVerificationState.Step.Completed,
eventSink = eventsRecorder
),
)
rule.pressBackKey()
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
}
@Test
fun `when flow is completed and the user clicks on the done button, the expected event is emitted`() {
val eventsRecorder = EventsRecorder<IncomingVerificationViewEvents>()
rule.setIncomingVerificationView(
anIncomingVerificationState(
step = IncomingVerificationState.Step.Completed,
eventSink = eventsRecorder
),
)
rule.clickOn(CommonStrings.action_done)
eventsRecorder.assertSingle(IncomingVerificationViewEvents.GoBack)
}
// endregion
private fun <R : TestRule> AndroidComposeTestRule<R, ComponentActivity>.setIncomingVerificationView(
state: IncomingVerificationState,
) {
setContent {
IncomingVerificationView(
state = state,
)
}
}
}

227
features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionPresenterTest.kt → features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionPresenterTest.kt

@ -5,7 +5,7 @@ @@ -5,7 +5,7 @@
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl
package io.element.android.features.verifysession.impl.outgoing
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
@ -14,12 +14,13 @@ import app.cash.turbine.test @@ -14,12 +14,13 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.logout.api.LogoutUseCase
import io.element.android.features.logout.test.FakeLogoutUseCase
import io.element.android.features.verifysession.impl.VerifySelfSessionState.VerificationStep
import io.element.android.features.verifysession.impl.outgoing.VerifySelfSessionState.Step
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.matrix.api.encryption.EncryptionService
import io.element.android.libraries.matrix.api.encryption.RecoveryState
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
@ -29,6 +30,7 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService @@ -29,6 +30,7 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService
import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService
import io.element.android.libraries.preferences.test.InMemorySessionPreferencesStore
import io.element.android.tests.testutils.WarmUpRule
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.lambda.lambdaRecorder
import io.element.android.tests.testutils.lambda.value
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ -43,12 +45,14 @@ class VerifySelfSessionPresenterTest { @@ -43,12 +45,14 @@ class VerifySelfSessionPresenterTest {
@Test
fun `present - Initial state is received`() = runTest {
val presenter = createVerifySelfSessionPresenter()
val presenter = createVerifySelfSessionPresenter(
service = unverifiedSessionService(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
awaitItem().run {
assertThat(verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
assertThat(step).isEqualTo(Step.Initial(false))
assertThat(displaySkipButton).isTrue()
}
}
@ -57,7 +61,10 @@ class VerifySelfSessionPresenterTest { @@ -57,7 +61,10 @@ class VerifySelfSessionPresenterTest {
@Test
fun `present - hides skip verification button on non-debuggable builds`() = runTest {
val buildMeta = aBuildMeta(isDebuggable = false)
val presenter = createVerifySelfSessionPresenter(buildMeta = buildMeta)
val presenter = createVerifySelfSessionPresenter(
service = unverifiedSessionService(),
buildMeta = buildMeta,
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
@ -67,7 +74,11 @@ class VerifySelfSessionPresenterTest { @@ -67,7 +74,11 @@ class VerifySelfSessionPresenterTest {
@Test
fun `present - Initial state is received, can use recovery key`() = runTest {
val resetLambda = lambdaRecorder<Boolean, Unit> { }
val presenter = createVerifySelfSessionPresenter(
service = unverifiedSessionService(
resetLambda = resetLambda
),
encryptionService = FakeEncryptionService().apply {
emitRecoveryState(RecoveryState.INCOMPLETE)
}
@ -75,13 +86,15 @@ class VerifySelfSessionPresenterTest { @@ -75,13 +86,15 @@ class VerifySelfSessionPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(true))
assertThat(awaitItem().step).isEqualTo(Step.Initial(true))
resetLambda.assertions().isCalledOnce().with(value(true))
}
}
@Test
fun `present - Initial state is received, can use recovery key and is last device`() = runTest {
val presenter = createVerifySelfSessionPresenter(
service = unverifiedSessionService(),
encryptionService = FakeEncryptionService().apply {
emitIsLastDevice(true)
emitRecoveryState(RecoveryState.INCOMPLETE)
@ -90,13 +103,16 @@ class VerifySelfSessionPresenterTest { @@ -90,13 +103,16 @@ class VerifySelfSessionPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(canEnterRecoveryKey = true, isLastDevice = true))
assertThat(awaitItem().step).isEqualTo(Step.Initial(canEnterRecoveryKey = true, isLastDevice = true))
}
}
@Test
fun `present - Handles requestVerification`() = runTest {
val service = unverifiedSessionService()
val service = unverifiedSessionService(
requestVerificationLambda = { },
startVerificationLambda = { },
)
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -107,32 +123,36 @@ class VerifySelfSessionPresenterTest { @@ -107,32 +123,36 @@ class VerifySelfSessionPresenterTest {
@Test
fun `present - Handles startSasVerification`() = runTest {
val service = unverifiedSessionService()
val service = unverifiedSessionService(
startVerificationLambda = { },
)
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
val eventSink = initialState.eventSink
eventSink(VerifySelfSessionViewEvents.StartSasVerification)
assertThat(initialState.step).isEqualTo(Step.Initial(false))
initialState.eventSink(VerifySelfSessionViewEvents.StartSasVerification)
// Await for other device response:
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
assertThat(awaitItem().step).isEqualTo(Step.AwaitingOtherDeviceResponse)
service.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification)
// ChallengeReceived:
service.triggerReceiveVerificationData(SessionVerificationData.Emojis(emptyList()))
service.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(SessionVerificationData.Emojis(emptyList())))
val verifyingState = awaitItem()
assertThat(verifyingState.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
assertThat(verifyingState.step).isInstanceOf(Step.Verifying::class.java)
}
}
@Test
fun `present - Cancelation on initial state does nothing`() = runTest {
val presenter = createVerifySelfSessionPresenter()
fun `present - Cancellation on initial state does nothing`() = runTest {
val presenter = createVerifySelfSessionPresenter(
service = unverifiedSessionService(),
)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
assertThat(initialState.verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
assertThat(initialState.step).isEqualTo(Step.Initial(false))
val eventSink = initialState.eventSink
eventSink(VerifySelfSessionViewEvents.Cancel)
expectNoEvents()
@ -141,92 +161,110 @@ class VerifySelfSessionPresenterTest { @@ -141,92 +161,110 @@ class VerifySelfSessionPresenterTest {
@Test
fun `present - A failure when verifying cancels it`() = runTest {
val service = unverifiedSessionService()
val service = unverifiedSessionService(
requestVerificationLambda = { },
startVerificationLambda = { },
approveVerificationLambda = { },
)
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val state = requestVerificationAndAwaitVerifyingState(service)
service.shouldFail = true
state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
// Cancelling
assertThat(awaitItem().verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
assertThat(awaitItem().step).isInstanceOf(Step.Verifying::class.java)
service.emitVerificationFlowState(VerificationFlowState.DidFail)
// Cancelled
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
assertThat(awaitItem().step).isEqualTo(Step.Canceled)
}
}
@Test
fun `present - A fail when requesting verification resets the state to the initial one`() = runTest {
val service = unverifiedSessionService()
val service = unverifiedSessionService(
requestVerificationLambda = { },
)
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
service.shouldFail = true
awaitItem().eventSink(VerifySelfSessionViewEvents.RequestVerification)
service.shouldFail = false
assertThat(awaitItem().verificationFlowStep).isInstanceOf(VerificationStep.AwaitingOtherDeviceResponse::class.java)
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
service.emitVerificationFlowState(VerificationFlowState.DidFail)
assertThat(awaitItem().step).isInstanceOf(Step.AwaitingOtherDeviceResponse::class.java)
assertThat(awaitItem().step).isEqualTo(Step.Initial(false))
}
}
@Test
fun `present - Canceling the flow once it's verifying cancels it`() = runTest {
val service = unverifiedSessionService()
val service = unverifiedSessionService(
requestVerificationLambda = { },
startVerificationLambda = { },
cancelVerificationLambda = { },
)
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val state = requestVerificationAndAwaitVerifyingState(service)
state.eventSink(VerifySelfSessionViewEvents.Cancel)
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
assertThat(awaitItem().step).isEqualTo(Step.Canceled)
}
}
@Test
fun `present - When verifying, if we receive another challenge we ignore it`() = runTest {
val service = unverifiedSessionService()
val service = unverifiedSessionService(
requestVerificationLambda = { },
startVerificationLambda = { },
)
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
requestVerificationAndAwaitVerifyingState(service)
service.givenVerificationFlowState(VerificationFlowState.ReceivedVerificationData(SessionVerificationData.Emojis(emptyList())))
service.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(SessionVerificationData.Emojis(emptyList())))
ensureAllEventsConsumed()
}
}
@Test
fun `present - Restart after cancelation returns to requesting verification`() = runTest {
val service = unverifiedSessionService()
fun `present - Restart after cancellation returns to requesting verification`() = runTest {
val service = unverifiedSessionService(
requestVerificationLambda = { },
startVerificationLambda = { },
)
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val state = requestVerificationAndAwaitVerifyingState(service)
service.givenVerificationFlowState(VerificationFlowState.Canceled)
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
service.emitVerificationFlowState(VerificationFlowState.DidCancel)
assertThat(awaitItem().step).isEqualTo(Step.Canceled)
state.eventSink(VerifySelfSessionViewEvents.RequestVerification)
// Went back to requesting verification
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
assertThat(awaitItem().step).isEqualTo(Step.AwaitingOtherDeviceResponse)
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `present - Go back after cancelation returns to initial state`() = runTest {
val service = unverifiedSessionService()
fun `present - Go back after cancellation returns to initial state`() = runTest {
val service = unverifiedSessionService(
requestVerificationLambda = { },
startVerificationLambda = { },
)
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val state = requestVerificationAndAwaitVerifyingState(service)
service.givenVerificationFlowState(VerificationFlowState.Canceled)
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
service.emitVerificationFlowState(VerificationFlowState.DidCancel)
assertThat(awaitItem().step).isEqualTo(Step.Canceled)
state.eventSink(VerifySelfSessionViewEvents.Reset)
// Went back to initial state
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
assertThat(awaitItem().step).isEqualTo(Step.Initial(false))
cancelAndIgnoreRemainingEvents()
}
}
@ -236,7 +274,11 @@ class VerifySelfSessionPresenterTest { @@ -236,7 +274,11 @@ class VerifySelfSessionPresenterTest {
val emojis = listOf(
VerificationEmoji(number = 30, emoji = "😀", description = "Smiley")
)
val service = unverifiedSessionService()
val service = unverifiedSessionService(
requestVerificationLambda = { },
startVerificationLambda = { },
approveVerificationLambda = { },
)
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
@ -246,54 +288,65 @@ class VerifySelfSessionPresenterTest { @@ -246,54 +288,65 @@ class VerifySelfSessionPresenterTest {
SessionVerificationData.Emojis(emojis)
)
state.eventSink(VerifySelfSessionViewEvents.ConfirmVerification)
assertThat(awaitItem().verificationFlowStep).isEqualTo(
VerificationStep.Verifying(
assertThat(awaitItem().step).isEqualTo(
Step.Verifying(
SessionVerificationData.Emojis(emojis),
AsyncData.Loading(),
)
)
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed)
service.emitVerificationFlowState(VerificationFlowState.DidFinish)
assertThat(awaitItem().step).isEqualTo(Step.Completed)
}
}
@Test
fun `present - When verification is declined, the flow is canceled`() = runTest {
val service = unverifiedSessionService()
val service = unverifiedSessionService(
requestVerificationLambda = { },
startVerificationLambda = { },
declineVerificationLambda = { },
)
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val state = requestVerificationAndAwaitVerifyingState(service)
state.eventSink(VerifySelfSessionViewEvents.DeclineVerification)
assertThat(awaitItem().verificationFlowStep).isEqualTo(
VerificationStep.Verifying(
assertThat(awaitItem().step).isEqualTo(
Step.Verifying(
SessionVerificationData.Emojis(emptyList()),
AsyncData.Loading(),
)
)
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Canceled)
service.emitVerificationFlowState(VerificationFlowState.DidCancel)
assertThat(awaitItem().step).isEqualTo(Step.Canceled)
}
}
@Test
fun `present - Skip event skips the flow`() = runTest {
val service = unverifiedSessionService()
val service = unverifiedSessionService(
requestVerificationLambda = { },
startVerificationLambda = { },
)
val presenter = createVerifySelfSessionPresenter(service)
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
val state = requestVerificationAndAwaitVerifyingState(service)
state.eventSink(VerifySelfSessionViewEvents.SkipVerification)
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Skipped)
assertThat(awaitItem().step).isEqualTo(Step.Skipped)
}
}
@Test
fun `present - When verification is done using recovery key, the flow is completed`() = runTest {
val service = FakeSessionVerificationService().apply {
givenNeedsSessionVerification(false)
givenVerifiedStatus(SessionVerifiedStatus.Verified)
givenVerificationFlowState(VerificationFlowState.Finished)
val service = FakeSessionVerificationService(
resetLambda = { },
).apply {
emitNeedsSessionVerification(false)
emitVerifiedStatus(SessionVerifiedStatus.Verified)
emitVerificationFlowState(VerificationFlowState.DidFinish)
}
val presenter = createVerifySelfSessionPresenter(
service = service,
@ -302,16 +355,18 @@ class VerifySelfSessionPresenterTest { @@ -302,16 +355,18 @@ class VerifySelfSessionPresenterTest {
moleculeFlow(RecompositionMode.Immediate) {
presenter.present()
}.test {
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Completed)
assertThat(awaitItem().step).isEqualTo(Step.Completed)
}
}
@Test
fun `present - When verification is not needed, the flow is skipped`() = runTest {
val service = FakeSessionVerificationService().apply {
givenNeedsSessionVerification(false)
givenVerifiedStatus(SessionVerifiedStatus.Verified)
givenVerificationFlowState(VerificationFlowState.Finished)
val service = FakeSessionVerificationService(
resetLambda = { },
).apply {
emitNeedsSessionVerification(false)
emitVerifiedStatus(SessionVerifiedStatus.Verified)
emitVerificationFlowState(VerificationFlowState.DidFinish)
}
val presenter = createVerifySelfSessionPresenter(
service = service,
@ -321,16 +376,18 @@ class VerifySelfSessionPresenterTest { @@ -321,16 +376,18 @@ class VerifySelfSessionPresenterTest {
presenter.present()
}.test {
skipItems(1)
assertThat(awaitItem().verificationFlowStep).isEqualTo(VerificationStep.Skipped)
assertThat(awaitItem().step).isEqualTo(Step.Skipped)
}
}
@Test
fun `present - When user request to sign out, the sign out use case is invoked`() = runTest {
val service = FakeSessionVerificationService().apply {
givenNeedsSessionVerification(false)
givenVerifiedStatus(SessionVerifiedStatus.Verified)
givenVerificationFlowState(VerificationFlowState.Finished)
val service = FakeSessionVerificationService(
resetLambda = { },
).apply {
emitNeedsSessionVerification(false)
emitVerifiedStatus(SessionVerifiedStatus.Verified)
emitVerificationFlowState(VerificationFlowState.DidFinish)
}
val signOutLambda = lambdaRecorder<Boolean, String?> { "aUrl" }
val presenter = createVerifySelfSessionPresenter(
@ -356,33 +413,53 @@ class VerifySelfSessionPresenterTest { @@ -356,33 +413,53 @@ class VerifySelfSessionPresenterTest {
sessionVerificationData: SessionVerificationData = SessionVerificationData.Emojis(emptyList()),
): VerifySelfSessionState {
var state = awaitItem()
assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Initial(false))
assertThat(state.step).isEqualTo(Step.Initial(false))
state.eventSink(VerifySelfSessionViewEvents.RequestVerification)
// Await for other device response:
fakeService.emitVerificationFlowState(VerificationFlowState.DidAcceptVerificationRequest)
state = awaitItem()
assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
assertThat(state.step).isEqualTo(Step.AwaitingOtherDeviceResponse)
// Await for the state to be Ready
state = awaitItem()
assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.Ready)
assertThat(state.step).isEqualTo(Step.Ready)
state.eventSink(VerifySelfSessionViewEvents.StartSasVerification)
// Await for other device response (again):
fakeService.emitVerificationFlowState(VerificationFlowState.DidStartSasVerification)
state = awaitItem()
assertThat(state.verificationFlowStep).isEqualTo(VerificationStep.AwaitingOtherDeviceResponse)
fakeService.triggerReceiveVerificationData(sessionVerificationData)
assertThat(state.step).isEqualTo(Step.AwaitingOtherDeviceResponse)
// Finally, ChallengeReceived:
fakeService.emitVerificationFlowState(VerificationFlowState.DidReceiveVerificationData(sessionVerificationData))
state = awaitItem()
assertThat(state.verificationFlowStep).isInstanceOf(VerificationStep.Verifying::class.java)
assertThat(state.step).isInstanceOf(Step.Verifying::class.java)
return state
}
private fun unverifiedSessionService(): FakeSessionVerificationService {
return FakeSessionVerificationService().apply {
givenVerifiedStatus(SessionVerifiedStatus.NotVerified)
private suspend fun unverifiedSessionService(
requestVerificationLambda: () -> Unit = { lambdaError() },
cancelVerificationLambda: () -> Unit = { lambdaError() },
approveVerificationLambda: () -> Unit = { lambdaError() },
declineVerificationLambda: () -> Unit = { lambdaError() },
startVerificationLambda: () -> Unit = { lambdaError() },
resetLambda: (Boolean) -> Unit = { },
acknowledgeVerificationRequestLambda: (SessionVerificationRequestDetails) -> Unit = { lambdaError() },
acceptVerificationRequestLambda: () -> Unit = { lambdaError() },
): FakeSessionVerificationService {
return FakeSessionVerificationService(
requestVerificationLambda = requestVerificationLambda,
cancelVerificationLambda = cancelVerificationLambda,
approveVerificationLambda = approveVerificationLambda,
declineVerificationLambda = declineVerificationLambda,
startVerificationLambda = startVerificationLambda,
resetLambda = resetLambda,
acknowledgeVerificationRequestLambda = acknowledgeVerificationRequestLambda,
acceptVerificationRequestLambda = acceptVerificationRequestLambda,
).apply {
emitVerifiedStatus(SessionVerifiedStatus.NotVerified)
}
}
private fun createVerifySelfSessionPresenter(
service: SessionVerificationService = unverifiedSessionService(),
service: SessionVerificationService,
encryptionService: EncryptionService = FakeEncryptionService(),
buildMeta: BuildMeta = aBuildMeta(),
sessionPreferencesStore: InMemorySessionPreferencesStore = InMemorySessionPreferencesStore(),

30
features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/VerifySelfSessionViewTest.kt → features/verifysession/impl/src/test/kotlin/io/element/android/features/verifysession/impl/outgoing/VerifySelfSessionViewTest.kt

@ -5,12 +5,14 @@ @@ -5,12 +5,14 @@
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.features.verifysession.impl
package io.element.android.features.verifysession.impl.outgoing
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.junit4.AndroidComposeTestRule
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.element.android.features.verifysession.impl.R
import io.element.android.features.verifysession.impl.ui.aEmojisSessionVerificationData
import io.element.android.libraries.architecture.AsyncAction
import io.element.android.libraries.architecture.AsyncData
import io.element.android.libraries.ui.strings.CommonStrings
@ -36,7 +38,7 @@ class VerifySelfSessionViewTest { @@ -36,7 +38,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Canceled,
step = VerifySelfSessionState.Step.Canceled,
eventSink = eventsRecorder
),
)
@ -49,7 +51,7 @@ class VerifySelfSessionViewTest { @@ -49,7 +51,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.AwaitingOtherDeviceResponse,
step = VerifySelfSessionState.Step.AwaitingOtherDeviceResponse,
eventSink = eventsRecorder
),
)
@ -62,7 +64,7 @@ class VerifySelfSessionViewTest { @@ -62,7 +64,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Ready,
step = VerifySelfSessionState.Step.Ready,
eventSink = eventsRecorder
),
)
@ -75,7 +77,7 @@ class VerifySelfSessionViewTest { @@ -75,7 +77,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(
step = VerifySelfSessionState.Step.Verifying(
data = aEmojisSessionVerificationData(),
state = AsyncData.Uninitialized,
),
@ -91,7 +93,7 @@ class VerifySelfSessionViewTest { @@ -91,7 +93,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(
step = VerifySelfSessionState.Step.Verifying(
data = aEmojisSessionVerificationData(),
state = AsyncData.Loading(),
),
@ -107,7 +109,7 @@ class VerifySelfSessionViewTest { @@ -107,7 +109,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed,
step = VerifySelfSessionState.Step.Completed,
eventSink = eventsRecorder
),
)
@ -121,7 +123,7 @@ class VerifySelfSessionViewTest { @@ -121,7 +123,7 @@ class VerifySelfSessionViewTest {
ensureCalledOnce { callback ->
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Completed,
step = VerifySelfSessionState.Step.Completed,
eventSink = eventsRecorder
),
onFinished = callback,
@ -137,7 +139,7 @@ class VerifySelfSessionViewTest { @@ -137,7 +139,7 @@ class VerifySelfSessionViewTest {
ensureCalledOnce { callback ->
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true),
step = VerifySelfSessionState.Step.Initial(true),
eventSink = eventsRecorder
),
onEnterRecoveryKey = callback,
@ -153,7 +155,7 @@ class VerifySelfSessionViewTest { @@ -153,7 +155,7 @@ class VerifySelfSessionViewTest {
ensureCalledOnce { callback ->
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(true),
step = VerifySelfSessionState.Step.Initial(true),
eventSink = eventsRecorder
),
onLearnMoreClick = callback,
@ -167,7 +169,7 @@ class VerifySelfSessionViewTest { @@ -167,7 +169,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(
step = VerifySelfSessionState.Step.Verifying(
data = aEmojisSessionVerificationData(),
state = AsyncData.Uninitialized,
),
@ -183,7 +185,7 @@ class VerifySelfSessionViewTest { @@ -183,7 +185,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Verifying(
step = VerifySelfSessionState.Step.Verifying(
data = aEmojisSessionVerificationData(),
state = AsyncData.Uninitialized,
),
@ -199,7 +201,7 @@ class VerifySelfSessionViewTest { @@ -199,7 +201,7 @@ class VerifySelfSessionViewTest {
val eventsRecorder = EventsRecorder<VerifySelfSessionViewEvents>()
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Initial(canEnterRecoveryKey = true),
step = VerifySelfSessionState.Step.Initial(canEnterRecoveryKey = true),
displaySkipButton = true,
eventSink = eventsRecorder
),
@ -213,7 +215,7 @@ class VerifySelfSessionViewTest { @@ -213,7 +215,7 @@ class VerifySelfSessionViewTest {
ensureCalledOnce { callback ->
rule.setVerifySelfSessionView(
aVerifySelfSessionState(
verificationFlowStep = VerifySelfSessionState.VerificationStep.Skipped,
step = VerifySelfSessionState.Step.Skipped,
displaySkipButton = true,
eventSink = EnsureNeverCalledWithParam(),
),

5
libraries/dateformatter/test/src/main/kotlin/io/element/android/libraries/dateformatter/test/FakeLastMessageTimestampFormatter.kt

@ -11,8 +11,9 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat @@ -11,8 +11,9 @@ import io.element.android.libraries.dateformatter.api.LastMessageTimestampFormat
const val A_FORMATTED_DATE = "formatted_date"
class FakeLastMessageTimestampFormatter : LastMessageTimestampFormatter {
private var format = ""
class FakeLastMessageTimestampFormatter(
var format: String = "",
) : LastMessageTimestampFormatter {
fun givenFormat(format: String) {
this.format = format
}

34
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/TextWithLabelMolecule.kt

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.designsystem.atomic.molecules
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun TextWithLabelMolecule(
label: String,
text: String,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Text(
text = label,
style = ElementTheme.typography.fontBodySmRegular,
color = ElementTheme.colors.textSecondary,
)
Text(
text = text,
style = ElementTheme.typography.fontBodyMdRegular,
color = ElementTheme.colors.textPrimary,
)
}
}

15
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/FlowId.kt

@ -0,0 +1,15 @@ @@ -0,0 +1,15 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.core
import java.io.Serializable
@JvmInline
value class FlowId(val value: String) : Serializable {
override fun toString(): String = value
}

23
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationRequestDetails.kt

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.api.verification
import android.os.Parcelable
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.FlowId
import io.element.android.libraries.matrix.api.core.UserId
import kotlinx.parcelize.Parcelize
@Parcelize
data class SessionVerificationRequestDetails(
val senderId: UserId,
val flowId: FlowId,
val deviceId: DeviceId,
val displayName: String?,
val firstSeenTimestamp: Long,
) : Parcelable

34
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/verification/SessionVerificationService.kt

@ -56,7 +56,27 @@ interface SessionVerificationService { @@ -56,7 +56,27 @@ interface SessionVerificationService {
/**
* Returns the verification service state to the initial step.
*/
suspend fun reset()
suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean)
/**
* Register a listener to be notified of incoming session verification requests.
*/
fun setListener(listener: SessionVerificationServiceListener?)
/**
* Set this particular request as the currently active one and register for
* events pertaining it.
*/
suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails)
/**
* Accept the previously acknowledged verification request.
*/
suspend fun acceptVerificationRequest()
}
interface SessionVerificationServiceListener {
fun onIncomingSessionRequest(sessionVerificationRequestDetails: SessionVerificationRequestDetails)
}
/** Verification status of the current session. */
@ -82,20 +102,20 @@ sealed interface VerificationFlowState { @@ -82,20 +102,20 @@ sealed interface VerificationFlowState {
data object Initial : VerificationFlowState
/** Session verification request was accepted by another device. */
data object AcceptedVerificationRequest : VerificationFlowState
data object DidAcceptVerificationRequest : VerificationFlowState
/** Short Authentication String (SAS) verification started between the 2 devices. */
data object StartedSasVerification : VerificationFlowState
data object DidStartSasVerification : VerificationFlowState
/** Verification data for the SAS verification received. */
data class ReceivedVerificationData(val data: SessionVerificationData) : VerificationFlowState
data class DidReceiveVerificationData(val data: SessionVerificationData) : VerificationFlowState
/** Verification completed successfully. */
data object Finished : VerificationFlowState
data object DidFinish : VerificationFlowState
/** Verification was cancelled by either device. */
data object Canceled : VerificationFlowState
data object DidCancel : VerificationFlowState
/** Verification failed with an error. */
data object Failed : VerificationFlowState
data object DidFail : VerificationFlowState
}

55
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/RustSessionVerificationService.kt

@ -9,12 +9,15 @@ package io.element.android.libraries.matrix.impl.verification @@ -9,12 +9,15 @@ package io.element.android.libraries.matrix.impl.verification
import io.element.android.libraries.core.data.tryOrNull
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationEmoji
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.libraries.matrix.impl.util.cancelAndDestroy
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
@ -28,6 +31,7 @@ import kotlinx.coroutines.flow.stateIn @@ -28,6 +31,7 @@ import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import org.matrix.rustcomponents.sdk.Client
import org.matrix.rustcomponents.sdk.Encryption
@ -35,13 +39,13 @@ import org.matrix.rustcomponents.sdk.RecoveryState @@ -35,13 +39,13 @@ import org.matrix.rustcomponents.sdk.RecoveryState
import org.matrix.rustcomponents.sdk.RecoveryStateListener
import org.matrix.rustcomponents.sdk.SessionVerificationController
import org.matrix.rustcomponents.sdk.SessionVerificationControllerDelegate
import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails
import org.matrix.rustcomponents.sdk.VerificationState
import org.matrix.rustcomponents.sdk.VerificationStateListener
import org.matrix.rustcomponents.sdk.use
import timber.log.Timber
import kotlin.time.Duration.Companion.seconds
import org.matrix.rustcomponents.sdk.SessionVerificationData as RustSessionVerificationData
import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails
class RustSessionVerificationService(
private val client: Client,
@ -101,6 +105,16 @@ class RustSessionVerificationService( @@ -101,6 +105,16 @@ class RustSessionVerificationService(
.launchIn(sessionCoroutineScope)
}
override fun didReceiveVerificationRequest(details: RustSessionVerificationRequestDetails) {
listener?.onIncomingSessionRequest(details.map())
}
private var listener: SessionVerificationServiceListener? = null
override fun setListener(listener: SessionVerificationServiceListener?) {
this.listener = listener
}
override suspend fun requestVerification() = tryOrFail {
initVerificationControllerIfNeeded()
verificationController.requestVerification()
@ -120,9 +134,24 @@ class RustSessionVerificationService( @@ -120,9 +134,24 @@ class RustSessionVerificationService(
verificationController.startSasVerification()
}
override suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) = tryOrFail {
verificationController.acknowledgeVerificationRequest(
senderId = details.senderId.value,
flowId = details.flowId.value,
)
}
override suspend fun acceptVerificationRequest() = tryOrFail {
verificationController.acceptVerificationRequest()
}
private suspend fun tryOrFail(block: suspend () -> Unit) {
runCatching {
block()
// Ensure the block cannot be cancelled, else if the Rust SDK emit a new state during the API execution,
// the state machine may cancel the api call.
withContext(NonCancellable) {
block()
}
}.onFailure {
Timber.e(it, "Failed to verify session")
didFail()
@ -133,16 +162,16 @@ class RustSessionVerificationService( @@ -133,16 +162,16 @@ class RustSessionVerificationService(
// When verification attempt is accepted by the other device
override fun didAcceptVerificationRequest() {
_verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
_verificationFlowState.value = VerificationFlowState.DidAcceptVerificationRequest
}
override fun didCancel() {
_verificationFlowState.value = VerificationFlowState.Canceled
_verificationFlowState.value = VerificationFlowState.DidCancel
}
override fun didFail() {
Timber.e("Session verification failed with an unknown error")
_verificationFlowState.value = VerificationFlowState.Failed
_verificationFlowState.value = VerificationFlowState.DidFail
}
override fun didFinish() {
@ -158,7 +187,7 @@ class RustSessionVerificationService( @@ -158,7 +187,7 @@ class RustSessionVerificationService(
}
.onSuccess {
// Order here is important, first set the flow state as finished, then update the verification status
_verificationFlowState.value = VerificationFlowState.Finished
_verificationFlowState.value = VerificationFlowState.DidFinish
updateVerificationStatus()
}
.onFailure {
@ -169,22 +198,18 @@ class RustSessionVerificationService( @@ -169,22 +198,18 @@ class RustSessionVerificationService(
}
override fun didReceiveVerificationData(data: RustSessionVerificationData) {
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(data.map())
_verificationFlowState.value = VerificationFlowState.DidReceiveVerificationData(data.map())
}
// When the actual SAS verification starts
override fun didStartSasVerification() {
_verificationFlowState.value = VerificationFlowState.StartedSasVerification
}
override fun didReceiveVerificationRequest(details: SessionVerificationRequestDetails) {
// TODO
_verificationFlowState.value = VerificationFlowState.DidStartSasVerification
}
// end-region
override suspend fun reset() {
if (isReady.value) {
override suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) {
if (isReady.value && cancelAnyPendingVerificationAttempt) {
// Cancel any pending verification attempt
tryOrNull { verificationController.cancelVerification() }
}
@ -213,7 +238,7 @@ class RustSessionVerificationService( @@ -213,7 +238,7 @@ class RustSessionVerificationService(
}
private suspend fun updateVerificationStatus() {
if (verificationFlowState.value == VerificationFlowState.Finished) {
if (verificationFlowState.value == VerificationFlowState.DidFinish) {
// Calling `encryptionService.verificationState()` performs a network call and it will deadlock if there is no network
// So we need to check that *only* if we know there is network connection, which is the case when the verification flow just finished
Timber.d("Updating verification status: flow just finished")

22
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/verification/SessionVerificationRequestDetails.kt

@ -0,0 +1,22 @@ @@ -0,0 +1,22 @@
/*
* Copyright 2024 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only
* Please see LICENSE in the repository root for full details.
*/
package io.element.android.libraries.matrix.impl.verification
import io.element.android.libraries.matrix.api.core.DeviceId
import io.element.android.libraries.matrix.api.core.FlowId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import org.matrix.rustcomponents.sdk.SessionVerificationRequestDetails as RustSessionVerificationRequestDetails
fun RustSessionVerificationRequestDetails.map() = SessionVerificationRequestDetails(
senderId = UserId(senderId),
flowId = FlowId(flowId),
deviceId = DeviceId(deviceId),
displayName = displayName,
firstSeenTimestamp = firstSeenTimestamp.toLong(),
)

69
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/verification/FakeSessionVerificationService.kt

@ -7,79 +7,84 @@ @@ -7,79 +7,84 @@
package io.element.android.libraries.matrix.test.verification
import io.element.android.libraries.matrix.api.verification.SessionVerificationData
import io.element.android.libraries.matrix.api.verification.SessionVerificationRequestDetails
import io.element.android.libraries.matrix.api.verification.SessionVerificationService
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus
import io.element.android.libraries.matrix.api.verification.VerificationFlowState
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class FakeSessionVerificationService(
initialSessionVerifiedStatus: SessionVerifiedStatus = SessionVerifiedStatus.Unknown,
private val requestVerificationLambda: () -> Unit = { lambdaError() },
private val cancelVerificationLambda: () -> Unit = { lambdaError() },
private val approveVerificationLambda: () -> Unit = { lambdaError() },
private val declineVerificationLambda: () -> Unit = { lambdaError() },
private val startVerificationLambda: () -> Unit = { lambdaError() },
private val resetLambda: (Boolean) -> Unit = { lambdaError() },
private val acknowledgeVerificationRequestLambda: (SessionVerificationRequestDetails) -> Unit = { lambdaError() },
private val acceptVerificationRequestLambda: () -> Unit = { lambdaError() },
) : SessionVerificationService {
private val _sessionVerifiedStatus = MutableStateFlow(initialSessionVerifiedStatus)
private var _verificationFlowState = MutableStateFlow<VerificationFlowState>(VerificationFlowState.Initial)
private var _needsSessionVerification = MutableStateFlow(true)
var shouldFail = false
override val verificationFlowState: StateFlow<VerificationFlowState> = _verificationFlowState
override val sessionVerifiedStatus: StateFlow<SessionVerifiedStatus> = _sessionVerifiedStatus
override val needsSessionVerification: Flow<Boolean> = _needsSessionVerification
override suspend fun requestVerification() {
if (!shouldFail) {
_verificationFlowState.value = VerificationFlowState.AcceptedVerificationRequest
} else {
_verificationFlowState.value = VerificationFlowState.Failed
}
requestVerificationLambda()
}
override suspend fun cancelVerification() {
_verificationFlowState.value = VerificationFlowState.Canceled
cancelVerificationLambda()
}
override suspend fun approveVerification() {
if (!shouldFail) {
_verificationFlowState.value = VerificationFlowState.Finished
} else {
_verificationFlowState.value = VerificationFlowState.Failed
}
approveVerificationLambda()
}
override suspend fun declineVerification() {
if (!shouldFail) {
_verificationFlowState.value = VerificationFlowState.Canceled
} else {
_verificationFlowState.value = VerificationFlowState.Failed
}
declineVerificationLambda()
}
fun triggerReceiveVerificationData(sessionVerificationData: SessionVerificationData) {
_verificationFlowState.value = VerificationFlowState.ReceivedVerificationData(sessionVerificationData)
override suspend fun startVerification() {
startVerificationLambda()
}
override suspend fun startVerification() {
_verificationFlowState.value = VerificationFlowState.StartedSasVerification
override suspend fun reset(cancelAnyPendingVerificationAttempt: Boolean) {
resetLambda(cancelAnyPendingVerificationAttempt)
}
fun givenVerifiedStatus(status: SessionVerifiedStatus) {
_sessionVerifiedStatus.value = status
var listener: SessionVerificationServiceListener? = null
private set
override fun setListener(listener: SessionVerificationServiceListener?) {
this.listener = listener
}
suspend fun emitVerifiedStatus(status: SessionVerifiedStatus) {
_sessionVerifiedStatus.emit(status)
override suspend fun acknowledgeVerificationRequest(details: SessionVerificationRequestDetails) {
acknowledgeVerificationRequestLambda(details)
}
fun givenVerificationFlowState(state: VerificationFlowState) {
_verificationFlowState.value = state
override suspend fun acceptVerificationRequest() = simulateLongTask {
acceptVerificationRequestLambda()
}
fun givenNeedsSessionVerification(needsVerification: Boolean) {
_needsSessionVerification.value = needsVerification
suspend fun emitVerificationFlowState(state: VerificationFlowState) {
_verificationFlowState.emit(state)
}
suspend fun emitVerifiedStatus(status: SessionVerifiedStatus) {
_sessionVerifiedStatus.emit(status)
}
override suspend fun reset() {
_verificationFlowState.value = VerificationFlowState.Initial
suspend fun emitNeedsSessionVerification(needsVerification: Boolean) {
_needsSessionVerification.emit(needsVerification)
}
}

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Day_0_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f18cc4f6c04a59e1a5d51a091efe66bfa7525e2fe229302174fe40fefa00cc28
size 14026

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming.ui_SessionDetailsView_Night_0_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d82fea268b5a9271497a3b6518a88c757e6ba6f62f4a8990491351509383858a
size 13974

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_0_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f781e977784d8bfb1bd84947519f31e28106cdc036c6dedc406be92fdbbc0c54
size 40077

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_1_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dbf6f78ad928bcc9878e546345a98d334ae92c9b81d4d8404892a16d19b446c3
size 41534

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_2_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_2_en.png

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_3_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dfc69dc6d93a62e23df2f817ad5a167b1e94d7fb0d408d6ec0051d666b6cf175
size 44869

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_6_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_4_en.png

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_5_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:81c559a9661b3fdccc8deb444b897f9a45b33de0d4d58367ff021f92202a17b6
size 21883

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_6_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d3340a2d29e6c1d86f5ec5664179cae9501fcc95381311174d7d6b45b15af326
size 24123

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Day_7_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:bee1454db757b0897ae2113d3a11ed9adef0eb6bc52f3775edac56ed8533a88f
size 24076

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_0_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:68b70ae5220e244acdfa3f1a2e36089c1994e8f05eeb6c344b4734858540e55c
size 38942

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_1_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b76212b5942484621b7a58044a958203d838807d50687e8f4e2f9c8bdb6ad37c
size 40232

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_2_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_2_en.png

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_3_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1033af0fc84e2819509fc798c17e8f0b07d74a08da99d0e059b7ff19db2ce56a
size 43674

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_6_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_4_en.png

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_5_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:8565e90948aa18f04e1b868467a007d68fe3d18d534289d5c4d59d4b38915585
size 21190

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_6_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c6c7e8cdf40bdf018931635565ac649fed518140a047a71cf70cf63e9edf54d3
size 23932

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.incoming_IncomingVerificationView_Night_7_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:59a23ec5e3086349d3382f006cf1bcb4121183c83ea8c749aa95e3160561aef1
size 23524

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_0_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_0_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_10_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_10_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_11_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_11_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_12_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_12_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_1_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_1_en.png

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_2_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c238637a0a3107cfcd98230ac52fad7779d8c260b0b97bf30d231cd5493a944a
size 46453

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_3_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_3_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_4_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_4_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_5_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_5_en.png

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_6_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a25f85925a38d02a65af4fb342949e9545d3e5e19012885f2c3aee4caa8b8f7d
size 31535

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_7_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_7_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_8_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_8_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Day_9_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Day_9_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_0_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_0_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_10_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_10_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_11_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_11_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_12_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_12_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_1_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_1_en.png

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_2_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc4cbb2bea7749bda9f6c5647ff178717711bad5b1ee25621155bccb1e5f2328
size 45528

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_3_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_3_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_4_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_4_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_5_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_5_en.png

3
tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_6_en.png

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:9ff1487c8c63a476a570d271f7a0b00c8990e1f90d49a2cae5056a9cc7e51e0e
size 30765

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_7_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_7_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_8_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_8_en.png

0
tests/uitests/src/test/snapshots/images/features.verifysession.impl_VerifySelfSessionView_Night_9_en.png → tests/uitests/src/test/snapshots/images/features.verifysession.impl.outgoing_VerifySelfSessionView_Night_9_en.png

Loading…
Cancel
Save