[#988] Scan UI
- new layout for the scan screen [#988] Scan UI - new layout and design for scan - camera not authorized case + UI - scan store refactored to the latest TCA - unit test fixed [#988] Scan UI (#1069) - changelog update
This commit is contained in:
parent
32b7afb867
commit
5a96727b01
|
@ -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.
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1520"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Scan"
|
||||
BuildableName = "Scan"
|
||||
BlueprintName = "Scan"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Scan"
|
||||
BuildableName = "Scan"
|
||||
BlueprintName = "Scan"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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 }
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -42,7 +42,7 @@ public struct SandboxView: View {
|
|||
case .recoveryPhraseDisplay:
|
||||
RecoveryPhraseDisplayView(store: RecoveryPhraseDisplay.placeholder)
|
||||
case .scan:
|
||||
ScanView(store: .placeholder)
|
||||
ScanView(store: Scan.placeholder)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<ScanReducer.State, ScanReducer.Action>
|
||||
public typealias ScanViewStore = ViewStore<ScanReducer.State, ScanReducer.Action>
|
||||
|
||||
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<Action>?
|
||||
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<Action>)
|
||||
case cancelPressed
|
||||
case clearInfo
|
||||
case onAppear
|
||||
case onDisappear
|
||||
case found(RedactableString)
|
||||
|
@ -79,98 +57,54 @@ public struct ScanReducer: Reducer {
|
|||
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())
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,134 +11,165 @@ import Generated
|
|||
import UIComponents
|
||||
|
||||
public struct ScanView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
let store: StoreOf<Scan>
|
||||
|
||||
let store: ScanStore
|
||||
|
||||
public init(store: ScanStore) {
|
||||
|
||||
public init(store: StoreOf<Scan>) {
|
||||
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<Scan>, 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<Mask: View>(
|
||||
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<Scan>(
|
||||
initialState: .initial
|
||||
) {
|
||||
Scan()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Scan> {
|
||||
self.scope(
|
||||
state: \.scanState,
|
||||
action: SendFlowReducer.Action.scan
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
12
modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/Contents.json
vendored
Normal file
12
modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "torchOff.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/torchOff.png
vendored
Normal file
BIN
modules/Sources/Generated/Resources/Assets.xcassets/torchOff.imageset/torchOff.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.6 KiB |
12
modules/Sources/Generated/Resources/Assets.xcassets/torchOn.imageset/Contents.json
vendored
Normal file
12
modules/Sources/Generated/Resources/Assets.xcassets/torchOn.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "torchOn.png",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
modules/Sources/Generated/Resources/Assets.xcassets/torchOn.imageset/torchOn.png
vendored
Normal file
BIN
modules/Sources/Generated/Resources/Assets.xcassets/torchOn.imageset/torchOn.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.9 KiB |
|
@ -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";
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Reference in New Issue