[#887] UI tweaks for transactions on Home (#890)

- Concept of WalletEvents removed and replaced with TransactionState and TransactionList
- UI updated to enable list of transactions and expansion with the details
- Color naming cleanups and refactors
- Conditional font modifier implemented
- Conditional strikethrough modifier implemented
- Title + tests of the title for the transaction implemented
- Color + tests for the title of the transaction implemented
- All texts localized
- tests fot the TCA TransactionList implemented
This commit is contained in:
Lukas Korba 2023-11-13 11:28:43 +01:00 committed by GitHub
parent 3fa10e5147
commit 34f0077604
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 2211 additions and 1509 deletions

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1500"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "TransactionList"
BuildableName = "TransactionList"
BlueprintName = "TransactionList"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "TransactionList"
BuildableName = "TransactionList"
BlueprintName = "TransactionList"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -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",

View File

@ -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?

View File

@ -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

View File

@ -74,11 +74,11 @@ extension SDKSynchronizerClient {
rewind: @escaping (RewindPolicy) -> AnyPublisher<Void, Error> = { _ in return Empty<Void, Error>().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)
)

View File

@ -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)

View File

@ -9,7 +9,7 @@ import Utils
import Models
import Generated
import ReviewRequest
import WalletEventsFlow
import TransactionList
import Scan
public typealias HomeStore = Store<HomeReducer.State, HomeReducer.Action>
@ -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<State, Action> {
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)
)

View File

@ -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)

View File

@ -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
}

View File

@ -1,6 +1,6 @@
import ComposableArchitecture
import SwiftUI
import WalletEventsFlow
import TransactionList
public typealias SandboxStore = Store<SandboxReducer.State, SandboxReducer.Action>
public typealias SandboxViewStore = ViewStore<SandboxReducer.State, SandboxReducer.Action>
@ -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<Bool> {
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()

View File

@ -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)) }

View File

@ -117,7 +117,6 @@ public struct SettingsReducer: ReducerProtocol {
return .none
case .privateDataConsent(.shareFinished):
state.destination = nil
return .none
case .privateDataConsent:

View File

@ -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()
}

View File

@ -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)")

View File

@ -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<TransactionListReducer.State, TransactionListReducer.Action>
public typealias TransactionListViewStore = ViewStore<TransactionListReducer.State, TransactionListReducer.Action>
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<TransactionState>
public var latestTranassctionId = ""
public init(
latestMinedHeight: BlockHeight? = nil,
isScrollable: Bool = true,
requiredTransactionConfirmations: Int = 0,
transactionList: IdentifiedArrayOf<TransactionState>
) {
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<Action> {
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<TransactionListReducer.State, TransactionListReducer.Action> {
return Store(
initialState: .placeHolder,
reducer: TransactionListReducer()
.dependency(\.zcashSDKEnvironment, .testnet)
)
}
}
extension IdentifiedArrayOf where Element == TransactionState {
public static var placeholder: IdentifiedArrayOf<TransactionState> {
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<TransactionState> {
return .init(
uniqueElements: [
TransactionState.mockedSent,
TransactionState.mockedReceived
]
)
}
}

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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)
}
}

View File

@ -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))
}

View File

@ -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)
}

View File

@ -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()
)
}

View File

@ -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)
}

View File

