[#564] Add transaction history as standalone screen (#569)

- the transaction history is now separated
- unit tests fixed
- snapshot test updated
This commit is contained in:
Lukas Korba 2023-02-22 14:50:59 +01:00 committed by GitHub
parent 1027a06ecd
commit 93b1b8c01f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 50 additions and 266 deletions

View File

@ -10,7 +10,7 @@ import ComposableArchitecture
extension DependencyValues {
var userStoredPreferences: UserPreferencesStorageClient {
get { self [UserPreferencesStorageClient.self] }
get { self[UserPreferencesStorageClient.self] }
set { self[UserPreferencesStorageClient.self] = newValue }
}
}

View File

@ -16,6 +16,7 @@ struct HomeReducer: ReducerProtocol {
case notEnoughFreeDiskSpace
case profile
case request
case transactionHistory
case send
case scan
case balanceBreakdown
@ -24,7 +25,6 @@ struct HomeReducer: ReducerProtocol {
var destination: Destination?
var balanceBreakdownState: BalanceBreakdownReducer.State
var drawerOverlay: DrawerOverlay
var profileState: ProfileReducer.State
var requestState: RequestReducer.State
var requiredTransactionConfirmations = 0
@ -67,7 +67,6 @@ struct HomeReducer: ReducerProtocol {
case scan(ScanReducer.Action)
case synchronizerStateChanged(SDKSynchronizerState)
case walletEvents(WalletEventsFlowReducer.Action)
case updateDrawer(DrawerOverlay)
case updateDestination(HomeReducer.State.Destination?)
case updateSynchronizerStatus
case updateWalletEvents([WalletEvent])
@ -118,23 +117,9 @@ struct HomeReducer: ReducerProtocol {
case .onDisappear:
return .cancel(id: CancelId.self)
case .synchronizerStateChanged(.synced):
return .merge(
sdkSynchronizer.getAllTransactions()
.receive(on: mainQueue)
.map(HomeReducer.Action.updateWalletEvents)
.eraseToEffect(),
EffectTask(value: .updateSynchronizerStatus)
)
case .synchronizerStateChanged:
return EffectTask(value: .updateSynchronizerStatus)
case .updateDrawer(let drawerOverlay):
state.drawerOverlay = drawerOverlay
state.walletEventsState.isScrollable = drawerOverlay == .full ? true : false
return .none
case .updateWalletEvents:
return .none
@ -190,12 +175,6 @@ struct HomeReducer: ReducerProtocol {
// TODO: [#221] error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
return .none
case .walletEvents(.updateDestination(.all)):
return state.drawerOverlay != .full ? EffectTask(value: .updateDrawer(.full)) : .none
case .walletEvents(.updateDestination(.latest)):
return state.drawerOverlay != .partial ? EffectTask(value: .updateDrawer(.partial)) : .none
case .walletEvents:
return .none
@ -283,13 +262,6 @@ extension HomeViewStore {
}
)
}
func bindingForDrawer() -> Binding<DrawerOverlay> {
self.binding(
get: { $0.drawerOverlay },
send: { .updateDrawer($0) }
)
}
}
// MARK: Placeholders
@ -298,7 +270,6 @@ extension HomeReducer.State {
static var placeholder: Self {
.init(
balanceBreakdownState: .placeholder,
drawerOverlay: .partial,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,

View File

@ -6,35 +6,38 @@ struct HomeView: View {
var body: some View {
WithViewStore(store) { viewStore in
GeometryReader { proxy in
VStack {
ZStack {
scanButton(viewStore)
profileButton(viewStore)
circularArea(viewStore, proxy.size)
sendButton(viewStore)
if proxy.size.height > 0 {
Drawer(overlay: viewStore.bindingForDrawer(), maxHeight: proxy.size.height) {
WalletEventsFlowView(store: store.historyStore())
.applyScreenBackground()
}
}
circularArea(viewStore)
sendButton(viewStore)
}
.applyScreenBackground()
.navigationBarHidden(true)
.onAppear(perform: { viewStore.send(.onAppear) })
.onDisappear(perform: { viewStore.send(.onDisappear) })
.fullScreenCover(isPresented: viewStore.bindingForDestination(.balanceBreakdown)) {
BalanceBreakdownView(store: store.balanceBreakdownStore())
Button {
viewStore.send(.updateDestination(.transactionHistory))
} label: {
Text("See transaction history")
}
}
.applyScreenBackground()
.navigationBarHidden(true)
.onAppear(perform: { viewStore.send(.onAppear) })
.onDisappear(perform: { viewStore.send(.onDisappear) })
.fullScreenCover(isPresented: viewStore.bindingForDestination(.balanceBreakdown)) {
BalanceBreakdownView(store: store.balanceBreakdownStore())
}
.navigationLinkEmpty(
isActive: viewStore.bindingForDestination(.notEnoughFreeDiskSpace),
destination: { NotEnoughFreeSpaceView(viewStore: viewStore) }
)
.navigationLinkEmpty(
isActive: viewStore.bindingForDestination(.transactionHistory),
destination: { WalletEventsFlowView(store: store.historyStore()) }
)
}
}
}
@ -114,7 +117,7 @@ extension HomeView {
}
}
func circularArea(_ viewStore: HomeViewStore, _ size: CGSize) -> some View {
func circularArea(_ viewStore: HomeViewStore) -> some View {
VStack {
ZStack {
CircularProgress(
@ -123,7 +126,6 @@ extension HomeView {
maxSegments: viewStore.requiredTransactionConfirmations,
innerCircleHidden: viewStore.isUpToDate
)
.frame(width: size.width * 0.65, height: size.width * 0.65)
.padding(.top, 50)
VStack {

View File

@ -19,7 +19,7 @@ struct WalletEventsFlowReducer: ReducerProtocol {
@BindingState var alert: AlertState<WalletEventsFlowReducer.Action>?
var latestMinedHeight: BlockHeight?
var isScrollable = false
var isScrollable = true
var requiredTransactionConfirmations = 0
var walletEvents = IdentifiedArrayOf<WalletEvent>.placeholder
var selectedWalletEvent: WalletEvent?
@ -60,7 +60,7 @@ struct WalletEventsFlowReducer: ReducerProtocol {
if let latestMinedHeight = sdkSynchronizer.synchronizer?.latestScannedHeight {
state.latestMinedHeight = latestMinedHeight
}
return sdkSynchronizer.getAllClearedTransactions()
return sdkSynchronizer.getAllTransactions()
.receive(on: mainQueue)
.map(WalletEventsFlowReducer.Action.updateWalletEvents)
.eraseToEffect()

View File

@ -3,32 +3,15 @@ import ComposableArchitecture
struct WalletEventsFlowView: View {
let store: WalletEventsFlowStore
@State var flag = true
var body: some View {
return WithViewStore(store) { viewStore in
VStack {
header(with: viewStore)
if viewStore.isScrollable {
List {
walletEventsList(with: viewStore)
}
.listStyle(.plain)
.padding(.bottom, 60)
} else {
walletEventsList(with: viewStore)
}
Spacer()
List {
walletEventsList(with: viewStore)
}
.onAppear(
perform: {
UITableView.appearance().backgroundColor = .clear
UITableView.appearance().separatorColor = .clear
viewStore.send(.onAppear)
}
)
.navigationTitle("Transactions")
.listStyle(.plain)
.onAppear { viewStore.send(.onAppear) }
.onDisappear(perform: { viewStore.send(.onDisappear) })
.navigationLinkEmpty(isActive: viewStore.bindingForSelectedWalletEvent(viewStore.selectedWalletEvent)) {
viewStore.selectedWalletEvent?.detailView(store)
@ -47,52 +30,9 @@ extension WalletEventsFlowView {
.listRowInsets(EdgeInsets())
.foregroundColor(Asset.Colors.Text.body.color)
.listRowBackground(Color.clear)
.frame(height: 60)
}
}
func header(with viewStore: WalletEventsFlowViewStore) -> some View {
HStack(spacing: 0) {
VStack {
Button {
viewStore.send(.updateDestination(.latest))
} label: {
Text("Latest")
.font(.custom(FontFamily.Rubik.regular.name, size: 18))
}
.frame(width: 100)
.foregroundColor(Asset.Colors.Text.drawerTabsText.color)
.opacity(viewStore.isScrollable ? 0.23 : 1.0)
Rectangle()
.frame(height: 1.5)
.foregroundColor(latestUnderline(viewStore))
}
VStack {
Button {
viewStore.send(.updateDestination(.all))
} label: {
Text("All")
.font(.custom(FontFamily.Rubik.regular.name, size: 18))
}
.frame(width: 100)
.foregroundColor(Asset.Colors.Text.drawerTabsText.color)
.opacity(viewStore.isScrollable ? 1.0 : 0.23)
Rectangle()
.frame(height: 1.5)
.foregroundColor(allUnderline(viewStore))
}
}
}
private func latestUnderline(_ viewStore: WalletEventsFlowViewStore) -> Color {
viewStore.isScrollable ? Asset.Colors.TextField.Underline.gray.color : Asset.Colors.TextField.Underline.purple.color
}
private func allUnderline(_ viewStore: WalletEventsFlowViewStore) -> Color {
viewStore.isScrollable ? Asset.Colors.TextField.Underline.purple.color : Asset.Colors.TextField.Underline.gray.color
}
}
// MARK: - Previews

View File

@ -22,9 +22,7 @@ class HomeTests: XCTestCase {
store.receive(.updateSynchronizerStatus)
}
/// When the synchronizer status change to .synced, several things happen
/// 1. the .updateSynchronizerStatus is called
/// 2. the side effect to update the transactions history is called
/// When the synchronizer status change to .synced, the .updateSynchronizerStatus is called
func testSynchronizerStateChanged_Synced() throws {
// setup the store and environment to be fully mocked
let testScheduler = DispatchQueue.test
@ -41,95 +39,7 @@ class HomeTests: XCTestCase {
testScheduler.advance(by: 0.01)
// ad 1.
store.receive(.updateSynchronizerStatus)
// ad 2.
let transactionsHelper: [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] = transactionsHelper.map {
let transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.amount.amount > 5 ? .pending : $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
return WalletEvent(id: transaction.id, state: $0.amount.amount > 5 ? .pending(transaction) : .send(transaction), timestamp: transaction.timestamp)
}
store.receive(.updateWalletEvents(walletEvents))
}
func testWalletEventsPartial_to_FullDrawer() throws {
let homeState = HomeReducer.State(
balanceBreakdownState: .placeholder,
drawerOverlay: .partial,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
sendState: .placeholder,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .default,
walletEventsState: .emptyPlaceHolder
)
let store = TestStore(
initialState: homeState,
reducer: HomeReducer()
)
store.send(.walletEvents(.updateDestination(.all))) { state in
state.walletEventsState.destination = .all
}
store.receive(.updateDrawer(.full)) { state in
state.drawerOverlay = .full
state.walletEventsState.isScrollable = true
}
}
func testWalletEventsFull_to_PartialDrawer() throws {
let homeState = HomeReducer.State(
balanceBreakdownState: .placeholder,
drawerOverlay: .full,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
sendState: .placeholder,
shieldedBalance: Balance.zero,
synchronizerStatusSnapshot: .default,
walletEventsState: .emptyPlaceHolder
)
let store = TestStore(
initialState: homeState,
reducer: HomeReducer()
)
store.send(.walletEvents(.updateDestination(.latest))) { state in
state.walletEventsState.destination = .latest
}
store.receive(.updateDrawer(.partial)) { state in
state.drawerOverlay = .partial
state.walletEventsState.isScrollable = false
}
}
/// The .onAppear action is important to register for the synchronizer state updates.
@ -182,7 +92,6 @@ class HomeTests: XCTestCase {
let homeState = HomeReducer.State(
destination: .profile,
balanceBreakdownState: .placeholder,
drawerOverlay: .full,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
@ -208,7 +117,6 @@ class HomeTests: XCTestCase {
let homeState = HomeReducer.State(
destination: .profile,
balanceBreakdownState: .placeholder,
drawerOverlay: .full,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,

View File

@ -30,7 +30,6 @@ class HomeCircularProgressSnapshotTests: XCTestCase {
let store = HomeStore(
initialState: .init(
balanceBreakdownState: .placeholder,
drawerOverlay: .partial,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
@ -66,7 +65,6 @@ class HomeCircularProgressSnapshotTests: XCTestCase {
let store = HomeStore(
initialState: .init(
balanceBreakdownState: .placeholder,
drawerOverlay: .partial,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
@ -94,7 +92,6 @@ class HomeCircularProgressSnapshotTests: XCTestCase {
let store = HomeStore(
initialState: .init(
balanceBreakdownState: .placeholder,
drawerOverlay: .partial,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,

View File

@ -38,7 +38,6 @@ class HomeSnapshotTests: XCTestCase {
let store = HomeStore(
initialState: .init(
balanceBreakdownState: .placeholder,
drawerOverlay: .partial,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,

View File

@ -12,54 +12,16 @@ import ZcashLightClientKit
class WalletEventsSnapshotTests: XCTestCase {
func testFullWalletEventsSnapshot() throws {
let transactionsHelper: [TransactionStateMockHelper] = [
TransactionStateMockHelper(date: 1651039202, amount: Zatoshi(1), status: .paid(success: true), uuid: "1"),
TransactionStateMockHelper(date: 1651039101, amount: Zatoshi(2), status: .pending, 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 {
var transaction = TransactionState.placeholder(
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
timestamp: $0.date,
uuid: $0.uuid
)
transaction.zAddress = "t1gXqfSSQt6WfpwyuCU3Wi7sSVZ66DYQ3Po"
return WalletEvent(id: transaction.id, state: .send(transaction), timestamp: transaction.timestamp)
}
let balance = WalletBalance(verified: 12_345_000, total: 12_345_000)
let store = HomeStore(
initialState: .init(
balanceBreakdownState: .placeholder,
drawerOverlay: .partial,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
sendState: .placeholder,
shieldedBalance: balance.redacted,
synchronizerStatusSnapshot: .default,
walletEventsState: .init(walletEvents: IdentifiedArrayOf(uniqueElements: walletEvents))
),
reducer: HomeReducer()
.dependency(\.diskSpaceChecker, .mockEmptyDisk)
let store = WalletEventsFlowStore(
initialState: .placeHolder,
reducer: WalletEventsFlowReducer()
)
// landing home screen
// landing wallet events screen
addAttachments(
name: "\(#function)_initial",
HomeView(store: store)
WalletEventsFlowView(store: store)
)
// all transactions
ViewStore(store).send(.updateDrawer(.full))
addAttachments(HomeView(store: store))
}
func testWalletEventDetailSnapshot_sent() throws {
@ -91,7 +53,6 @@ class WalletEventsSnapshotTests: XCTestCase {
let store = HomeStore(
initialState: .init(
balanceBreakdownState: .placeholder,
drawerOverlay: .partial,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
@ -144,7 +105,6 @@ class WalletEventsSnapshotTests: XCTestCase {
let store = HomeStore(
initialState: .init(
balanceBreakdownState: .placeholder,
drawerOverlay: .partial,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
@ -197,7 +157,6 @@ class WalletEventsSnapshotTests: XCTestCase {
let store = HomeStore(
initialState: .init(
balanceBreakdownState: .placeholder,
drawerOverlay: .partial,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,
@ -256,7 +215,6 @@ class WalletEventsSnapshotTests: XCTestCase {
let store = HomeStore(
initialState: .init(
balanceBreakdownState: .placeholder,
drawerOverlay: .partial,
profileState: .placeholder,
requestState: .placeholder,
scanState: .placeholder,

View File

@ -39,7 +39,16 @@ class WalletEventsTests: XCTestCase {
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: 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 {
@ -47,7 +56,7 @@ class WalletEventsTests: XCTestCase {
amount: $0.amount,
fee: Zatoshi(10),
shielded: $0.shielded,
status: $0.status,
status: $0.amount.amount > 5 ? .pending : $0.status,
timestamp: $0.date,
uuid: $0.uuid
)