Skip to content

Commit

Permalink
ABW-3715 Disable Accounts with Insufficient Funds (#1291)
Browse files Browse the repository at this point in the history
  • Loading branch information
kugel3 committed Aug 21, 2024
1 parent aad5d3c commit 379af48
Show file tree
Hide file tree
Showing 12 changed files with 157 additions and 89 deletions.
26 changes: 16 additions & 10 deletions RadixWallet/Core/DesignSystem/Components/RadioButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ public struct RadioButton: View {
public enum State {
case unselected
case selected
case disabled
}

public enum Appearance {
Expand All @@ -12,32 +11,39 @@ public struct RadioButton: View {
}

public let appearance: Appearance
public var state: State
public let state: State
public let isDisabled: Bool

public init(
appearance: Appearance,
state: State
state: State,
disabled: Bool = false
) {
self.appearance = appearance
self.state = state
self.isDisabled = disabled
}
}

extension RadioButton {
public var body: some View {
let resource: ImageAsset = switch (appearance, state) {
case (.light, .unselected):
let resource: ImageAsset = switch (appearance, state, isDisabled) {
case (.light, .unselected, false):
AssetResource.radioButtonLightUnselected
case (.light, .selected):
case (.light, .selected, false):
AssetResource.radioButtonLightSelected
case (.light, .disabled):
case (.light, .selected, true):
AssetResource.radioButtonLightDisabled
case (.dark, .unselected):
case (.light, .unselected, true):
AssetResource.radioButtonLightDisabledUnselected
case (.dark, .unselected, false):
AssetResource.radioButtonDarkUnselected
case (.dark, .selected):
case (.dark, .selected, false):
AssetResource.radioButtonDarkSelected
case (.dark, .disabled):
case (.dark, .selected, true):
AssetResource.radioButtonDarkDisabled
case (.dark, .unselected, true):
AssetResource.radioButtonDarkDisabledUnselected
}

return Image(asset: resource)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,11 @@ public enum AssetResource {
public static let lock = ImageAsset(name: "lock")
public static let minusCircle = ImageAsset(name: "minus-circle")
public static let plusCircle = ImageAsset(name: "plus-circle")
public static let radioButtonDarkDisabledUnselected = ImageAsset(name: "radioButton-dark-disabled-unselected")
public static let radioButtonDarkDisabled = ImageAsset(name: "radioButton-dark-disabled")
public static let radioButtonDarkSelected = ImageAsset(name: "radioButton-dark-selected")
public static let radioButtonDarkUnselected = ImageAsset(name: "radioButton-dark-unselected")
public static let radioButtonLightDisabledUnselected = ImageAsset(name: "radioButton-light-disabled-unselected")
public static let radioButtonLightDisabled = ImageAsset(name: "radioButton-light-disabled")
public static let radioButtonLightSelected = ImageAsset(name: "radioButton-light-selected")
public static let radioButtonLightUnselected = ImageAsset(name: "radioButton-light-unselected")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "radioButton-dark-disabled-unselected.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "radioButton-light-disabled-unselected.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ extension CustomizeFees {
public struct ViewState: Equatable {
let mode: TransactionFee.Mode
let feePayer: Account?
let feePayingValidation: FeeValidationOutcome?
let feePayingValidation: FeePayerValidationOutcome?
}

@MainActor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public struct CustomizeFees: FeatureReducer, Sendable {
reviewedTransaction.transactionFee
}

var feePayingValidation: FeeValidationOutcome? {
var feePayingValidation: FeePayerValidationOutcome? {
reviewedTransaction.feePayingValidation.wrappedValue
}

Expand Down Expand Up @@ -107,7 +107,13 @@ public struct CustomizeFees: FeatureReducer, Sendable {
public func reduce(into state: inout State, viewAction: ViewAction) -> Effect<Action> {
switch viewAction {
case .changeFeePayerTapped:
state.destination = .selectFeePayer(.init(feePayer: state.feePayer, transactionFee: state.transactionFee))
state.destination = .selectFeePayer(
.init(
reviewedTransaction: state.reviewedTransaction,
selectedFeePayer: state.feePayer,
transactionFee: state.transactionFee
)
)
return .none
case .toggleMode:
state.reviewedTransaction.transactionFee.toggleMode()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,24 @@ import ComposableArchitecture
import SwiftUI

extension SelectFeePayer.State {
var viewState: SelectFeePayer.ViewState {
.init(
feePayerCandidates: feePayerCandidates.rawValue,
fee: transactionFee.totalFee.displayedTotalFee,
selectedPayer: feePayer
)
var feeString: String {
transactionFee.totalFee.displayedTotalFee
}
}

// MARK: - SelectFeePayer.View
extension SelectFeePayer {
public struct ViewState: Equatable {
let feePayerCandidates: Loadable<IdentifiedArrayOf<FeePayerCandidate>>
let fee: String
let selectedPayer: FeePayerCandidate?

var selectButtonControlState: ControlState {
switch feePayerCandidates {
case .idle, .loading:
.loading(.local)
case .success:
.enabled
case .failure:
.disabled
}
var selectButtonControlState: ControlState {
switch feePayerCandidates {
case .idle, .loading:
.loading(.local)
case .success:
.enabled
case .failure:
.disabled
}
}
}

// MARK: - SelectFeePayer.View
extension SelectFeePayer {
@MainActor
public struct View: SwiftUI.View {
private let store: StoreOf<SelectFeePayer>
Expand All @@ -39,7 +29,7 @@ extension SelectFeePayer {
}

public var body: some SwiftUI.View {
WithViewStore(store, observe: \.viewState, send: { .view($0) }) { viewStore in
WithViewStore(store, observe: { $0 }, send: { .view($0) }) { viewStore in
VStack {
Text(L10n.CustomizeNetworkFees.SelectFeePayer.navigationTitle)
.multilineTextAlignment(.center)
Expand All @@ -48,7 +38,7 @@ extension SelectFeePayer {
.padding(.horizontal, .medium1)
.padding(.bottom, .small2)

Text(L10n.CustomizeNetworkFees.SelectFeePayer.subtitle(viewStore.fee))
Text(L10n.CustomizeNetworkFees.SelectFeePayer.subtitle(viewStore.feeString))
.multilineTextAlignment(.center)
.textStyle(.body1HighImportance)
.foregroundColor(.app.gray2)
Expand All @@ -63,8 +53,8 @@ extension SelectFeePayer {
VStack(spacing: .small1) {
Selection(
viewStore.binding(
get: \.selectedPayer,
send: { .selectedPayer($0) }
get: \.selectedFeePayer,
send: { .selectedFeePayer($0) }
),
from: candidates
) { item in
Expand All @@ -79,7 +69,7 @@ extension SelectFeePayer {
.padding(.horizontal, .medium1)
.padding(.bottom, .medium2)
.onFirstAppear {
proxy.scrollTo(viewStore.selectedPayer?.id, anchor: .center)
proxy.scrollTo(viewStore.selectedFeePayer?.id, anchor: .center)
}
}
}
Expand All @@ -93,8 +83,8 @@ extension SelectFeePayer {
}
.footer {
WithControlRequirements(
viewStore.selectedPayer,
forAction: { viewStore.send(.confirmedFeePayer($0)) }
viewStore.selectedFeePayer,
forAction: { viewStore.send(.confirmedFeePayer($0.candidate)) }
) { action in
Button(L10n.CustomizeNetworkFees.SelectFeePayer.selectAccountButtonTitle, action: action)
.buttonStyle(.primaryRectangular)
Expand All @@ -114,10 +104,12 @@ enum SelectAccountToPayForFeeRow {
struct ViewState: Equatable {
let account: Account
let fungible: ResourceBalance.ViewState.Fungible
let isBalanceInsufficient: Bool

init(candidate: FeePayerCandidate) {
self.account = candidate.account
self.fungible = .xrd(balance: .init(nominalAmount: candidate.xrdBalance), network: account.networkID)
init(candidate: ValidatedFeePayerCandidate) {
self.account = candidate.candidate.account
self.fungible = .xrd(balance: .init(nominalAmount: candidate.candidate.xrdBalance), network: account.networkID)
self.isBalanceInsufficient = candidate.outcome == .insufficientBalance
}
}

Expand All @@ -127,22 +119,36 @@ enum SelectAccountToPayForFeeRow {
let isSelected: Bool
let action: () -> Void

var buttonState: RadioButton.State {
isSelected ? .selected : .unselected
}

var isDisabled: Bool {
viewState.isBalanceInsufficient
}

var body: some SwiftUI.View {
Button(action: action) {
Card {
VStack(spacing: .zero) {
VStack(alignment: .leading, spacing: .zero) {
AccountCard(kind: .innerCompact, account: viewState.account)

HStack {
ResourceBalanceView(.fungible(viewState.fungible), appearance: .compact)

RadioButton(appearance: .dark, state: isSelected ? .selected : .unselected)
RadioButton(appearance: .dark, state: buttonState, disabled: isDisabled)
}
.padding(.medium3)

if viewState.isBalanceInsufficient {
WarningErrorView(text: L10n.TransactionReview.FeePayerValidation.insufficientBalance, type: .error)
.padding([.horizontal, .bottom], .medium3)
}
}
}
}
.buttonStyle(.inert)
.disabled(isDisabled)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,37 @@
import ComposableArchitecture
import SwiftUI

// MARK: - ValidatedFeePayerCandidate
public struct ValidatedFeePayerCandidate: Sendable, Hashable, Identifiable {
public var id: FeePayerCandidate.ID { candidate.id }
public let candidate: FeePayerCandidate
public let outcome: FeePayerValidationOutcome
}

// MARK: - SelectFeePayer
public struct SelectFeePayer: Sendable, FeatureReducer {
public typealias FeePayerCandidates = NonEmpty<IdentifiedArrayOf<FeePayerCandidate>>

public struct State: Sendable, Hashable {
public var feePayer: FeePayerCandidate?
public let reviewedTransaction: ReviewedTransaction
public var selectedFeePayer: ValidatedFeePayerCandidate?
public let transactionFee: TransactionFee
public var feePayerCandidates: Loadable<FeePayerCandidates> = .idle
public var feePayerCandidates: Loadable<[ValidatedFeePayerCandidate]> = .idle

public init(
feePayer: FeePayerCandidate?,
reviewedTransaction: ReviewedTransaction,
selectedFeePayer: FeePayerCandidate?,
transactionFee: TransactionFee
) {
self.feePayer = feePayer
self.reviewedTransaction = reviewedTransaction
self.selectedFeePayer = selectedFeePayer.map { .init(candidate: $0, outcome: reviewedTransaction.validateFeePayer($0)) }
self.transactionFee = transactionFee
}
}

public enum ViewAction: Sendable, Equatable {
case task
case selectedPayer(FeePayerCandidate?)
case selectedFeePayer(ValidatedFeePayerCandidate?)
case confirmedFeePayer(FeePayerCandidate)
case pullToRefreshStarted
case closeButtonTapped
Expand All @@ -47,8 +57,8 @@ public struct SelectFeePayer: Sendable, FeatureReducer {
state.feePayerCandidates = .loading
return loadCandidates(refresh: false)

case let .selectedPayer(candidate):
state.feePayer = candidate
case let .selectedFeePayer(candidate):
state.selectedFeePayer = candidate
return .none

case let .confirmedFeePayer(payer):
Expand All @@ -67,7 +77,13 @@ public struct SelectFeePayer: Sendable, FeatureReducer {
public func reduce(into state: inout State, internalAction: InternalAction) -> Effect<Action> {
switch internalAction {
case let .feePayerCandidatesLoaded(.success(candidates)):
state.feePayerCandidates = .success(candidates)
let validated = candidates.rawValue.map { candidate in
ValidatedFeePayerCandidate(
candidate: candidate,
outcome: state.reviewedTransaction.validateFeePayer(candidate)
)
}
state.feePayerCandidates = .success(validated)
return .none
case let .feePayerCandidatesLoaded(.failure(error)):
errorQueue.schedule(error)
Expand Down
Loading

0 comments on commit 379af48

Please sign in to comment.