Skip to content

Commit

Permalink
MOB-1924 - Added activity tab (#463)
Browse files Browse the repository at this point in the history
* Updated confirm send transaction pull up

* Fixed wallet icons tint color on receiver selection screen

* Fixed copy for max token pull up

* Added activity view to tab

* Created wallet transactions service and API to get transactions

* Removed test code

* Refactoring

* Updated transactions response structure.

* Moved cache transactions logic to separate service.
Created tests for transactions service

* Refactoring.
Added wallet txs service to app context.
Created functions to create mock txs related objects

* Updated tx timestamp property

* Created structure for WalletTransactionDisplayInfo

* Load txs in view model

* Setup home activity stack

* Created simple ui for transactions

* Fixed icon for external wallet in select receiver screen

* Open Tx details on tap
Updated UI for nft Txs.
Added PTR

* Pass force refresh flag to request
  • Loading branch information
Oleg-Pecheneg authored Mar 27, 2024
1 parent b6a5a33 commit 2500d43
Show file tree
Hide file tree
Showing 38 changed files with 1,400 additions and 26 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ final class AppContext: AppContextProtocol {
var hotFeatureSuggestionsService: HotFeatureSuggestionsServiceProtocol = HotFeatureSuggestionsService(fetcher: PreviewHotFeaturesSuggestionsFetcher())
var walletsDataService: WalletsDataServiceProtocol = PreviewWalletsDataService()
var domainProfilesService: DomainProfilesServiceProtocol
var walletTransactionsService: WalletTransactionsServiceProtocol

func createStripeInstance(amount: Int, using secret: String) -> StripeServiceProtocol {
StripeService(paymentDetails: .init(amount: amount, paymentSecret: secret))
Expand All @@ -86,6 +87,9 @@ final class AppContext: AppContextProtocol {
walletsDataService: walletsDataService)
domainProfilesService = DomainProfilesService(storage: PreviewPublicDomainProfileDisplayInfoStorageService(),
walletsDataService: walletsDataService)

walletTransactionsService = WalletTransactionsService(networkService: NetworkService(),
cache: InMemoryWalletTransactionsCache())
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,16 @@ extension NetworkService: DomainProfileNetworkServiceProtocol {
}
}

// MARK: - WalletTransactionsNetworkServiceProtocol
extension NetworkService: WalletTransactionsNetworkServiceProtocol {
func getTransactionsFor(wallet: HexAddress,
cursor: String?,
chain: String?,
forceRefresh: Bool) async throws -> [WalletTransactionsPerChainResponse] {
MockEntitiesFabric.WalletTxs.createMockTxsResponses()
}
}

extension NetworkService {
static let ipfsRedirectKey = "ipfs.html.value"

Expand Down
122 changes: 122 additions & 0 deletions unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ protocol AppContextProtocol {
var walletNFTsService: WalletNFTsServiceProtocol { get }
var walletsDataService: WalletsDataServiceProtocol { get }
var domainProfilesService: DomainProfilesServiceProtocol { get }
var walletTransactionsService: WalletTransactionsServiceProtocol { get }

var persistedProfileSignaturesStorage: PersistedSignaturesStorageProtocol { get }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ final class GeneralAppContext: AppContextProtocol {
let walletsDataService: WalletsDataServiceProtocol
let walletNFTsService: WalletNFTsServiceProtocol
let domainProfilesService: DomainProfilesServiceProtocol
let walletTransactionsService: WalletTransactionsServiceProtocol

private(set) lazy var coinRecordsService: CoinRecordsServiceProtocol = CoinRecordsService()
private(set) lazy var imageLoadingService: ImageLoadingServiceProtocol = ImageLoadingService(qrCodeService: qrCodeService,
Expand Down Expand Up @@ -62,6 +63,8 @@ final class GeneralAppContext: AppContextProtocol {
udDomainsService = UDDomainsService()
udWalletsService = UDWalletsService()
walletNFTsService = WalletNFTsService()
walletTransactionsService = WalletTransactionsService(networkService: NetworkService(),
cache: InMemoryWalletTransactionsCache())

let walletConnectServiceV2 = WalletConnectServiceV2(udWalletsService: udWalletsService)
self.walletConnectServiceV2 = walletConnectServiceV2
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ final class MockContext: AppContextProtocol {
private(set) lazy var walletsDataService: WalletsDataServiceProtocol = PreviewWalletsDataService()
private(set) lazy var domainProfilesService: DomainProfilesServiceProtocol = DomainProfilesService(storage: DomainProfileDisplayInfoCoreDataStorage(),
walletsDataService: walletsDataService)
private(set) lazy var walletTransactionsService: WalletTransactionsServiceProtocol = WalletTransactionsService(networkService: NetworkService(),
cache: InMemoryWalletTransactionsCache())

var persistedProfileSignaturesStorage: PersistedSignaturesStorageProtocol = MockPersistedSignaturesStorage()

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// MockEntitiesFabric+Txs.swift
// domains-manager-ios
//
// Created by Oleg Kuplin on 27.03.2024.
//

import Foundation

// MARK: - WalletTxs
extension MockEntitiesFabric {
enum WalletTxs {
@MainActor
static func createViewModel() -> HomeActivityViewModel {
createViewModelUsing(Home.createHomeTabRouter())
}

@MainActor
static func createViewModelUsing(_ router: HomeTabRouter) -> HomeActivityViewModel {
HomeActivityViewModel(router: router)
}

static func createMockTxsResponses(canLoadMore: Bool = false,
amount: Int = 20) -> [WalletTransactionsPerChainResponse] {
[createMockTxsResponse(chain: "ETH",
canLoadMore: canLoadMore,
amount: amount),
createMockTxsResponse(chain: "MATIC",
canLoadMore: canLoadMore,
amount: amount),
createMockTxsResponse(chain: "BASE",
canLoadMore: canLoadMore,
amount: amount)]
}

static func createMockTxsResponse(chain: String,
canLoadMore: Bool = false,
amount: Int = 20) -> WalletTransactionsPerChainResponse {
WalletTransactionsPerChainResponse(chain: chain,
cursor: canLoadMore ? UUID().uuidString : nil,
txs: createMockEmptyTxs(range: 1...amount))
}


static func createMockEmptyTxs(range: ClosedRange<Int> = 1...3) -> [SerializedWalletTransaction] {
range.map { createMockEmptyTx(id: "\($0)", dateOffset: TimeInterval($0)) }
}

static func createMockEmptyTx(id: String = "1",
dateOffset: TimeInterval = 0) -> SerializedWalletTransaction {
SerializedWalletTransaction(hash: id,
block: "",
timestamp: Date().addingTimeInterval(dateOffset),
success: true,
value: 1,
gas: 1,
method: "",
link: "",
imageUrl: "",
symbol: "",
type: "",
from: .init(address: "1", label: nil, link: ""),
to: .init(address: "2", label: nil, link: ""))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,7 @@ extension String {
static let transactionTakesNMinutes = "TRANSACTION_TAKES_N_MINUTES"
static let sendMaxCryptoInfoPullUpTitle = "SEND_MAX_CRYPTO_INFO_PULL_UP_TITLE"
static let notEnoughToken = "NOT_ENOUGH_TOKEN"
static let activity = "ACTIVITY"
}

enum BlockChainIcons: String {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// HomeActivity.swift
// domains-manager-ios
//
// Created by Oleg Kuplin on 27.03.2024.
//

import Foundation

// Namespace
enum HomeActivity { }

// MARK: - Open methods
extension HomeActivity {
struct GroupedTransactions: Hashable {
let date: Date
let txs: [WalletTransactionDisplayInfo]

init(date: Date, txs: [WalletTransactionDisplayInfo]) {
self.date = date
self.txs = txs.sorted { $0.time > $1.time }
}

static func buildGroupsFrom(txs: [WalletTransactionDisplayInfo]) -> [GroupedTransactions] {
txs.reduce(into: [Date: [WalletTransactionDisplayInfo]]()) { result, tx in
let date = tx.time.dayStart
result[date, default: []].append(tx)
}
.map { GroupedTransactions(date: $0.key, txs: $0.value) }
.sorted { $0.date > $1.date }
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
//
// HomeActivityNavigationDestination.swift
// domains-manager-ios
//
// Created by Oleg Kuplin on 27.03.2024.
//

import SwiftUI

enum HomeActivityNavigationDestination: Hashable {

}

struct HomeActivityLinkNavigationDestination {

@ViewBuilder
static func viewFor(navigationDestination: HomeActivityNavigationDestination,
tabRouter: HomeTabRouter) -> some View {
switch navigationDestination {
default:
EmptyView()
}
}

}

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//
// HomeActivityTransactionsSectionView.swift
// domains-manager-ios
//
// Created by Oleg Kuplin on 27.03.2024.
//

import SwiftUI

struct HomeActivityTransactionsSectionView: View {

@EnvironmentObject var viewModel: HomeActivityViewModel
let groupedTxs: HomeActivity.GroupedTransactions

var body: some View {
Section {
ForEach(groupedTxs.txs) { tx in
clickableTxRowView(tx)
.onAppear {
viewModel.willDisplayTransaction(tx)
}
}
} header: {
HStack {
Text(DateFormattingService.shared.formatICloudBackUpDate(groupedTxs.date))
.font(.currentFont(size: 14, weight: .semibold))
.foregroundStyle(Color.foregroundSecondary)
Spacer()
}
}
}
}

// MARK: - Private methods
private extension HomeActivityTransactionsSectionView {
@ViewBuilder
func clickableTxRowView(_ tx: WalletTransactionDisplayInfo) -> some View {
Button {
UDVibration.buttonTap.vibrate()
if let url = tx.link {
openLink(.direct(url: url))
}
} label: {
WalletTransactionDisplayInfoListItemView(transaction: tx)
}
.buttonStyle(.plain)
.allowsHitTesting(tx.link != nil)
}
}

#Preview {
HomeActivityTransactionsSectionView(groupedTxs: HomeActivity.GroupedTransactions(date: Date(), txs: []))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//
// ActivityView.swift
// domains-manager-ios
//
// Created by Oleg Kuplin on 27.03.2024.
//

import SwiftUI

struct HomeActivityView: View, ViewAnalyticsLogger {

@EnvironmentObject var tabRouter: HomeTabRouter
@State private var navigationState: NavigationStateManager?
@StateObject var viewModel: HomeActivityViewModel

var isOtherScreenPushed: Bool { !tabRouter.activityTabNavPath.isEmpty }
var analyticsName: Analytics.ViewName { .homeActivity }

var body: some View {
NavigationViewWithCustomTitle(content: {
contentList()
.animation(.default, value: UUID())
.background(Color.backgroundDefault)
.navigationTitle("")
.navigationBarTitleDisplayMode(.inline)
.environmentObject(viewModel)
.passViewAnalyticsDetails(logger: self)
.displayError($viewModel.error)
.background(Color.backgroundMuted2)
.refreshable {
logAnalytic(event: .didPullToRefresh)
await viewModel.didPullToRefresh()
}
.onReceive(keyboardPublisher) { value in
viewModel.isKeyboardActive = value
if !value {
UDVibration.buttonTap.vibrate()
}
}
.onChange(of: tabRouter.activityTabNavPath) { path in
withAnimation {
tabRouter.isTabBarVisible = !isOtherScreenPushed
if path.isEmpty {
setupTitle()
} else {
setTitleVisibility()
}
}
}
.navigationDestination(for: HomeActivityNavigationDestination.self) { destination in
HomeActivityLinkNavigationDestination.viewFor(navigationDestination: destination,
tabRouter: tabRouter)
.environmentObject(navigationState!)
.environmentObject(viewModel)
}
.toolbar(content: {
// To keep nav bar background visible when scrolling
ToolbarItem(placement: .topBarLeading) {
Color.clear
}
})
}, navigationStateProvider: { state in
self.navigationState = state
}, path: $tabRouter.activityTabNavPath)
.onAppear(perform: onAppear)
}
}

// MARK: - Private methods
private extension HomeActivityView {
func onAppear() {
setupTitle()
}

func setupTitle() {
navigationState?.setCustomTitle(customTitle: { HomeProfileSelectorNavTitleView() },
id: UUID().uuidString)
setTitleVisibility()
}

func setTitleVisibility() {
withAnimation {
navigationState?.isTitleVisible = !isOtherScreenPushed
}
}
}

// MARK: - Views
private extension HomeActivityView {
@ViewBuilder
func contentList() -> some View {
List {
txsList()
}
.listStyle(.plain)
}

@ViewBuilder
func txsList() -> some View {
ForEach(viewModel.groupedTxs, id: \.self) { groupedTx in
HomeActivityTransactionsSectionView(groupedTxs: groupedTx)
}
}
}

#Preview {
let router = MockEntitiesFabric.Home.createHomeTabRouter()
let viewModel = MockEntitiesFabric.WalletTxs.createViewModelUsing(router)

return HomeActivityView(viewModel: viewModel)
.environmentObject(router)

}
Loading

0 comments on commit 2500d43

Please sign in to comment.