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

177 lines
5.3 KiB
Swift

//
// ScanUIView.swift
// secant-testnet
//
// Created by Lukáš Korba on 16.05.2022.
//
import ComposableArchitecture
import Foundation
import CaptureDevice
import Utils
import URIParser
import ZcashLightClientKit
import Generated
import ZcashSDKEnvironment
public typealias ScanStore = Store<ScanReducer.State, ScanReducer.Action>
public typealias ScanViewStore = ViewStore<ScanReducer.State, ScanReducer.Action>
public struct ScanReducer: Reducer {
private enum CancelId { case timer }
public struct State: Equatable {
public enum ScanStatus: Equatable {
case failed
case value(RedactableString)
case unknown
}
@PresentationState public var alert: AlertState<Action>?
public var isTorchAvailable = false
public var isTorchOn = false
public var scanStatus: ScanStatus = .unknown
public var scannedValue: String? {
guard case let .value(scannedValue) = scanStatus else {
return nil
}
return scannedValue.data
}
public var isValidValue: Bool {
if case .value = scanStatus {
return true
}
return false
}
public init(
isTorchAvailable: Bool = false,
isTorchOn: Bool = false,
scanStatus: ScanStatus = .unknown
) {
self.isTorchAvailable = isTorchAvailable
self.isTorchOn = isTorchOn
self.scanStatus = scanStatus
}
}
@Dependency(\.captureDevice) var captureDevice
@Dependency(\.mainQueue) var mainQueue
@Dependency(\.uriParser) var uriParser
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
public enum Action: Equatable {
case alert(PresentationAction<Action>)
case onAppear
case onDisappear
case found(RedactableString)
case scanFailed
case scan(RedactableString)
case torchPressed
}
public init() { }
// swiftlint:disable:next cyclomatic_complexity
public var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .alert(.presented(let action)):
return Effect.send(action)
case .alert(.dismiss):
state.alert = nil
return .none
case .alert:
return .none
case .onAppear:
// reset the values
state.scanStatus = .unknown
state.isTorchOn = false
// check the torch availability
do {
state.isTorchAvailable = try captureDevice.isTorchAvailable()
} catch {
state.alert = AlertState.cantInitializeCamera(error.toZcashError())
}
return .none
case .onDisappear:
return .cancel(id: CancelId.timer)
case .found:
return .none
case .scanFailed:
state.scanStatus = .failed
return .none
case .scan(let code):
// the logic for the same scanned code is skipped until some new code
if let prevCode = state.scannedValue, prevCode == code.data {
return .none
}
if uriParser.isValidURI(code.data, zcashSDKEnvironment.network.networkType) {
state.scanStatus = .value(code)
// once valid URI is scanned we want to start the timer to deliver the code
// any new code cancels the schedule and fires new one
return .concatenate(
Effect.cancel(id: CancelId.timer),
.run { send in
try await mainQueue.sleep(for: .seconds(1))
await send(.found(code))
}
.cancellable(id: CancelId.timer, cancelInFlight: true)
)
} else {
state.scanStatus = .failed
}
return .cancel(id: CancelId.timer)
case .torchPressed:
do {
try captureDevice.torch(!state.isTorchOn)
state.isTorchOn.toggle()
} catch {
state.alert = AlertState.cantInitializeCamera(error.toZcashError())
}
return .none
}
}
.ifLet(\.$alert, action: /Action.alert)
}
}
// MARK: Alerts
extension AlertState where Action == ScanReducer.Action {
public static func cantInitializeCamera(_ error: ZcashError) -> AlertState {
AlertState {
TextState(L10n.Scan.Alert.CantInitializeCamera.title)
} message: {
TextState(L10n.Scan.Alert.CantInitializeCamera.message(error.message, error.code.rawValue))
}
}
}
// MARK: Placeholders
extension ScanReducer.State {
public static var initial: Self {
.init()
}
}
extension ScanStore {
public static let placeholder = ScanStore(
initialState: .initial
) {
ScanReducer()
}
}