[#720] User can proceed to send flow without funds (#723)

- the send button is enabled only when some funds are available
- the verified balance used for sufficiency of funds instead of total
- changed the pending transaction logic and split into sending/receiving instead
- code cleanup of unused CancelId
- UI cleanup, detail of receiving transaction rendered "to" prefix with no address

[#720] User can proceed to send flow without funds (#723)

- totalBalance -> totalSpendableBalance
- improved code syntax for the conditions in TransactionState deciding the status
This commit is contained in:
Lukas Korba 2023-05-17 16:31:08 +02:00 committed by GitHub
parent b204c42a13
commit b142609af9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 77 additions and 54 deletions

View File

@ -114,7 +114,7 @@ extension SDKSynchronizerClient {
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.amount.amount > 5 ? .pending : $0.status,
status: $0.amount.amount > 5 ? .sending : $0.status,
timestamp: $0.date,
uuid: $0.uuid
)

View File

@ -106,7 +106,9 @@ extension AlertRequest {
case .cantStoreThatUserPassedPhraseBackupTest(let error):
return AlertState(
title: TextState(L10n.Root.Initialization.Alert.Failed.title),
message: TextState(L10n.Root.Initialization.Alert.CantStoreThatUserPassedPhraseBackupTest.message(error.message, error.code.rawValue)),
message: TextState(
L10n.Root.Initialization.Alert.CantStoreThatUserPassedPhraseBackupTest.message(error.message, error.code.rawValue)
),
dismissButton: .default(TextState(L10n.General.ok), action: .send(.dismissAlert))
)
case let .failedToProcessDeeplink(url, error):

View File

@ -22,8 +22,8 @@ struct BalanceBreakdownReducer: ReducerProtocol {
var shieldingFunds: Bool
var transparentBalance: Balance
var totalBalance: Zatoshi {
shieldedBalance.data.total + transparentBalance.data.total
var totalSpendableBalance: Zatoshi {
shieldedBalance.data.verified + transparentBalance.data.verified
}
var isShieldableBalanceAvailable: Bool {

View File

@ -25,11 +25,11 @@ struct BalanceBreakdownView: View {
balanceView(
title: L10n.BalanceBreakdown.shieldedZec(TargetConstants.tokenName),
viewStore.shieldedBalance.data.total,
viewStore.shieldedBalance.data.verified,
titleColor: Asset.Colors.Mfp.fontDark.color
)
balanceView(title: L10n.BalanceBreakdown.transparentBalance, viewStore.transparentBalance.data.total)
balanceView(title: L10n.BalanceBreakdown.totalBalance, viewStore.totalBalance)
balanceView(title: L10n.BalanceBreakdown.transparentBalance, viewStore.transparentBalance.data.verified)
balanceView(title: L10n.BalanceBreakdown.totalSpendableBalance, viewStore.totalSpendableBalance)
shieldButton(viewStore)

View File

@ -10,8 +10,7 @@ typealias HomeViewStore = ViewStore<HomeReducer.State, HomeReducer.Action>
struct HomeReducer: ReducerProtocol {
private enum CancelId {}
private enum CancelEventsId {}
struct State: Equatable {
enum Destination: Equatable {
case balanceBreakdown
@ -38,7 +37,7 @@ struct HomeReducer: ReducerProtocol {
var zecPrice = Decimal(140.0)
var totalCurrencyBalance: Zatoshi {
Zatoshi.from(decimal: shieldedBalance.data.total.decimalValue.decimalValue * zecPrice)
Zatoshi.from(decimal: shieldedBalance.data.verified.decimalValue.decimalValue * zecPrice)
}
var isSyncing: Bool {
@ -58,7 +57,7 @@ struct HomeReducer: ReducerProtocol {
var isSendButtonDisabled: Bool {
// If the destination is `.send` the button must be enabled
// to avoid involuntary navigation pop.
self.destination != .send && self.isSyncing
(self.destination != .send && self.isSyncing) || shieldedBalance.data.verified.amount == 0
}
}
@ -131,10 +130,7 @@ struct HomeReducer: ReducerProtocol {
}
case .onDisappear:
return .merge(
.cancel(id: CancelId.self),
.cancel(id: CancelEventsId.self)
)
return .cancel(id: CancelId.self)
case .resolveReviewRequest:
if reviewRequest.canRequestReview() {
@ -163,10 +159,10 @@ struct HomeReducer: ReducerProtocol {
switch snapshot.syncStatus {
case .error(let error):
return EffectTask(value: .showSynchronizerErrorAlert(error.toZcashError()))
case .upToDate:
return .fireAndForget { await reviewRequest.syncFinished() }
default:
return .none
}

View File

@ -110,7 +110,7 @@ extension HomeView {
Button {
viewStore.send(.updateDestination(.balanceBreakdown))
} label: {
Text(L10n.balance(viewStore.shieldedBalance.data.total.decimalString(), TargetConstants.tokenName))
Text(L10n.balance(viewStore.shieldedBalance.data.verified.decimalString(), TargetConstants.tokenName))
.font(.system(size: 32))
.fontWeight(.bold)
}

View File

@ -75,7 +75,7 @@ struct SendFlowReducer: ReducerProtocol {
}
var totalCurrencyBalance: Zatoshi {
Zatoshi.from(decimal: shieldedBalance.data.total.decimalValue.decimalValue * transactionAmountInputState.zecPrice)
Zatoshi.from(decimal: shieldedBalance.data.verified.decimalValue.decimalValue * transactionAmountInputState.zecPrice)
}
}
@ -213,7 +213,7 @@ struct SendFlowReducer: ReducerProtocol {
case .synchronizerStateChanged(let latestState):
let shieldedBalance = latestState.shieldedBalance
state.shieldedBalance = shieldedBalance.redacted
state.transactionAmountInputState.maxValue = shieldedBalance.total.amount.redacted
state.transactionAmountInputState.maxValue = shieldedBalance.verified.amount.redacted
return .none
case .memo:

View File

@ -10,7 +10,7 @@ struct CreateTransaction: View {
return WithViewStore(store) { viewStore in
VStack(spacing: 5) {
VStack(spacing: 0) {
Text(L10n.Balance.available(viewStore.shieldedBalance.data.total.decimalString(), TargetConstants.tokenName))
Text(L10n.Balance.available(viewStore.shieldedBalance.data.verified.decimalString(), TargetConstants.tokenName))
.font(.system(size: 26))
.fontWeight(.bold)
.multilineTextAlignment(.center)

View File

@ -28,12 +28,17 @@ struct TransactionDetailView: View {
address(mark: .inactive, viewStore: viewStore)
memo(transaction, viewStore, mark: .highlight)
case .pending:
case .sending:
Text(L10n.Transaction.youAreSending(transaction.zecAmount.decimalString(), TargetConstants.tokenName))
.padding()
address(mark: .inactive, viewStore: viewStore)
memo(transaction, viewStore, mark: .highlight)
case .receiving:
Text(L10n.Transaction.youAreReceiving(transaction.zecAmount.decimalString(), TargetConstants.tokenName))
.padding()
memo(transaction, viewStore, mark: .highlight)
case .received:
Text(L10n.Transaction.youReceived(transaction.zecAmount.decimalString(), TargetConstants.tokenName))
.padding()
@ -66,8 +71,11 @@ extension TransactionDetailView {
var header: some View {
HStack {
switch transaction.status {
case .pending:
Text(L10n.Transaction.pending)
case .sending:
Text(L10n.Transaction.sending)
Spacer()
case .receiving:
Text(L10n.Transaction.receiving)
Spacer()
case .failed:
Text("\(transaction.date?.asHumanReadable() ?? L10n.General.dateNotAvailable)")
@ -126,7 +134,8 @@ extension TransactionDetailView {
extension TransactionDetailView {
var addressPrefixText: String {
transaction.status == .received ? L10n.Transaction.from : L10n.Transaction.to
(transaction.status == .received || transaction.status == .receiving)
? "" : L10n.Transaction.to
}
var heightText: String {
@ -216,7 +225,7 @@ struct TransactionDetail_Previews: PreviewProvider {
zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po",
fee: Zatoshi(1_000_000),
id: "ff3927e1f83df9b1b0dc75540ddc59ee435eecebae914d2e6dfe8576fbedc9a8",
status: .pending,
status: .sending,
timestamp: 1234567,
zecAmount: Zatoshi(25_000_000)
),

View File

@ -63,23 +63,26 @@ extension TransactionRowView {
case .failed:
// TODO: [#392] final text to be provided (https://github.com/zcash/secant-ios-wallet/issues/392)
return L10n.Transaction.failed
case .pending:
case .sending:
return L10n.Transaction.sending
case .receiving:
return L10n.Transaction.receiving
}
}
var icon: some View {
HStack {
let inTransaction = transaction.status == .received || transaction.status == .receiving
return HStack {
switch transaction.status {
case .paid, .received, .pending:
case .paid, .received, .sending, .receiving:
Image(systemName: "arrow.forward")
.resizable()
.frame(width: 12, height: 12)
.foregroundColor(transaction.status == .received ? .yellow : .white)
.foregroundColor(inTransaction ? .yellow : .white)
.padding(10)
.background(Asset.Colors.Mfp.primary.color)
.cornerRadius(40)
.rotationEffect(Angle(degrees: transaction.status == .received ? 135 : -45))
.rotationEffect(Angle(degrees: inTransaction ? 135 : -45))
.padding(.leading, 14)
case .failed:
// TODO: [#392] final icon to be provided (https://github.com/zcash/secant-ios-wallet/issues/392)
@ -131,7 +134,7 @@ struct TransactionRowView_Previews: PreviewProvider {
zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po",
fee: Zatoshi(10),
id: "2",
status: .pending,
status: .sending,
timestamp: 1234567,
zecAmount: Zatoshi(123_000_000)
)

View File

@ -14,7 +14,8 @@ struct TransactionState: Equatable, Identifiable {
case paid(success: Bool)
case received
case failed
case pending
case sending
case receiving
}
var errorMessage: String?
@ -36,9 +37,9 @@ struct TransactionState: Equatable, Identifiable {
var unarySymbol: String {
switch status {
case .paid, .pending:
case .paid, .sending:
return "-"
case .received:
case .received, .receiving:
return "+"
case .failed:
return ""
@ -74,11 +75,18 @@ extension TransactionState {
minedHeight = transaction.minedHeight
fee = transaction.fee ?? .zero
id = transaction.rawID.toHexStringTxId()
status = transaction.isPending(currentHeight: latestBlockHeight ?? 0) ? .pending
: transaction.isSentTransaction ? .paid(success: minedHeight ?? 0 > 0) : .received
timestamp = transaction.blockTime
zecAmount = transaction.isSentTransaction ? Zatoshi(-transaction.value.amount) : transaction.value
self.memos = memos
let isSent = transaction.isSentTransaction
let isPending = transaction.isPending(currentHeight: latestBlockHeight ?? 0)
switch (isSent, isPending) {
case (true, true): status = .sending
case (true, false): status = .paid(success: minedHeight ?? 0 > 0)
case (false, true): status = .receiving
case (false, false): status = .received
}
}
}

View File

@ -76,11 +76,12 @@ private extension WalletEvent {
}
static func mockedWalletEventState(atIndex: Int) -> WalletEvent.WalletEventState {
switch atIndex % 4 {
switch atIndex % 5 {
case 0: return .transaction(.statePlaceholder(.received))
case 1: return .transaction(.statePlaceholder(.failed))
case 2: return .transaction(.statePlaceholder(.pending))
case 3: return .transaction(.placeholder)
case 2: return .transaction(.statePlaceholder(.sending))
case 3: return .transaction(.statePlaceholder(.receiving))
case 4: return .transaction(.placeholder)
default: return .transaction(.placeholder)
}
}

View File

@ -58,7 +58,7 @@ internal enum L10n {
/// Shielding funds
internal static let shieldingFunds = L10n.tr("Localizable", "balanceBreakdown.shieldingFunds", fallback: "Shielding funds")
/// TOTAL BALANCE
internal static let totalBalance = L10n.tr("Localizable", "balanceBreakdown.totalBalance", fallback: "TOTAL BALANCE")
internal static let totalSpendableBalance = L10n.tr("Localizable", "balanceBreakdown.totalSpendableBalance", fallback: "TOTAL BALANCE")
/// TRANSPARENT BALANCE
internal static let transparentBalance = L10n.tr("Localizable", "balanceBreakdown.transparentBalance", fallback: "TRANSPARENT BALANCE")
internal enum Alert {
@ -592,14 +592,12 @@ internal enum L10n {
}
/// Failed
internal static let failed = L10n.tr("Localizable", "transaction.failed", fallback: "Failed")
/// from
internal static let from = L10n.tr("Localizable", "transaction.from", fallback: "from")
/// PENDING
internal static let pending = L10n.tr("Localizable", "transaction.pending", fallback: "PENDING")
/// Received
internal static let received = L10n.tr("Localizable", "transaction.received", fallback: "Received")
/// Sending
internal static let sending = L10n.tr("Localizable", "transaction.sending", fallback: "Sending")
/// RECEIVING
internal static let receiving = L10n.tr("Localizable", "transaction.receiving", fallback: "RECEIVING")
/// SENDING
internal static let sending = L10n.tr("Localizable", "transaction.sending", fallback: "SENDING")
/// Sent
internal static let sent = L10n.tr("Localizable", "transaction.sent", fallback: "Sent")
/// to
@ -608,6 +606,10 @@ internal enum L10n {
internal static let unconfirmed = L10n.tr("Localizable", "transaction.unconfirmed", fallback: "unconfirmed")
/// With memo:
internal static let withMemo = L10n.tr("Localizable", "transaction.withMemo", fallback: "With memo:")
/// You are receiving %@ %@
internal static func youAreReceiving(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "transaction.youAreReceiving", String(describing: p1), String(describing: p2), fallback: "You are receiving %@ %@")
}
/// You are sending %@ %@
internal static func youAreSending(_ p1: Any, _ p2: Any) -> String {
return L10n.tr("Localizable", "transaction.youAreSending", String(describing: p1), String(describing: p2), fallback: "You are sending %@ %@")

View File

@ -91,7 +91,7 @@
"balanceBreakdown.blockId" = "Block: %@";
"balanceBreakdown.shieldedZec" = "SHIELDED %@ (SPENDABLE)";
"balanceBreakdown.transparentBalance" = "TRANSPARENT BALANCE";
"balanceBreakdown.totalBalance" = "TOTAL BALANCE";
"balanceBreakdown.totalSpendableBalance" = "TOTAL BALANCE";
"balanceBreakdown.autoShieldingThreshold" = "Shielding Threshold: %@ %@";
"balanceBreakdown.shieldFunds" = "Shield funds";
"balanceBreakdown.shieldingFunds" = "Shielding funds";
@ -143,18 +143,20 @@
"transactions.title" = "Transactions";
"transaction.sent" = "Sent";
"transaction.sending" = "Sending";
"transaction.receiving" = "Receiving";
"transaction.received" = "Received";
"transaction.failed" = "Failed";
"transaction.youSent" = "You sent %@ %@";
"transaction.youAreSending" = "You are sending %@ %@";
"transaction.youAreReceiving" = "You are receiving %@ %@";
"transaction.youReceived" = "You received %@ %@";
"transaction.youDidNotSent" = "You DID NOT send %@ %@";
"transaction.pending" = "PENDING";
"transaction.sending" = "SENDING";
"transaction.receiving" = "RECEIVING";
"transaction.confirmed" = "Confirmed";
"transaction.confirmedTimes" = "%@ times";
"transaction.confirming" = "Confirming ~%@mins";
"transaction.withMemo" = "With memo:";
"transaction.from" = "from";
"transaction.to" = "to";
"transaction.unconfirmed" = "unconfirmed";
"transactionDetail.title" = "Transaction detail";

View File

@ -14,7 +14,7 @@ class HomeSnapshotTests: XCTestCase {
func testHomeSnapshot() throws {
let transactionsHelper: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: true), uuid: "1"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), status: .pending, uuid: "2"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), status: .sending, uuid: "2"),
TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .received, uuid: "3"),
TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), status: .failed, uuid: "4")
]

View File

@ -150,7 +150,7 @@ class WalletEventsSnapshotTests: XCTestCase {
zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po",
fee: Zatoshi(1_000_000),
id: "ff3927e1f83df9b1b0dc75540ddc59ee435eecebae914d2e6dfe8576fbedc9a8",
status: .pending,
status: .sending,
timestamp: 1234567,
zecAmount: Zatoshi(25_000_000)
)

View File

@ -57,7 +57,7 @@ class WalletEventsTests: XCTestCase {
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.amount.amount > 5 ? .pending : $0.status,
status: $0.amount.amount > 5 ? .sending : $0.status,
timestamp: $0.date,
uuid: $0.uuid
)