[106] [Scaffold] Scan QR Screen (#321)
- scan UI view - UI representable - scaffold of scan view - rect of interest support - simple URI parser and validator - tests [106] [Scaffold] Scan QR Screen - scan status added - scan status tests [106] [Scaffold] Scan QR Screen - custom back button added [106] [Scaffold] Scan QR Screen - fixed typo [106] [Scaffold] Scan QR Screen (321) - valid vs. invalid code part of the enum [106] [Scaffold] Scan QR Screen (321) - refactor [106] [Scaffold] Scan QR Screen (321) - cleanup [106] [Scaffold] Scan QR Screen (321) - review comments solved - some improvements added - firing the valid qr code after 1s [106] [Scaffold] Scan QR Screen (321) - tests fixed - onAppear test added - print() cleanup [106] [Scaffold] Scan QR Screen (321) - wrapped audio services - vibrate when QR found - alphabetical order of environmental parameters - AVCaptureSession stopRunning explicit call, probably handled by AVCaptureSession itself but I didn't find proof - QRCodeScanView simplified and cleaned up [106] [Scaffold] Scan QR Screen (321) - URIParser added to the project
This commit is contained in:
parent
b753efbc5f
commit
4cc7737b21
|
@ -82,6 +82,10 @@
|
|||
66A0807B271993C500118B79 /* OnboardingProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */; };
|
||||
66D50668271D9B6100E51F0D /* NavigationButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D50667271D9B6100E51F0D /* NavigationButtonStyle.swift */; };
|
||||
66DC733F271D88CC0053CBB6 /* StandardButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66DC733E271D88CC0053CBB6 /* StandardButtonStyle.swift */; };
|
||||
9E01F8202833861A000EFC57 /* WrappedCaptureDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E01F81F2833861A000EFC57 /* WrappedCaptureDevice.swift */; };
|
||||
9E01F8222833BFAE000EFC57 /* URIParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E01F8212833BFAE000EFC57 /* URIParser.swift */; };
|
||||
9E01F8242833C0D8000EFC57 /* WrappedURIParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E01F8232833C0D8000EFC57 /* WrappedURIParser.swift */; };
|
||||
9E01F8282833CDA0000EFC57 /* ScanTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E01F8272833CDA0000EFC57 /* ScanTests.swift */; };
|
||||
9E02B56A27FED43E005B809B /* WrappedFileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E02B56927FED43E005B809B /* WrappedFileManager.swift */; };
|
||||
9E02B56C27FED475005B809B /* DatabaseFilesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */; };
|
||||
9E02B5C3280458D2005B809B /* WrappedDerivationTool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E02B5C2280458D2005B809B /* WrappedDerivationTool.swift */; };
|
||||
|
@ -118,7 +122,10 @@
|
|||
9E7FE0E8282E7B7C00C374E8 /* WrappedDatabaseFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0E7282E7B7C00C374E8 /* WrappedDatabaseFiles.swift */; };
|
||||
9E7FE0EC282E7D9400C374E8 /* TransactionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF63B2818305D00BA3F17 /* TransactionState.swift */; };
|
||||
9E7FE0EE282E7E8700C374E8 /* SendFlowTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0D0282D26BD00C374E8 /* SendFlowTransaction.swift */; };
|
||||
9E7FE0F628327F6F00C374E8 /* ScanUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0F528327F6F00C374E8 /* ScanUIView.swift */; };
|
||||
9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */; };
|
||||
9E80B47227E4B34B008FF493 /* UserPreferencesStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */; };
|
||||
9E87ADF128363DE400122FCC /* WrappedAudioServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E87ADF028363DE400122FCC /* WrappedAudioServices.swift */; };
|
||||
9EAFEB822805793200199FC9 /* AppReducerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB812805793200199FC9 /* AppReducerTests.swift */; };
|
||||
9EAFEB84280597B700199FC9 /* WrappedSecItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB83280597B700199FC9 /* WrappedSecItem.swift */; };
|
||||
9EAFEB862805A23100199FC9 /* WrappedSecItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EAFEB852805A23100199FC9 /* WrappedSecItemTests.swift */; };
|
||||
|
@ -265,6 +272,10 @@
|
|||
66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProgressIndicator.swift; sourceTree = "<group>"; };
|
||||
66D50667271D9B6100E51F0D /* NavigationButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationButtonStyle.swift; sourceTree = "<group>"; };
|
||||
66DC733E271D88CC0053CBB6 /* StandardButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardButtonStyle.swift; sourceTree = "<group>"; };
|
||||
9E01F81F2833861A000EFC57 /* WrappedCaptureDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedCaptureDevice.swift; sourceTree = "<group>"; };
|
||||
9E01F8212833BFAE000EFC57 /* URIParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URIParser.swift; sourceTree = "<group>"; };
|
||||
9E01F8232833C0D8000EFC57 /* WrappedURIParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedURIParser.swift; sourceTree = "<group>"; };
|
||||
9E01F8272833CDA0000EFC57 /* ScanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanTests.swift; sourceTree = "<group>"; };
|
||||
9E02B56927FED43E005B809B /* WrappedFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedFileManager.swift; sourceTree = "<group>"; };
|
||||
9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFilesTests.swift; sourceTree = "<group>"; };
|
||||
9E02B5C2280458D2005B809B /* WrappedDerivationTool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedDerivationTool.swift; sourceTree = "<group>"; };
|
||||
|
@ -300,7 +311,10 @@
|
|||
9E7FE0DE282D2DD600C374E8 /* ZcashBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZcashBadge.swift; sourceTree = "<group>"; };
|
||||
9E7FE0E5282E7B1100C374E8 /* StoredWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredWallet.swift; sourceTree = "<group>"; };
|
||||
9E7FE0E7282E7B7C00C374E8 /* WrappedDatabaseFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedDatabaseFiles.swift; sourceTree = "<group>"; };
|
||||
9E7FE0F528327F6F00C374E8 /* ScanUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanUIView.swift; sourceTree = "<group>"; };
|
||||
9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeScanView.swift; sourceTree = "<group>"; };
|
||||
9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferencesStorage.swift; sourceTree = "<group>"; };
|
||||
9E87ADF028363DE400122FCC /* WrappedAudioServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedAudioServices.swift; sourceTree = "<group>"; };
|
||||
9EAFEB812805793200199FC9 /* AppReducerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducerTests.swift; sourceTree = "<group>"; };
|
||||
9EAFEB83280597B700199FC9 /* WrappedSecItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedSecItem.swift; sourceTree = "<group>"; };
|
||||
9EAFEB852805A23100199FC9 /* WrappedSecItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedSecItemTests.swift; sourceTree = "<group>"; };
|
||||
|
@ -462,6 +476,7 @@
|
|||
0D4E7A1926B364180058B01E /* secantTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9E01F8262833CD84000EFC57 /* ScanTests */,
|
||||
9E5BF642281FEC8700BA3F17 /* SendTests */,
|
||||
9E5BF63D281953F900BA3F17 /* TransactionHistoryTests */,
|
||||
9EAFEB802805791400199FC9 /* AppReducerTests */,
|
||||
|
@ -685,6 +700,14 @@
|
|||
path = CircularFrame;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9E01F8262833CD84000EFC57 /* ScanTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9E01F8272833CDA0000EFC57 /* ScanTests.swift */,
|
||||
);
|
||||
path = ScanTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9E02B56827FED42D005B809B /* Wrappers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -699,6 +722,9 @@
|
|||
9E7FE0D8282D289B00C374E8 /* WrappedFeedbackGenerator.swift */,
|
||||
9E7FE0DA282D28F100C374E8 /* WrappedPasteboard.swift */,
|
||||
9E7FE0E7282E7B7C00C374E8 /* WrappedDatabaseFiles.swift */,
|
||||
9E01F81F2833861A000EFC57 /* WrappedCaptureDevice.swift */,
|
||||
9E01F8232833C0D8000EFC57 /* WrappedURIParser.swift */,
|
||||
9E87ADF028363DE400122FCC /* WrappedAudioServices.swift */,
|
||||
);
|
||||
path = Wrappers;
|
||||
sourceTree = "<group>";
|
||||
|
@ -808,6 +834,7 @@
|
|||
9EAFEB892806F48100199FC9 /* ZCashSDKEnvironment.swift */,
|
||||
9E80B47127E4B34B008FF493 /* UserPreferencesStorage.swift */,
|
||||
9ECAE56727FC713C0089A0EF /* DatabaseFiles.swift */,
|
||||
9E01F8212833BFAE000EFC57 /* URIParser.swift */,
|
||||
);
|
||||
path = Dependencies;
|
||||
sourceTree = "<group>";
|
||||
|
@ -885,6 +912,15 @@
|
|||
path = TCATextField;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9E7FE0F72832823100C374E8 /* UIKitBridge */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9E7FE0F528327F6F00C374E8 /* ScanUIView.swift */,
|
||||
9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */,
|
||||
);
|
||||
path = UIKitBridge;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9EAFEB802805791400199FC9 /* AppReducerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -972,6 +1008,7 @@
|
|||
children = (
|
||||
F9971A5E27680DF600A2DB75 /* ScanStore.swift */,
|
||||
F9971A5D27680DF600A2DB75 /* ScanView.swift */,
|
||||
9E7FE0F72832823100C374E8 /* UIKitBridge */,
|
||||
);
|
||||
path = Scan;
|
||||
sourceTree = "<group>";
|
||||
|
@ -1224,6 +1261,7 @@
|
|||
9EAFEB902808183D00199FC9 /* SandboxStore.swift in Sources */,
|
||||
0D35CC46277A36E00074316A /* ScrollableWhenScaled.swift in Sources */,
|
||||
F96B41E9273B501F0021B49A /* TransactionHistoryFlowView.swift in Sources */,
|
||||
9E01F8202833861A000EFC57 /* WrappedCaptureDevice.swift in Sources */,
|
||||
2EDA07A027EDE18C00D6F09B /* TCATextField.swift in Sources */,
|
||||
9E7FE0DB282D28F100C374E8 /* WrappedPasteboard.swift in Sources */,
|
||||
2EB7758727FC67FD00269373 /* TransactionAmountTextFieldStore.swift in Sources */,
|
||||
|
@ -1264,6 +1302,7 @@
|
|||
9E7FE0D3282D274E00C374E8 /* Date+Readable.swift in Sources */,
|
||||
F93673D62742CB840099C6AF /* Previews.swift in Sources */,
|
||||
0D18581B272728D60046B928 /* PhraseChip.swift in Sources */,
|
||||
9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */,
|
||||
0DF482BA2787ADA800EB37D6 /* ConditionalModifier.swift in Sources */,
|
||||
9E7FE0EC282E7D9400C374E8 /* TransactionState.swift in Sources */,
|
||||
9E2F1C8F280EDE09004E65FE /* Drawer.swift in Sources */,
|
||||
|
@ -1278,6 +1317,7 @@
|
|||
0D3D04082728B3440032ABC1 /* RecoveryPhraseDisplayView.swift in Sources */,
|
||||
F9971A5F27680DF600A2DB75 /* ScanView.swift in Sources */,
|
||||
F9971A4E27680DC400A2DB75 /* AppView.swift in Sources */,
|
||||
9E87ADF128363DE400122FCC /* WrappedAudioServices.swift in Sources */,
|
||||
2EA11F5B27467EF800709571 /* OnboardingFooterView.swift in Sources */,
|
||||
66D50668271D9B6100E51F0D /* NavigationButtonStyle.swift in Sources */,
|
||||
2EDA07A427EDE2A900D6F09B /* DebugFrame.swift in Sources */,
|
||||
|
@ -1307,6 +1347,7 @@
|
|||
9ECAE56827FC713C0089A0EF /* DatabaseFiles.swift in Sources */,
|
||||
9E5BF6462821028C00BA3F17 /* WrappedUserDefaults.swift in Sources */,
|
||||
F9971A6B27680E1000A2DB75 /* WalletInfoStore.swift in Sources */,
|
||||
9E7FE0F628327F6F00C374E8 /* ScanUIView.swift in Sources */,
|
||||
0D185819272723FF0046B928 /* ColoredChip.swift in Sources */,
|
||||
2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */,
|
||||
9E7FE0EE282E7E8700C374E8 /* SendFlowTransaction.swift in Sources */,
|
||||
|
@ -1316,6 +1357,7 @@
|
|||
0D8A43C6272B129C005A6414 /* WordChipGrid.swift in Sources */,
|
||||
66A0807B271993C500118B79 /* OnboardingProgressIndicator.swift in Sources */,
|
||||
0D7DF08C271DCC0E00530046 /* ScreenBackground.swift in Sources */,
|
||||
9E01F8242833C0D8000EFC57 /* WrappedURIParser.swift in Sources */,
|
||||
F9C165C22740403600592F76 /* CreateTransactionView.swift in Sources */,
|
||||
F9C165B4274031F600592F76 /* Bindings.swift in Sources */,
|
||||
2E35F99A27B3E99C00EB79CD /* TextFieldTitleAccessoryButtonStyle.swift in Sources */,
|
||||
|
@ -1324,6 +1366,7 @@
|
|||
F96B41EB273B50520021B49A /* Strings.swift in Sources */,
|
||||
2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */,
|
||||
F9971A5427680DD000A2DB75 /* ProfileView.swift in Sources */,
|
||||
9E01F8222833BFAE000EFC57 /* URIParser.swift in Sources */,
|
||||
F9971A6027680DF600A2DB75 /* ScanStore.swift in Sources */,
|
||||
9EF8139127F191BF0075AF48 /* WrappedWalletStorage.swift in Sources */,
|
||||
0DFE93E1272C9ECB000FCCA5 /* RecoveryPhraseBackupView.swift in Sources */,
|
||||
|
@ -1346,6 +1389,7 @@
|
|||
files = (
|
||||
0DFE93DF272C6D4B000FCCA5 /* RecoveryPhraseBackupTests.swift in Sources */,
|
||||
9EDDEAA22829610D00B4100C /* CurrencySelectionTests.swift in Sources */,
|
||||
9E01F8282833CDA0000EFC57 /* ScanTests.swift in Sources */,
|
||||
9EDDEAA42829610D00B4100C /* TransactionAddressInputTests.swift in Sources */,
|
||||
6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */,
|
||||
9EDDEAA32829610D00B4100C /* TransactionAmountInputTests.swift in Sources */,
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// URIParser.swift
|
||||
// secant-testnet
|
||||
//
|
||||
// Created by Lukáš Korba on 17.05.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct URIParser {
|
||||
enum URIParserError: Error {
|
||||
}
|
||||
|
||||
private let derivationTool: WrappedDerivationTool
|
||||
|
||||
init(derivationTool: WrappedDerivationTool) {
|
||||
self.derivationTool = derivationTool
|
||||
}
|
||||
|
||||
func isValidURI(_ uri: String) throws -> Bool {
|
||||
try derivationTool.isValidZcashAddress(uri)
|
||||
}
|
||||
}
|
|
@ -60,33 +60,39 @@ enum AppAction: Equatable {
|
|||
// MARK: - Environment
|
||||
|
||||
struct AppEnvironment {
|
||||
let SDKSynchronizer: WrappedSDKSynchronizer
|
||||
let audioServices: WrappedAudioServices
|
||||
let databaseFiles: WrappedDatabaseFiles
|
||||
let derivationTool: WrappedDerivationTool
|
||||
let feedbackGenerator: WrappedFeedbackGenerator
|
||||
let mnemonic: WrappedMnemonic
|
||||
let scheduler: AnySchedulerOf<DispatchQueue>
|
||||
let SDKSynchronizer: WrappedSDKSynchronizer
|
||||
let walletStorage: WrappedWalletStorage
|
||||
let derivationTool: WrappedDerivationTool
|
||||
let zcashSDKEnvironment: ZCashSDKEnvironment
|
||||
}
|
||||
|
||||
extension AppEnvironment {
|
||||
static let live = AppEnvironment(
|
||||
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
|
||||
audioServices: .haptic,
|
||||
databaseFiles: .live(),
|
||||
derivationTool: .live(),
|
||||
feedbackGenerator: .haptic,
|
||||
mnemonic: .live,
|
||||
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
||||
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
|
||||
walletStorage: .live(),
|
||||
derivationTool: .live(),
|
||||
zcashSDKEnvironment: .mainnet
|
||||
)
|
||||
|
||||
static let mock = AppEnvironment(
|
||||
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
|
||||
audioServices: .silent,
|
||||
databaseFiles: .live(),
|
||||
derivationTool: .live(derivationTool: DerivationTool(networkType: .mainnet)),
|
||||
feedbackGenerator: .silent,
|
||||
mnemonic: .mock,
|
||||
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
||||
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
|
||||
walletStorage: .live(),
|
||||
derivationTool: .live(derivationTool: DerivationTool(networkType: .mainnet)),
|
||||
zcashSDKEnvironment: .mainnet
|
||||
)
|
||||
}
|
||||
|
@ -94,7 +100,7 @@ extension AppEnvironment {
|
|||
// MARK: - Reducer
|
||||
|
||||
extension AppReducer {
|
||||
private struct ListenerId: Hashable {}
|
||||
private struct CancelId: Hashable {}
|
||||
|
||||
static let `default` = AppReducer.combine(
|
||||
[
|
||||
|
@ -144,7 +150,7 @@ extension AppReducer {
|
|||
return Effect(value: .updateRoute(.onboarding))
|
||||
.delay(for: 3, scheduler: environment.scheduler)
|
||||
.eraseToEffect()
|
||||
.cancellable(id: ListenerId(), cancelInFlight: true)
|
||||
.cancellable(id: CancelId(), cancelInFlight: true)
|
||||
}
|
||||
|
||||
return .none
|
||||
|
@ -205,7 +211,7 @@ extension AppReducer {
|
|||
return Effect(value: .updateRoute(landingRoute))
|
||||
.delay(for: 3, scheduler: environment.scheduler)
|
||||
.eraseToEffect()
|
||||
.cancellable(id: ListenerId(), cancelInFlight: true)
|
||||
.cancellable(id: CancelId(), cancelInFlight: true)
|
||||
|
||||
case .createNewWallet:
|
||||
do {
|
||||
|
@ -251,7 +257,7 @@ extension AppReducer {
|
|||
|
||||
case .welcome(.debugMenuStartup), .home(.debugMenuStartup):
|
||||
return .concatenate(
|
||||
Effect.cancel(id: ListenerId()),
|
||||
Effect.cancel(id: CancelId()),
|
||||
Effect(value: .updateRoute(.startup))
|
||||
)
|
||||
|
||||
|
@ -308,11 +314,13 @@ extension AppReducer {
|
|||
action: /AppAction.home,
|
||||
environment: { environment in
|
||||
HomeEnvironment(
|
||||
audioServices: environment.audioServices,
|
||||
derivationTool: environment.derivationTool,
|
||||
feedbackGenerator: environment.feedbackGenerator,
|
||||
mnemonic: environment.mnemonic,
|
||||
scheduler: environment.scheduler,
|
||||
walletStorage: environment.walletStorage,
|
||||
derivationTool: environment.derivationTool,
|
||||
SDKSynchronizer: environment.SDKSynchronizer
|
||||
SDKSynchronizer: environment.SDKSynchronizer,
|
||||
walletStorage: environment.walletStorage
|
||||
)
|
||||
}
|
||||
)
|
||||
|
@ -338,7 +346,14 @@ extension AppReducer {
|
|||
private static let phraseDisplayReducer: AppReducer = RecoveryPhraseDisplayReducer.default.pullback(
|
||||
state: \AppState.phraseDisplayState,
|
||||
action: /AppAction.phraseDisplay,
|
||||
environment: { _ in RecoveryPhraseDisplayEnvironment.live }
|
||||
environment: { environment in
|
||||
RecoveryPhraseDisplayEnvironment(
|
||||
scheduler: environment.scheduler,
|
||||
newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
|
||||
pasteboard: .live,
|
||||
feedbackGenerator: environment.feedbackGenerator
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
private static let sandboxReducer: AppReducer = SandboxReducer.default.pullback(
|
||||
|
|
|
@ -2,6 +2,9 @@ import ComposableArchitecture
|
|||
import SwiftUI
|
||||
import ZcashLightClientKit
|
||||
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
|
||||
typealias HomeReducer = Reducer<HomeState, HomeAction, HomeEnvironment>
|
||||
typealias HomeStore = Store<HomeState, HomeAction>
|
||||
typealias HomeViewStore = ViewStore<HomeState, HomeAction>
|
||||
|
@ -51,23 +54,26 @@ enum HomeAction: Equatable {
|
|||
// MARK: Environment
|
||||
|
||||
struct HomeEnvironment {
|
||||
let audioServices: WrappedAudioServices
|
||||
let derivationTool: WrappedDerivationTool
|
||||
let feedbackGenerator: WrappedFeedbackGenerator
|
||||
let mnemonic: WrappedMnemonic
|
||||
let scheduler: AnySchedulerOf<DispatchQueue>
|
||||
let walletStorage: WrappedWalletStorage
|
||||
let derivationTool: WrappedDerivationTool
|
||||
let SDKSynchronizer: WrappedSDKSynchronizer
|
||||
let walletStorage: WrappedWalletStorage
|
||||
}
|
||||
|
||||
// MARK: - Reducer
|
||||
|
||||
extension HomeReducer {
|
||||
private struct ListenerId: Hashable {}
|
||||
private struct CancelId: Hashable {}
|
||||
|
||||
static let `default` = HomeReducer.combine(
|
||||
[
|
||||
homeReducer,
|
||||
historyReducer,
|
||||
sendReducer,
|
||||
scanReducer,
|
||||
profileReducer
|
||||
]
|
||||
)
|
||||
|
@ -79,10 +85,10 @@ extension HomeReducer {
|
|||
return environment.SDKSynchronizer.stateChanged
|
||||
.map(HomeAction.synchronizerStateChanged)
|
||||
.eraseToEffect()
|
||||
.cancellable(id: ListenerId(), cancelInFlight: true)
|
||||
.cancellable(id: CancelId(), cancelInFlight: true)
|
||||
|
||||
case .onDisappear:
|
||||
return Effect.cancel(id: ListenerId())
|
||||
return Effect.cancel(id: CancelId())
|
||||
|
||||
case .synchronizerStateChanged(.synced):
|
||||
return .merge(
|
||||
|
@ -119,7 +125,7 @@ extension HomeReducer {
|
|||
case .updateSynchronizerStatus:
|
||||
state.synchronizerStatus = environment.SDKSynchronizer.status()
|
||||
return .none
|
||||
|
||||
|
||||
case .updateRoute(let route):
|
||||
state.route = route
|
||||
return .none
|
||||
|
@ -129,9 +135,6 @@ extension HomeReducer {
|
|||
|
||||
case .request(let action):
|
||||
return .none
|
||||
|
||||
case .scan(let action):
|
||||
return .none
|
||||
|
||||
case .transactionHistory(.updateRoute(.all)):
|
||||
return state.drawerOverlay != .full ? Effect(value: .updateDrawer(.full)) : .none
|
||||
|
@ -148,6 +151,13 @@ extension HomeReducer {
|
|||
case .send(let action):
|
||||
return .none
|
||||
|
||||
case .scan(.found(let code)):
|
||||
environment.audioServices.systemSoundVibrate()
|
||||
return Effect(value: .updateRoute(nil))
|
||||
|
||||
case .scan(let action):
|
||||
return .none
|
||||
|
||||
case .debugMenuStartup:
|
||||
return .none
|
||||
}
|
||||
|
@ -178,6 +188,18 @@ extension HomeReducer {
|
|||
}
|
||||
)
|
||||
|
||||
private static let scanReducer: HomeReducer = ScanReducer.default.pullback(
|
||||
state: \HomeState.scanState,
|
||||
action: /HomeAction.scan,
|
||||
environment: { environment in
|
||||
ScanEnvironment(
|
||||
captureDevice: .real,
|
||||
scheduler: environment.scheduler,
|
||||
uriParser: .live(uriParser: URIParser(derivationTool: environment.derivationTool))
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
private static let profileReducer: HomeReducer = ProfileReducer.default.pullback(
|
||||
state: \HomeState.profileState,
|
||||
action: /HomeAction.profile,
|
||||
|
@ -273,11 +295,13 @@ extension HomeStore {
|
|||
initialState: .placeholder,
|
||||
reducer: .default.debug(),
|
||||
environment: HomeEnvironment(
|
||||
audioServices: .silent,
|
||||
derivationTool: .live(),
|
||||
feedbackGenerator: .silent,
|
||||
mnemonic: .live,
|
||||
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
||||
walletStorage: .live(),
|
||||
derivationTool: .live(),
|
||||
SDKSynchronizer: LiveWrappedSDKSynchronizer()
|
||||
SDKSynchronizer: LiveWrappedSDKSynchronizer(),
|
||||
walletStorage: .live()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ enum RecoveryPhraseDisplayAction: Equatable {
|
|||
// MARK: - Environment
|
||||
|
||||
struct RecoveryPhraseDisplayEnvironment {
|
||||
let mainQueue: AnySchedulerOf<DispatchQueue>
|
||||
let scheduler: AnySchedulerOf<DispatchQueue>
|
||||
let newPhrase: () -> Effect<RecoveryPhrase, RecoveryPhraseError>
|
||||
let pasteboard: WrappedPasteboard
|
||||
let feedbackGenerator: WrappedFeedbackGenerator
|
||||
|
@ -44,18 +44,11 @@ extension RecoveryPhraseDisplayEnvironment {
|
|||
}
|
||||
|
||||
static let demo = Self(
|
||||
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
|
||||
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
||||
newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
|
||||
pasteboard: .test,
|
||||
feedbackGenerator: .silent
|
||||
)
|
||||
|
||||
static let live = Self(
|
||||
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
|
||||
newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
|
||||
pasteboard: .live,
|
||||
feedbackGenerator: .haptic
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Reducer
|
||||
|
@ -65,7 +58,7 @@ extension RecoveryPhraseDisplayReducer {
|
|||
switch action {
|
||||
case .createPhrase:
|
||||
return environment.newPhrase()
|
||||
.receive(on: environment.mainQueue)
|
||||
.receive(on: environment.scheduler)
|
||||
.catchToEffect(RecoveryPhraseDisplayAction.phraseResponse)
|
||||
case .copyToBufferPressed:
|
||||
guard let phrase = state.phrase?.toString() else { return .none }
|
||||
|
|
|
@ -133,7 +133,7 @@ enum RecoveryPhraseValidationFlowAction: Equatable {
|
|||
// MARK: - Environment
|
||||
|
||||
struct RecoveryPhraseValidationFlowEnvironment {
|
||||
let mainQueue: AnySchedulerOf<DispatchQueue>
|
||||
let scheduler: AnySchedulerOf<DispatchQueue>
|
||||
let newPhrase: () -> Effect<RecoveryPhrase, RecoveryPhraseError>
|
||||
let pasteboard: WrappedPasteboard
|
||||
let feedbackGenerator: WrappedFeedbackGenerator
|
||||
|
@ -146,14 +146,14 @@ extension RecoveryPhraseValidationFlowEnvironment {
|
|||
}
|
||||
|
||||
static let demo = Self(
|
||||
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
|
||||
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
||||
newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
|
||||
pasteboard: .test,
|
||||
feedbackGenerator: .silent
|
||||
)
|
||||
|
||||
static let live = Self(
|
||||
mainQueue: DispatchQueue.main.eraseToAnyScheduler(),
|
||||
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
||||
newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
|
||||
pasteboard: .live,
|
||||
feedbackGenerator: .haptic
|
||||
|
@ -182,7 +182,7 @@ extension RecoveryPhraseValidationFlowReducer {
|
|||
if state.isComplete {
|
||||
let value: RecoveryPhraseValidationFlowAction = state.isValid ? .succeed : .fail
|
||||
let effect = Effect<RecoveryPhraseValidationFlowAction, Never>(value: value)
|
||||
.delay(for: 1, scheduler: environment.mainQueue)
|
||||
.delay(for: 1, scheduler: environment.scheduler)
|
||||
.eraseToEffect()
|
||||
|
||||
if value == .succeed {
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
//
|
||||
// ScanUIView.swift
|
||||
// secant-testnet
|
||||
//
|
||||
// Created by Lukáš Korba on 16.05.2022.
|
||||
//
|
||||
|
||||
import ComposableArchitecture
|
||||
|
||||
typealias ScanReducer = Reducer<ScanState, ScanAction, ScanEnvironment>
|
||||
|
@ -7,24 +14,107 @@ typealias ScanViewStore = ViewStore<ScanState, ScanAction>
|
|||
// MARK: - State
|
||||
|
||||
struct ScanState: Equatable {
|
||||
enum ScanStatus: Equatable {
|
||||
case failed
|
||||
case value(String)
|
||||
case unknown
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Action
|
||||
|
||||
enum ScanAction: Equatable {
|
||||
case noOp
|
||||
case onAppear
|
||||
case onDisappear
|
||||
case found(String)
|
||||
case scanFailed
|
||||
case scan(String)
|
||||
case torchPressed
|
||||
}
|
||||
|
||||
// MARK: - Environment
|
||||
|
||||
struct ScanEnvironment { }
|
||||
struct ScanEnvironment {
|
||||
let captureDevice: WrappedCaptureDevice
|
||||
let scheduler: AnySchedulerOf<DispatchQueue>
|
||||
let uriParser: WrappedURIParser
|
||||
}
|
||||
|
||||
// MARK: - Reducer
|
||||
|
||||
extension ScanReducer {
|
||||
static let `default` = ScanReducer { _, action, _ in
|
||||
private struct CancelId: Hashable {}
|
||||
|
||||
static let `default` = ScanReducer { state, action, environment in
|
||||
switch action {
|
||||
default:
|
||||
case .onAppear:
|
||||
// reset the values
|
||||
state.scanStatus = .unknown
|
||||
state.isValidValue = false
|
||||
state.isTorchOn = false
|
||||
// check the torch availability
|
||||
do {
|
||||
state.isTorchAvailable = try environment.captureDevice.isTorchAvailable()
|
||||
} catch {
|
||||
// TODO: handle torch errors, issue #322 (https://github.com/zcash/secant-ios-wallet/issues/322)
|
||||
}
|
||||
return .none
|
||||
|
||||
case .onDisappear:
|
||||
return Effect.cancel(id: CancelId())
|
||||
|
||||
case .found(let code):
|
||||
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 {
|
||||
return .none
|
||||
}
|
||||
state.scanStatus = .value(code)
|
||||
state.isValidValue = false
|
||||
do {
|
||||
if try environment.uriParser.isValidURI(code) {
|
||||
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(
|
||||
Effect.cancel(id: CancelId()),
|
||||
Effect(value: .found(code))
|
||||
.delay(for: 1.0, scheduler: environment.scheduler)
|
||||
.eraseToEffect()
|
||||
.cancellable(id: CancelId(), cancelInFlight: true)
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
state.scanStatus = .failed
|
||||
}
|
||||
return Effect.cancel(id: CancelId())
|
||||
|
||||
case .torchPressed:
|
||||
do {
|
||||
try environment.captureDevice.torch(!state.isTorchOn)
|
||||
state.isTorchOn.toggle()
|
||||
} catch {
|
||||
// TODO: handle torch errors, issue #322 (https://github.com/zcash/secant-ios-wallet/issues/322)
|
||||
}
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
@ -42,6 +132,10 @@ extension ScanStore {
|
|||
static let placeholder = ScanStore(
|
||||
initialState: .placeholder,
|
||||
reducer: .default,
|
||||
environment: ScanEnvironment()
|
||||
environment: ScanEnvironment(
|
||||
captureDevice: .real,
|
||||
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
||||
uriParser: .live()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,16 +1,148 @@
|
|||
//
|
||||
// ScanStore.swift
|
||||
// secant-testnet
|
||||
//
|
||||
// Created by Lukáš Korba on 16.05.2022.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
|
||||
struct ScanView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
let store: ScanStore
|
||||
|
||||
var body: some View {
|
||||
WithViewStore(store) { _ in
|
||||
Text("\(String(describing: Self.self)) PlaceHolder")
|
||||
WithViewStore(store) { viewStore in
|
||||
GeometryReader { proxy in
|
||||
ZStack {
|
||||
QRCodeScanView(
|
||||
rectOfInterest: normalizedRectOfInterest(proxy.size),
|
||||
onQRScanningDidFail: { viewStore.send(.scanFailed) },
|
||||
onQRScanningSucceededWithCode: { viewStore.send(.scan($0)) }
|
||||
)
|
||||
|
||||
backButton
|
||||
|
||||
if viewStore.isTorchAvailable {
|
||||
torchButton(viewStore)
|
||||
}
|
||||
|
||||
frameOfInterest(proxy.size)
|
||||
|
||||
VStack {
|
||||
Spacer()
|
||||
|
||||
Text("We will validate any Zcash URI and take you to the appropriate action.")
|
||||
.padding(.bottom, 10)
|
||||
|
||||
if let scannedValue = viewStore.scannedValue {
|
||||
Text("\(scannedValue)")
|
||||
.foregroundColor(viewStore.isValidValue ? .green : .red)
|
||||
} else {
|
||||
Text("Scanning...")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
.applyScreenBackground()
|
||||
.onAppear { viewStore.send(.onAppear) }
|
||||
.onDisappear { viewStore.send(.onDisappear) }
|
||||
}
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension ScanView {
|
||||
var backButton: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Button(action: {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}, label: {
|
||||
Image(systemName: "arrow.backward")
|
||||
.foregroundColor(Asset.Colors.QRScan.frame.color)
|
||||
.font(.system(size: 30.0))
|
||||
})
|
||||
.padding(.top, 10)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.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.QRScan.frame.color)
|
||||
.font(.system(size: 30.0))
|
||||
}
|
||||
)
|
||||
.padding(.top, 10)
|
||||
}
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
func frameOfInterest(_ size: CGSize) -> some View {
|
||||
RoundedRectangle(cornerSize: CGSize(width: 5.0, height: 5.0))
|
||||
.stroke(Asset.Colors.QRScan.frame.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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension ScanView {
|
||||
func frameSize(_ size: CGSize) -> CGFloat {
|
||||
size.width * 0.55
|
||||
}
|
||||
|
||||
func rectOfInterest(_ size: CGSize) -> CGRect {
|
||||
CGRect(
|
||||
x: size.width * 0.5,
|
||||
y: size.height * 0.5,
|
||||
width: frameSize(size),
|
||||
height: frameSize(size)
|
||||
)
|
||||
}
|
||||
|
||||
func normalizedRectOfInterest(_ size: CGSize) -> CGRect {
|
||||
CGRect(
|
||||
x: 0.25,
|
||||
y: 0.25,
|
||||
width: 0.5,
|
||||
height: 0.5
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct ScanView_Previews: PreviewProvider {
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// QRCodeScanView.swift
|
||||
// secant-testnet
|
||||
//
|
||||
// Created by Lukáš Korba on 16.05.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
struct QRCodeScanView: UIViewRepresentable {
|
||||
let rectOfInterest: CGRect
|
||||
let onQRScanningDidFail: () -> Void
|
||||
let onQRScanningSucceededWithCode: (String) -> Void
|
||||
|
||||
public func makeUIView(context: UIViewRepresentableContext<QRCodeScanView>) -> ScanUIView {
|
||||
let view = ScanUIView()
|
||||
view.rectOfInterest = rectOfInterest
|
||||
view.onQRScanningDidFail = onQRScanningDidFail
|
||||
view.onQRScanningSucceededWithCode = onQRScanningSucceededWithCode
|
||||
return view
|
||||
}
|
||||
|
||||
public func updateUIView(_ uiView: ScanUIView, context: UIViewRepresentableContext<QRCodeScanView>) { }
|
||||
|
||||
public typealias UIViewType = ScanUIView
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
//
|
||||
// ScanUIView.swift
|
||||
// secant-testnet
|
||||
//
|
||||
// Created by Lukáš Korba on 16.05.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AVFoundation
|
||||
|
||||
public class ScanUIView: UIView {
|
||||
var captureSession: AVCaptureSession?
|
||||
var metadataOutput: AVCaptureMetadataOutput?
|
||||
|
||||
/// Rect of interest = area of the camera view used to try to recognize the qr codes.
|
||||
private var internalRectOfInterest = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
|
||||
var rectOfInterest: CGRect {
|
||||
get { internalRectOfInterest }
|
||||
set {
|
||||
internalRectOfInterest = newValue
|
||||
metadataOutput?.rectOfInterest = internalRectOfInterest
|
||||
}
|
||||
}
|
||||
|
||||
var onQRScanningDidFail: (() -> Void)?
|
||||
var onQRScanningSucceededWithCode: ((String) -> Void)?
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
doInitialSetup()
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
doInitialSetup()
|
||||
}
|
||||
|
||||
deinit {
|
||||
captureSession?.stopRunning()
|
||||
}
|
||||
|
||||
override public class var layerClass: AnyClass {
|
||||
return AVCaptureVideoPreviewLayer.self
|
||||
}
|
||||
|
||||
override public var layer: AVCaptureVideoPreviewLayer {
|
||||
return super.layer as? AVCaptureVideoPreviewLayer ?? AVCaptureVideoPreviewLayer()
|
||||
}
|
||||
}
|
||||
|
||||
extension ScanUIView {
|
||||
private func doInitialSetup() {
|
||||
clipsToBounds = true
|
||||
captureSession = AVCaptureSession()
|
||||
|
||||
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
|
||||
scanningDidFail()
|
||||
return
|
||||
}
|
||||
|
||||
let videoInput: AVCaptureDeviceInput
|
||||
do {
|
||||
videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
|
||||
} catch {
|
||||
scanningDidFail()
|
||||
return
|
||||
}
|
||||
|
||||
if captureSession?.canAddInput(videoInput) ?? false {
|
||||
captureSession?.addInput(videoInput)
|
||||
} else {
|
||||
scanningDidFail()
|
||||
return
|
||||
}
|
||||
|
||||
metadataOutput = AVCaptureMetadataOutput()
|
||||
|
||||
if let metadataOutput = metadataOutput, captureSession?.canAddOutput(metadataOutput) ?? false {
|
||||
captureSession?.addOutput(metadataOutput)
|
||||
|
||||
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
||||
metadataOutput.metadataObjectTypes = [.qr]
|
||||
} else {
|
||||
scanningDidFail()
|
||||
return
|
||||
}
|
||||
|
||||
self.layer.session = captureSession
|
||||
self.layer.videoGravity = .resizeAspectFill
|
||||
|
||||
captureSession?.commitConfiguration()
|
||||
captureSession?.startRunning()
|
||||
}
|
||||
|
||||
func scanningDidFail() {
|
||||
onQRScanningDidFail?()
|
||||
captureSession = nil
|
||||
}
|
||||
|
||||
func found(code: String) {
|
||||
onQRScanningSucceededWithCode?(code)
|
||||
}
|
||||
}
|
||||
|
||||
extension ScanUIView: AVCaptureMetadataOutputObjectsDelegate {
|
||||
public func metadataOutput(
|
||||
_ output: AVCaptureMetadataOutput,
|
||||
didOutput metadataObjects: [AVMetadataObject],
|
||||
from connection: AVCaptureConnection
|
||||
) {
|
||||
if let metadataObject = metadataObjects.first {
|
||||
guard let readableObject = metadataObject as? AVMetadataMachineReadableCodeObject else { return }
|
||||
guard let stringValue = readableObject.stringValue else { return }
|
||||
found(code: stringValue)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ struct TransactionHistoryFlowEnvironment {
|
|||
// MARK: - Reducer
|
||||
|
||||
extension TransactionHistoryFlowReducer {
|
||||
private struct ListenerId: Hashable {}
|
||||
private struct CancelId: Hashable {}
|
||||
|
||||
static let `default` = TransactionHistoryFlowReducer { state, action, environment in
|
||||
switch action {
|
||||
|
@ -48,10 +48,10 @@ extension TransactionHistoryFlowReducer {
|
|||
return environment.SDKSynchronizer.stateChanged
|
||||
.map(TransactionHistoryFlowAction.synchronizerStateChanged)
|
||||
.eraseToEffect()
|
||||
.cancellable(id: ListenerId(), cancelInFlight: true)
|
||||
.cancellable(id: CancelId(), cancelInFlight: true)
|
||||
|
||||
case .onDisappear:
|
||||
return Effect.cancel(id: ListenerId())
|
||||
return Effect.cancel(id: CancelId())
|
||||
|
||||
case .synchronizerStateChanged(.synced):
|
||||
return environment.SDKSynchronizer.getAllTransactions()
|
||||
|
|
|
@ -25,6 +25,8 @@
|
|||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Scan zAddress Qr Codes</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0x00",
|
||||
"green" : "0xB8",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -23,9 +23,9 @@
|
|||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.000",
|
||||
"green" : "0.725",
|
||||
"red" : "1.000"
|
||||
"blue" : "0x00",
|
||||
"green" : "0xB8",
|
||||
"red" : "0xFF"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
|
|
|
@ -85,6 +85,9 @@ internal enum Asset {
|
|||
internal static let gradientRight = ColorAsset(name: "GradientRight")
|
||||
internal static let negativeSpace = ColorAsset(name: "NegativeSpace")
|
||||
}
|
||||
internal enum QRScan {
|
||||
internal static let frame = ColorAsset(name: "frame")
|
||||
}
|
||||
internal enum ScreenBackground {
|
||||
internal static let gradientEnd = ColorAsset(name: "gradientEnd")
|
||||
internal static let gradientStart = ColorAsset(name: "gradientStart")
|
||||
|
|
|
@ -39,11 +39,11 @@ struct EnumeratedChip: View {
|
|||
.cornerRadius(6)
|
||||
.shadow(color: Asset.Colors.Shadow.numberedTextShadow.color, radius: 3, x: 0, y: 1)
|
||||
.overlay(
|
||||
GeometryReader { geometry in
|
||||
GeometryReader { proxy in
|
||||
Text("\(index)")
|
||||
.foregroundColor(Asset.Colors.Text.highlightedSuperscriptText.color)
|
||||
.font(.custom(FontFamily.Roboto.bold.name, size: 10))
|
||||
.frame(width: geometry.size.width, height: geometry.size.height, alignment: .topLeading)
|
||||
.frame(width: proxy.size.width, height: proxy.size.height, alignment: .topLeading)
|
||||
.padding(.leading, basePadding)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// WrappedAudioServices.swift
|
||||
// secant-testnet
|
||||
//
|
||||
// Created by Lukáš Korba on 19.05.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
struct WrappedAudioServices {
|
||||
let systemSoundVibrate: () -> Void
|
||||
}
|
||||
|
||||
extension WrappedAudioServices {
|
||||
static let haptic = WrappedAudioServices(
|
||||
systemSoundVibrate: { AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate)) }
|
||||
)
|
||||
|
||||
static let silent = WrappedAudioServices(
|
||||
systemSoundVibrate: { }
|
||||
)
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// WrappedCaptureDevice.swift
|
||||
// secant-testnet
|
||||
//
|
||||
// Created by Lukáš Korba on 17.05.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
|
||||
struct WrappedCaptureDevice {
|
||||
enum WrappedCaptureDeviceError: Error {
|
||||
case captureDeviceFailed
|
||||
case lockFailed
|
||||
case torchUnavailable
|
||||
}
|
||||
|
||||
let isTorchAvailable: () throws -> Bool
|
||||
let torch: (Bool) throws -> Void
|
||||
}
|
||||
|
||||
extension WrappedCaptureDevice {
|
||||
static let real = WrappedCaptureDevice(
|
||||
isTorchAvailable: {
|
||||
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
|
||||
throw WrappedCaptureDeviceError.captureDeviceFailed
|
||||
}
|
||||
|
||||
return videoCaptureDevice.hasTorch
|
||||
},
|
||||
torch: { isTorchOn in
|
||||
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else {
|
||||
throw WrappedCaptureDeviceError.captureDeviceFailed
|
||||
}
|
||||
|
||||
guard videoCaptureDevice.hasTorch else {
|
||||
throw WrappedCaptureDeviceError.torchUnavailable
|
||||
}
|
||||
|
||||
do {
|
||||
try videoCaptureDevice.lockForConfiguration()
|
||||
videoCaptureDevice.torchMode = isTorchOn ? .on : .off
|
||||
videoCaptureDevice.unlockForConfiguration()
|
||||
} catch {
|
||||
throw WrappedCaptureDeviceError.lockFailed
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
static let none = WrappedCaptureDevice(
|
||||
isTorchAvailable: { false },
|
||||
torch: { _ in }
|
||||
)
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// WrappedURIParser.swift
|
||||
// secant-testnet
|
||||
//
|
||||
// Created by Lukáš Korba on 17.05.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct WrappedURIParser {
|
||||
let isValidURI: (String) throws -> Bool
|
||||
}
|
||||
|
||||
extension WrappedURIParser {
|
||||
static func live(uriParser: URIParser = URIParser(derivationTool: .live())) -> Self {
|
||||
Self(
|
||||
isValidURI: { uri in
|
||||
try uriParser.isValidURI(uri)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -13,23 +13,27 @@ class AppReducerTests: XCTestCase {
|
|||
static let testScheduler = DispatchQueue.test
|
||||
|
||||
let testEnvironment = AppEnvironment(
|
||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||
audioServices: .silent,
|
||||
databaseFiles: .throwing,
|
||||
derivationTool: .live(),
|
||||
feedbackGenerator: .silent,
|
||||
mnemonic: .mock,
|
||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||
walletStorage: .throwing,
|
||||
derivationTool: .live(),
|
||||
zcashSDKEnvironment: .mainnet
|
||||
)
|
||||
|
||||
func testWalletInitializationState_Uninitialized() throws {
|
||||
let uninitializedEnvironment = AppEnvironment(
|
||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||
audioServices: .silent,
|
||||
databaseFiles: .throwing,
|
||||
derivationTool: .live(),
|
||||
feedbackGenerator: .silent,
|
||||
mnemonic: .mock,
|
||||
scheduler: DispatchQueue.test.eraseToAnyScheduler(),
|
||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||
walletStorage: .throwing,
|
||||
derivationTool: .live(),
|
||||
zcashSDKEnvironment: .mainnet
|
||||
)
|
||||
|
||||
|
@ -46,12 +50,14 @@ class AppReducerTests: XCTestCase {
|
|||
)
|
||||
|
||||
let keysMissingEnvironment = AppEnvironment(
|
||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||
audioServices: .silent,
|
||||
databaseFiles: .live(databaseFiles: DatabaseFiles(fileManager: wfmMock)),
|
||||
derivationTool: .live(),
|
||||
feedbackGenerator: .silent,
|
||||
mnemonic: .mock,
|
||||
scheduler: Self.testScheduler.eraseToAnyScheduler(),
|
||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||
walletStorage: .throwing,
|
||||
derivationTool: .live(),
|
||||
zcashSDKEnvironment: .mainnet
|
||||
)
|
||||
|
||||
|
@ -68,12 +74,14 @@ class AppReducerTests: XCTestCase {
|
|||
)
|
||||
|
||||
let keysMissingEnvironment = AppEnvironment(
|
||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||
audioServices: .silent,
|
||||
databaseFiles: .live(databaseFiles: DatabaseFiles(fileManager: wfmMock)),
|
||||
derivationTool: .live(),
|
||||
feedbackGenerator: .silent,
|
||||
mnemonic: .mock,
|
||||
scheduler: Self.testScheduler.eraseToAnyScheduler(),
|
||||
SDKSynchronizer: TestWrappedSDKSynchronizer(),
|
||||
walletStorage: .throwing,
|
||||
derivationTool: .live(),
|
||||
zcashSDKEnvironment: .testnet
|
||||
)
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ class RecoveryPhraseValidationTests: XCTestCase {
|
|||
static let testScheduler = DispatchQueue.test
|
||||
|
||||
let testEnvironment = RecoveryPhraseValidationFlowEnvironment(
|
||||
mainQueue: testScheduler.eraseToAnyScheduler(),
|
||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||
newPhrase: { Effect(value: .init(words: RecoveryPhrase.placeholder.words)) },
|
||||
pasteboard: .test,
|
||||
feedbackGenerator: .silent
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
//
|
||||
// ScanTests.swift
|
||||
// secantTests
|
||||
//
|
||||
// Created by Lukáš Korba on 17.05.2022.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import secant_testnet
|
||||
import ComposableArchitecture
|
||||
import ZcashLightClientKit
|
||||
|
||||
class ScanTests: XCTestCase {
|
||||
func testOnAppearResetValues() throws {
|
||||
let store = TestStore(
|
||||
initialState:
|
||||
ScanState(
|
||||
isTorchAvailable: true,
|
||||
isTorchOn: true,
|
||||
isValidValue: true,
|
||||
scanStatus: .value("t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po")
|
||||
),
|
||||
reducer: ScanReducer.default,
|
||||
environment: ScanEnvironment(
|
||||
captureDevice: .none,
|
||||
scheduler: DispatchQueue.test.eraseToAnyScheduler(),
|
||||
uriParser: .live()
|
||||
)
|
||||
)
|
||||
|
||||
store.send(.onAppear) { state in
|
||||
state.isTorchAvailable = false
|
||||
state.isTorchOn = false
|
||||
state.isValidValue = false
|
||||
state.scanStatus = .unknown
|
||||
}
|
||||
}
|
||||
|
||||
func testTorchOn() throws {
|
||||
let store = TestStore(
|
||||
initialState: ScanState(),
|
||||
reducer: ScanReducer.default,
|
||||
environment: ScanEnvironment(
|
||||
captureDevice: .none,
|
||||
scheduler: DispatchQueue.test.eraseToAnyScheduler(),
|
||||
uriParser: .live()
|
||||
)
|
||||
)
|
||||
|
||||
store.send(.torchPressed) { state in
|
||||
state.isTorchOn = true
|
||||
}
|
||||
}
|
||||
|
||||
func testTorchOff() throws {
|
||||
let store = TestStore(
|
||||
initialState: ScanState(
|
||||
isTorchOn: true
|
||||
),
|
||||
reducer: ScanReducer.default,
|
||||
environment: ScanEnvironment(
|
||||
captureDevice: .none,
|
||||
scheduler: DispatchQueue.test.eraseToAnyScheduler(),
|
||||
uriParser: .live()
|
||||
)
|
||||
)
|
||||
|
||||
store.send(.torchPressed) { state in
|
||||
state.isTorchOn = false
|
||||
}
|
||||
}
|
||||
|
||||
func testScannedInvalidValue() throws {
|
||||
let store = TestStore(
|
||||
initialState: ScanState(),
|
||||
reducer: ScanReducer.default,
|
||||
environment: ScanEnvironment(
|
||||
captureDevice: .none,
|
||||
scheduler: DispatchQueue.test.eraseToAnyScheduler(),
|
||||
uriParser: .live()
|
||||
)
|
||||
)
|
||||
|
||||
store.send(.scan("test")) { state in
|
||||
state.scanStatus = .value("test")
|
||||
state.isValidValue = false
|
||||
}
|
||||
}
|
||||
|
||||
func testScannedValidAddress() throws {
|
||||
let testScheduler = DispatchQueue.test
|
||||
|
||||
let store = TestStore(
|
||||
initialState: ScanState(),
|
||||
reducer: ScanReducer.default,
|
||||
environment: ScanEnvironment(
|
||||
captureDevice: .none,
|
||||
scheduler: testScheduler.eraseToAnyScheduler(),
|
||||
uriParser: .live()
|
||||
)
|
||||
)
|
||||
|
||||
store.send(.scan("t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po")) { state in
|
||||
state.scanStatus = .value("t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po")
|
||||
state.isValidValue = true
|
||||
}
|
||||
|
||||
testScheduler.advance(by: 1.01)
|
||||
|
||||
store.receive(.found("t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po"))
|
||||
}
|
||||
|
||||
func testScanFailed() throws {
|
||||
let store = TestStore(
|
||||
initialState: ScanState(),
|
||||
reducer: ScanReducer.default,
|
||||
environment: ScanEnvironment(
|
||||
captureDevice: .none,
|
||||
scheduler: DispatchQueue.test.eraseToAnyScheduler(),
|
||||
uriParser: .live()
|
||||
)
|
||||
)
|
||||
|
||||
store.send(.scanFailed) { state in
|
||||
state.scanStatus = .failed
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue