Add support for manually starting SaS verification flows and accepting remotely started ones

This commit is contained in:
Stefan Ceriu
2023-01-05 18:06:39 +02:00
committed by Stefan Ceriu
parent 7888736a4d
commit 956e92a9ac
13 changed files with 185 additions and 46 deletions
@@ -23,8 +23,6 @@
"session_verification_banner_title" = "Help keep your messages secure";
"session_verification_banner_message" = "Looks like you’re using a new device. Verify its you.";
"session_verification_screen_emojis_title" = "Lets check if these";
"session_verification_screen_emojis_message" = "Open Element on one of your other sessions to compare.";
"server_selection_server_footer" = "You can only connect to a server that has already been set up";
@@ -58,10 +58,6 @@ extension ElementL10n {
public static let sessionVerificationBannerMessage = ElementL10n.tr("Untranslated", "session_verification_banner_message")
/// Help keep your messages secure
public static let sessionVerificationBannerTitle = ElementL10n.tr("Untranslated", "session_verification_banner_title")
/// Open Element on one of your other sessions to compare.
public static let sessionVerificationScreenEmojisMessage = ElementL10n.tr("Untranslated", "session_verification_screen_emojis_message")
/// Lets check if these
public static let sessionVerificationScreenEmojisTitle = ElementL10n.tr("Untranslated", "session_verification_screen_emojis_title")
/// Appearance
public static let settingsAppearance = ElementL10n.tr("Untranslated", "settings_appearance")
/// Timeline Style
@@ -22,22 +22,19 @@ enum SessionVerificationViewModelAction {
struct SessionVerificationViewState: BindableState {
var verificationState: SessionVerificationStateMachine.State = .initial
var title: String? {
switch verificationState {
case .showingChallenge:
return ElementL10n.sessionVerificationScreenEmojisTitle
default:
return nil
}
}
var message: String {
switch verificationState {
case .initial:
return ElementL10n.verificationOpenOtherToVerify
case .requestingVerification:
return ElementL10n.verificationRequestWaiting
case .verificationRequestAccepted:
return ElementL10n.verificationEmojiNotice
case .startingSasVerification:
return ElementL10n.verificationRequestWaiting
case .sasVerificationStarted:
return ElementL10n.verificationRequestWaiting
case .acceptingChallenge:
return ElementL10n.verificationRequestWaiting
case .decliningChallenge:
@@ -45,7 +42,7 @@ struct SessionVerificationViewState: BindableState {
case .cancelling:
return ElementL10n.verificationRequestWaiting
case .showingChallenge:
return ElementL10n.sessionVerificationScreenEmojisMessage
return ElementL10n.verificationCodeNotice
case .verified:
return ElementL10n.verificationConclusionOkSelfNotice
case .cancelled:
@@ -55,7 +52,8 @@ struct SessionVerificationViewState: BindableState {
}
enum SessionVerificationViewAction {
case start
case requestVerification
case startSasVerification
case restart
case accept
case decline
@@ -24,6 +24,12 @@ class SessionVerificationStateMachine {
case initial
/// Waiting for verification acceptance
case requestingVerification
/// Verification request accepted. Waiting for start
case verificationRequestAccepted
/// Waiting for SaS verification start
case startingSasVerification
/// A SaS verification flow has been started
case sasVerificationStarted
/// Verification accepted and emojis received
case showingChallenge(emojis: [SessionVerificationEmoji])
/// Emojis match locally
@@ -42,6 +48,12 @@ class SessionVerificationStateMachine {
enum Event: EventType {
/// Request verification
case requestVerification
/// The current verification request has been accepted
case didAcceptVerificationRequest
/// Start a SaS verification flow
case startSasVerification
/// Started a SaS verification flow
case didStartSasVerification
/// Has received emojis
case didReceiveChallenge(emojis: [SessionVerificationEmoji])
/// Emojis match
@@ -69,15 +81,19 @@ class SessionVerificationStateMachine {
init() {
stateMachine = StateMachine(state: .initial) { machine in
machine.addRoutes(event: .requestVerification, transitions: [.initial => .requestingVerification])
machine.addRoutes(event: .didAcceptVerificationRequest, transitions: [.requestingVerification => .verificationRequestAccepted])
machine.addRoutes(event: .startSasVerification, transitions: [.verificationRequestAccepted => .startingSasVerification])
machine.addRoutes(event: .didFail, transitions: [.requestingVerification => .initial])
machine.addRoutes(event: .restart, transitions: [.cancelled => .initial])
// Transitions with associated values need to be handled through `addRouteMapping`
machine.addRouteMapping { event, fromState, _ in
switch (event, fromState) {
case (.didReceiveChallenge(let emojis), .requestingVerification):
case (.didStartSasVerification, _):
return .sasVerificationStarted
case (.didReceiveChallenge(let emojis), .sasVerificationStarted):
return .showingChallenge(emojis: emojis)
case (.acceptChallenge, .showingChallenge(let emojis)):
return .acceptingChallenge(emojis: emojis)
case (.didFail, .acceptingChallenge(let emojis)):
@@ -100,6 +116,14 @@ class SessionVerificationStateMachine {
return nil
}
}
addTransitionHandler { context in
if let event = context.event {
MXLog.info("Transitioning from `\(context.fromState)` to `\(context.toState)` with event `\(event)`")
} else {
MXLog.info("Transitioning from \(context.fromState)` to `\(context.toState)`")
}
}
}
}
@@ -41,8 +41,12 @@ class SessionVerificationViewModel: SessionVerificationViewModelType, SessionVer
guard let self else { return }
switch callback {
case .acceptedVerificationRequest:
self.stateMachine.processEvent(.didAcceptVerificationRequest)
case .startedSasVerification:
self.stateMachine.processEvent(.didStartSasVerification)
case .receivedVerificationData(let emojis):
guard self.stateMachine.state == .requestingVerification else {
guard self.stateMachine.state == .sasVerificationStarted else {
MXLog.warning("Callbacks: Ignoring receivedVerificationData due to invalid state.")
return
}
@@ -61,8 +65,10 @@ class SessionVerificationViewModel: SessionVerificationViewModelType, SessionVer
override func process(viewAction: SessionVerificationViewAction) async {
switch viewAction {
case .start:
case .requestVerification:
stateMachine.processEvent(.requestVerification)
case .startSasVerification:
stateMachine.processEvent(.startSasVerification)
case .restart:
stateMachine.processEvent(.restart)
case .close:
@@ -92,6 +98,8 @@ class SessionVerificationViewModel: SessionVerificationViewModelType, SessionVer
switch (context.fromState, context.event, context.toState) {
case (.initial, .requestVerification, .requestingVerification):
self.requestVerification()
case (.verificationRequestAccepted, .startSasVerification, .startingSasVerification):
self.startSasVerification()
case (.showingChallenge, .acceptChallenge, .acceptingChallenge):
self.acceptChallenge()
case (.showingChallenge, .declineChallenge, .decliningChallenge):
@@ -131,6 +139,18 @@ class SessionVerificationViewModel: SessionVerificationViewModelType, SessionVer
}
}
private func startSasVerification() {
Task {
switch await sessionVerificationControllerProxy.startSasVerification() {
case .success:
// Need to wait for the callback from the remote
break
case .failure:
stateMachine.processEvent(.didFail)
}
}
}
private func acceptChallenge() {
Task {
switch await sessionVerificationControllerProxy.approveVerification() {
@@ -23,13 +23,6 @@ struct SessionVerificationScreen: View {
NavigationView {
ScrollView {
VStack(spacing: 32.0) {
if let title = context.viewState.title {
Text(title)
.font(.element.headlineBold)
.foregroundColor(.element.systemPrimaryLabel)
.multilineTextAlignment(.center)
}
Text(context.viewState.message)
.font(.body)
.multilineTextAlignment(.center)
@@ -66,6 +59,15 @@ struct SessionVerificationScreen: View {
case .requestingVerification:
ProgressView()
.accessibilityIdentifier("requestingVerificationProgressView")
case .verificationRequestAccepted:
StateIcon(systemName: "lock.shield")
case .startingSasVerification:
ProgressView()
.accessibilityIdentifier("startingSasVerification")
case .sasVerificationStarted:
ProgressView()
.accessibilityIdentifier("startedSasVerification")
case .cancelling:
ProgressView()
.accessibilityIdentifier("cancellingVerificationProgressView")
@@ -98,10 +100,10 @@ struct SessionVerificationScreen: View {
switch context.viewState.verificationState {
case .initial:
Button(ElementL10n.startVerification) {
context.send(viewAction: .start)
context.send(viewAction: .requestVerification)
}
.buttonStyle(.elementAction(.xLarge))
.accessibilityIdentifier("startButton")
.accessibilityIdentifier("requestVerificationButton")
case .cancelled:
Button(ElementL10n.globalRetry) {
@@ -109,16 +111,23 @@ struct SessionVerificationScreen: View {
}
.buttonStyle(.elementAction(.xLarge))
.accessibilityIdentifier("restartButton")
case .verificationRequestAccepted:
Button(ElementL10n.startVerification) {
context.send(viewAction: .startSasVerification)
}
.buttonStyle(.elementAction(.xLarge))
.accessibilityIdentifier("sasVerificationStartButton")
case .showingChallenge:
VStack(spacing: 30) {
Button { context.send(viewAction: .accept) } label: {
Label(ElementL10n.actionMatch, systemImage: "checkmark")
Label(ElementL10n.verificationSasMatch, systemImage: "checkmark")
}
.buttonStyle(.elementAction(.xLarge))
.accessibilityLabel("challengeAcceptButton")
Button(ElementL10n.no) {
Button(ElementL10n.verificationSasDoNotMatch) {
context.send(viewAction: .decline)
}
.font(.element.bodyBold)
@@ -27,7 +27,21 @@ struct MockSessionVerificationControllerProxy: SessionVerificationControllerProx
func requestVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
Task.detached {
try await Task.sleep(for: requestDelay)
callbacks.send(.receivedVerificationData(Self.emojis))
callbacks.send(.acceptedVerificationRequest)
}
return .success(())
}
func startSasVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
Task.detached {
try await Task.sleep(for: requestDelay)
callbacks.send(.startedSasVerification)
Task.detached {
try await Task.sleep(for: requestDelay)
callbacks.send(.receivedVerificationData(Self.emojis))
}
}
return .success(())
@@ -31,6 +31,14 @@ private class WeakSessionVerificationControllerProxy: SessionVerificationControl
proxy?.didReceiveData(data)
}
func didAcceptVerificationRequest() {
proxy?.didAcceptVerificationRequest()
}
func didStartSasVerification() {
proxy?.didStartSasVerification()
}
func didFail() {
proxy?.didFail()
}
@@ -73,6 +81,17 @@ class SessionVerificationControllerProxy: SessionVerificationControllerProxyProt
}
}
func startSasVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
await Task.dispatch(on: .global()) {
do {
try self.sessionVerificationController.startSasVerification()
return .success(())
} catch {
return .failure(.failedStartingSasVerification)
}
}
}
func approveVerification() async -> Result<Void, SessionVerificationControllerProxyError> {
await Task.dispatch(on: .global()) {
do {
@@ -108,6 +127,14 @@ class SessionVerificationControllerProxy: SessionVerificationControllerProxyProt
// MARK: - Private
fileprivate func didAcceptVerificationRequest() {
callbacks.send(.acceptedVerificationRequest)
}
fileprivate func didStartSasVerification() {
callbacks.send(.startedSasVerification)
}
fileprivate func didReceiveData(_ data: [MatrixRustSDK.SessionVerificationEmoji]) {
callbacks.send(.receivedVerificationData(data.map { emoji in
SessionVerificationEmoji(symbol: emoji.symbol(), description: emoji.description())
@@ -19,12 +19,15 @@ import Foundation
enum SessionVerificationControllerProxyError: Error {
case failedRequestingVerification
case failedStartingSasVerification
case failedApprovingVerification
case failedDecliningVerification
case failedCancellingVerification
}
enum SessionVerificationControllerProxyCallback {
case acceptedVerificationRequest
case startedSasVerification
case receivedVerificationData([SessionVerificationEmoji])
case finished
case cancelled
@@ -43,6 +46,8 @@ protocol SessionVerificationControllerProxyProtocol {
func requestVerification() async -> Result<Void, SessionVerificationControllerProxyError>
func startSasVerification() async -> Result<Void, SessionVerificationControllerProxyError>
func approveVerification() async -> Result<Void, SessionVerificationControllerProxyError>
func declineVerification() async -> Result<Void, SessionVerificationControllerProxyError>
@@ -22,16 +22,22 @@ class SessionVerificationUITests: XCTestCase {
let app = Application.launch()
app.goToScreenWithIdentifier(.sessionVerification)
XCTAssert(app.buttons["startButton"].exists)
XCTAssert(app.buttons["requestVerificationButton"].exists)
XCTAssert(app.buttons["closeButton"].exists)
XCTAssert(app.staticTexts["titleLabel"].exists)
app.assertScreenshot(.sessionVerification)
app.buttons["startButton"].tap()
app.buttons["requestVerificationButton"].tap()
XCTAssert(app.activityIndicators["requestingVerificationProgressView"].exists)
XCTAssert(app.buttons["sasVerificationStartButton"].waitForExistence(timeout: 5.0))
app.buttons["sasVerificationStartButton"].tap()
XCTAssert(app.activityIndicators["startingSasVerification"].waitForExistence(timeout: 5.0))
XCTAssert(app.activityIndicators["startedSasVerification"].waitForExistence(timeout: 5.0))
XCTAssert(app.buttons["challengeAcceptButton"].waitForExistence(timeout: 5.0))
XCTAssert(app.buttons["challengeDeclineButton"].waitForExistence(timeout: 5.0))
@@ -50,14 +56,20 @@ class SessionVerificationUITests: XCTestCase {
let app = Application.launch()
app.goToScreenWithIdentifier(.sessionVerification)
XCTAssert(app.buttons["startButton"].exists)
XCTAssert(app.buttons["requestVerificationButton"].exists)
XCTAssert(app.buttons["closeButton"].exists)
XCTAssert(app.staticTexts["titleLabel"].exists)
app.buttons["startButton"].tap()
app.buttons["requestVerificationButton"].tap()
XCTAssert(app.activityIndicators["requestingVerificationProgressView"].exists)
XCTAssert(app.buttons["sasVerificationStartButton"].waitForExistence(timeout: 5.0))
app.buttons["sasVerificationStartButton"].tap()
XCTAssert(app.activityIndicators["startingSasVerification"].waitForExistence(timeout: 5.0))
XCTAssert(app.activityIndicators["startedSasVerification"].waitForExistence(timeout: 5.0))
XCTAssert(app.buttons["challengeAcceptButton"].waitForExistence(timeout: 5.0))
XCTAssert(app.buttons["challengeDeclineButton"].waitForExistence(timeout: 5.0))
@@ -74,14 +86,20 @@ class SessionVerificationUITests: XCTestCase {
let app = Application.launch()
app.goToScreenWithIdentifier(.sessionVerification)
XCTAssert(app.buttons["startButton"].exists)
XCTAssert(app.buttons["requestVerificationButton"].exists)
XCTAssert(app.buttons["closeButton"].exists)
XCTAssert(app.staticTexts["titleLabel"].exists)
app.buttons["startButton"].tap()
app.buttons["requestVerificationButton"].tap()
XCTAssert(app.activityIndicators["requestingVerificationProgressView"].waitForExistence(timeout: 1))
XCTAssert(app.buttons["sasVerificationStartButton"].waitForExistence(timeout: 5.0))
app.buttons["sasVerificationStartButton"].tap()
XCTAssert(app.activityIndicators["startingSasVerification"].waitForExistence(timeout: 5.0))
XCTAssert(app.activityIndicators["startedSasVerification"].waitForExistence(timeout: 5.0))
app.buttons["closeButton"].tap()
XCTAssert(app.images["sessionVerificationFailedIcon"].waitForExistence(timeout: 1))
@@ -33,6 +33,12 @@ class SessionVerificationStateMachineTests: XCTestCase {
stateMachine.processEvent(.requestVerification)
XCTAssertEqual(stateMachine.state, .requestingVerification)
stateMachine.processEvent(.didAcceptVerificationRequest)
XCTAssertEqual(stateMachine.state, .verificationRequestAccepted)
stateMachine.processEvent(.didStartSasVerification)
XCTAssertEqual(stateMachine.state, .sasVerificationStarted)
stateMachine.processEvent(.didReceiveChallenge(emojis: MockSessionVerificationControllerProxy.emojis))
XCTAssertEqual(stateMachine.state, .showingChallenge(emojis: MockSessionVerificationControllerProxy.emojis))
@@ -49,6 +55,12 @@ class SessionVerificationStateMachineTests: XCTestCase {
stateMachine.processEvent(.requestVerification)
XCTAssertEqual(stateMachine.state, .requestingVerification)
stateMachine.processEvent(.didAcceptVerificationRequest)
XCTAssertEqual(stateMachine.state, .verificationRequestAccepted)
stateMachine.processEvent(.didStartSasVerification)
XCTAssertEqual(stateMachine.state, .sasVerificationStarted)
stateMachine.processEvent(.didReceiveChallenge(emojis: MockSessionVerificationControllerProxy.emojis))
XCTAssertEqual(stateMachine.state, .showingChallenge(emojis: MockSessionVerificationControllerProxy.emojis))
@@ -84,6 +96,12 @@ class SessionVerificationStateMachineTests: XCTestCase {
stateMachine.processEvent(.requestVerification)
XCTAssertEqual(stateMachine.state, .requestingVerification)
stateMachine.processEvent(.didAcceptVerificationRequest)
XCTAssertEqual(stateMachine.state, .verificationRequestAccepted)
stateMachine.processEvent(.didStartSasVerification)
XCTAssertEqual(stateMachine.state, .sasVerificationStarted)
stateMachine.processEvent(.didReceiveChallenge(emojis: MockSessionVerificationControllerProxy.emojis))
XCTAssertEqual(stateMachine.state, .showingChallenge(emojis: MockSessionVerificationControllerProxy.emojis))
@@ -35,7 +35,7 @@ class SessionVerificationViewModelTests: XCTestCase {
func testRequestVerification() async {
XCTAssertEqual(context.viewState.verificationState, .initial)
context.send(viewAction: .start)
context.send(viewAction: .requestVerification)
await Task.yield()
@@ -45,7 +45,7 @@ class SessionVerificationViewModelTests: XCTestCase {
func testVerificationCancellation() async throws {
XCTAssertEqual(context.viewState.verificationState, .initial)
context.send(viewAction: .start)
context.send(viewAction: .requestVerification)
context.send(viewAction: .close)
@@ -125,14 +125,20 @@ class SessionVerificationViewModelTests: XCTestCase {
// MARK: - Private
private func setupChallengeReceived() {
let expectation = XCTestExpectation(description: "Wait for challenge")
let requestAcceptanceExpectation = XCTestExpectation(description: "Wait for request acceptance")
let sasVerificationStartExpectation = XCTestExpectation(description: "Wait for SaS verification start")
let verificationDataReceivalExpectation = XCTestExpectation(description: "Wait for Emoji data")
let cancellable = sessionVerificationController.callbacks
.debounce(for: .seconds(2.0), scheduler: DispatchQueue.main)
.sink { callback in
switch callback {
case .acceptedVerificationRequest:
requestAcceptanceExpectation.fulfill()
case .startedSasVerification:
sasVerificationStartExpectation.fulfill()
case .receivedVerificationData:
expectation.fulfill()
verificationDataReceivalExpectation.fulfill()
default:
break
}
@@ -142,10 +148,15 @@ class SessionVerificationViewModelTests: XCTestCase {
cancellable.cancel()
}
context.send(viewAction: .start)
context.send(viewAction: .requestVerification)
wait(for: [requestAcceptanceExpectation], timeout: 10.0)
XCTAssertEqual(context.viewState.verificationState, .verificationRequestAccepted)
wait(for: [expectation], timeout: 10.0)
context.send(viewAction: .startSasVerification)
wait(for: [sasVerificationStartExpectation], timeout: 10.0)
XCTAssertEqual(context.viewState.verificationState, .sasVerificationStarted)
wait(for: [verificationDataReceivalExpectation], timeout: 10.0)
XCTAssertEqual(context.viewState.verificationState, .showingChallenge(emojis: MockSessionVerificationControllerProxy.emojis))
}
}
+1
View File
@@ -0,0 +1 @@
Add support for manually starting SaS verification flows and accepting remotely started ones