secant-ios-wallet/modules/Sources/Features/Scan/ScanStore.swift

230 lines
7.3 KiB
Swift

//
// ScanStore.swift
// Zashi
//
// Created by Lukáš Korba on 16.05.2022.
//
import SwiftUI
import CoreImage
import ComposableArchitecture
import Foundation
import CaptureDevice
import QRImageDetector
import Utils
import URIParser
import ZcashLightClientKit
import Generated
import ZcashSDKEnvironment
import Models
import ZcashPaymentURI
import KeystoneHandler
import KeystoneSDK
@Reducer
public struct Scan {
public enum ScanImageResult: Equatable {
case invalidQRCode
case noQRCodeFound
case severalQRCodesFound
case keystoneCheckOnly
}
@ObservableState
public struct State: Equatable {
public var cancelId = UUID()
public var checkers: [ScanCheckerWrapper] = []
public var forceLibraryToHide = false
public var info = ""
public var instructions: String?
public var isAnythingFound = false
public var isCameraEnabled = true
public var isTorchAvailable = false
public var isTorchOn = false
public var isRPFound = false
public var progress: Int?
public var expectedParts = 0
public var reportedParts = 0
public var reportedPart = -1
var countedProgress: Int {
guard expectedParts > 0 else { return 0 }
return min(99, Int(Float(reportedParts) / Float(expectedParts) * 100))
}
public init(
info: String = "",
isTorchAvailable: Bool = false,
isTorchOn: Bool = false,
isCameraEnabled: Bool = true
) {
self.info = info
self.isTorchAvailable = isTorchAvailable
self.isTorchOn = isTorchOn
}
}
@Dependency(\.captureDevice) var captureDevice
@Dependency(\.mainQueue) var mainQueue
@Dependency(\.keystoneHandler) var keystoneHandler
@Dependency(\.qrImageDetector) var qrImageDetector
@Dependency(\.uriParser) var uriParser
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
public enum Action: Equatable {
case cancelTapped
case checkCameraPermission
case clearInfo
case libraryImage(UIImage?)
case onAppear
case onDisappear
case foundAddress(RedactableString)
case foundRequestZec(ParserResult)
case foundAccounts(ZcashAccounts)
case foundPCZT(Data)
case animatedQRProgress(Int, Int?, Int?)
case scanFailed(ScanImageResult)
case scan(RedactableString)
case torchTapped
}
public init() { }
// swiftlint:disable:next cyclomatic_complexity
public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .onAppear:
// reset the values
state.isAnythingFound = false
state.reportedPart = -1
state.reportedParts = 0
state.expectedParts = 0
state.progress = nil
state.isTorchOn = false
state.isRPFound = false
state.info = ""
// check the torch availability
state.isTorchAvailable = captureDevice.isTorchAvailable()
return .send(.checkCameraPermission)
case .onDisappear:
return .cancel(id: state.cancelId)
case .checkCameraPermission:
if !captureDevice.isAuthorized() {
state.isCameraEnabled = false
state.info = L10n.Scan.cameraSettings
return .run { send in
try? await mainQueue.sleep(for: .seconds(1))
await send(.checkCameraPermission)
}
} else {
state.isCameraEnabled = true
state.info = ""
}
return .none
case .foundAddress:
state.isAnythingFound = true
return .none
case .foundRequestZec:
state.isAnythingFound = true
return .none
case .foundAccounts:
state.isAnythingFound = true
state.progress = nil
return .none
case .foundPCZT:
state.isAnythingFound = true
state.progress = nil
return .none
case .cancelTapped:
return .none
case .clearInfo:
state.info = ""
return .cancel(id: state.cancelId)
case let .animatedQRProgress(progress, part, expectedParts):
let partInt = part ?? -1
if partInt != -1 && partInt != state.reportedPart {
state.reportedPart = partInt
state.reportedParts = state.reportedParts + 1
}
state.expectedParts = Int(Float(expectedParts ?? 0) * 1.75)
state.progress = progress
return .none
case .libraryImage(let image):
guard !state.isRPFound else {
return .none
}
guard let codes = qrImageDetector.check(image) else {
return .send(.scanFailed(.noQRCodeFound))
}
guard codes.count == 1 else {
return .send(.scanFailed(.severalQRCodesFound))
}
guard let code = codes.first else {
return .send(.scanFailed(.noQRCodeFound))
}
return .send(.scan(code.redacted))
case .scanFailed(let result):
switch result {
case .invalidQRCode:
state.info = L10n.Scan.invalidQR
case .noQRCodeFound:
state.info = L10n.Scan.invalidImage
case .severalQRCodesFound:
state.info = L10n.Scan.severalCodesFound
case .keystoneCheckOnly:
state.info = ""
}
return .concatenate(
Effect.cancel(id: state.cancelId),
.run { send in
try await mainQueue.sleep(for: .seconds(1))
await send(.clearInfo)
}
.cancellable(id: state.cancelId, cancelInFlight: true)
)
case .scan(let code):
guard !state.isAnythingFound else {
return .none
}
for checker in state.checkers {
if let action = checker.checker.checkQRCode(code.data) {
return .send(action)
}
}
if state.checkers.count == 2 && state.checkers[0] == .keystoneScanChecker && state.checkers[1] == .keystonePCZTScanChecker {
return .none
}
return .send(.scanFailed(.noQRCodeFound))
case .torchTapped:
do {
try captureDevice.torch(!state.isTorchOn)
state.isTorchOn.toggle()
} catch { }
return .none
}
}
}
}