[153] [Scaffold] Progress Status Circular Bar (#389)

- initial frame connected to available data (progress)
- zboto balance
- $ balance
- inner / outer progress logic
- isDownloading and isUpToDate controls added
- outer circle parametrical solution implemented
- unit tests fixed
- SyncStatusSnapshot implemented
- snapshot tests
This commit is contained in:
Lukas Korba 2022-07-08 16:49:31 +02:00 committed by GitHub
parent 180a5a9bc2
commit 282fdbcdf0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 513 additions and 92 deletions

View File

@ -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 = "<group>"; };
9E5BF64D2823E94900BA3F17 /* TransactionAddressTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressTextField.swift; sourceTree = "<group>"; };
9E5BF64E2823E94900BA3F17 /* TransactionAddressTextFieldStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionAddressTextFieldStore.swift; sourceTree = "<group>"; };
9E661229287717A900C75B70 /* HomeCircularProgressSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCircularProgressSnapshotTests.swift; sourceTree = "<group>"; };
9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncStatusSnapshot.swift; sourceTree = "<group>"; };
9E69A24C27FB002800A55317 /* WelcomeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStore.swift; sourceTree = "<group>"; };
9E7CB6112869882D00A02233 /* WalletEventsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletEventsSnapshotTests.swift; sourceTree = "<group>"; };
9E7CB6142869E8C300A02233 /* CircularProgress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularProgress.swift; sourceTree = "<group>"; };
9E7CB619287310EC00A02233 /* QRCodeGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeGenerator.swift; sourceTree = "<group>"; };
9E7CB61E2874143800A02233 /* AddressDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDetailsView.swift; sourceTree = "<group>"; };
9E7CB61F2874143800A02233 /* AddressDetailsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddressDetailsStore.swift; sourceTree = "<group>"; };
9E7CB6232874246800A02233 /* ProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTests.swift; sourceTree = "<group>"; };
9E7CB6262874269F00A02233 /* ProfileSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileSnapshotTests.swift; sourceTree = "<group>"; };
9E7CB6282875AC2D00A02233 /* AppVersionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersionHandler.swift; sourceTree = "<group>"; };
9E7FE0CE282D257400C374E8 /* SDKSynchronizer+SyncStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SDKSynchronizer+SyncStatus.swift"; sourceTree = "<group>"; };
9E7FE0D2282D274E00C374E8 /* Date+Readable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Readable.swift"; sourceTree = "<group>"; };
9E7FE0D4282D281800C374E8 /* Array+Chunked.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Chunked.swift"; sourceTree = "<group>"; };
9E7FE0D6282D286500C374E8 /* RecoveryPhrase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhrase.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -884,6 +891,14 @@
path = WalletEventsSnapshotTests;
sourceTree = "<group>";
};
9E7CB6132869E8A700A02233 /* CircularProgress */ = {
isa = PBXGroup;
children = (
9E7CB6142869E8C300A02233 /* CircularProgress.swift */,
);
path = CircularProgress;
sourceTree = "<group>";
};
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 = "<group>";
@ -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" */;

View File

@ -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",

View File

@ -6,7 +6,7 @@
//
import Foundation
import _URLRouting
import URLRouting
import ComposableArchitecture
import ZcashLightClientKit

View File

@ -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

View File

@ -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

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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")

View File

@ -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..<maxSegments), id: \.self) {
Circle()
.trim(
from: fromValue($0, segmentTrimComputed, 1.0),
to: toValue($0, segmentTrimComputed, 1.0, gap)
)
.stroke(
Asset.Colors.ProgressIndicator.negativeSpace.color,
lineWidth: 5.0
)
.scaleEffect(1.15)
.opacity(0.2)
.rotationEffect(Angle(degrees: -90))
Circle()
.trim(
from: fromValue($0, segmentTrimComputed, outerCircleProgressCG),
to: toValue($0, segmentTrimComputed, outerCircleProgressCG, gap)
)
.stroke(
Asset.Colors.ProgressIndicator.negativeSpace.color,
lineWidth: 5.0
)
.scaleEffect(1.15)
.rotationEffect(Angle(degrees: -90))
}
}
}
}
extension CircularProgress {
var segmentTrim: CGFloat {
guard maxSegments != 0 else { return 1.0 }
return CGFloat(1.0 / Double(maxSegments))
}
func fromValue(_ segmentIndex: Int, _ segmentTrim: CGFloat, _ progress: CGFloat) -> 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)
}
}

View File

@ -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)"
}
}
}

View File

@ -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<Balance, Never>
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<Balance, Never> {
@ -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<Balance, Never> {
@ -397,7 +397,7 @@ class TestWrappedSDKSynchronizer: WrappedSDKSynchronizer {
func synchronizerSynced() { }
func status() -> String { "" }
func statusSnapshot() -> SyncStatusSnapshot { .default }
func getShieldedBalance() -> Effect<Balance, Never> {
return .none

View File

@ -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)

View File

@ -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))
}
}

View File

@ -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)

View File

@ -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)