From 3abd221b822a01f1ce6bb533f5290497249307db Mon Sep 17 00:00:00 2001 From: Francisco Gindre Date: Wed, 24 Jun 2020 23:26:14 -0300 Subject: [PATCH] starting to wire app the screens --- Podfile | 4 +- Podfile.lock | 9 +- Zircles.xcodeproj/project.pbxproj | 68 ++++++ Zircles/AppDelegate.swift | 2 +- Zircles/CombineSynchronizer.swift | 161 +++++++++++++ Zircles/Environment/ZirclesEnvironment.swift | 221 ++++++++++++++++++ Zircles/SceneDelegate.swift | 11 +- Zircles/Utils/BalanceUtils.swift | 61 +++++ Zircles/Utils/CameraAccessHelper.swift | 38 +++ Zircles/Utils/FirstScreen.swift | 32 +++ .../Utils/MnemonicSeedPhraseHandling.swift | 38 +++ .../Utils/MnemonicSeedPhraseProvider.swift | 37 +++ Zircles/Utils/QRCodeGenerator.swift | 51 ++++ Zircles/Utils/SeedManagement.swift | 115 +++++++++ Zircles/Utils/SimpleLogger.swift | 74 ++++++ Zircles/Utils/String+Zcash.swift | 24 ++ Zircles/Utils/URL+Zcash.swift | 38 +++ Zircles/Utils/ZircleData.swift | 24 ++ Zircles/Utils/zECC+SwiftUI.swift | 16 ++ Zircles/Views/AppDetails.swift | 84 +++++++ .../Views/CreateNewZircleDescription.swift | 2 +- Zircles/Views/SplashScreen.swift | 81 ++++--- Zircles/Views/WelcomeView.swift | 49 +++- 23 files changed, 1178 insertions(+), 62 deletions(-) create mode 100644 Zircles/CombineSynchronizer.swift create mode 100644 Zircles/Environment/ZirclesEnvironment.swift create mode 100644 Zircles/Utils/BalanceUtils.swift create mode 100644 Zircles/Utils/CameraAccessHelper.swift create mode 100644 Zircles/Utils/FirstScreen.swift create mode 100644 Zircles/Utils/MnemonicSeedPhraseHandling.swift create mode 100644 Zircles/Utils/MnemonicSeedPhraseProvider.swift create mode 100644 Zircles/Utils/QRCodeGenerator.swift create mode 100644 Zircles/Utils/SeedManagement.swift create mode 100644 Zircles/Utils/SimpleLogger.swift create mode 100644 Zircles/Utils/String+Zcash.swift create mode 100644 Zircles/Utils/URL+Zcash.swift create mode 100644 Zircles/Utils/ZircleData.swift create mode 100644 Zircles/Utils/zECC+SwiftUI.swift create mode 100644 Zircles/Views/AppDetails.swift diff --git a/Podfile b/Podfile index 257db07..94d3dca 100644 --- a/Podfile +++ b/Podfile @@ -3,7 +3,7 @@ use_frameworks! inhibit_all_warnings! target 'Zircles' do - pod 'ZcashLightClientKit' + pod 'ZcashLightClientKit', :path => "../hackathon-ZcashLightClientKit" pod 'KeychainSwift', '~> 19.0.0' pod 'MnemonicSwift' pod 'lottie-ios' @@ -25,4 +25,4 @@ post_install do |installer| config.build_settings['ENABLE_BITCODE'] = 'NO' end end -end \ No newline at end of file +end diff --git a/Podfile.lock b/Podfile.lock index 8991ce3..2ea6d01 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -69,7 +69,7 @@ DEPENDENCIES: - KeychainSwift (~> 19.0.0) - lottie-ios - MnemonicSwift - - ZcashLightClientKit + - ZcashLightClientKit (from `../hackathon-ZcashLightClientKit`) SPEC REPOS: trunk: @@ -97,7 +97,10 @@ SPEC REPOS: - SwiftNIOTLS - SwiftNIOTransportServices - SwiftProtobuf - - ZcashLightClientKit + +EXTERNAL SOURCES: + ZcashLightClientKit: + :path: "../hackathon-ZcashLightClientKit" SPEC CHECKSUMS: CGRPCZlib: 06247b0687f3a3edbfbfb204d53721c3ba262a34 @@ -126,6 +129,6 @@ SPEC CHECKSUMS: SwiftProtobuf: ecbec1be9036d15655f6b3443a1c4ea693c97932 ZcashLightClientKit: d23fd0513acf929deb5b2a7ef803b03ceda7afb9 -PODFILE CHECKSUM: 45c93ff87749c1bbd2a0f94715cb8be9a0c546ae +PODFILE CHECKSUM: aa8517fa897eca8d3c5d128fe3489e0dea05708e COCOAPODS: 1.9.3 diff --git a/Zircles.xcodeproj/project.pbxproj b/Zircles.xcodeproj/project.pbxproj index c6d852a..72c1b4a 100644 --- a/Zircles.xcodeproj/project.pbxproj +++ b/Zircles.xcodeproj/project.pbxproj @@ -28,6 +28,21 @@ 0D5142C624A14D0F00F9AE2E /* CreateNewZircleNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D5142C524A14D0F00F9AE2E /* CreateNewZircleNameView.swift */; }; 0D5142C824A16F9600F9AE2E /* CreateNewTypeOfZircle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D5142C724A16F9600F9AE2E /* CreateNewTypeOfZircle.swift */; }; 0D64CEA624A3854C0080AA4F /* ZircleOptionSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEA524A3854C0080AA4F /* ZircleOptionSelector.swift */; }; + 0D64CEA924A3CD1B0080AA4F /* ZirclesEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEA824A3CD1B0080AA4F /* ZirclesEnvironment.swift */; }; + 0D64CEB424A3CDA90080AA4F /* SeedManagement.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEAA24A3CDA80080AA4F /* SeedManagement.swift */; }; + 0D64CEB524A3CDA90080AA4F /* String+Zcash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEAB24A3CDA80080AA4F /* String+Zcash.swift */; }; + 0D64CEB624A3CDA90080AA4F /* MnemonicSeedPhraseProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEAC24A3CDA80080AA4F /* MnemonicSeedPhraseProvider.swift */; }; + 0D64CEB724A3CDA90080AA4F /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEAD24A3CDA80080AA4F /* QRCodeGenerator.swift */; }; + 0D64CEB824A3CDA90080AA4F /* MnemonicSeedPhraseHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEAE24A3CDA80080AA4F /* MnemonicSeedPhraseHandling.swift */; }; + 0D64CEB924A3CDA90080AA4F /* URL+Zcash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEAF24A3CDA80080AA4F /* URL+Zcash.swift */; }; + 0D64CEBA24A3CDA90080AA4F /* zECC+SwiftUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEB024A3CDA80080AA4F /* zECC+SwiftUI.swift */; }; + 0D64CEBB24A3CDA90080AA4F /* CameraAccessHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEB124A3CDA90080AA4F /* CameraAccessHelper.swift */; }; + 0D64CEBC24A3CDA90080AA4F /* BalanceUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEB224A3CDA90080AA4F /* BalanceUtils.swift */; }; + 0D64CEBD24A3CDA90080AA4F /* SimpleLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEB324A3CDA90080AA4F /* SimpleLogger.swift */; }; + 0D64CEBF24A3D3A30080AA4F /* CombineSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEBE24A3D3A30080AA4F /* CombineSynchronizer.swift */; }; + 0D64CEC124A3F2580080AA4F /* FirstScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEC024A3F2580080AA4F /* FirstScreen.swift */; }; + 0D64CEC324A4137D0080AA4F /* ZircleData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEC224A4137D0080AA4F /* ZircleData.swift */; }; + 0D64CEC524A41AF30080AA4F /* AppDetails.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D64CEC424A41AF30080AA4F /* AppDetails.swift */; }; 0D6A22C0249A9C3000B4E946 /* Colors+Zircles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D6A22BF249A9C3000B4E946 /* Colors+Zircles.swift */; }; 0D6A22C5249AB1FC00B4E946 /* ZcashSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D6A22C4249AB1FC00B4E946 /* ZcashSymbol.swift */; }; 0D6A22C7249AB36100B4E946 /* ZcashButtonBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D6A22C6249AB36100B4E946 /* ZcashButtonBackground.swift */; }; @@ -83,6 +98,21 @@ 0D5142C524A14D0F00F9AE2E /* CreateNewZircleNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateNewZircleNameView.swift; sourceTree = ""; }; 0D5142C724A16F9600F9AE2E /* CreateNewTypeOfZircle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateNewTypeOfZircle.swift; sourceTree = ""; }; 0D64CEA524A3854C0080AA4F /* ZircleOptionSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZircleOptionSelector.swift; sourceTree = ""; }; + 0D64CEA824A3CD1B0080AA4F /* ZirclesEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZirclesEnvironment.swift; sourceTree = ""; }; + 0D64CEAA24A3CDA80080AA4F /* SeedManagement.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SeedManagement.swift; sourceTree = ""; }; + 0D64CEAB24A3CDA80080AA4F /* String+Zcash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Zcash.swift"; sourceTree = ""; }; + 0D64CEAC24A3CDA80080AA4F /* MnemonicSeedPhraseProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicSeedPhraseProvider.swift; sourceTree = ""; }; + 0D64CEAD24A3CDA80080AA4F /* QRCodeGenerator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = ""; }; + 0D64CEAE24A3CDA80080AA4F /* MnemonicSeedPhraseHandling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MnemonicSeedPhraseHandling.swift; sourceTree = ""; }; + 0D64CEAF24A3CDA80080AA4F /* URL+Zcash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL+Zcash.swift"; sourceTree = ""; }; + 0D64CEB024A3CDA80080AA4F /* zECC+SwiftUI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "zECC+SwiftUI.swift"; sourceTree = ""; }; + 0D64CEB124A3CDA90080AA4F /* CameraAccessHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraAccessHelper.swift; sourceTree = ""; }; + 0D64CEB224A3CDA90080AA4F /* BalanceUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceUtils.swift; sourceTree = ""; }; + 0D64CEB324A3CDA90080AA4F /* SimpleLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SimpleLogger.swift; sourceTree = ""; }; + 0D64CEBE24A3D3A30080AA4F /* CombineSynchronizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CombineSynchronizer.swift; sourceTree = ""; }; + 0D64CEC024A3F2580080AA4F /* FirstScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FirstScreen.swift; sourceTree = ""; }; + 0D64CEC224A4137D0080AA4F /* ZircleData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZircleData.swift; sourceTree = ""; }; + 0D64CEC424A41AF30080AA4F /* AppDetails.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetails.swift; sourceTree = ""; }; 0D6A22BF249A9C3000B4E946 /* Colors+Zircles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Colors+Zircles.swift"; sourceTree = ""; }; 0D6A22C4249AB1FC00B4E946 /* ZcashSymbol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZcashSymbol.swift; sourceTree = ""; }; 0D6A22C6249AB36100B4E946 /* ZcashButtonBackground.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ZcashButtonBackground.swift; sourceTree = ""; }; @@ -142,8 +172,20 @@ 0D11D3D5249D2E0400223146 /* Utils */ = { isa = PBXGroup; children = ( + 0D64CEB224A3CDA90080AA4F /* BalanceUtils.swift */, + 0D64CEB124A3CDA90080AA4F /* CameraAccessHelper.swift */, + 0D64CEAE24A3CDA80080AA4F /* MnemonicSeedPhraseHandling.swift */, + 0D64CEAC24A3CDA80080AA4F /* MnemonicSeedPhraseProvider.swift */, + 0D64CEAD24A3CDA80080AA4F /* QRCodeGenerator.swift */, + 0D64CEAA24A3CDA80080AA4F /* SeedManagement.swift */, + 0D64CEB324A3CDA90080AA4F /* SimpleLogger.swift */, + 0D64CEAB24A3CDA80080AA4F /* String+Zcash.swift */, + 0D64CEAF24A3CDA80080AA4F /* URL+Zcash.swift */, + 0D64CEB024A3CDA80080AA4F /* zECC+SwiftUI.swift */, 0D11D3D8249D51FC00223146 /* ZircleTextField_Preview.swift */, 0D6A22CA249AB61200B4E946 /* Glow_Preview.swift */, + 0D64CEC024A3F2580080AA4F /* FirstScreen.swift */, + 0D64CEC224A4137D0080AA4F /* ZircleData.swift */, ); path = Utils; sourceTree = ""; @@ -173,6 +215,8 @@ 0D1366AA24991A6000F0EB54 /* Zircles */ = { isa = PBXGroup; children = ( + 0D64CEBE24A3D3A30080AA4F /* CombineSynchronizer.swift */, + 0D64CEA724A3CD040080AA4F /* Environment */, 0D5142C424A1323900F9AE2E /* Views */, 0D5142C224A0F0B800F9AE2E /* Zboto.otf */, 0D11D3D5249D2E0400223146 /* Utils */, @@ -223,10 +267,19 @@ 0D5142C724A16F9600F9AE2E /* CreateNewTypeOfZircle.swift */, 0DEE59A724A24B7300447C15 /* WelcomeView.swift */, 0DEE59A924A2B90F00447C15 /* CreateNewZircleDescription.swift */, + 0D64CEC424A41AF30080AA4F /* AppDetails.swift */, ); path = Views; sourceTree = ""; }; + 0D64CEA724A3CD040080AA4F /* Environment */ = { + isa = PBXGroup; + children = ( + 0D64CEA824A3CD1B0080AA4F /* ZirclesEnvironment.swift */, + ); + path = Environment; + sourceTree = ""; + }; 0D6A22C1249AB12200B4E946 /* Components */ = { isa = PBXGroup; children = ( @@ -506,23 +559,38 @@ buildActionMask = 2147483647; files = ( 0D6A22C9249AB3CA00B4E946 /* ZcashButton.swift in Sources */, + 0D64CEB924A3CDA90080AA4F /* URL+Zcash.swift in Sources */, + 0D64CEC524A41AF30080AA4F /* AppDetails.swift in Sources */, 0D6A22C0249A9C3000B4E946 /* Colors+Zircles.swift in Sources */, 0D11D3DD249D81B900223146 /* Pie.swift in Sources */, + 0D64CEB824A3CDA90080AA4F /* MnemonicSeedPhraseHandling.swift in Sources */, 0D6A22C5249AB1FC00B4E946 /* ZcashSymbol.swift in Sources */, 0D1366AC24991A6000F0EB54 /* AppDelegate.swift in Sources */, 0D11D3CE249C294E00223146 /* NeumorphicButtons.swift in Sources */, 0D6A22C7249AB36100B4E946 /* ZcashButtonBackground.swift in Sources */, + 0D64CEB624A3CDA90080AA4F /* MnemonicSeedPhraseProvider.swift in Sources */, 0D11D3D4249D05D800223146 /* Wedge.swift in Sources */, 0D1366AE24991A6000F0EB54 /* SceneDelegate.swift in Sources */, 0D11D3D0249C3AE400223146 /* Card.swift in Sources */, + 0D64CEB724A3CDA90080AA4F /* QRCodeGenerator.swift in Sources */, + 0D64CEBF24A3D3A30080AA4F /* CombineSynchronizer.swift in Sources */, 0D6A22CB249AB61200B4E946 /* Glow_Preview.swift in Sources */, 0D5142C624A14D0F00F9AE2E /* CreateNewZircleNameView.swift in Sources */, 0DEE59AA24A2B90F00447C15 /* CreateNewZircleDescription.swift in Sources */, 0D11D3D7249D2F0B00223146 /* ZircleProgress.swift in Sources */, 0D11D3D9249D51FC00223146 /* ZircleTextField_Preview.swift in Sources */, + 0D64CEC124A3F2580080AA4F /* FirstScreen.swift in Sources */, + 0D64CEBA24A3CDA90080AA4F /* zECC+SwiftUI.swift in Sources */, 0D11D3D2249CE6C800223146 /* GlowEffect.swift in Sources */, + 0D64CEB524A3CDA90080AA4F /* String+Zcash.swift in Sources */, 0D11D3DB249D5F1600223146 /* ProgressBar.swift in Sources */, + 0D64CEBD24A3CDA90080AA4F /* SimpleLogger.swift in Sources */, + 0D64CEBC24A3CDA90080AA4F /* BalanceUtils.swift in Sources */, 0D5142C824A16F9600F9AE2E /* CreateNewTypeOfZircle.swift in Sources */, + 0D64CEBB24A3CDA90080AA4F /* CameraAccessHelper.swift in Sources */, + 0D64CEB424A3CDA90080AA4F /* SeedManagement.swift in Sources */, + 0D64CEC324A4137D0080AA4F /* ZircleData.swift in Sources */, + 0D64CEA924A3CD1B0080AA4F /* ZirclesEnvironment.swift in Sources */, 0D1366B024991A6000F0EB54 /* SplashScreen.swift in Sources */, 0DEE59A824A24B7300447C15 /* WelcomeView.swift in Sources */, 0D64CEA624A3854C0080AA4F /* ZircleOptionSelector.swift in Sources */, diff --git a/Zircles/AppDelegate.swift b/Zircles/AppDelegate.swift index 97623e1..48ca2a7 100644 --- a/Zircles/AppDelegate.swift +++ b/Zircles/AppDelegate.swift @@ -7,7 +7,7 @@ // import UIKit - +let logger = SimpleLogger(logLevel: .debug) @UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate { diff --git a/Zircles/CombineSynchronizer.swift b/Zircles/CombineSynchronizer.swift new file mode 100644 index 0000000..b01ba94 --- /dev/null +++ b/Zircles/CombineSynchronizer.swift @@ -0,0 +1,161 @@ +// +// CombineSynchronizer.swift +// wallet +// +// Created by Francisco Gindre on 1/27/20. +// Copyright © 2020 Francisco Gindre. All rights reserved. +// + +import Foundation +import Combine +import ZcashLightClientKit +class CombineSynchronizer { + + var initializer: Initializer { + synchronizer.initializer + } + private var synchronizer: SDKSynchronizer + + var status: CurrentValueSubject + var progress: CurrentValueSubject + var syncBlockHeight: CurrentValueSubject + var minedTransaction = PassthroughSubject() + var balance: CurrentValueSubject + var verifiedBalance: CurrentValueSubject + var cancellables = [AnyCancellable]() + var error = PassthroughSubject() + var receivedTransactions: Future<[ConfirmedTransactionEntity],Never> { + Future<[ConfirmedTransactionEntity], Never>() { + promise in + DispatchQueue.global().async { + [weak self] in + guard let self = self else { + promise(.success([])) + return + } + promise(.success(self.synchronizer.receivedTransactions)) + } + } + } + + var sentTransactions: Future<[ConfirmedTransactionEntity], Never> { + Future<[ConfirmedTransactionEntity], Never>() { + promise in + DispatchQueue.global().async { + [weak self] in + guard let self = self else { + promise(.success([])) + return + } + promise(.success(self.synchronizer.sentTransactions)) + } + } + } + + var pendingTransactions: Future<[PendingTransactionEntity], Never> { + + Future<[PendingTransactionEntity], Never>(){ + [weak self ] promise in + + guard let self = self else { + promise(.success([])) + return + } + + DispatchQueue.global().async { + promise(.success(self.synchronizer.pendingTransactions)) + } + } + } + + + init(initializer: Initializer) throws { + + self.synchronizer = try SDKSynchronizer(initializer: initializer) + self.status = CurrentValueSubject(.disconnected) + self.progress = CurrentValueSubject(0) + self.balance = CurrentValueSubject(0) + self.verifiedBalance = CurrentValueSubject(0) + self.syncBlockHeight = CurrentValueSubject(ZcashSDK.SAPLING_ACTIVATION_HEIGHT) + + NotificationCenter.default.publisher(for: .synchronizerSynced).sink(receiveValue: { _ in + self.balance.send(initializer.getBalance().asHumanReadableZecBalance()) + self.verifiedBalance.send(initializer.getVerifiedBalance().asHumanReadableZecBalance()) + }).store(in: &cancellables) + + NotificationCenter.default.publisher(for: .synchronizerStarted).sink { _ in + self.status.send(.syncing) + }.store(in: &cancellables) + + + NotificationCenter.default.publisher(for: .synchronizerProgressUpdated).receive(on: DispatchQueue.main).sink(receiveValue: { (progressNotification) in + guard let newProgress = progressNotification.userInfo?[SDKSynchronizer.NotificationKeys.progress] as? Float else { return } + self.progress.send(newProgress) + + guard let blockHeight = progressNotification.userInfo?[SDKSynchronizer.NotificationKeys.blockHeight] as? BlockHeight else { return } + self.syncBlockHeight.send(blockHeight) + }).store(in: &cancellables) + + + NotificationCenter.default.publisher(for: .synchronizerMinedTransaction).sink(receiveValue: {minedNotification in + guard let minedTx = minedNotification.userInfo?[SDKSynchronizer.NotificationKeys.minedTransaction] as? PendingTransactionEntity else { return } + self.minedTransaction.send(minedTx) + }).store(in: &cancellables) + + NotificationCenter.default.publisher(for: .synchronizerFailed).sink { (notification) in + guard let error = notification.userInfo?[SDKSynchronizer.NotificationKeys.error] as? Error else { + self.error.send(ZirclesEnvironment.WalletError.genericError(message: "An error ocurred, but we can't figure out what it is. Please check device logs for more details") + ) + return + } + self.error.send(error) + }.store(in: &cancellables) + + } + + func start(retry: Bool = false){ + + do { + if retry { + stop() + } + try synchronizer.start(retry: retry) + } catch { + logger.error("error starting \(error)") + } + } + + func stop() { + do { + try synchronizer.stop() + } catch { + logger.error("error stopping \(error)") + } + } + + func cancel(pendingTransaction: PendingTransactionEntity) -> Bool { + synchronizer.cancelSpend(transaction: pendingTransaction) + } + + deinit { + for c in cancellables { + c.cancel() + } + } + + + func send(with spendingKey: String, zatoshi: Int64, to recipientAddress: String, memo: String?,from account: Int) -> Future { + Future() { + promise in + self.synchronizer.sendToAddress(spendingKey: spendingKey, zatoshi: zatoshi, toAddress: recipientAddress, memo: memo, from: account) { (result) in + switch result { + case .failure(let error): + promise(.failure(error)) + case .success(let pendingTx): + promise(.success(pendingTx)) + } + } + } + + } +} diff --git a/Zircles/Environment/ZirclesEnvironment.swift b/Zircles/Environment/ZirclesEnvironment.swift new file mode 100644 index 0000000..1fa1241 --- /dev/null +++ b/Zircles/Environment/ZirclesEnvironment.swift @@ -0,0 +1,221 @@ +// +// ZirclesEnvironment.swift +// Zircles +// +// Created by Francisco Gindre on 6/24/20. +// Copyright © 2020 Electric Coin Company. All rights reserved. +// + +import Foundation +import SwiftUI +import ZcashLightClientKit +import Combine +enum WalletState { + case initalized + case uninitialized + case syncing + case synced +} + +protocol AppEnvironment { + +} + +final class ZirclesEnvironment: ObservableObject { + enum WalletError: Error { + case createFailed + case initializationFailed(message: String) + case genericError(message: String) + case connectionFailed(message: String) + case maxRetriesReached(attempts: Int) + } + static let genericErrorMessage = "An error ocurred, please check your device logs" + static var shared: ZirclesEnvironment = try! ZirclesEnvironment() // app can't live without this existing. + + @Published var state: WalletState + + let endpoint = LightWalletEndpoint(address: ZcashSDK.isMainnet ? "lightwalletd.z.cash" : "lightwalletd.testnet.z.cash", port: 9067, secure: true) + var dataDbURL: URL + var cacheDbURL: URL + var pendingDbURL: URL + var outputParamsURL: URL + var spendParamsURL: URL + var initializer: Initializer { + synchronizer.initializer + } + var synchronizer: CombineSynchronizer + var cancellables = [AnyCancellable]() + + static func getInitialState() -> WalletState { + guard let keys = SeedManager.default.getKeys(), keys.count > 0 else { + return .uninitialized + } + return .initalized + } + + static func isInitialized() -> Bool { + switch getInitialState() { + case .uninitialized: + return false + default: + return true + } + } + + private init() throws { + self.dataDbURL = try URL.dataDbURL() + self.cacheDbURL = try URL.cacheDbURL() + self.pendingDbURL = try URL.pendingDbURL() + self.outputParamsURL = try URL.outputParamsURL() + self.spendParamsURL = try URL.spendParamsURL() + + self.state = Self.getInitialState() + + let initializer = Initializer( + cacheDbURL: self.cacheDbURL, + dataDbURL: self.dataDbURL, + pendingDbURL: self.pendingDbURL, + endpoint: endpoint, + spendParamsURL: self.spendParamsURL, + outputParamsURL: self.outputParamsURL, + loggerProxy: logger) + self.synchronizer = try CombineSynchronizer(initializer: initializer) + cancellables.append( + self.synchronizer.status.map({ + status -> WalletState in + switch status { + case .synced: + return WalletState.synced + case .syncing: + return WalletState.syncing + default: + return Self.getInitialState() + + } + }).sink(receiveValue: { status in + self.state = status + }) + ) + + } + + func createNewWallet() throws { + + guard let randomPhrase = MnemonicSeedProvider.default.randomMnemonic(), + let randomSeed = MnemonicSeedProvider.default.toSeed(mnemonic: randomPhrase) else { + throw WalletError.createFailed + } + let birthday = WalletBirthday.birthday(with: BlockHeight.max) + try SeedManager.default.importSeed(randomSeed) + try SeedManager.default.importBirthday(birthday.height) + try SeedManager.default.importPhrase(bip39: randomPhrase) + try self.initialize() + } + + func initialize() throws { + + if let keys = try self.initializer.initialize(seedProvider: SeedManager.default, walletBirthdayHeight: try SeedManager.default.exportBirthday()) { + + SeedManager.default.saveKeys(keys) + } + + + self.synchronizer.start() + } + + /** + only for internal use + */ + func nuke(abortApplication: Bool = false) { + self.synchronizer.stop() + + SeedManager.default.nukeWallet() + + do { + try FileManager.default.removeItem(at: self.dataDbURL) + } catch { + logger.error("could not nuke wallet: \(error)") + } + do { + try FileManager.default.removeItem(at: self.cacheDbURL) + } catch { + logger.error("could not nuke wallet: \(error)") + } + do { + try FileManager.default.removeItem(at: self.pendingDbURL) + } catch { + logger.error("could not nuke wallet: \(error)") + } + + if abortApplication { + abort() + } + } + + static func mapError(error: Error) -> ZirclesEnvironment.WalletError { + + if let rustError = error as? RustWeldingError { + switch rustError { + case .genericError(let message): + return ZirclesEnvironment.WalletError.genericError(message: message) + case .dataDbInitFailed(let message): + return ZirclesEnvironment.WalletError.genericError(message: message) + case .dataDbNotEmpty: + return ZirclesEnvironment.WalletError.genericError(message: "attempt to initialize a db that was not empty") + case .saplingSpendParametersNotFound: + return ZirclesEnvironment.WalletError.createFailed + case .malformedStringInput: + return ZirclesEnvironment.WalletError.genericError(message: "Malformed address or key detected") + default: + return WalletError.genericError(message: "\(rustError)") + } + } else if let synchronizerError = error as? SynchronizerError { + switch synchronizerError { + case .generalError(let message): + return ZirclesEnvironment.WalletError.genericError(message: message) + case .initFailed(let message): + return WalletError.initializationFailed(message: "Synchronizer failed to initialize: \(message)") + case .syncFailed: + return WalletError.genericError(message: "Synchronizing failed") + case .connectionFailed(let message): + return WalletError.connectionFailed(message: message) + case .maxRetryAttemptsReached(attempts: let attempts): + return WalletError.maxRetriesReached(attempts: attempts) + case .connectionError(_, let message): + return WalletError.connectionFailed(message: message) + } + } + + return ZirclesEnvironment.WalletError.genericError(message: Self.genericErrorMessage) + } + deinit { + cancellables.forEach { + c in + c.cancel() + } + } + +} + +extension ZirclesEnvironment { + static var appBuild: String? { + Bundle.main.infoDictionary?["CFBundleVersion"] as? String + } + + static var appVersion: String? { + Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + } + + func isValidAddress(_ address: String) -> Bool { + self.initializer.isValidShieldedAddress(address) || self.initializer.isValidTransparentAddress(address) + } + func sufficientFundsToSend(amount: Double) -> Bool { + return sufficientFunds(availableBalance: self.initializer.getVerifiedBalance(), zatoshiToSend: amount.toZatoshi()) + } + private func sufficientFunds(availableBalance: Int64, zatoshiToSend: Int64) -> Bool { + availableBalance - zatoshiToSend - Int64(ZcashSDK.MINERS_FEE_ZATOSHI) >= 0 + } + static var minerFee: Double { + Int64(ZcashSDK.MINERS_FEE_ZATOSHI).asHumanReadableZecBalance() + } +} diff --git a/Zircles/SceneDelegate.swift b/Zircles/SceneDelegate.swift index 26a5521..c19e493 100644 --- a/Zircles/SceneDelegate.swift +++ b/Zircles/SceneDelegate.swift @@ -20,7 +20,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). // Create the SwiftUI view that provides the window contents. - let contentView = SplashScreen() + let isInitialized = ZirclesEnvironment.isInitialized() + let contentView = FirstScreen() { + if isInitialized { + SplashScreen() + } else { + WelcomeView() + } + } // Use a UIHostingController as window root view controller. if let windowScene = scene as? UIWindowScene { @@ -30,7 +37,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { window.makeKeyAndVisible() } } - + func sceneDidDisconnect(_ scene: UIScene) { // Called as the scene is being released by the system. // This occurs shortly after the scene enters the background, or when its session is discarded. diff --git a/Zircles/Utils/BalanceUtils.swift b/Zircles/Utils/BalanceUtils.swift new file mode 100644 index 0000000..2d4c6bb --- /dev/null +++ b/Zircles/Utils/BalanceUtils.swift @@ -0,0 +1,61 @@ +// +// BalanceUtils.swift +// wallet +// +// Created by Francisco Gindre on 1/2/20. +// Copyright © 2020 Francisco Gindre. All rights reserved. +// + +import Foundation +import ZcashLightClientKit + +extension Int64 { + func asHumanReadableZecBalance() -> Double { + var decimal = Decimal(self) / Decimal(ZcashSDK.ZATOSHI_PER_ZEC) + var rounded = Decimal() + NSDecimalRound(&rounded, &decimal, 6, .bankers) + return (rounded as NSDecimalNumber).doubleValue + } +} + +extension Double { + func toZatoshi() -> Int64 { + var decimal = Decimal(self) * Decimal(ZcashSDK.ZATOSHI_PER_ZEC) + var rounded = Decimal() + NSDecimalRound(&rounded, &decimal, 6, .bankers) + return (rounded as NSDecimalNumber).int64Value + } + + func toZecAmount() -> String { + NumberFormatter.zecAmountFormatter.string(from: NSNumber(value: self)) ?? "\(self)" + } +} + +extension NumberFormatter { + static var zecAmountFormatter: NumberFormatter { + + let fmt = NumberFormatter() + + fmt.alwaysShowsDecimalSeparator = false + fmt.allowsFloats = true + fmt.maximumFractionDigits = 8 + fmt.minimumFractionDigits = 0 + fmt.minimumIntegerDigits = 1 + return fmt + + } + + static var zeroBalanceFormatter: NumberFormatter { + + let fmt = NumberFormatter() + + fmt.alwaysShowsDecimalSeparator = false + fmt.allowsFloats = true + fmt.maximumFractionDigits = 0 + fmt.minimumFractionDigits = 0 + fmt.minimumIntegerDigits = 1 + return fmt + + } +} + diff --git a/Zircles/Utils/CameraAccessHelper.swift b/Zircles/Utils/CameraAccessHelper.swift new file mode 100644 index 0000000..1d9ed85 --- /dev/null +++ b/Zircles/Utils/CameraAccessHelper.swift @@ -0,0 +1,38 @@ +// +// CameraAccessHelper.swift +// wallet +// +// Created by Francisco Gindre on 2/5/20. +// Copyright © 2020 Francisco Gindre. All rights reserved. +// + +import Foundation +import AVFoundation + +class CameraAccessHelper { + + enum Status { + case authorized + case unauthorized + case unavailable + case undetermined + } + + static var authorizationStatus: CameraAccessHelper.Status { + let cameraMediaType = AVMediaType.video + let cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: cameraMediaType) + + switch cameraAuthorizationStatus { + case .authorized: + return Status.authorized + case .denied: + return Status.unauthorized + case .restricted: + return .unavailable + case .notDetermined: + return Status.undetermined + @unknown default: + return .unavailable + } + } +} diff --git a/Zircles/Utils/FirstScreen.swift b/Zircles/Utils/FirstScreen.swift new file mode 100644 index 0000000..28fc68f --- /dev/null +++ b/Zircles/Utils/FirstScreen.swift @@ -0,0 +1,32 @@ +// +// FirstScreen.swift +// Zircles +// +// Created by Francisco Gindre on 6/24/20. +// Copyright © 2020 Electric Coin Company. All rights reserved. +// + +import SwiftUI + +struct FirstScreen: View { + let content: Content + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + var body: some View { + NavigationView { + content + } + } +} + +struct FirstScreen_Previews: PreviewProvider { + static var previews: some View { + FirstScreen() { + ZStack { + Color.background + Text("hello") + }.navigationBarTitle("Welcome") + } + } +} diff --git a/Zircles/Utils/MnemonicSeedPhraseHandling.swift b/Zircles/Utils/MnemonicSeedPhraseHandling.swift new file mode 100644 index 0000000..fe682e4 --- /dev/null +++ b/Zircles/Utils/MnemonicSeedPhraseHandling.swift @@ -0,0 +1,38 @@ +// +// MnemonicSeedPhraseHandling.swift +// wallet +// +// Created by Francisco Gindre on 2/28/20. +// Copyright © 2020 Francisco Gindre. All rights reserved. +// + +import Foundation +enum MnemonicError: Error { + case invalidSeed +} + +protocol MnemonicSeedPhraseHandling { + /** + random 24 words mnemonic phrase + */ + func randomMnemonic() -> String? + /** + random 24 words mnemonic phrase as array of words + */ + func randomMnemonicWords() -> [String]? + + /** + generate deterministic seed from mnemonic phrase + */ + func toSeed(mnemonic: String) -> [UInt8]? + + /** + get this mnemonic + */ + func asWords(mnemonic: String) -> [String]? + + /** + validates whether the given mnemonic is correct + */ + func isValid(mnemonic: String) -> Bool +} diff --git a/Zircles/Utils/MnemonicSeedPhraseProvider.swift b/Zircles/Utils/MnemonicSeedPhraseProvider.swift new file mode 100644 index 0000000..8b6e2ae --- /dev/null +++ b/Zircles/Utils/MnemonicSeedPhraseProvider.swift @@ -0,0 +1,37 @@ +// +// MnemonicSeedPhraseProvider.swift +// wallet +// +// Created by Francisco Gindre on 2/28/20. +// Copyright © 2020 Francisco Gindre. All rights reserved. +// + +import Foundation +import MnemonicSwift +class MnemonicSeedProvider: MnemonicSeedPhraseHandling { + + static let `default` = MnemonicSeedProvider() + + private init(){} + + func randomMnemonic() -> String? { + Mnemonic.generateMnemonic(strength: 256) + } + + func randomMnemonicWords() -> [String]? { + randomMnemonic()?.split(separator: " ").map({ String($0) }) + } + + func toSeed(mnemonic: String) -> [UInt8]? { + guard let data = Mnemonic.deterministicSeedBytes(from: mnemonic) else { return nil } + return [UInt8](data) + } + + func asWords(mnemonic: String) -> [String]? { + mnemonic.split(separator: " ").map({ String($0) }) + } + + func isValid(mnemonic: String) -> Bool { + Mnemonic.validate(mnemonic: mnemonic) + } +} diff --git a/Zircles/Utils/QRCodeGenerator.swift b/Zircles/Utils/QRCodeGenerator.swift new file mode 100644 index 0000000..a6af743 --- /dev/null +++ b/Zircles/Utils/QRCodeGenerator.swift @@ -0,0 +1,51 @@ +// +// QRCodeGenerator.swift +// wallet +// +// Created by Francisco Gindre on 2/3/20. +// Copyright © 2020 Francisco Gindre. All rights reserved. +// + +import Foundation +import Combine +import CoreImage +import CoreImage.CIFilterBuiltins +import SwiftUI + + +class QRCodeGenerator { + enum QRCodeError: Error { + case failedToGenerate + } + + static func generate(from string: String) -> Future { + + Future() { promise in + DispatchQueue.global().async { + + guard let image = generate(from: string) else { + promise(.failure(QRCodeGenerator.QRCodeError.failedToGenerate)) + return + } + + return promise(.success(image)) + } + } + } + + static func generate(from string: String, scale: CGFloat = 5) -> CGImage? { + let data = string.data(using: String.Encoding.utf8) + + let context = CIContext() + let filter = CoreImage.CIFilter.qrCodeGenerator() + filter.setValue(data, forKey: "inputMessage") + let transform = CGAffineTransform(scaleX: scale, y: scale) + + + guard let output = filter.outputImage?.transformed(by: transform) else { + return nil + } + + return context.createCGImage(output, from: output.extent) + } +} diff --git a/Zircles/Utils/SeedManagement.swift b/Zircles/Utils/SeedManagement.swift new file mode 100644 index 0000000..cf9cec8 --- /dev/null +++ b/Zircles/Utils/SeedManagement.swift @@ -0,0 +1,115 @@ +// +// SeedManagement.swift +// wallet +// +// Created by Francisco Gindre on 1/23/20. +// Copyright © 2020 Francisco Gindre. All rights reserved. +// + +import Foundation +import KeychainSwift +import ZcashLightClientKit +final class SeedManager { + + enum SeedManagerError: Error { + case alreadyImported + case uninitializedWallet + } + + static var `default`: SeedManager = SeedManager() + private static let zECCWalletKeys = "zECCWalletKeys" + private static let zECCWalletSeedKey = "zEECWalletSeedKey" + private static let zECCWalletBirthday = "zECCWalletBirthday" + private static let zECCWalletPhrase = "zECCWalletPhrase" + + private let keychain = KeychainSwift() + + func importBirthday(_ height: BlockHeight) throws { + guard keychain.get(Self.zECCWalletBirthday) == nil else { + throw SeedManagerError.alreadyImported + } + keychain.set(String(height), forKey: Self.zECCWalletBirthday) + } + + func exportBirthday() throws -> BlockHeight { + guard let birthday = keychain.get(Self.zECCWalletBirthday), + let value = BlockHeight(birthday) else { + throw SeedManagerError.uninitializedWallet + } + return value + } + + func importSeed(_ seed: [UInt8]) throws { + guard keychain.get(Self.zECCWalletSeedKey) == nil else { throw SeedManagerError.alreadyImported } + keychain.set(Data(seed), forKey: Self.zECCWalletSeedKey) + + } + + func exportSeed() throws -> [UInt8] { + guard let seedData = keychain.getData(Self.zECCWalletSeedKey) else { throw SeedManagerError.uninitializedWallet } + return [UInt8](seedData) + } + + func importPhrase(bip39 phrase: String) throws { + guard keychain.get(Self.zECCWalletPhrase) == nil else { throw SeedManagerError.alreadyImported } + keychain.set(phrase, forKey: Self.zECCWalletPhrase) + } + + func exportPhrase() throws -> String { + guard let seed = keychain.get(Self.zECCWalletPhrase) else { throw SeedManagerError.uninitializedWallet } + return seed + } + + func saveKeys(_ keys: [String]) { + keychain.set(keys.joined(separator: ";"), forKey: Self.zECCWalletKeys) + } + + func getKeys() -> [String]? { + keychain.get(Self.zECCWalletKeys)?.split(separator: ";").map{String($0)} + } + + /** + + */ + func nukePhrase() { + keychain.delete(Self.zECCWalletPhrase) + } + /** + Use carefully: Deletes the keys from the keychain + */ + func nukeKeys() { + keychain.delete(Self.zECCWalletKeys) + } + + /** + Use carefully: Deletes the seed from the keychain. + */ + func nukeSeed() { + keychain.delete(Self.zECCWalletSeedKey) + } + + /** + Use carefully: deletes the wallet birthday from the keychain + */ + + func nukeBirthday() { + keychain.delete(Self.zECCWalletBirthday) + } + + + /** + There's no fate but what we make for ourselves - Sarah Connor + */ + func nukeWallet() { + nukeKeys() + nukeSeed() + nukePhrase() + nukeBirthday() + } +} + +extension SeedManager: SeedProvider { + func seed() -> [UInt8] { + (try? exportSeed()) ?? [] + } +} diff --git a/Zircles/Utils/SimpleLogger.swift b/Zircles/Utils/SimpleLogger.swift new file mode 100644 index 0000000..8738d1d --- /dev/null +++ b/Zircles/Utils/SimpleLogger.swift @@ -0,0 +1,74 @@ +// +// SimpleLogger.swift +// wallet +// +// Created by Francisco Gindre on 3/9/20. +// Copyright © 2020 Francisco Gindre. All rights reserved. +// + +import Foundation +import ZcashLightClientKit +import os + +class SimpleLogger: ZcashLightClientKit.Logger { + enum LogLevel: Int { + case debug + case error + case warning + case event + case info + } + + enum LoggerType { + case osLog + case printerLog + } + + var level: LogLevel + var loggerType: LoggerType + + init(logLevel: LogLevel, type: LoggerType = .osLog) { + self.level = logLevel + self.loggerType = type + } + + private static let subsystem = Bundle.main.bundleIdentifier! + static let oslog = OSLog(subsystem: subsystem, category: "logs") + + func debug(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + guard level.rawValue == LogLevel.debug.rawValue else { return } + log(level: "DEBUG 🐞", message: message, file: file, function: function, line: line) + } + + func error(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + guard level.rawValue <= LogLevel.error.rawValue else { return } + log(level: "ERROR 💥", message: message, file: file, function: function, line: line) + } + + func warn(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + guard level.rawValue <= LogLevel.warning.rawValue else { return } + log(level: "WARNING ⚠️", message: message, file: file, function: function, line: line) + } + + func event(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + guard level.rawValue <= LogLevel.event.rawValue else { return } + log(level: "EVENT ⏱", message: message, file: file, function: function, line: line) + } + + func info(_ message: String, file: String = #file, function: String = #function, line: Int = #line) { + guard level.rawValue <= LogLevel.info.rawValue else { return } + log(level: "INFO ℹ️", message: message, file: file, function: function, line: line) + } + + private func log(level: String, message: String, file: String, function: String, line: Int) { + let fileName = (file as NSString).lastPathComponent + switch loggerType { + case .printerLog: + print("[\(level)] \(fileName) - \(function) - line: \(line) -> \(message)") + default: +// os_log("[%@] %@ - %@ - Line: %d -> %@", log: Self.oslog, type: .debug, level, fileName, function, line, message) + os_log("[%{public}@] %{public}@ - %{public}@ - Line: %{public}d -> %{public}@", level, fileName, function, line, message) + } + } + +} diff --git a/Zircles/Utils/String+Zcash.swift b/Zircles/Utils/String+Zcash.swift new file mode 100644 index 0000000..160006a --- /dev/null +++ b/Zircles/Utils/String+Zcash.swift @@ -0,0 +1,24 @@ +// +// String+Zcash.swift +// wallet +// +// Created by Francisco Gindre on 1/22/20. +// Copyright © 2020 Francisco Gindre. All rights reserved. +// + +import Foundation + + +extension String { + + var isValidShieldedAddress: Bool { + ZirclesEnvironment.shared.initializer.isValidShieldedAddress(self) + } + + var shortZaddress: String? { + guard isValidShieldedAddress else { return nil } + return String(self[self.startIndex ..< self.index(self.startIndex, offsetBy: 8)]) + + "..." + + String(self[self.index(self.endIndex, offsetBy: -8) ..< self.endIndex]) + } +} diff --git a/Zircles/Utils/URL+Zcash.swift b/Zircles/Utils/URL+Zcash.swift new file mode 100644 index 0000000..80c1428 --- /dev/null +++ b/Zircles/Utils/URL+Zcash.swift @@ -0,0 +1,38 @@ +// +// URL+Zcash.swift +// wallet +// +// Created by Francisco Gindre on 1/24/20. +// Copyright © 2020 Francisco Gindre. All rights reserved. +// + +import Foundation +import ZcashLightClientKit + + +extension URL { + + static func documentsDirectory() throws -> URL { + try FileManager.default.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true) + } + + static func cacheDbURL() throws -> URL { + try documentsDirectory().appendingPathComponent(ZcashSDK.DEFAULT_DB_NAME_PREFIX+ZcashSDK.DEFAULT_CACHES_DB_NAME, isDirectory: false) + } + + static func dataDbURL() throws -> URL { + try documentsDirectory().appendingPathComponent(ZcashSDK.DEFAULT_DB_NAME_PREFIX+ZcashSDK.DEFAULT_DATA_DB_NAME, isDirectory: false) + } + + static func pendingDbURL() throws -> URL { + try documentsDirectory().appendingPathComponent(ZcashSDK.DEFAULT_DB_NAME_PREFIX+ZcashSDK.DEFAULT_PENDING_DB_NAME) + } + + static func spendParamsURL() throws -> URL { + Bundle.main.url(forResource: "sapling-spend", withExtension: ".params")! + } + + static func outputParamsURL() throws -> URL { + Bundle.main.url(forResource: "sapling-output", withExtension: ".params")! + } +} diff --git a/Zircles/Utils/ZircleData.swift b/Zircles/Utils/ZircleData.swift new file mode 100644 index 0000000..0266f9d --- /dev/null +++ b/Zircles/Utils/ZircleData.swift @@ -0,0 +1,24 @@ +// +// ZircleData.swift +// Zircles +// +// Created by Francisco Gindre on 6/24/20. +// Copyright © 2020 Electric Coin Company. All rights reserved. +// + +import Foundation + +class ZircleDataStorage { + static let usernameKey = "zircleusername" + static var `default`: ZircleDataStorage = ZircleDataStorage() + + private init() {} + + func saveUsername(_ name: String) { + UserDefaults.standard.setValue(name, forKey: Self.usernameKey) + } + var username: String { + UserDefaults.standard.string(forKey: Self.usernameKey) ?? "" + } + +} diff --git a/Zircles/Utils/zECC+SwiftUI.swift b/Zircles/Utils/zECC+SwiftUI.swift new file mode 100644 index 0000000..f7d4288 --- /dev/null +++ b/Zircles/Utils/zECC+SwiftUI.swift @@ -0,0 +1,16 @@ +// +// zECC+SwiftUI.swift +// wallet +// +// Created by Francisco Gindre on 2/12/20. +// Copyright © 2020 Francisco Gindre. All rights reserved. +// + +import Foundation +import SwiftUI + +extension ZcashButton { + static func nukeButton() -> ZcashButton { + ZcashButton(color: Color.red, fill: Color.clear, text: "NUKE WALLET") + } +} diff --git a/Zircles/Views/AppDetails.swift b/Zircles/Views/AppDetails.swift new file mode 100644 index 0000000..d243572 --- /dev/null +++ b/Zircles/Views/AppDetails.swift @@ -0,0 +1,84 @@ +// +// AppDetails.swift +// Zircles +// +// Created by Francisco Gindre on 6/24/20. +// Copyright © 2020 Electric Coin Company. All rights reserved. +// + +import SwiftUI + +struct AppDetails: View { + @State var showNukeAlert = false + + var body: some View { + ZStack { + Color.background.edgesIgnoringSafeArea(.all) + + VStack { + VStack(alignment: .leading, spacing: 3) { + Text("Username") + .foregroundColor(.textLightGray) + .fontWeight(.heavy) + .font(.footnote) + .frame(alignment: .leading) + Card(isOn: .constant(true),cornerRadius: 5,padding: 8) { + + Text(ZircleDataStorage.default.username) + .foregroundColor(Color.textDarkGray) + .font(.system(size: 14, weight: .heavy, design: .default)) + + } + }.padding(.all, 0) + VStack(alignment: .leading, spacing: 3) { + Text("App Version") + .foregroundColor(.textLightGray) + .fontWeight(.heavy) + .font(.footnote) + .frame(alignment: .leading) + Card(isOn: .constant(true),cornerRadius: 5,padding: 8) { + + Text("Zircles v\(ZirclesEnvironment.appVersion ?? "Unknown") - Build: \(ZirclesEnvironment.appBuild ?? "Unknown")") + .foregroundColor(Color.textDarkGray) + .font(.system(size: 14, weight: .heavy, design: .default)) + + } + }.padding(.all, 0) + + Button(action: { + self.showNukeAlert = true + }) { + Text("NUKE WALLET") + .foregroundColor(.red) + .zcashButtonBackground(shape: .roundedCorners(fillStyle: .outline(color: .red, lineWidth: 2))) + .frame(height: 48) + }.alert(isPresented: $showNukeAlert) { + Alert(title: Text("Delete Wallet?"), + message: Text("You are about to")+Text(" nuke your wallet. ").foregroundColor(.red) + Text("Are you sure you want to proceed?"), + primaryButton: .default( + Text("I'm not sure") + ,action: { self.showNukeAlert = false} + ), + secondaryButton: .destructive( + Text("NUKE WALLET!"), + action: { + ZirclesEnvironment.shared.nuke(abortApplication: true) + } + ) + ) + } + } + }.navigationBarTitle(Text("Backstage")) + .navigationBarItems(trailing: Button(action:{ + + }){ + Text("close") + }) + } +} + +struct AppDetails_Previews: PreviewProvider { + static var previews: some View { + AppDetails() + } +} diff --git a/Zircles/Views/CreateNewZircleDescription.swift b/Zircles/Views/CreateNewZircleDescription.swift index aa63a03..2b2da27 100644 --- a/Zircles/Views/CreateNewZircleDescription.swift +++ b/Zircles/Views/CreateNewZircleDescription.swift @@ -80,7 +80,7 @@ struct CreateNewZircleDescription: View { .frame(alignment: .leading) Card(isOn: .constant(true),cornerRadius: 5,padding: 8) { - TextField("some title text", text: .constant("Hackathon Happy Hour")) + TextField("some title text", text: .constant("3 ZEC")) .foregroundColor(Color.textDarkGray) .font(.system(size: 14, weight: .heavy, design: .default)) diff --git a/Zircles/Views/SplashScreen.swift b/Zircles/Views/SplashScreen.swift index 628c2cb..2b214c0 100644 --- a/Zircles/Views/SplashScreen.swift +++ b/Zircles/Views/SplashScreen.swift @@ -11,53 +11,50 @@ import SwiftUI struct SplashScreen: View { var body: some View { - NavigationView { - ZStack { - Color.background - .edgesIgnoringSafeArea(.all) - VStack { - Toggle(isOn: .constant(false)) { - Text("No Proyects Yet") - .foregroundColor(Color.textLightGray) - .fontWeight(.heavy) + ZStack { + Color.background + .edgesIgnoringSafeArea(.all) + VStack { + Toggle(isOn: .constant(false)) { + Text("No Proyects Yet") + .foregroundColor(Color.textLightGray) + .fontWeight(.heavy) .contentShape(RoundedRectangle(cornerRadius: 5)) .scaledToFill() - .frame(width: 250) - } - .contentShape(RoundedRectangle(cornerRadius: 5)) - .toggleStyle(SimpleToggleStyle(shape: RoundedRectangle(cornerRadius: 5), padding: 8)) - Spacer() - FancyLogo() - .frame(width: 200, height: 200) - Spacer() - VStack(spacing: 16) { - Text("Join a Zircle") + .frame(width: 250) + } + .contentShape(RoundedRectangle(cornerRadius: 5)) + .toggleStyle(SimpleToggleStyle(shape: RoundedRectangle(cornerRadius: 5), padding: 8)) + Spacer() + FancyLogo() + .frame(width: 200, height: 200) + Spacer() + VStack(spacing: 16) { + Text("Join a Zircle") + .font(.system(size: 20, weight: .bold, design: .default)) + .shadow(color:Color(red: 0.2, green: 0.2, blue: 0.2).opacity(0.2), radius: 1, x: 0, y: 2) + .foregroundColor(Color.buttonBlue) + .modifier(ZcashButtonBackground(buttonShape: .roundedCorners(fillStyle: .solid(color: Color.background)))) + + .shadow(color: Color(red: 0.2, green: 0.2, blue: 0.2).opacity(0.3), radius: 15, x: 10, y: 15) + .shadow(color: Color.white.opacity(0.5), radius: 25, x:-10, y: -10) + .frame(height: 50) + + Button(action: {}) { + Text("Create New") .font(.system(size: 20, weight: .bold, design: .default)) .shadow(color:Color(red: 0.2, green: 0.2, blue: 0.2).opacity(0.2), radius: 1, x: 0, y: 2) - .foregroundColor(Color.buttonBlue) - .modifier(ZcashButtonBackground(buttonShape: .roundedCorners(fillStyle: .solid(color: Color.background)))) + .foregroundColor(Color.background) + .modifier(ZcashButtonBackground(buttonShape: .roundedCorners(fillStyle: .solid(color: Color.buttonBlue)))) - .shadow(color: Color(red: 0.2, green: 0.2, blue: 0.2).opacity(0.3), radius: 15, x: 10, y: 15) - .shadow(color: Color.white.opacity(0.5), radius: 25, x:-10, y: -10) + .shadow(color: Color(red: 0.2, green: 0.2, blue: 0.2).opacity(0.5), radius: 25, x: 10, y: 10) .frame(height: 50) - - Button(action: {}) { - Text("Create New") - .font(.system(size: 20, weight: .bold, design: .default)) - .shadow(color:Color(red: 0.2, green: 0.2, blue: 0.2).opacity(0.2), radius: 1, x: 0, y: 2) - .foregroundColor(Color.background) - .modifier(ZcashButtonBackground(buttonShape: .roundedCorners(fillStyle: .solid(color: Color.buttonBlue)))) - - .shadow(color: Color(red: 0.2, green: 0.2, blue: 0.2).opacity(0.5), radius: 25, x: 10, y: 10) - .frame(height: 50) - } - }.padding(.all, 0) - }.padding(30) - - - }.navigationBarTitle(Text("Welcome to Zircles")) - } - + } + }.padding(.all, 0) + }.padding(30) + + + }.navigationBarTitle(Text("Welcome to Zircles")) } } @@ -86,7 +83,7 @@ struct FancyLogo: View { .foregroundColor(.buttonGray) .font( .custom("Zboto", size: 200) - ).padding() + ).padding() .frame(alignment: .center) .contentShape(Circle()) .offset(x: 0, y: 50) diff --git a/Zircles/Views/WelcomeView.swift b/Zircles/Views/WelcomeView.swift index b9a6084..ecbc6c4 100644 --- a/Zircles/Views/WelcomeView.swift +++ b/Zircles/Views/WelcomeView.swift @@ -7,11 +7,14 @@ // import SwiftUI - +import MnemonicSwift struct WelcomeView: View { + @State var username: String = "" + @State var seedPhrase: String = "" + @State var isWelcome = false var body: some View { ZStack { - Color.background + Color.background.edgesIgnoringSafeArea(.all) VStack(alignment: .leading, spacing: 16) { Spacer() Text("It looks like you are a new user, let's get to know you! What is your name?") @@ -26,7 +29,7 @@ struct WelcomeView: View { .frame(alignment: .leading) Card(isOn: .constant(true),cornerRadius: 5,padding: 8) { - TextField("some title text", text: .constant("Hackathon Happy Hour")) + TextField("some title text", text: $username) .foregroundColor(Color.textDarkGray) .font(.system(size: 14, weight: .heavy, design: .default)) @@ -44,7 +47,7 @@ struct WelcomeView: View { .font(.footnote) .frame(alignment: .leading) Card(isOn: .constant(true),cornerRadius: 5,padding: 8) { - TextField("Existing Wallet Seed Phrase", text: .constant("words words words words words")) + TextField("Existing Wallet Seed Phrase", text: $seedPhrase) .foregroundColor(Color.textDarkGray) .font(.system(size: 14, weight: .heavy, design: .default)) } @@ -56,14 +59,25 @@ struct WelcomeView: View { .font(.footnote) .frame(alignment: .center) Spacer() - Text("Add Wallet to Zircles") - .font(.system(size: 20, weight: .bold, design: .default)) - .shadow(color:Color(.sRGBLinear, red: 0.2, green: 0.2, blue: 0.2, opacity: 0.5), radius: 1, x: 0, y: 2) - .foregroundColor(Color.background) - .modifier(ZcashButtonBackground(buttonShape: .roundedCorners(fillStyle: .gradient(gradient: LinearGradient.zButtonGradient)))) + Button(action: { - .shadow(color: Color(red: 0.2, green: 0.2, blue: 0.2).opacity(0.5), radius: 25, x: 10, y: 10) - .frame(height: 50) + self.isWelcome = true + }) { + Text("Add Wallet to Zircles") + .font(.system(size: 20, weight: .bold, design: .default)) + .shadow(color:Color(.sRGBLinear, red: 0.2, green: 0.2, blue: 0.2, opacity: 0.5), radius: 1, x: 0, y: 2) + .foregroundColor(Color.background) + .modifier(ZcashButtonBackground(buttonShape: .roundedCorners(fillStyle: .gradient(gradient: LinearGradient.zButtonGradient)))) + + .shadow(color: Color(red: 0.2, green: 0.2, blue: 0.2).opacity(0.5), radius: 25, x: 10, y: 10) + .frame(height: 50) + }.disabled(!validInput()) + NavigationLink( + destination: SplashScreen(), + isActive: $isWelcome, + label: { + EmptyView() + }) } .padding([.horizontal,.bottom], 30) @@ -71,6 +85,19 @@ struct WelcomeView: View { }.navigationBarTitle(Text("Welcome")) } + + + func validInput() -> Bool { + validSeedPhrase() && validName() + } + + func validSeedPhrase() -> Bool { + Mnemonic.validate(mnemonic: seedPhrase) + } + + func validName() -> Bool { + !username.isEmpty + } } struct WelcomeView_Previews: PreviewProvider {