Handling incoming verification

Fix SAS state signaling problem, more tests, back navigation
This commit is contained in:
valere 2023-01-09 09:19:04 +01:00
parent 8606ac92e1
commit a217ec220f
19 changed files with 529 additions and 163 deletions

View file

@ -2517,6 +2517,7 @@
</string> </string>
<string name="verify_cancelled_notice">Verification has been canceled. You can start verification again.</string> <string name="verify_cancelled_notice">Verification has been canceled. You can start verification again.</string>
<string name="verification_not_found">The verification request was not found. It may have been cancelled, or handled by another session.</string>
<string name="verify_invalid_qr_notice">This QR code looks malformed. Please try to verify with another method.</string> <string name="verify_invalid_qr_notice">This QR code looks malformed. Please try to verify with another method.</string>
<string name="verification_cancelled">Verification Canceled</string> <string name="verification_cancelled">Verification Canceled</string>

View file

@ -1663,16 +1663,22 @@ internal class VerificationActor @AssistedInject constructor(
dispatchUpdate(VerificationEvent.TransactionUpdated(it)) dispatchUpdate(VerificationEvent.TransactionUpdated(it))
} }
cancelRequest(request.requestId, request.roomId, request.otherUserId, request.otherDeviceId(), code) cancelRequest(
request.requestId,
request.roomId,
request.otherUserId,
request.otherDeviceId()?.let { listOf(it) } ?: request.targetDevices ?: emptyList(),
code
)
} }
private suspend fun cancelRequest(transactionId: String, roomId: String?, otherUserId: String?, otherDeviceId: String?, code: CancelCode) { private suspend fun cancelRequest(transactionId: String, roomId: String?, otherUserId: String?, otherDeviceIds: List<String>, code: CancelCode) {
try { try {
if (roomId == null) { if (roomId == null) {
cancelTransactionToDevice( cancelTransactionToDevice(
transactionId, transactionId,
otherUserId.orEmpty(), otherUserId.orEmpty(),
otherDeviceId.orEmpty(), otherDeviceIds,
code code
) )
} else { } else {
@ -1688,7 +1694,7 @@ internal class VerificationActor @AssistedInject constructor(
} }
} }
private suspend fun cancelTransactionToDevice(transactionId: String, otherUserId: String, otherUserDeviceId: String?, code: CancelCode) { private suspend fun cancelTransactionToDevice(transactionId: String, otherUserId: String, otherUserDeviceIds: List<String>, code: CancelCode) {
Timber.d("## SAS canceling transaction $transactionId for reason $code") Timber.d("## SAS canceling transaction $transactionId for reason $code")
val cancelMessage = KeyVerificationCancel.create(transactionId, code) val cancelMessage = KeyVerificationCancel.create(transactionId, code)
// val contentMap = MXUsersDevicesMap<Any>() // val contentMap = MXUsersDevicesMap<Any>()
@ -1697,7 +1703,7 @@ internal class VerificationActor @AssistedInject constructor(
messageType = EventType.KEY_VERIFICATION_CANCEL, messageType = EventType.KEY_VERIFICATION_CANCEL,
toSendToDeviceObject = cancelMessage, toSendToDeviceObject = cancelMessage,
otherUserId = otherUserId, otherUserId = otherUserId,
targetDevices = otherUserDeviceId?.let { listOf(it) } ?: emptyList() targetDevices = otherUserDeviceIds
) )
// sendToDeviceTask // sendToDeviceTask
// .execute(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap)) // .execute(SendToDeviceTask.Params(EventType.KEY_VERIFICATION_CANCEL, contentMap))

View file

@ -93,7 +93,7 @@ internal class VerificationTransportLayer @Inject constructor(
} }
suspend fun sendToDeviceEvent(messageType: String, toSendToDeviceObject: SendToDeviceObject, otherUserId: String, targetDevices: List<String>) { suspend fun sendToDeviceEvent(messageType: String, toSendToDeviceObject: SendToDeviceObject, otherUserId: String, targetDevices: List<String>) {
// TODO currently to device verification messages are sent unencrypted // currently to device verification messages are sent unencrypted
// as per spec not recommended // as per spec not recommended
// > verification messages may be sent unencrypted, though this is not encouraged. // > verification messages may be sent unencrypted, though this is not encouraged.

View file

@ -44,15 +44,15 @@ internal class VerificationListenersHolder @Inject constructor(
} }
fun dispatchTxUpdated(tx: VerificationTransaction) { fun dispatchTxUpdated(tx: VerificationTransaction) {
Timber.v("## SAS dispatchTxUpdated txId:${tx.transactionId} $tx")
scope.launch { scope.launch {
Timber.v("## SAS dispatchTxUpdated txId:${tx.transactionId} $tx")
eventFlow.emit(VerificationEvent.TransactionUpdated(tx)) eventFlow.emit(VerificationEvent.TransactionUpdated(tx))
} }
} }
fun dispatchRequestAdded(verificationRequest: PendingVerificationRequest) { fun dispatchRequestAdded(verificationRequest: PendingVerificationRequest) {
Timber.v("## SAS dispatchRequestAdded txId:${verificationRequest.transactionId} $verificationRequest")
scope.launch { scope.launch {
Timber.v("## SAS dispatchRequestAdded txId:${verificationRequest.transactionId} $verificationRequest")
eventFlow.emit(VerificationEvent.RequestAdded(verificationRequest)) eventFlow.emit(VerificationEvent.RequestAdded(verificationRequest))
} }
} }

View file

@ -588,7 +588,7 @@ internal class RustCryptoService @Inject constructor(
// Notify the our listeners about room keys so decryption is retried. // Notify the our listeners about room keys so decryption is retried.
toDeviceEvents.events.orEmpty().forEach { event -> toDeviceEvents.events.orEmpty().forEach { event ->
Timber.tag(loggerTag.value).d("Processed ToDevice event msgid:${event.toDeviceTracingId()}") Timber.tag(loggerTag.value).d("Processed ToDevice event msgid:${event.toDeviceTracingId()} id:${event.eventId} type:${event.type}")
if (event.getClearType() == EventType.ENCRYPTED) { if (event.getClearType() == EventType.ENCRYPTED) {
// rust failed to decrypt it // rust failed to decrypt it

View file

@ -29,7 +29,6 @@ import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionStat
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf import org.matrix.android.sdk.api.session.crypto.verification.safeValueOf
import org.matrix.android.sdk.internal.crypto.OlmMachine
import org.matrix.android.sdk.internal.crypto.network.RequestSender import org.matrix.android.sdk.internal.crypto.network.RequestSender
import org.matrix.rustcomponents.sdk.crypto.CryptoStoreException import org.matrix.rustcomponents.sdk.crypto.CryptoStoreException
import org.matrix.rustcomponents.sdk.crypto.Sas import org.matrix.rustcomponents.sdk.crypto.Sas
@ -39,7 +38,7 @@ import org.matrix.rustcomponents.sdk.crypto.SasState
/** Class representing a short auth string verification flow */ /** Class representing a short auth string verification flow */
internal class SasVerification @AssistedInject constructor( internal class SasVerification @AssistedInject constructor(
@Assisted private var inner: Sas, @Assisted private var inner: Sas,
private val olmMachine: OlmMachine, // private val olmMachine: OlmMachine,
private val sender: RequestSender, private val sender: RequestSender,
private val coroutineDispatchers: MatrixCoroutineDispatchers, private val coroutineDispatchers: MatrixCoroutineDispatchers,
private val verificationListenersHolder: VerificationListenersHolder, private val verificationListenersHolder: VerificationListenersHolder,
@ -254,6 +253,7 @@ internal class SasVerification @AssistedInject constructor(
override fun onChange(state: SasState) { override fun onChange(state: SasState) {
innerState = state innerState = state
verificationListenersHolder.dispatchTxUpdated(this)
} }
override fun toString(): String { override fun toString(): String {

View file

@ -23,6 +23,7 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
import org.matrix.android.sdk.api.extensions.tryOrNull import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.crypto.verification.CancelCode
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
@ -260,7 +261,11 @@ internal class VerificationRequest @AssistedInject constructor(
private fun state(): EVerificationState { private fun state(): EVerificationState {
if (innerVerificationRequest.isCancelled()) { if (innerVerificationRequest.isCancelled()) {
return EVerificationState.Cancelled return if (innerVerificationRequest.cancelInfo()?.cancelCode == CancelCode.AcceptedByAnotherDevice.value) {
EVerificationState.HandledByOtherSession
} else {
EVerificationState.Cancelled
}
} }
if (innerVerificationRequest.isPassive()) { if (innerVerificationRequest.isPassive()) {
return EVerificationState.HandledByOtherSession return EVerificationState.HandledByOtherSession

View file

@ -17,21 +17,36 @@
package im.vector.app package im.vector.app
import android.net.Uri import android.net.Uri
import android.view.View
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import im.vector.app.ui.robot.ElementRobot import androidx.test.espresso.Espresso
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.matcher.ViewMatchers
import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.espresso.tools.waitUntilViewVisible
import im.vector.app.features.home.HomeActivity
import im.vector.app.ui.robot.AnalyticsRobot
import im.vector.app.ui.robot.OnboardingRobot import im.vector.app.ui.robot.OnboardingRobot
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import org.hamcrest.CoreMatchers
import org.junit.Assert import org.junit.Assert
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.registration.RegistrationResult import org.matrix.android.sdk.api.auth.registration.RegistrationResult
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.getRequest
import org.matrix.android.sdk.api.session.sync.SyncState import org.matrix.android.sdk.api.session.sync.SyncState
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -42,7 +57,8 @@ abstract class VerificationTestBase {
val homeServerUrl: String = "http://10.0.2.2:8080" val homeServerUrl: String = "http://10.0.2.2:8080"
protected val uiTestBase = OnboardingRobot() protected val uiTestBase = OnboardingRobot()
protected val elementRobot = ElementRobot()
protected val testScope = CoroutineScope(SupervisorJob())
fun createAccountAndSync( fun createAccountAndSync(
matrix: Matrix, matrix: Matrix,
@ -136,4 +152,67 @@ abstract class VerificationTestBase {
lock.await(20_000, TimeUnit.MILLISECONDS) lock.await(20_000, TimeUnit.MILLISECONDS)
} }
protected fun loginAndClickVerifyToast(userId: String): Session {
uiTestBase.login(userId = userId, password = password, homeServerUrl = homeServerUrl)
tryOrNull {
val analyticsRobot = AnalyticsRobot()
analyticsRobot.optOut()
}
waitUntilActivityVisible<HomeActivity> {
waitUntilViewVisible(ViewMatchers.withId(R.id.roomListContainer))
}
val activity = EspressoHelper.getCurrentActivity()!!
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
withIdlingResource(initialSyncIdlingResource(uiSession)) {
waitUntilViewVisible(ViewMatchers.withId(R.id.roomListContainer))
}
// THIS IS THE ONLY WAY I FOUND TO CLICK ON ALERTERS... :(
// Cannot wait for view because of alerter animation? ...
Espresso.onView(ViewMatchers.isRoot())
.perform(waitForView(ViewMatchers.withId(com.tapadoo.alerter.R.id.llAlertBackground)))
Thread.sleep(1000)
val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)
activity.runOnUiThread {
popup.performClick()
}
Espresso.onView(ViewMatchers.isRoot())
.perform(waitForView(ViewMatchers.withId(R.id.bottomSheetFragmentContainer)))
Espresso.onView(ViewMatchers.withText(R.string.verification_verify_identity))
.check(ViewAssertions.matches(ViewMatchers.isDisplayed()))
// 4S is not setup so passphrase option should be hidden
Espresso.onView(ViewMatchers.withId(R.id.bottomSheetVerificationRecyclerView))
.check(ViewAssertions.matches(CoreMatchers.not(ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.verification_cannot_access_other_session)))))
Espresso.onView(ViewMatchers.withId(R.id.bottomSheetVerificationRecyclerView))
.check(ViewAssertions.matches(ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.verification_verify_with_another_device))))
Espresso.onView(ViewMatchers.withId(R.id.bottomSheetVerificationRecyclerView))
.check(ViewAssertions.matches(ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.bad_passphrase_key_reset_all_action))))
return uiSession
}
protected fun deferredRequestUntil(session: Session, block: ((PendingVerificationRequest) -> Boolean)): CompletableDeferred<PendingVerificationRequest> {
val completableDeferred = CompletableDeferred<PendingVerificationRequest>()
testScope.launch {
session.cryptoService().verificationService().requestEventFlow().collect {
val request = it.getRequest()
if (request != null && block(request)) {
completableDeferred.complete(request)
return@collect cancel()
}
}
}
return completableDeferred
}
} }

View file

@ -16,38 +16,24 @@
package im.vector.app package im.vector.app
import android.view.View import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.IdlingResource
import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItem
import androidx.test.espresso.matcher.ViewMatchers.hasDescendant import androidx.test.espresso.matcher.ViewMatchers.hasDescendant
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.isRoot
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.espresso.matcher.ViewMatchers.withText import androidx.test.espresso.matcher.ViewMatchers.withText
import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest import androidx.test.filters.LargeTest
import im.vector.app.core.utils.getMatrixInstance import im.vector.app.core.utils.getMatrixInstance
import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.espresso.tools.waitUntilViewVisible
import im.vector.app.features.MainActivity import im.vector.app.features.MainActivity
import im.vector.app.features.home.HomeActivity
import im.vector.app.ui.robot.AnalyticsRobot
import im.vector.app.ui.robot.ElementRobot import im.vector.app.ui.robot.ElementRobot
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.internal.assertEquals
import org.hamcrest.CoreMatchers.not import org.junit.After
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Before import org.junit.Before
import org.junit.Rule import org.junit.Rule
@ -58,12 +44,8 @@ import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.SasTransactionState
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod import org.matrix.android.sdk.api.session.crypto.verification.VerificationMethod
import org.matrix.android.sdk.api.session.crypto.verification.getRequest
import org.matrix.android.sdk.api.session.crypto.verification.getTransaction
import kotlin.coroutines.Continuation import kotlin.coroutines.Continuation
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.random.Random import kotlin.random.Random
@ -77,8 +59,6 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
@get:Rule @get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java) val activityRule = ActivityScenarioRule(MainActivity::class.java)
private val testScope = CoroutineScope(SupervisorJob())
@Before @Before
fun createSessionWithCrossSigning() { fun createSessionWithCrossSigning() {
val matrix = getMatrixInstance() val matrix = getMatrixInstance()
@ -102,60 +82,33 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
} }
} }
@After
fun cleanUp() {
runTest {
existingSession?.signOutService()?.signOut(true)
}
val app = EspressoHelper.getCurrentActivity()!!.application as VectorApplication
while (app.authenticationService.getLastAuthenticatedSession() != null) {
val session = app.authenticationService.getLastAuthenticatedSession()!!
runTest {
session.signOutService().signOut(true)
}
}
val activity = EspressoHelper.getCurrentActivity()!!
val editor = PreferenceManager.getDefaultSharedPreferences(activity).edit()
editor.clear()
editor.commit()
}
@Test @Test
fun checkVerifyPopup() { fun checkVerifyPopup() {
val userId: String = existingSession!!.myUserId val userId: String = existingSession!!.myUserId
uiTestBase.login(userId = userId, password = password, homeServerUrl = homeServerUrl) val uiSession = loginAndClickVerifyToast(userId)
val analyticsRobot = AnalyticsRobot() val otherRequest = deferredRequestUntil(existingSession!!) {
analyticsRobot.optOut() true
waitUntilActivityVisible<HomeActivity> {
waitUntilViewVisible(withId(R.id.roomListContainer))
}
val activity = EspressoHelper.getCurrentActivity()!!
val uiSession = (activity as HomeActivity).activeSessionHolder.getActiveSession()
withIdlingResource(initialSyncIdlingResource(uiSession)) {
waitUntilViewVisible(withId(R.id.roomListContainer))
}
// THIS IS THE ONLY WAY I FOUND TO CLICK ON ALERTERS... :(
// Cannot wait for view because of alerter animation? ...
onView(isRoot())
.perform(waitForView(withId(com.tapadoo.alerter.R.id.llAlertBackground)))
Thread.sleep(1000)
val popup = activity.findViewById<View>(com.tapadoo.alerter.R.id.llAlertBackground)
activity.runOnUiThread {
popup.performClick()
}
onView(isRoot())
.perform(waitForView(withId(R.id.bottomSheetFragmentContainer)))
onView(withText(R.string.verification_verify_identity))
.check(matches(isDisplayed()))
// 4S is not setup so passphrase option should be hidden
onView(withId(R.id.bottomSheetVerificationRecyclerView))
.check(matches(not(hasDescendant(withText(R.string.verification_cannot_access_other_session)))))
onView(withId(R.id.bottomSheetVerificationRecyclerView))
.check(matches(hasDescendant(withText(R.string.verification_verify_with_another_device))))
onView(withId(R.id.bottomSheetVerificationRecyclerView))
.check(matches(hasDescendant(withText(R.string.bad_passphrase_key_reset_all_action))))
val otherRequest = CompletableDeferred<PendingVerificationRequest>()
testScope.launch {
existingSession!!.cryptoService().verificationService().requestEventFlow().collect {
if (it.getRequest() != null) {
otherRequest.complete(it.getRequest()!!)
return@collect cancel()
}
}
} }
// Send out a self verification request // Send out a self verification request
@ -261,55 +214,4 @@ class VerifySessionInteractiveTest : VerificationTestBase() {
ElementRobot().signout(false) ElementRobot().signout(false)
} }
fun signout() {
onView(withId(R.id.groupToolbarAvatarImageView))
.perform(click())
onView(withId(R.id.homeDrawerHeaderSettingsView))
.perform(click())
onView(withText("General"))
.perform(click())
}
fun verificationStateIdleResource(transactionId: String, checkForState: SasTransactionState, session: Session): IdlingResource {
val scope = CoroutineScope(SupervisorJob())
val idle = object : IdlingResource {
private var callback: IdlingResource.ResourceCallback? = null
private var currentState: SasTransactionState? = null
override fun getName() = "verificationSuccessIdle"
override fun isIdleNow(): Boolean {
return currentState == checkForState
}
override fun registerIdleTransitionCallback(callback: IdlingResource.ResourceCallback?) {
this.callback = callback
}
fun update(state: SasTransactionState) {
currentState = state
if (state == checkForState) {
callback?.onTransitionToIdle()
scope.cancel()
}
}
}
session.cryptoService().verificationService()
.requestEventFlow()
.filter {
it.transactionId == transactionId
}
.onEach {
(it.getTransaction() as? SasVerificationTransaction)?.state()?.let {
idle.update(it)
}
}.launchIn(scope)
return idle
}
} }

View file

@ -0,0 +1,136 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.app
import androidx.recyclerview.widget.RecyclerView
import androidx.test.espresso.Espresso
import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions
import androidx.test.espresso.contrib.RecyclerViewActions
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.ext.junit.rules.ActivityScenarioRule
import im.vector.app.core.utils.getMatrixInstance
import im.vector.app.espresso.tools.waitUntilActivityVisible
import im.vector.app.espresso.tools.waitUntilViewVisible
import im.vector.app.features.MainActivity
import im.vector.app.features.home.HomeActivity
import im.vector.app.ui.robot.ElementRobot
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.matrix.android.sdk.api.auth.UIABaseAuth
import org.matrix.android.sdk.api.auth.UserInteractiveAuthInterceptor
import org.matrix.android.sdk.api.auth.UserPasswordAuth
import org.matrix.android.sdk.api.auth.registration.RegistrationFlowResponse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import kotlin.coroutines.Continuation
import kotlin.coroutines.resume
import kotlin.random.Random
class VerifySessionNavigationTest : VerificationTestBase() {
var existingSession: Session? = null
@get:Rule
val activityRule = ActivityScenarioRule(MainActivity::class.java)
@Before
fun createSessionWithCrossSigning() {
val matrix = getMatrixInstance()
val userName = "foobar_${Random.nextLong()}"
existingSession = createAccountAndSync(matrix, userName, password, true)
runTest {
existingSession!!.cryptoService().crossSigningService()
.initializeCrossSigning(
object : UserInteractiveAuthInterceptor {
override fun performStage(flowResponse: RegistrationFlowResponse, errCode: String?, promise: Continuation<UIABaseAuth>) {
promise.resume(
UserPasswordAuth(
user = existingSession!!.myUserId,
password = "password",
session = flowResponse.session
)
)
}
}
)
}
}
@After
fun cleanUp() {
runTest {
existingSession?.signOutService()?.signOut(true)
}
}
@Test
fun testStartThenCancelRequest() {
val userId: String = existingSession!!.myUserId
loginAndClickVerifyToast(userId)
val otherRequest = deferredRequestUntil(existingSession!!) {
true
}
// Send out a self verification request
Espresso.onView(ViewMatchers.withId(R.id.bottomSheetVerificationRecyclerView))
.perform(
RecyclerViewActions.actionOnItem<RecyclerView.ViewHolder>(
ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.verification_verify_with_another_device)),
ViewActions.click()
)
)
Espresso.onView(ViewMatchers.withId(R.id.bottomSheetVerificationRecyclerView))
.check(ViewAssertions.matches(ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.verification_request_was_sent))))
val txId = runBlockingTest {
otherRequest.await().transactionId
}
// if we press back it should cancel
val otherGetCancelledRequest = deferredRequestUntil(existingSession!!) {
it.transactionId == txId && it.state == EVerificationState.Cancelled
}
Espresso.pressBack()
// Should go back to main verification options
Espresso.onView(ViewMatchers.isRoot())
.perform(waitForView(ViewMatchers.withId(R.id.bottomSheetFragmentContainer)))
Espresso.onView(ViewMatchers.withId(R.id.bottomSheetVerificationRecyclerView))
.check(ViewAssertions.matches(ViewMatchers.hasDescendant(ViewMatchers.withText(R.string.verification_verify_with_another_device))))
runBlockingTest {
otherGetCancelledRequest.await()
}
Espresso.pressBack()
waitUntilActivityVisible<HomeActivity> {
waitUntilViewVisible(ViewMatchers.withId(R.id.roomListContainer))
}
ElementRobot().signout(false)
}
}

View file

@ -224,6 +224,7 @@ class IncomingVerificationRequestHandler @Inject constructor(
override fun verificationRequestUpdated(pr: PendingVerificationRequest) { override fun verificationRequestUpdated(pr: PendingVerificationRequest) {
// If an incoming request is readied (by another device?) we should discard the alert // If an incoming request is readied (by another device?) we should discard the alert
if (pr.isIncoming && (pr.state == EVerificationState.HandledByOtherSession || if (pr.isIncoming && (pr.state == EVerificationState.HandledByOtherSession ||
pr.state == EVerificationState.Cancelled ||
pr.state == EVerificationState.Started || pr.state == EVerificationState.Started ||
pr.state == EVerificationState.WeStarted)) { pr.state == EVerificationState.WeStarted)) {
popupAlertManager.cancelAlert(uniqueIdForVerificationRequest(pr)) popupAlertManager.cancelAlert(uniqueIdForVerificationRequest(pr))

View file

@ -38,4 +38,5 @@ sealed class VerificationAction : VectorViewModelAction {
data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction() data class GotResultFromSsss(val cypherData: String, val alias: String) : VerificationAction()
object CancelledFromSsss : VerificationAction() object CancelledFromSsss : VerificationAction()
object SecuredStorageHasBeenReset : VerificationAction() object SecuredStorageHasBeenReset : VerificationAction()
object SelfVerificationWasNotMe : VerificationAction()
} }

View file

@ -23,8 +23,10 @@ import im.vector.app.core.platform.VectorViewEvents
*/ */
sealed class VerificationBottomSheetViewEvents : VectorViewEvents { sealed class VerificationBottomSheetViewEvents : VectorViewEvents {
object Dismiss : VerificationBottomSheetViewEvents() object Dismiss : VerificationBottomSheetViewEvents()
object DismissAndOpenDeviceSettings : VerificationBottomSheetViewEvents()
object AccessSecretStore : VerificationBottomSheetViewEvents() object AccessSecretStore : VerificationBottomSheetViewEvents()
object ResetAll : VerificationBottomSheetViewEvents() object ResetAll : VerificationBottomSheetViewEvents()
object GoToSettings : VerificationBottomSheetViewEvents() object GoToSettings : VerificationBottomSheetViewEvents()
data class ModalError(val errorMessage: CharSequence) : VerificationBottomSheetViewEvents() data class ModalError(val errorMessage: CharSequence) : VerificationBottomSheetViewEvents()
data class RequestNotFound(val transactionId: String) : VerificationBottomSheetViewEvents()
} }

View file

@ -17,8 +17,10 @@
package im.vector.app.features.crypto.verification.self package im.vector.app.features.crypto.verification.self
import android.app.Activity import android.app.Activity
import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.os.Parcelable import android.os.Parcelable
import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -30,6 +32,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.commitTransaction import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.registerStartForActivityResult
import im.vector.app.core.extensions.singletonEntryPoint
import im.vector.app.core.extensions.toMvRxBundle import im.vector.app.core.extensions.toMvRxBundle
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.databinding.BottomSheetVerificationBinding import im.vector.app.databinding.BottomSheetVerificationBinding
@ -37,6 +40,7 @@ import im.vector.app.features.crypto.quads.SharedSecureStorageActivity
import im.vector.app.features.crypto.quads.SharedSecureStorageViewState import im.vector.app.features.crypto.quads.SharedSecureStorageViewState
import im.vector.app.features.crypto.verification.VerificationAction import im.vector.app.features.crypto.verification.VerificationAction
import im.vector.app.features.crypto.verification.VerificationBottomSheetViewEvents import im.vector.app.features.crypto.verification.VerificationBottomSheetViewEvents
import im.vector.app.features.settings.VectorSettingsActivity
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.MASTER_KEY_SSSS_NAME
@ -58,6 +62,11 @@ class SelfVerificationBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSh
private val viewModel by fragmentViewModel(SelfVerificationViewModel::class) private val viewModel by fragmentViewModel(SelfVerificationViewModel::class)
init {
// we manage dismiss/back manually as verification could be required
isCancelable = false
}
override fun getBinding( override fun getBinding(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup? container: ViewGroup?
@ -85,6 +94,23 @@ class SelfVerificationBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSh
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
observeViewModelEvents()
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return super.onCreateDialog(savedInstanceState).apply {
setOnKeyListener { _, keyCode, keyEvent ->
if (keyCode == KeyEvent.KEYCODE_BACK && keyEvent.action == KeyEvent.ACTION_UP) {
viewModel.queryCancel()
true
} else {
false
}
}
}
}
private fun observeViewModelEvents() {
viewModel.observeViewEvents { event -> viewModel.observeViewEvents { event ->
when (event) { when (event) {
VerificationBottomSheetViewEvents.AccessSecretStore -> { VerificationBottomSheetViewEvents.AccessSecretStore -> {
@ -122,6 +148,16 @@ class SelfVerificationBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSh
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
VerificationBottomSheetViewEvents.DismissAndOpenDeviceSettings -> {
dismiss()
requireActivity().singletonEntryPoint().navigator().openSettings(
requireActivity(),
VectorSettingsActivity.EXTRA_DIRECT_ACCESS_SECURITY_PRIVACY_MANAGE_SESSIONS
)
}
is VerificationBottomSheetViewEvents.RequestNotFound -> {
dismiss()
}
} }
} }
} }

View file

@ -58,8 +58,19 @@ class SelfVerificationController @Inject constructor(
fun onClickResetSecurity() fun onClickResetSecurity()
fun onDoneFrom4S() fun onDoneFrom4S()
fun keysNotIn4S() fun keysNotIn4S()
fun confirmCancelRequest(confirm: Boolean)
/**
* An incoming request, when I am the verified request
*/
fun wasNotMe()
} }
private val selfVerificationListener: InteractionListener?
get() {
return listener as? InteractionListener
}
var state: SelfVerificationViewState? = null var state: SelfVerificationViewState? = null
fun update(state: SelfVerificationViewState) { fun update(state: SelfVerificationViewState) {
@ -70,6 +81,18 @@ class SelfVerificationController @Inject constructor(
override fun buildModels() { override fun buildModels() {
val state = this.state ?: return val state = this.state ?: return
// actions have priority
if (state.activeAction !is UserAction.None) {
when (state.activeAction) {
UserAction.ConfirmCancel -> {
renderConfirmCancel(state)
}
UserAction.None -> {}
}
return
}
when (state.pendingRequest) { when (state.pendingRequest) {
Uninitialized -> { Uninitialized -> {
renderBaseNoActiveRequest(state) renderBaseNoActiveRequest(state)
@ -80,6 +103,50 @@ class SelfVerificationController @Inject constructor(
} }
} }
private fun renderConfirmCancel(state: SelfVerificationViewState) {
val host = this
if (state.currentDeviceCanCrossSign) {
bottomSheetVerificationNoticeItem {
id("notice")
notice(host.stringProvider.getString(R.string.verify_cancel_self_verification_from_trusted).toEpoxyCharSequence())
}
} else {
bottomSheetVerificationNoticeItem {
id("notice")
notice(host.stringProvider.getString(R.string.verify_cancel_self_verification_from_untrusted).toEpoxyCharSequence())
}
}
bottomSheetDividerItem {
id("sep0")
}
bottomSheetVerificationActionItem {
id("cancel")
title(host.stringProvider.getString(R.string.action_skip))
titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
iconRes(R.drawable.ic_arrow_right)
iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
listener {
// host.listener?.confirmCancelRequest(true)
}
}
bottomSheetDividerItem {
id("sep1")
}
bottomSheetVerificationActionItem {
id("continue")
title(host.stringProvider.getString(R.string._continue))
titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary))
iconRes(R.drawable.ic_arrow_right)
iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorPrimary))
listener {
// host.listener?.confirmCancelRequest(false)
}
}
}
private fun renderBaseNoActiveRequest(state: SelfVerificationViewState) { private fun renderBaseNoActiveRequest(state: SelfVerificationViewState) {
if (state.verifyingFrom4SAction !is Uninitialized) { if (state.verifyingFrom4SAction !is Uninitialized) {
render4SCheckState(state) render4SCheckState(state)
@ -142,15 +209,16 @@ class SelfVerificationController @Inject constructor(
EVerificationState.Requested -> { EVerificationState.Requested -> {
// add accept buttons? // add accept buttons?
renderAcceptDeclineRequest() renderAcceptDeclineRequest()
bottomSheetVerificationActionItem { if (state.isThisSessionVerified) {
id("not me") bottomSheetVerificationActionItem {
title(host.stringProvider.getString(R.string.verify_new_session_was_not_me)) id("not me")
titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) title(host.stringProvider.getString(R.string.verify_new_session_was_not_me))
iconRes(R.drawable.ic_arrow_right) titleColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) iconRes(R.drawable.ic_arrow_right)
listener { iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
TODO() listener {
// host.listener?.wasNotMe() host.selfVerificationListener?.wasNotMe()
}
} }
} }
} }
@ -172,14 +240,27 @@ class SelfVerificationController @Inject constructor(
} }
EVerificationState.Cancelled -> { EVerificationState.Cancelled -> {
renderCancel(pendingRequest.cancelConclusion ?: CancelCode.User) renderCancel(pendingRequest.cancelConclusion ?: CancelCode.User)
gotIt {
listener?.onDone(false)
}
} }
EVerificationState.HandledByOtherSession -> { EVerificationState.HandledByOtherSession -> {
// we should dismiss // we should dismiss
renderCancel(pendingRequest.cancelConclusion ?: CancelCode.User)
gotIt {
listener?.onDone(false)
}
} }
} }
} }
is Fail -> { is Fail -> {
// TODO bottomSheetVerificationNoticeItem {
id("request_fail")
notice(host.stringProvider.getString(R.string.verification_not_found).toEpoxyCharSequence())
}
gotIt {
listener?.onDone(false)
}
} }
} }
} }
@ -202,7 +283,7 @@ class SelfVerificationController @Inject constructor(
// subTitle(host.stringProvider.getString(R.string.verification_request_start_notice)) // subTitle(host.stringProvider.getString(R.string.verification_request_start_notice))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
listener { (host.listener as? InteractionListener)?.onClickOnVerificationStart() } listener { host.selfVerificationListener?.onClickOnVerificationStart() }
} }
bottomSheetDividerItem { bottomSheetDividerItem {
@ -218,7 +299,7 @@ class SelfVerificationController @Inject constructor(
subTitle(host.stringProvider.getString(R.string.verification_use_passphrase)) subTitle(host.stringProvider.getString(R.string.verification_use_passphrase))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
listener { (host.listener as? InteractionListener)?.onClickRecoverFromPassphrase() } listener { host.selfVerificationListener?.onClickRecoverFromPassphrase() }
} }
bottomSheetDividerItem { bottomSheetDividerItem {
@ -234,7 +315,7 @@ class SelfVerificationController @Inject constructor(
subTitle(host.stringProvider.getString(R.string.secure_backup_reset_all_no_other_devices)) subTitle(host.stringProvider.getString(R.string.secure_backup_reset_all_no_other_devices))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary)) iconColor(host.colorProvider.getColorFromAttribute(R.attr.vctr_content_primary))
listener { (host.listener as? InteractionListener)?.onClickResetSecurity() } listener { host.selfVerificationListener?.onClickResetSecurity() }
} }
if (!state.isVerificationRequired) { if (!state.isVerificationRequired) {
@ -248,7 +329,7 @@ class SelfVerificationController @Inject constructor(
titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) titleColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
iconRes(R.drawable.ic_arrow_right) iconRes(R.drawable.ic_arrow_right)
iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError)) iconColor(host.colorProvider.getColorFromAttribute(R.attr.colorError))
listener { (host.listener as? InteractionListener)?.onClickSkip() } listener { host.selfVerificationListener?.onClickSkip() }
} }
} }
} }
@ -268,7 +349,7 @@ class SelfVerificationController @Inject constructor(
val value = action.invoke() val value = action.invoke()
if (value) { if (value) {
verifiedSuccessTile() verifiedSuccessTile()
bottomDone { (host.listener as? InteractionListener)?.onDoneFrom4S() } bottomDone { host.selfVerificationListener?.onDoneFrom4S() }
} else { } else {
bottomSheetVerificationNoticeItem { bottomSheetVerificationNoticeItem {
id("notice_4s_failed'") id("notice_4s_failed'")
@ -279,7 +360,7 @@ class SelfVerificationController @Inject constructor(
.toEpoxyCharSequence() .toEpoxyCharSequence()
) )
} }
gotIt { (host.listener as? InteractionListener)?.keysNotIn4S() } gotIt { host.selfVerificationListener?.keysNotIn4S() }
} }
} }
else -> { else -> {

View file

@ -92,6 +92,14 @@ class SelfVerificationFragment : VectorBaseFragment<BottomSheetVerificationChil
viewModel.handle(VerificationAction.FailedToGetKeysFrom4S) viewModel.handle(VerificationAction.FailedToGetKeysFrom4S)
} }
override fun confirmCancelRequest(confirm: Boolean) {
}
override fun wasNotMe() {
// we just want to cancel and open device settings
viewModel.handle(VerificationAction.SelfVerificationWasNotMe)
}
override fun onClickOnVerificationStart() { override fun onClickOnVerificationStart() {
viewModel.handle(VerificationAction.RequestSelfVerification) viewModel.handle(VerificationAction.RequestSelfVerification)
} }

