diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index 93c3a77..6f6fdd6 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -119,15 +119,18 @@ 9E5BF648282277BE00BA3F17 /* WrappedNotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF647282277BE00BA3F17 /* WrappedNotificationCenter.swift */; }; 9E5BF64F2823E94900BA3F17 /* TransactionAddressTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF64D2823E94900BA3F17 /* TransactionAddressTextField.swift */; }; 9E5BF6502823E94900BA3F17 /* TransactionAddressTextFieldStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF64E2823E94900BA3F17 /* TransactionAddressTextFieldStore.swift */; }; + 9E66122A287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E661229287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift */; }; + 9E66122C2877188700C75B70 /* SyncStatusSnapshot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */; }; 9E69A24D27FB002800A55317 /* WelcomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* WelcomeStore.swift */; }; 9E7CB6122869882D00A02233 /* WalletEventsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB6112869882D00A02233 /* WalletEventsSnapshotTests.swift */; }; + 9E7CB6152869E8C300A02233 /* CircularProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB6142869E8C300A02233 /* CircularProgress.swift */; }; 9E7CB61A287310EC00A02233 /* QRCodeGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB619287310EC00A02233 /* QRCodeGenerator.swift */; }; 9E7CB6202874143800A02233 /* AddressDetailsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB61F2874143800A02233 /* AddressDetailsStore.swift */; }; 9E7CB6212874143800A02233 /* AddressDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB61E2874143800A02233 /* AddressDetailsView.swift */; }; 9E7CB6242874246800A02233 /* ProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB6232874246800A02233 /* ProfileTests.swift */; }; 9E7CB6272874269F00A02233 /* ProfileSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB6262874269F00A02233 /* ProfileSnapshotTests.swift */; }; 9E7CB6292875AC2D00A02233 /* AppVersionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB6282875AC2D00A02233 /* AppVersionHandler.swift */; }; - 9E7FE0CF282D257400C374E8 /* SDKSynchronizer+SyncStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0CE282D257400C374E8 /* SDKSynchronizer+SyncStatus.swift */; }; + 9E7CB62C2875C6E700A02233 /* URLRouting in Frameworks */ = {isa = PBXBuildFile; productRef = 9E7CB62B2875C6E700A02233 /* URLRouting */; }; 9E7FE0D3282D274E00C374E8 /* Date+Readable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0D2282D274E00C374E8 /* Date+Readable.swift */; }; 9E7FE0D5282D281800C374E8 /* Array+Chunked.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0D4282D281800C374E8 /* Array+Chunked.swift */; }; 9E7FE0D7282D286500C374E8 /* RecoveryPhrase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0D6282D286500C374E8 /* RecoveryPhrase.swift */; }; @@ -335,15 +338,17 @@ 9E5BF647282277BE00BA3F17 /* WrappedNotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedNotificationCenter.swift; sourceTree = ""; }; 9E5BF64D2823E94900BA3F17 /* TransactionAddressTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressTextField.swift; sourceTree = ""; }; 9E5BF64E2823E94900BA3F17 /* TransactionAddressTextFieldStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressTextFieldStore.swift; sourceTree = ""; }; + 9E661229287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCircularProgressSnapshotTests.swift; sourceTree = ""; }; + 9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncStatusSnapshot.swift; sourceTree = ""; }; 9E69A24C27FB002800A55317 /* WelcomeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStore.swift; sourceTree = ""; }; 9E7CB6112869882D00A02233 /* WalletEventsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletEventsSnapshotTests.swift; sourceTree = ""; }; + 9E7CB6142869E8C300A02233 /* CircularProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgress.swift; sourceTree = ""; }; 9E7CB619287310EC00A02233 /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = ""; }; 9E7CB61E2874143800A02233 /* AddressDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDetailsView.swift; sourceTree = ""; }; 9E7CB61F2874143800A02233 /* AddressDetailsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDetailsStore.swift; sourceTree = ""; }; 9E7CB6232874246800A02233 /* ProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTests.swift; sourceTree = ""; }; 9E7CB6262874269F00A02233 /* ProfileSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSnapshotTests.swift; sourceTree = ""; }; 9E7CB6282875AC2D00A02233 /* AppVersionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionHandler.swift; sourceTree = ""; }; - 9E7FE0CE282D257400C374E8 /* SDKSynchronizer+SyncStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SDKSynchronizer+SyncStatus.swift"; sourceTree = ""; }; 9E7FE0D2282D274E00C374E8 /* Date+Readable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Readable.swift"; sourceTree = ""; }; 9E7FE0D4282D281800C374E8 /* Array+Chunked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Chunked.swift"; sourceTree = ""; }; 9E7FE0D6282D286500C374E8 /* RecoveryPhrase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhrase.swift; sourceTree = ""; }; @@ -418,6 +423,7 @@ buildActionMask = 2147483647; files = ( 9EF8139827F1FAEC0075AF48 /* ZcashLightClientKit in Frameworks */, + 9E7CB62C2875C6E700A02233 /* URLRouting in Frameworks */, 9E2AC0FF27D8EC120042AA47 /* MnemonicSwift in Frameworks */, 6654C73A2715A38000901167 /* ComposableArchitecture in Frameworks */, 9EAB466F285A0468002904A0 /* _URLRouting in Frameworks */, @@ -597,6 +603,7 @@ 9E7FE0DC282D298900C374E8 /* ValidationWord.swift */, 9E7FE0E5282E7B1100C374E8 /* StoredWallet.swift */, 9EAB46772860A1D2002904A0 /* WalletEvent.swift */, + 9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */, ); path = Models; sourceTree = ""; @@ -884,6 +891,14 @@ path = WalletEventsSnapshotTests; sourceTree = ""; }; + 9E7CB6132869E8A700A02233 /* CircularProgress */ = { + isa = PBXGroup; + children = ( + 9E7CB6142869E8C300A02233 /* CircularProgress.swift */, + ); + path = CircularProgress; + sourceTree = ""; + }; 9E7CB61B2874140900A02233 /* AddressDetails */ = { isa = PBXGroup; children = ( @@ -938,7 +953,6 @@ 0DACFA8027208D940039EEA5 /* UInt+SuperscriptText.swift */, 0D7CE63327349B5D0020E050 /* View+WhenDraggable.swift */, 9E7FE0D2282D274E00C374E8 /* Date+Readable.swift */, - 9E7FE0CE282D257400C374E8 /* SDKSynchronizer+SyncStatus.swift */, 0DACFA7E27208CE00039EEA5 /* Clamped.swift */, F9322DBF273B555C00C105B5 /* NavigationLinks.swift */, F9C165B3274031F600592F76 /* Bindings.swift */, @@ -971,6 +985,7 @@ 9E7FE0BE282D1DFE00C374E8 /* UI Components */ = { isa = PBXGroup; children = ( + 9E7CB6132869E8A700A02233 /* CircularProgress */, 0DF2DC5227235E1F00FA31E2 /* Extensions */, 0DB8AA80271DC7520035BC9D /* DesignGuide.swift */, 9E7FE0E9282E7CF800C374E8 /* ImportSeedEditor */, @@ -1054,6 +1069,7 @@ isa = PBXGroup; children = ( 9E9ECC8C28589E150099D5A2 /* HomeSnapshotTests.swift */, + 9E661229287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift */, ); path = HomeSnapshotTests; sourceTree = ""; @@ -1264,6 +1280,7 @@ 9EF8139727F1FAEC0075AF48 /* ZcashLightClientKit */, 9EAB466C285A0468002904A0 /* Parsing */, 9EAB466E285A0468002904A0 /* _URLRouting */, + 9E7CB62B2875C6E700A02233 /* URLRouting */, ); productName = secant; productReference = 0D4E7A0526B364170058B01E /* secant-testnet.app */; @@ -1343,6 +1360,7 @@ 9E2AC0FD27D8EC120042AA47 /* XCRemoteSwiftPackageReference "MnemonicSwift" */, 9EF8139627F1FAEC0075AF48 /* XCRemoteSwiftPackageReference "ZcashLightClientKit" */, 9EAB466B285A0468002904A0 /* XCRemoteSwiftPackageReference "swift-parsing" */, + 9E7CB62A2875C6E700A02233 /* XCRemoteSwiftPackageReference "swift-url-routing" */, ); productRefGroup = 0D4E7A0626B364170058B01E /* Products */; projectDirPath = ""; @@ -1471,7 +1489,6 @@ 9E5BF648282277BE00BA3F17 /* WrappedNotificationCenter.swift in Sources */, 0D8A43C4272AEEDE005A6414 /* SecantTextStyles.swift in Sources */, 9E5BF641281FD7B600BA3F17 /* TransactionFailedView.swift in Sources */, - 9E7FE0CF282D257400C374E8 /* SDKSynchronizer+SyncStatus.swift in Sources */, 9E4DC6E027C409A100E657F4 /* NeumorphicDesignModifier.swift in Sources */, 9E7CB6292875AC2D00A02233 /* AppVersionHandler.swift in Sources */, 0DACFA7F27208CE00039EEA5 /* Clamped.swift in Sources */, @@ -1484,6 +1501,7 @@ 9E7FE0D9282D289B00C374E8 /* WrappedFeedbackGenerator.swift in Sources */, 2E6CF8DD27D78319004DCD7A /* CurrencySelectionStore.swift in Sources */, 9EBEF87A27CE369800B4F343 /* RecoveryPhraseValidationFlowView.swift in Sources */, + 9E66122C2877188700C75B70 /* SyncStatusSnapshot.swift in Sources */, 9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */, 0DDB6A5127737D4A0012A410 /* RecoveryPhraseBackupFailedView.swift in Sources */, 9E391129283F74590073DD9A /* Zatoshi.swift in Sources */, @@ -1551,6 +1569,7 @@ 9E7FE0F628327F6F00C374E8 /* ScanUIView.swift in Sources */, 0D185819272723FF0046B928 /* ColoredChip.swift in Sources */, 2EA11F5D27467F7700709571 /* OnboardingContentView.swift in Sources */, + 9E7CB6152869E8C300A02233 /* CircularProgress.swift in Sources */, 2E58E73B274679F000B2B84B /* OnboardingHeaderView.swift in Sources */, 9E5BF64F2823E94900BA3F17 /* TransactionAddressTextField.swift in Sources */, 2E35F99227B28E7600EB79CD /* SingleLineTextField.swift in Sources */, @@ -1613,6 +1632,7 @@ 9E5BF63F2819542C00BA3F17 /* WalletEventsTests.swift in Sources */, 0D4E7A1B26B364180058B01E /* secantTests.swift in Sources */, 0DFE93E6272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift in Sources */, + 9E66122A287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift in Sources */, 9EAB4676285B5C7C002904A0 /* DeeplinkTests.swift in Sources */, 9E3911392848AD500073DD9A /* HomeTests.swift in Sources */, 9E9ECC9C28589E150099D5A2 /* OnboardingSnapshotTests.swift in Sources */, @@ -1953,6 +1973,14 @@ minimumVersion = 2.0.0; }; }; + 9E7CB62A2875C6E700A02233 /* XCRemoteSwiftPackageReference "swift-url-routing" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "http://github.com/pointfreeco/swift-url-routing"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.3.0; + }; + }; 9EAB466B285A0468002904A0 /* XCRemoteSwiftPackageReference "swift-parsing" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/pointfreeco/swift-parsing"; @@ -1982,6 +2010,11 @@ package = 9E2AC0FD27D8EC120042AA47 /* XCRemoteSwiftPackageReference "MnemonicSwift" */; productName = MnemonicSwift; }; + 9E7CB62B2875C6E700A02233 /* URLRouting */ = { + isa = XCSwiftPackageProductDependency; + package = 9E7CB62A2875C6E700A02233 /* XCRemoteSwiftPackageReference "swift-url-routing" */; + productName = URLRouting; + }; 9EAB466C285A0468002904A0 /* Parsing */ = { isa = XCSwiftPackageProductDependency; package = 9EAB466B285A0468002904A0 /* XCRemoteSwiftPackageReference "swift-parsing" */; diff --git a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 71b1116..909c08b 100644 --- a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -162,6 +162,15 @@ "version" : "1.19.0" } }, + { + "identity" : "swift-url-routing", + "kind" : "remoteSourceControl", + "location" : "http://github.com/pointfreeco/swift-url-routing", + "state" : { + "revision" : "5bf79bb370015e43842a61d558a9ee053171124e", + "version" : "0.3.0" + } + }, { "identity" : "xctest-dynamic-overlay", "kind" : "remoteSourceControl", diff --git a/secant/Dependencies/DeeplinkHandler.swift b/secant/Dependencies/DeeplinkHandler.swift index 573c23b..35a3a31 100644 --- a/secant/Dependencies/DeeplinkHandler.swift +++ b/secant/Dependencies/DeeplinkHandler.swift @@ -6,7 +6,7 @@ // import Foundation -import _URLRouting +import URLRouting import ComposableArchitecture import ZcashLightClientKit diff --git a/secant/Features/Home/HomeStore.swift b/secant/Features/Home/HomeStore.swift index f3e5428..801b721 100644 --- a/secant/Features/Home/HomeStore.swift +++ b/secant/Features/Home/HomeStore.swift @@ -24,12 +24,33 @@ struct HomeState: Equatable { var drawerOverlay: DrawerOverlay var profileState: ProfileState var requestState: RequestState + var requiredTransactionConfirmations = 0 var sendState: SendFlowState var scanState: ScanState - var synchronizerStatus: String + var synchronizerStatusSnapshot: SyncStatusSnapshot var totalBalance: Zatoshi var walletEventsState: WalletEventsFlowState var verifiedBalance: Zatoshi + // TODO: - Get the ZEC price from the SDK, issue 311, https://github.com/zcash/secant-ios-wallet/issues/311 + var zecPrice = Decimal(140.0) + + var totalCurrencyBalance: Zatoshi { + Zatoshi.from(decimal: totalBalance.decimalValue.decimalValue * zecPrice) + } + + var isDownloading: Bool { + if case .downloading = synchronizerStatusSnapshot.syncStatus { + return true + } + return false + } + + var isUpToDate: Bool { + if case .synced = synchronizerStatusSnapshot.syncStatus { + return true + } + return false + } } // MARK: Action @@ -96,6 +117,7 @@ extension HomeReducer { private static let homeReducer = HomeReducer { state, action, environment in switch action { case .onAppear: + state.requiredTransactionConfirmations = environment.zcashSDKEnvironment.requiredTransactionConfirmations return environment.SDKSynchronizer.stateChanged .map(HomeAction.synchronizerStateChanged) .eraseToEffect() @@ -110,13 +132,6 @@ extension HomeReducer { .receive(on: environment.scheduler) .map(HomeAction.updateWalletEvents) .eraseToEffect(), - - environment.SDKSynchronizer.getShieldedBalance() - .receive(on: environment.scheduler) - .map({ Balance(verified: $0.verified, total: $0.total) }) - .map(HomeAction.updateBalance) - .eraseToEffect(), - Effect(value: .updateSynchronizerStatus) ) @@ -137,8 +152,12 @@ extension HomeReducer { return .none case .updateSynchronizerStatus: - state.synchronizerStatus = environment.SDKSynchronizer.status() - return .none + state.synchronizerStatusSnapshot = environment.SDKSynchronizer.statusSnapshot() + return environment.SDKSynchronizer.getShieldedBalance() + .receive(on: environment.scheduler) + .map({ Balance(verified: $0.verified, total: $0.total) }) + .map(HomeAction.updateBalance) + .eraseToEffect() case .updateRoute(let route): state.route = route @@ -305,7 +324,7 @@ extension HomeState { requestState: .placeholder, sendState: .placeholder, scanState: .placeholder, - synchronizerStatus: "", + synchronizerStatusSnapshot: .default, totalBalance: Zatoshi.zero, walletEventsState: .emptyPlaceHolder, verifiedBalance: Zatoshi.zero diff --git a/secant/Features/Home/HomeView.swift b/secant/Features/Home/HomeView.swift index a9ee855..d9e11f2 100644 --- a/secant/Features/Home/HomeView.swift +++ b/secant/Features/Home/HomeView.swift @@ -11,22 +11,11 @@ struct HomeView: View { scanButton(viewStore) profileButton(viewStore) - + + circularArea(viewStore, proxy.size) + sendButton(viewStore) - VStack { - Text("\(viewStore.synchronizerStatus)") - .padding(.top, 60) - - Text("balance \(viewStore.totalBalance.decimalString()) ZEC") - .accessDebugMenuWithHiddenGesture { - viewStore.send(.debugMenuStartup) - } - .padding(.top, 120) - - Spacer() - } - if proxy.size.height > 0 { Drawer(overlay: viewStore.bindingForDrawer(), maxHeight: proxy.size.height) { VStack { @@ -122,6 +111,40 @@ extension HomeView { Spacer() } } + + func circularArea(_ viewStore: HomeViewStore, _ size: CGSize) -> some View { + VStack { + ZStack { + CircularProgress( + outerCircleProgress: viewStore.isDownloading ? 0 : viewStore.synchronizerStatusSnapshot.progress, + innerCircleProgress: viewStore.isDownloading ? viewStore.synchronizerStatusSnapshot.progress : 1, + maxSegments: viewStore.requiredTransactionConfirmations, + innerCircleHidden: viewStore.isUpToDate + ) + .frame(width: size.width * 0.65, height: size.width * 0.65) + .padding(.top, 50) + + VStack { + Text("$\(viewStore.totalBalance.decimalString())") + .font(.custom(FontFamily.Zboto.regular.name, size: 40)) + .foregroundColor(Asset.Colors.Text.balanceText.color) + .accessDebugMenuWithHiddenGesture { + viewStore.send(.debugMenuStartup) + } + .padding(.top, 80) + + Text("$\(viewStore.totalCurrencyBalance.decimalString())") + .font(.custom(FontFamily.Rubik.regular.name, size: 13)) + .opacity(0.6) + .padding(.bottom, 50) + + Text("\(viewStore.synchronizerStatusSnapshot.message)") + } + } + + Spacer() + } + } } // MARK: - Previews diff --git a/secant/Models/SyncStatusSnapshot.swift b/secant/Models/SyncStatusSnapshot.swift new file mode 100644 index 0000000..19a1cf2 --- /dev/null +++ b/secant/Models/SyncStatusSnapshot.swift @@ -0,0 +1,59 @@ +// +// SyncStatusSnapshot.swift +// secant-testnet +// +// Created by Lukáš Korba on 07.07.2022. +// + +import Foundation +import ZcashLightClientKit + +struct SyncStatusSnapshot: Equatable { + let message: String + let progress: Float + let syncStatus: SyncStatus + + init(_ syncStatus: SyncStatus = .unprepared, _ message: String = "", _ progress: Float = 0) { + self.message = message + self.progress = progress + self.syncStatus = syncStatus + } + + static func snapshotFor(state: SyncStatus) -> SyncStatusSnapshot { + switch state { + case .downloading(let progress): + return SyncStatusSnapshot(state, "downloading - \(String(format: "%d%%", Int(progress.progress * 100.0)))", progress.progress) + + case .enhancing(let enhanceProgress): + return SyncStatusSnapshot(state, "Enhancing tx \(enhanceProgress.enhancedTransactions) of \(enhanceProgress.totalTransactions)") + + case .fetching: + return SyncStatusSnapshot(state, "fetching UTXOs") + + case .scanning(let progress): + return SyncStatusSnapshot(state, "scanning - \(String(format: "%d%%", Int(progress.progress * 100.0)))", progress.progress) + + case .disconnected: + return SyncStatusSnapshot(state, "disconnected 💔") + + case .stopped: + return SyncStatusSnapshot(state, "Stopped 🚫") + + case .synced: + return SyncStatusSnapshot(state, "Up-To-Date") + + case .unprepared: + return SyncStatusSnapshot(state, "Unprepared 😅") + + case .validating: + return SyncStatusSnapshot(state, "Validating") + + case .error(let err): + return SyncStatusSnapshot(state, "Error: \(err.localizedDescription)") + } + } +} + +extension SyncStatusSnapshot { + static let `default` = SyncStatusSnapshot() +} diff --git a/secant/Resources/Colors.xcassets/Text/balanceText.colorset/Contents.json b/secant/Resources/Colors.xcassets/Text/balanceText.colorset/Contents.json new file mode 100644 index 0000000..1f23311 --- /dev/null +++ b/secant/Resources/Colors.xcassets/Text/balanceText.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0xB8", + "red" : "0xFF" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x00", + "green" : "0xB8", + "red" : "0xFF" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/secant/Resources/Generated/XCAssets+Generated.swift b/secant/Resources/Generated/XCAssets+Generated.swift index 2f84692..3478275 100644 --- a/secant/Resources/Generated/XCAssets+Generated.swift +++ b/secant/Resources/Generated/XCAssets+Generated.swift @@ -113,6 +113,7 @@ internal enum Asset { internal static let titleText = ColorAsset(name: "TitleText") internal static let transactionDetailText = ColorAsset(name: "TransactionDetailText") internal static let validMnemonic = ColorAsset(name: "ValidMnemonic") + internal static let balanceText = ColorAsset(name: "balanceText") internal static let captionText = ColorAsset(name: "captionText") internal static let captionTextShadow = ColorAsset(name: "captionTextShadow") internal static let highlightedSuperscriptText = ColorAsset(name: "highlightedSuperscriptText") diff --git a/secant/UI Components/CircularProgress/CircularProgress.swift b/secant/UI Components/CircularProgress/CircularProgress.swift new file mode 100644 index 0000000..59b9c9b --- /dev/null +++ b/secant/UI Components/CircularProgress/CircularProgress.swift @@ -0,0 +1,118 @@ +// +// CircularProgress.swift +// secant-testnet +// +// Created by Lukáš Korba on 27.06.2022. +// + +import SwiftUI + +struct CircularProgress: View { + let outerCircleProgress: Float + let innerCircleProgress: Float + let maxSegments: Int + let innerCircleHidden: Bool + + var outerCircleProgressCG: CGFloat { + CGFloat(outerCircleProgress) + } + + var innerCircleProgressCG: CGFloat { + CGFloat(innerCircleProgress) + } + + var body: some View { + let segmentTrimComputed = segmentTrim + let gap = (2.0 / 360.0) // 2 degrees + + return ZStack { + if !innerCircleHidden { + Circle() + .trim( + from: 0.0, + to: innerCircleProgressCG + ) + .stroke( + Asset.Colors.ProgressIndicator.negativeSpace.color, + style: StrokeStyle(lineWidth: 17.0, dash: [2]) + ) + .rotationEffect(Angle(degrees: -90)) + } + + ForEach((0.. CGFloat { + var result = segmentTrim * CGFloat(segmentIndex) + + if result > progress { + result = 0 + } + + return result + } + + func toValue(_ segmentIndex: Int, _ segmentTrim: CGFloat, _ progress: CGFloat, _ gap: CGFloat) -> CGFloat { + var result = fromValue(segmentIndex, segmentTrim, progress) + + if result > progress { + result = 0 + } else if result + segmentTrim - gap < progress { + result += segmentTrim - gap + } else { + result += progress - (segmentTrim * CGFloat(segmentIndex)) - gap + } + + return result + } +} + +struct CircularProgress_Previews: PreviewProvider { + static var previews: some View { + GeometryReader { proxy in + CircularProgress( + outerCircleProgress: 0.33, + innerCircleProgress: 0.8, + maxSegments: 10, + innerCircleHidden: true + ) + .frame(width: proxy.size.width * 0.8, height: proxy.size.width * 0.8) + .offset(x: 40, y: 200) + } + .applyScreenBackground() + .preferredColorScheme(.dark) + } +} diff --git a/secant/Utils/SDKSynchronizer+SyncStatus.swift b/secant/Utils/SDKSynchronizer+SyncStatus.swift deleted file mode 100644 index b6b7568..0000000 --- a/secant/Utils/SDKSynchronizer+SyncStatus.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// SDKSynchronizer+SyncStatus.swift -// secant-testnet -// -// Created by Lukáš Korba on 12.05.2022. -// - -import Foundation -import ZcashLightClientKit - -extension SDKSynchronizer { - static func textFor(state: SyncStatus) -> String { - switch state { - case .downloading(let progress): - return "Downloading \(progress.progressHeight)/\(progress.targetHeight)" - - case .enhancing(let enhanceProgress): - return "Enhancing tx \(enhanceProgress.enhancedTransactions) of \(enhanceProgress.totalTransactions)" - - case .fetching: - return "fetching UTXOs" - - case .scanning(let scanProgress): - return "Scanning: \(scanProgress.progressHeight)/\(scanProgress.targetHeight)" - - case .disconnected: - return "disconnected 💔" - - case .stopped: - return "Stopped 🚫" - - case .synced: - return "Synced 😎" - - case .unprepared: - return "Unprepared 😅" - - case .validating: - return "Validating" - - case .error(let err): - return "Error: \(err.localizedDescription)" - } - } -} diff --git a/secant/Wrappers/WrappedSDKSynchronizer.swift b/secant/Wrappers/WrappedSDKSynchronizer.swift index f3391d7..0dc645e 100644 --- a/secant/Wrappers/WrappedSDKSynchronizer.swift +++ b/secant/Wrappers/WrappedSDKSynchronizer.swift @@ -45,7 +45,7 @@ protocol WrappedSDKSynchronizer { func prepareWith(initializer: Initializer) throws func start(retry: Bool) throws func stop() - func status() -> String + func statusSnapshot() -> SyncStatusSnapshot func getShieldedBalance() -> Effect func getAllClearedTransactions() -> Effect<[WalletEvent], Never> @@ -143,12 +143,12 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer { stateChanged.send(.stopped) } - func status() -> String { + func statusSnapshot() -> SyncStatusSnapshot { guard let synchronizer = synchronizer else { - return "" + return .default } - return SDKSynchronizer.textFor(state: synchronizer.status) + return SyncStatusSnapshot.snapshotFor(state: synchronizer.status) } func getShieldedBalance() -> Effect { @@ -281,12 +281,12 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { stateChanged.send(.synced) } - func status() -> String { + func statusSnapshot() -> SyncStatusSnapshot { guard let synchronizer = synchronizer else { - return "" + return .default } - return SDKSynchronizer.textFor(state: synchronizer.status) + return SyncStatusSnapshot.snapshotFor(state: synchronizer.status) } func getShieldedBalance() -> Effect { @@ -397,7 +397,7 @@ class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer { func synchronizerSynced() { } - func status() -> String { "" } + func statusSnapshot() -> SyncStatusSnapshot { .default } func getShieldedBalance() -> Effect { return .none diff --git a/secantTests/HomeTests/HomeTests.swift b/secantTests/HomeTests/HomeTests.swift index 9a2b97c..9323287 100644 --- a/secantTests/HomeTests/HomeTests.swift +++ b/secantTests/HomeTests/HomeTests.swift @@ -33,7 +33,15 @@ class HomeTests: XCTestCase { store.send(.synchronizerStateChanged(.downloading)) + testScheduler.advance(by: 0.01) + store.receive(.updateSynchronizerStatus) + + let balance = Balance(verified: 12_345_000, total: 12_345_000) + store.receive(.updateBalance(balance)) { state in + state.totalBalance = Zatoshi(amount: 12_345_000) + state.verifiedBalance = Zatoshi(amount: 12_345_000) + } } /// When the synchronizer status change to .synced, several things happen @@ -120,7 +128,7 @@ class HomeTests: XCTestCase { requestState: .placeholder, sendState: .placeholder, scanState: .placeholder, - synchronizerStatus: "", + synchronizerStatusSnapshot: .default, totalBalance: Zatoshi.zero, walletEventsState: .emptyPlaceHolder, verifiedBalance: Zatoshi.zero @@ -163,7 +171,7 @@ class HomeTests: XCTestCase { requestState: .placeholder, sendState: .placeholder, scanState: .placeholder, - synchronizerStatus: "", + synchronizerStatusSnapshot: .default, totalBalance: Zatoshi.zero, walletEventsState: .emptyPlaceHolder, verifiedBalance: Zatoshi.zero @@ -208,7 +216,9 @@ class HomeTests: XCTestCase { environment: testEnvironment ) - store.send(.onAppear) + store.send(.onAppear) { state in + state.requiredTransactionConfirmations = 10 + } testScheduler.advance(by: 0.01) @@ -216,6 +226,12 @@ class HomeTests: XCTestCase { store.receive(.synchronizerStateChanged(.unknown)) store.receive(.updateSynchronizerStatus) + let balance = Balance(verified: 12_345_000, total: 12_345_000) + store.receive(.updateBalance(balance)) { state in + state.totalBalance = Zatoshi(amount: 12_345_000) + state.verifiedBalance = Zatoshi(amount: 12_345_000) + } + // long-living (cancelable) effects need to be properly canceled. // the .onDisappear action cancles the observer of the synchronizer status change. store.send(.onDisappear) diff --git a/secantTests/SnapshotTests/HomeSnapshotTests/HomeCircularProgressSnapshotTests.swift b/secantTests/SnapshotTests/HomeSnapshotTests/HomeCircularProgressSnapshotTests.swift new file mode 100644 index 0000000..b02a7c9 --- /dev/null +++ b/secantTests/SnapshotTests/HomeSnapshotTests/HomeCircularProgressSnapshotTests.swift @@ -0,0 +1,150 @@ +// +// HomeCircularProgressSnapshotTests.swift +// secantTests +// +// Created by Lukáš Korba on 07.07.2022. +// + +import XCTest +import ComposableArchitecture +@testable import secant_testnet +@testable import ZcashLightClientKit + +class HomeCircularProgressSnapshotTests: XCTestCase { + func testCircularProgress_DownloadingInnerCircle() throws { + class SnapshotTestWrappedSDKSynchronizer: TestWrappedSDKSynchronizer { + // heights purposely set so we visually see 55% progress + override func statusSnapshot() -> SyncStatusSnapshot { + let blockProgress = BlockProgress( + startHeight: BlockHeight(0), + targetHeight: BlockHeight(100), + progressHeight: BlockHeight(55) + ) + + return SyncStatusSnapshot.snapshotFor(state: .downloading(blockProgress)) + } + } + + let balance = Balance(verified: 15_345_000, total: 15_345_000) + + let testScheduler = DispatchQueue.test + + let testEnvironment = HomeEnvironment( + audioServices: .silent, + derivationTool: .live(), + feedbackGenerator: .silent, + mnemonic: .mock, + scheduler: testScheduler.eraseToAnyScheduler(), + SDKSynchronizer: SnapshotTestWrappedSDKSynchronizer(), + walletStorage: .throwing, + zcashSDKEnvironment: .testnet + ) + + let store = HomeStore( + initialState: .init( + drawerOverlay: .partial, + profileState: .placeholder, + requestState: .placeholder, + sendState: .placeholder, + scanState: .placeholder, + synchronizerStatusSnapshot: .default, + totalBalance: Zatoshi(amount: balance.total), + walletEventsState: .emptyPlaceHolder, + verifiedBalance: Zatoshi(amount: balance.verified) + ), + reducer: .default, + environment: testEnvironment + ) + + addAttachments(HomeView(store: store)) + } + + func testCircularProgress_ScanningOuterCircle() throws { + class SnapshotTestWrappedSDKSynchronizer: TestWrappedSDKSynchronizer { + override func statusSnapshot() -> SyncStatusSnapshot { + // heights purposely set so we visually see 72% progress + let blockProgress = BlockProgress( + startHeight: BlockHeight(0), + targetHeight: BlockHeight(100), + progressHeight: BlockHeight(72) + ) + + return SyncStatusSnapshot.snapshotFor(state: .scanning(blockProgress)) + } + } + + let balance = Balance(verified: 15_345_000, total: 15_345_000) + + let testScheduler = DispatchQueue.test + + let testEnvironment = HomeEnvironment( + audioServices: .silent, + derivationTool: .live(), + feedbackGenerator: .silent, + mnemonic: .mock, + scheduler: testScheduler.eraseToAnyScheduler(), + SDKSynchronizer: SnapshotTestWrappedSDKSynchronizer(), + walletStorage: .throwing, + zcashSDKEnvironment: .testnet + ) + + let store = HomeStore( + initialState: .init( + drawerOverlay: .partial, + profileState: .placeholder, + requestState: .placeholder, + sendState: .placeholder, + scanState: .placeholder, + synchronizerStatusSnapshot: .default, + totalBalance: Zatoshi(amount: balance.total), + walletEventsState: .emptyPlaceHolder, + verifiedBalance: Zatoshi(amount: balance.verified) + ), + reducer: .default, + environment: testEnvironment + ) + + addAttachments(HomeView(store: store)) + } + + func testCircularProgress_UpToDateOnlyOuterCircle() throws { + class SnapshotTestWrappedSDKSynchronizer: TestWrappedSDKSynchronizer { + override func statusSnapshot() -> SyncStatusSnapshot { + SyncStatusSnapshot.snapshotFor(state: .synced) + } + } + + let balance = Balance(verified: 15_345_000, total: 15_345_000) + + let testScheduler = DispatchQueue.test + + let testEnvironment = HomeEnvironment( + audioServices: .silent, + derivationTool: .live(), + feedbackGenerator: .silent, + mnemonic: .mock, + scheduler: testScheduler.eraseToAnyScheduler(), + SDKSynchronizer: SnapshotTestWrappedSDKSynchronizer(), + walletStorage: .throwing, + zcashSDKEnvironment: .testnet + ) + + let store = HomeStore( + initialState: .init( + drawerOverlay: .partial, + profileState: .placeholder, + requestState: .placeholder, + sendState: .placeholder, + scanState: .placeholder, + synchronizerStatusSnapshot: .default, + totalBalance: Zatoshi(amount: balance.total), + walletEventsState: .emptyPlaceHolder, + verifiedBalance: Zatoshi(amount: balance.verified) + ), + reducer: .default, + environment: testEnvironment + ) + + addAttachments(HomeView(store: store)) + } +} diff --git a/secantTests/SnapshotTests/HomeSnapshotTests/HomeSnapshotTests.swift b/secantTests/SnapshotTests/HomeSnapshotTests/HomeSnapshotTests.swift index 0ce7d5d..4d9eeb4 100644 --- a/secantTests/SnapshotTests/HomeSnapshotTests/HomeSnapshotTests.swift +++ b/secantTests/SnapshotTests/HomeSnapshotTests/HomeSnapshotTests.swift @@ -40,7 +40,7 @@ class HomeSnapshotTests: XCTestCase { requestState: .placeholder, sendState: .placeholder, scanState: .placeholder, - synchronizerStatus: "", + synchronizerStatusSnapshot: .default, totalBalance: Zatoshi(amount: balance.total), walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: walletEvents)), verifiedBalance: Zatoshi(amount: balance.verified) @@ -90,7 +90,7 @@ class HomeSnapshotTests: XCTestCase { requestState: .placeholder, sendState: .placeholder, scanState: .placeholder, - synchronizerStatus: "", + synchronizerStatusSnapshot: .default, totalBalance: Zatoshi(amount: balance.total), walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])), verifiedBalance: Zatoshi(amount: balance.verified) diff --git a/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift b/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift index be5ad6c..925ac7a 100644 --- a/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift +++ b/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift @@ -37,7 +37,7 @@ class WalletEventsSnapshotTests: XCTestCase { requestState: .placeholder, sendState: .placeholder, scanState: .placeholder, - synchronizerStatus: "", + synchronizerStatusSnapshot: .default, totalBalance: Zatoshi(amount: balance.total), walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])), verifiedBalance: Zatoshi(amount: balance.verified) @@ -94,7 +94,7 @@ class WalletEventsSnapshotTests: XCTestCase { requestState: .placeholder, sendState: .placeholder, scanState: .placeholder, - synchronizerStatus: "", + synchronizerStatusSnapshot: .default, totalBalance: Zatoshi(amount: balance.total), walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])), verifiedBalance: Zatoshi(amount: balance.verified) @@ -151,7 +151,7 @@ class WalletEventsSnapshotTests: XCTestCase { requestState: .placeholder, sendState: .placeholder, scanState: .placeholder, - synchronizerStatus: "", + synchronizerStatusSnapshot: .default, totalBalance: Zatoshi(amount: balance.total), walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])), verifiedBalance: Zatoshi(amount: balance.verified) @@ -214,7 +214,7 @@ class WalletEventsSnapshotTests: XCTestCase { requestState: .placeholder, sendState: .placeholder, scanState: .placeholder, - synchronizerStatus: "", + synchronizerStatusSnapshot: .default, totalBalance: Zatoshi(amount: balance.total), walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])), verifiedBalance: Zatoshi(amount: balance.verified)