Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ABW-3715 Disable Accounts with Insufficient Funds #1291

Merged
merged 6 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading