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()