Skip to content

Commit

Permalink
[ABW-3565] Show resource details on Asset Transfer (#1211)
Browse files Browse the repository at this point in the history
  • Loading branch information
matiasbzurovski committed Jul 15, 2024
1 parent ae485d7 commit 57824ff
Show file tree
Hide file tree
Showing 17 changed files with 459 additions and 180 deletions.
14 changes: 9 additions & 5 deletions RadixWallet.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
48CFC2C72ADC10D900E77A5C /* MessageMode+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD3D2ADC10D800E77A5C /* MessageMode+View.swift */; };
48CFC2C82ADC10D900E77A5C /* ResourceAsset+Reducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD412ADC10D800E77A5C /* ResourceAsset+Reducer.swift */; };
48CFC2C92ADC10D900E77A5C /* FungibleResourceAsset+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD422ADC10D800E77A5C /* FungibleResourceAsset+View.swift */; };
48CFC2CA2ADC10D900E77A5C /* NonFungibleResourceAsset+Reducer+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD432ADC10D800E77A5C /* NonFungibleResourceAsset+Reducer+View.swift */; };
48CFC2CA2ADC10D900E77A5C /* NonFungibleResourceAsset+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD432ADC10D800E77A5C /* NonFungibleResourceAsset+View.swift */; };
48CFC2CB2ADC10D900E77A5C /* ResourceAsset+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD442ADC10D800E77A5C /* ResourceAsset+View.swift */; };
48CFC2CC2ADC10D900E77A5C /* FungibleResourceAsset+Reducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD452ADC10D800E77A5C /* FungibleResourceAsset+Reducer.swift */; };
48CFC2CD2ADC10D900E77A5C /* ReceivingAccount+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48CFBD462ADC10D800E77A5C /* ReceivingAccount+View.swift */; };
Expand Down Expand Up @@ -913,6 +913,7 @@
5BBC7D9E2C3D390E00B04BD6 /* BootstrapClient+Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBC7D9B2C3D390D00B04BD6 /* BootstrapClient+Interface.swift */; };
5BBC7D9F2C3D390E00B04BD6 /* BootstrapClient+Live.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBC7D9C2C3D390D00B04BD6 /* BootstrapClient+Live.swift */; };
5BBC7DA42C3D442800B04BD6 /* URLRequest+Extra.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBC7DA32C3D442800B04BD6 /* URLRequest+Extra.swift */; };
5BBC7DA82C40278E00B04BD6 /* NonFungibleResourceAsset+Reducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BBC7DA72C40278E00B04BD6 /* NonFungibleResourceAsset+Reducer.swift */; };
5BC82B6C2BED18A1009AC162 /* FactoryReset+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC82B6A2BED18A1009AC162 /* FactoryReset+View.swift */; };
5BC82B6D2BED18A1009AC162 /* FactoryReset+Reducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BC82B6B2BED18A1009AC162 /* FactoryReset+Reducer.swift */; };
830818482B9F1621002D8351 /* HTTPClient+Live.swift in Sources */ = {isa = PBXBuildFile; fileRef = 830818472B9F1621002D8351 /* HTTPClient+Live.swift */; };
Expand Down Expand Up @@ -1366,7 +1367,7 @@
48CFBD3D2ADC10D800E77A5C /* MessageMode+View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageMode+View.swift"; sourceTree = "<group>"; };
48CFBD412ADC10D800E77A5C /* ResourceAsset+Reducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ResourceAsset+Reducer.swift"; sourceTree = "<group>"; };
48CFBD422ADC10D800E77A5C /* FungibleResourceAsset+View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FungibleResourceAsset+View.swift"; sourceTree = "<group>"; };
48CFBD432ADC10D800E77A5C /* NonFungibleResourceAsset+Reducer+View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NonFungibleResourceAsset+Reducer+View.swift"; sourceTree = "<group>"; };
48CFBD432ADC10D800E77A5C /* NonFungibleResourceAsset+View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NonFungibleResourceAsset+View.swift"; sourceTree = "<group>"; };
48CFBD442ADC10D800E77A5C /* ResourceAsset+View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ResourceAsset+View.swift"; sourceTree = "<group>"; };
48CFBD452ADC10D800E77A5C /* FungibleResourceAsset+Reducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "FungibleResourceAsset+Reducer.swift"; sourceTree = "<group>"; };
48CFBD462ADC10D800E77A5C /* ReceivingAccount+View.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReceivingAccount+View.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2061,6 +2062,7 @@
5BBC7D9B2C3D390D00B04BD6 /* BootstrapClient+Interface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BootstrapClient+Interface.swift"; sourceTree = "<group>"; };
5BBC7D9C2C3D390D00B04BD6 /* BootstrapClient+Live.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "BootstrapClient+Live.swift"; sourceTree = "<group>"; };
5BBC7DA32C3D442800B04BD6 /* URLRequest+Extra.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLRequest+Extra.swift"; sourceTree = "<group>"; };
5BBC7DA72C40278E00B04BD6 /* NonFungibleResourceAsset+Reducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NonFungibleResourceAsset+Reducer.swift"; sourceTree = "<group>"; };
5BC82B6A2BED18A1009AC162 /* FactoryReset+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FactoryReset+View.swift"; sourceTree = "<group>"; };
5BC82B6B2BED18A1009AC162 /* FactoryReset+Reducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FactoryReset+Reducer.swift"; sourceTree = "<group>"; };
830818472B9F1621002D8351 /* HTTPClient+Live.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "HTTPClient+Live.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3236,10 +3238,11 @@
isa = PBXGroup;
children = (
48CFBD412ADC10D800E77A5C /* ResourceAsset+Reducer.swift */,
48CFBD422ADC10D800E77A5C /* FungibleResourceAsset+View.swift */,
48CFBD432ADC10D800E77A5C /* NonFungibleResourceAsset+Reducer+View.swift */,
48CFBD442ADC10D800E77A5C /* ResourceAsset+View.swift */,
48CFBD452ADC10D800E77A5C /* FungibleResourceAsset+Reducer.swift */,
48CFBD422ADC10D800E77A5C /* FungibleResourceAsset+View.swift */,
5BBC7DA72C40278E00B04BD6 /* NonFungibleResourceAsset+Reducer.swift */,
48CFBD432ADC10D800E77A5C /* NonFungibleResourceAsset+View.swift */,
);
path = Asset;
sourceTree = "<group>";
Expand Down Expand Up @@ -7526,7 +7529,7 @@
48CFC5982ADC10DA00E77A5C /* ControlState.swift in Sources */,
A41266F92B160F4C00EA38E9 /* ManualAccountRecoveryCoordinator+View.swift in Sources */,
A43F1E232BC72884001DD3FA /* AnyRange.swift in Sources */,
48CFC2CA2ADC10D900E77A5C /* NonFungibleResourceAsset+Reducer+View.swift in Sources */,
48CFC2CA2ADC10D900E77A5C /* NonFungibleResourceAsset+View.swift in Sources */,
48CFC4B72ADC10DA00E77A5C /* TransactionPreviewResponse.swift in Sources */,
48CFC3452ADC10D900E77A5C /* Completion+View.swift in Sources */,
5B43B08B2BDAAD4B00AA1E92 /* AddressDetails+View.swift in Sources */,
Expand Down Expand Up @@ -7827,6 +7830,7 @@
83EE47852AF0EE3C00155F03 /* ProgrammaticScryptoSborValueString.swift in Sources */,
83EE47912AF0EE3C00155F03 /* FromLedgerStateMixin.swift in Sources */,
48CFC5BD2ADC10DA00E77A5C /* Hint.swift in Sources */,
5BBC7DA82C40278E00B04BD6 /* NonFungibleResourceAsset+Reducer.swift in Sources */,
48CFC3622ADC10D900E77A5C /* LedgerHardwareDevices+Reducer.swift in Sources */,
48CFC2B22ADC10D900E77A5C /* DerivePublicKeys.swift in Sources */,
48CFC34D2ADC10D900E77A5C /* Login.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ private final class HomeCardsObserver: Sargon.HomeCardsObserver, Sendable {
let subject: AsyncCurrentValueSubject<[HomeCard]> = .init([])

func handleCardsUpdate(cards: [HomeCard]) {
print("M- Received cards from Sargon \(cards)")
subject.send(cards)
}
}
14 changes: 14 additions & 0 deletions RadixWallet/Core/DesignSystem/Extensions/View+Extra.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,18 @@ extension View {
func eraseToAnyView() -> AnyView {
AnyView(self)
}

/// Embeds the view on a `Button` when an action is provided.
/// Otherwise returns the same view unmodified.
func embedInButton(when action: (() -> Void)?) -> some View {
Group {
if let action {
Button(action: action) {
self
}
} else {
self
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public struct FungibleResourceAsset: Sendable, FeatureReducer {
// MARK: - Mutable state

@PresentationState
public var alert: AlertState<ViewAction.Alert>?
public var destination: Destination.State? = nil

public var transferAmountStr: String = ""
public var transferAmount: Decimal192? = nil
Expand All @@ -51,15 +51,25 @@ public struct FungibleResourceAsset: Sendable, FeatureReducer {
case amountChanged(String)
case maxAmountTapped
case focusChanged(Bool)
case removeTapped
case resourceTapped
}

case alert(PresentationAction<Alert>)
public enum DelegateAction: Equatable, Sendable {
case amountChanged
case resourceTapped
}

public enum Alert: Hashable, Sendable {
case chooseXRDAmountAlert(ChooseXRDAmountAlert)
public struct Destination: DestinationReducer {
public enum State: Sendable, Hashable {
case chooseXRDAmount(AlertState<Action.ChooseXRDAmount>)
case needsToPayFeeFromOtherAccount(AlertState<Action.NeedsToPayFeeFromOtherAccount>)
}

public enum Action: Sendable, Equatable {
case chooseXRDAmount(ChooseXRDAmount)
case needsToPayFeeFromOtherAccount(NeedsToPayFeeFromOtherAccount)

public enum ChooseXRDAmountAlert: Hashable, Sendable {
public enum ChooseXRDAmount: Hashable, Sendable {
case deductFee(Decimal192)
case sendAll(Decimal192)
case cancel
Expand All @@ -70,13 +80,23 @@ public struct FungibleResourceAsset: Sendable, FeatureReducer {
case cancel
}
}

public var body: some ReducerOf<Self> {
EmptyReducer()
}
}

public enum DelegateAction: Equatable, Sendable {
case removed
case amountChanged
public init() {}

public var body: some ReducerOf<Self> {
Reduce(core)
.ifLet(destinationPath, action: /Action.destination) {
Destination()
}
}

private let destinationPath: WritableKeyPath<State, PresentationState<Destination.State>> = \.$destination

public func reduce(into state: inout State, viewAction: ViewAction) -> Effect<Action> {
switch viewAction {
case let .amountChanged(transferAmountStr):
Expand All @@ -95,13 +115,13 @@ public struct FungibleResourceAsset: Sendable, FeatureReducer {

if state.isXRD {
if remainingAmount >= State.defaultFee {
state.alert = .chooseXRDAmount(
state.destination = .chooseXRDAmount(.alert(
feeDeductedAmount: remainingAmount - State.defaultFee,
maxAmount: remainingAmount
)
))
return .none
} else if remainingAmount > 0 {
state.alert = .willNeedToPayFeeFromOtherAccount(remainingAmount)
state.destination = .needsToPayFeeFromOtherAccount(.anotherAccount(remainingAmount))
return .none
}
}
Expand All @@ -114,68 +134,59 @@ public struct FungibleResourceAsset: Sendable, FeatureReducer {
state.focused = focused
return .none

case .removeTapped:
return .send(.delegate(.removed))

case let .alert(action):
state.alert = nil
switch action {
case let .presented(.chooseXRDAmountAlert(.deductFee(amount))),
let .presented(.chooseXRDAmountAlert(.sendAll(amount))),
let .presented(.needsToPayFeeFromOtherAccount(.confirm(amount))):
state.transferAmount = amount
state.transferAmountStr = amount.formattedPlain(useGroupingSeparator: false)
return .send(.delegate(.amountChanged))

case .presented(.needsToPayFeeFromOtherAccount(.cancel)),
.presented(.chooseXRDAmountAlert(.cancel)):
return .none

case .dismiss:
return .none
}
case .resourceTapped:
return .send(.delegate(.resourceTapped))
}
}

public func reduce(into state: inout State, presentedAction: Destination.Action) -> Effect<Action> {
switch presentedAction {
case let .chooseXRDAmount(.deductFee(amount)),
let .chooseXRDAmount(.sendAll(amount)),
let .needsToPayFeeFromOtherAccount(.confirm(amount)):
state.transferAmount = amount
state.transferAmountStr = amount.formattedPlain(useGroupingSeparator: false)
return .send(.delegate(.amountChanged))

default:
return .none
}
}
}

extension AlertState where Action == FungibleResourceAsset.ViewAction.Alert {
fileprivate static func chooseXRDAmount(feeDeductedAmount: Decimal192, maxAmount: Decimal192) -> Self {
AlertState(
title: TextState(L10n.AssetTransfer.MaxAmountDialog.title),
message: TextState(L10n.AssetTransfer.MaxAmountDialog.body),
buttons:
[
ButtonState.default(
TextState(L10n.AssetTransfer.MaxAmountDialog.saveXrdForFeeButton(feeDeductedAmount.formatted())),
action: .send(.chooseXRDAmountAlert(.deductFee(feeDeductedAmount)))
),
ButtonState.default(
TextState(L10n.AssetTransfer.MaxAmountDialog.sendAllButton(maxAmount.formatted())),
action: .send(.chooseXRDAmountAlert(.sendAll(maxAmount)))
),
ButtonState.default(
TextState(L10n.Common.cancel),
action: .send(.chooseXRDAmountAlert(.cancel))
),
]
)
extension AlertState<FungibleResourceAsset.Destination.Action.ChooseXRDAmount> {
fileprivate static func alert(feeDeductedAmount: Decimal192, maxAmount: Decimal192) -> AlertState {
AlertState {
TextState(L10n.AssetTransfer.MaxAmountDialog.title)
} actions: {
ButtonState(action: .deductFee(feeDeductedAmount)) {
TextState(L10n.AssetTransfer.MaxAmountDialog.saveXrdForFeeButton(feeDeductedAmount.formatted()))
}
ButtonState(action: .sendAll(maxAmount)) {
TextState(L10n.AssetTransfer.MaxAmountDialog.sendAllButton(maxAmount.formatted()))
}
ButtonState(role: .cancel, action: .cancel) {
TextState(L10n.Common.cancel)
}
} message: {
TextState(L10n.AssetTransfer.MaxAmountDialog.body)
}
}
}

fileprivate static func willNeedToPayFeeFromOtherAccount(_ amount: Decimal192) -> Self {
AlertState(
title: TextState(L10n.AssetTransfer.MaxAmountDialog.title),
message: TextState("Sending the full amount of XRD in this account will require you to pay the transaction fee from a different account"),
buttons:
[
ButtonState.default(
TextState(L10n.Common.ok),
action: .send(.needsToPayFeeFromOtherAccount(.confirm(amount)))
),
ButtonState.default(
TextState(L10n.Common.cancel),
action: .send(.needsToPayFeeFromOtherAccount(.cancel))
),
]
)
extension AlertState<FungibleResourceAsset.Destination.Action.NeedsToPayFeeFromOtherAccount> {
fileprivate static func anotherAccount(_ amount: Decimal192) -> AlertState {
AlertState {
TextState(L10n.AssetTransfer.MaxAmountDialog.title)
} actions: {
ButtonState(action: .confirm(amount)) {
TextState(L10n.Common.ok)
}
ButtonState(role: .cancel, action: .cancel) {
TextState(L10n.Common.cancel)
}
} message: {
TextState("Sending the full amount of XRD in this account will require you to pay the transaction fee from a different account")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,24 +37,26 @@ extension FungibleResourceAsset.View {
public var body: some View {
WithViewStore(store, observe: { $0 }, send: { .view($0) }) { viewStore in
VStack(alignment: .trailing) {
ResourceBalanceView(viewStore.resourceBalance, appearance: .compact)
.withAuxiliary(spacing: .small2) {
TextField(
Decimal192.zero.formatted(),
text: viewStore.binding(
get: \.transferAmountStr,
send: { .amountChanged($0) }
)
ResourceBalanceView(viewStore.resourceBalance, appearance: .compact) {
viewStore.send(.resourceTapped)
}
.withAuxiliary(spacing: .small2) {
TextField(
Decimal192.zero.formatted(),
text: viewStore.binding(
get: \.transferAmountStr,
send: { .amountChanged($0) }
)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.lineLimit(1)
.minimumScaleFactor(0.7)
.foregroundColor(.app.gray1)
.textStyle(.sectionHeader)
.focused($focused)
.bind(viewStore.focusedBinding, to: $focused)
}
)
.keyboardType(.decimalPad)
.multilineTextAlignment(.trailing)
.lineLimit(1)
.minimumScaleFactor(0.7)
.foregroundColor(.app.gray1)
.textStyle(.sectionHeader)
.focused($focused)
.bind(viewStore.focusedBinding, to: $focused)
}

if viewStore.totalExceedsBalance {
// TODO: Add better style
Expand Down Expand Up @@ -84,13 +86,41 @@ extension FungibleResourceAsset.View {
}
}
.padding(.medium3)
.alert(store: store.alert)
.destinations(with: store)
}
}
}

private extension StoreOf<FungibleResourceAsset> {
var alert: AlertPresentationStore<FungibleResourceAsset.ViewAction.Alert> {
scope(state: \.$alert, action: { .view(.alert($0)) })
var destination: PresentationStoreOf<FungibleResourceAsset.Destination> {
func scopeState(state: State) -> PresentationState<FungibleResourceAsset.Destination.State> {
state.$destination
}
return scope(state: scopeState, action: Action.destination)
}
}

@MainActor
private extension View {
func destinations(with store: StoreOf<FungibleResourceAsset>) -> some View {
let destinationStore = store.destination
return chooseXRDAmount(with: destinationStore)
.needsToPayFeeFromOtherAccount(with: destinationStore)
}

private func chooseXRDAmount(with destinationStore: PresentationStoreOf<FungibleResourceAsset.Destination>) -> some View {
alert(
store: destinationStore,
state: /FungibleResourceAsset.Destination.State.chooseXRDAmount,
action: FungibleResourceAsset.Destination.Action.chooseXRDAmount
)
}

private func needsToPayFeeFromOtherAccount(with destinationStore: PresentationStoreOf<FungibleResourceAsset.Destination>) -> some View {
alert(
store: destinationStore,
state: /FungibleResourceAsset.Destination.State.needsToPayFeeFromOtherAccount,
action: FungibleResourceAsset.Destination.Action.needsToPayFeeFromOtherAccount
)
}
}
Loading

0 comments on commit 57824ff

Please sign in to comment.