From 368f95e7a79d4216fb7ba9a5642f68ea6608ad6d Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Wed, 17 Aug 2022 15:22:35 +0200 Subject: [PATCH] [#224] [Scaffold] Balance Breakdown (#412) - full screen cover for the balance breakdown - clearing out the background so we can do semi transparency - bindings for the full screen cover [224] [Scaffold] Balance Breakdown - draft of UI [224] [Scaffold] Balance Breakdown - latest block business logic - mocked auto shielding threshold - unit tests - snapshot tests [224] [Scaffold] Balance Breakdown (412) - comments resolved - bigger refactor of the synchronizer, taking advantage of SynchronizerState being reported every time .synced pass - unit tests fixed and refactored [224] [Scaffold] Balance Breakdown (412) - ClearBackgroundView documented, reference to the proposed solution added --- secant.xcodeproj/project.pbxproj | 65 +++++++--- .../xcshareddata/swiftpm/Package.resolved | 4 +- secant/Dependencies/ZCashSDKEnvironment.swift | 33 ++++- .../BalanceBreakdownStore.swift | 121 ++++++++++++++++++ .../BalanceBreakdownView.swift | 68 ++++++++++ secant/Features/Home/HomeStore.swift | 62 ++++++--- secant/Features/Home/HomeView.swift | 23 ++-- secant/Features/SendFlow/SendFlowStore.swift | 20 +-- .../Views/CreateTransactionView.swift | 2 +- .../gradientEnd.colorset/Contents.json | 6 +- .../gradientStart.colorset/Contents.json | 6 +- .../modalDialog.colorset/Contents.json | 38 ++++++ .../Contents.json | 38 ++++++ .../Contents.json | 38 ++++++ .../Generated/XCAssets+Generated.swift | 3 + .../Backgrounds/ScreenBackground.swift | 11 ++ secant/Utils/ClearBackgroundView.swift | 25 ++++ secant/Wrappers/WrappedNumberFormatter.swift | 9 ++ secant/Wrappers/WrappedSDKSynchronizer.swift | 51 +++++--- .../BalanceBreakdownTests.swift | 42 ++++++ secantTests/HomeTests/HomeTests.swift | 54 +++----- secantTests/ProfileTests/ProfileTests.swift | 2 +- secantTests/SendTests/SendTests.swift | 2 +- .../BalanceBreakdownSnapshotTests.swift | 29 +++++ .../HomeCircularProgressSnapshotTests.swift | 24 ++-- .../HomeSnapshotTests/HomeSnapshotTests.swift | 8 +- .../WalletEventsSnapshotTests.swift | 40 +++--- 27 files changed, 654 insertions(+), 170 deletions(-) create mode 100644 secant/Features/BalanceBreakdown/BalanceBreakdownStore.swift create mode 100644 secant/Features/BalanceBreakdown/BalanceBreakdownView.swift create mode 100644 secant/Resources/Colors.xcassets/ScreenBackground/modalDialog.colorset/Contents.json create mode 100644 secant/Resources/Colors.xcassets/ScreenBackground/semiTransparentGradientEnd.colorset/Contents.json create mode 100644 secant/Resources/Colors.xcassets/ScreenBackground/semiTransparentGradientStart.colorset/Contents.json create mode 100644 secant/Utils/ClearBackgroundView.swift create mode 100644 secantTests/BalanceBreakdownTests/BalanceBreakdownTests.swift create mode 100644 secantTests/SnapshotTests/BalanceBreakdownSnapshotTests/BalanceBreakdownSnapshotTests.swift diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index 38d31a0..e4108d9 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -128,6 +128,9 @@ 9E66129B28884BFB00C75B70 /* LocalAuthenticationHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E66129A28884BFB00C75B70 /* LocalAuthenticationHandler.swift */; }; 9E66129E288938A300C75B70 /* SettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E66129D288938A300C75B70 /* SettingsTests.swift */; }; 9E6713F12897F81B00A6796F /* MultiLineTextFieldTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6713F02897F81B00A6796F /* MultiLineTextFieldTests.swift */; }; + 9E6713F7289BC58C00A6796F /* BalanceBreakdownStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6713F6289BC58C00A6796F /* BalanceBreakdownStore.swift */; }; + 9E6713F8289BC58C00A6796F /* BalanceBreakdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6713F5289BC58C00A6796F /* BalanceBreakdownView.swift */; }; + 9E6713FA289BE0E100A6796F /* ClearBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6713F9289BE0E100A6796F /* ClearBackgroundView.swift */; }; 9E69A24D27FB002800A55317 /* WelcomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* WelcomeStore.swift */; }; 9E7225F12889539300DF7F17 /* SettingsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F02889539300DF7F17 /* SettingsSnapshotTests.swift */; }; 9E7225F3288AB6DD00DF7F17 /* MultipleLineTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F2288AB6DD00DF7F17 /* MultipleLineTextField.swift */; }; @@ -155,6 +158,8 @@ 9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */; }; 9E87ADF128363DE400122FCC /* WrappedAudioServices.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E87ADF028363DE400122FCC /* WrappedAudioServices.swift */; }; 9E92AF0828530EBF007367AD /* View+UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E92AF0728530EBF007367AD /* View+UIImage.swift */; }; + 9E94C62028AA7DEE008256E9 /* BalanceBreakdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E94C61F28AA7DEE008256E9 /* BalanceBreakdownTests.swift */; }; + 9E94C62328AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */; }; 9E9ECC9728589E150099D5A2 /* HomeSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9ECC8C28589E150099D5A2 /* HomeSnapshotTests.swift */; }; 9E9ECC9828589E150099D5A2 /* WelcomeSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9ECC8E28589E150099D5A2 /* WelcomeSnapshotTests.swift */; }; 9E9ECC9928589E150099D5A2 /* RecoveryPhraseDisplaySnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9ECC9028589E150099D5A2 /* RecoveryPhraseDisplaySnapshotTests.swift */; }; @@ -184,7 +189,6 @@ 9EF8135D27ECC25E0075AF48 /* UserPreferencesStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8135B27ECC25E0075AF48 /* UserPreferencesStorageTests.swift */; }; 9EF8136027F043CC0075AF48 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8135F27F043CC0075AF48 /* AppDelegate.swift */; }; 9EF8139127F191BF0075AF48 /* WrappedWalletStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8139027F191BF0075AF48 /* WrappedWalletStorage.swift */; }; - 9EF8139827F1FAEC0075AF48 /* ZcashLightClientKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9EF8139727F1FAEC0075AF48 /* ZcashLightClientKit */; }; 9EF8139C27F47AED0075AF48 /* InitializationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EF8139B27F47AED0075AF48 /* InitializationState.swift */; }; F9322DC0273B555C00C105B5 /* NavigationLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9322DBF273B555C00C105B5 /* NavigationLinks.swift */; }; F93673D62742CB840099C6AF /* Previews.swift in Sources */ = {isa = PBXBuildFile; fileRef = F93673D52742CB840099C6AF /* Previews.swift */; }; @@ -355,6 +359,9 @@ 9E66129A28884BFB00C75B70 /* LocalAuthenticationHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAuthenticationHandler.swift; sourceTree = ""; }; 9E66129D288938A300C75B70 /* SettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTests.swift; sourceTree = ""; }; 9E6713F02897F81B00A6796F /* MultiLineTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiLineTextFieldTests.swift; sourceTree = ""; }; + 9E6713F5289BC58C00A6796F /* BalanceBreakdownView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownView.swift; sourceTree = ""; }; + 9E6713F6289BC58C00A6796F /* BalanceBreakdownStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownStore.swift; sourceTree = ""; }; + 9E6713F9289BE0E100A6796F /* ClearBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearBackgroundView.swift; sourceTree = ""; }; 9E69A24C27FB002800A55317 /* WelcomeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStore.swift; sourceTree = ""; }; 9E7225F02889539300DF7F17 /* SettingsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSnapshotTests.swift; sourceTree = ""; }; 9E7225F2288AB6DD00DF7F17 /* MultipleLineTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleLineTextField.swift; sourceTree = ""; }; @@ -380,6 +387,8 @@ 9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeScanView.swift; sourceTree = ""; }; 9E87ADF028363DE400122FCC /* WrappedAudioServices.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrappedAudioServices.swift; sourceTree = ""; }; 9E92AF0728530EBF007367AD /* View+UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+UIImage.swift"; sourceTree = ""; }; + 9E94C61F28AA7DEE008256E9 /* BalanceBreakdownTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownTests.swift; sourceTree = ""; }; + 9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownSnapshotTests.swift; sourceTree = ""; }; 9E9ECC8C28589E150099D5A2 /* HomeSnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeSnapshotTests.swift; sourceTree = ""; }; 9E9ECC8E28589E150099D5A2 /* WelcomeSnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeSnapshotTests.swift; sourceTree = ""; }; 9E9ECC9028589E150099D5A2 /* RecoveryPhraseDisplaySnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseDisplaySnapshotTests.swift; sourceTree = ""; }; @@ -440,7 +449,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9EF8139827F1FAEC0075AF48 /* ZcashLightClientKit in Frameworks */, 9E6612312878337F00C75B70 /* Lottie in Frameworks */, 9E7CB6182872D3DF00A02233 /* URLRouting in Frameworks */, 0DB4E0B42881FD9100947B78 /* ZcashLightClientKit in Frameworks */, @@ -555,6 +563,7 @@ isa = PBXGroup; children = ( 9E391162284E3ECF0073DD9A /* SnapshotTests */, + 9E94C61E28AA7DD5008256E9 /* BalanceBreakdownTests */, 9E6713EF2897F80A00A6796F /* MultiLineTextFieldTests */, 9E7CB6222874245400A02233 /* ProfileTests */, 9EAB4674285B5C68002904A0 /* DeeplinkTests */, @@ -718,6 +727,7 @@ 0D0781C2278750C00083ACD7 /* Welcome */, F9971A4927680DC400A2DB75 /* App */, F93874EC273C4DE200F0E875 /* Home */, + 9E6713F2289BC51200A6796F /* BalanceBreakdown */, F9971A4F27680DD000A2DB75 /* Profile */, 9E7CB61B2874140900A02233 /* AddressDetails */, F9971A5527680DDE00A2DB75 /* Request */, @@ -856,6 +866,7 @@ 9E391162284E3ECF0073DD9A /* SnapshotTests */ = { isa = PBXGroup; children = ( + 9E94C62128AA7ECD008256E9 /* BalanceBreakdownSnapshotTests */, 9E7225EF2889537E00DF7F17 /* SettingsSnapshotTests */, 9E7CB6252874267B00A02233 /* ProfileSnapshotTests */, 9E7CB6102869881300A02233 /* WalletEventsSnapshotTests */, @@ -931,6 +942,15 @@ path = MultiLineTextFieldTests; sourceTree = ""; }; + 9E6713F2289BC51200A6796F /* BalanceBreakdown */ = { + isa = PBXGroup; + children = ( + 9E6713F6289BC58C00A6796F /* BalanceBreakdownStore.swift */, + 9E6713F5289BC58C00A6796F /* BalanceBreakdownView.swift */, + ); + path = BalanceBreakdown; + sourceTree = ""; + }; 9E7225EF2889537E00DF7F17 /* SettingsSnapshotTests */ = { isa = PBXGroup; children = ( @@ -1029,6 +1049,7 @@ 2EDA07A327EDE2A900D6F09B /* DebugFrame.swift */, 9E2F1C832809B606004E65FE /* DebugMenu.swift */, 9E7CB619287310EC00A02233 /* QRCodeGenerator.swift */, + 9E6713F9289BE0E100A6796F /* ClearBackgroundView.swift */, ); path = Utils; sourceTree = ""; @@ -1132,6 +1153,22 @@ path = UIKitBridge; sourceTree = ""; }; + 9E94C61E28AA7DD5008256E9 /* BalanceBreakdownTests */ = { + isa = PBXGroup; + children = ( + 9E94C61F28AA7DEE008256E9 /* BalanceBreakdownTests.swift */, + ); + path = BalanceBreakdownTests; + sourceTree = ""; + }; + 9E94C62128AA7ECD008256E9 /* BalanceBreakdownSnapshotTests */ = { + isa = PBXGroup; + children = ( + 9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */, + ); + path = BalanceBreakdownSnapshotTests; + sourceTree = ""; + }; 9E9ECC8B28589E150099D5A2 /* HomeSnapshotTests */ = { isa = PBXGroup; children = ( @@ -1345,7 +1382,6 @@ packageProductDependencies = ( 6654C7392715A38000901167 /* ComposableArchitecture */, 9E2AC0FE27D8EC120042AA47 /* MnemonicSwift */, - 9EF8139727F1FAEC0075AF48 /* ZcashLightClientKit */, 9EAB466C285A0468002904A0 /* Parsing */, 9EAB466E285A0468002904A0 /* _URLRouting */, 9E7CB6172872D3DF00A02233 /* URLRouting */, @@ -1428,7 +1464,6 @@ packageReferences = ( 6654C7382715A38000901167 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, 9E2AC0FD27D8EC120042AA47 /* XCRemoteSwiftPackageReference "MnemonicSwift" */, - 9EF8139627F1FAEC0075AF48 /* XCRemoteSwiftPackageReference "ZcashLightClientKit" */, 9EAB466B285A0468002904A0 /* XCRemoteSwiftPackageReference "swift-parsing" */, 9E7CB6162872D3DF00A02233 /* XCRemoteSwiftPackageReference "swift-url-routing" */, 9E66122F2878337F00C75B70 /* XCRemoteSwiftPackageReference "lottie-ios" */, @@ -1574,6 +1609,7 @@ 9E7FE0D9282D289B00C374E8 /* WrappedFeedbackGenerator.swift in Sources */, 2E6CF8DD27D78319004DCD7A /* CurrencySelectionStore.swift in Sources */, 9EBEF87A27CE369800B4F343 /* RecoveryPhraseValidationFlowView.swift in Sources */, + 9E6713F7289BC58C00A6796F /* BalanceBreakdownStore.swift in Sources */, 9E66122C2877188700C75B70 /* SyncStatusSnapshot.swift in Sources */, 9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */, 0DDB6A5127737D4A0012A410 /* RecoveryPhraseBackupFailedView.swift in Sources */, @@ -1657,6 +1693,7 @@ F9C165B4274031F600592F76 /* Bindings.swift in Sources */, 2E35F99A27B3E99C00EB79CD /* TextFieldTitleAccessoryButtonStyle.swift in Sources */, 9E2DF99C27CF704D00649636 /* ImportWalletStore.swift in Sources */, + 9E6713F8289BC58C00A6796F /* BalanceBreakdownView.swift in Sources */, F9971A6627680DFE00A2DB75 /* SettingsView.swift in Sources */, F96B41EB273B50520021B49A /* Strings.swift in Sources */, 2EDA07A227EDE1AE00D6F09B /* TextFieldFooter.swift in Sources */, @@ -1675,6 +1712,7 @@ 0D0781C9278776D20083ACD7 /* ZcashSymbol.swift in Sources */, 2E8719CB27FB09990082C926 /* TransactionAmountTextField.swift in Sources */, 9E7CB6212874143800A02233 /* AddressDetailsView.swift in Sources */, + 9E6713FA289BE0E100A6796F /* ClearBackgroundView.swift in Sources */, F9C165C42740403600592F76 /* TransactionSentView.swift in Sources */, F9971A5927680DDE00A2DB75 /* RequestStore.swift in Sources */, ); @@ -1694,6 +1732,7 @@ 9E7CB6272874269F00A02233 /* ProfileSnapshotTests.swift in Sources */, 9E92AF0828530EBF007367AD /* View+UIImage.swift in Sources */, 6654C7442715A4AC00901167 /* OnboardingStoreTests.swift in Sources */, + 9E94C62328AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift in Sources */, 9E39112E283F91600073DD9A /* ZatoshiTests.swift in Sources */, 9E9ECC9B28589E150099D5A2 /* ImportWalletSnapshotTests.swift in Sources */, 9EDDEAA32829610D00B4100C /* TransactionAmountInputTests.swift in Sources */, @@ -1720,6 +1759,7 @@ 0DB4E0B12881F2DB00947B78 /* WalletBalance+testing.swift in Sources */, 9E02B56C27FED475005B809B /* DatabaseFilesTests.swift in Sources */, 9EF8135D27ECC25E0075AF48 /* UserPreferencesStorageTests.swift in Sources */, + 9E94C62028AA7DEE008256E9 /* BalanceBreakdownTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2041,8 +2081,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/zcash/ZcashLightClientKit/"; requirement = { - branch = master; - kind = branch; + kind = exactVersion; + version = "0.16.6-beta"; }; }; 6654C7382715A38000901167 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { @@ -2085,14 +2125,6 @@ minimumVersion = 0.9.2; }; }; - 9EF8139627F1FAEC0075AF48 /* XCRemoteSwiftPackageReference "ZcashLightClientKit" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/zcash/ZcashLightClientKit"; - requirement = { - branch = master; - kind = branch; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -2131,11 +2163,6 @@ package = 9EAB466B285A0468002904A0 /* XCRemoteSwiftPackageReference "swift-parsing" */; productName = _URLRouting; }; - 9EF8139727F1FAEC0075AF48 /* ZcashLightClientKit */ = { - isa = XCSwiftPackageProductDependency; - package = 9EF8139627F1FAEC0075AF48 /* XCRemoteSwiftPackageReference "ZcashLightClientKit" */; - productName = ZcashLightClientKit; - }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 0D4E79FD26B364170058B01E /* Project object */; diff --git a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 84dfa94..e2be8e8 100644 --- a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -203,8 +203,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/zcash/ZcashLightClientKit", "state" : { - "branch" : "master", - "revision" : "fba4cecbe61cce424ada9fe1f98b05b88d5c8920" + "revision" : "a37c140441d78fa6ad9c89e2fbb539f1ddb7e5a1", + "version" : "0.16.6-beta" } } ], diff --git a/secant/Dependencies/ZCashSDKEnvironment.swift b/secant/Dependencies/ZCashSDKEnvironment.swift index ae417f4..21a45ac 100644 --- a/secant/Dependencies/ZCashSDKEnvironment.swift +++ b/secant/Dependencies/ZCashSDKEnvironment.swift @@ -16,6 +16,7 @@ fileprivate enum ZcashSDKConstants { static let endpointPort = 9067 static let mnemonicWordsMaxCount = 24 static let requiredTransactionConfirmations = 10 + static let streamingCallTimeoutInMillis = Int64(10 * 60 * 60 * 1000) // ten hours } struct ZCashSDKEnvironment { @@ -33,29 +34,49 @@ struct ZCashSDKEnvironment { extension ZCashSDKEnvironment { static let mainnet = ZCashSDKEnvironment( defaultBirthday: BlockHeight(ZcashSDKConstants.defaultBlockHeight), - endpoint: LightWalletEndpoint(address: ZcashSDKConstants.endpointMainnetAddress, port: ZcashSDKConstants.endpointPort), + endpoint: LightWalletEndpoint( + address: ZcashSDKConstants.endpointMainnetAddress, + port: ZcashSDKConstants.endpointPort, + secure: true, + streamingCallTimeoutInMillis: ZcashSDKConstants.streamingCallTimeoutInMillis + ), isMainnet: { true }, lightWalletService: LightWalletGRPCService( - endpoint: LightWalletEndpoint(address: ZcashSDKConstants.endpointMainnetAddress, port: ZcashSDKConstants.endpointPort) + endpoint: LightWalletEndpoint( + address: ZcashSDKConstants.endpointMainnetAddress, + port: ZcashSDKConstants.endpointPort, + secure: true, + streamingCallTimeoutInMillis: ZcashSDKConstants.streamingCallTimeoutInMillis + ) ), memoCharLimit: 512, mnemonicWordsMaxCount: ZcashSDKConstants.mnemonicWordsMaxCount, network: ZcashNetworkBuilder.network(for: .mainnet), requiredTransactionConfirmations: ZcashSDKConstants.requiredTransactionConfirmations, - sdkVersion: "0.14.0-beta" + sdkVersion: "0.16.5-beta" ) static let testnet = ZCashSDKEnvironment( defaultBirthday: BlockHeight(ZcashSDKConstants.defaultBlockHeight), - endpoint: LightWalletEndpoint(address: ZcashSDKConstants.endpointTestnetAddress, port: ZcashSDKConstants.endpointPort), + endpoint: LightWalletEndpoint( + address: ZcashSDKConstants.endpointTestnetAddress, + port: ZcashSDKConstants.endpointPort, + secure: true, + streamingCallTimeoutInMillis: ZcashSDKConstants.streamingCallTimeoutInMillis + ), isMainnet: { false }, lightWalletService: LightWalletGRPCService( - endpoint: LightWalletEndpoint(address: ZcashSDKConstants.endpointTestnetAddress, port: ZcashSDKConstants.endpointPort) + endpoint: LightWalletEndpoint( + address: ZcashSDKConstants.endpointTestnetAddress, + port: ZcashSDKConstants.endpointPort, + secure: true, + streamingCallTimeoutInMillis: ZcashSDKConstants.streamingCallTimeoutInMillis + ) ), memoCharLimit: 512, mnemonicWordsMaxCount: ZcashSDKConstants.mnemonicWordsMaxCount, network: ZcashNetworkBuilder.network(for: .testnet), requiredTransactionConfirmations: ZcashSDKConstants.requiredTransactionConfirmations, - sdkVersion: "0.14.0-beta" + sdkVersion: "0.16.5-beta" ) } diff --git a/secant/Features/BalanceBreakdown/BalanceBreakdownStore.swift b/secant/Features/BalanceBreakdown/BalanceBreakdownStore.swift new file mode 100644 index 0000000..dee4584 --- /dev/null +++ b/secant/Features/BalanceBreakdown/BalanceBreakdownStore.swift @@ -0,0 +1,121 @@ +// +// BalanceBreakdownStore.swift +// secant-testnet +// +// Created by Lukáš Korba on 04.08.2022. +// + +import Foundation +import ComposableArchitecture +import ZcashLightClientKit + +typealias BalanceBreakdownReducer = Reducer +typealias BalanceBreakdownStore = Store +typealias BalanceBreakdownViewStore = ViewStore + +// MARK: - State + +struct BalanceBreakdownState: Equatable { + var autoShieldingTreshold: Zatoshi + var latestBlock: String + var shieldedBalance: WalletBalance + var transparentBalance: WalletBalance + + var totalBalance: Zatoshi { + shieldedBalance.total + transparentBalance.total + } +} + +// MARK: - Action + +enum BalanceBreakdownAction: Equatable { + case onAppear + case onDisappear + case synchronizerStateChanged(WrappedSDKSynchronizerState) + case updateLatestBlock + case updateSynchronizerStatus +} + +// MARK: - Environment + +struct BalanceBreakdownEnvironment { + let numberFormatter: WrappedNumberFormatter + let SDKSynchronizer: WrappedSDKSynchronizer + let scheduler: AnySchedulerOf +} + +extension BalanceBreakdownEnvironment { + static let live = BalanceBreakdownEnvironment( + numberFormatter: .live(), + SDKSynchronizer: LiveWrappedSDKSynchronizer(), + scheduler: DispatchQueue.main.eraseToAnyScheduler() + ) + + static let mock = BalanceBreakdownEnvironment( + numberFormatter: .live(), + SDKSynchronizer: MockWrappedSDKSynchronizer(), + scheduler: DispatchQueue.main.eraseToAnyScheduler() + ) +} + +// MARK: - Reducer + +extension BalanceBreakdownReducer { + private enum CancelId {} + + static let `default` = BalanceBreakdownReducer { state, action, environment in + switch action { + case .onAppear: + return environment.SDKSynchronizer.stateChanged + .map(BalanceBreakdownAction.synchronizerStateChanged) + .eraseToEffect() + .cancellable(id: CancelId.self, cancelInFlight: true) + + case .onDisappear: + return Effect.cancel(id: CancelId.self) + + case .synchronizerStateChanged(.synced): + return Effect(value: .updateSynchronizerStatus) + + case .synchronizerStateChanged(let synchronizerState): + return Effect(value: .updateSynchronizerStatus) + + case .updateSynchronizerStatus: + if let shieldedBalance = environment.SDKSynchronizer.latestScannedSynchronizerState?.shieldedBalance { + state.shieldedBalance = shieldedBalance + } + if let transparentBalance = environment.SDKSynchronizer.latestScannedSynchronizerState?.transparentBalance { + state.transparentBalance = transparentBalance + } + return Effect(value: .updateLatestBlock) + + case .updateLatestBlock: + guard let latestBlockNumber = environment.SDKSynchronizer.latestScannedSynchronizerState?.latestScannedHeight, + let latestBlock = environment.numberFormatter.string(NSDecimalNumber(value: latestBlockNumber)) else { + state.latestBlock = "unknown" + return .none + } + state.latestBlock = "\(latestBlock)" + return .none + } + } +} + +// MARK: - Placeholders + +extension BalanceBreakdownState { + static let placeholder = BalanceBreakdownState( + autoShieldingTreshold: Zatoshi(1_000_000), + latestBlock: "unknown", + shieldedBalance: WalletBalance.zero, + transparentBalance: WalletBalance.zero + ) +} + +extension BalanceBreakdownStore { + static let placeholder = BalanceBreakdownStore( + initialState: .placeholder, + reducer: .default, + environment: .live + ) +} diff --git a/secant/Features/BalanceBreakdown/BalanceBreakdownView.swift b/secant/Features/BalanceBreakdown/BalanceBreakdownView.swift new file mode 100644 index 0000000..24faa2d --- /dev/null +++ b/secant/Features/BalanceBreakdown/BalanceBreakdownView.swift @@ -0,0 +1,68 @@ +// +// BalanceBreakdownView.swift +// secant-testnet +// +// Created by Lukáš Korba on 04.08.2022. +// + +import SwiftUI +import ComposableArchitecture +import ZcashLightClientKit + +struct BalanceBreakdown: View { + let store: BalanceBreakdownStore + + var body: some View { + WithViewStore(store) { viewStore in + VStack { + HStack { + Spacer() + Text("Block: \(viewStore.latestBlock)") + } + + .padding(.horizontal, 50) + VStack(alignment: .leading, spacing: 10) { + balanceView(title: "SHIELDED ZEC (SPENDABLE)", viewStore.shieldedBalance.total, titleColor: Asset.Colors.Text.balanceText.color) + balanceView(title: "TRANSPARENT BALANCE", viewStore.transparentBalance.total) + balanceView(title: "TOTAL BALANCE", viewStore.totalBalance) + } + .padding(30) + .background(Asset.Colors.ScreenBackground.modalDialog.color) + .cornerRadius(8) + .onAppear { viewStore.send(.onAppear) } + + HStack { + Spacer() + Text("Auto Shielding Treshold: \(viewStore.autoShieldingTreshold.decimalString()) ZEC") + } + .padding(.horizontal, 50) + } + .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) + .applySemiTransparentScreenBackground() + .edgesIgnoringSafeArea(.all) + .onTapGesture { + viewStore.send(.onDisappear) + } + } + .background(ClearBackgroundView()) + } +} + +extension BalanceBreakdown { + func balanceView(title: String, _ balance: Zatoshi, titleColor: Color = .white) -> some View { + VStack(alignment: .leading) { + Text("\(title)") + .foregroundColor(titleColor) + Text("$\(balance.decimalString(formatter: NumberFormatter.zcashNumberFormatter8FractionDigits))") + .font(.custom(FontFamily.Zboto.regular.name, size: 40)) + .foregroundColor(Color.white) + } + } +} + +struct BalanceBreakdown_Previews: PreviewProvider { + static var previews: some View { + BalanceBreakdown(store: .placeholder) + .preferredColorScheme(.dark) + } +} diff --git a/secant/Features/Home/HomeStore.swift b/secant/Features/Home/HomeStore.swift index 8bc676d..20f1d3c 100644 --- a/secant/Features/Home/HomeStore.swift +++ b/secant/Features/Home/HomeStore.swift @@ -17,25 +17,26 @@ struct HomeState: Equatable { case request case send case scan + case balanceBreakdown } var route: Route? + var balanceBreakdown: BalanceBreakdownState var drawerOverlay: DrawerOverlay var profileState: ProfileState var requestState: RequestState var requiredTransactionConfirmations = 0 - var sendState: SendFlowState var scanState: ScanState + var sendState: SendFlowState + var shieldedBalance: WalletBalance 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) + Zatoshi.from(decimal: shieldedBalance.total.decimalValue.decimalValue * zecPrice) } var isDownloading: Bool { @@ -56,6 +57,7 @@ struct HomeState: Equatable { // MARK: Action enum HomeAction: Equatable { + case balanceBreakdown(BalanceBreakdownAction) case debugMenuStartup case onAppear case onDisappear @@ -65,7 +67,6 @@ enum HomeAction: Equatable { case scan(ScanAction) case synchronizerStateChanged(WrappedSDKSynchronizerState) case walletEvents(WalletEventsFlowAction) - case updateBalance(WalletBalance) case updateDrawer(DrawerOverlay) case updateRoute(HomeState.Route?) case updateSynchronizerStatus @@ -109,7 +110,8 @@ extension HomeReducer { historyReducer, sendReducer, scanReducer, - profileReducer + profileReducer, + balanceBreakdownReducer ] ) @@ -137,11 +139,6 @@ extension HomeReducer { case .synchronizerStateChanged(let synchronizerState): return Effect(value: .updateSynchronizerStatus) - case .updateBalance(let balance): - state.totalBalance = balance.total - state.verifiedBalance = balance.verified - return .none - case .updateDrawer(let drawerOverlay): state.drawerOverlay = drawerOverlay state.walletEventsState.isScrollable = drawerOverlay == .full ? true : false @@ -152,11 +149,10 @@ extension HomeReducer { case .updateSynchronizerStatus: state.synchronizerStatusSnapshot = environment.SDKSynchronizer.statusSnapshot() - return environment.SDKSynchronizer.getShieldedBalance() - .receive(on: environment.scheduler) - .map({ WalletBalance(verified: $0.verified, total: $0.total) }) - .map(HomeAction.updateBalance) - .eraseToEffect() + if let shieldedBalance = environment.SDKSynchronizer.latestScannedSynchronizerState?.shieldedBalance { + state.shieldedBalance = shieldedBalance + } + return .none case .updateRoute(let route): state.route = route @@ -212,6 +208,13 @@ extension HomeReducer { case .scan(let action): return .none + case .balanceBreakdown(.onDisappear): + state.route = nil + return .none + + case .balanceBreakdown: + return .none + case .debugMenuStartup: return .none } @@ -272,6 +275,18 @@ extension HomeReducer { ) } ) + + private static let balanceBreakdownReducer: HomeReducer = BalanceBreakdownReducer.default.pullback( + state: \HomeState.balanceBreakdown, + action: /HomeAction.balanceBreakdown, + environment: { environment in + BalanceBreakdownEnvironment( + numberFormatter: .live(), + SDKSynchronizer: environment.SDKSynchronizer, + scheduler: environment.scheduler + ) + } + ) } // MARK: - Store @@ -311,6 +326,13 @@ extension HomeStore { action: HomeAction.scan ) } + + func balanceBreakdownStore() -> BalanceBreakdownStore { + self.scope( + state: \.balanceBreakdown, + action: HomeAction.balanceBreakdown + ) + } } // MARK: - ViewStore @@ -338,15 +360,15 @@ extension HomeViewStore { extension HomeState { static var placeholder: Self { .init( + balanceBreakdown: .placeholder, drawerOverlay: .partial, profileState: .placeholder, requestState: .placeholder, - sendState: .placeholder, scanState: .placeholder, + sendState: .placeholder, + shieldedBalance: WalletBalance.zero, synchronizerStatusSnapshot: .default, - totalBalance: Zatoshi.zero, - walletEventsState: .emptyPlaceHolder, - verifiedBalance: Zatoshi.zero + walletEventsState: .emptyPlaceHolder ) } } diff --git a/secant/Features/Home/HomeView.swift b/secant/Features/Home/HomeView.swift index eee7659..211efd0 100644 --- a/secant/Features/Home/HomeView.swift +++ b/secant/Features/Home/HomeView.swift @@ -27,6 +27,9 @@ struct HomeView: View { .navigationBarHidden(true) .onAppear(perform: { viewStore.send(.onAppear) }) .onDisappear(perform: { viewStore.send(.onDisappear) }) + .fullScreenCover(isPresented: viewStore.bindingForRoute(.balanceBreakdown)) { + BalanceBreakdown(store: store.balanceBreakdownStore()) + } } } } @@ -120,20 +123,24 @@ extension HomeView { .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) - + Button { + viewStore.send(.updateRoute(.balanceBreakdown)) + } label: { + Text("$\(viewStore.shieldedBalance.total.decimalString())") + .font(.custom(FontFamily.Zboto.regular.name, size: 40)) + .foregroundColor(Asset.Colors.Text.balanceText.color) + .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)") + .accessDebugMenuWithHiddenGesture { + viewStore.send(.debugMenuStartup) + } } } diff --git a/secant/Features/SendFlow/SendFlowStore.swift b/secant/Features/SendFlow/SendFlowStore.swift index ab89ec6..dae3b8e 100644 --- a/secant/Features/SendFlow/SendFlowStore.swift +++ b/secant/Features/SendFlow/SendFlowStore.swift @@ -26,7 +26,7 @@ struct SendFlowState: Equatable { var isSendingTransaction = false var memoState: MultiLineTextFieldState var route: Route? - var totalBalance = Zatoshi.zero + var shieldedBalance = WalletBalance.zero var transactionAddressInputState: TransactionAddressTextFieldState var transactionAmountInputState: TransactionAmountTextFieldState @@ -67,7 +67,7 @@ struct SendFlowState: Equatable { } var totalCurrencyBalance: Zatoshi { - Zatoshi.from(decimal: totalBalance.decimalValue.decimalValue * transactionAmountInputState.zecPrice) + Zatoshi.from(decimal: shieldedBalance.total.decimalValue.decimalValue * transactionAmountInputState.zecPrice) } } @@ -82,7 +82,6 @@ enum SendFlowAction: Equatable { case synchronizerStateChanged(WrappedSDKSynchronizerState) case transactionAddressInput(TransactionAddressTextFieldAction) case transactionAmountInput(TransactionAmountTextFieldAction) - case updateBalance(Zatoshi) case updateRoute(SendFlowState.Route?) } @@ -183,18 +182,13 @@ extension SendFlowReducer { return Effect.cancel(id: SyncStatusUpdatesID()) case .synchronizerStateChanged(.synced): - return environment.SDKSynchronizer.getShieldedBalance() - .receive(on: environment.scheduler) - .map({ $0.total }) - .map(SendFlowAction.updateBalance) - .eraseToEffect() - - case .synchronizerStateChanged(let synchronizerState): + if let shieldedBalance = environment.SDKSynchronizer.latestScannedSynchronizerState?.shieldedBalance { + state.shieldedBalance = shieldedBalance + state.transactionAmountInputState.maxValue = shieldedBalance.total.amount + } return .none - case .updateBalance(let balance): - state.totalBalance = balance - state.transactionAmountInputState.maxValue = balance.amount + case .synchronizerStateChanged(let synchronizerState): return .none case .memo: diff --git a/secant/Features/SendFlow/Views/CreateTransactionView.swift b/secant/Features/SendFlow/Views/CreateTransactionView.swift index c20abcf..ba3f406 100644 --- a/secant/Features/SendFlow/Views/CreateTransactionView.swift +++ b/secant/Features/SendFlow/Views/CreateTransactionView.swift @@ -10,7 +10,7 @@ struct CreateTransaction: View { return WithViewStore(store) { viewStore in VStack { VStack(spacing: 0) { - Text("WalletBalance \(viewStore.totalBalance.decimalString()) ZEC") + Text("WalletBalance \(viewStore.shieldedBalance.total.decimalString()) ZEC") Text("($\(viewStore.totalCurrencyBalance.decimalString()))") .font(.system(size: 13)) .opacity(0.6) diff --git a/secant/Resources/Colors.xcassets/ScreenBackground/gradientEnd.colorset/Contents.json b/secant/Resources/Colors.xcassets/ScreenBackground/gradientEnd.colorset/Contents.json index a043fd9..1886764 100644 --- a/secant/Resources/Colors.xcassets/ScreenBackground/gradientEnd.colorset/Contents.json +++ b/secant/Resources/Colors.xcassets/ScreenBackground/gradientEnd.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "243", - "green" : "228", - "red" : "210" + "blue" : "0xF3", + "green" : "0xE4", + "red" : "0xD2" } }, "idiom" : "universal" diff --git a/secant/Resources/Colors.xcassets/ScreenBackground/gradientStart.colorset/Contents.json b/secant/Resources/Colors.xcassets/ScreenBackground/gradientStart.colorset/Contents.json index f526c8b..ec11488 100644 --- a/secant/Resources/Colors.xcassets/ScreenBackground/gradientStart.colorset/Contents.json +++ b/secant/Resources/Colors.xcassets/ScreenBackground/gradientStart.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "249", - "green" : "239", - "red" : "227" + "blue" : "0xF9", + "green" : "0xEF", + "red" : "0xE3" } }, "idiom" : "universal" diff --git a/secant/Resources/Colors.xcassets/ScreenBackground/modalDialog.colorset/Contents.json b/secant/Resources/Colors.xcassets/ScreenBackground/modalDialog.colorset/Contents.json new file mode 100644 index 0000000..0e9d20e --- /dev/null +++ b/secant/Resources/Colors.xcassets/ScreenBackground/modalDialog.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.953", + "green" : "0.894", + "red" : "0.824" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.323", + "green" : "0.099", + "red" : "0.083" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/secant/Resources/Colors.xcassets/ScreenBackground/semiTransparentGradientEnd.colorset/Contents.json b/secant/Resources/Colors.xcassets/ScreenBackground/semiTransparentGradientEnd.colorset/Contents.json new file mode 100644 index 0000000..bb8a74a --- /dev/null +++ b/secant/Resources/Colors.xcassets/ScreenBackground/semiTransparentGradientEnd.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.900", + "blue" : "0xF3", + "green" : "0xE4", + "red" : "0xD2" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.900", + "blue" : "0x55", + "green" : "0x31", + "red" : "0x24" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/secant/Resources/Colors.xcassets/ScreenBackground/semiTransparentGradientStart.colorset/Contents.json b/secant/Resources/Colors.xcassets/ScreenBackground/semiTransparentGradientStart.colorset/Contents.json new file mode 100644 index 0000000..b8b7218 --- /dev/null +++ b/secant/Resources/Colors.xcassets/ScreenBackground/semiTransparentGradientStart.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.900", + "blue" : "0xF9", + "green" : "0xEF", + "red" : "0xE3" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.900", + "blue" : "0x5A", + "green" : "0x36", + "red" : "0x29" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/secant/Resources/Generated/XCAssets+Generated.swift b/secant/Resources/Generated/XCAssets+Generated.swift index 05c34c4..9ab0816 100644 --- a/secant/Resources/Generated/XCAssets+Generated.swift +++ b/secant/Resources/Generated/XCAssets+Generated.swift @@ -95,8 +95,11 @@ internal enum Asset { internal static let gradientStart = ColorAsset(name: "gradientStart") internal static let greenGradientEnd = ColorAsset(name: "greenGradientEnd") internal static let greenGradientStart = ColorAsset(name: "greenGradientStart") + internal static let modalDialog = ColorAsset(name: "modalDialog") internal static let redGradientEnd = ColorAsset(name: "redGradientEnd") internal static let redGradientStart = ColorAsset(name: "redGradientStart") + internal static let semiTransparentGradientEnd = ColorAsset(name: "semiTransparentGradientEnd") + internal static let semiTransparentGradientStart = ColorAsset(name: "semiTransparentGradientStart") } internal enum Shadow { internal static let drawerShadow = ColorAsset(name: "drawerShadow") diff --git a/secant/UI Components/Backgrounds/ScreenBackground.swift b/secant/UI Components/Backgrounds/ScreenBackground.swift index 96d065f..25b1abc 100644 --- a/secant/UI Components/Backgrounds/ScreenBackground.swift +++ b/secant/UI Components/Backgrounds/ScreenBackground.swift @@ -86,6 +86,17 @@ extension View { ) ) } + + func applySemiTransparentScreenBackground() -> some View { + self.modifier( + ScreenBackgroundModifier( + colors: [ + Asset.Colors.ScreenBackground.semiTransparentGradientStart.color, + Asset.Colors.ScreenBackground.semiTransparentGradientEnd.color + ] + ) + ) + } } struct ScreenBackground_Previews: PreviewProvider { diff --git a/secant/Utils/ClearBackgroundView.swift b/secant/Utils/ClearBackgroundView.swift new file mode 100644 index 0000000..d5b68c3 --- /dev/null +++ b/secant/Utils/ClearBackgroundView.swift @@ -0,0 +1,25 @@ +// +// ClearBackgroundView.swift +// secant-testnet +// +// Created by Lukáš Korba on 04.08.2022. +// + +import SwiftUI + +/// Purpose of this utility is to solve background transparency of the view used for example in `.fullScreenCover`. +/// Usually used for the modal full screen views with semi-transparent backgrounds. +struct ClearBackgroundView: UIViewRepresentable { + func makeUIView(context: Context) -> UIView { + let view = UIView() + /// Wrapped in the dispatch queue to achieve the background clearance, + /// it doesn't work otherwise (opaque background instead of fully transparent). + /// Comes from https://stackoverflow.com/a/66925883 + DispatchQueue.main.async { + view.superview?.superview?.backgroundColor = .clear + } + return view + } + + func updateUIView(_ uiView: UIView, context: Context) {} +} diff --git a/secant/Wrappers/WrappedNumberFormatter.swift b/secant/Wrappers/WrappedNumberFormatter.swift index da8259a..1d4ec6c 100644 --- a/secant/Wrappers/WrappedNumberFormatter.swift +++ b/secant/Wrappers/WrappedNumberFormatter.swift @@ -16,6 +16,15 @@ extension NumberFormatter { formatter.usesGroupingSeparator = true return formatter }() + + static let zcashNumberFormatter8FractionDigits: NumberFormatter = { + var formatter = NumberFormatter() + formatter.minimumFractionDigits = 8 + formatter.maximumIntegerDigits = 8 + formatter.numberStyle = .decimal + formatter.usesGroupingSeparator = true + return formatter + }() } struct WrappedNumberFormatter { diff --git a/secant/Wrappers/WrappedSDKSynchronizer.swift b/secant/Wrappers/WrappedSDKSynchronizer.swift index a42f118..8898e80 100644 --- a/secant/Wrappers/WrappedSDKSynchronizer.swift +++ b/secant/Wrappers/WrappedSDKSynchronizer.swift @@ -37,15 +37,18 @@ protocol WrappedSDKSynchronizer { var synchronizer: SDKSynchronizer? { get } var stateChanged: CurrentValueSubject { get } var walletBirthday: BlockHeight? { get } + var latestScannedSynchronizerState: SDKSynchronizer.SynchronizerState? { get } func prepareWith(initializer: Initializer) throws func start(retry: Bool) throws func stop() + func synchronizerSynced(_ synchronizerState: SDKSynchronizer.SynchronizerState?) func statusSnapshot() -> SyncStatusSnapshot func rewind(_ policy: RewindPolicy) throws - func getShieldedBalance() -> Effect + func getShieldedBalance() -> WalletBalance? + func getTransparentBalance() -> WalletBalance? func getAllClearedTransactions() -> Effect<[WalletEvent], Never> func getAllPendingTransactions() -> Effect<[WalletEvent], Never> func getAllTransactions() -> Effect<[WalletEvent], Never> @@ -83,6 +86,7 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer { private(set) var stateChanged: CurrentValueSubject private(set) var notificationCenter: WrappedNotificationCenter private(set) var walletBirthday: BlockHeight? + private(set) var latestScannedSynchronizerState: SDKSynchronizer.SynchronizerState? init(notificationCenter: WrappedNotificationCenter = .live) { self.notificationCenter = notificationCenter @@ -103,7 +107,10 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer { notificationCenter.publisherFor(.synchronizerSynced)? .receive(on: DispatchQueue.main) - .sink { [weak self] _ in self?.synchronizerSynced() } + .sink { [weak self] output in + let synchronizerState = output.userInfo?[SDKSynchronizer.NotificationKeys.synchronizerState] as? SDKSynchronizer.SynchronizerState + self?.synchronizerSynced(synchronizerState) + } .store(in: &cancellables) notificationCenter.publisherFor(.synchronizerProgressUpdated)? @@ -133,8 +140,9 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer { stateChanged.send(.started) } - func synchronizerSynced() { + func synchronizerSynced(_ synchronizerState: SDKSynchronizer.SynchronizerState?) { stateChanged.send(.synced) + latestScannedSynchronizerState = synchronizerState } func synchronizerProgressUpdated() { @@ -176,15 +184,14 @@ class LiveWrappedSDKSynchronizer: WrappedSDKSynchronizer { } } - func getShieldedBalance() -> Effect { - if let shieldedVerifiedBalance: Zatoshi = synchronizer?.getShieldedVerifiedBalance(), - let shieldedTotalBalance: Zatoshi = synchronizer?.getShieldedBalance(accountIndex: 0) { - return Effect(value: WalletBalance(verified: shieldedVerifiedBalance, total: shieldedTotalBalance)) - } - - return .none + func getShieldedBalance() -> WalletBalance? { + latestScannedSynchronizerState?.shieldedBalance } - + + func getTransparentBalance() -> WalletBalance? { + latestScannedSynchronizerState?.transparentBalance + } + func getAllClearedTransactions() -> Effect<[WalletEvent], Never> { if let clearedTransactions = try? synchronizer?.allClearedTransactions() { return Effect(value: clearedTransactions.map { @@ -282,6 +289,7 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { private(set) var synchronizer: SDKSynchronizer? private(set) var stateChanged: CurrentValueSubject private(set) var walletBirthday: BlockHeight? + private(set) var latestScannedSynchronizerState: SDKSynchronizer.SynchronizerState? init(notificationCenter: WrappedNotificationCenter = .mock) { self.notificationCenter = notificationCenter @@ -304,7 +312,7 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { synchronizer?.stop() } - func synchronizerSynced() { + func synchronizerSynced(_ synchronizerState: SDKSynchronizer.SynchronizerState?) { stateChanged.send(.synced) } @@ -318,10 +326,14 @@ class MockWrappedSDKSynchronizer: WrappedSDKSynchronizer { func rewind(_ policy: RewindPolicy) throws { } - func getShieldedBalance() -> Effect { - return Effect(value: WalletBalance(verified: Zatoshi(12345000), total: Zatoshi(12345000))) + func getShieldedBalance() -> WalletBalance? { + WalletBalance(verified: Zatoshi(12345000), total: Zatoshi(12345000)) } - + + func getTransparentBalance() -> WalletBalance? { + WalletBalance(verified: Zatoshi(12345000), total: Zatoshi(12345000)) + } + func getAllClearedTransactions() -> Effect<[WalletEvent], Never> { let mocked: [TransactionStateMockHelper] = [ TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "1"), @@ -414,6 +426,7 @@ class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer { private(set) var synchronizer: SDKSynchronizer? private(set) var stateChanged: CurrentValueSubject private(set) var walletBirthday: BlockHeight? + private(set) var latestScannedSynchronizerState: SDKSynchronizer.SynchronizerState? init(notificationCenter: WrappedNotificationCenter = .mock) { self.notificationCenter = notificationCenter @@ -426,16 +439,16 @@ class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer { func stop() { } - func synchronizerSynced() { } + func synchronizerSynced(_ synchronizerState: SDKSynchronizer.SynchronizerState?) { } func statusSnapshot() -> SyncStatusSnapshot { .default } func rewind(_ policy: RewindPolicy) throws { } - func getShieldedBalance() -> Effect { - return .none - } + func getShieldedBalance() -> WalletBalance? { nil } + func getTransparentBalance() -> WalletBalance? { nil } + func getAllClearedTransactions() -> Effect<[WalletEvent], Never> { let mocked: [TransactionStateMockHelper] = [ TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"), diff --git a/secantTests/BalanceBreakdownTests/BalanceBreakdownTests.swift b/secantTests/BalanceBreakdownTests/BalanceBreakdownTests.swift new file mode 100644 index 0000000..1da2af4 --- /dev/null +++ b/secantTests/BalanceBreakdownTests/BalanceBreakdownTests.swift @@ -0,0 +1,42 @@ +// +// BalanceBreakdownTests.swift +// secantTests +// +// Created by Lukáš Korba on 15.08.2022. +// + +import XCTest +@testable import secant_testnet +import ComposableArchitecture +import ZcashLightClientKit + +class BalanceBreakdownTests: XCTestCase { + func testOnAppear() throws { + // setup the store and environment to be fully mocked + let testScheduler = DispatchQueue.test + + let store = TestStore( + initialState: .placeholder, + reducer: BalanceBreakdownReducer.default, + environment: + BalanceBreakdownEnvironment( + numberFormatter: .live(), + SDKSynchronizer: MockWrappedSDKSynchronizer(), + scheduler: testScheduler.eraseToAnyScheduler() + ) + ) + + store.send(.onAppear) + + testScheduler.advance(by: 0.1) + + // expected side effects as a result of .onAppear registration + store.receive(.synchronizerStateChanged(.unknown)) + store.receive(.updateSynchronizerStatus) + store.receive(.updateLatestBlock) + + // 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/HomeTests/HomeTests.swift b/secantTests/HomeTests/HomeTests.swift index d62a9d5..4dff05f 100644 --- a/secantTests/HomeTests/HomeTests.swift +++ b/secantTests/HomeTests/HomeTests.swift @@ -10,7 +10,6 @@ import XCTest import ComposableArchitecture import ZcashLightClientKit -// swiftlint:disable type_body_length class HomeTests: XCTestCase { func testSynchronizerStateChanged_AnyButSynced() throws { // setup the store and environment to be fully mocked @@ -38,18 +37,11 @@ class HomeTests: XCTestCase { testScheduler.advance(by: 0.01) store.receive(.updateSynchronizerStatus) - - let balance = WalletBalance(verified: Zatoshi(12_345_000), total: Zatoshi(12_345_000)) - store.receive(.updateBalance(balance)) { state in - state.totalBalance = Zatoshi(12_345_000) - state.verifiedBalance = Zatoshi(12_345_000) - } } /// When the synchronizer status change to .synced, several things happen /// 1. the .updateSynchronizerStatus is called /// 2. the side effect to update the transactions history is called - /// 3. the side effect to update the balance is called func testSynchronizerStateChanged_Synced() throws { // setup the store and environment to be fully mocked let testScheduler = DispatchQueue.test @@ -99,14 +91,6 @@ class HomeTests: XCTestCase { } store.receive(.updateWalletEvents(walletEvents)) - - // ad 3. - let balance = WalletBalance(verified: Zatoshi(12_345_000), total: Zatoshi(12_345_000)) - - store.receive(.updateBalance(balance)) { state in - state.verifiedBalance = Zatoshi(12_345_000) - state.totalBalance = Zatoshi(12_345_000) - } } func testWalletEventsPartial_to_FullDrawer() throws { @@ -125,15 +109,15 @@ class HomeTests: XCTestCase { ) let homeState = HomeState( + balanceBreakdown: .placeholder, drawerOverlay: .partial, profileState: .placeholder, requestState: .placeholder, - sendState: .placeholder, scanState: .placeholder, + sendState: .placeholder, + shieldedBalance: WalletBalance.zero, synchronizerStatusSnapshot: .default, - totalBalance: Zatoshi.zero, - walletEventsState: .emptyPlaceHolder, - verifiedBalance: Zatoshi.zero + walletEventsState: .emptyPlaceHolder ) let store = TestStore( @@ -168,15 +152,15 @@ class HomeTests: XCTestCase { ) let homeState = HomeState( + balanceBreakdown: .placeholder, drawerOverlay: .full, profileState: .placeholder, requestState: .placeholder, - sendState: .placeholder, scanState: .placeholder, + sendState: .placeholder, + shieldedBalance: WalletBalance.zero, synchronizerStatusSnapshot: .default, - totalBalance: Zatoshi.zero, - walletEventsState: .emptyPlaceHolder, - verifiedBalance: Zatoshi.zero + walletEventsState: .emptyPlaceHolder ) let store = TestStore( @@ -227,12 +211,6 @@ class HomeTests: XCTestCase { // expected side effects as a result of .onAppear registration store.receive(.synchronizerStateChanged(.unknown)) store.receive(.updateSynchronizerStatus) - - let balance = WalletBalance(verified: Zatoshi(12_345_000), total: Zatoshi(12_345_000)) - store.receive(.updateBalance(balance)) { state in - state.totalBalance = Zatoshi(12_345_000) - state.verifiedBalance = Zatoshi(12_345_000) - } // long-living (cancelable) effects need to be properly canceled. // the .onDisappear action cancles the observer of the synchronizer status change. @@ -256,15 +234,15 @@ class HomeTests: XCTestCase { let homeState = HomeState( route: .profile, + balanceBreakdown: .placeholder, drawerOverlay: .full, profileState: .placeholder, requestState: .placeholder, - sendState: .placeholder, scanState: .placeholder, + sendState: .placeholder, + shieldedBalance: WalletBalance.zero, synchronizerStatusSnapshot: .default, - totalBalance: Zatoshi.zero, - walletEventsState: .emptyPlaceHolder, - verifiedBalance: Zatoshi.zero + walletEventsState: .emptyPlaceHolder ) let store = TestStore( @@ -295,15 +273,15 @@ class HomeTests: XCTestCase { let homeState = HomeState( route: .profile, + balanceBreakdown: .placeholder, drawerOverlay: .full, profileState: .placeholder, requestState: .placeholder, - sendState: .placeholder, scanState: .placeholder, + sendState: .placeholder, + shieldedBalance: WalletBalance.zero, synchronizerStatusSnapshot: .default, - totalBalance: Zatoshi.zero, - walletEventsState: .emptyPlaceHolder, - verifiedBalance: Zatoshi.zero + walletEventsState: .emptyPlaceHolder ) let store = TestStore( diff --git a/secantTests/ProfileTests/ProfileTests.swift b/secantTests/ProfileTests/ProfileTests.swift index a4ecdb3..2a3d31a 100644 --- a/secantTests/ProfileTests/ProfileTests.swift +++ b/secantTests/ProfileTests/ProfileTests.swift @@ -32,7 +32,7 @@ class ProfileTests: XCTestCase { state.address = "ff3927e1f83df9b1b0dc75540ddc59ee435eecebae914d2e6dfe8576fbedc9a8" state.appVersion = "0.0.1" state.appBuild = "31" - state.sdkVersion = "0.14.0-beta" + state.sdkVersion = "0.16.5-beta" } } } diff --git a/secantTests/SendTests/SendTests.swift b/secantTests/SendTests/SendTests.swift index 68bd871..246af87 100644 --- a/secantTests/SendTests/SendTests.swift +++ b/secantTests/SendTests/SendTests.swift @@ -536,7 +536,7 @@ class SendTests: XCTestCase { func testInvalidForm_ExceededMemoCharLimit() throws { let sendState = SendFlowState( memoState: MultiLineTextFieldState(charLimit: 3), - totalBalance: Zatoshi(1), + shieldedBalance: WalletBalance(verified: Zatoshi(1), total: Zatoshi(1)), transactionAddressInputState: TransactionAddressTextFieldState( isValidAddress: true, diff --git a/secantTests/SnapshotTests/BalanceBreakdownSnapshotTests/BalanceBreakdownSnapshotTests.swift b/secantTests/SnapshotTests/BalanceBreakdownSnapshotTests/BalanceBreakdownSnapshotTests.swift new file mode 100644 index 0000000..80266fe --- /dev/null +++ b/secantTests/SnapshotTests/BalanceBreakdownSnapshotTests/BalanceBreakdownSnapshotTests.swift @@ -0,0 +1,29 @@ +// +// BalanceBreakdownSnapshotTests.swift +// secantTests +// +// Created by Lukáš Korba on 15.08.2022. +// + +import XCTest +@testable import secant_testnet +import ComposableArchitecture +import ZcashLightClientKit +import SwiftUI + +class BalanceBreakdownSnapshotTests: XCTestCase { + func testBalanceBreakdownSnapshot() throws { + let store = Store( + initialState: BalanceBreakdownState( + autoShieldingTreshold: Zatoshi(1_000_000), + latestBlock: "unknown", + shieldedBalance: WalletBalance(verified: Zatoshi(123_000_000_000), total: Zatoshi(123_000_000_000)), + transparentBalance: WalletBalance(verified: Zatoshi(850_000_000), total: Zatoshi(850_000_000)) + ), + reducer: BalanceBreakdownReducer.default, + environment: .mock + ) + + addAttachments(BalanceBreakdown(store: store)) + } +} diff --git a/secantTests/SnapshotTests/HomeSnapshotTests/HomeCircularProgressSnapshotTests.swift b/secantTests/SnapshotTests/HomeSnapshotTests/HomeCircularProgressSnapshotTests.swift index ef14e2b..da95060 100644 --- a/secantTests/SnapshotTests/HomeSnapshotTests/HomeCircularProgressSnapshotTests.swift +++ b/secantTests/SnapshotTests/HomeSnapshotTests/HomeCircularProgressSnapshotTests.swift @@ -42,15 +42,15 @@ class HomeCircularProgressSnapshotTests: XCTestCase { let store = HomeStore( initialState: .init( + balanceBreakdown: .placeholder, drawerOverlay: .partial, profileState: .placeholder, requestState: .placeholder, - sendState: .placeholder, scanState: .placeholder, + sendState: .placeholder, + shieldedBalance: balance, synchronizerStatusSnapshot: .default, - totalBalance: balance.total, - walletEventsState: .emptyPlaceHolder, - verifiedBalance: balance.verified + walletEventsState: .emptyPlaceHolder ), reducer: .default, environment: testEnvironment @@ -90,15 +90,15 @@ class HomeCircularProgressSnapshotTests: XCTestCase { let store = HomeStore( initialState: .init( + balanceBreakdown: .placeholder, drawerOverlay: .partial, profileState: .placeholder, requestState: .placeholder, - sendState: .placeholder, scanState: .placeholder, + sendState: .placeholder, + shieldedBalance: balance, synchronizerStatusSnapshot: .default, - totalBalance: balance.total, - walletEventsState: .emptyPlaceHolder, - verifiedBalance: balance.verified + walletEventsState: .emptyPlaceHolder ), reducer: .default, environment: testEnvironment @@ -131,15 +131,15 @@ class HomeCircularProgressSnapshotTests: XCTestCase { let store = HomeStore( initialState: .init( + balanceBreakdown: .placeholder, drawerOverlay: .partial, profileState: .placeholder, requestState: .placeholder, - sendState: .placeholder, scanState: .placeholder, + sendState: .placeholder, + shieldedBalance: balance, synchronizerStatusSnapshot: .default, - totalBalance: balance.total, - walletEventsState: .emptyPlaceHolder, - verifiedBalance: balance.verified + walletEventsState: .emptyPlaceHolder ), reducer: .default, environment: testEnvironment diff --git a/secantTests/SnapshotTests/HomeSnapshotTests/HomeSnapshotTests.swift b/secantTests/SnapshotTests/HomeSnapshotTests/HomeSnapshotTests.swift index 2693466..f62f444 100644 --- a/secantTests/SnapshotTests/HomeSnapshotTests/HomeSnapshotTests.swift +++ b/secantTests/SnapshotTests/HomeSnapshotTests/HomeSnapshotTests.swift @@ -37,15 +37,15 @@ class HomeSnapshotTests: XCTestCase { let store = HomeStore( initialState: .init( + balanceBreakdown: .placeholder, drawerOverlay: .partial, profileState: .placeholder, requestState: .placeholder, - sendState: .placeholder, scanState: .placeholder, + sendState: .placeholder, + shieldedBalance: balance, synchronizerStatusSnapshot: .default, - totalBalance: balance.total, - walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: walletEvents)), - verifiedBalance: balance.verified + walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: walletEvents)) ), reducer: .default, environment: .demo diff --git a/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift b/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift index f921fc5..d5b1166 100644 --- a/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift +++ b/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift @@ -38,15 +38,15 @@ class WalletEventsSnapshotTests: XCTestCase { let store = HomeStore( initialState: .init( + balanceBreakdown: .placeholder, drawerOverlay: .partial, profileState: .placeholder, requestState: .placeholder, - sendState: .placeholder, scanState: .placeholder, + sendState: .placeholder, + shieldedBalance: balance, synchronizerStatusSnapshot: .default, - totalBalance: balance.total, - walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: walletEvents)), - verifiedBalance: balance.verified + walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: walletEvents)) ), reducer: .default, environment: .demo @@ -85,15 +85,15 @@ class WalletEventsSnapshotTests: XCTestCase { let balance = WalletBalance(verified: 12_345_000, total: 12_345_000) let store = HomeStore( initialState: .init( + balanceBreakdown: .placeholder, drawerOverlay: .partial, profileState: .placeholder, requestState: .placeholder, - sendState: .placeholder, scanState: .placeholder, + sendState: .placeholder, + shieldedBalance: balance, synchronizerStatusSnapshot: .default, - totalBalance: balance.total, - walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])), - verifiedBalance: balance.verified + walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])) ), reducer: .default, environment: .demo @@ -142,15 +142,15 @@ class WalletEventsSnapshotTests: XCTestCase { let balance = WalletBalance(verified: 12_345_000, total: 12_345_000) let store = HomeStore( initialState: .init( + balanceBreakdown: .placeholder, drawerOverlay: .partial, profileState: .placeholder, requestState: .placeholder, - sendState: .placeholder, scanState: .placeholder, + sendState: .placeholder, + shieldedBalance: balance, synchronizerStatusSnapshot: .default, - totalBalance: balance.total, - walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])), - verifiedBalance: balance.verified + walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])) ), reducer: .default, environment: .demo @@ -199,15 +199,15 @@ class WalletEventsSnapshotTests: XCTestCase { let balance = WalletBalance(verified: 12_345_000, total: 12_345_000) let store = HomeStore( initialState: .init( + balanceBreakdown: .placeholder, drawerOverlay: .partial, profileState: .placeholder, requestState: .placeholder, - sendState: .placeholder, scanState: .placeholder, + sendState: .placeholder, + shieldedBalance: balance, synchronizerStatusSnapshot: .default, - totalBalance: balance.total, - walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])), - verifiedBalance: balance.verified + walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])) ), reducer: .default, environment: .demo @@ -262,15 +262,15 @@ class WalletEventsSnapshotTests: XCTestCase { let balance = WalletBalance(verified: 12_345_000, total: 12_345_000) let store = HomeStore( initialState: .init( + balanceBreakdown: .placeholder, drawerOverlay: .partial, profileState: .placeholder, requestState: .placeholder, - sendState: .placeholder, scanState: .placeholder, + sendState: .placeholder, + shieldedBalance: balance, synchronizerStatusSnapshot: .default, - totalBalance: balance.total, - walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])), - verifiedBalance: balance.verified + walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])) ), reducer: .default, environment: .demo