[#409] Rewrite LocalAuthenticationHandler so it supports new concurrency (#410)

- refactored to async/await TCA
- reduced a few dependencies
- removed schedulers
- simplified the code
- cleaned up the tests

[409] Rewrite LocalAuthenticationHandler so it supports new concurrency (410)

- Sendable protocol

[409] Rewrite LocalAuthenticationHandler so it supports new concurrency (410)

- fixed typo
This commit is contained in:
Lukas Korba 2022-08-17 15:23:31 +02:00 committed by GitHub
parent 368f95e7a7
commit 2ec00beeef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 38 additions and 88 deletions

View File

@ -7,47 +7,39 @@
import Foundation
import LocalAuthentication
import ComposableArchitecture
import Combine
struct LocalAuthenticationHandler {
let authenticate: () -> Effect<Result<Bool, Never>, Never>
let authenticate: @Sendable () async -> Bool
}
/// The `Result` of live implementation of `LocalAuthentication` has been purposely simplified to Never fails (never returns a .failure)
/// Instead, we care only about `Bool` result of authentication.
extension LocalAuthenticationHandler {
enum LocalAuthenticationNotAvailable: Error {}
static let live = LocalAuthenticationHandler {
Deferred {
Future { promise in
let context = LAContext()
var error: NSError?
let reason = "Folowing content requires authentication."
static let live = LocalAuthenticationHandler(
authenticate: {
let context = LAContext()
var error: NSError?
let reason = "The Following content requires authentication."
do {
/// Biometrics validation
if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason) { success, _ in
promise(.success(success))
}
return try await context.evaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, localizedReason: reason)
} else {
/// Biometrics not supported by the device, fallback to passcode
if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) {
context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, _ in
promise(.success(success))
}
return try await context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason)
} else {
/// No local authentication available, user's device is not protected, fallback to allow access to sensetive content
promise(.success(true))
return true
}
}
} catch {
/// Some interuption occured during the authentication, access to the sensitive content is therefore forbiden
return false
}
}
.catchToEffect()
}
)
static let unimplemented = LocalAuthenticationHandler(
authenticate: { Effect(value: Result.success(false)) }
authenticate: { false }
)
}

View File

@ -269,7 +269,6 @@ extension HomeReducer {
appVersionHandler: .live,
mnemonic: environment.mnemonic,
SDKSynchronizer: environment.SDKSynchronizer,
scheduler: environment.scheduler,
walletStorage: environment.walletStorage,
zcashSDKEnvironment: environment.zcashSDKEnvironment
)

View File

@ -38,7 +38,6 @@ struct ProfileEnvironment {
let appVersionHandler: AppVersionHandler
let mnemonic: WrappedMnemonic
let SDKSynchronizer: WrappedSDKSynchronizer
let scheduler: AnySchedulerOf<DispatchQueue>
let walletStorage: WrappedWalletStorage
let zcashSDKEnvironment: ZCashSDKEnvironment
}
@ -48,7 +47,6 @@ extension ProfileEnvironment {
appVersionHandler: .live,
mnemonic: .live,
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
walletStorage: .live(),
zcashSDKEnvironment: .mainnet
)
@ -57,7 +55,6 @@ extension ProfileEnvironment {
appVersionHandler: .test,
mnemonic: .mock,
SDKSynchronizer: MockWrappedSDKSynchronizer(),
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
walletStorage: .live(),
zcashSDKEnvironment: .testnet
)
@ -116,7 +113,6 @@ extension ProfileReducer {
localAuthenticationHandler: .live,
mnemonic: environment.mnemonic,
SDKSynchronizer: environment.SDKSynchronizer,
scheduler: environment.scheduler,
userPreferencesStorage: .live,
walletStorage: environment.walletStorage
)

View File