View file

@ -16,6 +16,7 @@
package im.vector.app.features.crypto.verification.self package im.vector.app.features.crypto.verification.self
import android.content.res.Resources.NotFoundException
import com.airbnb.mvrx.Async import com.airbnb.mvrx.Async
import com.airbnb.mvrx.Fail import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading import com.airbnb.mvrx.Loading
@ -46,6 +47,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.Matrix import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.extensions.tryOrNull
import org.matrix.android.sdk.api.raw.RawService import org.matrix.android.sdk.api.raw.RawService
import org.matrix.android.sdk.api.session.Session import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME import org.matrix.android.sdk.api.session.crypto.crosssigning.KEYBACKUP_SECRET_SSSS_NAME
@ -56,6 +58,7 @@ import org.matrix.android.sdk.api.session.crypto.crosssigning.isVerified
import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupUtils import org.matrix.android.sdk.api.session.crypto.keysbackup.BackupUtils
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult import org.matrix.android.sdk.api.session.crypto.keysbackup.toKeysVersionResult
import org.matrix.android.sdk.api.session.crypto.verification.EVerificationState
import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest import org.matrix.android.sdk.api.session.crypto.verification.PendingVerificationRequest
import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.QrCodeVerificationTransaction
import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction import org.matrix.android.sdk.api.session.crypto.verification.SasVerificationTransaction
@ -65,7 +68,13 @@ import org.matrix.android.sdk.api.session.crypto.verification.getTransaction
import org.matrix.android.sdk.api.util.fromBase64 import org.matrix.android.sdk.api.util.fromBase64
import timber.log.Timber import timber.log.Timber
sealed class UserAction {
object ConfirmCancel : UserAction()
object None : UserAction()
}
data class SelfVerificationViewState( data class SelfVerificationViewState(
val activeAction: UserAction = UserAction.None,
val pendingRequest: Async<PendingVerificationRequest> = Uninitialized, val pendingRequest: Async<PendingVerificationRequest> = Uninitialized,
// need something immutable for state to work properly, VerificationTransaction is not // need something immutable for state to work properly, VerificationTransaction is not
val startedTransaction: Async<VerificationTransactionData> = Uninitialized, val startedTransaction: Async<VerificationTransactionData> = Uninitialized,
@ -110,9 +119,19 @@ class SelfVerificationViewModel @AssistedInject constructor(
copy(pendingRequest = Loading()) copy(pendingRequest = Loading())
} }
viewModelScope.launch { viewModelScope.launch {
session.cryptoService().verificationService().getExistingVerificationRequest(session.myUserId, initialState.transactionId)?.let { val matchingRequest = session.cryptoService().verificationService().getExistingVerificationRequest(session.myUserId, initialState.transactionId)
if (matchingRequest == null) {
// it's unexpected for now dismiss.
// Could happen when you click on the popup for an incoming request
// that has been deleted by the time you clicked on it
// maybe give some feedback?
setState { setState {
copy(pendingRequest = Success(it)) copy(pendingRequest = Fail(NotFoundException()))
}
_viewEvents.post(VerificationBottomSheetViewEvents.RequestNotFound(initialState.transactionId))
} else {
setState {
copy(pendingRequest = Success(matchingRequest))
} }
} }
} }
@ -204,8 +223,13 @@ class SelfVerificationViewModel @AssistedInject constructor(
} }
} }
is VerificationAction.GotItConclusion -> { is VerificationAction.GotItConclusion -> {
// just dismiss withState { state ->
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) if (action.verified || state.isThisSessionVerified) {
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
} else {
queryCancel()
}
}
} }
is VerificationAction.GotResultFromSsss -> handleSecretBackFromSSSS(action) is VerificationAction.GotResultFromSsss -> handleSecretBackFromSSSS(action)
VerificationAction.OtherUserDidNotScanned -> { VerificationAction.OtherUserDidNotScanned -> {
@ -292,7 +316,7 @@ class SelfVerificationViewModel @AssistedInject constructor(
} }
} }
VerificationAction.SkipVerification -> { VerificationAction.SkipVerification -> {
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss) queryCancel()
} }
VerificationAction.ForgotResetAll -> { VerificationAction.ForgotResetAll -> {
_viewEvents.post(VerificationBottomSheetViewEvents.ResetAll) _viewEvents.post(VerificationBottomSheetViewEvents.ResetAll)
@ -324,6 +348,87 @@ class SelfVerificationViewModel @AssistedInject constructor(
VerificationAction.RequestVerificationByDM -> { VerificationAction.RequestVerificationByDM -> {
// not applicable in self verification // not applicable in self verification
} }
VerificationAction.SelfVerificationWasNotMe -> {
// cancel transaction then open settings
withState { state ->
val request = state.pendingRequest.invoke() ?: return@withState
session.coroutineScope.launch {
tryOrNull {
session.cryptoService().verificationService().cancelVerificationRequest(request)
}
}
_viewEvents.post(VerificationBottomSheetViewEvents.DismissAndOpenDeviceSettings)
}
}
}
}
fun queryCancel() = withState { state ->
when (state.pendingRequest) {
is Uninitialized -> {
// No active request currently
if (state.isVerificationRequired) {
// we can't let you dismiss :)
} else {
// verification is not required so just dismiss
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
}
}
is Success -> {
val activeRequest = state.pendingRequest.invoke()
// there is an active request or transaction, we need confirmation to cancel it
if (activeRequest.state == EVerificationState.Cancelled) {
// equivalent of got it?
if (state.isThisSessionVerified) {
// we can always dismiss??
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
} else {
// go back to main verification screen
setState {
copy(pendingRequest = Uninitialized)
}
}
return@withState
} else if (activeRequest.state == EVerificationState.Done) {
_viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
} else {
if (state.isThisSessionVerified) {
// we are the verified session, so just dismiss?
// do we want to prompt confirm??
} else {
// cancel the active request and go back?
setState {
copy(
transactionId = null,
pendingRequest = Uninitialized,
)
}
viewModelScope.launch(Dispatchers.IO) {
session.cryptoService().verificationService().cancelVerificationRequest(
activeRequest
)
setState {
copy(
transactionId = null,
pendingRequest = Uninitialized
)
}
}
}
}
// if (state.isThisSessionVerified) {
// // we can always dismiss??
// _viewEvents.post(VerificationBottomSheetViewEvents.Dismiss)
// }
// setState {
// copy(
// activeAction = UserAction.ConfirmCancel
// )
// }
}
else -> {
// just ignore?
}
} }
} }