@ -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.
""")
}

View File

@ -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))
}
}

View File

@ -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<WalletEvent> {
.init(
uniqueElements: (0..<30).map {
WalletEvent(
id: String($0),
state: WalletEvent.mockedWalletEventState(atIndex: $0),
timestamp: 1234567
)
}
)
}
}

View File

@ -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<WalletEventsFlowReducer.State, WalletEventsFlowReducer.Action>
public typealias WalletEventsFlowViewStore = ViewStore<WalletEventsFlowReducer.State, WalletEventsFlowReducer.Action>
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<Action>?
public var destination: Destination?
public var latestMinedHeight: BlockHeight?
public var isScrollable = true
public var requiredTransactionConfirmations = 0
public var walletEvents = IdentifiedArrayOf<WalletEvent>.placeholder
public var selectedWalletEvent: WalletEvent?
public init(
destination: Destination? = nil,
latestMinedHeight: BlockHeight? = nil,
isScrollable: Bool = true,
requiredTransactionConfirmations: Int = 0,
walletEvents: IdentifiedArrayOf<WalletEvent> = .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<Action>)
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<Action> {
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<Bool> {
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<WalletEventsFlowReducer.State, WalletEventsFlowReducer.Action> {
return Store(
initialState: .placeHolder,
reducer: WalletEventsFlowReducer()
.dependency(\.zcashSDKEnvironment, .testnet)
)
}
}
extension IdentifiedArrayOf where Element == TransactionState {
public static var placeholder: IdentifiedArrayOf<TransactionState> {
return .init(
uniqueElements: (0..<30).map {
TransactionState(
fee: Zatoshi(10),
id: String($0),
status: .paid(success: true),
timestamp: 1234567,
zecAmount: Zatoshi(25)
)
}
)
}
}

View File

@ -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)
}
}
}

View File

@ -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")

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "flyReceived.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "shield.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "upArrow.png",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 822 B

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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.";

View File

@ -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

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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)

View File

@ -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
)
}
}

View File

@ -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))

View File

@ -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)
}
}

View File

@ -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`)
}
}
}

View File

@ -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
}
}
}

View File

@ -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
)
}

View File

@ -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) ?? ""
}
}

View File

@ -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
}()

View File

@ -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 = "<group>"; };
9E01F8272833CDA0000EFC57 /* ScanTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanTests.swift; sourceTree = "<group>"; };
9E02B56B27FED475005B809B /* DatabaseFilesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseFilesTests.swift; sourceTree = "<group>"; };
9E0C0D6F2AFB842B00D69A16 /* TransactionStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionStateTests.swift; sourceTree = "<group>"; };
9E0F574A2980260D005304FA /* LoggerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoggerTests.swift; sourceTree = "<group>"; };
9E1FAFB62AF2C6D40084CA3D /* PrivateDataConsentSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateDataConsentSnapshotTests.swift; sourceTree = "<group>"; };
9E1FAFB92AF2C7F20084CA3D /* PrivateDataConsentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PrivateDataConsentTests.swift; sourceTree = "<group>"; };
@ -133,7 +135,7 @@
9E4691992AD5735E0082D7DF /* TabsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabsTests.swift; sourceTree = "<group>"; };
9E4938D72ACE8E8F003C4C1D /* SecurityWarningSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecurityWarningSnapshotTests.swift; sourceTree = "<group>"; };
9E5AAEBF2A67CEC4003F283D /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = "<group>"; };
9E5BF63E2819542C00BA3F17 /* WalletEventsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletEventsTests.swift; sourceTree = "<group>"; };
9E5BF63E2819542C00BA3F17 /* TransactionListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionListTests.swift; sourceTree = "<group>"; };
9E5BF643281FEC9900BA3F17 /* SendTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendTests.swift; sourceTree = "<group>"; };
9E612C7829913F3600D09B09 /* SensitiveDataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SensitiveDataTests.swift; sourceTree = "<group>"; };
9E6612352878345000C75B70 /* endlessCircleProgress.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = endlessCircleProgress.json; sourceTree = "<group>"; };
@ -141,7 +143,7 @@
9E6713F02897F81B00A6796F /* MultiLineTextFieldTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiLineTextFieldTests.swift; sourceTree = "<group>"; };
9E7225F02889539300DF7F17 /* SettingsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSnapshotTests.swift; sourceTree = "<group>"; };
9E74CCCF29DC0628003D6E32 /* ReviewRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewRequestTests.swift; sourceTree = "<group>"; };
9E7CB6112869882D00A02233 /* WalletEventsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletEventsSnapshotTests.swift; sourceTree = "<group>"; };
9E7CB6112869882D00A02233 /* TransactionListSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionListSnapshotTests.swift; sourceTree = "<group>"; };
9E852D6429B0A86300CF4AC1 /* DebugTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugTests.swift; sourceTree = "<group>"; };
9E90751D2A269BE300269308 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
9E92AF0728530EBF007367AD /* View+UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+UIImage.swift"; sourceTree = "<group>"; };
@ -270,7 +272,7 @@
9E4691982AD573420082D7DF /* TabsTests */,
9EF8135927ECC25E0075AF48 /* UtilTests */,
34F039B129ABCE8500CF0053 /* WalletConfigProviderTests */,
9E5BF63D281953F900BA3F17 /* WalletEventsTests */,
9E5BF63D281953F900BA3F17 /* TransactionListTests */,
);
path = secantTests;
sourceTree = "<group>";
@ -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 = "<group>";
};
9E5BF63D281953F900BA3F17 /* WalletEventsTests */ = {
9E5BF63D281953F900BA3F17 /* TransactionListTests */ = {
isa = PBXGroup;
children = (
9E5BF63E2819542C00BA3F17 /* WalletEventsTests.swift */,
9E5BF63E2819542C00BA3F17 /* TransactionListTests.swift */,
9E0C0D6F2AFB842B00D69A16 /* TransactionStateTests.swift */,
);
path = WalletEventsTests;
path = TransactionListTests;
sourceTree = "<group>";
};
9E5BF642281FEC8700BA3F17 /* SendTests */ = {
@ -476,12 +479,12 @@
path = ReviewRequestTests;
sourceTree = "<group>";
};
9E7CB6102869881300A02233 /* WalletEventsSnapshotTests */ = {
9E7CB6102869881300A02233 /* TransactionListSnapshotTests */ = {
isa = PBXGroup;
children = (
9E7CB6112869882D00A02233 /* WalletEventsSnapshotTests.swift */,
9E7CB6112869882D00A02233 /* TransactionListSnapshotTests.swift */,
);
path = WalletEventsSnapshotTests;
path = TransactionListSnapshotTests;
sourceTree = "<group>";
};
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 */,

View File

@ -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"
}
},
{

View File

@ -27,7 +27,7 @@ class HomeTests: XCTestCase {
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: mockSnapshot,
walletConfig: .default,
walletEventsState: .emptyPlaceHolder
transactionListState: .emptyPlaceHolder
),
reducer: HomeReducer(networkType: .testnet)
)

View File

@ -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)

View File

@ -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")
)
}
}

View File

@ -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")
)
}
}

View File

@ -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
}
}
}

View File

@ -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")
}
}

View File

@ -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)`"
)
}
}