// // ScanUIView.swift // secant-testnet // // Created by Lukáš Korba on 16.05.2022. // import ComposableArchitecture import Foundation typealias ScanStore = Store typealias ScanViewStore = ViewStore struct ScanReducer: ReducerProtocol { private enum CancelId {} struct State: Equatable { enum ScanStatus: Equatable { case failed case value(RedactableString) case unknown } @BindingState var alert: AlertState? var isTorchAvailable = false var isTorchOn = false var isValidValue = false var scanStatus: ScanStatus = .unknown var scannedValue: String? { guard case let .value(scannedValue) = scanStatus else { return nil } return scannedValue.data } } @Dependency(\.captureDevice) var captureDevice @Dependency(\.mainQueue) var mainQueue @Dependency(\.uriParser) var uriParser enum Action: Equatable { case dismissAlert case onAppear case onDisappear case found(RedactableString) case scanFailed case scan(RedactableString) case torchPressed } // swiftlint:disable:next cyclomatic_complexity func reduce(into state: inout State, action: Action) -> ComposableArchitecture.EffectTask { switch action { case .dismissAlert: state.alert = nil return .none case .onAppear: // reset the values state.scanStatus = .unknown state.isValidValue = false state.isTorchOn = false // check the torch availability do { state.isTorchAvailable = try captureDevice.isTorchAvailable() } catch { // TODO: [#322] Handle error more properly (https://github.com/zcash/secant-ios-wallet/issues/322) state.alert = AlertState( title: TextState(L10n.Scan.Alert.CantInitializeCamera.title), message: TextState(L10n.Scan.Alert.CantInitializeCamera.message(error.localizedDescription)), dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert)) ) } return .none case .onDisappear: return .cancel(id: CancelId.self) 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 } state.scanStatus = .value(code) state.isValidValue = false do { if try uriParser.isValidURI(code.data) { state.isValidValue = true // 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( EffectTask.cancel(id: CancelId.self), EffectTask(value: .found(code)) .delay(for: 1.0, scheduler: mainQueue) .eraseToEffect() .cancellable(id: CancelId.self, cancelInFlight: true) ) } } catch { state.scanStatus = .failed } return .cancel(id: CancelId.self) case .torchPressed: do { try captureDevice.torch(!state.isTorchOn) state.isTorchOn.toggle() } catch { // TODO: [#322] handle torch errors (https://github.com/zcash/secant-ios-wallet/issues/322) state.alert = AlertState( title: TextState(L10n.Scan.Alert.CantInitializeCamera.title), message: TextState(L10n.Scan.Alert.CantInitializeCamera.message(error.localizedDescription)), dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert)) ) } return .none } } } // MARK: Placeholders extension ScanReducer.State { static var placeholder: Self { .init() } } extension ScanStore { static let placeholder = ScanStore( initialState: .placeholder, reducer: ScanReducer() ) }