[#1087] Total and available balances component
- The total and available balances have been extracted into separate and fully autonomous component used in 3 independent places (Account, Send and Balances) - Code massively cleaned up - Available balance underline logic + redirect to Balances [#1087] Total and available balances component - Unit tests fixes [#1087] Total and available balances component - tests fixed [#1087] Total and available balances component - small fixes and tweaks
This commit is contained in:
parent
3e68ed8e33
commit
6e054f5b52
|
@ -60,6 +60,7 @@ let package = Package(
|
|||
.library(name: "UserDefaults", targets: ["UserDefaults"]),
|
||||
.library(name: "UserPreferencesStorage", targets: ["UserPreferencesStorage"]),
|
||||
.library(name: "Utils", targets: ["Utils"]),
|
||||
.library(name: "WalletBalances", targets: ["WalletBalances"]),
|
||||
.library(name: "WalletConfigProvider", targets: ["WalletConfigProvider"]),
|
||||
.library(name: "WalletStorage", targets: ["WalletStorage"]),
|
||||
.library(name: "Welcome", targets: ["Welcome"]),
|
||||
|
@ -245,6 +246,7 @@ let package = Package(
|
|||
"UIComponents",
|
||||
"Utils",
|
||||
"TransactionList",
|
||||
"WalletBalances",
|
||||
"ZcashSDKEnvironment",
|
||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
|
||||
.product(name: "ZcashLightClientKit", package: "zcash-swift-wallet-sdk")
|
||||
|
@ -506,6 +508,7 @@ let package = Package(
|
|||
"SDKSynchronizer",
|
||||
"UIComponents",
|
||||
"Utils",
|
||||
"WalletBalances",
|
||||
"WalletStorage",
|
||||
"ZcashSDKEnvironment",
|
||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
|
||||
|
@ -653,6 +656,19 @@ let package = Package(
|
|||
],
|
||||
path: "Sources/Utils"
|
||||
),
|
||||
.target(
|
||||
name: "WalletBalances",
|
||||
dependencies: [
|
||||
"Generated",
|
||||
"Models",
|
||||
"SDKSynchronizer",
|
||||
"UIComponents",
|
||||
"Utils",
|
||||
.product(name: "ComposableArchitecture", package: "swift-composable-architecture"),
|
||||
.product(name: "ZcashLightClientKit", package: "zcash-swift-wallet-sdk")
|
||||
],
|
||||
path: "Sources/Features/WalletBalances"
|
||||
),
|
||||
.target(
|
||||
name: "WalletConfigProvider",
|
||||
dependencies: [
|
||||
|
|
|
@ -19,6 +19,7 @@ import SDKSynchronizer
|
|||
import Models
|
||||
import SyncProgress
|
||||
import RestoreWalletStorage
|
||||
import WalletBalances
|
||||
import ZcashSDKEnvironment
|
||||
|
||||
public typealias BalanceBreakdownStore = Store<BalanceBreakdownReducer.State, BalanceBreakdownReducer.Action>
|
||||
|
@ -42,10 +43,9 @@ public struct BalanceBreakdownReducer: Reducer {
|
|||
public var partialProposalErrorState: PartialProposalError.State
|
||||
public var pendingTransactions: Zatoshi
|
||||
public var shieldedBalance: Zatoshi
|
||||
public var shieldedWithPendingBalance: Zatoshi
|
||||
public var syncProgressState: SyncProgressReducer.State
|
||||
public var totalBalance: Zatoshi
|
||||
public var transparentBalance: Zatoshi
|
||||
public var walletBalancesState: WalletBalances.State
|
||||
|
||||
public var isShieldableBalanceAvailable: Bool {
|
||||
transparentBalance.amount >= autoShieldingThreshold.amount
|
||||
|
@ -54,15 +54,7 @@ public struct BalanceBreakdownReducer: Reducer {
|
|||
public var isShieldingButtonDisabled: Bool {
|
||||
isShieldingFunds || !isShieldableBalanceAvailable
|
||||
}
|
||||
|
||||
public var isProcessingZeroAvailableBalance: Bool {
|
||||
if shieldedBalance.amount == 0 && transparentBalance.amount > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return totalBalance.amount != shieldedBalance.amount && shieldedBalance.amount == 0
|
||||
}
|
||||
|
||||
|
||||
public init(
|
||||
autoShieldingThreshold: Zatoshi,
|
||||
changePending: Zatoshi,
|
||||
|
@ -72,11 +64,10 @@ public struct BalanceBreakdownReducer: Reducer {
|
|||
isHintBoxVisible: Bool = false,
|
||||
partialProposalErrorState: PartialProposalError.State,
|
||||
pendingTransactions: Zatoshi,
|
||||
shieldedBalance: Zatoshi,
|
||||
shieldedWithPendingBalance: Zatoshi = .zero,
|
||||
shieldedBalance: Zatoshi = .zero,
|
||||
syncProgressState: SyncProgressReducer.State,
|
||||
totalBalance: Zatoshi,
|
||||
transparentBalance: Zatoshi
|
||||
transparentBalance: Zatoshi = .zero,
|
||||
walletBalancesState: WalletBalances.State
|
||||
) {
|
||||
self.autoShieldingThreshold = autoShieldingThreshold
|
||||
self.changePending = changePending
|
||||
|
@ -87,10 +78,9 @@ public struct BalanceBreakdownReducer: Reducer {
|
|||
self.partialProposalErrorState = partialProposalErrorState
|
||||
self.pendingTransactions = pendingTransactions
|
||||
self.shieldedBalance = shieldedBalance
|
||||
self.shieldedWithPendingBalance = shieldedWithPendingBalance
|
||||
self.totalBalance = totalBalance
|
||||
self.syncProgressState = syncProgressState
|
||||
self.transparentBalance = transparentBalance
|
||||
self.walletBalancesState = walletBalancesState
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,10 +95,10 @@ public struct BalanceBreakdownReducer: Reducer {
|
|||
case shieldFundsFailure(ZcashError)
|
||||
case shieldFundsPartial([String], [String])
|
||||
case shieldFundsSuccess
|
||||
case synchronizerStateChanged(RedactableSynchronizerState)
|
||||
case syncProgress(SyncProgressReducer.Action)
|
||||
case updateDestination(BalanceBreakdownReducer.State.Destination?)
|
||||
case updateHintBoxVisibility(Bool)
|
||||
case walletBalances(WalletBalances.Action)
|
||||
}
|
||||
|
||||
@Dependency(\.derivationTool) var derivationTool
|
||||
|
@ -131,6 +121,10 @@ public struct BalanceBreakdownReducer: Reducer {
|
|||
PartialProposalError()
|
||||
}
|
||||
|
||||
Scope(state: \.walletBalancesState, action: /Action.walletBalances) {
|
||||
WalletBalances()
|
||||
}
|
||||
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .alert(.presented(let action)):
|
||||
|
@ -145,13 +139,7 @@ public struct BalanceBreakdownReducer: Reducer {
|
|||
|
||||
case .onAppear:
|
||||
state.autoShieldingThreshold = zcashSDKEnvironment.shieldingThreshold
|
||||
return .publisher {
|
||||
sdkSynchronizer.stateStream()
|
||||
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
|
||||
.map { $0.redacted }
|
||||
.map(Action.synchronizerStateChanged)
|
||||
}
|
||||
.cancellable(id: CancelId, cancelInFlight: true)
|
||||
return .none
|
||||
|
||||
case .onDisappear:
|
||||
return .cancel(id: CancelId)
|
||||
|
@ -207,24 +195,13 @@ public struct BalanceBreakdownReducer: Reducer {
|
|||
|
||||
case .shieldFundsSuccess:
|
||||
state.isShieldingFunds = false
|
||||
state.transparentBalance = .zero
|
||||
state.walletBalancesState.transparentBalance = .zero
|
||||
return .none
|
||||
|
||||
case let .shieldFundsPartial(txIds, statuses):
|
||||
state.partialProposalErrorState.txIds = txIds
|
||||
state.partialProposalErrorState.statuses = statuses
|
||||
return .send(.updateDestination(.partialProposalError))
|
||||
|
||||
case .synchronizerStateChanged(let latestState):
|
||||
let accountBalance = latestState.data.accountBalance?.data
|
||||
|
||||
state.shieldedBalance = (accountBalance?.saplingBalance.spendableValue ?? .zero) + (accountBalance?.orchardBalance.spendableValue ?? .zero)
|
||||
state.shieldedWithPendingBalance = (accountBalance?.saplingBalance.total() ?? .zero) + (accountBalance?.orchardBalance.total() ?? .zero)
|
||||
state.transparentBalance = accountBalance?.unshielded ?? .zero
|
||||
state.totalBalance = state.shieldedWithPendingBalance + state.transparentBalance
|
||||
state.changePending = (accountBalance?.saplingBalance.changePendingConfirmation ?? .zero) + (accountBalance?.orchardBalance.changePendingConfirmation ?? .zero)
|
||||
state.pendingTransactions = (accountBalance?.saplingBalance.valuePendingSpendability ?? .zero) + (accountBalance?.orchardBalance.valuePendingSpendability ?? .zero)
|
||||
return .none
|
||||
|
||||
case .syncProgress:
|
||||
return .none
|
||||
|
@ -236,6 +213,14 @@ public struct BalanceBreakdownReducer: Reducer {
|
|||
case .updateHintBoxVisibility(let visibility):
|
||||
state.isHintBoxVisible = visibility
|
||||
return .none
|
||||
|
||||
case .walletBalances(.balancesUpdated):
|
||||
state.shieldedBalance = state.walletBalancesState.shieldedBalance
|
||||
state.transparentBalance = state.walletBalancesState.transparentBalance
|
||||
return .none
|
||||
|
||||
case .walletBalances:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -291,10 +276,8 @@ extension BalanceBreakdownReducer.State {
|
|||
isShieldingFunds: false,
|
||||
partialProposalErrorState: .initial,
|
||||
pendingTransactions: .zero,
|
||||
shieldedBalance: .zero,
|
||||
syncProgressState: .initial,
|
||||
totalBalance: .zero,
|
||||
transparentBalance: .zero
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
|
||||
public static let initial = BalanceBreakdownReducer.State(
|
||||
|
@ -303,10 +286,8 @@ extension BalanceBreakdownReducer.State {
|
|||
isShieldingFunds: false,
|
||||
partialProposalErrorState: .initial,
|
||||
pendingTransactions: .zero,
|
||||
shieldedBalance: .zero,
|
||||
syncProgressState: .initial,
|
||||
totalBalance: .zero,
|
||||
transparentBalance: .zero
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ import Utils
|
|||
import Models
|
||||
import BalanceFormatter
|
||||
import SyncProgress
|
||||
import WalletBalances
|
||||
|
||||
public struct BalanceBreakdownView: View {
|
||||
let store: BalanceBreakdownStore
|
||||
|
@ -28,21 +29,18 @@ public struct BalanceBreakdownView: View {
|
|||
public var body: some View {
|
||||
ScrollView {
|
||||
WithViewStore(store, observe: { $0 }) { viewStore in
|
||||
BalanceWithIconView(balance: viewStore.totalBalance)
|
||||
.padding(.top, 40)
|
||||
.padding(.bottom, 5)
|
||||
.onAppear { viewStore.send(.onAppear) }
|
||||
.onDisappear { viewStore.send(.onDisappear) }
|
||||
|
||||
AvailableBalanceView(
|
||||
balance: viewStore.shieldedBalance,
|
||||
WalletBalancesView(
|
||||
store: store.scope(
|
||||
state: \.walletBalancesState,
|
||||
action: BalanceBreakdownReducer.Action.walletBalances
|
||||
),
|
||||
tokenName: tokenName,
|
||||
showIndicator: viewStore.isProcessingZeroAvailableBalance
|
||||
underlinedAvailableBalance: false
|
||||
)
|
||||
|
||||
|
||||
Asset.Colors.primary.color
|
||||
.frame(height: 1)
|
||||
.padding(EdgeInsets(top: 30, leading: 30, bottom: 10, trailing: 30))
|
||||
.padding(EdgeInsets(top: 0, leading: 30, bottom: 10, trailing: 30))
|
||||
|
||||
balancesBlock(viewStore)
|
||||
|
||||
|
@ -81,10 +79,12 @@ public struct BalanceBreakdownView: View {
|
|||
}
|
||||
.padding(.vertical, 1)
|
||||
.applyScreenBackground()
|
||||
.alert(store: store.scope(
|
||||
state: \.$alert,
|
||||
action: { .alert($0) }
|
||||
))
|
||||
.alert(
|
||||
store: store.scope(
|
||||
state: \.$alert,
|
||||
action: { .alert($0) }
|
||||
)
|
||||
)
|
||||
.task { await store.send(.restoreWalletTask).finish() }
|
||||
}
|
||||
}
|
||||
|
@ -272,14 +272,12 @@ extension BalanceBreakdownView {
|
|||
isHintBoxVisible: true,
|
||||
partialProposalErrorState: .initial,
|
||||
pendingTransactions: Zatoshi(25_234_000),
|
||||
shieldedBalance: Zatoshi(25_234_778),
|
||||
syncProgressState: .init(
|
||||
lastKnownSyncPercentage: 0.43,
|
||||
synchronizerStatusSnapshot: SyncStatusSnapshot(.syncing(0.41)),
|
||||
syncStatusMessage: "Syncing"
|
||||
),
|
||||
totalBalance: Zatoshi(25_234_778),
|
||||
transparentBalance: Zatoshi(25_234_778)
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
) {
|
||||
BalanceBreakdownReducer()
|
||||
|
|
|
@ -11,6 +11,7 @@ import TransactionList
|
|||
import Scan
|
||||
import SyncProgress
|
||||
import RestoreWalletStorage
|
||||
import WalletBalances
|
||||
|
||||
public typealias HomeStore = Store<HomeReducer.State, HomeReducer.Action>
|
||||
public typealias HomeViewStore = ViewStore<HomeReducer.State, HomeReducer.Action>
|
||||
|
@ -23,71 +24,36 @@ public struct HomeReducer: Reducer {
|
|||
@PresentationState public var alert: AlertState<Action>?
|
||||
public var canRequestReview = false
|
||||
public var isRestoringWallet = false
|
||||
public var requiredTransactionConfirmations = 0
|
||||
public var migratingDatabase = true
|
||||
public var scanState: Scan.State
|
||||
public var shieldedBalance: Zatoshi
|
||||
public var shieldedWithPendingBalance: Zatoshi
|
||||
public var synchronizerStatusSnapshot: SyncStatusSnapshot
|
||||
public var syncProgressState: SyncProgressReducer.State
|
||||
public var walletConfig: WalletConfig
|
||||
public var totalBalance: Zatoshi
|
||||
public var transactionListState: TransactionListReducer.State
|
||||
public var transparentBalance: Zatoshi
|
||||
public var migratingDatabase = true
|
||||
// TODO: [#311] - Get the ZEC price from the SDK, https://github.com/Electric-Coin-Company/zashi-ios/issues/311
|
||||
public var zecPrice = Decimal(140.0)
|
||||
|
||||
public var totalCurrencyBalance: Zatoshi {
|
||||
Zatoshi.from(decimal: shieldedBalance.decimalValue.decimalValue * zecPrice)
|
||||
}
|
||||
|
||||
public var isSendButtonDisabled: Bool {
|
||||
shieldedBalance.amount == 0
|
||||
}
|
||||
|
||||
public var isProcessingZeroAvailableBalance: Bool {
|
||||
if shieldedBalance.amount == 0 && transparentBalance.amount > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return totalBalance.amount != shieldedBalance.amount && shieldedBalance.amount == 0
|
||||
}
|
||||
public var walletBalancesState: WalletBalances.State
|
||||
|
||||
public init(
|
||||
canRequestReview: Bool = false,
|
||||
isRestoringWallet: Bool = false,
|
||||
requiredTransactionConfirmations: Int = 0,
|
||||
migratingDatabase: Bool = true,
|
||||
scanState: Scan.State,
|
||||
shieldedBalance: Zatoshi,
|
||||
shieldedWithPendingBalance: Zatoshi = .zero,
|
||||
synchronizerStatusSnapshot: SyncStatusSnapshot,
|
||||
syncProgressState: SyncProgressReducer.State,
|
||||
totalBalance: Zatoshi = .zero,
|
||||
transactionListState: TransactionListReducer.State,
|
||||
transparentBalance: Zatoshi = .zero,
|
||||
walletConfig: WalletConfig,
|
||||
zecPrice: Decimal = Decimal(140.0)
|
||||
walletBalancesState: WalletBalances.State,
|
||||
walletConfig: WalletConfig
|
||||
) {
|
||||
self.canRequestReview = canRequestReview
|
||||
self.isRestoringWallet = isRestoringWallet
|
||||
self.requiredTransactionConfirmations = requiredTransactionConfirmations
|
||||
self.migratingDatabase = migratingDatabase
|
||||
self.scanState = scanState
|
||||
self.shieldedBalance = shieldedBalance
|
||||
self.shieldedWithPendingBalance = shieldedWithPendingBalance
|
||||
self.synchronizerStatusSnapshot = synchronizerStatusSnapshot
|
||||
self.syncProgressState = syncProgressState
|
||||
self.totalBalance = totalBalance
|
||||
self.transactionListState = transactionListState
|
||||
self.transparentBalance = transparentBalance
|
||||
self.walletConfig = walletConfig
|
||||
self.zecPrice = zecPrice
|
||||
self.walletBalancesState = walletBalancesState
|
||||
}
|
||||
}
|
||||
|
||||
public enum Action: Equatable {
|
||||
case alert(PresentationAction<Action>)
|
||||
case balanceBreakdown
|
||||
case debugMenuStartup
|
||||
case foundTransactions
|
||||
case onAppear
|
||||
case onDisappear
|
||||
|
@ -102,6 +68,7 @@ public struct HomeReducer: Reducer {
|
|||
case syncProgress(SyncProgressReducer.Action)
|
||||
case updateTransactionList([TransactionState])
|
||||
case transactionList(TransactionListReducer.Action)
|
||||
case walletBalances(WalletBalances.Action)
|
||||
}
|
||||
|
||||
@Dependency(\.mainQueue) var mainQueue
|
||||
|
@ -121,19 +88,16 @@ public struct HomeReducer: Reducer {
|
|||
SyncProgressReducer()
|
||||
}
|
||||
|
||||
Scope(state: \.walletBalancesState, action: /Action.walletBalances) {
|
||||
WalletBalances()
|
||||
}
|
||||
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .onAppear:
|
||||
state.requiredTransactionConfirmations = zcashSDKEnvironment.requiredTransactionConfirmations
|
||||
return .merge(
|
||||
.publisher {
|
||||
sdkSynchronizer.stateStream()
|
||||
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
|
||||
.map { $0.redacted }
|
||||
.map(HomeReducer.Action.synchronizerStateChanged)
|
||||
}
|
||||
.cancellable(id: CancelStateId, cancelInFlight: true),
|
||||
.publisher {
|
||||
state.walletBalancesState.migratingDatabase = state.migratingDatabase
|
||||
state.migratingDatabase = false
|
||||
return .publisher {
|
||||
sdkSynchronizer.eventStream()
|
||||
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
|
||||
.compactMap {
|
||||
|
@ -144,7 +108,6 @@ public struct HomeReducer: Reducer {
|
|||
}
|
||||
}
|
||||
.cancellable(id: CancelEventId, cancelInFlight: true)
|
||||
)
|
||||
|
||||
case .onDisappear:
|
||||
return .concatenate(
|
||||
|
@ -181,19 +144,6 @@ public struct HomeReducer: Reducer {
|
|||
|
||||
case .synchronizerStateChanged(let latestState):
|
||||
let snapshot = SyncStatusSnapshot.snapshotFor(state: latestState.data.syncStatus)
|
||||
|
||||
if snapshot.syncStatus != .unprepared {
|
||||
state.migratingDatabase = false
|
||||
}
|
||||
|
||||
state.synchronizerStatusSnapshot = snapshot
|
||||
let accountBalance = latestState.data.accountBalance?.data
|
||||
|
||||
state.shieldedBalance = (accountBalance?.saplingBalance.spendableValue ?? .zero) + (accountBalance?.orchardBalance.spendableValue ?? .zero)
|
||||
state.shieldedWithPendingBalance = (accountBalance?.saplingBalance.total() ?? .zero) + (accountBalance?.orchardBalance.total() ?? .zero)
|
||||
state.transparentBalance = accountBalance?.unshielded ?? .zero
|
||||
state.totalBalance = state.shieldedWithPendingBalance + state.transparentBalance
|
||||
|
||||
switch snapshot.syncStatus {
|
||||
case .error(let error):
|
||||
return Effect.send(.showSynchronizerErrorAlert(error.toZcashError()))
|
||||
|
@ -230,13 +180,10 @@ public struct HomeReducer: Reducer {
|
|||
case .showSynchronizerErrorAlert:
|
||||
return .none
|
||||
|
||||
case .debugMenuStartup:
|
||||
return .none
|
||||
|
||||
case .syncFailed:
|
||||
return .none
|
||||
|
||||
case .balanceBreakdown:
|
||||
case .walletBalances:
|
||||
return .none
|
||||
|
||||
case .alert(.presented(let action)):
|
||||
|
@ -270,10 +217,9 @@ extension HomeReducer.State {
|
|||
public static var initial: Self {
|
||||
.init(
|
||||
scanState: .initial,
|
||||
shieldedBalance: .zero,
|
||||
synchronizerStatusSnapshot: .initial,
|
||||
syncProgressState: .initial,
|
||||
transactionListState: .initial,
|
||||
walletBalancesState: .initial,
|
||||
walletConfig: .initial
|
||||
)
|
||||
}
|
||||
|
@ -292,12 +238,9 @@ extension HomeStore {
|
|||
HomeStore(
|
||||
initialState: .init(
|
||||
scanState: .initial,
|
||||
shieldedBalance: .zero,
|
||||
synchronizerStatusSnapshot: .snapshotFor(
|
||||
state: .error(ZcashError.synchronizerNotPrepared)
|
||||
),
|
||||
syncProgressState: .initial,
|
||||
transactionListState: .initial,
|
||||
walletBalancesState: .initial,
|
||||
walletConfig: .initial
|
||||
)
|
||||
) {
|
||||
|
|
|
@ -8,6 +8,7 @@ import UIComponents
|
|||
import SyncProgress
|
||||
import Utils
|
||||
import Models
|
||||
import WalletBalances
|
||||
|
||||
public struct HomeView: View {
|
||||
let store: HomeStore
|
||||
|
@ -21,7 +22,14 @@ public struct HomeView: View {
|
|||
public var body: some View {
|
||||
WithViewStore(store, observe: { $0 }) { viewStore in
|
||||
VStack(spacing: 0) {
|
||||
balance(viewStore)
|
||||
WalletBalancesView(
|
||||
store: store.scope(
|
||||
state: \.walletBalancesState,
|
||||
action: HomeReducer.Action.walletBalances
|
||||
),
|
||||
tokenName: tokenName
|
||||
)
|
||||
.padding(.top, 1)
|
||||
|
||||
if viewStore.isRestoringWallet {
|
||||
SyncProgressView(
|
||||
|
@ -60,42 +68,6 @@ public struct HomeView: View {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Buttons
|
||||
|
||||
extension HomeView {
|
||||
func balance(_ viewStore: HomeViewStore) -> some View {
|
||||
VStack(spacing: 0) {
|
||||
Button {
|
||||
viewStore.send(.balanceBreakdown)
|
||||
} label: {
|
||||
BalanceWithIconView(balance: viewStore.totalBalance)
|
||||
}
|
||||
.padding(.top, 40)
|
||||
|
||||
if viewStore.migratingDatabase {
|
||||
Text(L10n.Home.migratingDatabases)
|
||||
.font(.custom(FontFamily.Inter.regular.name, size: 14))
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 30)
|
||||
} else {
|
||||
AvailableBalanceView(
|
||||
balance: viewStore.shieldedBalance,
|
||||
tokenName: tokenName,
|
||||
showIndicator: viewStore.isProcessingZeroAvailableBalance
|
||||
)
|
||||
#if !SECANT_DISTRIB
|
||||
.accessDebugMenuWithHiddenGesture {
|
||||
viewStore.send(.debugMenuStartup)
|
||||
}
|
||||
#endif
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 30)
|
||||
}
|
||||
}
|
||||
.foregroundColor(Asset.Colors.primary.color)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
struct HomeView_Previews: PreviewProvider {
|
||||
|
@ -108,15 +80,13 @@ struct HomeView_Previews: PreviewProvider {
|
|||
.init(
|
||||
isRestoringWallet: true,
|
||||
scanState: .initial,
|
||||
shieldedBalance: .zero,
|
||||
synchronizerStatusSnapshot: .initial,
|
||||
syncProgressState: .init(
|
||||
lastKnownSyncPercentage: Float(0.43),
|
||||
synchronizerStatusSnapshot: SyncStatusSnapshot(.syncing(0.41)),
|
||||
syncStatusMessage: "Syncing"
|
||||
),
|
||||
totalBalance: .zero,
|
||||
transactionListState: .placeholder,
|
||||
walletBalancesState: .initial,
|
||||
walletConfig: .initial
|
||||
)
|
||||
) {
|
||||
|
|
|
@ -386,7 +386,7 @@ extension RootReducer {
|
|||
}
|
||||
return .none
|
||||
|
||||
case .welcome(.debugMenuStartup), .tabs(.home(.debugMenuStartup)):
|
||||
case .welcome(.debugMenuStartup), .tabs(.home(.walletBalances(.debugMenuStartup))):
|
||||
return .concatenate(
|
||||
Effect.cancel(id: CancelId),
|
||||
Effect.send(.destination(.updateDestination(.startup)))
|
||||
|
|
|
@ -153,7 +153,6 @@ public struct SendFlowConfirmationView: View {
|
|||
),
|
||||
partialProposalErrorState: .initial,
|
||||
scanState: .initial,
|
||||
spendableBalance: Zatoshi(4412323012_345),
|
||||
transactionAddressInputState:
|
||||
TransactionAddressTextFieldReducer.State(
|
||||
textFieldState:
|
||||
|
@ -162,7 +161,8 @@ public struct SendFlowConfirmationView: View {
|
|||
text: "utest1zkkkjfxkamagznjr6ayemffj2d2gacdwpzcyw669pvg06xevzqslpmm27zjsctlkstl2vsw62xrjktmzqcu4yu9zdhdxqz3kafa4j2q85y6mv74rzjcgjg8c0ytrg7dwyzwtgnuc76h".redacted
|
||||
)
|
||||
),
|
||||
transactionAmountInputState: .initial
|
||||
transactionAmountInputState: .initial,
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
) {
|
||||
SendFlowReducer()
|
||||
|
|
|
@ -20,13 +20,12 @@ import UIComponents
|
|||
import Models
|
||||
import Generated
|
||||
import BalanceFormatter
|
||||
import WalletBalances
|
||||
|
||||
public typealias SendFlowStore = Store<SendFlowReducer.State, SendFlowReducer.Action>
|
||||
public typealias SendFlowViewStore = ViewStore<SendFlowReducer.State, SendFlowReducer.Action>
|
||||
|
||||
public struct SendFlowReducer: Reducer {
|
||||
private let SyncStatusUpdatesID = UUID()
|
||||
|
||||
public struct State: Equatable {
|
||||
public enum Destination: Equatable {
|
||||
case partialProposalError
|
||||
|
@ -42,12 +41,10 @@ public struct SendFlowReducer: Reducer {
|
|||
public var partialProposalErrorState: PartialProposalError.State
|
||||
public var proposal: Proposal?
|
||||
public var scanState: Scan.State
|
||||
public var spendableBalance = Zatoshi.zero
|
||||
public var shieldedWithPendingBalance = Zatoshi.zero
|
||||
public var totalBalance = Zatoshi.zero
|
||||
public var shieldedBalance: Zatoshi
|
||||
public var transactionAddressInputState: TransactionAddressTextFieldReducer.State
|
||||
public var transactionAmountInputState: TransactionAmountTextFieldReducer.State
|
||||
public var transparentBalance: Zatoshi
|
||||
public var walletBalancesState: WalletBalances.State
|
||||
|
||||
public var address: String {
|
||||
get { transactionAddressInputState.textFieldState.text.data }
|
||||
|
@ -96,7 +93,7 @@ public struct SendFlowReducer: Reducer {
|
|||
public var isInsufficientFunds: Bool {
|
||||
guard transactionAmountInputState.isValidInput else { return false }
|
||||
|
||||
return transactionAmountInputState.amount.data > spendableBalance.amount
|
||||
return transactionAmountInputState.amount.data > shieldedBalance.amount
|
||||
}
|
||||
|
||||
public var isMemoInputEnabled: Bool {
|
||||
|
@ -104,19 +101,11 @@ public struct SendFlowReducer: Reducer {
|
|||
}
|
||||
|
||||
public var totalCurrencyBalance: Zatoshi {
|
||||
Zatoshi.from(decimal: spendableBalance.decimalValue.decimalValue * transactionAmountInputState.zecPrice)
|
||||
Zatoshi.from(decimal: shieldedBalance.decimalValue.decimalValue * transactionAmountInputState.zecPrice)
|
||||
}
|
||||
|
||||
public var spendableBalanceString: String {
|
||||
spendableBalance.decimalString(formatter: NumberFormatter.zashiBalanceFormatter)
|
||||
}
|
||||
|
||||
public var isProcessingZeroAvailableBalance: Bool {
|
||||
if spendableBalance.amount == 0 && transparentBalance.amount > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return totalBalance.amount != spendableBalance.amount && spendableBalance.amount == 0
|
||||
shieldedBalance.decimalString(formatter: NumberFormatter.zashiBalanceFormatter)
|
||||
}
|
||||
|
||||
public init(
|
||||
|
@ -126,12 +115,10 @@ public struct SendFlowReducer: Reducer {
|
|||
memoState: MessageEditorReducer.State,
|
||||
partialProposalErrorState: PartialProposalError.State,
|
||||
scanState: Scan.State,
|
||||
spendableBalance: Zatoshi = .zero,
|
||||
shieldedWithPendingBalance: Zatoshi = .zero,
|
||||
totalBalance: Zatoshi = .zero,
|
||||
shieldedBalance: Zatoshi = .zero,
|
||||
transactionAddressInputState: TransactionAddressTextFieldReducer.State,
|
||||
transactionAmountInputState: TransactionAmountTextFieldReducer.State,
|
||||
transparentBalance: Zatoshi = .zero
|
||||
walletBalancesState: WalletBalances.State
|
||||
) {
|
||||
self.addMemoState = addMemoState
|
||||
self.destination = destination
|
||||
|
@ -139,11 +126,10 @@ public struct SendFlowReducer: Reducer {
|
|||
self.memoState = memoState
|
||||
self.partialProposalErrorState = partialProposalErrorState
|
||||
self.scanState = scanState
|
||||
self.spendableBalance = spendableBalance
|
||||
self.totalBalance = totalBalance
|
||||
self.shieldedBalance = shieldedBalance
|
||||
self.transactionAddressInputState = transactionAddressInputState
|
||||
self.transactionAmountInputState = transactionAmountInputState
|
||||
self.transparentBalance = transparentBalance
|
||||
self.walletBalancesState = walletBalancesState
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,7 +138,6 @@ public struct SendFlowReducer: Reducer {
|
|||
case goBackPressed
|
||||
case memo(MessageEditorReducer.Action)
|
||||
case onAppear
|
||||
case onDisappear
|
||||
case partialProposalError(PartialProposalError.Action)
|
||||
case proposal(Proposal)
|
||||
case reviewPressed
|
||||
|
@ -161,10 +146,10 @@ public struct SendFlowReducer: Reducer {
|
|||
case sendDone
|
||||
case sendFailed(ZcashError)
|
||||
case sendPartial([String], [String])
|
||||
case synchronizerStateChanged(RedactableSynchronizerState)
|
||||
case transactionAddressInput(TransactionAddressTextFieldReducer.Action)
|
||||
case transactionAmountInput(TransactionAmountTextFieldReducer.Action)
|
||||
case updateDestination(SendFlowReducer.State.Destination?)
|
||||
case walletBalances(WalletBalances.Action)
|
||||
}
|
||||
|
||||
@Dependency(\.audioServices) var audioServices
|
||||
|
@ -198,6 +183,10 @@ public struct SendFlowReducer: Reducer {
|
|||
PartialProposalError()
|
||||
}
|
||||
|
||||
Scope(state: \.walletBalancesState, action: /Action.walletBalances) {
|
||||
WalletBalances()
|
||||
}
|
||||
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .alert(.presented(let action)):
|
||||
|
@ -212,16 +201,7 @@ public struct SendFlowReducer: Reducer {
|
|||
|
||||
case .onAppear:
|
||||
state.memoState.charLimit = zcashSDKEnvironment.memoCharLimit
|
||||
return Effect.publisher {
|
||||
sdkSynchronizer.stateStream()
|
||||
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
|
||||
.map{ $0.redacted }
|
||||
.map(SendFlowReducer.Action.synchronizerStateChanged)
|
||||
}
|
||||
.cancellable(id: SyncStatusUpdatesID, cancelInFlight: true)
|
||||
|
||||
case .onDisappear:
|
||||
return .cancel(id: SyncStatusUpdatesID)
|
||||
return .none
|
||||
|
||||
case .goBackPressed:
|
||||
state.destination = nil
|
||||
|
@ -328,16 +308,6 @@ public struct SendFlowReducer: Reducer {
|
|||
case .transactionAddressInput:
|
||||
return .none
|
||||
|
||||
case .synchronizerStateChanged(let latestState):
|
||||
let latestAccountBalance = latestState.data.accountBalance?.data
|
||||
|
||||
state.spendableBalance = (latestAccountBalance?.saplingBalance.spendableValue ?? .zero) + (latestAccountBalance?.orchardBalance.spendableValue ?? .zero)
|
||||
state.shieldedWithPendingBalance = (latestAccountBalance?.saplingBalance.total() ?? .zero) + (latestAccountBalance?.orchardBalance.total() ?? .zero)
|
||||
state.transparentBalance = latestAccountBalance?.unshielded ?? .zero
|
||||
state.totalBalance = state.shieldedWithPendingBalance + state.transparentBalance
|
||||
state.transactionAmountInputState.maxValue = state.spendableBalance.amount.redacted
|
||||
return .none
|
||||
|
||||
case .memo:
|
||||
return .none
|
||||
|
||||
|
@ -359,6 +329,13 @@ public struct SendFlowReducer: Reducer {
|
|||
|
||||
case .scan:
|
||||
return .none
|
||||
|
||||
case .walletBalances(.balancesUpdated):
|
||||
state.shieldedBalance = state.walletBalancesState.shieldedBalance
|
||||
return .none
|
||||
|
||||
case .walletBalances:
|
||||
return .none
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -448,7 +425,8 @@ extension SendFlowReducer.State {
|
|||
partialProposalErrorState: .initial,
|
||||
scanState: .initial,
|
||||
transactionAddressInputState: .initial,
|
||||
transactionAmountInputState: .initial
|
||||
transactionAmountInputState: .initial,
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import Scan
|
|||
import UIComponents
|
||||
import BalanceFormatter
|
||||
import PartialProposalError
|
||||
import WalletBalances
|
||||
|
||||
public struct SendFlowView: View {
|
||||
private enum InputID: Hashable {
|
||||
|
@ -36,14 +37,12 @@ public struct SendFlowView: View {
|
|||
ScrollView {
|
||||
ScrollViewReader { value in
|
||||
VStack(alignment: .center) {
|
||||
BalanceWithIconView(balance: viewStore.totalBalance)
|
||||
.padding(.top, 40)
|
||||
.padding(.bottom, 5)
|
||||
|
||||
AvailableBalanceView(
|
||||
balance: viewStore.spendableBalance,
|
||||
tokenName: tokenName,
|
||||
showIndicator: viewStore.isProcessingZeroAvailableBalance
|
||||
WalletBalancesView(
|
||||
store: store.scope(
|
||||
state: \.walletBalancesState,
|
||||
action: SendFlowReducer.Action.walletBalances
|
||||
),
|
||||
tokenName: tokenName
|
||||
)
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
|
@ -98,7 +97,6 @@ public struct SendFlowView: View {
|
|||
}
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.padding(.top, 40)
|
||||
|
||||
MessageEditor(store: store.memoStore())
|
||||
.frame(height: 175)
|
||||
|
@ -143,7 +141,6 @@ public struct SendFlowView: View {
|
|||
}
|
||||
}
|
||||
.onAppear { viewStore.send(.onAppear) }
|
||||
.onDisappear { viewStore.send(.onDisappear) }
|
||||
.applyScreenBackground()
|
||||
.navigationLinkEmpty(
|
||||
isActive: viewStore.bindingForScanQR,
|
||||
|
@ -188,7 +185,8 @@ public struct SendFlowView: View {
|
|||
partialProposalErrorState: .initial,
|
||||
scanState: .initial,
|
||||
transactionAddressInputState: .initial,
|
||||
transactionAmountInputState: .initial
|
||||
transactionAmountInputState: .initial,
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
) {
|
||||
SendFlowReducer()
|
||||
|
|
|
@ -125,7 +125,8 @@ public struct TabsReducer: Reducer {
|
|||
case .balanceBreakdown:
|
||||
return .none
|
||||
|
||||
case .home(.balanceBreakdown):
|
||||
case .home(.walletBalances(.availableBalanceTapped)),
|
||||
.send(.walletBalances(.availableBalanceTapped)):
|
||||
state.selectedTab = .balances
|
||||
return .none
|
||||
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
//
|
||||
// WalletBalancesStore.swift
|
||||
// secant-testnet
|
||||
//
|
||||
// Created by Lukáš Korba on 04-02-2024
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import ComposableArchitecture
|
||||
|
||||
import Models
|
||||
import SDKSynchronizer
|
||||
import Utils
|
||||
import ZcashLightClientKit
|
||||
|
||||
@Reducer
|
||||
public struct WalletBalances {
|
||||
private let CancelStateId = UUID()
|
||||
|
||||
@ObservableState
|
||||
public struct State: Equatable {
|
||||
public var isAvailableBalanceTappable = true
|
||||
public var migratingDatabase = false
|
||||
public var shieldedBalance: Zatoshi
|
||||
public var shieldedWithPendingBalance: Zatoshi
|
||||
public var totalBalance: Zatoshi
|
||||
public var transparentBalance: Zatoshi
|
||||
|
||||
public var isProcessingZeroAvailableBalance: Bool {
|
||||
if shieldedBalance.amount == 0 && transparentBalance.amount > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return totalBalance.amount != shieldedBalance.amount && shieldedBalance.amount == 0
|
||||
}
|
||||
|
||||
public init(
|
||||
isAvailableBalanceTappable: Bool = true,
|
||||
migratingDatabase: Bool = false,
|
||||
shieldedBalance: Zatoshi = .zero,
|
||||
shieldedWithPendingBalance: Zatoshi = .zero,
|
||||
totalBalance: Zatoshi = .zero,
|
||||
transparentBalance: Zatoshi = .zero
|
||||
) {
|
||||
self.isAvailableBalanceTappable = isAvailableBalanceTappable
|
||||
self.migratingDatabase = migratingDatabase
|
||||
self.shieldedBalance = shieldedBalance
|
||||
self.shieldedWithPendingBalance = shieldedWithPendingBalance
|
||||
self.totalBalance = totalBalance
|
||||
self.transparentBalance = transparentBalance
|
||||
}
|
||||
}
|
||||
|
||||
public enum Action: Equatable {
|
||||
case availableBalanceTapped
|
||||
case balancesUpdated
|
||||
case debugMenuStartup
|
||||
case onAppear
|
||||
case onDisappear
|
||||
case synchronizerStateChanged(RedactableSynchronizerState)
|
||||
}
|
||||
|
||||
@Dependency(\.mainQueue) var mainQueue
|
||||
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
|
||||
|
||||
public init() { }
|
||||
|
||||
public var body: some Reducer<State, Action> {
|
||||
Reduce { state, action in
|
||||
switch action {
|
||||
case .onAppear:
|
||||
let accountBalance = sdkSynchronizer.latestState().accountBalance
|
||||
|
||||
state.shieldedBalance = (accountBalance?.saplingBalance.spendableValue ?? .zero) + (accountBalance?.orchardBalance.spendableValue ?? .zero)
|
||||
state.shieldedWithPendingBalance = (accountBalance?.saplingBalance.total() ?? .zero) + (accountBalance?.orchardBalance.total() ?? .zero)
|
||||
state.transparentBalance = accountBalance?.unshielded ?? .zero
|
||||
state.totalBalance = state.shieldedWithPendingBalance + state.transparentBalance
|
||||
|
||||
return .merge(
|
||||
.send(.balancesUpdated),
|
||||
.publisher {
|
||||
sdkSynchronizer.stateStream()
|
||||
.throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true)
|
||||
.map { $0.redacted }
|
||||
.map(WalletBalances.Action.synchronizerStateChanged)
|
||||
}
|
||||
.cancellable(id: CancelStateId, cancelInFlight: true)
|
||||
)
|
||||
|
||||
case .onDisappear:
|
||||
return .cancel(id: CancelStateId)
|
||||
|
||||
case .availableBalanceTapped:
|
||||
return .none
|
||||
|
||||
case .balancesUpdated:
|
||||
return .none
|
||||
|
||||
case .debugMenuStartup:
|
||||
return .none
|
||||
|
||||
case .synchronizerStateChanged(let latestState):
|
||||
let snapshot = SyncStatusSnapshot.snapshotFor(state: latestState.data.syncStatus)
|
||||
|
||||
if snapshot.syncStatus != .unprepared {
|
||||
state.migratingDatabase = false
|
||||
}
|
||||
|
||||
let accountBalance = latestState.data.accountBalance?.data
|
||||
|
||||
state.shieldedBalance = (accountBalance?.saplingBalance.spendableValue ?? .zero) + (accountBalance?.orchardBalance.spendableValue ?? .zero)
|
||||
state.shieldedWithPendingBalance = (accountBalance?.saplingBalance.total() ?? .zero) + (accountBalance?.orchardBalance.total() ?? .zero)
|
||||
state.transparentBalance = accountBalance?.unshielded ?? .zero
|
||||
state.totalBalance = state.shieldedWithPendingBalance + state.transparentBalance
|
||||
return .send(.balancesUpdated)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,99 @@
|
|||
//
|
||||
// WalletBalancesView.swift
|
||||
// secant-testnet
|
||||
//
|
||||
// Created by Lukáš Korba on 04-02-2024
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import ComposableArchitecture
|
||||
import Generated
|
||||
import UIComponents
|
||||
|
||||
public struct WalletBalancesView: View {
|
||||
@Perception.Bindable var store: StoreOf<WalletBalances>
|
||||
let tokenName: String
|
||||
let underlinedAvailableBalance: Bool
|
||||
|
||||
public init(
|
||||
store: StoreOf<WalletBalances>,
|
||||
tokenName: String,
|
||||
underlinedAvailableBalance: Bool = true
|
||||
) {
|
||||
self.store = store
|
||||
self.tokenName = tokenName
|
||||
self.underlinedAvailableBalance = underlinedAvailableBalance
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
WithPerceptionTracking {
|
||||
VStack(spacing: 0) {
|
||||
BalanceWithIconView(balance: store.totalBalance)
|
||||
.padding(.top, 40)
|
||||
#if !SECANT_DISTRIB
|
||||
.accessDebugMenuWithHiddenGesture {
|
||||
store.send(.debugMenuStartup)
|
||||
}
|
||||
#endif
|
||||
|
||||
if store.migratingDatabase {
|
||||
Text(L10n.Home.migratingDatabases)
|
||||
.font(.custom(FontFamily.Inter.regular.name, size: 14))
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 30)
|
||||
} else {
|
||||
if underlinedAvailableBalance {
|
||||
Button {
|
||||
store.send(.availableBalanceTapped)
|
||||
} label: {
|
||||
AvailableBalanceView(
|
||||
balance: store.shieldedBalance,
|
||||
tokenName: tokenName,
|
||||
showIndicator: store.isProcessingZeroAvailableBalance,
|
||||
underlined: underlinedAvailableBalance
|
||||
)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 30)
|
||||
}
|
||||
} else {
|
||||
AvailableBalanceView(
|
||||
balance: store.shieldedBalance,
|
||||
tokenName: tokenName,
|
||||
showIndicator: store.isProcessingZeroAvailableBalance,
|
||||
underlined: underlinedAvailableBalance
|
||||
)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 30)
|
||||
}
|
||||
}
|
||||
}
|
||||
.foregroundColor(Asset.Colors.primary.color)
|
||||
.onAppear { store.send(.onAppear) }
|
||||
.onDisappear { store.send(.onDisappear) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview {
|
||||
WalletBalancesView(store: WalletBalances.initial, tokenName: "ZEC")
|
||||
}
|
||||
|
||||
// MARK: - Store
|
||||
|
||||
extension WalletBalances {
|
||||
public static var initial = StoreOf<WalletBalances>(
|
||||
initialState: .initial
|
||||
) {
|
||||
WalletBalances()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Placeholders
|
||||
|
||||
extension WalletBalances.State {
|
||||
public static let initial = WalletBalances.State(
|
||||
shieldedBalance: .zero
|
||||
)
|
||||
}
|
|
@ -37,12 +37,8 @@ public enum L10n {
|
|||
}
|
||||
}
|
||||
public enum Balance {
|
||||
/// Available balance %@ %@
|
||||
public static func available(_ p1: Any, _ p2: Any) -> String {
|
||||
return L10n.tr("Localizable", "balance.available", String(describing: p1), String(describing: p2), fallback: "Available balance %@ %@")
|
||||
}
|
||||
/// Available Balance
|
||||
public static let availableTitle = L10n.tr("Localizable", "balance.availableTitle", fallback: "Available Balance")
|
||||
/// Available Balance:
|
||||
public static let availableTitle = L10n.tr("Localizable", "balance.availableTitle", fallback: "Available Balance:")
|
||||
}
|
||||
public enum Balances {
|
||||
/// Change pending
|
||||
|
|
|
@ -241,8 +241,7 @@ Sharing this private data is irrevocable — once you have shared this private d
|
|||
"general.done" = "Done";
|
||||
"general.save" = "Save";
|
||||
"general.restoringWallet" = "[RESTORING YOUR WALLET…]";
|
||||
"balance.available" = "Available balance %@ %@";
|
||||
"balance.availableTitle" = "Available Balance";
|
||||
"balance.availableTitle" = "Available Balance:";
|
||||
"qrCodeFor" = "QR Code for %@";
|
||||
"general.dateNotAvailable" = "date not available";
|
||||
"general.tapToCopy" = "Tap to copy";
|
||||
|
|
|
@ -13,22 +13,30 @@ public struct AvailableBalanceView: View {
|
|||
let balance: Zatoshi
|
||||
let tokenName: String
|
||||
let showIndicator: Bool
|
||||
let underlined: Bool
|
||||
|
||||
public init(
|
||||
balance: Zatoshi,
|
||||
tokenName: String,
|
||||
showIndicator: Bool = false
|
||||
showIndicator: Bool = false,
|
||||
underlined: Bool = true
|
||||
) {
|
||||
self.balance = balance
|
||||
self.tokenName = tokenName
|
||||
self.showIndicator = showIndicator
|
||||
self.underlined = underlined
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
HStack(spacing: 3) {
|
||||
Text(L10n.Balance.availableTitle)
|
||||
.font(.custom(FontFamily.Inter.regular.name, size: 14))
|
||||
.underline()
|
||||
if underlined {
|
||||
Text(L10n.Balance.availableTitle)
|
||||
.font(.custom(FontFamily.Inter.regular.name, size: 14))
|
||||
.underline()
|
||||
} else {
|
||||
Text(L10n.Balance.availableTitle)
|
||||
.font(.custom(FontFamily.Inter.regular.name, size: 14))
|
||||
}
|
||||
|
||||
if showIndicator {
|
||||
ProgressView()
|
||||
|
|
|
@ -14,35 +14,11 @@ import Generated
|
|||
import BalanceBreakdown
|
||||
import Models
|
||||
import ZcashSDKEnvironment
|
||||
import WalletBalances
|
||||
@testable import secant_testnet
|
||||
|
||||
@MainActor
|
||||
class BalanceBreakdownTests: XCTestCase {
|
||||
func testOnAppear() async throws {
|
||||
let store = TestStore(
|
||||
initialState: .placeholder
|
||||
) {
|
||||
BalanceBreakdownReducer()
|
||||
}
|
||||
|
||||
store.dependencies.sdkSynchronizer = .mocked()
|
||||
store.dependencies.mainQueue = .immediate
|
||||
store.dependencies.numberFormatter = .noOp
|
||||
|
||||
await store.send(.onAppear) { state in
|
||||
state.autoShieldingThreshold = ZcashSDKEnvironment.liveValue.shieldingThreshold
|
||||
}
|
||||
|
||||
// expected side effects as a result of .onAppear registration
|
||||
await store.receive(.synchronizerStateChanged(SynchronizerState.zero.redacted))
|
||||
|
||||
// long-living (cancelable) effects need to be properly canceled.
|
||||
// the .onDisappear action cancels the observer of the synchronizer status change.
|
||||
await store.send(.onDisappear)
|
||||
|
||||
await store.finish()
|
||||
}
|
||||
|
||||
func testShieldFundsFails() async throws {
|
||||
let store = TestStore(
|
||||
initialState: .placeholder
|
||||
|
@ -101,10 +77,9 @@ class BalanceBreakdownTests: XCTestCase {
|
|||
isShieldingFunds: false,
|
||||
partialProposalErrorState: .initial,
|
||||
pendingTransactions: .zero,
|
||||
shieldedBalance: .zero,
|
||||
syncProgressState: .initial,
|
||||
totalBalance: .zero,
|
||||
transparentBalance: Zatoshi(1_000_000)
|
||||
transparentBalance: Zatoshi(1_000_000),
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
) {
|
||||
BalanceBreakdownReducer()
|
||||
|
@ -123,10 +98,9 @@ class BalanceBreakdownTests: XCTestCase {
|
|||
isShieldingFunds: true,
|
||||
partialProposalErrorState: .initial,
|
||||
pendingTransactions: .zero,
|
||||
shieldedBalance: .zero,
|
||||
syncProgressState: .initial,
|
||||
totalBalance: .zero,
|
||||
transparentBalance: Zatoshi(1_000_000)
|
||||
transparentBalance: Zatoshi(1_000_000),
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
) {
|
||||
BalanceBreakdownReducer()
|
||||
|
|
|
@ -16,27 +16,6 @@ import Home
|
|||
@testable import ZcashLightClientKit
|
||||
|
||||
class HomeTests: XCTestCase {
|
||||
func testSendButtonIsDisabledWhenSyncing() {
|
||||
let mockSnapshot = SyncStatusSnapshot.init(
|
||||
.syncing(0.7)
|
||||
)
|
||||
|
||||
let store = TestStore(
|
||||
initialState: .init(
|
||||
scanState: .initial,
|
||||
shieldedBalance: .zero,
|
||||
synchronizerStatusSnapshot: mockSnapshot,
|
||||
syncProgressState: .initial,
|
||||
transactionListState: .initial,
|
||||
walletConfig: .initial
|
||||
)
|
||||
) {
|
||||
HomeReducer()
|
||||
}
|
||||
|
||||
XCTAssertTrue(store.state.isSendButtonDisabled)
|
||||
}
|
||||
|
||||
/// The .onAppear action is important to register for the synchronizer state updates.
|
||||
/// The integration tests make sure registrations and side effects are properly implemented.
|
||||
@MainActor func testOnAppear() async throws {
|
||||
|
@ -52,17 +31,12 @@ class HomeTests: XCTestCase {
|
|||
store.dependencies.reviewRequest = .noOp
|
||||
|
||||
await store.send(.onAppear) { state in
|
||||
state.requiredTransactionConfirmations = 10
|
||||
state.migratingDatabase = false
|
||||
state.walletBalancesState.migratingDatabase = true
|
||||
}
|
||||
|
||||
var syncState: SynchronizerState = .zero
|
||||
syncState.syncStatus = .unprepared
|
||||
let snapshot = SyncStatusSnapshot.snapshotFor(state: syncState.syncStatus)
|
||||
|
||||
// expected side effects as a result of .onAppear registration
|
||||
await store.receive(.synchronizerStateChanged(SynchronizerState.zero.redacted)) { state in
|
||||
state.synchronizerStatusSnapshot = snapshot
|
||||
}
|
||||
|
||||
// long-living (cancelable) effects need to be properly canceled.
|
||||
// the .onDisappear action cancels the observer of the synchronizer status change.
|
||||
|
@ -73,9 +47,6 @@ class HomeTests: XCTestCase {
|
|||
|
||||
@MainActor func testSynchronizerErrorBringsUpAlert() async {
|
||||
let testError = ZcashError.synchronizerNotPrepared
|
||||
let errorSnapshot = SyncStatusSnapshot.snapshotFor(
|
||||
state: .error(testError)
|
||||
)
|
||||
|
||||
var state = SynchronizerState.zero
|
||||
state.syncStatus = .error(testError)
|
||||
|
@ -86,10 +57,7 @@ class HomeTests: XCTestCase {
|
|||
HomeReducer()
|
||||
}
|
||||
|
||||
await store.send(.synchronizerStateChanged(state.redacted)) { state in
|
||||
state.synchronizerStatusSnapshot = errorSnapshot
|
||||
state.migratingDatabase = false
|
||||
}
|
||||
await store.send(.synchronizerStateChanged(state.redacted))
|
||||
|
||||
await store.receive(.showSynchronizerErrorAlert(testError))
|
||||
|
||||
|
|
|
@ -44,12 +44,8 @@ final class ReviewRequestTests: XCTestCase {
|
|||
|
||||
var syncState: SynchronizerState = .zero
|
||||
syncState.syncStatus = .upToDate
|
||||
let snapshot = SyncStatusSnapshot.snapshotFor(state: syncState.syncStatus)
|
||||
|
||||
await store.send(.synchronizerStateChanged(syncState.redacted)) { state in
|
||||
state.synchronizerStatusSnapshot = snapshot
|
||||
state.migratingDatabase = false
|
||||
}
|
||||
await store.send(.synchronizerStateChanged(syncState.redacted))
|
||||
|
||||
let storedDate = userDefaultsClient.objectForKey(ReviewRequestClient.Constants.latestSyncKey) as? TimeInterval
|
||||
XCTAssertEqual(now.timeIntervalSince1970, storedDate, "Review Request: stored date doesn't match the input.")
|
||||
|
|
|
@ -14,6 +14,7 @@ import Models
|
|||
import WalletStorage
|
||||
import SendFlow
|
||||
import UIComponents
|
||||
import WalletBalances
|
||||
@testable import secant_testnet
|
||||
|
||||
// swiftlint:disable type_body_length
|
||||
|
@ -260,9 +261,10 @@ class SendTests: XCTestCase {
|
|||
memoState: .initial,
|
||||
partialProposalErrorState: .initial,
|
||||
scanState: .initial,
|
||||
spendableBalance: Zatoshi(100_000),
|
||||
shieldedBalance: Zatoshi(100_000),
|
||||
transactionAddressInputState: .initial,
|
||||
transactionAmountInputState: .initial
|
||||
transactionAmountInputState: .initial,
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
|
||||
let store = TestStore(
|
||||
|
@ -295,9 +297,9 @@ class SendTests: XCTestCase {
|
|||
memoState: .initial,
|
||||
partialProposalErrorState: .initial,
|
||||
scanState: .initial,
|
||||
spendableBalance: Zatoshi(90_000),
|
||||
transactionAddressInputState: .initial,
|
||||
transactionAmountInputState: .initial
|
||||
transactionAmountInputState: .initial,
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
|
||||
let store = TestStore(
|
||||
|
@ -352,9 +354,10 @@ class SendTests: XCTestCase {
|
|||
memoState: .initial,
|
||||
partialProposalErrorState: .initial,
|
||||
scanState: .initial,
|
||||
spendableBalance: Zatoshi(100_000),
|
||||
shieldedBalance: Zatoshi(100_000),
|
||||
transactionAddressInputState: .initial,
|
||||
transactionAmountInputState: .initial
|
||||
transactionAmountInputState: .initial,
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
|
||||
let store = TestStore(
|
||||
|
@ -414,7 +417,8 @@ class SendTests: XCTestCase {
|
|||
validationType: .customFloatingPoint(usNumberFormatter),
|
||||
text: "0.00501301".redacted
|
||||
)
|
||||
)
|
||||
),
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
|
||||
let store = TestStore(
|
||||
|
@ -459,7 +463,8 @@ class SendTests: XCTestCase {
|
|||
validationType: .floatingPoint,
|
||||
text: "0.00501301".redacted
|
||||
)
|
||||
)
|
||||
),
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
|
||||
let store = TestStore(
|
||||
|
@ -504,7 +509,8 @@ class SendTests: XCTestCase {
|
|||
validationType: .floatingPoint,
|
||||
text: "0.00501301".redacted
|
||||
)
|
||||
)
|
||||
),
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
|
||||
let store = TestStore(
|
||||
|
@ -537,7 +543,6 @@ class SendTests: XCTestCase {
|
|||
memoState: MessageEditorReducer.State(charLimit: 3),
|
||||
partialProposalErrorState: .initial,
|
||||
scanState: .initial,
|
||||
spendableBalance: Zatoshi(1),
|
||||
transactionAddressInputState:
|
||||
TransactionAddressTextFieldReducer.State(
|
||||
isValidAddress: true,
|
||||
|
@ -557,7 +562,8 @@ class SendTests: XCTestCase {
|
|||
validationType: .floatingPoint,
|
||||
text: "0.0.0501301".redacted
|
||||
)
|
||||
)
|
||||
),
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
|
||||
let store = TestStore(
|
||||
|
@ -595,7 +601,8 @@ class SendTests: XCTestCase {
|
|||
validationType: .floatingPoint,
|
||||
text: "0.0.0501301".redacted
|
||||
)
|
||||
)
|
||||
),
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
|
||||
let store = TestStore(
|
||||
|
@ -610,10 +617,6 @@ class SendTests: XCTestCase {
|
|||
await store.send(.onAppear) { state in
|
||||
state.memoState.charLimit = 512
|
||||
}
|
||||
|
||||
// .onAppear action starts long living cancelable action .synchronizerStateChanged
|
||||
// .onDisappear cancels it, must have for the test to pass
|
||||
await store.send(.onDisappear)
|
||||
}
|
||||
|
||||
func testScannedAddress() async throws {
|
||||
|
@ -623,7 +626,8 @@ class SendTests: XCTestCase {
|
|||
partialProposalErrorState: .initial,
|
||||
scanState: .initial,
|
||||
transactionAddressInputState: .initial,
|
||||
transactionAmountInputState: .initial
|
||||
transactionAmountInputState: .initial,
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
|
||||
let store = TestStore(
|
||||
|
@ -654,7 +658,8 @@ class SendTests: XCTestCase {
|
|||
partialProposalErrorState: .initial,
|
||||
scanState: .initial,
|
||||
transactionAddressInputState: .initial,
|
||||
transactionAmountInputState: .initial
|
||||
transactionAmountInputState: .initial,
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
sendState.address = "tmViyFacKkncJ6zhEqs8rjqNPkGqXsMV4uq"
|
||||
|
||||
|
@ -690,7 +695,8 @@ class SendTests: XCTestCase {
|
|||
partialProposalErrorState: .initial,
|
||||
scanState: .initial,
|
||||
transactionAddressInputState: .initial,
|
||||
transactionAmountInputState: .initial
|
||||
transactionAmountInputState: .initial,
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
|
||||
let store = TestStore(
|
||||
|
@ -714,7 +720,8 @@ class SendTests: XCTestCase {
|
|||
partialProposalErrorState: .initial,
|
||||
scanState: .initial,
|
||||
transactionAddressInputState: .initial,
|
||||
transactionAmountInputState: .initial
|
||||
transactionAmountInputState: .initial,
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
|
||||
let store = TestStore(
|
||||
|
|
|
@ -22,16 +22,15 @@ class BalanceBreakdownSnapshotTests: XCTestCase {
|
|||
isShieldingFunds: false,
|
||||
partialProposalErrorState: .initial,
|
||||
pendingTransactions: .zero,
|
||||
shieldedBalance: Zatoshi(123_000_000_000),
|
||||
syncProgressState: .initial,
|
||||
totalBalance: Zatoshi(123_000_000_000),
|
||||
transparentBalance: Zatoshi(850_000_000)
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
) {
|
||||
BalanceBreakdownReducer()
|
||||
.dependency(\.sdkSynchronizer, .noOp)
|
||||
.dependency(\.mainQueue, .immediate)
|
||||
.dependency(\.restoreWalletStorage, .noOp)
|
||||
.dependency(\.diskSpaceChecker, .mockEmptyDisk)
|
||||
}
|
||||
|
||||
addAttachments(BalanceBreakdownView(store: store, tokenName: "ZEC"))
|
||||
|
@ -46,16 +45,15 @@ class BalanceBreakdownSnapshotTests: XCTestCase {
|
|||
isHintBoxVisible: true,
|
||||
partialProposalErrorState: .initial,
|
||||
pendingTransactions: .zero,
|
||||
shieldedBalance: Zatoshi(123_000_000_000),
|
||||
syncProgressState: .initial,
|
||||
totalBalance: Zatoshi(123_000_000_000),
|
||||
transparentBalance: Zatoshi(850_000_000)
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
) {
|
||||
BalanceBreakdownReducer()
|
||||
.dependency(\.sdkSynchronizer, .noOp)
|
||||
.dependency(\.mainQueue, .immediate)
|
||||
.dependency(\.restoreWalletStorage, .noOp)
|
||||
.dependency(\.diskSpaceChecker, .mockEmptyDisk)
|
||||
}
|
||||
|
||||
addAttachments(BalanceBreakdownView(store: store, tokenName: "ZEC"))
|
||||
|
|
|
@ -38,10 +38,9 @@ class HomeSnapshotTests: XCTestCase {
|
|||
let store = HomeStore(
|
||||
initialState: .init(
|
||||
scanState: .initial,
|
||||
shieldedBalance: Zatoshi(12_345_000),
|
||||
synchronizerStatusSnapshot: .initial,
|
||||
syncProgressState: .initial,
|
||||
transactionListState: .init(transactionList: IdentifiedArrayOf(uniqueElements: transactionList)),
|
||||
walletBalancesState: .initial,
|
||||
walletConfig: .initial
|
||||
)
|
||||
) {
|
||||
|
|
|
@ -41,6 +41,7 @@ class SendSnapshotTests: XCTestCase {
|
|||
.dependency(\.numberFormatter, .live())
|
||||
.dependency(\.walletStorage, .live())
|
||||
.dependency(\.sdkSynchronizer, .mock)
|
||||
.dependency(\.diskSpaceChecker, .mockEmptyDisk)
|
||||
}
|
||||
|
||||
addAttachments(SendFlowView(store: store, tokenName: "ZEC"))
|
||||
|
@ -57,7 +58,6 @@ class SendSnapshotTests: XCTestCase {
|
|||
),
|
||||
partialProposalErrorState: .initial,
|
||||
scanState: .initial,
|
||||
spendableBalance: Zatoshi(4412323012_345),
|
||||
transactionAddressInputState:
|
||||
TransactionAddressTextFieldReducer.State(
|
||||
textFieldState:
|
||||
|
@ -75,7 +75,8 @@ class SendSnapshotTests: XCTestCase {
|
|||
validationType: nil,
|
||||
text: "0.9153234".redacted
|
||||
)
|
||||
)
|
||||
),
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
) {
|
||||
SendFlowReducer()
|
||||
|
@ -84,6 +85,7 @@ class SendSnapshotTests: XCTestCase {
|
|||
.dependency(\.numberFormatter, .live())
|
||||
.dependency(\.walletStorage, .live())
|
||||
.dependency(\.sdkSynchronizer, .mock)
|
||||
.dependency(\.diskSpaceChecker, .mockEmptyDisk)
|
||||
}
|
||||
|
||||
addAttachments(SendFlowConfirmationView(store: store, tokenName: "ZEC"))
|
||||
|
@ -97,7 +99,6 @@ class SendSnapshotTests: XCTestCase {
|
|||
memoState: MessageEditorReducer.State(),
|
||||
partialProposalErrorState: .initial,
|
||||
scanState: .initial,
|
||||
spendableBalance: Zatoshi(4412323012_345),
|
||||
transactionAddressInputState:
|
||||
TransactionAddressTextFieldReducer.State(
|
||||
textFieldState:
|
||||
|
@ -115,7 +116,8 @@ class SendSnapshotTests: XCTestCase {
|
|||
validationType: nil,
|
||||
text: "0.9153234".redacted
|
||||
)
|
||||
)
|
||||
),
|
||||
walletBalancesState: .initial
|
||||
)
|
||||
) {
|
||||
SendFlowReducer()
|
||||
|
@ -124,6 +126,7 @@ class SendSnapshotTests: XCTestCase {
|
|||
.dependency(\.numberFormatter, .live())
|
||||
.dependency(\.walletStorage, .live())
|
||||
.dependency(\.sdkSynchronizer, .mock)
|
||||
.dependency(\.diskSpaceChecker, .mockEmptyDisk)
|
||||
}
|
||||
|
||||
addAttachments(SendFlowConfirmationView(store: store, tokenName: "ZEC"))
|
||||
|
|
|
@ -24,7 +24,7 @@ class TabsTests: XCTestCase {
|
|||
TabsReducer()
|
||||
}
|
||||
|
||||
await store.send(.home(.balanceBreakdown)) { state in
|
||||
await store.send(.home(.walletBalances(.availableBalanceTapped))) { state in
|
||||
state.selectedTab = .balances
|
||||
}
|
||||
}
|
||||
|
@ -170,7 +170,7 @@ class TabsTests: XCTestCase {
|
|||
func testShieldFundsSucceed() async throws {
|
||||
var placeholderState = TabsReducer.State.initial
|
||||
placeholderState.selectedTab = .send
|
||||
placeholderState.balanceBreakdownState.transparentBalance = Zatoshi(10_000)
|
||||
placeholderState.balanceBreakdownState.walletBalancesState.transparentBalance = Zatoshi(10_000)
|
||||
|
||||
let store = TestStore(
|
||||
initialState: placeholderState
|
||||
|
@ -193,7 +193,7 @@ class TabsTests: XCTestCase {
|
|||
}
|
||||
|
||||
await store.receive(.balanceBreakdown(.shieldFundsSuccess)) { state in
|
||||
state.balanceBreakdownState.transparentBalance = .zero
|
||||
state.balanceBreakdownState.walletBalancesState.transparentBalance = .zero
|
||||
state.balanceBreakdownState.isShieldingFunds = false
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue