- 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:
parent
368f95e7a7
commit
2ec00beeef
|
@ -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 }
|
||||
)
|
||||
}
|
||||
|
|
|
@ -269,7 +269,6 @@ extension HomeReducer {
|
|||
appVersionHandler: .live,
|
||||
mnemonic: environment.mnemonic,
|
||||
SDKSynchronizer: environment.SDKSynchronizer,
|
||||
scheduler: environment.scheduler,
|
||||
walletStorage: environment.walletStorage,
|
||||
zcashSDKEnvironment: environment.zcashSDKEnvironment
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -16,7 +16,6 @@ class SettingsSnapshotTests: XCTestCase {
|
|||
localAuthenticationHandler: .unimplemented,
|
||||
mnemonic: .mock,
|
||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
||||
userPreferencesStorage: .mock,
|
||||
walletStorage: .throwing
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue