diff --git a/RadixWallet/Features/TransactionReviewFeature/TransactionReviewNetworkFee/TransactionReviewNetworkFee+View.swift b/RadixWallet/Features/TransactionReviewFeature/TransactionReviewNetworkFee/TransactionReviewNetworkFee+View.swift index 71b2a236c3..692ec8e4a8 100644 --- a/RadixWallet/Features/TransactionReviewFeature/TransactionReviewNetworkFee/TransactionReviewNetworkFee+View.swift +++ b/RadixWallet/Features/TransactionReviewFeature/TransactionReviewNetworkFee/TransactionReviewNetworkFee+View.swift @@ -3,7 +3,7 @@ import SwiftUI extension TransactionReviewNetworkFee.State { var displayedTotalFee: String { - "\(reviewedTransaction.transactionFee.totalFee.displayedTotalFee) XRD" + L10n.TransactionReview.xrdAmount(reviewedTransaction.transactionFee.totalFee.displayedTotalFee) } } @@ -19,8 +19,8 @@ extension TransactionReviewNetworkFee { public var body: some SwiftUI.View { WithViewStore(store, observe: { $0 }, send: { .view($0) }) { viewStore in - VStack(alignment: .leading, spacing: .small2) { - HStack { + VStack(alignment: .leading, spacing: .zero) { + HStack(alignment: .top) { Text(L10n.TransactionReview.NetworkFee.heading) .sectionHeading .textCase(.uppercase) @@ -32,9 +32,19 @@ extension TransactionReviewNetworkFee { Spacer(minLength: 0) - Text(viewStore.displayedTotalFee) - .textStyle(.body1HighImportance) - .foregroundColor(.app.gray1) + VStack(alignment: .trailing, spacing: .small3) { + Text(viewStore.displayedTotalFee) + .textStyle(.body1HighImportance) + .foregroundColor(.app.gray1) + + loadable(viewStore.fiatValue) { + ProgressView() + } successContent: { value in + Text(value) + .textStyle(.body2HighImportance) + .foregroundColor(.app.gray2) + } + } } loadable(viewStore.reviewedTransaction.feePayingValidation) { validation in @@ -56,6 +66,9 @@ extension TransactionReviewNetworkFee { .foregroundColor(.app.blue2) } } + .task { + viewStore.send(.task) + } } } } diff --git a/RadixWallet/Features/TransactionReviewFeature/TransactionReviewNetworkFee/TransactionReviewNetworkFee.swift b/RadixWallet/Features/TransactionReviewFeature/TransactionReviewNetworkFee/TransactionReviewNetworkFee.swift index 24512135ac..5e56c57d3b 100644 --- a/RadixWallet/Features/TransactionReviewFeature/TransactionReviewNetworkFee/TransactionReviewNetworkFee.swift +++ b/RadixWallet/Features/TransactionReviewFeature/TransactionReviewNetworkFee/TransactionReviewNetworkFee.swift @@ -5,6 +5,7 @@ import SwiftUI public struct TransactionReviewNetworkFee: Sendable, FeatureReducer { public struct State: Sendable, Hashable { public var reviewedTransaction: ReviewedTransaction + public var fiatValue: Loadable = .idle public init( reviewedTransaction: ReviewedTransaction @@ -14,22 +15,90 @@ public struct TransactionReviewNetworkFee: Sendable, FeatureReducer { } public enum ViewAction: Sendable, Equatable { + case task case infoTapped case customizeTapped } + public enum InternalAction: Sendable, Equatable { + case setTokenPrices(TaskResult) + } + public enum DelegateAction: Sendable, Equatable { case showCustomizeFees } + public struct PriceResult: Sendable, Equatable { + let prices: TokenPricesClient.TokenPrices + let currency: FiatCurrency + } + + @Dependency(\.appPreferencesClient) var appPreferencesClient + @Dependency(\.tokenPricesClient) var tokenPricesClient + @Dependency(\.errorQueue) var errorQueue + public init() {} public func reduce(into state: inout State, viewAction: ViewAction) -> Effect { switch viewAction { + case .task: + state.fiatValue = .loading + return .run { send in + let currency = await appPreferencesClient.getPreferences().display.fiatCurrencyPriceTarget + let result = await TaskResult { + let prices = try await tokenPricesClient.getTokenPrices(.init(tokens: [.mainnetXRD], currency: currency), false) + return PriceResult(prices: prices, currency: currency) + } + await send(.internal(.setTokenPrices(result))) + } case .infoTapped: - .none + return .none case .customizeTapped: - .send(.delegate(.showCustomizeFees)) + return .send(.delegate(.showCustomizeFees)) + } + } + + public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { + switch internalAction { + case let .setTokenPrices(.failure(error)): + loggerGlobal.error("TransactionReviewNetworkFee failed to fetch XRD price, error: \(error)") + state.fiatValue = .failure(error) + return .none + + case let .setTokenPrices(.success(result)): + guard let price = result.prices[.mainnetXRD] else { + loggerGlobal.error("TransactionReviewNetworkFee didn't get XRD price on response") + state.fiatValue = .failure(MissingXrdPriceError()) + return .none + } + state.fiatValue = .success(state.reviewedTransaction.transactionFee.totalFee.fiatValue(xrdPrice: price, currency: result.currency)) + return .none } } + + private struct MissingXrdPriceError: Error {} +} + +private extension TransactionFee.TotalFee { + func fiatValue(xrdPrice: Decimal192, currency: FiatCurrency) -> String { + let formatter = Self.feePriceFormatter + formatter.currencyCode = currency.currencyCode + + let maxPrice = max * xrdPrice + let maxValue = formatter.string(for: maxPrice.asDouble) ?? maxPrice.formatted() + guard max > min else { + return maxValue + } + + let minPrice = min * xrdPrice + let minValue = formatter.string(for: minPrice.asDouble) ?? minPrice.formatted() + return "\(minValue) - \(maxValue)" + } + + private static let feePriceFormatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .currency + formatter.maximumSignificantDigits = 3 + return formatter + }() }