diff --git a/Package.swift b/Package.swift index 0fa2c48797..5c70f1ac23 100644 --- a/Package.swift +++ b/Package.swift @@ -53,7 +53,6 @@ package.addModules([ "MainFeature", "OnboardingFeature", "SplashFeature", - "TransactionReviewFeature", ], tests: .yes() ), diff --git a/Sources/Clients/EngineToolkitClient/EngineToolkitClient+Test.swift b/Sources/Clients/EngineToolkitClient/EngineToolkitClient+Test.swift index d5d2526f89..4add55734d 100644 --- a/Sources/Clients/EngineToolkitClient/EngineToolkitClient+Test.swift +++ b/Sources/Clients/EngineToolkitClient/EngineToolkitClient+Test.swift @@ -19,8 +19,8 @@ extension EngineToolkitClient: TestDependencyKey { accountAddressesNeedingToSignTransaction: { _ in [] }, accountAddressesSuitableToPayTransactionFee: { _ in [] }, knownEntityAddresses: { _ in throw NoopError() }, - generateTransactionReview: unimplemented("\(Self.self).generateTransactionReview"), - decodeAddress: unimplemented("\(Self.self).decodeAddress") + generateTransactionReview: { _ in throw NoopError() }, + decodeAddress: { _ in throw NoopError() } ) public static let testValue = Self( diff --git a/Sources/Clients/GatewayAPI/GatewayAPIClient/GatewayAPIClient+Interface.swift b/Sources/Clients/GatewayAPI/GatewayAPIClient/GatewayAPIClient+Interface.swift index 6bcf328915..eb9cc3dd4a 100644 --- a/Sources/Clients/GatewayAPI/GatewayAPIClient/GatewayAPIClient+Interface.swift +++ b/Sources/Clients/GatewayAPI/GatewayAPIClient/GatewayAPIClient+Interface.swift @@ -43,3 +43,95 @@ extension GatewayAPIClient { public typealias TransactionPreview = @Sendable (GatewayAPI.TransactionPreviewRequest) async throws -> GatewayAPI.TransactionPreviewResponse } + +extension GatewayAPIClient { + public func getDappDefinition(_ address: String) async throws -> GatewayAPI.EntityMetadataCollection { + let entityMetadata = try await getEntityMetadata(address) + + guard let dappDefinitionAddress = entityMetadata.dappDefinition else { + throw GatewayAPI.EntityMetadataCollection.MetadataError.missingDappDefinition + } + + let dappDefinition = try await getEntityMetadata(dappDefinitionAddress) + + guard dappDefinition.accountType == .dappDefinition else { + throw GatewayAPI.EntityMetadataCollection.MetadataError.accountTypeNotDappDefinition + } + + guard let claimedEntities = dappDefinition.claimedEntities else { + throw GatewayAPI.EntityMetadataCollection.MetadataError.missingClaimedEntities + } + + guard claimedEntities.contains(address) else { + throw GatewayAPI.EntityMetadataCollection.MetadataError.entityNotClaimed + } + + return dappDefinition + } +} + +extension GatewayAPI.EntityMetadataCollection { + public var description: String? { + self["description"]?.asString + } + + public var symbol: String? { + self["symbol"]?.asString + } + + public var name: String? { + self["name"]?.asString + } + + public var domain: String? { + self["domain"]?.asString + } + + public var url: String? { + self["url"]?.asString + } + + public var dappDefinition: String? { + self["dapp_definition"]?.asString + } + + public var claimedEntities: [String]? { + self["claimed_entities"]?.asStringCollection + } + + public var claimedWebsites: [String]? { + self["claimed_websites"]?.asStringCollection + } + + public var accountType: AccountType? { + self["account_type"]?.asString.flatMap(AccountType.init) + } + + public subscript(key: String) -> GatewayAPI.EntityMetadataItemValue? { + items.first { $0.key == key }?.value + } + + public enum AccountType: String { + case dappDefinition = "dapp definition" + } + + public enum MetadataError: Error, CustomStringConvertible { + case missingDappDefinition + case accountTypeNotDappDefinition + case missingClaimedEntities + case entityNotClaimed + + public var description: String { + switch self { + case .missingDappDefinition: + return "The entity has no dApp definition address" + case .accountTypeNotDappDefinition: + return "The account is not of the type `dapp definition`" + case .missingClaimedEntities: + return "The dapp definition has no claimed_entities key" + case .entityNotClaimed: + return "The entity is not claimed by the dapp definition" + } + } + } +} diff --git a/Sources/Core/DesignSystem/Components/Card.swift b/Sources/Core/DesignSystem/Components/Card.swift index c746e2ba53..74d9a71951 100644 --- a/Sources/Core/DesignSystem/Components/Card.swift +++ b/Sources/Core/DesignSystem/Components/Card.swift @@ -84,8 +84,8 @@ extension View { // MARK: - SpeechbubbleShape public struct SpeechbubbleShape: Shape { let cornerRadius: CGFloat - public static let triangleSize: CGSize = .init(width: 20, height: 10) // TODO:  constant - public static let triangleInset: CGFloat = 50 // TODO:  constant + public static let triangleSize: CGSize = .init(width: 20, height: 10) + public static let triangleInset: CGFloat = 50 public init(cornerRadius: CGFloat) { self.cornerRadius = cornerRadius diff --git a/Sources/Core/DesignSystem/Fonts.swift b/Sources/Core/DesignSystem/Fonts.swift index 62114980f5..2b73a70b49 100644 --- a/Sources/Core/DesignSystem/Fonts.swift +++ b/Sources/Core/DesignSystem/Fonts.swift @@ -63,6 +63,10 @@ extension SwiftUI.Font.App { public var button: SwiftUI.Font { .custom(FontFamily.IBMPlexSans.bold, size: 16) } + + public var monospace: SwiftUI.Font { + .system(size: 13, design: .monospaced) + } } /// UIFont/NSFont depending on platform. diff --git a/Sources/Core/DesignSystem/Grid.swift b/Sources/Core/DesignSystem/Grid.swift index 4206f7a9f3..000a71db17 100644 --- a/Sources/Core/DesignSystem/Grid.swift +++ b/Sources/Core/DesignSystem/Grid.swift @@ -46,7 +46,7 @@ extension CGFloat { public static let standardButtonHeight: Self = 50 /// 32 - public static let toolbatButtonHeight: Self = 32 + public static let toolbarButtonHeight: Self = 32 /// 250 public static let standardButtonWidth: Self = 250 diff --git a/Sources/Core/DesignSystem/Styles/SecondaryRectangularButtonStyle.swift b/Sources/Core/DesignSystem/Styles/SecondaryRectangularButtonStyle.swift index b7cc4ca0c4..589816d6bf 100644 --- a/Sources/Core/DesignSystem/Styles/SecondaryRectangularButtonStyle.swift +++ b/Sources/Core/DesignSystem/Styles/SecondaryRectangularButtonStyle.swift @@ -16,7 +16,7 @@ public struct SecondaryRectangularButtonStyle: ButtonStyle { } .foregroundColor(foregroundColor) .font(.app.body1Header) - .frame(height: isInToolbar ? .toolbatButtonHeight : .standardButtonHeight) + .frame(height: isInToolbar ? .toolbarButtonHeight : .standardButtonHeight) .frame(maxWidth: shouldExpand ? .infinity : nil) .padding(.horizontal, isInToolbar ? .small1 : .medium1) .background(.app.gray4) diff --git a/Sources/Core/DesignSystem/ViewModifiers/TextStyleModifier.swift b/Sources/Core/DesignSystem/ViewModifiers/TextStyleModifier.swift index 88b1e947d2..26529ea76f 100644 --- a/Sources/Core/DesignSystem/ViewModifiers/TextStyleModifier.swift +++ b/Sources/Core/DesignSystem/ViewModifiers/TextStyleModifier.swift @@ -15,159 +15,47 @@ public enum TextStyle { case body2Regular case body2Link case button -} - -extension View { - @ViewBuilder public func textStyle(_ style: TextStyle) -> some View { - switch style { - case .sheetTitle: - modifier(TextStyle.SheetTitle()) - case .sectionHeader: - modifier(TextStyle.SectionHeader()) - case .secondaryHeader: - modifier(TextStyle.SecondaryHeader()) - case .body1Header: - modifier(TextStyle.Body1Header()) - case .body1HighImportance: - modifier(TextStyle.Body1HighImportance()) - case .body1Regular: - modifier(TextStyle.Body1Regular()) - case .body1StandaloneLink: - modifier(TextStyle.Body1StandaloneLink()) - case .body1Link: - modifier(TextStyle.Body1Link()) - case .body2Header: - modifier(TextStyle.Body2Header()) - case .body2HighImportance: - modifier(TextStyle.Body2HighImportance()) - case .body2Regular: - modifier(TextStyle.Body2Regular()) - case .body2Link: - modifier(TextStyle.Body2Link()) - case .button: - modifier(TextStyle.Button()) - } - } + case monospace } extension TextStyle { - fileprivate struct SheetTitle: ViewModifier { - func body(content: Content) -> some View { - content - .font(.app.sheetTitle) - .lineSpacing(.lineSpacing(.𝟛𝟞)) - } - } - - fileprivate struct SectionHeader: ViewModifier { - func body(content: Content) -> some View { - content - .font(.app.sectionHeader) - .lineSpacing(.lineSpacing(.𝟚𝟛)) - } - } - - fileprivate struct SecondaryHeader: ViewModifier { - func body(content: Content) -> some View { - content - .font(.app.secondaryHeader) - .lineSpacing(.lineSpacing(.𝟚𝟛)) - } - } - - fileprivate struct Body1Header: ViewModifier { - func body(content: Content) -> some View { - content - .font(.app.body1Header) - .lineSpacing(.lineSpacing(.𝟚𝟛)) - } - } - - fileprivate struct Body1HighImportance: ViewModifier { - func body(content: Content) -> some View { - content - .font(.app.body1HighImportance) - .lineSpacing(.lineSpacing(.𝟚𝟛)) - } - } - - fileprivate struct Body1Regular: ViewModifier { - func body(content: Content) -> some View { - content - .font(.app.body1Regular) - .lineSpacing(.lineSpacing(.𝟚𝟛)) - } - } - - fileprivate struct Body1StandaloneLink: ViewModifier { - func body(content: Content) -> some View { - content - .font(.app.body1StandaloneLink) - .lineSpacing(.lineSpacing(.𝟚𝟛)) - } - } - - fileprivate struct Body1Link: ViewModifier { - func body(content: Content) -> some View { - content - .font(.app.body1Link) - .lineSpacing(.lineSpacing(.𝟚𝟛)) - } - } - - fileprivate struct Body2Header: ViewModifier { - func body(content: Content) -> some View { - content - .font(.app.body2Header) - .lineSpacing(.lineSpacing(.𝟙𝟠)) - } - } - - fileprivate struct Body2HighImportance: ViewModifier { - func body(content: Content) -> some View { - content - .font(.app.body2HighImportance) - .lineSpacing(.lineSpacing(.𝟙𝟠)) - } - } - - fileprivate struct Body2Regular: ViewModifier { - func body(content: Content) -> some View { - content - .font(.app.body2Regular) - .lineSpacing(.lineSpacing(.𝟙𝟠)) - } - } - - fileprivate struct Body2Link: ViewModifier { - func body(content: Content) -> some View { - content - .font(.app.body2Link) - .lineSpacing(.lineSpacing(.𝟙𝟠)) - } - } - - fileprivate struct Button: ViewModifier { - func body(content: Content) -> some View { - content - .font(.app.button) - .lineSpacing(.lineSpacing(.𝟙𝟠)) + var font: Font { + switch self { + case .sheetTitle: return .app.sheetTitle + case .sectionHeader: return .app.sectionHeader + case .secondaryHeader: return .app.secondaryHeader + case .body1Header: return .app.body1Header + case .body1HighImportance: return .app.body1HighImportance + case .body1Regular: return .app.body1Regular + case .body1StandaloneLink: return .app.body1StandaloneLink + case .body1Link: return .app.body1Link + case .body2Header: return .app.body2Header + case .body2HighImportance: return .app.body2HighImportance + case .body2Regular: return .app.body2Regular + case .body2Link: return .app.body2Link + case .button: return .app.button + case .monospace: return .app.monospace + } + } + + var lineSpacing: CGFloat { + switch self { + case .sheetTitle: + return 36 / 4 + case .sectionHeader, .secondaryHeader, .body1Header, + .body1HighImportance, .body1Regular, .body1StandaloneLink, .body1Link: + return 23 / 4 + case .body2Header, .body2HighImportance, .body2Regular, + .body2Link, .button, .monospace: + return 18 / 4 } } } -extension CGFloat { - fileprivate static func lineSpacing(_ value: LineSpacing) -> CGFloat { - value.rawValue / 4 - } -} - -// MARK: - CGFloat.LineSpacing -extension CGFloat { - fileprivate enum LineSpacing: CGFloat { - case 𝟛𝟞 = 36 - case 𝟚𝟛 = 23 - case 𝟙𝟠 = 18 +extension View { + public func textStyle(_ style: TextStyle) -> some View { + font(style.font) + .lineSpacing(style.lineSpacing) } } diff --git a/Sources/Core/Resources/Generated/L10n.generated.swift b/Sources/Core/Resources/Generated/L10n.generated.swift index cd4b43d066..cb3ab0fb00 100644 --- a/Sources/Core/Resources/Generated/L10n.generated.swift +++ b/Sources/Core/Resources/Generated/L10n.generated.swift @@ -776,7 +776,7 @@ public enum L10n { /// Customize Guarantees public static let customizeGuaranteesButtonTitle = L10n.tr("Localizable", "transactionReview.customizeGuaranteesButtonTitle", fallback: "Customize Guarantees") /// Depositing - public static let depositingHeading = L10n.tr("Localizable", "transactionReview.depositingHeading", fallback: "Depositing") + public static let depositsHeading = L10n.tr("Localizable", "transactionReview.depositsHeading", fallback: "Depositing") /// Estimated public static let estimated = L10n.tr("Localizable", "transactionReview.estimated", fallback: "Estimated") /// Account @@ -793,10 +793,12 @@ public enum L10n { public static let sendingToHeading = L10n.tr("Localizable", "transactionReview.sendingToHeading", fallback: "Sending to") /// Review Transaction public static let title = L10n.tr("Localizable", "transactionReview.title", fallback: "Review Transaction") + /// Unknown + public static let unknown = L10n.tr("Localizable", "transactionReview.unknown", fallback: "Unknown") /// Using dApps public static let usingDappsHeading = L10n.tr("Localizable", "transactionReview.usingDappsHeading", fallback: "Using dApps") /// Withdrawing - public static let withdrawingHeading = L10n.tr("Localizable", "transactionReview.withdrawingHeading", fallback: "Withdrawing") + public static let withdrawalsHeading = L10n.tr("Localizable", "transactionReview.withdrawalsHeading", fallback: "Withdrawing") public enum Guarantees { /// Apply public static let applyButtonText = L10n.tr("Localizable", "transactionReview.guarantees.applyButtonText", fallback: "Apply") diff --git a/Sources/Core/Resources/Resources/en.lproj/Localizable.strings b/Sources/Core/Resources/Resources/en.lproj/Localizable.strings index cb001a2514..3b1161dd51 100644 --- a/Sources/Core/Resources/Resources/en.lproj/Localizable.strings +++ b/Sources/Core/Resources/Resources/en.lproj/Localizable.strings @@ -151,10 +151,11 @@ "transactionReview.title" = "Review Transaction"; +"transactionReview.unknown" = "Unknown"; "transactionReview.messageHeading" = "Message"; "transactionReview.usingDappsHeading" = "Using dApps"; -"transactionReview.withdrawingHeading" = "Withdrawing"; -"transactionReview.depositingHeading" = "Depositing"; +"transactionReview.withdrawalsHeading" = "Withdrawing"; +"transactionReview.depositsHeading" = "Depositing"; "transactionReview.sendingToHeading" = "Sending to"; "transactionReview.presentingHeading" = "Presenting"; "transactionReview.customizeGuaranteesButtonTitle" = "Customize Guarantees"; diff --git a/Sources/EngineToolkit/EngineToolkitModels/Models/Instruction/RemoveMetadata.swift b/Sources/EngineToolkit/EngineToolkitModels/Models/Instruction/RemoveMetadata.swift index 366d86617a..417523b418 100644 --- a/Sources/EngineToolkit/EngineToolkitModels/Models/Instruction/RemoveMetadata.swift +++ b/Sources/EngineToolkit/EngineToolkitModels/Models/Instruction/RemoveMetadata.swift @@ -10,7 +10,7 @@ public struct RemoveMetadata: InstructionProtocol { // MARK: Stored properties - public let entityAddress: Address_ // TODO:  What should this actually be? + public let entityAddress: Address_ public let key: String // MARK: Init diff --git a/Sources/EngineToolkit/EngineToolkitModels/Models/Instruction/SetMetadata.swift b/Sources/EngineToolkit/EngineToolkitModels/Models/Instruction/SetMetadata.swift index f84b774f6d..d01a01f18e 100644 --- a/Sources/EngineToolkit/EngineToolkitModels/Models/Instruction/SetMetadata.swift +++ b/Sources/EngineToolkit/EngineToolkitModels/Models/Instruction/SetMetadata.swift @@ -10,7 +10,7 @@ public struct SetMetadata: InstructionProtocol { // MARK: Stored properties - public let entityAddress: Address_ // TODO:  What should this actually be? + public let entityAddress: Address_ public let key: String public let value: Enum diff --git a/Sources/EngineToolkit/EngineToolkitModels/Models/Instruction/SetMethodAccessRule.swift b/Sources/EngineToolkit/EngineToolkitModels/Models/Instruction/SetMethodAccessRule.swift index 7cb3010d20..e4c26cf4fe 100644 --- a/Sources/EngineToolkit/EngineToolkitModels/Models/Instruction/SetMethodAccessRule.swift +++ b/Sources/EngineToolkit/EngineToolkitModels/Models/Instruction/SetMethodAccessRule.swift @@ -10,7 +10,7 @@ public struct SetMethodAccessRule: InstructionProtocol { // MARK: Stored properties - public let entityAddress: Address_ // TODO:  What should this actually be? + public let entityAddress: Address_ public let key: Tuple public let rule: Enum diff --git a/Sources/Features/AppFeature/App+View.swift b/Sources/Features/AppFeature/App+View.swift index 6f2324c68f..5725b92a35 100644 --- a/Sources/Features/AppFeature/App+View.swift +++ b/Sources/Features/AppFeature/App+View.swift @@ -2,7 +2,6 @@ import FeaturePrelude import MainFeature import OnboardingFeature import SplashFeature -import TransactionReviewFeature // MARK: - App.View extension App { diff --git a/Sources/Features/AuthorizedDAppsFeatures/DappDetails/DappDetails+View.swift b/Sources/Features/AuthorizedDAppsFeatures/DappDetails/DappDetails+View.swift index f16b336c73..3b1ca86975 100644 --- a/Sources/Features/AuthorizedDAppsFeatures/DappDetails/DappDetails+View.swift +++ b/Sources/Features/AuthorizedDAppsFeatures/DappDetails/DappDetails+View.swift @@ -115,7 +115,7 @@ private extension DappDetails.State { return .init( title: dApp.displayName.rawValue, description: metadata?.description ?? L10n.DAppDetails.missingDescription, - domain: metadata?["domain"], + domain: metadata?.domain, addressViewState: .init(address: dApp.dAppDefinitionAddress.address, format: .default), otherMetadata: otherMetadata, fungibleTokens: [], // TODO: Populate when we have it diff --git a/Sources/Features/AuthorizedDAppsFeatures/DappDetails/DappDetails.swift b/Sources/Features/AuthorizedDAppsFeatures/DappDetails/DappDetails.swift index a3d28dafa2..b0c0e19f79 100644 --- a/Sources/Features/AuthorizedDAppsFeatures/DappDetails/DappDetails.swift +++ b/Sources/Features/AuthorizedDAppsFeatures/DappDetails/DappDetails.swift @@ -184,30 +184,6 @@ public struct DappDetails: Sendable, FeatureReducer { } } -// MARK: - Extensions - -extension GatewayAPI.EntityMetadataCollection { - var description: String? { - self["description"] - } - - var symbol: String? { - self["symbol"] - } - - var name: String? { - self["name"] - } - - var url: String? { - self["url"] - } - - subscript(key: String) -> String? { - items.first { $0.key == key }?.value.asString - } -} - extension AlertState { static var confirmDisconnect: AlertState { AlertState { diff --git a/Sources/Features/TransactionReviewFeature/TransactionReview+View.swift b/Sources/Features/TransactionReviewFeature/TransactionReview+View.swift index 933bb89616..e77f62fa75 100644 --- a/Sources/Features/TransactionReviewFeature/TransactionReview+View.swift +++ b/Sources/Features/TransactionReviewFeature/TransactionReview+View.swift @@ -1,4 +1,3 @@ -import ComposableArchitecture import FeaturePrelude import Profile @@ -19,8 +18,9 @@ extension TransactionReview.State { .init( message: message, isExpandedDappUsed: dAppsUsed?.isExpanded == true, - showDepositingHeading: depositing != nil, - viewControlState: viewControlState + showDepositsHeading: deposits != nil, + viewControlState: viewControlState, + rawTransaction: displayMode.rawTransaction ) } @@ -40,8 +40,9 @@ extension TransactionReview { public struct ViewState: Equatable { let message: String? let isExpandedDappUsed: Bool - let showDepositingHeading: Bool + let showDepositsHeading: Bool let viewControlState: ControlState + let rawTransaction: String? } @MainActor @@ -58,49 +59,35 @@ extension TransactionReview { VStack(spacing: 0) { FixedSpacer(height: .medium2) - if let message = viewStore.message { - TransactionHeading(L10n.TransactionReview.messageHeading) - .padding(.bottom, .small2) - TransactionMessageView(message: message) - } + if let rawTransaction = viewStore.rawTransaction { + RawTransactionView(transaction: rawTransaction) + .padding(.bottom, .medium3) + } else { + VStack(spacing: 0) { + messageSection(with: viewStore.message) - let withdrawingStore = store.scope(state: \.withdrawing) { .child(.withdrawing($0)) } - IfLetStore(withdrawingStore) { withdrawingStore in - TransactionHeading(L10n.TransactionReview.withdrawingHeading) - .padding(.top, .medium2) - .padding(.bottom, .small2) - TransactionReviewAccounts.View(store: withdrawingStore) - } + withdrawalsSection - usingDappsSection(expanded: viewStore.isExpandedDappUsed, showDepositingHeading: viewStore.showDepositingHeading) + usingDappsSection(expanded: viewStore.isExpandedDappUsed, showDepositsHeading: viewStore.showDepositsHeading) - let depositingStore = store.scope(state: \.depositing) { .child(.depositing($0)) } - IfLetStore(depositingStore) { depositingStore in - TransactionReviewAccounts.View(store: depositingStore) - .padding(.bottom, .medium1) - } + depositsSection - Separator() - .padding(.bottom, .medium1) + Separator() + .padding(.bottom, .medium1) - let presentingStore = store.scope(state: \.presenting) { .child(.presenting($0)) } - IfLetStore(presentingStore) { childStore in - TransactionReviewPresenting.View(store: childStore) - - Separator() - .padding(.bottom, .medium1) - } + proofsSection - let feeStore = store.scope(state: \.networkFee) { .child(.networkFee($0)) } - IfLetStore(feeStore) { feeStore in - TransactionReviewNetworkFee.View(store: feeStore) + feeSection + } } Button(L10n.TransactionReview.approveButtonTitle, asset: AssetResource.lock) { viewStore.send(.approveTapped) } .buttonStyle(.primaryRectangular) + .padding(.bottom, .medium1) } + .animation(.easeInOut, value: viewStore.rawTransaction) .padding(.horizontal, .medium3) } .background(.app.gray5) @@ -112,14 +99,12 @@ extension TransactionReview { viewStore.send(.showRawTransactionTapped) } .buttonStyle(.secondaryRectangular(isInToolbar: true)) + .brightness(viewStore.rawTransaction == nil ? 0 : -0.15) } } .sheet(store: store.scope(state: \.$customizeGuarantees) { .child(.customizeGuarantees($0)) }) { childStore in TransactionReviewGuarantees.View(store: childStore) } - .sheet(store: store.scope(state: \.$rawTransaction) { .child(.rawTransaction($0)) }) { childStore in - TransactionReviewRawTransaction.View(store: childStore) - } .controlState(viewStore.viewControlState) .onAppear { viewStore.send(.appeared) @@ -127,7 +112,30 @@ extension TransactionReview { } } - private func usingDappsSection(expanded: Bool, showDepositingHeading: Bool) -> some SwiftUI.View { + @ViewBuilder + private func messageSection(with message: String?) -> some SwiftUI.View { + if let message { + TransactionHeading(L10n.TransactionReview.messageHeading) + .padding(.bottom, .small2) + + TransactionMessageView(message: message) + } + } + + @ViewBuilder + private var withdrawalsSection: some SwiftUI.View { + let withdrawalsStore = store.scope(state: \.withdrawals) { .child(.withdrawals($0)) } + IfLetStore(withdrawalsStore) { childStore in + TransactionHeading(L10n.TransactionReview.withdrawalsHeading) + .padding(.top, .medium2) + .padding(.bottom, .small2) + + TransactionReviewAccounts.View(store: childStore) + } + } + + @ViewBuilder + private func usingDappsSection(expanded: Bool, showDepositsHeading: Bool) -> some SwiftUI.View { VStack(alignment: .trailing, spacing: .medium2) { let usedDappsStore = store.scope(state: \.dAppsUsed) { .child(.dAppsUsed($0)) } IfLetStore(usedDappsStore) { childStore in @@ -137,8 +145,8 @@ extension TransactionReview { FixedSpacer(height: .medium2) } - if showDepositingHeading { - TransactionHeading(L10n.TransactionReview.depositingHeading) + if showDepositsHeading { + TransactionHeading(L10n.TransactionReview.depositsHeading) .padding(.bottom, .small2) } } @@ -149,6 +157,34 @@ extension TransactionReview { .padding(.trailing, SpeechbubbleShape.triangleInset) } } + + @ViewBuilder + private var depositsSection: some SwiftUI.View { + let depositsStore = store.scope(state: \.deposits) { .child(.deposits($0)) } + IfLetStore(depositsStore) { childStore in + TransactionReviewAccounts.View(store: childStore) + .padding(.bottom, .medium1) + } + } + + @ViewBuilder + private var proofsSection: some SwiftUI.View { + let proofsStore = store.scope(state: \.proofs) { .child(.proofs($0)) } + IfLetStore(proofsStore) { childStore in + TransactionReviewProofs.View(store: childStore) + + Separator() + .padding(.bottom, .medium1) + } + } + + @ViewBuilder + private var feeSection: some SwiftUI.View { + let feeStore = store.scope(state: \.networkFee) { .child(.networkFee($0)) } + IfLetStore(feeStore) { childStore in + TransactionReviewNetworkFee.View(store: childStore) + } + } } } @@ -162,34 +198,6 @@ struct VLine: Shape { } } -// MARK: - TransactionPresentingView -struct TransactionPresentingView: View { - let presenters: IdentifiedArrayOf - let tapPresenterAction: (TransactionReview.Dapp.ID) -> Void - - var body: some View { - Card { - List(presenters) { presenter in - Button { - tapPresenterAction(presenter.id) - } label: { - HStack(spacing: 0) { - DappPlaceholder(size: .smallest) - if let name = presenter.metadata?.name { - Text(name) - .textStyle(.body1HighImportance) - .foregroundColor(.app.gray1) - .padding(.leading, .small1) - } - Spacer(minLength: 0) - } - } - .padding(.horizontal, .large2) - } - } - } -} - // MARK: - TransactionHeading struct TransactionHeading: View { let heading: String @@ -221,6 +229,24 @@ struct TransactionMessageView: View { } } +// MARK: - RawTransactionView +struct RawTransactionView: SwiftUI.View { + let transaction: String + + var body: some SwiftUI.View { + Text(transaction) + .textStyle(.monospace) + .foregroundColor(.app.gray1) + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: .topLeading + ) + .padding() + .multilineTextAlignment(.leading) + } +} + // MARK: - TransactionReviewTokenView struct TransactionReviewTokenView: View { struct ViewState: Equatable { @@ -256,7 +282,7 @@ struct TransactionReviewTokenView: View { HStack(spacing: .small2) { if viewState.guaranteedAmount != nil { Text(L10n.TransactionReview.estimated) - .textStyle(.body2Regular) // TODO:  unknown textStyle + .textStyle(.body2HighImportance) .foregroundColor(.app.gray1) } Text(viewState.amount.format()) diff --git a/Sources/Features/TransactionReviewFeature/TransactionReview.swift b/Sources/Features/TransactionReviewFeature/TransactionReview.swift index 9cf6a63ae3..bb484442fe 100644 --- a/Sources/Features/TransactionReviewFeature/TransactionReview.swift +++ b/Sources/Features/TransactionReviewFeature/TransactionReview.swift @@ -6,23 +6,22 @@ import TransactionClient // MARK: - TransactionReview public struct TransactionReview: Sendable, FeatureReducer { public struct State: Sendable, Hashable { + public var displayMode: DisplayMode = .review + public let transactionManifest: TransactionManifest public let message: String? public var transactionWithLockFee: TransactionManifest? - public var withdrawing: TransactionReviewAccounts.State? = nil + public var withdrawals: TransactionReviewAccounts.State? = nil public var dAppsUsed: TransactionReviewDappsUsed.State? = nil - public var depositing: TransactionReviewAccounts.State? = nil - public var presenting: TransactionReviewPresenting.State? = nil + public var deposits: TransactionReviewAccounts.State? = nil + public var proofs: TransactionReviewProofs.State? = nil public var networkFee: TransactionReviewNetworkFee.State? = nil @PresentationState public var customizeGuarantees: TransactionReviewGuarantees.State? = nil - @PresentationState - public var rawTransaction: TransactionReviewRawTransaction.State? = nil - public var isProcessingTransaction: Bool = false public init( @@ -34,6 +33,16 @@ public struct TransactionReview: Sendable, FeatureReducer { self.message = message self.customizeGuarantees = customizeGuarantees } + + public enum DisplayMode: Sendable, Hashable { + case review + case raw(String) + + var rawTransaction: String? { + guard case let .raw(transaction) = self else { return nil } + return transaction + } + } } public enum ViewAction: Sendable, Equatable { @@ -45,14 +54,13 @@ public struct TransactionReview: Sendable, FeatureReducer { } public enum ChildAction: Sendable, Equatable { - case withdrawing(TransactionReviewAccounts.Action) - case depositing(TransactionReviewAccounts.Action) + case withdrawals(TransactionReviewAccounts.Action) + case deposits(TransactionReviewAccounts.Action) case dAppsUsed(TransactionReviewDappsUsed.Action) - case presenting(TransactionReviewPresenting.Action) + case proofs(TransactionReviewProofs.Action) case networkFee(TransactionReviewNetworkFee.Action) case customizeGuarantees(PresentationAction) - case rawTransaction(PresentationAction) } public enum InternalAction: Sendable, Equatable { @@ -81,21 +89,21 @@ public struct TransactionReview: Sendable, FeatureReducer { .ifLet(\.networkFee, action: /Action.child .. ChildAction.networkFee) { TransactionReviewNetworkFee() } - .ifLet(\.depositing, action: /Action.child .. ChildAction.depositing) { + .ifLet(\.deposits, action: /Action.child .. ChildAction.deposits) { TransactionReviewAccounts() } .ifLet(\.dAppsUsed, action: /Action.child .. ChildAction.dAppsUsed) { TransactionReviewDappsUsed() } - .ifLet(\.withdrawing, action: /Action.child .. ChildAction.withdrawing) { + .ifLet(\.withdrawals, action: /Action.child .. ChildAction.withdrawals) { TransactionReviewAccounts() } + .ifLet(\.proofs, action: /Action.child .. ChildAction.proofs) { + TransactionReviewProofs() + } .ifLet(\.$customizeGuarantees, action: /Action.child .. ChildAction.customizeGuarantees) { TransactionReviewGuarantees() } - .ifLet(\.$rawTransaction, action: /Action.child .. ChildAction.rawTransaction) { - TransactionReviewRawTransaction() - } } public func reduce(into state: inout State, viewAction: ViewAction) -> EffectTask { @@ -111,11 +119,18 @@ public struct TransactionReview: Sendable, FeatureReducer { return .none case .showRawTransactionTapped: - guard let transactionWithLockFee = state.transactionWithLockFee else { return .none } - let guarantees = state.allGuarantees - return .run { send in - let manifest = try await addingGuarantees(to: transactionWithLockFee, guarantees: guarantees) - await send(.internal(.rawTransactionCreated(manifest.description))) + switch state.displayMode { + case .review: + guard let transactionWithLockFee = state.transactionWithLockFee else { return .none } + let guarantees = state.allGuarantees + return .run { send in + let manifest = try await addingGuarantees(to: transactionWithLockFee, guarantees: guarantees) + await send(.internal(.rawTransactionCreated(manifest.description))) + } + + case .raw: + state.displayMode = .review + return .none } case .approveTapped: @@ -141,13 +156,13 @@ public struct TransactionReview: Sendable, FeatureReducer { public func reduce(into state: inout State, childAction: ChildAction) -> EffectTask { switch childAction { - case .withdrawing: + case .withdrawals: return .none - case .depositing(.delegate(.showCustomizeGuarantees)): - guard let depositing = state.depositing else { return .none } // TODO: Handle? + case .deposits(.delegate(.showCustomizeGuarantees)): + guard let deposits = state.deposits else { return .none } // TODO: Handle? - let guarantees = depositing.accounts + let guarantees = deposits.accounts .flatMap { account -> [TransactionReviewGuarantee.State] in account.transfers .filter { $0.metadata.type == .fungible } @@ -158,36 +173,27 @@ public struct TransactionReview: Sendable, FeatureReducer { return .none - case .depositing: + case .deposits: return .none case .dAppsUsed: return .none - case .presenting: + case .proofs: return .none case .networkFee: return .none - case let .customizeGuarantees(.presented(.delegate(.dismiss(apply: apply)))): - if apply, let guarantees = state.customizeGuarantees?.guarantees { - for transfer in guarantees.map(\.transfer) { - guard let guarantee = transfer.guarantee else { continue } - state.applyGuarantee(guarantee, transferID: transfer.id) - } + case let .customizeGuarantees(.presented(.delegate(.applyGuarantees(guarantees)))): + for transfer in guarantees.map(\.transfer) { + guard let guarantee = transfer.guarantee else { continue } + state.applyGuarantee(guarantee, transferID: transfer.id) } - state.customizeGuarantees = nil - return .none - case .customizeGuarantees: return .none - case let .rawTransaction(.presented(.delegate(.dismiss))): - state.rawTransaction = nil - return .none - - case .rawTransaction: + case .customizeGuarantees: return .none } } @@ -199,13 +205,13 @@ public struct TransactionReview: Sendable, FeatureReducer { state.transactionWithLockFee = review.manifestIncludingLockFee return .run { send in // TODO: Determine what is the minimal information required - let userAccounts = try await extractAccounts(reviewedManifest) + let userAccounts = try await extractUserAccounts(reviewedManifest) let content = await TransactionReview.TransactionContent( - withdrawing: try? extractWithdraws(reviewedManifest, userAccounts: userAccounts), + withdrawals: try? extractWithdrawals(reviewedManifest, userAccounts: userAccounts), dAppsUsed: try? extractUsedDapps(reviewedManifest), - depositing: try? extractDeposits(reviewedManifest, userAccounts: userAccounts), - presenting: try? exctractBadges(reviewedManifest), + deposits: try? extractDeposits(reviewedManifest, userAccounts: userAccounts), + proofs: try? exctractProofs(reviewedManifest), networkFee: .init(fee: review.transactionFeeAdded, isCongested: false) ) await send(.internal(.createTransactionReview(content))) @@ -213,10 +219,10 @@ public struct TransactionReview: Sendable, FeatureReducer { // TODO: Handle error } case let .createTransactionReview(content): - state.depositing = content.depositing + state.withdrawals = content.withdrawals state.dAppsUsed = content.dAppsUsed - state.withdrawing = content.withdrawing - state.presenting = content.presenting + state.deposits = content.deposits + state.proofs = content.proofs state.networkFee = content.networkFee return .none @@ -245,7 +251,7 @@ public struct TransactionReview: Sendable, FeatureReducer { return .send(.delegate(.failed(error))) case let .rawTransactionCreated(transaction): - state.rawTransaction = .init(transaction: transaction) + state.displayMode = .raw(transaction) return .none } } @@ -258,10 +264,10 @@ public struct TransactionReview: Sendable, FeatureReducer { extension TransactionReview { public struct TransactionContent: Sendable, Hashable { - let withdrawing: TransactionReviewAccounts.State? + let withdrawals: TransactionReviewAccounts.State? let dAppsUsed: TransactionReviewDappsUsed.State? - let depositing: TransactionReviewAccounts.State? - let presenting: TransactionReviewPresenting.State? + let deposits: TransactionReviewAccounts.State? + let proofs: TransactionReviewProofs.State? let networkFee: TransactionReviewNetworkFee.State? } @@ -271,7 +277,7 @@ extension TransactionReview { case estimated(instructionIndex: UInt32) } - private func extractAccounts(_ manifest: AnalyzeManifestWithPreviewContextResponse) async throws -> [Account] { + private func extractUserAccounts(_ manifest: AnalyzeManifestWithPreviewContextResponse) async throws -> [Account] { let userAccounts = try await accountsClient.getAccountsOnCurrentNetwork() return try manifest .encounteredAddresses @@ -289,38 +295,70 @@ extension TransactionReview { } } - private func exctractBadges(_ manifest: AnalyzeManifestWithPreviewContextResponse) async throws -> TransactionReviewPresenting.State? { - let dapps = try await extractDappsInfo(manifest.accountProofResources.map(\.address)) - guard !dapps.isEmpty else { return nil } + private func extractUsedDapps(_ manifest: AnalyzeManifestWithPreviewContextResponse) async throws -> TransactionReviewDappsUsed.State? { + let addresses = manifest.encounteredAddresses.componentAddresses.userApplications.map(\.address) + let dApps = try await addresses.asyncMap(extractDappInfo) + guard !dApps.isEmpty else { return nil } + + return TransactionReviewDappsUsed.State(isExpanded: true, dApps: .init(uniqueElements: dApps)) + } - return TransactionReviewPresenting.State(dApps: .init(uniqueElements: dapps)) + private func extractDappInfo(_ address: String) async throws -> LedgerEntity { + let metadata = try? await gatewayAPIClient.getDappDefinition(address) + return LedgerEntity( + id: address, + metadata: .init(name: metadata?.name ?? L10n.TransactionReview.unknown, + thumbnail: nil, + description: metadata?.description) + ) } - private func extractUsedDapps(_ manifest: AnalyzeManifestWithPreviewContextResponse) async throws -> TransactionReviewDappsUsed.State? { - let dapps = try await extractDappsInfo(manifest.encounteredAddresses.componentAddresses.userApplications.map(\.address)) - guard !dapps.isEmpty else { return nil } + private func exctractProofs(_ manifest: AnalyzeManifestWithPreviewContextResponse) async throws -> TransactionReviewProofs.State? { + let proofs = try await manifest.accountProofResources.map(\.address).asyncMap(extractProofInfo) + guard !proofs.isEmpty else { return nil } - return TransactionReviewDappsUsed.State(isExpanded: false, dApps: .init(uniqueElements: dapps)) + return TransactionReviewProofs.State(proofs: .init(uniqueElements: proofs)) } - private func extractDappsInfo(_ addresses: [String]) async throws -> [Dapp] { - var dapps: [Dapp] = [] - for address in addresses { - let metadata = try? await gatewayAPIClient.getEntityMetadata(address) - dapps.append( - Dapp( - id: address, - metadata: .init(name: metadata?.name ?? "Unknown", thumbnail: nil, description: metadata?.description) - ) + private func extractProofInfo(_ address: String) async throws -> LedgerEntity { + let metadata = try? await gatewayAPIClient.getEntityMetadata(address) + return LedgerEntity( + id: address, + metadata: .init(name: metadata?.name ?? L10n.TransactionReview.unknown, + thumbnail: nil, + description: metadata?.description) + ) + } + + private func extractWithdrawals( + _ manifest: AnalyzeManifestWithPreviewContextResponse, + userAccounts: [Account] + ) async throws -> TransactionReviewAccounts.State? { + var withdrawals: [Account: [Transfer]] = [:] + + for withdrawal in manifest.accountWithdraws { + try await collectTransferInfo( + componentAddress: withdrawal.componentAddress, + resourceSpecifier: withdrawal.resourceSpecifier, + userAccounts: userAccounts, + createdEntities: manifest.createdEntities, + container: &withdrawals, + type: .exact ) } - return dapps + + guard !withdrawals.isEmpty else { return nil } + + let accounts = withdrawals.map { + TransactionReviewAccount.State(account: $0.key, transfers: .init(uniqueElements: $0.value)) + } + return .init(accounts: .init(uniqueElements: accounts), showCustomizeGuarantees: false) } private func extractDeposits( _ manifest: AnalyzeManifestWithPreviewContextResponse, userAccounts: [Account] - ) async throws -> TransactionReviewAccounts.State { + ) async throws -> TransactionReviewAccounts.State? { var deposits: [Account: [Transfer]] = [:] for deposit in manifest.accountDeposits { @@ -346,9 +384,11 @@ extension TransactionReview { } } - let reviewAccounts = deposits.map { - TransactionReviewAccount.State(account: $0.key, transfers: .init(uniqueElements: $0.value)) - } + let reviewAccounts = deposits + .filter { !$0.value.isEmpty } + .map { TransactionReviewAccount.State(account: $0.key, transfers: .init(uniqueElements: $0.value)) } + + guard !reviewAccounts.isEmpty else { return nil } let requiresGuarantees = reviewAccounts.contains { reviewAccount in reviewAccount.transfers.contains { transfer in @@ -370,17 +410,15 @@ extension TransactionReview { let account = userAccounts.first { $0.address.address == componentAddress.address }! // TODO: Handle func addTransfer(_ resourceAddress: ResourceAddress, amount: BigDecimal) async throws { let isNewResources = createdEntities?.resourceAddresses.contains(resourceAddress) ?? false - let metadata: GatewayAPI.EntityMetadataCollection? = await { + + func getMetadata(address: String) async throws -> GatewayAPI.EntityMetadataCollection? { guard !isNewResources else { return nil } - return try? await gatewayAPIClient.getEntityMetadata(resourceAddress.address) - }() + return try await gatewayAPIClient.getEntityMetadata(address) + } let addressKind = try engineToolkitClient.decodeAddress(resourceAddress.address).entityType - let action = AccountAction( - componentAddress: componentAddress, - resourceAddress: resourceAddress, - amount: amount - ) + + let metadata = try? await getMetadata(address: resourceAddress.address) let guarantee: TransactionClient.Guarantee? = { if case let .estimated(instructionIndex) = type, !isNewResources { @@ -390,13 +428,14 @@ extension TransactionReview { }() let resourceMetadata = ResourceMetadata( - name: metadata?.symbol ?? metadata?.name ?? "Unknown", + name: metadata?.symbol ?? metadata?.name ?? L10n.TransactionReview.unknown, thumbnail: nil, type: addressKind.resourceType ) let transfer = TransactionReview.Transfer( - action: action, + amount: amount, + resourceAddress: resourceAddress, guarantee: guarantee, metadata: resourceMetadata ) @@ -411,37 +450,12 @@ extension TransactionReview { try await addTransfer(resourceAddress, amount: .init(fromString: "1")) } } - - private func extractWithdraws( - _ manifest: AnalyzeManifestWithPreviewContextResponse, - userAccounts: [Account] - ) async throws -> TransactionReviewAccounts.State? { - var withdraws: [Account: [Transfer]] = [:] - - for withdraw in manifest.accountWithdraws { - try await collectTransferInfo( - componentAddress: withdraw.componentAddress, - resourceSpecifier: withdraw.resourceSpecifier, - userAccounts: userAccounts, - createdEntities: manifest.createdEntities, - container: &withdraws, - type: .exact - ) - } - - guard !withdraws.isEmpty else { return nil } - - let accounts = withdraws.map { - TransactionReviewAccount.State(account: $0.key, transfers: .init(uniqueElements: $0.value)) - } - return .init(accounts: .init(uniqueElements: accounts), showCustomizeGuarantees: false) - } } // MARK: Useful types extension TransactionReview { - public struct Dapp: Sendable, Identifiable, Hashable { + public struct LedgerEntity: Sendable, Identifiable, Hashable { public let id: AccountAddress.ID public let metadata: Metadata? @@ -492,18 +506,22 @@ extension TransactionReview { } public struct Transfer: Sendable, Identifiable, Hashable { - public var id: AccountAction { action } + public let id: UUID = .init() + + public let amount: BigDecimal + public let resourceAddress: ResourceAddress - public let action: AccountAction public var guarantee: TransactionClient.Guarantee? public var metadata: ResourceMetadata public init( - action: AccountAction, + amount: BigDecimal, + resourceAddress: ResourceAddress, guarantee: TransactionClient.Guarantee? = nil, metadata: ResourceMetadata ) { - self.action = action + self.amount = amount + self.resourceAddress = resourceAddress self.guarantee = guarantee self.metadata = metadata } @@ -531,19 +549,21 @@ extension TransactionReview { extension TransactionReview.State { public var allGuarantees: [TransactionClient.Guarantee] { - depositing?.accounts.flatMap { $0.transfers.compactMap(\.guarantee) } ?? [] + deposits?.accounts.flatMap { $0.transfers.compactMap(\.guarantee) } ?? [] } public mutating func applyGuarantee(_ updated: TransactionClient.Guarantee, transferID: TransactionReview.Transfer.ID) { guard let accountID = accountID(for: transferID) else { return } - depositing? + deposits? .accounts[id: accountID]? - .transfers[id: transferID]?.guarantee?.amount = updated.amount + .transfers[id: transferID]? + .guarantee? + .amount = updated.amount } private func accountID(for transferID: TransactionReview.Transfer.ID) -> AccountAddress.ID? { - for account in depositing?.accounts ?? [] { + for account in deposits?.accounts ?? [] { for transfer in account.transfers { if transfer.id == transferID { return account.id @@ -564,27 +584,6 @@ extension Collection where Element: Equatable { } } -// MARK: - AccountAction -public struct AccountAction: Codable, Sendable, Hashable { - public let componentAddress: ComponentAddress - - public let resourceAddress: ResourceAddress - - public let amount: BigDecimal - - public enum CodingKeys: String, CodingKey { - case componentAddress = "component_address" - case resourceAddress = "resource_address" - case amount - } -} - -extension Collection { - public var groupedByAccount: [ComponentAddress: [AccountAction]] { - .init(grouping: self, by: \.componentAddress) - } -} - extension GatewayAPI.EntityMetadataCollection { var description: String? { self["description"] @@ -623,124 +622,3 @@ extension EngineToolkitModels.AddressKind { } } } - -#if DEBUG -extension TransactionReview.Dapp { - public static let mock0 = Self(id: .deadbeef32Bytes, - metadata: .init(name: "Collabofi User Badge", thumbnail: nil, description: nil)) - - public static let mock1 = Self(id: .deadbeef64Bytes, - metadata: .init(name: "Oh Babylon Founder NFT", thumbnail: nil, description: nil)) - - public static let mock2 = Self(id: "deadbeef64Bytes", metadata: nil) - - public static let mock3 = Self(id: "deadbeef32Bytes", metadata: nil) -} - -extension TransactionReviewAccount.State { - public static let mockWithdraw0 = Self(account: .mockUser0, transfers: [.mock0, .mock1]) - - public static let mockWithdraw1 = Self(account: .mockUser1, transfers: [.mock1, .mock3, .mock4]) - - public static let mockWithdraw2 = Self(account: .mockUser0, transfers: [.mock1, .mock3]) - - public static let mockDeposit1 = Self(account: .mockExternal0, transfers: [.mock0, .mock1, .mock2]) - - public static let mockDeposit2 = Self(account: .mockUser0, transfers: [.mock3, .mock4]) -} - -extension TransactionReview.Account { - public static let mockUser0 = user(.init(address: .mock0, - label: "My Main Account", - appearanceID: ._1)) - - public static let mockUser1 = user(.init(address: .mock1, - label: "My Savings Account", - appearanceID: ._2)) - - public static let mockExternal0 = external(.mock2, approved: true) - public static let mockExternal1 = external(.mock2, approved: false) -} - -extension AccountAddress { - public static let mock0 = try! Self(address: "account_tdx_b_k591p8y440g69dlqnuzghu84e84ak088fah9u6ay440g6pzq8y4") - public static let mock1 = try! Self(address: "account_tdx_b_e84ak088fah9u6ad6j9dlqnuz84e84ak088fau6ad6j9dlqnuzk") - public static let mock2 = try! Self(address: "account_tdx_b_1pzq8y440g6nc4vuz0ghu84e84ak088fah9u6ad6j9dlqnuzk59") -} - -extension ComponentAddress { - public static let mock0 = Self(address: "account_tdx_b_k591p8y440g69dlqnuzghu84e84ak088fah9u6ay440g6pzq8y4") - public static let mock1 = Self(address: "account_tdx_b_e84ak088fah9u6ad6j9dlqnuz84e84ak088fau6ad6j9dlqnuzk") - public static let mock2 = Self(address: "account_tdx_b_1pzq8y440g6nc4vuz0ghu84e84ak088fah9u6ad6j9dlqnuzk59") -} - -extension ResourceAddress { - public static let mock0 = Self(address: "resource_tdx_b_1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq8z96qp") - public static let mock1 = Self(address: "resource_tdx_b_1qre9sv98scqut4k9g3j6kxuvscczv0lzumefwgwhuf6qdu4c3r") -} - -extension URL { - static let mock = URL(string: "test")! -} - -extension TransactionReview.Transfer { - public static let mock0 = Self(action: .mock0, - guarantee: .init(amount: 1.0188, instructionIndex: 1, resourceAddress: .mock0), - metadata: .init(name: "TSLA", - thumbnail: .mock, - type: .fungible, - fiatAmount: 301.91)) - - public static let mock1 = Self(action: .mock1, - metadata: .init(name: "XRD", - thumbnail: .mock, - type: .fungible, - fiatAmount: 301.91)) - - public static let mock2 = Self(action: .mock2, - guarantee: .init(amount: 5.10, instructionIndex: 1, resourceAddress: .mock1), - metadata: .init(name: "PXL", - thumbnail: .mock, - type: .fungible)) - - public static let mock3 = Self(action: .mock3, - metadata: .init(name: "PXL", - thumbnail: .mock, - type: .fungible)) - - public static let mock4 = Self(action: .mock4, - metadata: .init(name: "Block 14F5", - thumbnail: .mock, - type: .nonFungible)) - - public static var all: Set { - [.mock0, .mock1, .mock2, .mock3, .mock4] - } -} - -extension AccountAction { - public static let mock0 = Self(componentAddress: .mock0, - resourceAddress: .mock0, - amount: 1.0396) - - public static let mock1 = Self(componentAddress: .mock1, - resourceAddress: .mock1, - amount: 500) - - public static let mock2 = Self(componentAddress: .mock0, - resourceAddress: .mock1, - amount: 5.123) - - public static let mock3 = Self(componentAddress: .mock1, - resourceAddress: .mock1, - amount: 300) - - public static let mock4 = Self(componentAddress: .mock0, - resourceAddress: .mock1, - amount: 1) - - public static var all: Set { - [.mock0, .mock1, .mock2, .mock3, .mock4] - } -} -#endif diff --git a/Sources/Features/TransactionReviewFeature/TransactionReviewAccount/TransactionReviewAccount+View.swift b/Sources/Features/TransactionReviewFeature/TransactionReviewAccount/TransactionReviewAccount+View.swift index c27c2c01bb..4eb70d7ae7 100644 --- a/Sources/Features/TransactionReviewFeature/TransactionReviewAccount/TransactionReviewAccount+View.swift +++ b/Sources/Features/TransactionReviewFeature/TransactionReviewAccount/TransactionReviewAccount+View.swift @@ -98,7 +98,7 @@ public struct TransactionDetailsView: View { TransactionReviewTokenView(viewState: .init( name: viewState.metadata.name, thumbnail: viewState.metadata.thumbnail, - amount: viewState.action.amount, + amount: viewState.amount, guaranteedAmount: viewState.guarantee?.amount, fiatAmount: viewState.metadata.fiatAmount )) diff --git a/Sources/Features/TransactionReviewFeature/TransactionReviewDappsUsed/TransactionReviewDappsUsed+View.swift b/Sources/Features/TransactionReviewFeature/TransactionReviewDappsUsed/TransactionReviewDappsUsed+View.swift index 791d2851e7..da4eb3b331 100644 --- a/Sources/Features/TransactionReviewFeature/TransactionReviewDappsUsed/TransactionReviewDappsUsed+View.swift +++ b/Sources/Features/TransactionReviewFeature/TransactionReviewDappsUsed/TransactionReviewDappsUsed+View.swift @@ -7,7 +7,7 @@ extension TransactionReviewDappsUsed.State { } } -extension TransactionReview.Dapp { +extension TransactionReview.LedgerEntity { fileprivate var knownDapp: TransactionReviewDappsUsed.ViewState.KnownDapp? { guard let metadata else { return nil } return .init(id: id, thumbnail: metadata.thumbnail, name: metadata.name, description: metadata.description) @@ -102,22 +102,18 @@ extension TransactionReviewDappsUsed { DappPlaceholder(known: true, size: .smaller) } - VStack(alignment: .leading, spacing: .small3) { - Text(name) - .lineLimit(description != nil ? 1 : 2) - if let description { - Text(description) - .lineLimit(1) - } - } - .padding(.leading, .small2) + Text(name) + .lineLimit(2) + .padding(.leading, .small2) case let .unknown(count): DappPlaceholder(known: false, size: .smaller) .padding(.trailing, .small2) + Text(L10n.TransactionReview.UsingDapps.unknownComponents(count)) .lineLimit(2) } + Spacer(minLength: 0) } .textStyle(.body2HighImportance) diff --git a/Sources/Features/TransactionReviewFeature/TransactionReviewDappsUsed/TransactionReviewDappsUsed.swift b/Sources/Features/TransactionReviewFeature/TransactionReviewDappsUsed/TransactionReviewDappsUsed.swift index f8e5e375fd..8a5beb4053 100644 --- a/Sources/Features/TransactionReviewFeature/TransactionReviewDappsUsed/TransactionReviewDappsUsed.swift +++ b/Sources/Features/TransactionReviewFeature/TransactionReviewDappsUsed/TransactionReviewDappsUsed.swift @@ -4,9 +4,9 @@ import FeaturePrelude public struct TransactionReviewDappsUsed: Sendable, FeatureReducer { public struct State: Sendable, Hashable { public var isExpanded: Bool - public var dApps: IdentifiedArrayOf + public var dApps: IdentifiedArrayOf - public init(isExpanded: Bool, dApps: IdentifiedArrayOf) { + public init(isExpanded: Bool, dApps: IdentifiedArrayOf) { self.isExpanded = isExpanded self.dApps = dApps } @@ -14,7 +14,7 @@ public struct TransactionReviewDappsUsed: Sendable, FeatureReducer { public enum ViewAction: Sendable, Equatable { case expandTapped - case dappTapped(TransactionReview.Dapp.ID) + case dappTapped(TransactionReview.LedgerEntity.ID) } public init() {} diff --git a/Sources/Features/TransactionReviewFeature/TransactionReviewGuarantees/MinimumPercentageStepper/MinimumPercentageStepper+View.swift b/Sources/Features/TransactionReviewFeature/TransactionReviewGuarantees/MinimumPercentageStepper/MinimumPercentageStepper+View.swift index 348c069d9e..efec5df383 100644 --- a/Sources/Features/TransactionReviewFeature/TransactionReviewGuarantees/MinimumPercentageStepper/MinimumPercentageStepper+View.swift +++ b/Sources/Features/TransactionReviewFeature/TransactionReviewGuarantees/MinimumPercentageStepper/MinimumPercentageStepper+View.swift @@ -16,7 +16,7 @@ extension MinimumPercentageStepper { Button(asset: AssetResource.minusCircle) { viewStore.send(.decreaseTapped) } - .opacity(viewStore.disableMinus ? 0.2 : 1) + .opacity(viewStore.disableMinus ? disabledOpacity : 1) .disabled(viewStore.disableMinus) let text = viewStore.binding(get: \.string) { .stringEntered($0) } @@ -26,21 +26,27 @@ extension MinimumPercentageStepper { .lineLimit(1) .textStyle(.body2Regular) .foregroundColor(.app.gray1) - .frame(width: 68, height: 48) + .frame(width: textFieldSize.width, height: textFieldSize.height) .background { RoundedRectangle(cornerRadius: 8) .fill(.app.gray5) RoundedRectangle(cornerRadius: 8) - .stroke(viewStore.isValid ? .app.gray4 : .app.red1.opacity(0.6)) + .stroke(viewStore.isValid ? .app.gray4 : transparentErrorRed) } Button(asset: AssetResource.plusCircle) { viewStore.send(.increaseTapped) } - .opacity(viewStore.disablePlus ? 0.2 : 1) + .opacity(viewStore.disablePlus ? disabledOpacity : 1) .disabled(viewStore.disablePlus) } } } + + private let disabledOpacity: CGFloat = 0.2 + + private let transparentErrorRed: Color = .app.red1.opacity(0.6) + + private let textFieldSize: CGSize = .init(width: 68, height: 48) } } diff --git a/Sources/Features/TransactionReviewFeature/TransactionReviewGuarantees/TransactionReviewGuarantees+View.swift b/Sources/Features/TransactionReviewFeature/TransactionReviewGuarantees/TransactionReviewGuarantees+View.swift index ae9b956a25..2416e191f6 100644 --- a/Sources/Features/TransactionReviewFeature/TransactionReviewGuarantees/TransactionReviewGuarantees+View.swift +++ b/Sources/Features/TransactionReviewFeature/TransactionReviewGuarantees/TransactionReviewGuarantees+View.swift @@ -78,7 +78,7 @@ extension TransactionReviewGuarantees { extension TransactionReviewGuarantee.State { var viewState: TransactionReviewGuarantee.ViewState { - .init(id: id, account: account, token: .init(transfer: transfer)) + .init(id: transfer.id, account: account, token: .init(transfer: transfer)) } } @@ -86,7 +86,7 @@ extension TransactionReviewTokenView.ViewState { init(transfer: TransactionReview.Transfer) { self.init(name: transfer.metadata.name, thumbnail: transfer.metadata.thumbnail, - amount: transfer.action.amount, + amount: transfer.amount, guaranteedAmount: transfer.guarantee?.amount, fiatAmount: transfer.metadata.fiatAmount) } @@ -94,7 +94,7 @@ extension TransactionReviewTokenView.ViewState { extension TransactionReviewGuarantee { public struct ViewState: Identifiable, Equatable { - public let id: AccountAction + public let id: TransactionReview.Transfer.ID let account: TransactionReview.Account let token: TransactionReviewTokenView.ViewState } diff --git a/Sources/Features/TransactionReviewFeature/TransactionReviewGuarantees/TransactionReviewGuarantees.swift b/Sources/Features/TransactionReviewFeature/TransactionReviewGuarantees/TransactionReviewGuarantees.swift index 6651b536cb..26ce18c6f2 100644 --- a/Sources/Features/TransactionReviewFeature/TransactionReviewGuarantees/TransactionReviewGuarantees.swift +++ b/Sources/Features/TransactionReviewFeature/TransactionReviewGuarantees/TransactionReviewGuarantees.swift @@ -2,6 +2,8 @@ import FeaturePrelude // MARK: - TransactionReviewGuarantees public struct TransactionReviewGuarantees: Sendable, FeatureReducer { + @Dependency(\.dismiss) var dismiss + public struct State: Sendable, Hashable { public var guarantees: IdentifiedArrayOf @@ -25,7 +27,7 @@ public struct TransactionReviewGuarantees: Sendable, FeatureReducer { } public enum DelegateAction: Sendable, Equatable { - case dismiss(apply: Bool) + case applyGuarantees(IdentifiedArrayOf) } public init() {} @@ -43,15 +45,22 @@ public struct TransactionReviewGuarantees: Sendable, FeatureReducer { public func reduce(into state: inout State, viewAction: ViewAction) -> EffectTask { switch viewAction { case .infoTapped: -// state.info = .init(title: L10n.TransactionReview.Guarantees.explanationTitle, -// explanation: L10n.TransactionReview.Guarantees.explanationText) + // FIXME: For mainnet + // state.info = .init(title: L10n.TransactionReview.Guarantees.explanationTitle, + // explanation: L10n.TransactionReview.Guarantees.explanationText) return .none case .applyTapped: - return .send(.delegate(.dismiss(apply: true))) + let guarantees = state.guarantees + return .run { send in + await send(.delegate(.applyGuarantees(guarantees))) + await dismiss() + } case .closeTapped: - return .send(.delegate(.dismiss(apply: false))) + return .fireAndForget { + await dismiss() + } } } } @@ -61,7 +70,7 @@ public struct TransactionReviewGuarantee: Sendable, FeatureReducer { @Dependency(\.pasteboardClient) var pasteboardClient public struct State: Identifiable, Sendable, Hashable { - public var id: AccountAction { transfer.id } + public var id: TransactionReview.Transfer.ID { transfer.id } public let account: TransactionReview.Account public var transfer: TransactionReview.Transfer @@ -74,8 +83,8 @@ public struct TransactionReviewGuarantee: Sendable, FeatureReducer { self.account = account self.transfer = transfer - if let guaranteed = transfer.guarantee?.amount, guaranteed >= 0, guaranteed <= transfer.action.amount { - self.percentageStepper = .init(value: 100 * guaranteed / transfer.action.amount) + if let guaranteed = transfer.guarantee?.amount, guaranteed >= 0, guaranteed <= transfer.amount { + self.percentageStepper = .init(value: 100 * guaranteed / transfer.amount) } else { self.percentageStepper = .init(value: 100) } @@ -115,8 +124,7 @@ public struct TransactionReviewGuarantee: Sendable, FeatureReducer { switch childAction { case .percentageStepper(.delegate(.valueChanged)): let newMinimumDecimal = state.percentageStepper.value * 0.01 - let newAmount = newMinimumDecimal * state.transfer.action.amount - + let newAmount = newMinimumDecimal * state.transfer.amount state.transfer.guarantee?.amount = newAmount return .none diff --git a/Sources/Features/TransactionReviewFeature/TransactionReviewNetworkFee/TransactionReviewNetworkFee+View.swift b/Sources/Features/TransactionReviewFeature/TransactionReviewNetworkFee/TransactionReviewNetworkFee+View.swift index aff65a3f66..cddcda6434 100644 --- a/Sources/Features/TransactionReviewFeature/TransactionReviewNetworkFee/TransactionReviewNetworkFee+View.swift +++ b/Sources/Features/TransactionReviewFeature/TransactionReviewNetworkFee/TransactionReviewNetworkFee+View.swift @@ -37,7 +37,7 @@ extension TransactionReviewNetworkFee { .foregroundColor(.app.alert) } - // TODO: Enable back + // FIXME: mainnet // Button(L10n.TransactionReview.NetworkFee.customizeButtonTitle) { // viewStore.send(.customizeTapped) // } diff --git a/Sources/Features/TransactionReviewFeature/TransactionReviewPresenting/TransactionReviewPresenting.swift b/Sources/Features/TransactionReviewFeature/TransactionReviewPresenting/TransactionReviewPresenting.swift deleted file mode 100644 index 09182f6f89..0000000000 --- a/Sources/Features/TransactionReviewFeature/TransactionReviewPresenting/TransactionReviewPresenting.swift +++ /dev/null @@ -1,28 +0,0 @@ -import FeaturePrelude - -// MARK: - TransactionReviewPresenting -public struct TransactionReviewPresenting: Sendable, FeatureReducer { - public struct State: Sendable, Hashable { - public var dApps: IdentifiedArrayOf - - public init(dApps: IdentifiedArrayOf) { - self.dApps = dApps - } - } - - public enum ViewAction: Sendable, Equatable { - case infoTapped - case dAppTapped(id: TransactionReview.Dapp.ID) - } - - public init() {} - - public func reduce(into state: inout State, viewAction: ViewAction) -> EffectTask { - switch viewAction { - case .infoTapped: - return .none - case let .dAppTapped(id): - return .none - } - } -} diff --git a/Sources/Features/TransactionReviewFeature/TransactionReviewPresenting/TransactionReviewPresenting+View.swift b/Sources/Features/TransactionReviewFeature/TransactionReviewProofs/TransactionReviewProofs+View.swift similarity index 75% rename from Sources/Features/TransactionReviewFeature/TransactionReviewPresenting/TransactionReviewPresenting+View.swift rename to Sources/Features/TransactionReviewFeature/TransactionReviewProofs/TransactionReviewProofs+View.swift index 0413da0f52..7f0314044b 100644 --- a/Sources/Features/TransactionReviewFeature/TransactionReviewPresenting/TransactionReviewPresenting+View.swift +++ b/Sources/Features/TransactionReviewFeature/TransactionReviewProofs/TransactionReviewProofs+View.swift @@ -1,12 +1,12 @@ import FeaturePrelude -// MARK: - TransactionReviewPresenting.View -extension TransactionReviewPresenting { +// MARK: - TransactionReviewProofs.View +extension TransactionReviewProofs { @MainActor public struct View: SwiftUI.View { - let store: StoreOf + let store: StoreOf - public init(store: StoreOf) { + public init(store: StoreOf) { self.store = store } @@ -26,15 +26,15 @@ extension TransactionReviewPresenting { } .padding(.bottom, .medium2) - ForEach(viewStore.dApps) { dApp in + ForEach(viewStore.proofs) { proof in VStack(spacing: 0) { - let metadata = dApp.metadata - DappView(thumbnail: metadata?.thumbnail, name: metadata?.name ?? "Unknown name") { // TODO:  - viewStore.send(.dAppTapped(id: dApp.id)) + let metadata = proof.metadata + DappView(thumbnail: metadata?.thumbnail, name: metadata?.name ?? L10n.TransactionReview.unknown) { + viewStore.send(.proofTapped(id: proof.id)) } .padding(.bottom, .medium3) - if dApp.id != viewStore.dApps.last?.id { + if proof.id != viewStore.proofs.last?.id { Separator() .padding(.bottom, .medium3) } diff --git a/Sources/Features/TransactionReviewFeature/TransactionReviewProofs/TransactionReviewProofs.swift b/Sources/Features/TransactionReviewFeature/TransactionReviewProofs/TransactionReviewProofs.swift new file mode 100644 index 0000000000..0d1de5b302 --- /dev/null +++ b/Sources/Features/TransactionReviewFeature/TransactionReviewProofs/TransactionReviewProofs.swift @@ -0,0 +1,28 @@ +import FeaturePrelude + +// MARK: - TransactionReviewProofs +public struct TransactionReviewProofs: Sendable, FeatureReducer { + public struct State: Sendable, Hashable { + public var proofs: IdentifiedArrayOf + + public init(proofs: IdentifiedArrayOf) { + self.proofs = proofs + } + } + + public enum ViewAction: Sendable, Equatable { + case infoTapped + case proofTapped(id: TransactionReview.LedgerEntity.ID) + } + + public init() {} + + public func reduce(into state: inout State, viewAction: ViewAction) -> EffectTask { + switch viewAction { + case .infoTapped: + return .none + case let .proofTapped(id): + return .none + } + } +} diff --git a/Sources/Features/TransactionReviewFeature/TransactionReviewRawTransaction/TransactionReviewRawTransaction+View.swift b/Sources/Features/TransactionReviewFeature/TransactionReviewRawTransaction/TransactionReviewRawTransaction+View.swift index c0cbc4b50b..7bf00b02f8 100644 --- a/Sources/Features/TransactionReviewFeature/TransactionReviewRawTransaction/TransactionReviewRawTransaction+View.swift +++ b/Sources/Features/TransactionReviewFeature/TransactionReviewRawTransaction/TransactionReviewRawTransaction+View.swift @@ -34,7 +34,8 @@ extension TransactionReviewRawTransaction { var body: some SwiftUI.View { Text(transaction) - .font(.system(size: 13, design: .monospaced)) + .textStyle(.monospace) + .foregroundColor(.app.gray1) .frame( maxWidth: .infinity, maxHeight: .infinity, diff --git a/Sources/Features/TransactionReviewFeature/TransactionReviewRawTransaction/TransactionReviewRawTransaction.swift b/Sources/Features/TransactionReviewFeature/TransactionReviewRawTransaction/TransactionReviewRawTransaction.swift index bf40edcff2..846ec56ef5 100644 --- a/Sources/Features/TransactionReviewFeature/TransactionReviewRawTransaction/TransactionReviewRawTransaction.swift +++ b/Sources/Features/TransactionReviewFeature/TransactionReviewRawTransaction/TransactionReviewRawTransaction.swift @@ -1,7 +1,9 @@ import FeaturePrelude -// MARK: - TransactionReviewPresenting +// MARK: - TransactionReviewRawTransaction public struct TransactionReviewRawTransaction: Sendable, FeatureReducer { + @Dependency(\.dismiss) var dismiss + public struct State: Sendable, Hashable { public var transaction: String @@ -14,16 +16,14 @@ public struct TransactionReviewRawTransaction: Sendable, FeatureReducer { case closeTapped } - public enum DelegateAction: Sendable, Equatable { - case dismiss - } - public init() {} public func reduce(into state: inout State, viewAction: ViewAction) -> EffectTask { switch viewAction { case .closeTapped: - return .send(.delegate(.dismiss)) + return .fireAndForget { + await dismiss() + } } } }