@ -20,7 +20,6 @@ struct SettingsState: Equatable {
// MARK: - Action
enum SettingsAction: Equatable {
case authenticate(Result<Bool, Never>)
case backupWallet
case backupWalletAccessRequest
case cancelRescan
@ -37,7 +36,6 @@ struct SettingsEnvironment {
let localAuthenticationHandler: LocalAuthenticationHandler
let mnemonic: WrappedMnemonic
let SDKSynchronizer: WrappedSDKSynchronizer
let scheduler: AnySchedulerOf<DispatchQueue>
let userPreferencesStorage: UserPreferencesStorage
let walletStorage: WrappedWalletStorage
}
@ -54,16 +52,12 @@ extension SettingsReducer {
private static let settingsReducer = SettingsReducer { state, action, environment in
switch action {
case .authenticate(let result):
return result == .success(false)
? .none
: Effect(value: .backupWallet)
case .backupWalletAccessRequest:
return environment.localAuthenticationHandler.authenticate()
.receive(on: environment.scheduler)
.map(SettingsAction.authenticate)
.eraseToEffect()
return .run { send in
if await environment.localAuthenticationHandler.authenticate() {
await send(.backupWallet)
}
}
case .backupWallet:
do {
@ -157,7 +151,6 @@ extension SettingsStore {
localAuthenticationHandler: .live,
mnemonic: .live,
SDKSynchronizer: MockWrappedSDKSynchronizer(),
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
userPreferencesStorage: .live,
walletStorage: .live()
)

View File

@ -11,13 +11,10 @@ import ComposableArchitecture
class ProfileTests: XCTestCase {
func testSynchronizerStateChanged_AnyButSynced() throws {
let testScheduler = DispatchQueue.test
let testEnvironment = ProfileEnvironment(
appVersionHandler: .test,
mnemonic: .mock,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .throwing,
zcashSDKEnvironment: .testnet
)

View File

@ -9,9 +9,9 @@ import XCTest
@testable import secant_testnet
import ComposableArchitecture
@MainActor
class SettingsTests: XCTestCase {
func testBackupWalletAccessRequest_AuthenticateSuccessPath() throws {
let testScheduler = DispatchQueue.test
func testBackupWalletAccessRequest_AuthenticateSuccessPath() async throws {
let mnemonic =
"""
still champion voice habit trend flight \
@ -45,12 +45,9 @@ class SettingsTests: XCTestCase {
)
let testEnvironment = SettingsEnvironment(
localAuthenticationHandler: LocalAuthenticationHandler(authenticate: {
Effect(value: Result.success(true))
}),
localAuthenticationHandler: LocalAuthenticationHandler(authenticate: { true }),
mnemonic: .live,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
userPreferencesStorage: .mock,
walletStorage: mockedWalletStorage
)
@ -61,27 +58,21 @@ class SettingsTests: XCTestCase {
environment: testEnvironment
)
store.send(.backupWalletAccessRequest)
await store.send(.backupWalletAccessRequest)
testScheduler.advance(by: 0.1)
store.receive(.authenticate(.success(true)))
store.receive(.backupWallet) { state in
await store.receive(.backupWallet) { state in
state.phraseDisplayState.phrase = RecoveryPhrase(words: mnemonic.components(separatedBy: " "))
}
store.receive(.updateRoute(.backupPhrase)) { state in
await store.receive(.updateRoute(.backupPhrase)) { state in
state.route = .backupPhrase
}
}
func testBackupWalletAccessRequest_AuthenticateFailedPath() throws {
let testScheduler = DispatchQueue.test
func testBackupWalletAccessRequest_AuthenticateFailedPath() async throws {
let testEnvironment = SettingsEnvironment(
localAuthenticationHandler: .unimplemented,
mnemonic: .mock,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
userPreferencesStorage: .mock,
walletStorage: .throwing
)
@ -92,21 +83,16 @@ class SettingsTests: XCTestCase {
environment: testEnvironment
)
store.send(.backupWalletAccessRequest)
await store.send(.backupWalletAccessRequest)
testScheduler.advance(by: 0.1)
store.receive(.authenticate(.success(false)))
await store.finish()
}
func testRescanBlockchain() throws {
let testScheduler = DispatchQueue.test
func testRescanBlockchain() async throws {
let testEnvironment = SettingsEnvironment(
localAuthenticationHandler: .unimplemented,
mnemonic: .mock,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
userPreferencesStorage: .mock,
walletStorage: .throwing
)
@ -117,7 +103,7 @@ class SettingsTests: XCTestCase {
environment: testEnvironment
)
store.send(.rescanBlockchain) { state in
await store.send(.rescanBlockchain) { state in
state.rescanDialog = .init(
title: TextState("Rescan"),
message: TextState("Select the rescan you want"),
@ -130,14 +116,11 @@ class SettingsTests: XCTestCase {
}
}
func testRescanBlockchain_Cancelling() throws {
let testScheduler = DispatchQueue.test
func testRescanBlockchain_Cancelling() async throws {
let testEnvironment = SettingsEnvironment(
localAuthenticationHandler: .unimplemented,
mnemonic: .mock,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
userPreferencesStorage: .mock,
walletStorage: .throwing
)
@ -160,19 +143,16 @@ class SettingsTests: XCTestCase {
environment: testEnvironment
)
store.send(.cancelRescan) { state in
await store.send(.cancelRescan) { state in
state.rescanDialog = nil
}
}
func testRescanBlockchain_QuickRescanClearance() throws {
let testScheduler = DispatchQueue.test
func testRescanBlockchain_QuickRescanClearance() async throws {
let testEnvironment = SettingsEnvironment(
localAuthenticationHandler: .unimplemented,
mnemonic: .mock,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
userPreferencesStorage: .mock,
walletStorage: .throwing
)
@ -195,19 +175,16 @@ class SettingsTests: XCTestCase {
environment: testEnvironment
)
store.send(.quickRescan) { state in
await store.send(.quickRescan) { state in
state.rescanDialog = nil
}
}
func testRescanBlockchain_FullRescanClearance() throws {
let testScheduler = DispatchQueue.test
func testRescanBlockchain_FullRescanClearance() async throws {
let testEnvironment = SettingsEnvironment(
localAuthenticationHandler: .unimplemented,
mnemonic: .mock,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
userPreferencesStorage: .mock,
walletStorage: .throwing
)
@ -230,7 +207,7 @@ class SettingsTests: XCTestCase {
environment: testEnvironment
)
store.send(.fullRescan) { state in
await store.send(.fullRescan) { state in
state.rescanDialog = nil
}
}

View File

@ -12,13 +12,10 @@ import SwiftUI
class ProfileSnapshotTests: XCTestCase {
func testProfileSnapshot_sent() throws {
let testScheduler = DispatchQueue.test
let testEnvironment = ProfileEnvironment(
appVersionHandler: .test,
mnemonic: .mock,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: testScheduler.eraseToAnyScheduler(),
walletStorage: .throwing,
zcashSDKEnvironment: .testnet
)

View File

@ -16,7 +16,6 @@ class SettingsSnapshotTests: XCTestCase {
localAuthenticationHandler: .unimplemented,
mnemonic: .mock,
SDKSynchronizer: TestWrappedSDKSynchronizer(),
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
userPreferencesStorage: .mock,
walletStorage: .throwing
)