diff --git a/modules/.swiftpm/xcode/xcshareddata/xcschemes/TransactionList.xcscheme b/modules/.swiftpm/xcode/xcshareddata/xcschemes/TransactionList.xcscheme new file mode 100644 index 00000000..eb4b4eeb --- /dev/null +++ b/modules/.swiftpm/xcode/xcshareddata/xcschemes/TransactionList.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/modules/Package.swift b/modules/Package.swift index 39e0993c..d4293d0d 100644 --- a/modules/Package.swift +++ b/modules/Package.swift @@ -52,7 +52,7 @@ let package = Package( .library(name: "UserPreferencesStorage", targets: ["UserPreferencesStorage"]), .library(name: "Utils", targets: ["Utils"]), .library(name: "WalletConfigProvider", targets: ["WalletConfigProvider"]), - .library(name: "WalletEventsFlow", targets: ["WalletEventsFlow"]), + .library(name: "TransactionList", targets: ["TransactionList"]), .library(name: "WalletStorage", targets: ["WalletStorage"]), .library(name: "Welcome", targets: ["Welcome"]), .library(name: "ZcashSDKEnvironment", targets: ["ZcashSDKEnvironment"]) @@ -209,7 +209,7 @@ let package = Package( "SDKSynchronizer", "UIComponents", "Utils", - "WalletEventsFlow", + "TransactionList", "ZcashSDKEnvironment", .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "ZcashLightClientKit", package: "ZcashLightClientKit") @@ -364,7 +364,7 @@ let package = Package( "RecoveryPhraseDisplay", "Scan", "SendFlow", - "WalletEventsFlow", + "TransactionList", .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "ZcashLightClientKit", package: "ZcashLightClientKit") ], @@ -531,7 +531,7 @@ let package = Package( path: "Sources/Dependencies/WalletConfigProvider" ), .target( - name: "WalletEventsFlow", + name: "TransactionList", dependencies: [ "Generated", "Models", @@ -543,7 +543,7 @@ let package = Package( .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), .product(name: "ZcashLightClientKit", package: "ZcashLightClientKit") ], - path: "Sources/Features/WalletEventsFlow" + path: "Sources/Features/TransactionList" ), .target( name: "WalletStorage", diff --git a/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift b/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift index 5829928c..a5f5bf9d 100644 --- a/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift +++ b/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerInterface.swift @@ -33,7 +33,7 @@ public struct SDKSynchronizerClient { public let getShieldedBalance: () -> WalletBalance? public let getTransparentBalance: () -> WalletBalance? - public var getAllTransactions: () async throws -> [WalletEvent] + public var getAllTransactions: () async throws -> [TransactionState] public let getUnifiedAddress: (_ account: Int) async throws -> UnifiedAddress? public let getTransparentAddress: (_ account: Int) async throws -> TransparentAddress? diff --git a/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift b/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift index 67039de7..fcbc4eee 100644 --- a/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift +++ b/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerLive.swift @@ -52,7 +52,7 @@ extension SDKSynchronizerClient { getAllTransactions: { let clearedTransactions = try await synchronizer.allTransactions() - var clearedTxs: [WalletEvent] = [] + var clearedTxs: [TransactionState] = [] for clearedTransaction in clearedTransactions { var transaction = TransactionState.init( @@ -72,13 +72,7 @@ extension SDKSynchronizerClient { transaction.zAddress = addresses.first?.stringEncoded - clearedTxs.append( - WalletEvent( - id: transaction.id, - state: .transaction(transaction), - timestamp: transaction.timestamp - ) - ) + clearedTxs.append(transaction) } return clearedTxs diff --git a/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift b/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift index 8dadcd3b..72830133 100644 --- a/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift +++ b/modules/Sources/Dependencies/SDKSynchronizer/SDKSynchronizerTest.swift @@ -74,11 +74,11 @@ extension SDKSynchronizerClient { rewind: @escaping (RewindPolicy) -> AnyPublisher = { _ in return Empty().eraseToAnyPublisher() }, getShieldedBalance: @escaping () -> WalletBalance? = { WalletBalance(verified: Zatoshi(12345000), total: Zatoshi(12345000)) }, getTransparentBalance: @escaping () -> WalletBalance? = { WalletBalance(verified: Zatoshi(12345000), total: Zatoshi(12345000)) }, - getAllTransactions: @escaping () -> [WalletEvent] = { + getAllTransactions: @escaping () -> [TransactionState] = { let mockedCleared: [TransactionStateMockHelper] = [ - TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"), + TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid, uuid: "aa11"), TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"), - TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"), + TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid, uuid: "cc33"), TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"), TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55") ] @@ -93,18 +93,18 @@ extension SDKSynchronizerClient { timestamp: $0.date, uuid: $0.uuid ) - return WalletEvent(id: transaction.id, state: .transaction(transaction), timestamp: transaction.timestamp ?? 0) + return transaction } let mockedPending: [TransactionStateMockHelper] = [ TransactionStateMockHelper( date: 1651039606, amount: Zatoshi(6), - status: .paid(success: false), + status: .paid, uuid: "ff66" ), TransactionStateMockHelper(date: 1651039303, amount: Zatoshi(7), uuid: "gg77"), - TransactionStateMockHelper(date: 1651039707, amount: Zatoshi(8), status: .paid(success: true), uuid: "hh88"), + TransactionStateMockHelper(date: 1651039707, amount: Zatoshi(8), status: .paid, uuid: "hh88"), TransactionStateMockHelper(date: 1651039808, amount: Zatoshi(9), uuid: "ii99") ] @@ -118,7 +118,7 @@ extension SDKSynchronizerClient { timestamp: $0.date, uuid: $0.uuid ) - return WalletEvent(id: transaction.id, state: .transaction(transaction), timestamp: transaction.timestamp) + return transaction } clearedTransactions.append(contentsOf: pendingTransactions) @@ -156,7 +156,7 @@ extension SDKSynchronizerClient { zAddress: "tteafadlamnelkqe", fee: Zatoshi(10), id: "id", - status: .paid(success: true), + status: .paid, timestamp: 1234567, zecAmount: Zatoshi(10) ) @@ -170,7 +170,7 @@ extension SDKSynchronizerClient { zAddress: "tteafadlamnelkqe", fee: Zatoshi(10), id: "id", - status: .paid(success: true), + status: .paid, timestamp: 1234567, zecAmount: Zatoshi(10) ) diff --git a/modules/Sources/Features/AddressDetails/AddressDetailsView.swift b/modules/Sources/Features/AddressDetails/AddressDetailsView.swift index 51b07469..8c9ea992 100644 --- a/modules/Sources/Features/AddressDetails/AddressDetailsView.swift +++ b/modules/Sources/Features/AddressDetails/AddressDetailsView.swift @@ -56,7 +56,7 @@ public struct AddressDetailsView: View { Text(address) .font(.custom(FontFamily.Inter.regular.name, size: 16)) - .foregroundColor(Asset.Colors.suppressed47.color) + .foregroundColor(Asset.Colors.shade47.color) .frame(width: 270) .padding(.bottom, 20) diff --git a/modules/Sources/Features/Home/HomeStore.swift b/modules/Sources/Features/Home/HomeStore.swift index 1cff8904..9161bfb7 100644 --- a/modules/Sources/Features/Home/HomeStore.swift +++ b/modules/Sources/Features/Home/HomeStore.swift @@ -9,7 +9,7 @@ import Utils import Models import Generated import ReviewRequest -import WalletEventsFlow +import TransactionList import Scan public typealias HomeStore = Store @@ -32,7 +32,7 @@ public struct HomeReducer: ReducerProtocol { public var shieldedBalance: Balance public var synchronizerStatusSnapshot: SyncStatusSnapshot public var walletConfig: WalletConfig - public var walletEventsState: WalletEventsFlowReducer.State + public var transactionListState: TransactionListReducer.State public var migratingDatabase = true // TODO: [#311] - Get the ZEC price from the SDK, https://github.com/zcash/secant-ios-wallet/issues/311 public var zecPrice = Decimal(140.0) @@ -53,7 +53,7 @@ public struct HomeReducer: ReducerProtocol { shieldedBalance: Balance, synchronizerStatusSnapshot: SyncStatusSnapshot, walletConfig: WalletConfig, - walletEventsState: WalletEventsFlowReducer.State, + transactionListState: TransactionListReducer.State, zecPrice: Decimal = Decimal(140.0) ) { self.destination = destination @@ -63,7 +63,7 @@ public struct HomeReducer: ReducerProtocol { self.shieldedBalance = shieldedBalance self.synchronizerStatusSnapshot = synchronizerStatusSnapshot self.walletConfig = walletConfig - self.walletEventsState = walletEventsState + self.transactionListState = transactionListState self.zecPrice = zecPrice } } @@ -82,8 +82,8 @@ public struct HomeReducer: ReducerProtocol { case synchronizerStateChanged(SynchronizerState) case syncFailed(ZcashError) case updateDestination(HomeReducer.State.Destination?) - case updateWalletEvents([WalletEvent]) - case walletEvents(WalletEventsFlowReducer.Action) + case updateTransactionList([TransactionState]) + case transactionList(TransactionListReducer.Action) } @Dependency(\.audioServices) var audioServices @@ -98,8 +98,8 @@ public struct HomeReducer: ReducerProtocol { } public var body: some ReducerProtocol { - Scope(state: \.walletEventsState, action: /Action.walletEvents) { - WalletEventsFlowReducer() + Scope(state: \.transactionListState, action: /Action.transactionList) { + TransactionListReducer() } Reduce { state, action in @@ -136,7 +136,7 @@ public struct HomeReducer: ReducerProtocol { state.canRequestReview = false return .none - case .updateWalletEvents: + case .updateTransactionList: return .none case .synchronizerStateChanged(let latestState): @@ -171,7 +171,7 @@ public struct HomeReducer: ReducerProtocol { state.destination = destination return .none - case .walletEvents: + case .transactionList: return .none case .retrySync: @@ -212,10 +212,10 @@ public struct HomeReducer: ReducerProtocol { // MARK: - Store extension HomeStore { - func historyStore() -> WalletEventsFlowStore { + func historyStore() -> TransactionListStore { self.scope( - state: \.walletEventsState, - action: HomeReducer.Action.walletEvents + state: \.transactionListState, + action: HomeReducer.Action.transactionList ) } } @@ -242,7 +242,7 @@ extension HomeReducer.State { shieldedBalance: Balance.zero, synchronizerStatusSnapshot: .default, walletConfig: .default, - walletEventsState: .emptyPlaceHolder + transactionListState: .emptyPlaceHolder ) } } @@ -264,7 +264,7 @@ extension HomeStore { state: .error(ZcashError.synchronizerNotPrepared) ), walletConfig: .default, - walletEventsState: .emptyPlaceHolder + transactionListState: .emptyPlaceHolder ), reducer: HomeReducer(networkType: .testnet) ) diff --git a/modules/Sources/Features/Home/HomeView.swift b/modules/Sources/Features/Home/HomeView.swift index 877f4d20..54025603 100644 --- a/modules/Sources/Features/Home/HomeView.swift +++ b/modules/Sources/Features/Home/HomeView.swift @@ -2,7 +2,7 @@ import SwiftUI import ComposableArchitecture import StoreKit import Generated -import WalletEventsFlow +import TransactionList import Settings import UIComponents @@ -20,9 +20,8 @@ public struct HomeView: View { VStack { balance(viewStore) - WalletEventsFlowView(store: store.historyStore(), tokenName: tokenName) + TransactionListView(store: store.historyStore(), tokenName: tokenName) } - .padding() .applyScreenBackground() .onAppear { viewStore.send(.onAppear) @@ -52,26 +51,26 @@ public struct HomeView: View { extension HomeView { func balance(_ viewStore: HomeViewStore) -> some View { - Group { + VStack(spacing: 0) { Button { viewStore.send(.balanceBreakdown) } label: { BalanceTitle(balance: viewStore.shieldedBalance.data.total) } + .padding(.top, 40) - if viewStore.walletConfig.isEnabled(.showFiatConversion) { - Text("$\(viewStore.totalCurrencyBalance.decimalZashiFormatted())") - .font(.custom(FontFamily.Inter.regular.name, size: 20)) - } - if viewStore.migratingDatabase { Text(L10n.Home.migratingDatabases) + .padding(.top, 10) + .padding(.bottom, 30) } else { Text(L10n.Balance.available(viewStore.shieldedBalance.data.verified.decimalZashiFormatted(), tokenName)) .font(.custom(FontFamily.Inter.regular.name, size: 12)) .accessDebugMenuWithHiddenGesture { viewStore.send(.debugMenuStartup) } + .padding(.top, 10) + .padding(.bottom, 30) } } .foregroundColor(Asset.Colors.primary.color) diff --git a/modules/Sources/Features/ImportWallet/ImportWalletView.swift b/modules/Sources/Features/ImportWallet/ImportWalletView.swift index 6e8ef3dc..b8d5d307 100644 --- a/modules/Sources/Features/ImportWallet/ImportWalletView.swift +++ b/modules/Sources/Features/ImportWallet/ImportWalletView.swift @@ -70,7 +70,7 @@ public struct ImportWalletView: View { VStack { Text(L10n.ImportWallet.enterPlaceholder) .font(.custom(FontFamily.Inter.regular.name, size: 13)) - .foregroundColor(Asset.Colors.suppressed72.color) + .foregroundColor(Asset.Colors.shade72.color) .onTapGesture { isFocused = true } diff --git a/modules/Sources/Features/Sandbox/SandboxStore.swift b/modules/Sources/Features/Sandbox/SandboxStore.swift index c934508c..68dbd045 100644 --- a/modules/Sources/Features/Sandbox/SandboxStore.swift +++ b/modules/Sources/Features/Sandbox/SandboxStore.swift @@ -1,6 +1,6 @@ import ComposableArchitecture import SwiftUI -import WalletEventsFlow +import TransactionList public typealias SandboxStore = Store public typealias SandboxViewStore = ViewStore @@ -13,13 +13,13 @@ public struct SandboxReducer: ReducerProtocol { case recoveryPhraseDisplay case scan } - public var walletEventsState: WalletEventsFlowReducer.State + public var transactionListState: TransactionListReducer.State public var destination: Destination? } public enum Action: Equatable { case updateDestination(SandboxReducer.State.Destination?) - case walletEvents(WalletEventsFlowReducer.Action) + case transactionList(TransactionListReducer.Action) case reset } @@ -31,10 +31,10 @@ public struct SandboxReducer: ReducerProtocol { state.destination = destination return .none - case let .walletEvents(walletEventsAction): - return WalletEventsFlowReducer() - .reduce(into: &state.walletEventsState, action: walletEventsAction) - .map(SandboxReducer.Action.walletEvents) + case let .transactionList(transactionListAction): + return TransactionListReducer() + .reduce(into: &state.transactionListState, action: transactionListAction) + .map(SandboxReducer.Action.transactionList) case .reset: return .none @@ -45,10 +45,10 @@ public struct SandboxReducer: ReducerProtocol { // MARK: - Store extension SandboxStore { - func historyStore() -> WalletEventsFlowStore { + func historyStore() -> TransactionListStore { self.scope( - state: \.walletEventsState, - action: SandboxReducer.Action.walletEvents + state: \.transactionListState, + action: SandboxReducer.Action.transactionList ) } } @@ -56,20 +56,6 @@ extension SandboxStore { // MARK: - ViewStore extension SandboxViewStore { - func toggleSelectedTransaction() { - let isAlreadySelected = (self.selectedTranactionID != nil) - let walletEvent = self.walletEventsState.walletEvents[5] - let newDestination = isAlreadySelected ? nil : WalletEventsFlowReducer.State.Destination.showWalletEvent(walletEvent) - send(.walletEvents(.updateDestination(newDestination))) - } - - var selectedTranactionID: String? { - self.walletEventsState - .destination - .flatMap(/WalletEventsFlowReducer.State.Destination.showWalletEvent) - .map(\.id) - } - func bindingForDestination(_ destination: SandboxReducer.State.Destination) -> Binding { self.binding( get: { $0.destination == destination }, @@ -85,7 +71,7 @@ extension SandboxViewStore { extension SandboxReducer.State { public static var placeholder: Self { .init( - walletEventsState: .placeHolder, + transactionListState: .placeHolder, destination: nil ) } @@ -95,7 +81,7 @@ extension SandboxStore { public static var placeholder: SandboxStore { SandboxStore( initialState: SandboxReducer.State( - walletEventsState: .placeHolder, + transactionListState: .placeHolder, destination: nil ), reducer: SandboxReducer() diff --git a/modules/Sources/Features/Sandbox/SandboxView.swift b/modules/Sources/Features/Sandbox/SandboxView.swift index ecba24bb..ffce96db 100644 --- a/modules/Sources/Features/Sandbox/SandboxView.swift +++ b/modules/Sources/Features/Sandbox/SandboxView.swift @@ -1,7 +1,7 @@ import SwiftUI import ComposableArchitecture import RecoveryPhraseDisplay -import WalletEventsFlow +import TransactionList import Scan import SendFlow import ZcashLightClientKit @@ -29,7 +29,7 @@ public struct SandboxView: View { @ViewBuilder func view(for destination: SandboxReducer.State.Destination) -> some View { switch destination { case .history: - WalletEventsFlowView(store: store.historyStore(), tokenName: tokenName) + TransactionListView(store: store.historyStore(), tokenName: tokenName) case .send: SendFlowView( store: .init( @@ -77,11 +77,6 @@ public struct SandboxView: View { } Section(header: Text("Other Actions")) { - Button( - action: { viewStore.toggleSelectedTransaction() }, - label: { Text("Toggle Selected Transaction") } - ) - Button( action: { viewStore.send(.reset) }, label: { Text("Reset (to startup)") } @@ -93,7 +88,7 @@ public struct SandboxView: View { isPresented: viewStore.bindingForDestination(.history), content: { NavigationView { - WalletEventsFlowView(store: store.historyStore(), tokenName: tokenName) + TransactionListView(store: store.historyStore(), tokenName: tokenName) .toolbar { ToolbarItem { Button("Done") { viewStore.send(.updateDestination(nil)) } diff --git a/modules/Sources/Features/Settings/SettingsStore.swift b/modules/Sources/Features/Settings/SettingsStore.swift index c7ee50ee..8d3556fc 100644 --- a/modules/Sources/Features/Settings/SettingsStore.swift +++ b/modules/Sources/Features/Settings/SettingsStore.swift @@ -117,7 +117,6 @@ public struct SettingsReducer: ReducerProtocol { return .none case .privateDataConsent(.shareFinished): - state.destination = nil return .none case .privateDataConsent: diff --git a/modules/Sources/Features/Settings/Views/About.swift b/modules/Sources/Features/Settings/Views/About.swift index b982d92a..83325e18 100644 --- a/modules/Sources/Features/Settings/Views/About.swift +++ b/modules/Sources/Features/Settings/Views/About.swift @@ -44,7 +44,7 @@ public struct About: View { Text(L10n.Settings.About.info) .font(.custom(FontFamily.Inter.regular.name, size: 14)) - .foregroundColor(Asset.Colors.suppressed30.color) + .foregroundColor(Asset.Colors.shade30.color) Spacer() } diff --git a/modules/Sources/Features/Tabs/TabsView.swift b/modules/Sources/Features/Tabs/TabsView.swift index e6eb4232..5a8215c1 100644 --- a/modules/Sources/Features/Tabs/TabsView.swift +++ b/modules/Sources/Features/Tabs/TabsView.swift @@ -82,7 +82,7 @@ public struct TabsView: View { .foregroundColor(Asset.Colors.primary.color) Rectangle() .frame(height: 2) - .foregroundColor(Asset.Colors.tabsUnderline.color) + .foregroundColor(Asset.Colors.primaryTint.color) .matchedGeometryEffect(id: "Tabs", in: tabsID, properties: .frame) } else { Text("\(item.title)") diff --git a/modules/Sources/Features/TransactionList/TransactionListStore.swift b/modules/Sources/Features/TransactionList/TransactionListStore.swift new file mode 100644 index 00000000..6fb3de87 --- /dev/null +++ b/modules/Sources/Features/TransactionList/TransactionListStore.swift @@ -0,0 +1,202 @@ +import ComposableArchitecture +import SwiftUI +import ZcashLightClientKit +import Utils +import Models +import Generated +import Pasteboard +import SDKSynchronizer +import ZcashSDKEnvironment + +public typealias TransactionListStore = Store +public typealias TransactionListViewStore = ViewStore + +public struct TransactionListReducer: ReducerProtocol { + private enum CancelId { case timer } + + public struct State: Equatable { + public var latestMinedHeight: BlockHeight? + public var isScrollable = true + public var requiredTransactionConfirmations = 0 + public var transactionList: IdentifiedArrayOf + public var latestTranassctionId = "" + + public init( + latestMinedHeight: BlockHeight? = nil, + isScrollable: Bool = true, + requiredTransactionConfirmations: Int = 0, + transactionList: IdentifiedArrayOf + ) { + self.latestMinedHeight = latestMinedHeight + self.isScrollable = isScrollable + self.requiredTransactionConfirmations = requiredTransactionConfirmations + self.transactionList = transactionList + } + } + + public enum Action: Equatable { + case copyToPastboard(RedactableString) + case onAppear + case onDisappear + case synchronizerStateChanged(SyncStatus) + case transactionCollapseRequested(String) + case transactionAddressExpandRequested(String) + case transactionExpandRequested(String) + case transactionIdExpandRequested(String) + case updateTransactionList([TransactionState]) + } + + @Dependency(\.mainQueue) var mainQueue + @Dependency(\.pasteboard) var pasteboard + @Dependency(\.sdkSynchronizer) var sdkSynchronizer + @Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment + + public init() {} + + // swiftlint:disable:next cyclomatic_complexity + public func reduce(into state: inout State, action: Action) -> ComposableArchitecture.EffectTask { + switch action { + case .onAppear: + state.requiredTransactionConfirmations = zcashSDKEnvironment.requiredTransactionConfirmations + return .merge( + sdkSynchronizer.stateStream() + .throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true) + .map { TransactionListReducer.Action.synchronizerStateChanged($0.syncStatus) } + .eraseToEffect() + .cancellable(id: CancelId.timer, cancelInFlight: true), + .run { send in + await send(.updateTransactionList(try await sdkSynchronizer.getAllTransactions())) + } + ) + + case .onDisappear: + return .cancel(id: CancelId.timer) + + case .synchronizerStateChanged(.upToDate): + state.latestMinedHeight = sdkSynchronizer.latestState().latestBlockHeight + return .task { + return .updateTransactionList(try await sdkSynchronizer.getAllTransactions()) + } + + case .synchronizerStateChanged: + return .none + + case .updateTransactionList(let transactionList): + let sortedTransactionList = transactionList + .sorted(by: { lhs, rhs in + guard let lhsTimestamp = lhs.timestamp, let rhsTimestamp = rhs.timestamp else { + return false + } + return lhsTimestamp > rhsTimestamp + }).map { transaction in + if let index = state.transactionList.index(id: transaction.id) { + var copiedTransaction = transaction + + copiedTransaction.isAddressExpanded = state.transactionList[index].isAddressExpanded + copiedTransaction.isExpanded = state.transactionList[index].isExpanded + copiedTransaction.isIdExpanded = state.transactionList[index].isIdExpanded + + return copiedTransaction + } + + return transaction + } + state.transactionList = IdentifiedArrayOf(uniqueElements: sortedTransactionList) + state.latestTranassctionId = state.transactionList.first?.id ?? "" + return .none + + case .copyToPastboard(let value): + pasteboard.setString(value) + return .none + + case .transactionCollapseRequested(let id): + if let index = state.transactionList.index(id: id) { + state.transactionList[index].isAddressExpanded = false + state.transactionList[index].isExpanded = false + state.transactionList[index].isIdExpanded = false + } + return .none + + case .transactionAddressExpandRequested(let id): + if let index = state.transactionList.index(id: id) { + if state.transactionList[index].isExpanded { + state.transactionList[index].isAddressExpanded = true + } else { + state.transactionList[index].isExpanded = true + } + } + return .none + + case .transactionExpandRequested(let id): + if let index = state.transactionList.index(id: id) { + state.transactionList[index].isExpanded = true + } + return .none + + case .transactionIdExpandRequested(let id): + if let index = state.transactionList.index(id: id) { + if state.transactionList[index].isExpanded { + state.transactionList[index].isIdExpanded = true + } else { + state.transactionList[index].isExpanded = true + } + } + return .none + } + } +} + +// MARK: ViewStore + +extension TransactionListViewStore { + func isLatestTransaction(id: String) -> Bool { + state.latestTranassctionId == id + } +} + +// MARK: Placeholders + +extension TransactionListReducer.State { + public static var placeHolder: Self { + .init(transactionList: .mocked) + } + + public static var emptyPlaceHolder: Self { + .init(transactionList: []) + } +} + +extension TransactionListStore { + public static var placeholder: Store { + return Store( + initialState: .placeHolder, + reducer: TransactionListReducer() + .dependency(\.zcashSDKEnvironment, .testnet) + ) + } +} + +extension IdentifiedArrayOf where Element == TransactionState { + public static var placeholder: IdentifiedArrayOf { + return .init( + uniqueElements: (0..<30).map { + TransactionState( + fee: Zatoshi(10), + id: String($0), + status: .paid, + timestamp: 1234567, + zecAmount: Zatoshi(25) + ) + } + ) + } + + public static var mocked: IdentifiedArrayOf { + return .init( + uniqueElements: [ + TransactionState.mockedSent, + TransactionState.mockedReceived + ] + ) + } +} diff --git a/modules/Sources/Features/TransactionList/TransactionListView.swift b/modules/Sources/Features/TransactionList/TransactionListView.swift new file mode 100644 index 00000000..f111a45e --- /dev/null +++ b/modules/Sources/Features/TransactionList/TransactionListView.swift @@ -0,0 +1,48 @@ +import SwiftUI +import ComposableArchitecture +import Generated +import UIComponents + +public struct TransactionListView: View { + let store: TransactionListStore + let tokenName: String + + public init(store: TransactionListStore, tokenName: String) { + self.store = store + self.tokenName = tokenName + } + + public var body: some View { + WithViewStore(store, observe: { $0 }) { viewStore in + List { + ForEach(viewStore.transactionList) { transaction in + TransactionRowView( + viewStore: viewStore, + transaction: transaction, + tokenName: tokenName, + isLatestTransaction: viewStore.isLatestTransaction(id: transaction.id) + ) + .listRowInsets(EdgeInsets()) + } + .listRowBackground(Asset.Colors.shade97.color) + .listRowSeparator(.hidden) + } + .refreshable { + viewStore.send(.synchronizerStateChanged(.upToDate)) + } + .background(Asset.Colors.shade97.color) + .listStyle(.plain) + .onAppear { viewStore.send(.onAppear) } + .onDisappear(perform: { viewStore.send(.onDisappear) }) + } + } +} + +// MARK: - Previews + +#Preview { + NavigationView { + TransactionListView(store: .placeholder, tokenName: "ZEC") + .preferredColorScheme(.light) + } +} diff --git a/modules/Sources/Features/TransactionList/Views/CollapseTransactionView.swift b/modules/Sources/Features/TransactionList/Views/CollapseTransactionView.swift new file mode 100644 index 00000000..22cd6227 --- /dev/null +++ b/modules/Sources/Features/TransactionList/Views/CollapseTransactionView.swift @@ -0,0 +1,37 @@ +// +// CollapseTransactionView.swift +// +// +// Created by Lukáš Korba on 04.11.2023. +// + +import SwiftUI +import Generated + +public struct CollapseTransactionView: View { + public var body: some View { + HStack { + Asset.Assets.upArrow.image + .resizable() + .frame(width: 10, height: 7) + .scaleEffect(0.6) + .font(.custom(FontFamily.Inter.black.name, size: 10)) + .foregroundColor(Asset.Colors.primaryTint.color) + .overlay { + Rectangle() + .stroke() + .frame(width: 10, height: 10) + .foregroundColor(Asset.Colors.shade72.color) + } + + Text(L10n.TransactionList.collapse) + .font(.custom(FontFamily.Inter.italic.name, size: 13)) + .foregroundColor(Asset.Colors.shade47.color) + .underline() + } + } +} + +#Preview { + CollapseTransactionView() +} diff --git a/modules/Sources/Features/TransactionList/Views/MessageView.swift b/modules/Sources/Features/TransactionList/Views/MessageView.swift new file mode 100644 index 00000000..dd2e1d2f --- /dev/null +++ b/modules/Sources/Features/TransactionList/Views/MessageView.swift @@ -0,0 +1,77 @@ +// +// MessageView.swift +// +// +// Created by Lukáš Korba on 05.11.2023. +// + +import SwiftUI +import Generated + +struct MessageView: View { + let message: String? + let isSpending: Bool + let isFailed: Bool + + public init( + message: String?, + isSpending: Bool, + isFailed: Bool = false + ) { + self.message = message + self.isSpending = isSpending + self.isFailed = isFailed + } + + var body: some View { + if let memoText = message { + VStack(alignment: .leading, spacing: 0) { + Text(L10n.TransactionList.messageTitle) + .font(.custom(FontFamily.Inter.medium.name, size: 13)) + .padding(.bottom, 8) + + VStack(alignment: .leading, spacing: 0) { + Color.clear.frame(height: 0) + + if isFailed { + Text(memoText) + .font(.custom(FontFamily.Inter.bold.name, size: 13)) + .foregroundColor(Asset.Colors.error.color) + .strikethrough() + .padding() + } else { + Text(memoText) + .font(.custom(FontFamily.Inter.bold.name, size: 13)) + .padding() + } + } + .messageShape( + filled: !isSpending + ? Asset.Colors.messageBcgReceived.color + : nil, + orientation: !isSpending + ? .right + : .left + ) + } + .padding(.bottom, 7) + .padding(.vertical, 10) + } else { + Text(L10n.TransactionList.noMessageIncluded) + .font(.custom(FontFamily.Inter.italic.name, size: 13)) + .foregroundColor(Asset.Colors.shade47.color) + } + } +} + +#Preview { + VStack(alignment: .leading) { + MessageView(message: "Test", isSpending: true) + .padding(.bottom, 50) + + MessageView(message: "Test", isSpending: true, isFailed: true) + .padding(.bottom, 50) + + MessageView(message: "Test", isSpending: false) + } +} diff --git a/modules/Sources/Features/TransactionList/Views/TransactionFeeView.swift b/modules/Sources/Features/TransactionList/Views/TransactionFeeView.swift new file mode 100644 index 00000000..0826e86a --- /dev/null +++ b/modules/Sources/Features/TransactionList/Views/TransactionFeeView.swift @@ -0,0 +1,38 @@ +// +// TransactionFeeView.swift +// +// +// Created by Lukáš Korba on 04.11.2023. +// + +import SwiftUI +import Generated +import ZcashLightClientKit + +public struct TransactionFeeView: View { + let fee: Zatoshi + + public init(fee: Zatoshi) { + self.fee = fee + } + + public var body: some View { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(L10n.TransactionList.transactionFee) + .font(.custom(FontFamily.Inter.regular.name, size: 13)) + .foregroundColor(Asset.Colors.shade47.color) + + Text(fee.decimalString(formatter: NumberFormatter.zashiBalanceFormatter)) + .font(.custom(FontFamily.Inter.bold.name, size: 13)) + .foregroundColor(Asset.Colors.shade47.color) + } + + Color.clear + } + } +} + +#Preview { + TransactionFeeView(fee: Zatoshi(10_000)) +} diff --git a/modules/Sources/Features/TransactionList/Views/TransactionHeaderView.swift b/modules/Sources/Features/TransactionList/Views/TransactionHeaderView.swift new file mode 100644 index 00000000..35344e3a --- /dev/null +++ b/modules/Sources/Features/TransactionList/Views/TransactionHeaderView.swift @@ -0,0 +1,193 @@ +// +// TransactionHeaderView.swift +// +// +// Created by Lukáš Korba on 05.11.2023. +// + +import SwiftUI +import ComposableArchitecture +import Generated +import Models +import UIComponents + +struct TransactionHeaderView: View { + let viewStore: TransactionListViewStore + let transaction: TransactionState + let isLatestTransaction: Bool + + init( + viewStore: TransactionListViewStore, + transaction: TransactionState, + isLatestTransaction: Bool = false + ) { + self.viewStore = viewStore + self.transaction = transaction + self.isLatestTransaction = isLatestTransaction + } + + var body: some View { + VStack(spacing: 0) { + Divider() + .padding(.horizontal, 30) + .padding(.bottom, 30) + .opacity(isLatestTransaction ? 0.0 : 1.0) + + HStack { + VStack(alignment: .leading, spacing: 5) { + HStack(spacing: 0) { + iconImage() + + titleText() + + addressArea() + + Spacer(minLength: 60) + + balanceView() + } + .padding(.trailing, 30) + + if transaction.zAddress != nil && transaction.isAddressExpanded { + HStack { + Text(transaction.address) + .font(.custom(FontFamily.Inter.bold.name, size: 13)) + .foregroundColor(Asset.Colors.shade47.color) + + Spacer(minLength: 100) + } + .padding(.horizontal, 60) + .padding(.bottom, 5) + } + + Text("\(transaction.dateString ?? "")") + .font(.custom(FontFamily.Inter.regular.name, size: 13)) + .foregroundColor(Asset.Colors.shade47.color) + .padding(.horizontal, 60) + } + } + } + .padding(.bottom, 30) + } + + @ViewBuilder private func iconImage() -> some View { + HStack { + Spacer() + + icon + .padding(.trailing, 10) + } + .frame(width: 60) + } + + @ViewBuilder private func titleText() -> some View { + Text(transaction.title) + .conditionalStrikethrough(transaction.status == .failed) + .conditionalFont( + condition: transaction.isPending, + true: .custom(FontFamily.Inter.boldItalic.name, size: 13), + else: .custom(FontFamily.Inter.bold.name, size: 13) + ) + .foregroundColor(transaction.titleColor) + .padding(.trailing, 8) + } + + @ViewBuilder private func addressArea() -> some View { + if transaction.zAddress == nil { + Asset.Assets.shield.image + .resizable() + .frame(width: 17, height: 13) + } else if !transaction.isAddressExpanded { + Button { + viewStore.send(.transactionAddressExpandRequested(transaction.id)) + } label: { + Text(transaction.address) + .font(.custom(FontFamily.Inter.regular.name, size: 13)) + .foregroundColor(Asset.Colors.shade47.color) + .lineLimit(1) + .truncationMode(.middle) + } + .disabled(!transaction.isExpanded) + } + } + + @ViewBuilder private func balanceView() -> some View { + if transaction.isExpanded { + HStack(spacing: 0) { + FullBalanceTitle( + primary: transaction.expandedAmountString.primary, + secondary: transaction.expandedAmountString.secondary, + fontName: FontFamily.Inter.regular.name, + primaryFontSize: 12, + secondaryFontSize: 8 + ) + } + .foregroundColor(transaction.balanceColor) + } else { + Text(transaction.roundedAmountString) + .font(.custom(FontFamily.Inter.regular.name, size: 12)) + .conditionalStrikethrough(transaction.status == .failed) + .foregroundColor(transaction.balanceColor) + } + } +} + +extension TransactionHeaderView { + var icon: some View { + HStack { + switch transaction.status { + case .paid, .failed: + Asset.Assets.fly.image + .resizable() + .frame(width: 20, height: 16) + + case .received, .sending, .receiving: + Asset.Assets.flyReceived.image + .resizable() + .frame(width: 17, height: 11) + } + } + } +} + +#Preview { + VStack(spacing: 0) { + TransactionHeaderView( + viewStore: ViewStore(.placeholder, observe: { $0 }), + transaction: .mockedFailed + ) + .listRowSeparator(.hidden) + + TransactionHeaderView( + viewStore: ViewStore(.placeholder, observe: { $0 }), + transaction: .mockedFailedReceive + ) + .listRowSeparator(.hidden) + + TransactionHeaderView( + viewStore: ViewStore(.placeholder, observe: { $0 }), + transaction: .mockedSent + ) + .listRowSeparator(.hidden) + + TransactionHeaderView( + viewStore: ViewStore(.placeholder, observe: { $0 }), + transaction: .mockedReceived + ) + .listRowSeparator(.hidden) + + TransactionHeaderView( + viewStore: ViewStore(.placeholder, observe: { $0 }), + transaction: .mockedSending + ) + .listRowSeparator(.hidden) + + TransactionHeaderView( + viewStore: ViewStore(.placeholder, observe: { $0 }), + transaction: .mockedReceiving + ) + .listRowSeparator(.hidden) + } + .listStyle(.plain) +} + diff --git a/modules/Sources/Features/TransactionList/Views/TransactionIDView.swift b/modules/Sources/Features/TransactionList/Views/TransactionIDView.swift new file mode 100644 index 00000000..9f2a488e --- /dev/null +++ b/modules/Sources/Features/TransactionList/Views/TransactionIDView.swift @@ -0,0 +1,65 @@ +// +// TransactionIdView.swift +// +// +// Created by Lukáš Korba on 05.11.2023. +// + +import SwiftUI +import ComposableArchitecture +import Generated +import Models + +struct TransactionIdView: View { + let viewStore: TransactionListViewStore + let transaction: TransactionState + + public init(viewStore: TransactionListViewStore, transaction: TransactionState) { + self.viewStore = viewStore + self.transaction = transaction + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + if !transaction.isIdExpanded { + HStack { + Text(L10n.TransactionList.transactionId) + + Button { + viewStore.send(.transactionIdExpandRequested(transaction.id)) + } label: { + Text(transaction.id) + .lineLimit(1) + .truncationMode(.middle) + } + + Spacer(minLength: 50) + } + .padding(.vertical, 20) + } + + if transaction.isIdExpanded { + Text(L10n.TransactionList.transactionId) + .padding(.top, 20) + .padding(.bottom, 4) + + HStack { + Text(transaction.id) + .font(.custom(FontFamily.Inter.bold.name, size: 13)) + + Spacer(minLength: 100) + } + .padding(.bottom, 20) + } + } + .font(.custom(FontFamily.Inter.regular.name, size: 13)) + .foregroundColor(Asset.Colors.shade47.color) + } +} + +#Preview { + TransactionIdView( + viewStore: ViewStore(.placeholder, observe: { $0 }), + transaction: .placeholder() + ) +} diff --git a/modules/Sources/Features/TransactionList/Views/TransactionRowView.swift b/modules/Sources/Features/TransactionList/Views/TransactionRowView.swift new file mode 100644 index 00000000..1678fbc3 --- /dev/null +++ b/modules/Sources/Features/TransactionList/Views/TransactionRowView.swift @@ -0,0 +1,109 @@ +// +// TransactionRowView.swift +// secant-testnet +// +// Created by Lukáš Korba on 21.06.2022. +// + +import SwiftUI +import ComposableArchitecture +import ZcashLightClientKit +import Models +import Generated +import UIComponents + +public struct TransactionRowView: View { + let viewStore: TransactionListViewStore + let transaction: TransactionState + let tokenName: String + let isLatestTransaction: Bool + + public init( + viewStore: TransactionListViewStore, + transaction: TransactionState, + tokenName: String, + isLatestTransaction: Bool = false + ) { + self.viewStore = viewStore + self.transaction = transaction + self.tokenName = tokenName + self.isLatestTransaction = isLatestTransaction + } + + public var body: some View { + Button { + viewStore.send(.transactionExpandRequested(transaction.id), animation: .default) + } label: { + if transaction.isExpanded { + TransactionHeaderView( + viewStore: viewStore, + transaction: transaction, + isLatestTransaction: isLatestTransaction + ) + } else { + TransactionHeaderView( + viewStore: viewStore, + transaction: transaction, + isLatestTransaction: isLatestTransaction + ) + } + } + + if transaction.isExpanded { + Group { + MessageView( + message: transaction.textMemo?.toString(), + isSpending: transaction.isSpending, + isFailed: transaction.status == .failed + ) + + TransactionIdView( + viewStore: viewStore, + transaction: transaction + ) + + if transaction.isSpending { + TransactionFeeView(fee: transaction.fee) + .padding(.vertical, 10) + } + + Button { + viewStore.send(.transactionCollapseRequested(transaction.id), animation: .default) + } label: { + CollapseTransactionView() + .padding(.vertical, 20) + } + } + .padding(.horizontal, 60) + } + } +} + +#Preview { + List { + TransactionRowView( + viewStore: ViewStore(.placeholder, observe: { $0 }), + transaction: .mockedFailed, + tokenName: "ZEC" + ) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets()) + + TransactionRowView( + viewStore: ViewStore(.placeholder, observe: { $0 }), + transaction: .mockedReceived, + tokenName: "ZEC" + ) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets()) + + TransactionRowView( + viewStore: ViewStore(.placeholder, observe: { $0 }), + transaction: .mockedSent, + tokenName: "ZEC" + ) + .listRowSeparator(.hidden) + .listRowInsets(EdgeInsets()) + } + .listStyle(.plain) +} diff --git a/modules/Sources/Features/WalletEventsFlow/Views/TransactionDetailView.swift b/modules/Sources/Features/WalletEventsFlow/Views/TransactionDetailView.swift deleted file mode 100644 index b53073b0..00000000 --- a/modules/Sources/Features/WalletEventsFlow/Views/TransactionDetailView.swift +++ /dev/null @@ -1,301 +0,0 @@ -import SwiftUI -import ComposableArchitecture -import ZcashLightClientKit -import Utils -import Models -import Generated -import UIComponents - -public struct TransactionDetailView: View { - public enum RowMark { - case neutral - case success - case fail - case inactive - case highlight - } - - let store: WalletEventsFlowStore - let transaction: TransactionState - let tokenName: String - - public init(store: WalletEventsFlowStore, transaction: TransactionState, tokenName: String) { - self.store = store - self.transaction = transaction - self.tokenName = tokenName - } - - public var body: some View { - WithViewStore(store) { viewStore in - VStack(alignment: .leading) { - header - - HStack { - VStack(alignment: .leading) { - switch transaction.status { - case .paid: - Text(L10n.Transaction.youSent(transaction.zecAmount.decimalZashiFormatted(), tokenName)) - .padding() - address(mark: .inactive, viewStore: viewStore) - memo(transaction, viewStore, mark: .highlight) - - case .sending: - Text(L10n.Transaction.youAreSending(transaction.zecAmount.decimalZashiFormatted(), tokenName)) - .padding() - address(mark: .inactive, viewStore: viewStore) - memo(transaction, viewStore, mark: .highlight) - - case .receiving: - Text(L10n.Transaction.youAreReceiving(transaction.zecAmount.decimalZashiFormatted(), tokenName)) - .padding() - memo(transaction, viewStore, mark: .highlight) - - case .received: - Text(L10n.Transaction.youReceived(transaction.zecAmount.decimalZashiFormatted(), tokenName)) - .padding() - memo(transaction, viewStore, mark: .highlight) - - case .failed: - Text(L10n.Transaction.youDidNotSent(transaction.zecAmount.decimalZashiFormatted(), tokenName)) - .padding() - - address(mark: .inactive, viewStore: viewStore) - memo(transaction, viewStore, mark: .highlight) - - Text(L10n.TransactionDetail.error(transaction.errorMessage ?? L10n.General.unknown)) - .padding() - } - } - - Spacer() - } - - Spacer() - } - .applyScreenBackground() - .navigationTitle(L10n.TransactionDetail.title) - } - .zashiBack() - } -} - -extension TransactionDetailView { - var header: some View { - HStack { - switch transaction.status { - case .sending: - Text(L10n.Transaction.sending) - Spacer() - case .receiving: - Text(L10n.Transaction.receiving) - Spacer() - case .failed: - Text("\(transaction.dateString ?? L10n.General.dateNotAvailable)") - default: - Text("\(transaction.dateString ?? L10n.General.dateNotAvailable)") - } - } - .padding() - } - - func address(mark: RowMark = .neutral, viewStore: WalletEventsFlowViewStore) -> some View { - Text("\(addressPrefixText) \(transaction.address)") - .lineLimit(1) - .truncationMode(.middle) - .padding() - } - - func memo( - _ transaction: TransactionState, - _ viewStore: WalletEventsFlowViewStore, - mark: RowMark = .neutral - ) -> some View { - Group { - if let memoText = transaction.textMemo?.toString() { - VStack(alignment: .leading) { - Text(L10n.Transaction.withMemo) - .padding(.leading) - Text("\"\(memoText)\"") - .multilineTextAlignment(.leading) - .padding(.leading) - } - } else { - EmptyView() - } - } - } - - func confirmed(mark: RowMark = .neutral, viewStore: WalletEventsFlowViewStore) -> some View { - HStack { - Text(L10n.Transaction.confirmed) - Spacer() - Text(L10n.Transaction.confirmedTimes(transaction.confirmationsWith(viewStore.latestMinedHeight))) - } - .transactionDetailRow(mark: mark) - } - - func confirming(mark: RowMark = .neutral, viewStore: WalletEventsFlowViewStore) -> some View { - HStack { - Text(L10n.Transaction.confirming(viewStore.requiredTransactionConfirmations)) - Spacer() - Text("\(transaction.confirmationsWith(viewStore.latestMinedHeight))/\(viewStore.requiredTransactionConfirmations)") - } - .transactionDetailRow(mark: mark) - } -} - -extension TransactionDetailView { - var addressPrefixText: String { - (transaction.status == .received || transaction.status == .receiving) - ? "" : L10n.Transaction.to - } - - var heightText: String { - guard let minedHeight = transaction.minedHeight else { return L10n.Transaction.unconfirmed } - return minedHeight > 0 ? String(minedHeight) : L10n.Transaction.unconfirmed - } -} - -// MARK: - Row modifier - -struct TransactionDetailRow: ViewModifier { - let mark: TransactionDetailView.RowMark - let textColor: Color - let backgroundColor: Color - - func body(content: Content) -> some View { - content - .foregroundColor(textColor) - .frame(maxWidth: .infinity, alignment: .leading) - .padding() - .background(backgroundColor) - .padding(.leading, 20) - .background(markColor(mark)) - } - - private func markColor(_ mark: TransactionDetailView.RowMark) -> Color { - let markColor: Color - - switch mark { - case .neutral: markColor = Asset.Colors.primary.color - case .success: markColor = Asset.Colors.primary.color - case .fail: markColor = Asset.Colors.primary.color - case .inactive: markColor = Asset.Colors.primary.color - case .highlight: markColor = Asset.Colors.primary.color - } - - return markColor - } -} - -extension View { - func transactionDetailRow( - mark: TransactionDetailView.RowMark = .neutral - ) -> some View { - modifier( - TransactionDetailRow( - mark: mark, - textColor: mark == .inactive ? - Asset.Colors.primary.color : - Asset.Colors.primary.color, - backgroundColor: Asset.Colors.primary.color - ) - ) - } -} - -// MARK: - Previews - -struct TransactionDetail_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - TransactionDetailView( - store: WalletEventsFlowStore.placeholder, - transaction: - TransactionState( - errorMessage: L10n.Error.rollBack, - memos: [Memo.placeholder], - minedHeight: 1_875_256, - zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", - fee: Zatoshi(1_000_000), - id: "ff3927e1f83df9b1b0dc75540ddc59ee435eecebae914d2e6dfe8576fbedc9a8", - status: .paid(success: true), - timestamp: 1234567, - zecAmount: Zatoshi(25_000_000) - ), - tokenName: "ZEC" - ) - .preferredColorScheme(.light) - } - - NavigationView { - TransactionDetailView( - store: WalletEventsFlowStore.placeholder, - transaction: - TransactionState( - errorMessage: L10n.Error.rollBack, - memos: [Memo.placeholder], - minedHeight: 1_875_256, - zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", - fee: Zatoshi(1_000_000), - id: "ff3927e1f83df9b1b0dc75540ddc59ee435eecebae914d2e6dfe8576fbedc9a8", - status: .sending, - timestamp: 1234567, - zecAmount: Zatoshi(25_000_000) - ), - tokenName: "ZEC" - ) - .preferredColorScheme(.light) - } - - NavigationView { - TransactionDetailView( - store: WalletEventsFlowStore.placeholder, - transaction: - TransactionState( - errorMessage: L10n.Error.rollBack, - memos: [Memo.placeholder], - minedHeight: 1_875_256, - zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", - fee: Zatoshi(1_000_000), - id: "ff3927e1f83df9b1b0dc75540ddc59ee435eecebae914d2e6dfe8576fbedc9a8", - status: .failed, - timestamp: 1234567, - zecAmount: Zatoshi(25_000_000) - ), - tokenName: "ZEC" - ) - .preferredColorScheme(.light) - } - - NavigationView { - TransactionDetailView( - store: WalletEventsFlowStore.placeholder, - transaction: - TransactionState( - errorMessage: L10n.Error.rollBack, - memos: [Memo.placeholder], - minedHeight: 1_875_256, - zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", - fee: Zatoshi(1_000_000), - id: "ff3927e1f83df9b1b0dc75540ddc59ee435eecebae914d2e6dfe8576fbedc9a8", - status: .received, - timestamp: 1234567, - zecAmount: Zatoshi(25_000_000) - ), - tokenName: "ZEC" - ) - .preferredColorScheme(.light) - } - } -} - -private extension Memo { - // swiftlint:disable:next force_try - static let placeholder = try! Memo(string: - """ - Testing some long memo so I can see many lines of text \ - instead of just one. This can take some time and I'm \ - bored to write all this stuff. - """) -} diff --git a/modules/Sources/Features/WalletEventsFlow/Views/TransactionRowView.swift b/modules/Sources/Features/WalletEventsFlow/Views/TransactionRowView.swift deleted file mode 100644 index 3210ab29..00000000 --- a/modules/Sources/Features/WalletEventsFlow/Views/TransactionRowView.swift +++ /dev/null @@ -1,162 +0,0 @@ -// -// TransactionRowView.swift -// secant-testnet -// -// Created by Lukáš Korba on 21.06.2022. -// - -import SwiftUI -import ZcashLightClientKit -import Models -import Generated - -public struct TransactionRowView: View { - let transaction: TransactionState - let tokenName: String - - public init(transaction: TransactionState, tokenName: String) { - self.transaction = transaction - self.tokenName = tokenName - } - - public var body: some View { - ZStack { - icon - - HStack { - VStack(alignment: .leading) { - Text(operationTitle) - .font(.custom(FontFamily.Inter.bold.name, size: 12)) - .foregroundColor(Asset.Colors.primary.color) - - Text("\(transaction.dateString ?? L10n.General.dateNotAvailable)") - .font(.custom(FontFamily.Inter.regular.name, size: 12)) - .foregroundColor(Asset.Colors.suppressed72.color) - .opacity(0.5) - } - - Spacer() - - Group { - Text(transaction.unarySymbol) - + Text(transaction.zecAmount.decimalZashiFormatted()) - } - .font(.custom(FontFamily.Inter.regular.name, size: 12)) - .foregroundColor( - transaction.unarySymbol == "-" - ? Asset.Colors.error.color - : Asset.Colors.primary.color - ) - .padding(.trailing, 30) - } - .padding(.leading, 80) - } - .frame(height: 60) - } -} - -extension TransactionRowView { - var operationTitle: String { - switch transaction.status { - case .paid: - return L10n.Transaction.sent - case .received: - return L10n.Transaction.received - case .failed: - // TODO: [#392] final text to be provided (https://github.com/zcash/secant-ios-wallet/issues/392) - return L10n.Transaction.failed - case .sending: - return L10n.Transaction.sending - case .receiving: - return L10n.Transaction.receiving - } - } - - var icon: some View { - let inTransaction = transaction.status == .received || transaction.status == .receiving - return HStack { - switch transaction.status { - case .paid, .received, .sending, .receiving: - Image(systemName: "envelope.fill") - .resizable() - .frame(width: 16, height: 12) - .foregroundColor(Asset.Colors.primary.color) - .padding(10) - .padding(.leading, 14) - case .failed: - // TODO: [#392] final icon to be provided (https://github.com/zcash/secant-ios-wallet/issues/392) - Circle() - .frame(width: 30, height: 30) - .foregroundColor(Color.red) - .padding(15) - } - - Spacer() - } - .padding(.leading, 15) - } -} - -struct TransactionRowView_Previews: PreviewProvider { - static var previews: some View { - TransactionRowView( - transaction: - .init( - zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", - fee: Zatoshi(10), - id: "2", - status: .paid(success: true), - timestamp: 1234567, - zecAmount: Zatoshi(123_000_000) - ), - tokenName: "ZEC" - ) - .applyScreenBackground() - .previewLayout(.fixed(width: 428, height: 60)) - - TransactionRowView( - transaction: - .init( - zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", - fee: Zatoshi(10), - id: "2", - status: .failed, - timestamp: 1234567, - zecAmount: Zatoshi(123_000_000) - ), - tokenName: "ZEC" - ) - .applyScreenBackground() - .previewLayout(.fixed(width: 428, height: 60)) - - TransactionRowView( - transaction: - .init( - zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", - fee: Zatoshi(10), - id: "2", - status: .sending, - timestamp: 1234567, - zecAmount: Zatoshi(123_000_000) - ), - tokenName: "ZEC" - ) - .applyScreenBackground() - .previewLayout(.fixed(width: 428, height: 60)) - - TransactionRowView( - transaction: - .init( - zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", - fee: Zatoshi(10), - id: "2", - status: .received, - timestamp: 1234567, - zecAmount: Zatoshi(123_000_000) - ), - tokenName: "ZEC" - ) - .applyScreenBackground() - .previewLayout(.fixed(width: 428, height: 60)) - } -} diff --git a/modules/Sources/Features/WalletEventsFlow/Views/WalletEvent+View.swift b/modules/Sources/Features/WalletEventsFlow/Views/WalletEvent+View.swift deleted file mode 100644 index 6541a6a3..00000000 --- a/modules/Sources/Features/WalletEventsFlow/Views/WalletEvent+View.swift +++ /dev/null @@ -1,89 +0,0 @@ -// -// WalletEvent+View.swift -// secant -// -// Created by Lukáš Korba on 30.05.2023. -// - -import ComposableArchitecture -import Models -import Generated -import SwiftUI -import ZcashLightClientKit - -// MARK: - Rows - -extension WalletEvent { - @ViewBuilder public func rowView(_ viewStore: WalletEventsFlowViewStore, tokenName: String) -> some View { - switch state { - case .transaction(let transaction): - TransactionRowView(transaction: transaction, tokenName: tokenName) - case .shielded(let zatoshi): - // TODO: [#390] implement design once shielding is supported - // https://github.com/zcash/secant-ios-wallet/issues/390 - Text(L10n.WalletEvent.Row.shielded(zatoshi.decimalZashiFormatted())) - .padding(.leading, 30) - case .walletImport: - // TODO: [#391] implement design once shielding is supported - // https://github.com/zcash/secant-ios-wallet/issues/391 - Text(L10n.WalletEvent.Row.import) - .padding(.leading, 30) - } - } -} - -// MARK: - Details - -extension WalletEvent { - @ViewBuilder public func detailView(_ store: WalletEventsFlowStore, tokenName: String) -> some View { - switch state { - case .transaction(let transaction): - TransactionDetailView(store: store, transaction: transaction, tokenName: tokenName) - case .shielded(let zatoshi): - // TODO: [#390] implement design once shielding is supported - // https://github.com/zcash/secant-ios-wallet/issues/390 - Text(L10n.WalletEvent.Detail.shielded(zatoshi.decimalZashiFormatted())) - case .walletImport: - // TODO: [#391] implement design once shielding is supported - // https://github.com/zcash/secant-ios-wallet/issues/391 - Text(L10n.WalletEvent.Detail.import) - } - } -} - -// MARK: - Placeholders - -private extension WalletEvent { - static func randomWalletEventState() -> WalletEvent.WalletEventState { - switch Int.random(in: 0..<3) { - case 1: return .shielded(Zatoshi(234_000_000)) - case 2: return .walletImport(BlockHeight(1_629_724)) - default: return .transaction(.placeholder) - } - } - - static func mockedWalletEventState(atIndex: Int) -> WalletEvent.WalletEventState { - switch atIndex % 5 { - case 0: return .transaction(.statePlaceholder(.received)) - case 1: return .transaction(.statePlaceholder(.failed)) - case 2: return .transaction(.statePlaceholder(.sending)) - case 3: return .transaction(.statePlaceholder(.receiving)) - case 4: return .transaction(.placeholder) - default: return .transaction(.placeholder) - } - } -} - -extension IdentifiedArrayOf where Element == WalletEvent { - public static var placeholder: IdentifiedArrayOf { - .init( - uniqueElements: (0..<30).map { - WalletEvent( - id: String($0), - state: WalletEvent.mockedWalletEventState(atIndex: $0), - timestamp: 1234567 - ) - } - ) - } -} diff --git a/modules/Sources/Features/WalletEventsFlow/WalletEventsFlowStore.swift b/modules/Sources/Features/WalletEventsFlow/WalletEventsFlowStore.swift deleted file mode 100644 index b71d239c..00000000 --- a/modules/Sources/Features/WalletEventsFlow/WalletEventsFlowStore.swift +++ /dev/null @@ -1,252 +0,0 @@ -import ComposableArchitecture -import SwiftUI -import ZcashLightClientKit -import Utils -import Models -import Generated -import Pasteboard -import SDKSynchronizer -import ZcashSDKEnvironment - -public typealias WalletEventsFlowStore = Store -public typealias WalletEventsFlowViewStore = ViewStore - -public struct WalletEventsFlowReducer: ReducerProtocol { - private enum CancelId { case timer } - - public struct State: Equatable { - public enum Destination: Equatable { - case latest - case all - case showWalletEvent(WalletEvent) - } - - @PresentationState public var alert: AlertState? - public var destination: Destination? - public var latestMinedHeight: BlockHeight? - public var isScrollable = true - public var requiredTransactionConfirmations = 0 - public var walletEvents = IdentifiedArrayOf.placeholder - public var selectedWalletEvent: WalletEvent? - - public init( - destination: Destination? = nil, - latestMinedHeight: BlockHeight? = nil, - isScrollable: Bool = true, - requiredTransactionConfirmations: Int = 0, - walletEvents: IdentifiedArrayOf = .placeholder, - selectedWalletEvent: WalletEvent? = nil - ) { - self.destination = destination - self.latestMinedHeight = latestMinedHeight - self.isScrollable = isScrollable - self.requiredTransactionConfirmations = requiredTransactionConfirmations - self.walletEvents = walletEvents - self.selectedWalletEvent = selectedWalletEvent - } - } - - public enum Action: Equatable { - case alert(PresentationAction) - case copyToPastboard(RedactableString) - case onAppear - case onDisappear - case openBlockExplorer(URL?) - case updateDestination(WalletEventsFlowReducer.State.Destination?) - case synchronizerStateChanged(SyncStatus) - case updateWalletEvents([WalletEvent]) - case warnBeforeLeavingApp(URL?) - } - - @Dependency(\.mainQueue) var mainQueue - @Dependency(\.pasteboard) var pasteboard - @Dependency(\.sdkSynchronizer) var sdkSynchronizer - @Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment - - public init() {} - - // swiftlint:disable:next cyclomatic_complexity - public func reduce(into state: inout State, action: Action) -> ComposableArchitecture.EffectTask { - switch action { - case .onAppear: - state.requiredTransactionConfirmations = zcashSDKEnvironment.requiredTransactionConfirmations - return .merge( - sdkSynchronizer.stateStream() - .throttle(for: .seconds(0.2), scheduler: mainQueue, latest: true) - .map { WalletEventsFlowReducer.Action.synchronizerStateChanged($0.syncStatus) } - .eraseToEffect() - .cancellable(id: CancelId.timer, cancelInFlight: true), - .run { send in - await send(.updateWalletEvents(try await sdkSynchronizer.getAllTransactions())) - } - ) - - case .onDisappear: - return .cancel(id: CancelId.timer) - - case .synchronizerStateChanged(.upToDate): - state.latestMinedHeight = sdkSynchronizer.latestState().latestBlockHeight - return .task { - return .updateWalletEvents(try await sdkSynchronizer.getAllTransactions()) - } - - case .synchronizerStateChanged: - return .none - - case .updateWalletEvents(let walletEvents): - let sortedWalletEvents = walletEvents - .sorted(by: { lhs, rhs in - guard let lhsTimestamp = lhs.timestamp, let rhsTimestamp = rhs.timestamp else { - return false - } - return lhsTimestamp > rhsTimestamp - }) - state.walletEvents = IdentifiedArrayOf(uniqueElements: sortedWalletEvents) - return .none - - case .updateDestination(.showWalletEvent(let walletEvent)): - state.selectedWalletEvent = walletEvent - state.destination = .showWalletEvent(walletEvent) - return .none - - case .updateDestination(let destination): - state.destination = destination - if destination == nil { - state.selectedWalletEvent = nil - } - return .none - - case .copyToPastboard(let value): - pasteboard.setString(value) - return .none - - case .alert(.presented(let action)): - return Effect.send(action) - - case .alert(.dismiss): - state.alert = nil - return .none - - case .alert: - return .none - - case .warnBeforeLeavingApp(let blockExplorerURL): - state.alert = AlertState.warnBeforeLeavingApp(blockExplorerURL) - return .none - - case .openBlockExplorer(let blockExplorerURL): - if let url = blockExplorerURL { - UIApplication.shared.open(url, options: [:], completionHandler: nil) - } - return .none - } - } -} - -// MARK: - ViewStore - -extension WalletEventsFlowViewStore { - private typealias Destination = WalletEventsFlowReducer.State.Destination - - func bindingForSelectedWalletEvent(_ walletEvent: WalletEvent?) -> Binding { - self.binding( - get: { - guard let walletEvent else { - return false - } - - return $0.destination.map(/WalletEventsFlowReducer.State.Destination.showWalletEvent) == walletEvent - }, - send: { isActive in - guard let walletEvent else { - return WalletEventsFlowReducer.Action.updateDestination(nil) - } - - return WalletEventsFlowReducer.Action.updateDestination( - isActive ? WalletEventsFlowReducer.State.Destination.showWalletEvent(walletEvent) : nil - ) - } - ) - } -} - -// MARK: Alerts - -extension AlertState where Action == WalletEventsFlowReducer.Action { - public static func warnBeforeLeavingApp(_ blockExplorerURL: URL?) -> AlertState { - AlertState { - TextState(L10n.WalletEvent.Alert.LeavingApp.title) - } actions: { - ButtonState(action: .openBlockExplorer(blockExplorerURL)) { - TextState(L10n.WalletEvent.Alert.LeavingApp.Button.seeOnline) - } - ButtonState(role: .cancel, action: .alert(.dismiss)) { - TextState(L10n.WalletEvent.Alert.LeavingApp.Button.nevermind) - } - } message: { - TextState(L10n.WalletEvent.Alert.LeavingApp.message) - } - } -} - -// MARK: Placeholders - -extension TransactionState { - public static var placeholder: Self { - .init( - zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", - fee: Zatoshi(10), - id: "2", - status: .paid(success: true), - timestamp: 1234567, - zecAmount: Zatoshi(123_000_000) - ) - } - - public static func statePlaceholder(_ status: Status) -> Self { - .init( - zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", - fee: Zatoshi(10), - id: "2", - status: status, - timestamp: 1234567, - zecAmount: Zatoshi(123_000_000) - ) - } -} - -extension WalletEventsFlowReducer.State { - public static var placeHolder: Self { - .init(walletEvents: .placeholder) - } - - public static var emptyPlaceHolder: Self { - .init(walletEvents: []) - } -} - -extension WalletEventsFlowStore { - public static var placeholder: Store { - return Store( - initialState: .placeHolder, - reducer: WalletEventsFlowReducer() - .dependency(\.zcashSDKEnvironment, .testnet) - ) - } -} - -extension IdentifiedArrayOf where Element == TransactionState { - public static var placeholder: IdentifiedArrayOf { - return .init( - uniqueElements: (0..<30).map { - TransactionState( - fee: Zatoshi(10), - id: String($0), - status: .paid(success: true), - timestamp: 1234567, - zecAmount: Zatoshi(25) - ) - } - ) - } -} diff --git a/modules/Sources/Features/WalletEventsFlow/WalletEventsFlowView.swift b/modules/Sources/Features/WalletEventsFlow/WalletEventsFlowView.swift deleted file mode 100644 index 994bca92..00000000 --- a/modules/Sources/Features/WalletEventsFlow/WalletEventsFlowView.swift +++ /dev/null @@ -1,58 +0,0 @@ -import SwiftUI -import ComposableArchitecture -import Generated -import UIComponents - -public struct WalletEventsFlowView: View { - let store: WalletEventsFlowStore - let tokenName: String - - public init(store: WalletEventsFlowStore, tokenName: String) { - self.store = store - self.tokenName = tokenName - } - - public var body: some View { - WithViewStore(store) { viewStore in - List { - walletEventsList(with: viewStore) - } - .navigationTitle(L10n.Transactions.title) - .listStyle(.plain) - .onAppear { viewStore.send(.onAppear) } - .onDisappear(perform: { viewStore.send(.onDisappear) }) - .navigationLinkEmpty(isActive: viewStore.bindingForSelectedWalletEvent(viewStore.selectedWalletEvent)) { - viewStore.selectedWalletEvent?.detailView(store, tokenName: tokenName) - } - } - .alert(store: store.scope( - state: \.$alert, - action: { .alert($0) } - )) - .zashiBack() - } -} - -extension WalletEventsFlowView { - func walletEventsList(with viewStore: WalletEventsFlowViewStore) -> some View { - ForEach(viewStore.walletEvents) { walletEvent in - walletEvent.rowView(viewStore, tokenName: tokenName) - .onTapGesture { - viewStore.send(.updateDestination(.showWalletEvent(walletEvent))) - } - .listRowInsets(EdgeInsets()) - .frame(height: 60) - } - } -} - -// MARK: - Previews - -struct TransactionView_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - WalletEventsFlowView(store: .placeholder, tokenName: "ZEC") - .preferredColorScheme(.light) - } - } -} diff --git a/modules/Sources/Generated/L10n.swift b/modules/Sources/Generated/L10n.swift index e53ee124..3a16bbb8 100644 --- a/modules/Sources/Generated/L10n.swift +++ b/modules/Sources/Generated/L10n.swift @@ -664,14 +664,16 @@ public enum L10n { public static func confirming(_ p1: Any) -> String { return L10n.tr("Localizable", "transaction.confirming", String(describing: p1), fallback: "Confirming ~%@mins") } - /// Failed - public static let failed = L10n.tr("Localizable", "transaction.failed", fallback: "Failed") + /// Receive failed + public static let failedReceive = L10n.tr("Localizable", "transaction.failedReceive", fallback: "Receive failed") + /// Send failed + public static let failedSend = L10n.tr("Localizable", "transaction.failedSend", fallback: "Send failed") /// Received public static let received = L10n.tr("Localizable", "transaction.received", fallback: "Received") - /// Receiving - public static let receiving = L10n.tr("Localizable", "transaction.receiving", fallback: "Receiving") - /// Sending - public static let sending = L10n.tr("Localizable", "transaction.sending", fallback: "Sending") + /// Receiving... + public static let receiving = L10n.tr("Localizable", "transaction.receiving", fallback: "Receiving...") + /// Sending... + public static let sending = L10n.tr("Localizable", "transaction.sending", fallback: "Sending...") /// Sent public static let sent = L10n.tr("Localizable", "transaction.sent", fallback: "Sent") /// to @@ -709,6 +711,18 @@ public enum L10n { /// Transaction detail public static let title = L10n.tr("Localizable", "transactionDetail.title", fallback: "Transaction detail") } + public enum TransactionList { + /// Collapse transaction + public static let collapse = L10n.tr("Localizable", "transactionList.collapse", fallback: "Collapse transaction") + /// Message + public static let messageTitle = L10n.tr("Localizable", "transactionList.messageTitle", fallback: "Message") + /// No message included in transaction + public static let noMessageIncluded = L10n.tr("Localizable", "transactionList.noMessageIncluded", fallback: "No message included in transaction") + /// Transaction Fee + public static let transactionFee = L10n.tr("Localizable", "transactionList.transactionFee", fallback: "Transaction Fee") + /// Transaction ID + public static let transactionId = L10n.tr("Localizable", "transactionList.transactionId", fallback: "Transaction ID") + } public enum Transactions { /// Transactions public static let title = L10n.tr("Localizable", "transactions.title", fallback: "Transactions") @@ -737,38 +751,6 @@ public enum L10n { public static let phraseAgain = L10n.tr("Localizable", "validationSuccess.button.phraseAgain", fallback: "Show me my phrase again") } } - public enum WalletEvent { - public enum Alert { - public enum LeavingApp { - /// While usually an acceptable risk, you will possibly exposing your behavior and interest in this transaction by going online. OH NOES! What will you do? - public static let message = L10n.tr("Localizable", "walletEvent.alert.leavingApp.message", fallback: "While usually an acceptable risk, you will possibly exposing your behavior and interest in this transaction by going online. OH NOES! What will you do?") - /// You are exiting your wallet - public static let title = L10n.tr("Localizable", "walletEvent.alert.leavingApp.title", fallback: "You are exiting your wallet") - public enum Button { - /// NEVERMIND - public static let nevermind = L10n.tr("Localizable", "walletEvent.alert.leavingApp.button.nevermind", fallback: "NEVERMIND") - /// SEE TX ONLINE - public static let seeOnline = L10n.tr("Localizable", "walletEvent.alert.leavingApp.button.seeOnline", fallback: "SEE TX ONLINE") - } - } - } - public enum Detail { - /// wallet import wallet event - public static let `import` = L10n.tr("Localizable", "walletEvent.detail.import", fallback: "wallet import wallet event") - /// shielded %@ detail - public static func shielded(_ p1: Any) -> String { - return L10n.tr("Localizable", "walletEvent.detail.shielded", String(describing: p1), fallback: "shielded %@ detail") - } - } - public enum Row { - /// wallet import wallet event - public static let `import` = L10n.tr("Localizable", "walletEvent.row.import", fallback: "wallet import wallet event") - /// shielded wallet event %@ - public static func shielded(_ p1: Any) -> String { - return L10n.tr("Localizable", "walletEvent.row.shielded", String(describing: p1), fallback: "shielded wallet event %@") - } - } - } public enum WelcomeScreen { /// Just Loading, one sec public static let subtitle = L10n.tr("Localizable", "welcomeScreen.subtitle", fallback: "Just Loading, one sec") diff --git a/modules/Sources/Generated/Resources/Assets.xcassets/FlyReceived.imageset/Contents.json b/modules/Sources/Generated/Resources/Assets.xcassets/FlyReceived.imageset/Contents.json new file mode 100644 index 00000000..8077a99b --- /dev/null +++ b/modules/Sources/Generated/Resources/Assets.xcassets/FlyReceived.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "flyReceived.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/Sources/Generated/Resources/Assets.xcassets/FlyReceived.imageset/flyReceived.png b/modules/Sources/Generated/Resources/Assets.xcassets/FlyReceived.imageset/flyReceived.png new file mode 100644 index 00000000..7c6c80b8 Binary files /dev/null and b/modules/Sources/Generated/Resources/Assets.xcassets/FlyReceived.imageset/flyReceived.png differ diff --git a/modules/Sources/Generated/Resources/Assets.xcassets/flyReceivedFilled.imageset/Contents.json b/modules/Sources/Generated/Resources/Assets.xcassets/flyReceivedFilled.imageset/Contents.json new file mode 100644 index 00000000..73522d2f --- /dev/null +++ b/modules/Sources/Generated/Resources/Assets.xcassets/flyReceivedFilled.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "flyReceivedFilled.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/Sources/Generated/Resources/Assets.xcassets/flyReceivedFilled.imageset/flyReceivedFilled.png b/modules/Sources/Generated/Resources/Assets.xcassets/flyReceivedFilled.imageset/flyReceivedFilled.png new file mode 100644 index 00000000..18cc0915 Binary files /dev/null and b/modules/Sources/Generated/Resources/Assets.xcassets/flyReceivedFilled.imageset/flyReceivedFilled.png differ diff --git a/modules/Sources/Generated/Resources/Assets.xcassets/shield.imageset/Contents.json b/modules/Sources/Generated/Resources/Assets.xcassets/shield.imageset/Contents.json new file mode 100644 index 00000000..dd8c90c7 --- /dev/null +++ b/modules/Sources/Generated/Resources/Assets.xcassets/shield.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "shield.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/Sources/Generated/Resources/Assets.xcassets/shield.imageset/shield.png b/modules/Sources/Generated/Resources/Assets.xcassets/shield.imageset/shield.png new file mode 100644 index 00000000..a943e484 Binary files /dev/null and b/modules/Sources/Generated/Resources/Assets.xcassets/shield.imageset/shield.png differ diff --git a/modules/Sources/Generated/Resources/Assets.xcassets/upArrow.imageset/Contents.json b/modules/Sources/Generated/Resources/Assets.xcassets/upArrow.imageset/Contents.json new file mode 100644 index 00000000..076d142a --- /dev/null +++ b/modules/Sources/Generated/Resources/Assets.xcassets/upArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "upArrow.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/Sources/Generated/Resources/Assets.xcassets/upArrow.imageset/upArrow.png b/modules/Sources/Generated/Resources/Assets.xcassets/upArrow.imageset/upArrow.png new file mode 100644 index 00000000..e6bc01f7 Binary files /dev/null and b/modules/Sources/Generated/Resources/Assets.xcassets/upArrow.imageset/upArrow.png differ diff --git a/modules/Sources/Generated/Resources/Colors.xcassets/messageBcgReceived.colorset/Contents.json b/modules/Sources/Generated/Resources/Colors.xcassets/messageBcgReceived.colorset/Contents.json new file mode 100644 index 00000000..5f48838f --- /dev/null +++ b/modules/Sources/Generated/Resources/Colors.xcassets/messageBcgReceived.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xCD", + "green" : "0xE9", + "red" : "0xF6" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/Sources/Generated/Resources/Colors.xcassets/tabsUnderline.colorset/Contents.json b/modules/Sources/Generated/Resources/Colors.xcassets/primaryTint.colorset/Contents.json similarity index 100% rename from modules/Sources/Generated/Resources/Colors.xcassets/tabsUnderline.colorset/Contents.json rename to modules/Sources/Generated/Resources/Colors.xcassets/primaryTint.colorset/Contents.json diff --git a/modules/Sources/Generated/Resources/Colors.xcassets/suppressed30.colorset/Contents.json b/modules/Sources/Generated/Resources/Colors.xcassets/shade30.colorset/Contents.json similarity index 100% rename from modules/Sources/Generated/Resources/Colors.xcassets/suppressed30.colorset/Contents.json rename to modules/Sources/Generated/Resources/Colors.xcassets/shade30.colorset/Contents.json diff --git a/modules/Sources/Generated/Resources/Colors.xcassets/suppressed47.colorset/Contents.json b/modules/Sources/Generated/Resources/Colors.xcassets/shade47.colorset/Contents.json similarity index 100% rename from modules/Sources/Generated/Resources/Colors.xcassets/suppressed47.colorset/Contents.json rename to modules/Sources/Generated/Resources/Colors.xcassets/shade47.colorset/Contents.json diff --git a/modules/Sources/Generated/Resources/Colors.xcassets/suppressed72.colorset/Contents.json b/modules/Sources/Generated/Resources/Colors.xcassets/shade72.colorset/Contents.json similarity index 100% rename from modules/Sources/Generated/Resources/Colors.xcassets/suppressed72.colorset/Contents.json rename to modules/Sources/Generated/Resources/Colors.xcassets/shade72.colorset/Contents.json diff --git a/modules/Sources/Generated/Resources/Colors.xcassets/shade97.colorset/Contents.json b/modules/Sources/Generated/Resources/Colors.xcassets/shade97.colorset/Contents.json new file mode 100644 index 00000000..3e7ba6b8 --- /dev/null +++ b/modules/Sources/Generated/Resources/Colors.xcassets/shade97.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.970", + "green" : "0.970", + "red" : "0.970" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/modules/Sources/Generated/Resources/Localizable.strings b/modules/Sources/Generated/Resources/Localizable.strings index 3d53bf16..8d41cc14 100644 --- a/modules/Sources/Generated/Resources/Localizable.strings +++ b/modules/Sources/Generated/Resources/Localizable.strings @@ -81,7 +81,6 @@ "importWallet.optionalBirthday" = "(optional)"; "importWallet.enterPlaceholder" = "Enter private seed here…"; - // MARK: - Tabs "tabs.account" = "Account"; "tabs.send" = "Send"; @@ -174,27 +173,6 @@ Sharing this private data is irrevocable — once you have shared this private d "sync.message.stopped" = "Stopped"; "sync.message.sync" = "%@%% Synced"; -// MARK: - Transactions -"transactions.title" = "Transactions"; -"transaction.sent" = "Sent"; -"transaction.sending" = "Sending"; -"transaction.receiving" = "Receiving"; -"transaction.received" = "Received"; -"transaction.failed" = "Failed"; -"transaction.youSent" = "You sent %@ %@"; -"transaction.youAreSending" = "You are sending %@ %@"; -"transaction.youAreReceiving" = "You are receiving %@ %@"; -"transaction.youReceived" = "You received %@ %@"; -"transaction.youDidNotSent" = "You DID NOT send %@ %@"; -"transaction.confirmed" = "Confirmed"; -"transaction.confirmedTimes" = "%@ times"; -"transaction.confirming" = "Confirming ~%@mins"; -"transaction.withMemo" = "With memo:"; -"transaction.to" = "to"; -"transaction.unconfirmed" = "unconfirmed"; -"transactionDetail.title" = "Transaction detail"; -"transactionDetail.error" = "Error: %@"; - // MARK: - Not Enough Free Space "nefs.message" = "Not enough space on disk to do synchronisation!"; @@ -221,15 +199,34 @@ Sharing this private data is irrevocable — once you have shared this private d "qrCodeFor" = "QR Code for %@"; "general.dateNotAvailable" = "date not available"; -// MARK: - Wallet event -"walletEvent.row.shielded" = "shielded wallet event %@"; -"walletEvent.row.import" = "wallet import wallet event"; -"walletEvent.detail.shielded" = "shielded %@ detail"; -"walletEvent.detail.import" = "wallet import wallet event"; -"walletEvent.alert.leavingApp.title" = "You are exiting your wallet"; -"walletEvent.alert.leavingApp.message" = "While usually an acceptable risk, you will possibly exposing your behavior and interest in this transaction by going online. OH NOES! What will you do?"; -"walletEvent.alert.leavingApp.button.nevermind" = "NEVERMIND"; -"walletEvent.alert.leavingApp.button.seeOnline" = "SEE TX ONLINE"; +// MARK: - Transaction List +"transactionList.collapse" = "Collapse transaction"; +"transactionList.messageTitle" = "Message"; +"transactionList.noMessageIncluded" = "No message included in transaction"; +"transactionList.transactionFee" = "Transaction Fee"; +"transactionList.transactionId" = "Transaction ID"; + +// MARK: - Transactions +"transactions.title" = "Transactions"; +"transaction.sent" = "Sent"; +"transaction.sending" = "Sending..."; +"transaction.receiving" = "Receiving..."; +"transaction.received" = "Received"; +"transaction.failedSend" = "Send failed"; +"transaction.failedReceive" = "Receive failed"; +"transaction.youSent" = "You sent %@ %@"; +"transaction.youAreSending" = "You are sending %@ %@"; +"transaction.youAreReceiving" = "You are receiving %@ %@"; +"transaction.youReceived" = "You received %@ %@"; +"transaction.youDidNotSent" = "You DID NOT send %@ %@"; +"transaction.confirmed" = "Confirmed"; +"transaction.confirmedTimes" = "%@ times"; +"transaction.confirming" = "Confirming ~%@mins"; +"transaction.withMemo" = "With memo:"; +"transaction.to" = "to"; +"transaction.unconfirmed" = "unconfirmed"; +"transactionDetail.title" = "Transaction detail"; +"transactionDetail.error" = "Error: %@"; // MARK: - Local authentication "localAuthentication.reason" = "The Following content requires authentication."; diff --git a/modules/Sources/Generated/XCAssets+Generated.swift b/modules/Sources/Generated/XCAssets+Generated.swift index 4aba3911..8135846e 100644 --- a/modules/Sources/Generated/XCAssets+Generated.swift +++ b/modules/Sources/Generated/XCAssets+Generated.swift @@ -23,21 +23,27 @@ public typealias AssetImageTypeAlias = ImageAsset.UniversalImage // swiftlint:disable identifier_name line_length nesting type_body_length type_name public enum Asset { public enum Assets { - public static let fly = ImageAsset(name: "Fly") - public static let splashHi = ImageAsset(name: "SplashHi") - public static let welcomeScreenLogo = ImageAsset(name: "WelcomeScreenLogo") - public static let zashiLogo = ImageAsset(name: "ZashiLogo") + public static let fly = ImageAsset(name: "fly") + public static let flyReceived = ImageAsset(name: "flyReceived") + public static let flyReceivedFilled = ImageAsset(name: "flyReceivedFilled") + public static let shield = ImageAsset(name: "shield") + public static let splashHi = ImageAsset(name: "splashHi") + public static let upArrow = ImageAsset(name: "upArrow") + public static let welcomeScreenLogo = ImageAsset(name: "welcomeScreenLogo") + public static let zashiLogo = ImageAsset(name: "zashiLogo") public static let zashiTitle = ImageAsset(name: "zashiTitle") } public enum Colors { public static let error = ColorAsset(name: "error") + public static let messageBcgReceived = ColorAsset(name: "messageBcgReceived") public static let primary = ColorAsset(name: "primary") + public static let primaryTint = ColorAsset(name: "primaryTint") public static let secondary = ColorAsset(name: "secondary") + public static let shade30 = ColorAsset(name: "shade30") + public static let shade47 = ColorAsset(name: "shade47") + public static let shade72 = ColorAsset(name: "shade72") + public static let shade97 = ColorAsset(name: "shade97") public static let splash = ColorAsset(name: "splash") - public static let suppressed30 = ColorAsset(name: "suppressed30") - public static let suppressed47 = ColorAsset(name: "suppressed47") - public static let suppressed72 = ColorAsset(name: "suppressed72") - public static let tabsUnderline = ColorAsset(name: "tabsUnderline") } } // swiftlint:enable identifier_name line_length nesting type_body_length type_name diff --git a/modules/Sources/Models/TransactionState.swift b/modules/Sources/Models/TransactionState.swift index 166afff6..1b44d281 100644 --- a/modules/Sources/Models/TransactionState.swift +++ b/modules/Sources/Models/TransactionState.swift @@ -6,17 +6,18 @@ // import Foundation +import SwiftUI import ZcashLightClientKit import Generated /// Representation of the transaction on the SDK side, used as a bridge to the TCA wallet side. public struct TransactionState: Equatable, Identifiable { public enum Status: Equatable { - case paid(success: Bool) - case received case failed - case sending + case paid + case received case receiving + case sending } public var errorMessage: String? @@ -25,6 +26,7 @@ public struct TransactionState: Equatable, Identifiable { public var minedHeight: BlockHeight? public var shielded = true public var zAddress: String? + public var isSentTransaction: Bool public var fee: Zatoshi public var id: String @@ -32,41 +34,129 @@ public struct TransactionState: Equatable, Identifiable { public var timestamp: TimeInterval? public var zecAmount: Zatoshi + public var isAddressExpanded: Bool + public var isExpanded: Bool + public var isIdExpanded: Bool + + // UI Colors + public var balanceColor: Color { + status == .failed + ? Asset.Colors.error.color + : isSpending + ? Asset.Colors.error.color + : Asset.Colors.primary.color + } + + public var titleColor: Color { + status == .failed + ? Asset.Colors.error.color + : isPending + ? Asset.Colors.shade47.color + : Asset.Colors.primary.color + } + + // UI Texts public var address: String { zAddress ?? "" } - public var unarySymbol: String { + public var title: String { + switch status { + case .failed: + return isSentTransaction + ? L10n.Transaction.failedSend + : L10n.Transaction.failedReceive + case .paid: + return L10n.Transaction.sent + case .received: + return L10n.Transaction.received + case .receiving: + return L10n.Transaction.receiving + case .sending: + return L10n.Transaction.sending + } + } + + public var roundedAmountString: String { + let formatted = zecAmount.decimalZashiFormatted() + switch status { case .paid, .sending: - return "-" + return "-\(formatted)" case .received, .receiving: - return "+" + return "+\(formatted)" case .failed: - return "" + return isSentTransaction ? "-\(formatted)" : "+\(formatted)" + } + } + + public var expandedAmountString: (primary: String, secondary: String) { + let formatted = zecAmount.decimalZashiFullFormatted() + + let smallPart = String(formatted.suffix(5)) + let normal = formatted.dropLast(5) + + switch status { + case .paid, .sending: + return (primary: "-\(normal)", secondary: smallPart) + case .received, .receiving: + return (primary: "+\(normal)", secondary: smallPart) + case .failed: + return isSentTransaction + ? (primary: "-\(normal)", secondary: smallPart) + : (primary: "+\(normal)", secondary: smallPart) } } public var dateString: String? { - guard let minedHeight else { return "" } + guard minedHeight != nil else { return "" } guard let timestamp else { return nil } return Date(timeIntervalSince1970: timestamp).asHumanReadable() } + // Helper flags + public var isPending: Bool { + switch status { + case .failed: + return false + case .paid: + return false + case .received: + return false + case .receiving: + return true + case .sending: + return true + } + } + + /// The purpose of this flag is to help understand if the transaction affected the wallet and a user paid a fee + public var isSpending: Bool { + switch status { + case .paid, .sending: + return true + case .received, .receiving: + return false + case .failed: + return isSentTransaction + } + } + + // Values public var totalAmount: Zatoshi { Zatoshi(zecAmount.amount + fee.amount) } - public var viewOnlineURL: URL? { - URL(string: "https://zcashblockexplorer.com/transactions/\(id)") - } - public var textMemo: Memo? { guard let memos else { return nil } for memo in memos { if case .text = memo { + guard let memoText = memo.toString(), !memoText.isEmpty else { + return nil + } + return memo } } @@ -85,7 +175,11 @@ public struct TransactionState: Equatable, Identifiable { id: String, status: Status, timestamp: TimeInterval? = nil, - zecAmount: Zatoshi + zecAmount: Zatoshi, + isSentTransaction: Bool = false, + isAddressExpanded: Bool = false, + isExpanded: Bool = false, + isIdExpanded: Bool = false ) { self.errorMessage = errorMessage self.expiryHeight = expiryHeight @@ -98,6 +192,10 @@ public struct TransactionState: Equatable, Identifiable { self.status = status self.timestamp = timestamp self.zecAmount = zecAmount + self.isSentTransaction = isSentTransaction + self.isAddressExpanded = isAddressExpanded + self.isExpanded = isExpanded + self.isIdExpanded = isIdExpanded } public func confirmationsWith(_ latestMinedHeight: BlockHeight?) -> BlockHeight { @@ -117,23 +215,30 @@ extension TransactionState { id = transaction.rawID.toHexStringTxId() timestamp = transaction.blockTime zecAmount = transaction.isSentTransaction ? Zatoshi(-transaction.value.amount) : transaction.value + isSentTransaction = transaction.isSentTransaction + isAddressExpanded = false + isExpanded = false + isIdExpanded = false self.memos = memos - let isSent = transaction.isSentTransaction - - // TODO: [#1313] SDK improvements so a client doesn't need to determing if the transaction isPending - // https://github.com/zcash/ZcashLightClientKit/issues/1313 - // The only reason why `latestBlockHeight` is provided here is to determine pending - // state of the transaction. SDK knows the latestBlockHeight so ideally ZcashTransaction.Overview - // already knows and provides isPending as a bool value. - // Once SDK's #1313 is done, adopt the SDK and remove latestBlockHeight here. - let isPending = transaction.isPending(currentHeight: latestBlockHeight) - - switch (isSent, isPending) { - case (true, true): status = .sending - case (true, false): status = .paid(success: minedHeight ?? 0 > 0) - case (false, true): status = .receiving - case (false, false): status = .received + // failed check + if let expiryHeight = transaction.expiryHeight, expiryHeight <= latestBlockHeight && minedHeight == nil { + status = .failed + } else { + // TODO: [#1313] SDK improvements so a client doesn't need to determing if the transaction isPending + // https://github.com/zcash/ZcashLightClientKit/issues/1313 + // The only reason why `latestBlockHeight` is provided here is to determine pending + // state of the transaction. SDK knows the latestBlockHeight so ideally ZcashTransaction.Overview + // already knows and provides isPending as a bool value. + // Once SDK's #1313 is done, adopt the SDK and remove latestBlockHeight here. + let isPending = transaction.isPending(currentHeight: latestBlockHeight) + + switch (isSentTransaction, isPending) { + case (true, true): status = .sending + case (true, false): status = .paid + case (false, true): status = .receiving + case (false, false): status = .received + } } } } @@ -162,6 +267,91 @@ extension TransactionState { zecAmount: status == .received ? amount : Zatoshi(-amount.amount) ) } + + public static let mockedSent = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "utest1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzjanqtl8uqp5vln3zyy246ejtx86vqftp73j7jg9099jxafyjhfm6u956j3", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .paid, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + public static let mockedReceived = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + fee: Zatoshi(10_000), + id: "t1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .received, + timestamp: 1699292621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + public static let mockedFailed = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: nil, + zAddress: "utest1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzjanqtl8uqp5vln3zyy246ejtx86vqftp73j7jg9099jxafyjhfm6u956j3", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .failed, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isSentTransaction: true, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + public static let mockedFailedReceive = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: nil, + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .failed, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isSentTransaction: false, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + public static let mockedSending = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: nil, + zAddress: "utest1vergg5jkp4xy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzjanqtl8uqp5vln3zyy246ejtx86vqftp73j7jg9099jxafyjhfm6u956j3", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .sending, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isSentTransaction: true, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + public static let mockedReceiving = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: nil, + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .receiving, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isSentTransaction: false, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) } public struct TransactionStateMockHelper { diff --git a/modules/Sources/Models/WalletEvent.swift b/modules/Sources/Models/WalletEvent.swift deleted file mode 100644 index f011b63b..00000000 --- a/modules/Sources/Models/WalletEvent.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// WalletEvent.swift -// secant-testnet -// -// Created by Lukáš Korba on 20.06.2022. -// - -import Foundation -import ComposableArchitecture -import SwiftUI -import ZcashLightClientKit -import Utils - -// MARK: - Model - -public struct WalletEvent: Equatable, Identifiable, Redactable { - public enum WalletEventState: Equatable { - case transaction(TransactionState) - case shielded(Zatoshi) - case walletImport(BlockHeight) - } - - public let id: String - public let state: WalletEventState - public var timestamp: TimeInterval? - - public init(id: String, state: WalletEventState, timestamp: TimeInterval? = nil) { - self.id = id - self.state = state - self.timestamp = timestamp - } -} diff --git a/modules/Sources/UIComponents/Balance/BalanceTitle.swift b/modules/Sources/UIComponents/Balance/BalanceTitle.swift index e6aa582c..1298c9df 100644 --- a/modules/Sources/UIComponents/Balance/BalanceTitle.swift +++ b/modules/Sources/UIComponents/Balance/BalanceTitle.swift @@ -25,7 +25,7 @@ public struct BalanceTitle: View { Circle() .frame(width: 25, height: 25) - .foregroundColor(Asset.Colors.tabsUnderline.color) + .foregroundColor(Asset.Colors.primaryTint.color) .overlay { ZcashSymbol() .frame(width: 15, height: 15) diff --git a/modules/Sources/UIComponents/Balance/FullBalanceTitle.swift b/modules/Sources/UIComponents/Balance/FullBalanceTitle.swift new file mode 100644 index 00000000..00c677b0 --- /dev/null +++ b/modules/Sources/UIComponents/Balance/FullBalanceTitle.swift @@ -0,0 +1,53 @@ +// +// FullBalanceTitle.swift +// +// +// Created by Lukáš Korba on 03.11.2023. +// + +import SwiftUI +import Generated +import ZcashLightClientKit + +public struct FullBalanceTitle: View { + let primary: String + let secondary: String + let fontName: String + let primaryFontSize: CGFloat + let secondaryFontSize: CGFloat + + public init( + primary: String, + secondary: String, + fontName: String, + primaryFontSize: CGFloat, + secondaryFontSize: CGFloat + ) { + self.primary = primary + self.secondary = secondary + self.fontName = fontName + self.primaryFontSize = primaryFontSize + self.secondaryFontSize = secondaryFontSize + } + + public var body: some View { + HStack { + Text(primary) + .font(.custom(fontName, size: primaryFontSize)) + + Text(secondary) + .font(.custom(fontName, size: secondaryFontSize)) + } + } +} + +#Preview { + VStack { + FullBalanceTitle( + primary: "0.001", + secondary: "35466", + fontName: FontFamily.Inter.regular.name, + primaryFontSize: 13, + secondaryFontSize: 9 + ) + } +} diff --git a/modules/Sources/UIComponents/Buttons/ZashiButton.swift b/modules/Sources/UIComponents/Buttons/ZashiButton.swift index 81b67e4f..ea34f296 100644 --- a/modules/Sources/UIComponents/Buttons/ZashiButton.swift +++ b/modules/Sources/UIComponents/Buttons/ZashiButton.swift @@ -24,16 +24,16 @@ public struct ZcashButtonStyle: ButtonStyle { .frame(height: 60) .foregroundColor( appearance == .primary ? .clear - : isEnabled ? Asset.Colors.primary.color : Asset.Colors.suppressed72.color + : isEnabled ? Asset.Colors.primary.color : Asset.Colors.shade72.color ) - .border(isEnabled ? Asset.Colors.primary.color : Asset.Colors.suppressed72.color) + .border(isEnabled ? Asset.Colors.primary.color : Asset.Colors.shade72.color) .offset(CGSize(width: 10, height: 10)) Rectangle() .frame(height: 60) .foregroundColor( appearance == .primary ? - isEnabled ? Asset.Colors.primary.color : Asset.Colors.suppressed72.color + isEnabled ? Asset.Colors.primary.color : Asset.Colors.shade72.color : Asset.Colors.secondary.color ) .border(Asset.Colors.primary.color) @@ -42,7 +42,7 @@ public struct ZcashButtonStyle: ButtonStyle { .font(.custom(FontFamily.Inter.medium.name, size: 14)) .foregroundColor( appearance == .primary ? Asset.Colors.secondary.color - : isEnabled ? Asset.Colors.primary.color : Asset.Colors.suppressed72.color + : isEnabled ? Asset.Colors.primary.color : Asset.Colors.shade72.color ) }) .offset(CGSize(width: offset, height: offset)) diff --git a/modules/Sources/UIComponents/Shapes/MessageShape.swift b/modules/Sources/UIComponents/Shapes/MessageShape.swift index 10248414..43f85867 100644 --- a/modules/Sources/UIComponents/Shapes/MessageShape.swift +++ b/modules/Sources/UIComponents/Shapes/MessageShape.swift @@ -8,15 +8,28 @@ import SwiftUI import Generated -struct MessageShape: Shape { - func path(in rect: CGRect) -> Path { +public struct MessageShape: Shape { + public enum Orientation: Sendable { + case left + case right + } + + let orientation: MessageShape.Orientation + + public func path(in rect: CGRect) -> Path { Path { path in path.move(to: CGPoint(x: 0, y: 0)) path.addLine(to: CGPoint(x: 0, y: rect.height)) - path.addLine(to: CGPoint(x: 15, y: rect.height)) - path.addLine(to: CGPoint(x: 15, y: rect.height + 7)) - path.addLine(to: CGPoint(x: 35, y: rect.height)) + if orientation == .left { + path.addLine(to: CGPoint(x: 15, y: rect.height)) + path.addLine(to: CGPoint(x: 15, y: rect.height + 7)) + path.addLine(to: CGPoint(x: 35, y: rect.height)) + } else { + path.addLine(to: CGPoint(x: rect.width - 35, y: rect.height)) + path.addLine(to: CGPoint(x: rect.width - 15, y: rect.height + 7)) + path.addLine(to: CGPoint(x: rect.width - 15, y: rect.height)) + } path.addLine(to: CGPoint(x: rect.width, y: rect.height)) path.addLine(to: CGPoint(x: rect.width, y: 0)) @@ -25,33 +38,56 @@ struct MessageShape: Shape { } } -struct MessageShapeModifier: ViewModifier { - let filled: Bool +public struct MessageShapeModifier: ViewModifier { + let filled: Color? + let orientation: MessageShape.Orientation - func body(content: Content) -> some View { + public func body(content: Content) -> some View { content - .overlay { - if filled { - MessageShape() - .foregroundColor(Asset.Colors.suppressed72.color) - MessageShape() - .stroke() - } else { - MessageShape() - .stroke() + .background { + if let filled { + MessageShape(orientation: orientation) + .foregroundColor(filled) } } + .overlay { + MessageShape(orientation: orientation) + .stroke() + } } } extension View { - public func messageShape(filled: Bool = false) -> some View { - modifier(MessageShapeModifier(filled: filled)) + public func messageShape( + filled: Color? = nil, + orientation: MessageShape.Orientation = .left + ) -> some View { + modifier( + MessageShapeModifier( + filled: filled, + orientation: orientation + ) + ) } } #Preview { - Text("some message") - .frame(width: 320, height: 145) - .messageShape(filled: true) + VStack { + Text("some message") + .padding(5) + .messageShape( + filled: Asset.Colors.messageBcgReceived.color, + orientation: .right + ) + .padding(.bottom, 20) + + Text("some message") + .padding(5) + .messageShape() + .padding(.bottom, 20) + + Text("some message") + .frame(width: 320, height: 145) + .messageShape(filled: Asset.Colors.shade72.color) + } } diff --git a/modules/Sources/UIComponents/Text/ConditionalFont.swift b/modules/Sources/UIComponents/Text/ConditionalFont.swift new file mode 100644 index 00000000..ebb9c779 --- /dev/null +++ b/modules/Sources/UIComponents/Text/ConditionalFont.swift @@ -0,0 +1,20 @@ +// +// ConditionalFont.swift +// +// +// Created by Lukáš Korba on 08.11.2023. +// + +import SwiftUI + +extension Text { + public func conditionalFont(condition: Bool, true: Font, else: Font) -> Text { + if condition { + return self + .font(`true`) + } else { + return self + .font(`else`) + } + } +} diff --git a/modules/Sources/UIComponents/Text/ConditionalStrikethrough.swift b/modules/Sources/UIComponents/Text/ConditionalStrikethrough.swift new file mode 100644 index 00000000..f512fcf9 --- /dev/null +++ b/modules/Sources/UIComponents/Text/ConditionalStrikethrough.swift @@ -0,0 +1,19 @@ +// +// ConditionalStrikethrough.swift +// +// +// Created by Lukáš Korba on 08.11.2023. +// + +import SwiftUI + +extension Text { + public func conditionalStrikethrough(_ on: Bool) -> Text { + if on { + return self + .strikethrough() + } else { + return self + } + } +} diff --git a/modules/Sources/UIComponents/TextFields/MessageEditor/MessageEditor.swift b/modules/Sources/UIComponents/TextFields/MessageEditor/MessageEditor.swift index 24db4e83..83d3442c 100644 --- a/modules/Sources/UIComponents/TextFields/MessageEditor/MessageEditor.swift +++ b/modules/Sources/UIComponents/TextFields/MessageEditor/MessageEditor.swift @@ -42,14 +42,18 @@ public struct MessageEditor: View { .focused($isFocused) .padding(2) .font(.custom(FontFamily.Inter.regular.name, size: 14)) - .messageShape(filled: !isEnabled) + .messageShape( + filled: isEnabled + ? nil + : Asset.Colors.shade72.color + ) .overlay { if message.isEmpty || !isEnabled { HStack { VStack { Text(L10n.Send.memoPlaceholder) .font(.custom(FontFamily.Inter.regular.name, size: 13)) - .foregroundColor(Asset.Colors.suppressed72.color) + .foregroundColor(Asset.Colors.shade72.color) .onTapGesture { isFocused = true } @@ -80,7 +84,7 @@ public struct MessageEditor: View { .font(.custom(FontFamily.Inter.bold.name, size: 13)) .foregroundColor( viewStore.isValid - ? Asset.Colors.suppressed72.color + ? Asset.Colors.shade72.color : Asset.Colors.error.color ) } diff --git a/modules/Sources/Utils/BalanceFormatter.swift b/modules/Sources/Utils/BalanceFormatter.swift index a63f89bf..55ce0e88 100644 --- a/modules/Sources/Utils/BalanceFormatter.swift +++ b/modules/Sources/Utils/BalanceFormatter.swift @@ -12,4 +12,8 @@ extension Zatoshi { public func decimalZashiFormatted() -> String { NumberFormatter.zashiBalanceFormatter.string(from: decimalValue.roundedZec) ?? "" } + + public func decimalZashiFullFormatted() -> String { + NumberFormatter.zcashNumberFormatter8FractionDigits.string(from: decimalValue.roundedZec) ?? "" + } } diff --git a/modules/Sources/Utils/Date+Readable.swift b/modules/Sources/Utils/Date+Readable.swift index f068e6a7..289f06b5 100644 --- a/modules/Sources/Utils/Date+Readable.swift +++ b/modules/Sources/Utils/Date+Readable.swift @@ -19,6 +19,9 @@ extension Date { let formatter = DateFormatter() formatter.dateStyle = .short formatter.timeStyle = .short + formatter.dateFormat = "yyyy-MM-dd HH:mm:ss"//"d MMM h:mm a" + formatter.amSymbol = "am" + formatter.pmSymbol = "pm" return formatter }() diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index c1eeebe0..06ac9999 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 34DA414928E439CD00F8CC61 /* sendingTransaction.json in Resources */ = {isa = PBXBuildFile; fileRef = 34DA414828E439CD00F8CC61 /* sendingTransaction.json */; }; 9E00319C2A272BB6003DFCEB /* SDKSynchronizer in Frameworks */ = {isa = PBXBuildFile; productRef = 9E00319B2A272BB6003DFCEB /* SDKSynchronizer */; }; 9E0031A42A272BC7003DFCEB /* SDKSynchronizer in Frameworks */ = {isa = PBXBuildFile; productRef = 9E0031A32A272BC7003DFCEB /* SDKSynchronizer */; }; + 9E0C0D702AFB842B00D69A16 /* TransactionStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0C0D6F2AFB842B00D69A16 /* TransactionStateTests.swift */; }; 9E1FAFB72AF2C6D40084CA3D /* PrivateDataConsentSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FAFB62AF2C6D40084CA3D /* PrivateDataConsentSnapshotTests.swift */; }; 9E1FAFBA2AF2C7F20084CA3D /* PrivateDataConsentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E1FAFB92AF2C7F20084CA3D /* PrivateDataConsentTests.swift */; }; 9E2F1C8C280ED6A7004E65FE /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 9E2F1C8B280ED6A7004E65FE /* LaunchScreen.storyboard */; }; @@ -52,7 +53,7 @@ 9E3451B529C8565500177D16 /* LoggerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E0F574A2980260D005304FA /* LoggerTests.swift */; }; 9E3451B629C8565500177D16 /* ZatoshiTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E39112D283F91600073DD9A /* ZatoshiTests.swift */; }; 9E3451B729C8565500177D16 /* DatabaseFilesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */; }; - 9E3451B829C856E700177D16 /* WalletEventsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF63E2819542C00BA3F17 /* WalletEventsTests.swift */; }; + 9E3451B829C856E700177D16 /* TransactionListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF63E2819542C00BA3F17 /* TransactionListTests.swift */; }; 9E3451B929C857C000177D16 /* AddressDetailsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E207C352966EC77003E2C9B /* AddressDetailsSnapshotTests.swift */; }; 9E3451BA29C857C500177D16 /* BalanceBreakdownSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */; }; 9E3451BB29C857C800177D16 /* HomeSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9ECC8C28589E150099D5A2 /* HomeSnapshotTests.swift */; }; @@ -62,7 +63,7 @@ 9E3451C229C857DB00177D16 /* TransactionSendingSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34429C6D28E703CD00F2B929 /* TransactionSendingSnapshotTests.swift */; }; 9E3451C329C857DD00177D16 /* SettingsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F02889539300DF7F17 /* SettingsSnapshotTests.swift */; }; 9E3451C429C857DF00177D16 /* View+UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E92AF0728530EBF007367AD /* View+UIImage.swift */; }; - 9E3451C529C857E400177D16 /* WalletEventsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB6112869882D00A02233 /* WalletEventsSnapshotTests.swift */; }; + 9E3451C529C857E400177D16 /* TransactionListSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7CB6112869882D00A02233 /* TransactionListSnapshotTests.swift */; }; 9E3451C629C857E700177D16 /* WelcomeSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9ECC8E28589E150099D5A2 /* WelcomeSnapshotTests.swift */; }; 9E46919A2AD5735E0082D7DF /* TabsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4691992AD5735E0082D7DF /* TabsTests.swift */; }; 9E4938D82ACE8E8F003C4C1D /* SecurityWarningSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4938D72ACE8E8F003C4C1D /* SecurityWarningSnapshotTests.swift */; }; @@ -120,6 +121,7 @@ 34F039B229ABCE8500CF0053 /* WalletConfigProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletConfigProviderTests.swift; sourceTree = ""; }; 9E01F8272833CDA0000EFC57 /* ScanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanTests.swift; sourceTree = ""; }; 9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFilesTests.swift; sourceTree = ""; }; + 9E0C0D6F2AFB842B00D69A16 /* TransactionStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionStateTests.swift; sourceTree = ""; }; 9E0F574A2980260D005304FA /* LoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests.swift; sourceTree = ""; }; 9E1FAFB62AF2C6D40084CA3D /* PrivateDataConsentSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateDataConsentSnapshotTests.swift; sourceTree = ""; }; 9E1FAFB92AF2C7F20084CA3D /* PrivateDataConsentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateDataConsentTests.swift; sourceTree = ""; }; @@ -133,7 +135,7 @@ 9E4691992AD5735E0082D7DF /* TabsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsTests.swift; sourceTree = ""; }; 9E4938D72ACE8E8F003C4C1D /* SecurityWarningSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityWarningSnapshotTests.swift; sourceTree = ""; }; 9E5AAEBF2A67CEC4003F283D /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; - 9E5BF63E2819542C00BA3F17 /* WalletEventsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletEventsTests.swift; sourceTree = ""; }; + 9E5BF63E2819542C00BA3F17 /* TransactionListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionListTests.swift; sourceTree = ""; }; 9E5BF643281FEC9900BA3F17 /* SendTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTests.swift; sourceTree = ""; }; 9E612C7829913F3600D09B09 /* SensitiveDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensitiveDataTests.swift; sourceTree = ""; }; 9E6612352878345000C75B70 /* endlessCircleProgress.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = endlessCircleProgress.json; sourceTree = ""; }; @@ -141,7 +143,7 @@ 9E6713F02897F81B00A6796F /* MultiLineTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiLineTextFieldTests.swift; sourceTree = ""; }; 9E7225F02889539300DF7F17 /* SettingsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSnapshotTests.swift; sourceTree = ""; }; 9E74CCCF29DC0628003D6E32 /* ReviewRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewRequestTests.swift; sourceTree = ""; }; - 9E7CB6112869882D00A02233 /* WalletEventsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletEventsSnapshotTests.swift; sourceTree = ""; }; + 9E7CB6112869882D00A02233 /* TransactionListSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionListSnapshotTests.swift; sourceTree = ""; }; 9E852D6429B0A86300CF4AC1 /* DebugTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugTests.swift; sourceTree = ""; }; 9E90751D2A269BE300269308 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 9E92AF0728530EBF007367AD /* View+UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+UIImage.swift"; sourceTree = ""; }; @@ -270,7 +272,7 @@ 9E4691982AD573420082D7DF /* TabsTests */, 9EF8135927ECC25E0075AF48 /* UtilTests */, 34F039B129ABCE8500CF0053 /* WalletConfigProviderTests */, - 9E5BF63D281953F900BA3F17 /* WalletEventsTests */, + 9E5BF63D281953F900BA3F17 /* TransactionListTests */, ); path = secantTests; sourceTree = ""; @@ -386,7 +388,7 @@ 346715A628E20FB30035F7C4 /* SendSnapshotTests */, 9E7225EF2889537E00DF7F17 /* SettingsSnapshotTests */, 9E92AF0728530EBF007367AD /* View+UIImage.swift */, - 9E7CB6102869881300A02233 /* WalletEventsSnapshotTests */, + 9E7CB6102869881300A02233 /* TransactionListSnapshotTests */, 9E9ECC8D28589E150099D5A2 /* WelcomeSnapshotTests */, ); path = SnapshotTests; @@ -408,12 +410,13 @@ path = SecurityWarningSnapshotTests; sourceTree = ""; }; - 9E5BF63D281953F900BA3F17 /* WalletEventsTests */ = { + 9E5BF63D281953F900BA3F17 /* TransactionListTests */ = { isa = PBXGroup; children = ( - 9E5BF63E2819542C00BA3F17 /* WalletEventsTests.swift */, + 9E5BF63E2819542C00BA3F17 /* TransactionListTests.swift */, + 9E0C0D6F2AFB842B00D69A16 /* TransactionStateTests.swift */, ); - path = WalletEventsTests; + path = TransactionListTests; sourceTree = ""; }; 9E5BF642281FEC8700BA3F17 /* SendTests */ = { @@ -476,12 +479,12 @@ path = ReviewRequestTests; sourceTree = ""; }; - 9E7CB6102869881300A02233 /* WalletEventsSnapshotTests */ = { + 9E7CB6102869881300A02233 /* TransactionListSnapshotTests */ = { isa = PBXGroup; children = ( - 9E7CB6112869882D00A02233 /* WalletEventsSnapshotTests.swift */, + 9E7CB6112869882D00A02233 /* TransactionListSnapshotTests.swift */, ); - path = WalletEventsSnapshotTests; + path = TransactionListSnapshotTests; sourceTree = ""; }; 9E7FE0B6282D1D9800C374E8 /* Resources */ = { @@ -908,7 +911,7 @@ 9E34519E29C849B400177D16 /* WalletConfigProviderTests.swift in Sources */, 9E3451C029C857D400177D16 /* RecoveryPhraseDisplaySnapshotTests.swift in Sources */, 9E3451B629C8565500177D16 /* ZatoshiTests.swift in Sources */, - 9E3451B829C856E700177D16 /* WalletEventsTests.swift in Sources */, + 9E3451B829C856E700177D16 /* TransactionListTests.swift in Sources */, 9E34519529C4A4BF00177D16 /* AddressDetailsTests.swift in Sources */, 9E3451B929C857C000177D16 /* AddressDetailsSnapshotTests.swift in Sources */, 9E34519B29C4A90700177D16 /* DeeplinkTests.swift in Sources */, @@ -917,12 +920,13 @@ 9E4938D82ACE8E8F003C4C1D /* SecurityWarningSnapshotTests.swift in Sources */, 9E3451C429C857DF00177D16 /* View+UIImage.swift in Sources */, 9E34519729C4A51100177D16 /* RecoveryPhraseBackupTests.swift in Sources */, + 9E0C0D702AFB842B00D69A16 /* TransactionStateTests.swift in Sources */, 9E3451BC29C857C800177D16 /* NotEnoughFeeSpaceSnapshots.swift in Sources */, 9E3451B229C8565500177D16 /* SecItemClientTests.swift in Sources */, 9E3451C629C857E700177D16 /* WelcomeSnapshotTests.swift in Sources */, 9E3451B329C8565500177D16 /* WalletBalance+testing.swift in Sources */, 9E3451AA29C84ED500177D16 /* CurrencySelectionTests.swift in Sources */, - 9E3451C529C857E400177D16 /* WalletEventsSnapshotTests.swift in Sources */, + 9E3451C529C857E400177D16 /* TransactionListSnapshotTests.swift in Sources */, 9E34519829C4A51100177D16 /* RecoveryPhraseDisplayReducerTests.swift in Sources */, 9E34519C29C4A91A00177D16 /* HomeTests.swift in Sources */, 9E1FAFB72AF2C6D40084CA3D /* PrivateDataConsentSnapshotTests.swift in Sources */, diff --git a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 557f71ea..88d93163 100644 --- a/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/secant.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -50,8 +50,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/google/GoogleUtilities.git", "state" : { - "revision" : "1cd556b33550982ec17f80e358253d905e756f0f", - "version" : "7.11.6" + "revision" : "f6532c8d65f8308cfdf2288cbe1971a509822680", + "version" : "7.12.0" } }, { @@ -95,8 +95,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/firebase/leveldb.git", "state" : { - "revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b", - "version" : "1.22.2" + "revision" : "9d108e9112aa1d65ce508facf804674546116d9c", + "version" : "1.22.3" } }, { diff --git a/secantTests/HomeTests/HomeTests.swift b/secantTests/HomeTests/HomeTests.swift index 608a239b..f1e16da3 100644 --- a/secantTests/HomeTests/HomeTests.swift +++ b/secantTests/HomeTests/HomeTests.swift @@ -27,7 +27,7 @@ class HomeTests: XCTestCase { shieldedBalance: Balance.zero, synchronizerStatusSnapshot: mockSnapshot, walletConfig: .default, - walletEventsState: .emptyPlaceHolder + transactionListState: .emptyPlaceHolder ), reducer: HomeReducer(networkType: .testnet) ) diff --git a/secantTests/SnapshotTests/HomeSnapshotTests/HomeSnapshotTests.swift b/secantTests/SnapshotTests/HomeSnapshotTests/HomeSnapshotTests.swift index db6901b6..151f5947 100644 --- a/secantTests/SnapshotTests/HomeSnapshotTests/HomeSnapshotTests.swift +++ b/secantTests/SnapshotTests/HomeSnapshotTests/HomeSnapshotTests.swift @@ -15,13 +15,13 @@ import Home class HomeSnapshotTests: XCTestCase { func testHomeSnapshot() throws { let transactionsHelper: [TransactionStateMockHelper] = [ - TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: true), uuid: "1"), + TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid, uuid: "1"), TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), status: .sending, uuid: "2"), TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .received, uuid: "3"), TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), status: .failed, uuid: "4") ] - let walletEvents: [WalletEvent] = transactionsHelper.map { + let transactionList: [TransactionState] = transactionsHelper.map { var transaction = TransactionState.placeholder( amount: $0.amount, fee: Zatoshi(10), @@ -32,7 +32,7 @@ class HomeSnapshotTests: XCTestCase { ) transaction.zAddress = "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po" - return WalletEvent(id: transaction.id, state: .transaction(transaction), timestamp: transaction.timestamp) + return transaction } let balance = WalletBalance(verified: 12_345_000, total: 12_345_000) @@ -43,7 +43,7 @@ class HomeSnapshotTests: XCTestCase { shieldedBalance: balance.redacted, synchronizerStatusSnapshot: .default, walletConfig: .default, - walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: walletEvents)) + transactionListState: .init(transactionList: IdentifiedArrayOf(uniqueElements: transactionList)) ), reducer: HomeReducer(networkType: .testnet) .dependency(\.diskSpaceChecker, .mockEmptyDisk) diff --git a/secantTests/SnapshotTests/TransactionListSnapshotTests/TransactionListSnapshotTests.swift b/secantTests/SnapshotTests/TransactionListSnapshotTests/TransactionListSnapshotTests.swift new file mode 100644 index 00000000..919c7178 --- /dev/null +++ b/secantTests/SnapshotTests/TransactionListSnapshotTests/TransactionListSnapshotTests.swift @@ -0,0 +1,31 @@ +// +// TransactionListSnapshotTests.swift +// secantTests +// +// Created by Lukáš Korba on 27.06.2022. +// + +import XCTest +import ComposableArchitecture +import ZcashLightClientKit +import Models +import TransactionList +import Home +@testable import secant_testnet + +class TransactionListSnapshotTests: XCTestCase { + func testFullTransactionListSnapshot() throws { + let store = TransactionListStore( + initialState: .placeHolder, + reducer: TransactionListReducer() + .dependency(\.sdkSynchronizer, .mock) + .dependency(\.mainQueue, .immediate) + ) + + // landing wallet events screen + addAttachments( + name: "\(#function)_initial", + TransactionListView(store: store, tokenName: "ZEC") + ) + } +} diff --git a/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift b/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift deleted file mode 100644 index 9c8762f0..00000000 --- a/secantTests/SnapshotTests/WalletEventsSnapshotTests/WalletEventsSnapshotTests.swift +++ /dev/null @@ -1,233 +0,0 @@ -// -// WalletEventsSnapshotTests.swift -// secantTests -// -// Created by Lukáš Korba on 27.06.2022. -// - -import XCTest -import ComposableArchitecture -import ZcashLightClientKit -import Models -import WalletEventsFlow -import Home -@testable import secant_testnet - -class WalletEventsSnapshotTests: XCTestCase { - func testFullWalletEventsSnapshot() throws { - let store = WalletEventsFlowStore( - initialState: .placeHolder, - reducer: WalletEventsFlowReducer() - .dependency(\.sdkSynchronizer, .mock) - .dependency(\.mainQueue, .immediate) - ) - - // landing wallet events screen - addAttachments( - name: "\(#function)_initial", - WalletEventsFlowView(store: store, tokenName: "ZEC") - ) - } - - func testWalletEventDetailSnapshot_sent() throws { - let memo = try? Memo(string: - """ - Testing some long memo so I can see many lines of text \ - instead of just one. This can take some time and I'm \ - bored to write all this stuff. - """) - guard let memo else { - XCTFail("testWalletEventDetailSnapshot_sent: memo is expected to be successfuly initialized") - return - } - - let transaction = TransactionState( - memos: [memo], - minedHeight: 1_875_256, - zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", - fee: Zatoshi(1_000_000), - id: "ff3927e1f83df9b1b0dc75540ddc59ee435eecebae914d2e6dfe8576fbedc9a8", - status: .paid(success: true), - timestamp: 1234567, - zecAmount: Zatoshi(25_000_000) - ) - - let walletEvent = WalletEvent(id: transaction.id, state: .transaction(transaction), timestamp: transaction.timestamp) - - let balance = WalletBalance(verified: 12_345_000, total: 12_345_000) - let store = HomeStore( - initialState: .init( - scanState: .placeholder, - shieldedBalance: balance.redacted, - synchronizerStatusSnapshot: .default, - walletConfig: .default, - walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])) - ), - reducer: HomeReducer(networkType: .testnet) - ) - - ViewStore(store).send(.walletEvents(.updateDestination(.showWalletEvent(walletEvent)))) - let walletEventsStore = WalletEventsFlowStore( - initialState: .placeHolder, - reducer: WalletEventsFlowReducer() - ) - - addAttachments( - name: "\(#function)_WalletEventDetail", - TransactionDetailView(store: walletEventsStore, transaction: transaction, tokenName: "ZEC") - ) - } - - func testWalletEventDetailSnapshot_received() throws { - let memo = try? Memo(string: - """ - Testing some long memo so I can see many lines of text \ - instead of just one. This can take some time and I'm \ - bored to write all this stuff. - """) - guard let memo else { - XCTFail("testWalletEventDetailSnapshot_received: memo is expected to be successfuly initialized") - return - } - - let transaction = TransactionState( - memos: [memo], - minedHeight: 1_875_256, - zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", - fee: Zatoshi(1_000_000), - id: "ff3927e1f83df9b1b0dc75540ddc59ee435eecebae914d2e6dfe8576fbedc9a8", - status: .received, - timestamp: 1234567, - zecAmount: Zatoshi(25_000_000) - ) - - let walletEvent = WalletEvent(id: transaction.id, state: .transaction(transaction), timestamp: transaction.timestamp) - - let balance = WalletBalance(verified: 12_345_000, total: 12_345_000) - let store = HomeStore( - initialState: .init( - scanState: .placeholder, - shieldedBalance: balance.redacted, - synchronizerStatusSnapshot: .default, - walletConfig: .default, - walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])) - ), - reducer: HomeReducer(networkType: .testnet) - ) - - ViewStore(store).send(.walletEvents(.updateDestination(.showWalletEvent(walletEvent)))) - let walletEventsStore = WalletEventsFlowStore( - initialState: .placeHolder, - reducer: WalletEventsFlowReducer() - ) - - addAttachments( - name: "\(#function)_WalletEventDetail", - TransactionDetailView(store: walletEventsStore, transaction: transaction, tokenName: "ZEC") - ) - } - - func testWalletEventDetailSnapshot_pending() throws { - let memo = try? Memo(string: - """ - Testing some long memo so I can see many lines of text \ - instead of just one. This can take some time and I'm \ - bored to write all this stuff. - """) - guard let memo else { - XCTFail("testWalletEventDetailSnapshot_pending: memo is expected to be successfuly initialized") - return - } - - let transaction = TransactionState( - memos: [memo], - minedHeight: 1_875_256, - zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", - fee: Zatoshi(1_000_000), - id: "ff3927e1f83df9b1b0dc75540ddc59ee435eecebae914d2e6dfe8576fbedc9a8", - status: .sending, - timestamp: 1234567, - zecAmount: Zatoshi(25_000_000) - ) - - let walletEvent = WalletEvent(id: transaction.id, state: .transaction(transaction), timestamp: transaction.timestamp) - - let balance = WalletBalance(verified: 12_345_000, total: 12_345_000) - let store = HomeStore( - initialState: .init( - scanState: .placeholder, - shieldedBalance: balance.redacted, - synchronizerStatusSnapshot: .default, - walletConfig: .default, - walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])) - ), - reducer: HomeReducer(networkType: .testnet) - ) - - let walletEventsState = WalletEventsFlowReducer.State( - requiredTransactionConfirmations: 10, - walletEvents: .placeholder - ) - - ViewStore(store).send(.walletEvents(.updateDestination(.showWalletEvent(walletEvent)))) - let walletEventsStore = WalletEventsFlowStore( - initialState: walletEventsState, - reducer: WalletEventsFlowReducer() - ) - - addAttachments( - name: "\(#function)_WalletEventDetail", - TransactionDetailView(store: walletEventsStore, transaction: transaction, tokenName: "ZEC") - ) - } - - func testWalletEventDetailSnapshot_failed() throws { - let memo = try? Memo(string: - """ - Testing some long memo so I can see many lines of text \ - instead of just one. This can take some time and I'm \ - bored to write all this stuff. - """) - guard let memo else { - XCTFail("testWalletEventDetailSnapshot_failed: memo is expected to be successfuly initialized") - return - } - - let transaction = TransactionState( - errorMessage: "possible roll back", - memos: [memo], - minedHeight: 1_875_256, - zAddress: "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po", - fee: Zatoshi(1_000_000), - id: "ff3927e1f83df9b1b0dc75540ddc59ee435eecebae914d2e6dfe8576fbedc9a8", - status: .failed, - timestamp: 1234567, - zecAmount: Zatoshi(25_000_000) - ) - - let walletEvent = WalletEvent(id: transaction.id, state: .transaction(transaction), timestamp: transaction.timestamp) - - let balance = WalletBalance(verified: 12_345_000, total: 12_345_000) - let store = HomeStore( - initialState: .init( - scanState: .placeholder, - shieldedBalance: balance.redacted, - synchronizerStatusSnapshot: .default, - walletConfig: .default, - walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: [walletEvent])) - ), - reducer: HomeReducer(networkType: .testnet) - ) - - ViewStore(store).send(.walletEvents(.updateDestination(.showWalletEvent(walletEvent)))) - let walletEventsStore = WalletEventsFlowStore( - initialState: .placeHolder, - reducer: WalletEventsFlowReducer() - ) - - addAttachments( - name: "\(#function)_WalletEventDetail", - TransactionDetailView(store: walletEventsStore, transaction: transaction, tokenName: "ZEC") - ) - } -} diff --git a/secantTests/TransactionListTests/TransactionListTests.swift b/secantTests/TransactionListTests/TransactionListTests.swift new file mode 100644 index 00000000..447cda5d --- /dev/null +++ b/secantTests/TransactionListTests/TransactionListTests.swift @@ -0,0 +1,279 @@ +// +// TransactionListTests.swift +// secantTests +// +// Created by Lukáš Korba on 27.04.2022. +// + +import XCTest +import ComposableArchitecture +import ZcashLightClientKit +import Pasteboard +import Models +import TransactionList +@testable import secant_testnet + +class TransactionListTests: XCTestCase { + @MainActor func testSynchronizerSubscription() async throws { + let store = TestStore( + initialState: TransactionListReducer.State( + isScrollable: true, + transactionList: [] + ), + reducer: TransactionListReducer() + ) + + store.dependencies.sdkSynchronizer = .mocked() + store.dependencies.sdkSynchronizer.getAllTransactions = { [] } + store.dependencies.mainQueue = .immediate + + await store.send(.onAppear) { state in + state.requiredTransactionConfirmations = 10 + } + + await store.receive(.synchronizerStateChanged(.unprepared)) + + await store.receive(.updateTransactionList([])) + + // ending the subscription + await store.send(.onDisappear) + + await store.finish() + } + + @MainActor func testSynchronizerStateChanged2Synced() async throws { + let mocked: [TransactionStateMockHelper] = [ + TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid, uuid: "aa11"), + TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"), + TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid, uuid: "cc33"), + TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"), + TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55"), + TransactionStateMockHelper( + date: 1651039606, + amount: Zatoshi(6), + status: .paid, + uuid: "ff66" + ), + TransactionStateMockHelper(date: 1651039303, amount: Zatoshi(7), uuid: "gg77"), + TransactionStateMockHelper(date: 1651039707, amount: Zatoshi(8), status: .paid, uuid: "hh88"), + TransactionStateMockHelper(date: 1651039808, amount: Zatoshi(9), uuid: "ii99") + ] + + let transactionList: [TransactionState] = mocked.map { + let transaction = TransactionState.placeholder( + amount: $0.amount, + fee: Zatoshi(10), + shielded: $0.shielded, + status: $0.amount.amount > 5 ? .sending : $0.status, + timestamp: $0.date, + uuid: $0.uuid + ) + return transaction + } + + let identifiedTransactionList = IdentifiedArrayOf(uniqueElements: transactionList) + + let store = TestStore( + initialState: TransactionListReducer.State( + isScrollable: true, + transactionList: identifiedTransactionList + ), + reducer: TransactionListReducer() + ) + + store.dependencies.mainQueue = .immediate + store.dependencies.sdkSynchronizer = .mocked() + + await store.send(.synchronizerStateChanged(.upToDate)) { state in + state.latestMinedHeight = 0 + } + + await store.receive(.updateTransactionList(transactionList)) { state in + let receivedTransactionList = IdentifiedArrayOf( + uniqueElements: + transactionList + .sorted(by: { lhs, rhs in + guard let lhsTimestamp = lhs.timestamp, let rhsTimestamp = rhs.timestamp else { + return false + } + return lhsTimestamp > rhsTimestamp + }) + ) + + state.transactionList = receivedTransactionList + state.latestTranassctionId = "ii99" + } + + await store.finish() + } + + func testCopyToPasteboard() throws { + let testPasteboard = PasteboardClient.testPasteboard + + let store = TestStore( + initialState: TransactionListReducer.State( + isScrollable: true, + transactionList: [] + ), + reducer: TransactionListReducer() + ) + + store.dependencies.pasteboard = testPasteboard + + let testText = "test text".redacted + store.send(.copyToPastboard(testText)) + + XCTAssertEqual( + testPasteboard.getString()?.data, + testText.data, + "WalletEvetns: `testCopyToPasteboard` is expected to match the input `\(testText.data)`" + ) + } + + // MARK: - Expansion + + func testTransactionExpansion() throws { + let id = "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja" + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: id, + status: .paid, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + let store = TestStore( + initialState: TransactionListReducer.State( + isScrollable: true, + transactionList: [transaction] + ), + reducer: TransactionListReducer() + ) + + store.send(.transactionExpandRequested(id)) { state in + state.transactionList[0].isExpanded = true + } + } + + func testAddressExpansionRequestedButTransactionIsNot() throws { + let id = "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja" + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: id, + status: .paid, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + let store = TestStore( + initialState: TransactionListReducer.State( + isScrollable: true, + transactionList: [transaction] + ), + reducer: TransactionListReducer() + ) + + store.send(.transactionAddressExpandRequested(id)) { state in + state.transactionList[0].isExpanded = true + } + } + + func testAddressExpansion() throws { + let id = "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja" + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: id, + status: .paid, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: true, + isIdExpanded: false + ) + + let store = TestStore( + initialState: TransactionListReducer.State( + isScrollable: true, + transactionList: [transaction] + ), + reducer: TransactionListReducer() + ) + + store.send(.transactionAddressExpandRequested(id)) { state in + state.transactionList[0].isAddressExpanded = true + } + } + + func testIdExpansionRequestedButTransactionIsNot() throws { + let id = "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja" + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: id, + status: .paid, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + let store = TestStore( + initialState: TransactionListReducer.State( + isScrollable: true, + transactionList: [transaction] + ), + reducer: TransactionListReducer() + ) + + store.send(.transactionIdExpandRequested(id)) { state in + state.transactionList[0].isExpanded = true + } + } + + func testIdExpansion() throws { + let id = "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja" + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: id, + status: .paid, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: true, + isIdExpanded: false + ) + + let store = TestStore( + initialState: TransactionListReducer.State( + isScrollable: true, + transactionList: [transaction] + ), + reducer: TransactionListReducer() + ) + + store.send(.transactionIdExpandRequested(id)) { state in + state.transactionList[0].isIdExpanded = true + } + } +} diff --git a/secantTests/TransactionListTests/TransactionStateTests.swift b/secantTests/TransactionListTests/TransactionStateTests.swift new file mode 100644 index 00000000..6aed640d --- /dev/null +++ b/secantTests/TransactionListTests/TransactionStateTests.swift @@ -0,0 +1,434 @@ +// +// TransactionStateTests.swift +// secantTests +// +// Created by Lukáš Korba on 08.11.2023. +// + +import XCTest +import ZcashLightClientKit +import Models +import Generated + +final class TransactionStateTests: XCTestCase { + // MARK: - Title tests (String & Color) + + func testTitleSent() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .paid, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.title, L10n.Transaction.sent) + } + + func testTitleSentColor() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .paid, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.titleColor, Asset.Colors.primary.color) + } + + func testTitleReceived() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .received, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.title, L10n.Transaction.received) + } + + func testTitleReceivedColor() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .received, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.titleColor, Asset.Colors.primary.color) + } + + func testTitleSending() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .sending, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.title, L10n.Transaction.sending) + } + + func testTitleSendingColor() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .sending, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.titleColor, Asset.Colors.shade47.color) + } + + func testTitleReceiving() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .receiving, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.title, L10n.Transaction.receiving) + } + + func testTitleReceivingColor() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .receiving, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.titleColor, Asset.Colors.shade47.color) + } + + func testTitleFailedSend() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .failed, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isSentTransaction: true, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.title, L10n.Transaction.failedSend) + } + + func testTitleFailedSendColor() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .failed, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isSentTransaction: true, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.titleColor, Asset.Colors.error.color) + } + + func testTitleFailedReceived() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .failed, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isSentTransaction: false, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.title, L10n.Transaction.failedReceive) + } + + func testTitleFailedReceivedColor() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .failed, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isSentTransaction: false, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.titleColor, Asset.Colors.error.color) + } + + // MARK: - Balance color + + func testBalanceSentColor() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .paid, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.balanceColor, Asset.Colors.error.color) + } + + func testBalanceReceivedColor() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .received, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.balanceColor, Asset.Colors.primary.color) + } + + func testBalanceFailedSendColor() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .failed, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isSentTransaction: true, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.balanceColor, Asset.Colors.error.color) + } + + func testBalanceFailedReceivedColor() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .failed, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isSentTransaction: false, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.balanceColor, Asset.Colors.error.color) + } + + func testBalanceSendingColor() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .sending, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isSentTransaction: false, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.balanceColor, Asset.Colors.error.color) + } + + func testBalanceReceivingColor() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .receiving, + timestamp: 1699290621, + zecAmount: Zatoshi(25_000_000), + isSentTransaction: false, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.balanceColor, Asset.Colors.primary.color) + } + + // MARK: - Balances and String representations for collapsed/expanded states + + func testExpandedPaidAmountTupple() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .paid, + timestamp: 1699290621, + zecAmount: Zatoshi(25_793_456), + isSentTransaction: false, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + // 0.025793456 + let expandedString = transaction.expandedAmountString + + XCTAssertEqual(expandedString.primary, "-0.257") + XCTAssertEqual(expandedString.secondary, "93456") + } + + func testExpandedReceivedAmountTupple() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .received, + timestamp: 1699290621, + zecAmount: Zatoshi(25_793_456), + isSentTransaction: false, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + let expandedString = transaction.expandedAmountString + + XCTAssertEqual(expandedString.primary, "+0.257") + XCTAssertEqual(expandedString.secondary, "93456") + } + + func testCollapsedPrimaryAmountRoundingForSend() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .paid, + timestamp: 1699290621, + zecAmount: Zatoshi(25_793_456), + isSentTransaction: false, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.roundedAmountString, "-0.258") + } + + func testCollapsedPrimaryAmountRoundingForReceive() throws { + let transaction = TransactionState( + memos: [try! Memo(string: "Hi, pay me and I'll pay you")], + minedHeight: BlockHeight(1), + zAddress: "tmP3uLtGx5GPddkq8a6ddmXhqJJ3vy6tpTE", + fee: Zatoshi(10_000), + id: "t1vergg5jkp4wy8sqfasw6s5zkdpnxvfxlxh35uuc3me7dp596y2r05t6dv9htwe3pf8ksrfr8ksca2lskzja", + status: .received, + timestamp: 1699290621, + zecAmount: Zatoshi(25_793_456), + isSentTransaction: false, + isAddressExpanded: false, + isExpanded: false, + isIdExpanded: false + ) + + XCTAssertEqual(transaction.roundedAmountString, "+0.258") + } +} diff --git a/secantTests/WalletEventsTests/WalletEventsTests.swift b/secantTests/WalletEventsTests/WalletEventsTests.swift deleted file mode 100644 index b05c1b88..00000000 --- a/secantTests/WalletEventsTests/WalletEventsTests.swift +++ /dev/null @@ -1,138 +0,0 @@ -// -// WalletEventsTests.swift -// secantTests -// -// Created by Lukáš Korba on 27.04.2022. -// - -import XCTest -import ComposableArchitecture -import ZcashLightClientKit -import Pasteboard -import Models -import WalletEventsFlow -@testable import secant_testnet - -class WalletEventsTests: XCTestCase { - @MainActor func testSynchronizerSubscription() async throws { - let store = TestStore( - initialState: WalletEventsFlowReducer.State( - destination: .latest, - isScrollable: true, - walletEvents: [] - ), - reducer: WalletEventsFlowReducer() - ) - - store.dependencies.sdkSynchronizer = .mocked() - store.dependencies.sdkSynchronizer.getAllTransactions = { [] } - store.dependencies.mainQueue = .immediate - - await store.send(.onAppear) { state in - state.requiredTransactionConfirmations = 10 - } - - await store.receive(.synchronizerStateChanged(.unprepared)) - - await store.receive(.updateWalletEvents([])) - - // ending the subscription - await store.send(.onDisappear) - - await store.finish() - } - - @MainActor func testSynchronizerStateChanged2Synced() async throws { - let mocked: [TransactionStateMockHelper] = [ - TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: false), uuid: "aa11"), - TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), uuid: "bb22"), - TransactionStateMockHelper(date: 1651039000, amount: Zatoshi(3), status: .paid(success: true), uuid: "cc33"), - TransactionStateMockHelper(date: 1651039505, amount: Zatoshi(4), uuid: "dd44"), - TransactionStateMockHelper(date: 1651039404, amount: Zatoshi(5), uuid: "ee55"), - TransactionStateMockHelper( - date: 1651039606, - amount: Zatoshi(6), - status: .paid(success: false), - uuid: "ff66" - ), - TransactionStateMockHelper(date: 1651039303, amount: Zatoshi(7), uuid: "gg77"), - TransactionStateMockHelper(date: 1651039707, amount: Zatoshi(8), status: .paid(success: true), uuid: "hh88"), - TransactionStateMockHelper(date: 1651039808, amount: Zatoshi(9), uuid: "ii99") - ] - - let walletEvents: [WalletEvent] = mocked.map { - let transaction = TransactionState.placeholder( - amount: $0.amount, - fee: Zatoshi(10), - shielded: $0.shielded, - status: $0.amount.amount > 5 ? .sending : $0.status, - timestamp: $0.date, - uuid: $0.uuid - ) - return WalletEvent( - id: transaction.id, - state: .transaction(transaction), - timestamp: transaction.timestamp - ) - } - - let identifiedWalletEvents = IdentifiedArrayOf(uniqueElements: walletEvents) - - let store = TestStore( - initialState: WalletEventsFlowReducer.State( - destination: .latest, - isScrollable: true, - walletEvents: identifiedWalletEvents - ), - reducer: WalletEventsFlowReducer() - ) - - store.dependencies.mainQueue = .immediate - store.dependencies.sdkSynchronizer = .mocked() - - await store.send(.synchronizerStateChanged(.upToDate)) { state in - state.latestMinedHeight = 0 - } - - await store.receive(.updateWalletEvents(walletEvents)) { state in - let receivedWalletEvents = IdentifiedArrayOf( - uniqueElements: - walletEvents - .sorted(by: { lhs, rhs in - guard let lhsTimestamp = lhs.timestamp, let rhsTimestamp = rhs.timestamp else { - return false - } - return lhsTimestamp > rhsTimestamp - }) - ) - - state.walletEvents = receivedWalletEvents - } - - await store.finish() - } - - func testCopyToPasteboard() throws { - let testPasteboard = PasteboardClient.testPasteboard - - let store = TestStore( - initialState: WalletEventsFlowReducer.State( - destination: .latest, - isScrollable: true, - walletEvents: [] - ), - reducer: WalletEventsFlowReducer() - ) - - store.dependencies.pasteboard = testPasteboard - - let testText = "test text".redacted - store.send(.copyToPastboard(testText)) - - XCTAssertEqual( - testPasteboard.getString()?.data, - testText.data, - "WalletEvetns: `testCopyToPasteboard` is expected to match the input `\(testText.data)`" - ) - } -}