diff --git a/CHANGELOG.md b/CHANGELOG.md index ef8fc6f..60114cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ directly impact users rather than highlighting other crucial architectural updat ### Changed - Settings screen options have been reduced and some were moved to the new Advanced Settings screen. +- Scan of QR codes has been re-worked with new design and behaviours. ### Added - Pending values (changes) at the Balances tab. diff --git a/modules/.swiftpm/xcode/xcshareddata/xcschemes/Scan.xcscheme b/modules/.swiftpm/xcode/xcshareddata/xcschemes/Scan.xcscheme new file mode 100644 index 0000000..4a16b67 --- /dev/null +++ b/modules/.swiftpm/xcode/xcshareddata/xcschemes/Scan.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceInterface.swift b/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceInterface.swift index 3e1ed8c..e1c001e 100644 --- a/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceInterface.swift +++ b/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceInterface.swift @@ -16,11 +16,13 @@ extension DependencyValues { public struct CaptureDeviceClient { public enum CaptureDeviceClientError: Error { - case captureDeviceFailed - case lockForConfigurationFailed + case authorizationStatus + case captureDevice + case lockForConfiguration case torchUnavailable } - - public let isTorchAvailable: () throws -> Bool + + public let isAuthorized: () -> Bool + public let isTorchAvailable: () -> Bool public let torch: (Bool) throws -> Void } diff --git a/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceLiveKey.swift b/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceLiveKey.swift index d9e0be3..e44474e 100644 --- a/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceLiveKey.swift +++ b/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceLiveKey.swift @@ -10,16 +10,19 @@ import ComposableArchitecture extension CaptureDeviceClient: DependencyKey { public static let liveValue = Self( + isAuthorized: { + AVCaptureDevice.authorizationStatus(for: .video) == .authorized + }, isTorchAvailable: { guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { - throw CaptureDeviceClientError.captureDeviceFailed + return false } return videoCaptureDevice.hasTorch }, torch: { isTorchOn in guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { - throw CaptureDeviceClientError.captureDeviceFailed + throw CaptureDeviceClientError.captureDevice } guard videoCaptureDevice.hasTorch else { @@ -31,7 +34,7 @@ extension CaptureDeviceClient: DependencyKey { videoCaptureDevice.torchMode = isTorchOn ? .on : .off videoCaptureDevice.unlockForConfiguration() } catch { - throw CaptureDeviceClientError.lockForConfigurationFailed + throw CaptureDeviceClientError.lockForConfiguration } } ) diff --git a/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceTestKey.swift b/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceTestKey.swift index 00ddf28..893d4fb 100644 --- a/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceTestKey.swift +++ b/modules/Sources/Dependencies/CaptureDevice/CaptureDeviceTestKey.swift @@ -10,6 +10,7 @@ import XCTestDynamicOverlay extension CaptureDeviceClient: TestDependencyKey { public static let testValue = Self( + isAuthorized: XCTUnimplemented("\(Self.self).isAuthorized", placeholder: false), isTorchAvailable: XCTUnimplemented("\(Self.self).isTorchAvailable", placeholder: false), torch: XCTUnimplemented("\(Self.self).torch") ) @@ -17,6 +18,7 @@ extension CaptureDeviceClient: TestDependencyKey { extension CaptureDeviceClient { public static let noOp = Self( + isAuthorized: { false }, isTorchAvailable: { false }, torch: { _ in } ) diff --git a/modules/Sources/Features/Home/HomeStore.swift b/modules/Sources/Features/Home/HomeStore.swift index 252b65c..eb63473 100644 --- a/modules/Sources/Features/Home/HomeStore.swift +++ b/modules/Sources/Features/Home/HomeStore.swift @@ -31,7 +31,7 @@ public struct HomeReducer: Reducer { public var canRequestReview = false public var isRestoringWallet = false public var requiredTransactionConfirmations = 0 - public var scanState: ScanReducer.State + public var scanState: Scan.State public var shieldedBalance: Zatoshi public var synchronizerStatusSnapshot: SyncStatusSnapshot public var syncProgressState: SyncProgressReducer.State @@ -55,7 +55,7 @@ public struct HomeReducer: Reducer { canRequestReview: Bool = false, isRestoringWallet: Bool = false, requiredTransactionConfirmations: Int = 0, - scanState: ScanReducer.State, + scanState: Scan.State, shieldedBalance: Zatoshi, synchronizerStatusSnapshot: SyncStatusSnapshot, syncProgressState: SyncProgressReducer.State, diff --git a/modules/Sources/Features/Sandbox/SandboxView.swift b/modules/Sources/Features/Sandbox/SandboxView.swift index 0d12563..29997d0 100644 --- a/modules/Sources/Features/Sandbox/SandboxView.swift +++ b/modules/Sources/Features/Sandbox/SandboxView.swift @@ -42,7 +42,7 @@ public struct SandboxView: View { case .recoveryPhraseDisplay: RecoveryPhraseDisplayView(store: RecoveryPhraseDisplay.placeholder) case .scan: - ScanView(store: .placeholder) + ScanView(store: Scan.placeholder) } } diff --git a/modules/Sources/Features/Scan/ScanStore.swift b/modules/Sources/Features/Scan/ScanStore.swift index 038d6b9..cbf91f1 100644 --- a/modules/Sources/Features/Scan/ScanStore.swift +++ b/modules/Sources/Features/Scan/ScanStore.swift @@ -1,5 +1,5 @@ // -// ScanUIView.swift +// Scan.swift // secant-testnet // // Created by Lukáš Korba on 16.05.2022. @@ -14,47 +14,24 @@ import ZcashLightClientKit import Generated import ZcashSDKEnvironment -public typealias ScanStore = Store -public typealias ScanViewStore = ViewStore - -public struct ScanReducer: Reducer { +@Reducer +public struct Scan { private enum CancelId { case timer } + @ObservableState public struct State: Equatable { - public enum ScanStatus: Equatable { - case failed - case value(RedactableString) - case unknown - } - - @PresentationState public var alert: AlertState? + public var info = "" 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( + info: String = "", isTorchAvailable: Bool = false, - isTorchOn: Bool = false, - scanStatus: ScanStatus = .unknown + isTorchOn: Bool = false ) { + self.info = info self.isTorchAvailable = isTorchAvailable self.isTorchOn = isTorchOn - self.scanStatus = scanStatus } } @@ -64,7 +41,8 @@ public struct ScanReducer: Reducer { @Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment public enum Action: Equatable { - case alert(PresentationAction) + case cancelPressed + case clearInfo case onAppear case onDisappear case found(RedactableString) @@ -79,98 +57,54 @@ public struct ScanReducer: Reducer { public var body: some ReducerOf { 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()) + state.isTorchAvailable = captureDevice.isTorchAvailable() + if !captureDevice.isAuthorized() { + state.info = L10n.Scan.cameraSettings } return .none case .onDisappear: return .cancel(id: CancelId.timer) + case .cancelPressed: + return .none + + case .clearInfo: + state.info = "" + return .cancel(id: CancelId.timer) + case .found: return .none - + case .scanFailed: - state.scanStatus = .failed - return .none - + state.info = L10n.Scan.invalidQR + return .concatenate( + Effect.cancel(id: CancelId.timer), + .run { send in + try await mainQueue.sleep(for: .seconds(3)) + await send(.clearInfo) + } + .cancellable(id: CancelId.timer, cancelInFlight: true) + ) + 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) - ) + return .send(.found(code)) } else { - state.scanStatus = .failed + return .send(.scanFailed) } - return .cancel(id: CancelId.timer) case .torchPressed: do { try captureDevice.torch(!state.isTorchOn) state.isTorchOn.toggle() - } catch { - state.alert = AlertState.cantInitializeCamera(error.toZcashError()) - } + } catch { } 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() } } diff --git a/modules/Sources/Features/Scan/ScanView.swift b/modules/Sources/Features/Scan/ScanView.swift index 332c849..99db94a 100644 --- a/modules/Sources/Features/Scan/ScanView.swift +++ b/modules/Sources/Features/Scan/ScanView.swift @@ -11,134 +11,165 @@ import Generated import UIComponents public struct ScanView: View { - @Environment(\.presentationMode) var presentationMode + let store: StoreOf - let store: ScanStore - - public init(store: ScanStore) { + + public init(store: StoreOf) { self.store = store } public var body: some View { - WithViewStore(store, observe: { $0 }) { viewStore in - GeometryReader { proxy in - ZStack { + WithPerceptionTracking { + ZStack { + GeometryReader { proxy in QRCodeScanView( rectOfInterest: normalizedRectOfInterest(proxy.size), - onQRScanningDidFail: { viewStore.send(.scanFailed) }, - onQRScanningSucceededWithCode: { viewStore.send(.scan($0.redacted)) } + onQRScanningDidFail: { store.send(.scanFailed) }, + onQRScanningSucceededWithCode: { store.send(.scan($0.redacted)) } ) - backButton - - if viewStore.isTorchAvailable { - torchButton(viewStore) - } - frameOfInterest(proxy.size) - VStack { - Spacer() - - Text(L10n.Scan.info) - .padding(.bottom, 10) - - if let scannedValue = viewStore.scannedValue { - Text(scannedValue) - .foregroundColor(viewStore.isValidValue ? .green : .red) - } else { - Text(L10n.Scan.scanning) - } + if store.isTorchAvailable { + torchButton(store, size: proxy.size) } - .padding() } - .navigationBarHidden(true) - .applyScreenBackground() - .onAppear { viewStore.send(.onAppear) } - .onDisappear { viewStore.send(.onDisappear) } + + VStack { + Spacer() + + Text(store.info) + .font(Font.custom("Inter", size: 14)) + .foregroundColor(Asset.Colors.secondary.color) + .multilineTextAlignment(.center) + .padding(.bottom, 20) + + Button(L10n.General.cancel.uppercased()) { + store.send(.cancelPressed) + } + .zcashStyle(.secondary) + .padding(.horizontal, 50) + .padding(.bottom, 70) + } + .padding(.horizontal, 30) } + .edgesIgnoringSafeArea(.all) .ignoresSafeArea() + .navigationBarHidden(true) + .applyScreenBackground() + .onAppear { store.send(.onAppear) } + .onDisappear { store.send(.onDisappear) } } - .alert(store: store.scope( - state: \.$alert, - action: { .alert($0) } - )) } } extension ScanView { - var backButton: some View { - VStack { - HStack { - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Image(systemName: "arrow.backward") - .foregroundColor(Asset.Colors.primary.color) - .font( - .custom(FontFamily.Inter.regular.name, size: 30) - ) - }) - .padding(.top, 10) - - Spacer() + func torchButton(_ store: StoreOf, size: CGSize) -> some View { + let center = ScanView.rectOfInterest(size).origin + let frameHalfSize = ScanView.frameSize(size) * 0.5 + + return Button { + store.send(.torchPressed) + } label: { + if store.isTorchOn { + Asset.Assets.torchOff.image + .renderingMode(.template) + .resizable() + .frame(width: 20, height: 20) + .tint(.white) + } else { + Asset.Assets.torchOn.image + .renderingMode(.template) + .resizable() + .frame(width: 20, height: 20) + .tint(.white) } - .padding() - - Spacer() } - .padding() - } - - func torchButton(_ viewStore: ScanViewStore) -> some View { - VStack { - HStack { - Spacer() - - Button( - action: { viewStore.send(.torchPressed) }, - label: { - Image( - systemName: viewStore.isTorchOn ? "lightbulb.fill" : "lightbulb.slash" - ) - .foregroundColor(Asset.Colors.primary.color) - .font( - .custom(FontFamily.Inter.regular.name, size: 30) - ) - } - ) - .padding(.top, 10) - } - .padding() - - Spacer() - } - .padding() + .position( + x: center.x + frameHalfSize - 5, + y: center.y + frameHalfSize + 20 + ) } func frameOfInterest(_ size: CGSize) -> some View { - Rectangle() - .stroke(Asset.Colors.primary.color, lineWidth: 5.0) - .frame( - width: frameSize(size), - height: frameSize(size), - alignment: .center - ) - .edgesIgnoringSafeArea(.all) - .ignoresSafeArea() - .position( - x: rectOfInterest(size).origin.x, - y: rectOfInterest(size).origin.y - ) + let center = ScanView.rectOfInterest(size).origin + let frameSize = ScanView.frameSize(size) + let halfSize = frameSize * 0.5 + let cornersLength = 36.0 + let cornersHalfLength = cornersLength * 0.5 + let leadMarkColor = Color.white + + return ZStack { + Color.black + .opacity(0.65) + .edgesIgnoringSafeArea(.all) + .ignoresSafeArea() + .reverseMask { + Rectangle() + .frame( + width: frameSize, + height: frameSize, + alignment: .center + ) + .position( + x: center.x, + y: center.y + ) + } + + // horizontal lead marks + leadMarkColor + .frame(width: cornersLength, height: 1) + .position(x: center.x - halfSize + cornersHalfLength, y: center.y - halfSize) + leadMarkColor + .frame(width: cornersLength, height: 1) + .position(x: center.x + halfSize - cornersHalfLength, y: center.y - halfSize) + leadMarkColor + .frame(width: cornersLength, height: 1) + .position(x: center.x - halfSize + cornersHalfLength, y: center.y + halfSize) + leadMarkColor + .frame(width: cornersLength, height: 1) + .position(x: center.x + halfSize - cornersHalfLength, y: center.y + halfSize) + + // vertical lead marks + leadMarkColor + .frame(width: 1, height: cornersLength) + .position(x: center.x - halfSize, y: center.y - halfSize + cornersHalfLength) + leadMarkColor + .frame(width: 1, height: cornersLength) + .position(x: center.x - halfSize, y: center.y + halfSize - cornersHalfLength) + leadMarkColor + .frame(width: 1, height: cornersLength) + .position(x: center.x + halfSize, y: center.y - halfSize + cornersHalfLength) + leadMarkColor + .frame(width: 1, height: cornersLength) + .position(x: center.x + halfSize, y: center.y + halfSize - cornersHalfLength) + } + } +} + +extension View { + @inlinable + public func reverseMask( + alignment: Alignment = .center, + @ViewBuilder _ mask: () -> Mask + ) -> some View { + self.mask { + Rectangle() + .overlay(alignment: alignment) { + mask() + .blendMode(.destinationOut) + } + } } } extension ScanView { - func frameSize(_ size: CGSize) -> CGFloat { + static func frameSize(_ size: CGSize) -> CGFloat { size.width * 0.55 } - func rectOfInterest(_ size: CGSize) -> CGRect { + static func rectOfInterest(_ size: CGSize) -> CGRect { CGRect( x: size.width * 0.5, y: size.height * 0.5, @@ -161,6 +192,20 @@ extension ScanView { struct ScanView_Previews: PreviewProvider { static var previews: some View { - ScanView(store: .placeholder) + ScanView(store: Scan.placeholder) + } +} + +// MARK: Placeholders + +extension Scan.State { + public static var initial = Scan.State() +} + +extension Scan { + public static let placeholder = StoreOf( + initialState: .initial + ) { + Scan() } } diff --git a/modules/Sources/Features/SendFlow/SendFlowStore.swift b/modules/Sources/Features/SendFlow/SendFlowStore.swift index e8290fe..c0fe1a3 100644 --- a/modules/Sources/Features/SendFlow/SendFlowStore.swift +++ b/modules/Sources/Features/SendFlow/SendFlowStore.swift @@ -37,7 +37,7 @@ public struct SendFlowReducer: Reducer { public var destination: Destination? public var isSending = false public var memoState: MessageEditorReducer.State - public var scanState: ScanReducer.State + public var scanState: Scan.State public var shieldedBalance = Zatoshi.zero public var totalBalance = Zatoshi.zero public var transactionAddressInputState: TransactionAddressTextFieldReducer.State @@ -106,7 +106,7 @@ public struct SendFlowReducer: Reducer { addMemoState: Bool, destination: Destination? = nil, memoState: MessageEditorReducer.State, - scanState: ScanReducer.State, + scanState: Scan.State, shieldedBalance: Zatoshi = .zero, totalBalance: Zatoshi = .zero, transactionAddressInputState: TransactionAddressTextFieldReducer.State, @@ -130,7 +130,7 @@ public struct SendFlowReducer: Reducer { case onAppear case onDisappear case reviewPressed - case scan(ScanReducer.Action) + case scan(Scan.Action) case sendPressed case sendDone(TransactionState) case sendFailed(ZcashError) @@ -164,7 +164,7 @@ public struct SendFlowReducer: Reducer { } Scope(state: \.scanState, action: /Action.scan) { - ScanReducer() + Scan() } Reduce { state, action in @@ -283,6 +283,10 @@ public struct SendFlowReducer: Reducer { audioServices.systemSoundVibrate() return Effect.send(.updateDestination(nil)) + case .scan(.cancelPressed): + state.destination = nil + return .none + case .scan: return .none } @@ -312,7 +316,7 @@ extension SendFlowStore { ) } - func scanStore() -> ScanStore { + func scanStore() -> StoreOf { self.scope( state: \.scanState, action: SendFlowReducer.Action.scan diff --git a/modules/Sources/Features/ServerSetup/ServerSetupView.swift b/modules/Sources/Features/ServerSetup/ServerSetupView.swift index d028206..fb1a4be 100644 --- a/modules/Sources/Features/ServerSetup/ServerSetupView.swift +++ b/modules/Sources/Features/ServerSetup/ServerSetupView.swift @@ -111,19 +111,14 @@ public struct ServerSetupView: View { #Preview { ServerSetupView( - store: .init( - initialState: - ServerSetup.State(server: .custom) - ) { - ServerSetup() - } + store: ServerSetup.placeholder ) } // MARK: Placeholders extension ServerSetup.State { - public static let initial = ServerSetup.State() + public static var initial = ServerSetup.State() } extension ServerSetup { diff --git a/modules/Sources/Generated/L10n.swift b/modules/Sources/Generated/L10n.swift index 8d40ad8..dc1ff4a 100644 --- a/modules/Sources/Generated/L10n.swift +++ b/modules/Sources/Generated/L10n.swift @@ -477,20 +477,10 @@ public enum L10n { } } public enum Scan { - /// We will validate any Zcash URI and take you to the appropriate action. - public static let info = L10n.tr("Localizable", "scan.info", fallback: "We will validate any Zcash URI and take you to the appropriate action.") - /// Scanning... - public static let scanning = L10n.tr("Localizable", "scan.scanning", fallback: "Scanning...") - public enum Alert { - public enum CantInitializeCamera { - /// Error: %@ (code: %@) - public static func message(_ p1: Any, _ p2: Any) -> String { - return L10n.tr("Localizable", "scan.alert.cantInitializeCamera.message", String(describing: p1), String(describing: p2), fallback: "Error: %@ (code: %@)") - } - /// Can't initialize the camera - public static let title = L10n.tr("Localizable", "scan.alert.cantInitializeCamera.title", fallback: "Can't initialize the camera") - } - } + /// The camera is not authorized. Please go to the system settings of Zashi and turn it on. + public static let cameraSettings = L10n.tr("Localizable", "scan.cameraSettings", fallback: "The camera is not authorized. Please go to the system settings of Zashi and turn it on.") + /// This QR code doesn't hold a valid Zcash address. + public static let invalidQR = L10n.tr("Localizable", "scan.invalidQR", fallback: "This QR code doesn't hold a valid Zcash address.") } public enum SecurityWarning { /// I acknowledge diff --git a/modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/Contents.json b/modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/Contents.json new file mode 100644 index 0000000..1db5e99 --- /dev/null +++ b/modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "torchOff.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/torchOff.png b/modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/torchOff.png new file mode 100644 index 0000000..0897761 Binary files /dev/null and b/modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/torchOff.png differ diff --git a/modules/Sources/Generated/Resources/Assets.xcassets/torchOn.imageset/Contents.json b/modules/Sources/Generated/Resources/Assets.xcassets/torchOn.imageset/Contents.json new file mode 100644 index 0000000..906a530 --- /dev/null +++ b/modules/Sources/Generated/Resources/Assets.xcassets/torchOn.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "torchOn.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/Sources/Generated/Resources/Assets.xcassets/torchOn.imageset/torchOn.png b/modules/Sources/Generated/Resources/Assets.xcassets/torchOn.imageset/torchOn.png new file mode 100644 index 0000000..636bf51 Binary files /dev/null and b/modules/Sources/Generated/Resources/Assets.xcassets/torchOn.imageset/torchOn.png differ diff --git a/modules/Sources/Generated/Resources/Localizable.strings b/modules/Sources/Generated/Resources/Localizable.strings index 423e3e9..eff0f91 100644 --- a/modules/Sources/Generated/Resources/Localizable.strings +++ b/modules/Sources/Generated/Resources/Localizable.strings @@ -130,10 +130,8 @@ "balances.alert.shieldFunds.failure.message" = "Error: %@ (code: %@)"; // MARK: - Scan -"scan.info" = "We will validate any Zcash URI and take you to the appropriate action."; -"scan.scanning" = "Scanning..."; -"scan.alert.cantInitializeCamera.title" = "Can't initialize the camera"; -"scan.alert.cantInitializeCamera.message" = "Error: %@ (code: %@)"; +"scan.invalidQR" = "This QR code doesn't hold a valid Zcash address."; +"scan.cameraSettings" = "The camera is not authorized. Please go to the system settings of Zashi and turn it on."; // MARK: - Send "send.title" = "Send Zcash"; diff --git a/modules/Sources/Generated/XCAssets+Generated.swift b/modules/Sources/Generated/XCAssets+Generated.swift index 690ab89..fc24d6f 100644 --- a/modules/Sources/Generated/XCAssets+Generated.swift +++ b/modules/Sources/Generated/XCAssets+Generated.swift @@ -34,6 +34,8 @@ public enum Asset { public static let share = ImageAsset(name: "share") public static let shield = ImageAsset(name: "shield") public static let surroundedShield = ImageAsset(name: "surroundedShield") + public static let torchOff = ImageAsset(name: "torchOff") + public static let torchOn = ImageAsset(name: "torchOn") public static let upArrow = ImageAsset(name: "upArrow") public static let zashiTitle = ImageAsset(name: "zashiTitle") } diff --git a/modules/Sources/UIComponents/Buttons/ZashiButton.swift b/modules/Sources/UIComponents/Buttons/ZashiButton.swift index abcbf35..4e914b8 100644 --- a/modules/Sources/UIComponents/Buttons/ZashiButton.swift +++ b/modules/Sources/UIComponents/Buttons/ZashiButton.swift @@ -28,7 +28,7 @@ public struct ZcashButtonStyle: ButtonStyle { : isEnabled ? Asset.Colors.primary.color : Asset.Colors.shade72.color ) .frame(height: height) - .border(isEnabled ? Asset.Colors.primary.color : Asset.Colors.shade72.color) + .border(appearance == .secondary && isEnabled ? Asset.Colors.secondary.color : Asset.Colors.primary.color) .offset(CGSize(width: shadowOffset, height: shadowOffset)) Rectangle() diff --git a/secantTests/ScanTests/ScanTests.swift b/secantTests/ScanTests/ScanTests.swift index e22e349..4051bf1 100644 --- a/secantTests/ScanTests/ScanTests.swift +++ b/secantTests/ScanTests/ScanTests.swift @@ -9,6 +9,7 @@ import XCTest import ComposableArchitecture import ZcashLightClientKit import Scan +import Generated @testable import secant_testnet @MainActor @@ -16,13 +17,12 @@ class ScanTests: XCTestCase { func testOnAppearResetValues() async throws { let store = TestStore( initialState: - ScanReducer.State( + Scan.State( isTorchAvailable: true, - isTorchOn: true, - scanStatus: .value("t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po".redacted) + isTorchOn: true ) ) { - ScanReducer() + Scan() } store.dependencies.captureDevice = .noOp @@ -30,7 +30,7 @@ class ScanTests: XCTestCase { await store.send(.onAppear) { state in state.isTorchAvailable = false state.isTorchOn = false - state.scanStatus = .unknown + state.info = L10n.Scan.cameraSettings } await store.finish() @@ -38,9 +38,9 @@ class ScanTests: XCTestCase { func testTorchOn() async throws { let store = TestStore( - initialState: ScanReducer.State() + initialState: Scan.State() ) { - ScanReducer() + Scan() } store.dependencies.captureDevice = .noOp @@ -54,11 +54,11 @@ class ScanTests: XCTestCase { func testTorchOff() async throws { let store = TestStore( - initialState: ScanReducer.State( + initialState: Scan.State( isTorchOn: true ) ) { - ScanReducer() + Scan() } store.dependencies.captureDevice = .noOp @@ -72,27 +72,34 @@ class ScanTests: XCTestCase { func testScannedInvalidValue() async throws { let store = TestStore( - initialState: ScanReducer.State() + initialState: Scan.State() ) { - ScanReducer() + Scan() } store.dependencies.uriParser.isValidURI = { _, _ in false } + store.dependencies.mainQueue = .immediate let value = "test".redacted - await store.send(.scan(value)) { state in - state.scanStatus = .failed + await store.send(.scan(value)) + + await store.receive(.scanFailed) { state in + state.info = L10n.Scan.invalidQR } + await store.receive(.clearInfo) { state in + state.info = "" + } + await store.finish() } - @MainActor func testScannedValidAddress() async throws { + func testScannedValidAddress() async throws { let store = TestStore( - initialState: ScanReducer.State() + initialState: Scan.State() ) { - ScanReducer() + Scan() } store.dependencies.mainQueue = .immediate @@ -100,9 +107,7 @@ class ScanTests: XCTestCase { let address = "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po".redacted - await store.send(.scan(address)) { state in - state.scanStatus = .value(address) - } + await store.send(.scan(address)) await store.receive(.found(address)) @@ -111,13 +116,19 @@ class ScanTests: XCTestCase { func testScanFailed() async throws { let store = TestStore( - initialState: ScanReducer.State() + initialState: Scan.State() ) { - ScanReducer() + Scan() } + store.dependencies.mainQueue = .immediate + await store.send(.scanFailed) { state in - state.scanStatus = .failed + state.info = L10n.Scan.invalidQR + } + + await store.receive(.clearInfo) { state in + state.info = "" } await store.finish()