View file

@ -90,7 +90,11 @@ class UserVerificationBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSh
.setPositiveButton(R.string.ok, null) .setPositiveButton(R.string.ok, null)
.show() .show()
} }
VerificationBottomSheetViewEvents.ResetAll -> { VerificationBottomSheetViewEvents.ResetAll,
VerificationBottomSheetViewEvents.DismissAndOpenDeviceSettings -> {
// no-op for user verification
}
is VerificationBottomSheetViewEvents.RequestNotFound -> {
// no-op for user verification // no-op for user verification
} }
} }

View file

@ -381,10 +381,9 @@ class UserVerificationViewModel @AssistedInject constructor(
VerificationAction.SkipVerification, VerificationAction.SkipVerification,
VerificationAction.VerifyFromPassphrase, VerificationAction.VerifyFromPassphrase,
VerificationAction.SecuredStorageHasBeenReset, VerificationAction.SecuredStorageHasBeenReset,
VerificationAction.FailedToGetKeysFrom4S -> { VerificationAction.FailedToGetKeysFrom4S,
// Not applicable for user verification VerificationAction.RequestSelfVerification,
} VerificationAction.SelfVerificationWasNotMe,
VerificationAction.RequestSelfVerification -> TODO()
VerificationAction.ForgotResetAll -> { VerificationAction.ForgotResetAll -> {
// Not applicable for user verification // Not applicable for user verification
} }