From ab47b9c12fc8db9cb703d401bc24314cbde304fd Mon Sep 17 00:00:00 2001 From: Oleg Date: Mon, 22 Jul 2024 15:17:28 +0300 Subject: [PATCH] MOB-2111 - Activities filter (#612) * Added simple UI to control applied activity filters * Fixed main target * Apply selected txs filters * Added mock data Added PTR when no txs in the list * Created UI to filter transaction destination and grid filter view * Implementing UI for activities filters view * Updated UI on the activity screen. Filter available networks for selected wallet * Updated filter view appearance * Show filter indicator in nav bar when applied --- .../Entities/PreviewNetworkService.swift | 2 +- .../project.pbxproj | 36 ++++- .../Domains/DomainWithDisplayInfo.swift | 2 +- .../Mock/MockEntitiesFabric+Txs.swift | 62 +++++---- .../Entities/Storages/Storage.swift | 2 +- .../Entities/Wallets/WalletEntity.swift | 9 ++ .../Extensions/Extension-String+Preview.swift | 6 + .../Extensions/Extensions.swift | 2 +- .../Extensions/UIColor.swift | 1 + .../Modules/Home/Activity/HomeActivity.swift | 63 +++++++++ .../Activity/HomeActivityFilterView.swift | 87 ++++++++++++ .../Home/Activity/HomeActivityView.swift | 46 ++++++- .../Home/Activity/HomeActivityViewModel.swift | 83 +++++++++++- .../Activity/IconTitleSelectionGridView.swift | 128 ++++++++++++++++++ .../WalletTransactionDisplayInfo.swift | 9 ++ .../SendCryptoAssetViewModel.swift | 7 +- .../BuyDomainsWebViewController.swift | 2 +- .../ApiRequestBuilder.swift | 8 +- .../Protocols/BlockChainItem.swift | 2 +- .../AnalyticsServiceEnvironment.swift | 5 +- .../NetworkService+ProfilesApi.swift | 4 +- .../UserProfilesService.swift | 2 +- ...etTransactionsNetworkServiceProtocol.swift | 2 +- .../WalletTransactionsPerChainResponse.swift | 4 + .../WalletTransactionsService.swift | 22 +-- .../WalletTransactionsServiceProtocol.swift | 4 +- .../SupportingFiles/AppDelegate.swift | 2 +- .../Common/baseIcon.imageset/Contents.json | 2 +- .../Common/filter.imageset/Contents.json | 15 ++ .../Common/filter.imageset/filter.svg | 5 + .../layoutGridTwo.imageset/Contents.json | 15 ++ .../layoutGridTwo.imageset/layoutGridTwo.svg | 10 ++ .../Common/solanaIcon.imageset/Contents.json | 3 + .../twoCoinsIcon.imageset/Contents.json | 15 ++ .../twoCoinsIcon.imageset/twoCoinsIcon.svg | 8 ++ .../Contents.json | 38 ++++++ .../Localization/en.lproj/Localizable.strings | 6 + .../SwiftUI/CommonViews/ListVGrid.swift | 4 +- .../SelectionPopoverView.swift | 120 ++++++++++++++++ .../SelectionPopoverViewItem.swift | 16 +++ .../CommonViews/UDSegmentedControlView.swift | 6 +- .../SwiftUI/Extensions/Color.swift | 1 + .../SwiftUI/Extensions/Image.swift | 3 + .../WalletTransactionsServiceTests.swift | 50 +++---- 44 files changed, 817 insertions(+), 102 deletions(-) create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivityFilterView.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/IconTitleSelectionGridView.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/filter.imageset/Contents.json create mode 100644 unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/filter.imageset/filter.svg create mode 100644 unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/layoutGridTwo.imageset/Contents.json create mode 100644 unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/layoutGridTwo.imageset/layoutGridTwo.svg create mode 100644 unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/twoCoinsIcon.imageset/Contents.json create mode 100644 unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/twoCoinsIcon.imageset/twoCoinsIcon.svg create mode 100644 unstoppable-ios-app/domains-manager-ios/SupportingFiles/Colors.xcassets/New/Background/backgroundAccentMuted.colorset/Contents.json create mode 100644 unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/SelectionPopoverView/SelectionPopoverView.swift create mode 100644 unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/SelectionPopoverView/SelectionPopoverViewItem.swift diff --git a/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewNetworkService.swift b/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewNetworkService.swift index 99b0edc83..97561a631 100644 --- a/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewNetworkService.swift +++ b/unstoppable-ios-app/domains-manager-ios-preview/Entities/PreviewNetworkService.swift @@ -263,7 +263,7 @@ extension NetworkService: DomainProfileNetworkServiceProtocol { extension NetworkService: WalletTransactionsNetworkServiceProtocol { func getTransactionsFor(wallet: HexAddress, cursor: String?, - chain: String?, + chains: [BlockchainType]?, forceRefresh: Bool) async throws -> [WalletTransactionsPerChainResponse] { MockEntitiesFabric.WalletTxs.createMockTxsResponses() } diff --git a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj index fe7cade2d..661838d03 100644 --- a/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj +++ b/unstoppable-ios-app/domains-manager-ios.xcodeproj/project.pbxproj @@ -756,6 +756,12 @@ C63F1D1128AF31D5000A5C12 /* AnalyticsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = C63F1D0628AF31D5000A5C12 /* AnalyticsService.swift */; }; C640D6122C48087C006B21C3 /* AnimatedMPCWalletGridMask.swift in Sources */ = {isa = PBXBuildFile; fileRef = C640D6112C48087C006B21C3 /* AnimatedMPCWalletGridMask.swift */; }; C640D6132C48087C006B21C3 /* AnimatedMPCWalletGridMask.swift in Sources */ = {isa = PBXBuildFile; fileRef = C640D6112C48087C006B21C3 /* AnimatedMPCWalletGridMask.swift */; }; + C640D6152C491B7B006B21C3 /* HomeActivityFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C640D6142C491B7B006B21C3 /* HomeActivityFilterView.swift */; }; + C640D6162C491B7B006B21C3 /* HomeActivityFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C640D6142C491B7B006B21C3 /* HomeActivityFilterView.swift */; }; + C640D6182C491DFF006B21C3 /* SelectionPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C640D6172C491DFF006B21C3 /* SelectionPopoverView.swift */; }; + C640D6192C491DFF006B21C3 /* SelectionPopoverView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C640D6172C491DFF006B21C3 /* SelectionPopoverView.swift */; }; + C640D61B2C491E13006B21C3 /* SelectionPopoverViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C640D61A2C491E13006B21C3 /* SelectionPopoverViewItem.swift */; }; + C640D61C2C491E13006B21C3 /* SelectionPopoverViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C640D61A2C491E13006B21C3 /* SelectionPopoverViewItem.swift */; }; C640F3552C06FC66009EB0F9 /* MPCWalletPasswordValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C640F3542C06FC66009EB0F9 /* MPCWalletPasswordValidator.swift */; }; C640F3562C06FC66009EB0F9 /* MPCWalletPasswordValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C640F3542C06FC66009EB0F9 /* MPCWalletPasswordValidator.swift */; }; C640F3582C06FCB6009EB0F9 /* MPCWalletPasswordValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C640F3572C06FCB6009EB0F9 /* MPCWalletPasswordValidatorTests.swift */; }; @@ -1989,6 +1995,8 @@ C6D20FC92BE4A9A6003617B7 /* SettingsProfileTileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D20FC72BE4A9A6003617B7 /* SettingsProfileTileView.swift */; }; C6D20FCB2BE4A9DC003617B7 /* SettingsProfilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D20FCA2BE4A9DC003617B7 /* SettingsProfilesView.swift */; }; C6D20FCC2BE4A9DC003617B7 /* SettingsProfilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D20FCA2BE4A9DC003617B7 /* SettingsProfilesView.swift */; }; + C6D2DF122C4E4CEB00F08A6F /* IconTitleSelectionGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D2DF112C4E4CEB00F08A6F /* IconTitleSelectionGridView.swift */; }; + C6D2DF132C4E4CEB00F08A6F /* IconTitleSelectionGridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D2DF112C4E4CEB00F08A6F /* IconTitleSelectionGridView.swift */; }; C6D2F13028729585005F4F2E /* DomainDisplayInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D2F12F28729585005F4F2E /* DomainDisplayInfo.swift */; }; C6D2F15328757C25005F4F2E /* UIImage+SVG.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D2F15228757C25005F4F2E /* UIImage+SVG.swift */; }; C6D3B9B52A6EB8DA0091B279 /* MessagingChannelsAPIServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = C6D3B9B42A6EB8DA0091B279 /* MessagingChannelsAPIServiceProtocol.swift */; }; @@ -3233,6 +3241,9 @@ C63F1D0528AF31D5000A5C12 /* AnalyticsServiceProtocol.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsServiceProtocol.swift; sourceTree = ""; }; C63F1D0628AF31D5000A5C12 /* AnalyticsService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnalyticsService.swift; sourceTree = ""; }; C640D6112C48087C006B21C3 /* AnimatedMPCWalletGridMask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatedMPCWalletGridMask.swift; sourceTree = ""; }; + C640D6142C491B7B006B21C3 /* HomeActivityFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeActivityFilterView.swift; sourceTree = ""; }; + C640D6172C491DFF006B21C3 /* SelectionPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionPopoverView.swift; sourceTree = ""; }; + C640D61A2C491E13006B21C3 /* SelectionPopoverViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectionPopoverViewItem.swift; sourceTree = ""; }; C640F3542C06FC66009EB0F9 /* MPCWalletPasswordValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCWalletPasswordValidator.swift; sourceTree = ""; }; C640F3572C06FCB6009EB0F9 /* MPCWalletPasswordValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCWalletPasswordValidatorTests.swift; sourceTree = ""; }; C640F35B2C070718009EB0F9 /* MPCOnboardingPurchaseTakeoverRecoveryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPCOnboardingPurchaseTakeoverRecoveryViewController.swift; sourceTree = ""; }; @@ -3932,6 +3943,7 @@ C6D1E74B2BA17E2700738365 /* WalletsDataNetworkServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletsDataNetworkServiceProtocol.swift; sourceTree = ""; }; C6D20FC72BE4A9A6003617B7 /* SettingsProfileTileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsProfileTileView.swift; sourceTree = ""; }; C6D20FCA2BE4A9DC003617B7 /* SettingsProfilesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsProfilesView.swift; sourceTree = ""; }; + C6D2DF112C4E4CEB00F08A6F /* IconTitleSelectionGridView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconTitleSelectionGridView.swift; sourceTree = ""; }; C6D2F12F28729585005F4F2E /* DomainDisplayInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DomainDisplayInfo.swift; sourceTree = ""; }; C6D2F15228757C25005F4F2E /* UIImage+SVG.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+SVG.swift"; sourceTree = ""; }; C6D3B9B42A6EB8DA0091B279 /* MessagingChannelsAPIServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagingChannelsAPIServiceProtocol.swift; sourceTree = ""; }; @@ -5693,6 +5705,15 @@ path = Services; sourceTree = ""; }; + C640D61D2C4944AB006B21C3 /* SelectionPopoverView */ = { + isa = PBXGroup; + children = ( + C640D61A2C491E13006B21C3 /* SelectionPopoverViewItem.swift */, + C640D6172C491DFF006B21C3 /* SelectionPopoverView.swift */, + ); + path = SelectionPopoverView; + sourceTree = ""; + }; C640F35A2C0706FF009EB0F9 /* TakeoverRecovery */ = { isa = PBXGroup; children = ( @@ -7130,12 +7151,13 @@ C639F39B2C32D133008CC1CB /* ConnectCurveLine */, C639F3952C32B9C3008CC1CB /* ConnectTransactionSign.swift */, C6DDF2412B147F1A006D1F0B /* FlowLayoutView.swift */, + C6DA0B712B7BC1C1009920B5 /* ListVGrid.swift */, + C63AD0992B95657C00BF8C83 /* LineView.swift */, C6DDF2692B148A31006D1F0B /* OffsetObservingScrollView.swift */, C63392FB2B7271E800941C9D /* OffsetObservingListView.swift */, C6DDF2682B148A31006D1F0B /* PositionObservingView.swift */, C6E856282BBE983900AAFDE4 /* PresentAsModalPreviewView.swift */, - C6DA0B712B7BC1C1009920B5 /* ListVGrid.swift */, - C63AD0992B95657C00BF8C83 /* LineView.swift */, + C640D61D2C4944AB006B21C3 /* SelectionPopoverView */, C6DDF2422B147F1A006D1F0B /* UDCollectionListRowButton.swift */, C6DDF2402B147F19006D1F0B /* UDCollectionSectionBackgroundView.swift */, C655CA9A2B16E5BE00FDA063 /* UDListItemView.swift */, @@ -7701,6 +7723,8 @@ C6B761FB2BB403F700773943 /* HomeActivity.swift */, C6B761D12BB3C19800773943 /* HomeActivityViewModel.swift */, C6B761CB2BB3BEB700773943 /* HomeActivityView.swift */, + C640D6142C491B7B006B21C3 /* HomeActivityFilterView.swift */, + C6D2DF112C4E4CEB00F08A6F /* IconTitleSelectionGridView.swift */, C6952A552BC6574200F4B475 /* HomeActivityEmptyView.swift */, C6B762012BB4057400773943 /* HomeActivityTransactionsSectionView.swift */, C6B762042BB4067600773943 /* WalletTransactionDisplayInfoListItemView.swift */, @@ -9237,6 +9261,7 @@ C61DB10B2B9588B300CDA243 /* PublicProfileSeparatorView.swift in Sources */, C61DB1052B95879200CDA243 /* PublicProfilePrimaryLargeTextView.swift in Sources */, C6B761F02BB3F9D900773943 /* WalletTransactionsResponse.swift in Sources */, + C640D61B2C491E13006B21C3 /* SelectionPopoverViewItem.swift in Sources */, C6F060CA2ACAA4DF00BA2E7E /* MessagingService+Chats.swift in Sources */, C6C8F85E2B21800E00A9834D /* ValetProtocol.swift in Sources */, C6526DF229D2E12A00D6F2EB /* ParkedDomainsFoundOnboardingViewPresenter.swift in Sources */, @@ -9842,6 +9867,7 @@ 3026FA1727E7678200FE058B /* CreateWalletViewController.swift in Sources */, C609827628251FBA00546392 /* ImportNewWalletPresenter.swift in Sources */, C630E4B62B7F58BC008F3269 /* MessageInputView.swift in Sources */, + C6D2DF122C4E4CEB00F08A6F /* IconTitleSelectionGridView.swift in Sources */, C62C3F90283F35610094FC93 /* CodeVerificationView.swift in Sources */, C6B761D92BB3CDDE00773943 /* WalletTransactionsServiceProtocol.swift in Sources */, C6A231FC2BEB494D0037E093 /* WalletDetailsDomainItemView.swift in Sources */, @@ -9884,6 +9910,7 @@ C6B65FA42B5791B4006D1812 /* HomeWalletDomainCellView.swift in Sources */, C66556812BF73DCB00F0BD7A /* MPCOnboardingPurchaseTakeoverCredentialsViewController.swift in Sources */, C6FF2D9C28F0245F00148007 /* DomainProfileSectionsController.swift in Sources */, + C640D6182C491DFF006B21C3 /* SelectionPopoverView.swift in Sources */, C6534A992BBFBA10008EEBB5 /* HomeExploreTrendingProfilesSectionView.swift in Sources */, C663A4D62B7D09490099BCE8 /* UpdateToWalletGreetingsView.swift in Sources */, C61DB1082B9587B600CDA243 /* PublicProfileSecondaryLargeTextView.swift in Sources */, @@ -9943,6 +9970,7 @@ C630BC772B18376100E29318 /* BaseHappyEndViewPresenter.swift in Sources */, C645130C280808ED00413C51 /* Codable.swift in Sources */, C6C995A2289D313D00367362 /* CNavigationControllerChildTransitioning.swift in Sources */, + C640D6152C491B7B006B21C3 /* HomeActivityFilterView.swift in Sources */, C6011AB128F3FA1500342666 /* DomainProfileTopInfoSection.swift in Sources */, C64513232809912F00413C51 /* PullUpViewController.swift in Sources */, C624D770281B884600F55530 /* ImageLoadingService.swift in Sources */, @@ -10244,6 +10272,7 @@ C640F3862C0868C3009EB0F9 /* PurchaseMPCWalletTakeoverProgressInAppView.swift in Sources */, C6D646E92B1ED51100D724AC /* PreviewNFCService.swift in Sources */, C6C8F95B2B21866700A9834D /* PasscodeButton.swift in Sources */, + C640D61C2C491E13006B21C3 /* SelectionPopoverViewItem.swift in Sources */, C6960C4F2B19989D00B79E28 /* Env.swift in Sources */, C61807D92B19A48F0032E543 /* PreviewAuthentificationService.swift in Sources */, C632BE572BA59E8400C95B2D /* FB_UD_MPCSetupTokenResponse.swift in Sources */, @@ -10594,10 +10623,12 @@ C6D647692B1EDF9700D724AC /* CoreAppCoordinatorProtocol.swift in Sources */, C68F156B2BD645DD0049BFA2 /* MPCOnboardingActivateWalletViewController.swift in Sources */, C6D647902B1EE73D00D724AC /* CoinRecordsServiceProtocol.swift in Sources */, + C640D6192C491DFF006B21C3 /* SelectionPopoverView.swift in Sources */, C6C8F8182B217CC000A9834D /* ConnectExternalWalletViewPresenter.swift in Sources */, C6C8F99A2B218E2900A9834D /* GIFAnimationImageView.swift in Sources */, C60319992C2C1F7400A77109 /* BlockchainType+Extension.swift in Sources */, C6D645BF2B1DBD1E00D724AC /* ViewAnalyticsLogger.swift in Sources */, + C6D2DF132C4E4CEB00F08A6F /* IconTitleSelectionGridView.swift in Sources */, C665566B2BF70C0000F0BD7A /* MPCOnboardingPurchaseUDAuthViewController.swift in Sources */, C6D646FE2B1ED7BE00D724AC /* MessagingChatType.swift in Sources */, C67213D72BAA8DB60075B9C7 /* SendCryptoAssetSelectReceiverFollowingRowView.swift in Sources */, @@ -10950,6 +10981,7 @@ C640F35D2C070718009EB0F9 /* MPCOnboardingPurchaseTakeoverRecoveryViewController.swift in Sources */, C6203A5A2B883847000A1A8E /* MockEntitiesFabric+Messaging.swift in Sources */, C6952A612BC664C600F4B475 /* PurchaseMPCWallet.swift in Sources */, + C640D6162C491B7B006B21C3 /* HomeActivityFilterView.swift in Sources */, C6D646D32B1ED2C800D724AC /* UDTextField.swift in Sources */, C61808742B19BC150032E543 /* Font.swift in Sources */, C67B1DD92BFEF5C300C2A4DA /* ReconnectMPCWalletViewModel.swift in Sources */, diff --git a/unstoppable-ios-app/domains-manager-ios/Entities/Domains/DomainWithDisplayInfo.swift b/unstoppable-ios-app/domains-manager-ios/Entities/Domains/DomainWithDisplayInfo.swift index a8dd2f841..0f6b49def 100644 --- a/unstoppable-ios-app/domains-manager-ios/Entities/Domains/DomainWithDisplayInfo.swift +++ b/unstoppable-ios-app/domains-manager-ios/Entities/Domains/DomainWithDisplayInfo.swift @@ -23,7 +23,7 @@ extension Array where Element == DomainWithDisplayInfo { let indeces = self.enumerated() .filter({domainNames.contains($0.element.name)}) .map({$0.offset}) - self.remove(at: indeces) + self.remove(atIndexes: indeces) } func sorted() -> [DomainWithDisplayInfo] { diff --git a/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric+Txs.swift b/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric+Txs.swift index 30ab8d582..722b7876b 100644 --- a/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric+Txs.swift +++ b/unstoppable-ios-app/domains-manager-ios/Entities/Mock/MockEntitiesFabric+Txs.swift @@ -21,7 +21,7 @@ extension MockEntitiesFabric { } static func createMockTxsResponses(canLoadMore: Bool = false, - amount: Int = 20) -> [WalletTransactionsPerChainResponse] { + amount: Int = 20) -> [WalletTransactionsPerChainResponse] { [createMockTxsResponse(chain: BlockchainType.Ethereum.shortCode, canLoadMore: canLoadMore, amount: amount), @@ -43,10 +43,14 @@ extension MockEntitiesFabric { static func createMockEmptyTxs(range: ClosedRange = 1...3) -> [SerializedWalletTransaction] { - range.map { createMockEmptyTx(id: "\($0)", dateOffset: TimeInterval($0 * -14000)) } + range.map { createMockTxOf(type: TxType.allCases.randomElement()!, + userWallet: "0", + id: "\($0)", + dateOffset: TimeInterval($0 * -14000), + isDeposit: [true, false].randomElement()!) } } - enum TxType { + enum TxType: CaseIterable { case crypto case nft case domain @@ -84,34 +88,34 @@ extension MockEntitiesFabric { case .domain: return SerializedWalletTransaction(hash: id, - block: "", - timestamp: Date().addingTimeInterval(dateOffset), - success: true, - value: 0, - gas: 0, - method: "oleg.x", - link: "", - imageUrl: ImageURLs.aiAvatar.rawValue, - symbol: "MATIC", - type: "nft", - from: from, - to: to) - + block: "", + timestamp: Date().addingTimeInterval(dateOffset), + success: true, + value: 0, + gas: 0, + method: "oleg.x", + link: "", + imageUrl: ImageURLs.aiAvatar.rawValue, + symbol: "MATIC", + type: "nft", + from: from, + to: to) + case .nft: return SerializedWalletTransaction(hash: id, - block: "", - timestamp: Date().addingTimeInterval(dateOffset), - success: true, - value: 0, - gas: 0, - method: "May the Grooves be with you", - link: "", - imageUrl: ImageURLs.aiAvatar.rawValue, - symbol: "MATIC", - type: "nft", - from: from, - to: to) - + block: "", + timestamp: Date().addingTimeInterval(dateOffset), + success: true, + value: 0, + gas: 0, + method: "May the Grooves be with you", + link: "", + imageUrl: ImageURLs.aiAvatar.rawValue, + symbol: "MATIC", + type: "nft", + from: from, + to: to) + } } diff --git a/unstoppable-ios-app/domains-manager-ios/Entities/Storages/Storage.swift b/unstoppable-ios-app/domains-manager-ios/Entities/Storages/Storage.swift index 82acb4eb5..c9f447c72 100644 --- a/unstoppable-ios-app/domains-manager-ios/Entities/Storages/Storage.swift +++ b/unstoppable-ios-app/domains-manager-ios/Entities/Storages/Storage.swift @@ -176,7 +176,7 @@ extension TxsStorage { Debugger.printFailure("Indeces found: \(indecesToRemove) is out of range: \(txs.count)") return txs } - txs.remove(at: indecesToRemove) + txs.remove(atIndexes: indecesToRemove) } return txs } diff --git a/unstoppable-ios-app/domains-manager-ios/Entities/Wallets/WalletEntity.swift b/unstoppable-ios-app/domains-manager-ios/Entities/Wallets/WalletEntity.swift index c818cafec..d48a06ef3 100644 --- a/unstoppable-ios-app/domains-manager-ios/Entities/Wallets/WalletEntity.swift +++ b/unstoppable-ios-app/domains-manager-ios/Entities/Wallets/WalletEntity.swift @@ -178,6 +178,15 @@ extension WalletEntity { marketUsd: nil, marketPctChange24Hr: nil)) } + + func getSupportedNetworks() -> [BlockchainType] { + switch udWallet.type { + case .mpc: + return [.Ethereum, .Matic, .Base, .Solana, .Bitcoin] + default: + return [.Ethereum, .Matic, .Base] + } + } } extension Array where Element == WalletEntity { diff --git a/unstoppable-ios-app/domains-manager-ios/Extensions/Extension-String+Preview.swift b/unstoppable-ios-app/domains-manager-ios/Extensions/Extension-String+Preview.swift index a810f9f0d..e354db710 100644 --- a/unstoppable-ios-app/domains-manager-ios/Extensions/Extension-String+Preview.swift +++ b/unstoppable-ios-app/domains-manager-ios/Extensions/Extension-String+Preview.swift @@ -245,6 +245,12 @@ extension String { static let setup = "SETUP" static let networkFee = "NETWORK_FEE" static let reconnect = "RECONNECT" + static let income = "INCOME" + static let outcome = "OUTCOME" + static let filter = "FILTER" + static let reset = "RESET" + static let chains = "CHAINS" + static let activityWith = "ACTIVITY_WITH" //Onboarding static let alreadyMintedDomain = "ALREADY_MINTED_DOMAIN" diff --git a/unstoppable-ios-app/domains-manager-ios/Extensions/Extensions.swift b/unstoppable-ios-app/domains-manager-ios/Extensions/Extensions.swift index f1688e7b9..58e66cb4d 100644 --- a/unstoppable-ios-app/domains-manager-ios/Extensions/Extensions.swift +++ b/unstoppable-ios-app/domains-manager-ios/Extensions/Extensions.swift @@ -174,7 +174,7 @@ extension UIViewController { } extension Array { - mutating func remove(at indexes: [Int]) { + mutating func remove(atIndexes indexes: [Int]) { for index in indexes.sorted(by: >) { remove(at: index) } diff --git a/unstoppable-ios-app/domains-manager-ios/Extensions/UIColor.swift b/unstoppable-ios-app/domains-manager-ios/Extensions/UIColor.swift index 9d2223fba..6ef9c5af5 100644 --- a/unstoppable-ios-app/domains-manager-ios/Extensions/UIColor.swift +++ b/unstoppable-ios-app/domains-manager-ios/Extensions/UIColor.swift @@ -41,6 +41,7 @@ extension UIColor { static let backgroundEmphasisOpacity = UIColor(named: "backgroundEmphasisOpacity") ?? .black static let backgroundEmphasisOpacity2 = UIColor(named: "backgroundEmphasisOpacity2") ?? .black static let backgroundAccent = UIColor(named: "backgroundAccent") ?? .black + static let backgroundAccentMuted = UIColor(named: "backgroundAccentMuted") ?? .black static let backgroundAccentEmphasis = UIColor(named: "backgroundAccentEmphasis") ?? .black static let backgroundAccentEmphasis2 = UIColor(named: "backgroundAccentEmphasis2") ?? .black static let backgroundSuccess = UIColor(named: "backgroundSuccess") ?? .black diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivity.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivity.swift index c32cf1945..8b3e0a33d 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivity.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivity.swift @@ -57,3 +57,66 @@ extension HomeActivity { } } + +import SwiftUI + +// MARK: - Filter options +extension HomeActivity { + + enum TransactionSubject: String, Hashable, CaseIterable, IconTitleSelectableGridItem { + case transfer + case collectible + case domain + + var gridTitle: String { + switch self { + case .transfer: + String.Constants.tokens.localized() + case .collectible: + String.Constants.collectibles.localized() + case .domain: + String.Constants.domains.localized() + } + } + var gridIcon: Image { + switch self { + case .transfer: + .twoCoinsIcon + case .collectible: + .cryptoFaceIcon + case .domain: + .layoutGridTwo + } + } + var gridAnalyticsValue: String { rawValue } + } + + enum TransactionDestination: String, CaseIterable, UDSegmentedControlItem { + case all + case income + case outcome + + var title: String { + switch self { + case .all: + String.Constants.all.localized() + case .income: + String.Constants.income.localized() + case .outcome: + String.Constants.outcome.localized() + } + } + + var analyticButton: Analytics.Button { + switch self { + case .all: + .all + case .income: + .income + case .outcome: + .outcome + } + } + } + +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivityFilterView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivityFilterView.swift new file mode 100644 index 000000000..5a9b42a0d --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivityFilterView.swift @@ -0,0 +1,87 @@ +// +// HomeActivityFilterView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 18.07.2024. +// + +import SwiftUI + +struct HomeActivityFilterView: View, ViewAnalyticsLogger { + + @Environment(\.dismiss) var dismiss + @EnvironmentObject var viewModel: HomeActivityViewModel + let analyticsName: Analytics.ViewName = .homeActivityFilters + + var body: some View { + NavigationStack { + ScrollView { + VStack(spacing: 24) { + UDSegmentedControlView(selection: $viewModel.selectedDestinationFilter, + items: HomeActivity.TransactionDestination.allCases, + height: 44, + customSegmentLabel: nil) + + HomeExploreSeparatorView() + + IconTitleSelectionGridView(title: String.Constants.chains.localized(), + selection: .multiple($viewModel.selectedChainsFilter), + items: viewModel.supportedNetworks) + + HomeExploreSeparatorView() + + IconTitleSelectionGridView(title: String.Constants.activityWith.localized(), + selection: .multiple($viewModel.selectedSubjectsFilter), + items: HomeActivity.TransactionSubject.allCases) + } + .padding() + } + .trackAppearanceAnalytics(analyticsLogger: self) + .passViewAnalyticsDetails(logger: self) + .background(Color.backgroundDefault) + .navigationTitle(String.Constants.filter.localized()) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + CloseButtonView(closeCallback: closeButtonPressed) + } + + ToolbarItem(placement: .topBarTrailing) { + resetButtonView() + } + } + } + } + +} + +// MARK: - Private methods +private extension HomeActivityFilterView { + func closeButtonPressed() { + dismiss() + } + + @ViewBuilder + func resetButtonView() -> some View { + Button { + UDVibration.buttonTap.vibrate() + logButtonPressedAnalyticEvents(button: .reset) + resetButtonPressed() + } label: { + Text(String.Constants.reset.localized()) + .textAttributes(color: .foregroundAccent, + fontSize: 16, + fontWeight: .medium) + } + .buttonStyle(.plain) + } + + func resetButtonPressed() { + viewModel.resetFilters() + } +} + +#Preview { + HomeActivityFilterView() + .environmentObject(MockEntitiesFabric.WalletTxs.createViewModel()) +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivityView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivityView.swift index 5d6353ecd..89aa0924f 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivityView.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivityView.swift @@ -16,6 +16,8 @@ struct HomeActivityView: View, ViewAnalyticsLogger { var isOtherScreenPushed: Bool { !tabRouter.activityTabNavPath.isEmpty } var analyticsName: Analytics.ViewName { .homeActivity } + @State private var showingFiltersView = false + var body: some View { NavigationViewWithCustomTitle(content: { contentList() @@ -23,7 +25,6 @@ struct HomeActivityView: View, ViewAnalyticsLogger { .background(Color.backgroundDefault) .navigationTitle("") .navigationBarTitleDisplayMode(.inline) - .environmentObject(viewModel) .passViewAnalyticsDetails(logger: self) .displayError($viewModel.error) .background(Color.backgroundMuted2) @@ -49,16 +50,20 @@ struct HomeActivityView: View, ViewAnalyticsLogger { } .navigationDestination(for: HomeActivityNavigationDestination.self) { destination in HomeActivityLinkNavigationDestination.viewFor(navigationDestination: destination, - tabRouter: tabRouter) + tabRouter: tabRouter) .environmentObject(navigationState!) .environmentObject(viewModel) } .toolbar(content: { // To keep nav bar background visible when scrolling - ToolbarItem(placement: .topBarLeading) { - Color.clear + ToolbarItem(placement: .topBarTrailing) { + filterButtonView() } }) + .sheet(isPresented: $showingFiltersView, content: { + HomeActivityFilterView() + }) + .environmentObject(viewModel) }, navigationStateProvider: { state in self.navigationState = state }, path: $tabRouter.activityTabNavPath) @@ -91,8 +96,14 @@ private extension HomeActivityView { func contentList() -> some View { if viewModel.groupedTxs.isEmpty, !viewModel.isLoadingMore { - HomeActivityEmptyView() - .frame(maxWidth: .infinity, maxHeight: .infinity) + GeometryReader { geometry in + /// ScrollView needed to keep PTR functionality + ScrollView { + HomeActivityEmptyView() + .frame(width: geometry.size.width, height: geometry.size.height) + } + .frame(width: geometry.size.width, height: geometry.size.height) + } } else { txsList() } @@ -108,6 +119,28 @@ private extension HomeActivityView { .listStyle(.plain) .listRowSpacing(0) } + + @ViewBuilder + func filterButtonView() -> some View { + ZStack(alignment: .topTrailing) { + Button { + UDVibration.buttonTap.vibrate() + showingFiltersView = true + } label: { + Image.filter + .resizable() + .foregroundStyle(Color.foregroundDefault) + .squareFrame(28) + } + + if viewModel.isFiltersApplied { + Circle() + .squareFrame(16) + .foregroundStyle(Color.foregroundAccent) + .offset(x: 6, y: -2) + } + } + } } #Preview { @@ -116,5 +149,4 @@ private extension HomeActivityView { return HomeActivityView(viewModel: viewModel) .environmentObject(router) - } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivityViewModel.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivityViewModel.swift index 4a18fcc9e..903dd26a1 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivityViewModel.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/HomeActivityViewModel.swift @@ -16,6 +16,9 @@ final class HomeActivityViewModel: ObservableObject, ViewAnalyticsLogger { @Published var searchKey: String = "" @Published var isKeyboardActive: Bool = false @Published var error: Error? + @Published var selectedChainsFilter: [BlockchainType] = [] + @Published var selectedSubjectsFilter: [HomeActivity.TransactionSubject] = [] + @Published var selectedDestinationFilter: HomeActivity.TransactionDestination = .all @Published private var txsResponses: [WalletTransactionsResponse] = [] @Published private(set) var isLoadingMore = false @@ -46,18 +49,37 @@ extension HomeActivityViewModel { HomeActivity.GroupedTransactions.buildGroupsFrom(txs: txsDisplayInfo) } + var isFiltersApplied: Bool { + guard selectedDestinationFilter == .all else { return true } + guard selectedChainsFilter.isEmpty else { return true } + guard selectedSubjectsFilter.isEmpty else { return true } + + return false + } + private var txsDisplayInfo: [WalletTransactionDisplayInfo] { switch selectedProfile { case .wallet(let wallet): - return txsResponses.flatMap { $0.txs }.map { + var txs: [WalletTransactionDisplayInfo] = txsResponses.flatMap { $0.txs }.map { WalletTransactionDisplayInfo(serializedTransaction: $0, userWallet: wallet.address) } + filterTxsForSelectedSubjects(&txs) + filterTxsForSelectedDestination(&txs) + + return txs case .webAccount: return [] } } + var supportedNetworks: [BlockchainType] { + if case .wallet(let wallet) = selectedProfile { + return wallet.getSupportedNetworks() + } + return [] + } + func willDisplayTransaction(_ transaction: WalletTransactionDisplayInfo) { let txsDisplayInfo = self.txsDisplayInfo.sorted(by: { $0.time > $1.time }) if txsResponses.first(where: { $0.canLoadMore }) != nil, @@ -75,6 +97,12 @@ extension HomeActivityViewModel { func didSelectTx(tx: WalletTransactionDisplayInfo) { router.pullUp = .custom(.transactionDetailsPullUp(tx: tx)) } + + func resetFilters() { + selectedChainsFilter = [] + selectedSubjectsFilter = [] + selectedDestinationFilter = .all + } } // MARK: - Setup methods @@ -87,11 +115,18 @@ private extension HomeActivityViewModel { self?.didUpdateSelectedProfile() } }.store(in: &cancellables) + $selectedChainsFilter.sink { [weak self] _ in + self?.resetAndReloadTxs() + }.store(in: &cancellables) loadTxsForSelectedProfileNonBlocking(forceReload: true) } func didUpdateSelectedProfile() { + resetAndReloadTxs() + } + + func resetAndReloadTxs() { txsResponses = [] loadTxsForSelectedProfileNonBlocking(forceReload: true) } @@ -109,12 +144,14 @@ private extension HomeActivityViewModel { do { let tokens = getWalletTokens() let addresses = Set(tokens.map { $0.address }) - + let chainsToLoad: [BlockchainType]? = getChainsListToLoad() + var responses = [WalletTransactionsResponse]() try await withThrowingTaskGroup(of: WalletTransactionsResponse.self) { group in for address in addresses { group.addTask { - try await self.walletTransactionsService.getTransactionsFor(wallet: address, + try await self.walletTransactionsService.getTransactionsFor(wallet: address, + chains: chainsToLoad, forceReload: forceReload) } } @@ -141,4 +178,44 @@ private extension HomeActivityViewModel { return [token] } } + + func getChainsListToLoad() -> [BlockchainType]? { + if !selectedChainsFilter.isEmpty { + return selectedChainsFilter + } + return nil + } + + func filterTxsForSelectedSubjects(_ txs: inout [WalletTransactionDisplayInfo]) { + guard !selectedSubjectsFilter.isEmpty else { return } + + txs = txs.filter({ tx in + let txSubject: HomeActivity.TransactionSubject = getSubjectOfTx(tx) + let isTxSubjectSelected: Bool = selectedSubjectsFilter.contains(txSubject) + + return isTxSubjectSelected + }) + } + + func getSubjectOfTx(_ tx: WalletTransactionDisplayInfo) -> HomeActivity.TransactionSubject { + if tx.type.isNFT { + if tx.isDomainNFT { + return .domain + } else { + return .collectible + } + } + return .transfer + } + + func filterTxsForSelectedDestination(_ txs: inout [WalletTransactionDisplayInfo]) { + switch selectedDestinationFilter { + case .all: + return + case .income: + txs = txs.filter({ $0.type.isDeposit }) + case .outcome: + txs = txs.filter({ !$0.type.isDeposit }) + } + } } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/IconTitleSelectionGridView.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/IconTitleSelectionGridView.swift new file mode 100644 index 000000000..4a039a167 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/IconTitleSelectionGridView.swift @@ -0,0 +1,128 @@ +// +// IconTitleSelectionGridView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 22.07.2024. +// + +import SwiftUI + +protocol IconTitleSelectableGridItem: Hashable { + var gridTitle: String { get } + var gridIcon: Image { get } + var gridAnalyticsValue: String { get } +} + +struct IconTitleSelectionGridView: View, ViewAnalyticsLogger { + @Environment(\.analyticsViewName) var analyticsName + @Environment(\.analyticsAdditionalProperties) var additionalAppearAnalyticParameters + + let title: String + let selection: SelectionStyle + let items: [Item] + private let cornerRadius: CGFloat = 12 + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Text(title) + .textAttributes(color: .foregroundDefault, + fontSize: 16, + fontWeight: .medium) + .frame(height: 24) + VStack(spacing: 0) { + ListVGrid(data: items, + numberOfColumns: 3, + verticalSpacing: 16, + horizontalSpacing: 16) { item in + Button { + UDVibration.buttonTap.vibrate() + + logButtonPressedAnalyticEvents(button: .filterOption, + parameters: [.value : item.gridAnalyticsValue]) + didSelectItem(item) + } label: { + gridItemViewFor(item) + } + .buttonStyle(.plain) + } + } + } + } + + func didSelectItem(_ item: Item) { + switch selection { + case .single(let binding): + if item == binding.wrappedValue { + binding.wrappedValue = nil + } else { + binding.wrappedValue = item + } + case .multiple(let binding): + if let i = binding.wrappedValue.firstIndex(of: item) { + binding.wrappedValue.remove(at: i) + } else { + binding.wrappedValue.append(item) + } + } + } + + @ViewBuilder + func gridItemViewFor(_ item: Item) -> some View { + VStack(spacing: 12) { + item.gridIcon + .resizable() + .squareFrame(24) + .foregroundStyle(iconTintColorFor(item)) + Text(item.gridTitle) + .foregroundStyle(Color.foregroundDefault) + .frame(height: 20) + .frame(maxWidth: .infinity) + } + .padding(12) + .aspectRatio(110/80, contentMode: .fit) + .background(backgroundColorFor(item)) + .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) + .overlay { + RoundedRectangle(cornerRadius: cornerRadius) + .stroke(borderColorFor(item), lineWidth: 1) + + if isItemSelected(item) { + RoundedRectangle(cornerRadius: cornerRadius) + .inset(by: -1.5) + .stroke(Color.foregroundAccent, lineWidth: 2) + } + } + } + + func iconTintColorFor(_ item: Item) -> Color { + isItemSelected(item) ? Color.foregroundAccent : Color.foregroundDefault + } + + func backgroundColorFor(_ item: Item) -> Color { + isItemSelected(item) ? Color.backgroundAccentMuted : Color.backgroundOverlay + } + + func borderColorFor(_ item: Item) -> Color { + isItemSelected(item) ? Color.backgroundDefault : Color.white.opacity(0.08) + } + + func isItemSelected(_ item: Item) -> Bool { + switch selection { + case .single(let binding): + return item == binding.wrappedValue + case .multiple(let binding): + return binding.wrappedValue.contains(item) + } + } + + enum SelectionStyle { + case single(Binding) + case multiple(Binding<[Item]>) + } +} + +extension BlockchainType: IconTitleSelectableGridItem { + var gridTitle: String { fullName } + var gridIcon: Image { Image(uiImage: self.chainIcon) } + var gridAnalyticsValue: String { shortCode } +} diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/WalletTransactionDisplayInfo.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/WalletTransactionDisplayInfo.swift index c54e0ba98..2d89489b0 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/WalletTransactionDisplayInfo.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/Activity/WalletTransactionDisplayInfo.swift @@ -116,6 +116,15 @@ extension WalletTransactionDisplayInfo { false } } + + var isNFT: Bool { + switch self { + case .nftWithdrawal, .nftDeposit: + true + case .tokenWithdrawal, .tokenDeposit: + false + } + } } } diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/Home/SendCryptoAsset/SendCryptoAssetViewModel.swift b/unstoppable-ios-app/domains-manager-ios/Modules/Home/SendCryptoAsset/SendCryptoAssetViewModel.swift index bdc816a26..7183d12ce 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/Home/SendCryptoAsset/SendCryptoAssetViewModel.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/Home/SendCryptoAsset/SendCryptoAssetViewModel.swift @@ -176,12 +176,7 @@ final class SendCryptoAssetViewModel: ObservableObject { } func getWalletAddressDetailsFor(address: String) -> SendCryptoAsset.WalletAddressDetails? { - let availableNetworks: [BlockchainType] - if sourceWallet.displayInfo.source == .mpc { - availableNetworks = BlockchainType.allCases - } else { - availableNetworks = [.Ethereum] - } + let availableNetworks: [BlockchainType] = sourceWallet.getSupportedNetworks() if let network = availableNetworks.first(where: { $0.isStringMatchingRegex(address) }) { return .init(address: address, network: network) diff --git a/unstoppable-ios-app/domains-manager-ios/Modules/WebViewController/BuyDomainsWebViewController.swift b/unstoppable-ios-app/domains-manager-ios/Modules/WebViewController/BuyDomainsWebViewController.swift index 1d82eb39a..660ff711a 100644 --- a/unstoppable-ios-app/domains-manager-ios/Modules/WebViewController/BuyDomainsWebViewController.swift +++ b/unstoppable-ios-app/domains-manager-ios/Modules/WebViewController/BuyDomainsWebViewController.swift @@ -90,7 +90,7 @@ private extension BuyDomainsWebViewController { // MARK: - Setup methods private extension BuyDomainsWebViewController { func setup() { - toolbarItems?.remove(at: [7,5]) + toolbarItems?.remove(atIndexes: [7,5]) } } diff --git a/unstoppable-ios-app/domains-manager-ios/NetworkEnvironment/ApiRequestBuilder.swift b/unstoppable-ios-app/domains-manager-ios/NetworkEnvironment/ApiRequestBuilder.swift index b238c0cd0..a3fd0c3be 100644 --- a/unstoppable-ios-app/domains-manager-ios/NetworkEnvironment/ApiRequestBuilder.swift +++ b/unstoppable-ios-app/domains-manager-ios/NetworkEnvironment/ApiRequestBuilder.swift @@ -914,14 +914,16 @@ extension Endpoint { static func getProfileWalletTransactions(for wallet: String, cursor: String?, - chain: String?, + chains: [BlockchainType]?, forceRefresh: Bool) -> Endpoint { var queryItems: [URLQueryItem] = [] if let cursor { queryItems.append(URLQueryItem(name: "cursor", value: cursor)) } - if let chain { - queryItems.append(URLQueryItem(name: "symbols", value: chain)) + if let chains, + !chains.isEmpty { + let symbolsList: String = chains.map { $0.shortCode }.joined(separator: ",") + queryItems.append(URLQueryItem(name: "symbols", value: symbolsList)) } if forceRefresh { queryItems.append(URLQueryItem(name: "forceRefresh", value: String(Int(Date().timeIntervalSince1970)))) diff --git a/unstoppable-ios-app/domains-manager-ios/Protocols/BlockChainItem.swift b/unstoppable-ios-app/domains-manager-ios/Protocols/BlockChainItem.swift index 59fcdc4e3..4b24cb536 100644 --- a/unstoppable-ios-app/domains-manager-ios/Protocols/BlockChainItem.swift +++ b/unstoppable-ios-app/domains-manager-ios/Protocols/BlockChainItem.swift @@ -68,7 +68,7 @@ extension Array where Element: DomainEntity { let indeces = self.enumerated() .filter({domainNames.contains($0.element.name)}) .map({$0.offset}) - self.remove(at: indeces) + self.remove(atIndexes: indeces) } } diff --git a/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift b/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift index ded8b6f60..dd40e13e3 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/AnalyticsService/AnalyticsServiceEnvironment.swift @@ -240,7 +240,7 @@ extension Analytics { case shareWalletInfo, nftDetails, profileSelection case updateToWalletGreetings case homeExplore - case homeActivity + case homeActivity, homeActivityFilters case sendCryptoReceiverSelection, sendCryptoAssetSelection, sendCryptoTokenAmountInput, sendCryptoDomainTransferConfirmation, sendCryptoTokenConfirmation, sendCryptoScanQRCode case transferDomainSuccess, sendCryptoSuccess @@ -445,6 +445,9 @@ extension Analytics { case useDifferentEmail, useRecovery, dontUseRecovery case contactSupport case reconnect + + case all, income, outcome + case filterOption, reset } } diff --git a/unstoppable-ios-app/domains-manager-ios/Services/Networking/NetworkService+ProfilesApi.swift b/unstoppable-ios-app/domains-manager-ios/Services/Networking/NetworkService+ProfilesApi.swift index 6ef77242d..12a791213 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/Networking/NetworkService+ProfilesApi.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/Networking/NetworkService+ProfilesApi.swift @@ -477,11 +477,11 @@ extension NetworkService: WalletTransactionsNetworkServiceProtocol { func getTransactionsFor(wallet: HexAddress, cursor: String?, - chain: String?, + chains: [BlockchainType]?, forceRefresh: Bool) async throws -> [WalletTransactionsPerChainResponse] { let endpoint = Endpoint.getProfileWalletTransactions(for: wallet, cursor: cursor, - chain: chain, + chains: chains, forceRefresh: forceRefresh) let data = try await fetchDataFor(endpoint: endpoint, method: .get) diff --git a/unstoppable-ios-app/domains-manager-ios/Services/UserProfileService/UserProfilesService.swift b/unstoppable-ios-app/domains-manager-ios/Services/UserProfileService/UserProfilesService.swift index 7666835b5..acdb80aa9 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/UserProfileService/UserProfilesService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/UserProfileService/UserProfilesService.swift @@ -48,7 +48,7 @@ final class UserProfilesService { } func loadParkedDomainsAndCheckProfile() { - Task { + Task { @MainActor in let domains = (try? await firebaseParkedDomainsService.getParkedDomains()) ?? [] if !domains.isEmpty { updateProfilesList() diff --git a/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsNetworkServiceProtocol.swift b/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsNetworkServiceProtocol.swift index 393ff05de..6481dff1c 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsNetworkServiceProtocol.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsNetworkServiceProtocol.swift @@ -10,6 +10,6 @@ import Foundation protocol WalletTransactionsNetworkServiceProtocol { func getTransactionsFor(wallet: HexAddress, cursor: String?, - chain: String?, + chains: [BlockchainType]?, forceRefresh: Bool) async throws -> [WalletTransactionsPerChainResponse] } diff --git a/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsPerChainResponse.swift b/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsPerChainResponse.swift index 796407604..065ff4ab0 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsPerChainResponse.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsPerChainResponse.swift @@ -11,4 +11,8 @@ struct WalletTransactionsPerChainResponse: Codable { let chain: String let cursor: String? let txs: [SerializedWalletTransaction] + + func resolveBlockchainType() -> BlockchainType? { + try? BlockchainType.resolve(shortCode: chain) + } } diff --git a/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsService.swift b/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsService.swift index 2718eb19d..ec4c514dc 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsService.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsService.swift @@ -22,7 +22,9 @@ final class WalletTransactionsService { // MARK: - WalletTransactionsServiceProtocol extension WalletTransactionsService: WalletTransactionsServiceProtocol { - func getTransactionsFor(wallet: HexAddress, forceReload: Bool) async throws -> WalletTransactionsResponse { + func getTransactionsFor(wallet: HexAddress, + chains: [BlockchainType]?, + forceReload: Bool) async throws -> WalletTransactionsResponse { if !forceReload, let cachedResponse = await cache.fetchTransactionsFromCache(wallet: wallet) { var response: [WalletTransactionsPerChainResponse] = [] if cachedResponse.hasAnyToLoadMore() { @@ -33,9 +35,9 @@ extension WalletTransactionsService: WalletTransactionsServiceProtocol { return WalletTransactionsResponse(canLoadMore: response.hasAnyToLoadMore(), txs: response.combinedTxs()) } else { - let newResponse = try await fetchAndCacheOnlyTransactions(for: wallet) + let newResponse = try await fetchAndCacheOnlyTransactions(for: wallet, chains: chains) return WalletTransactionsResponse(canLoadMore: newResponse.hasAnyToLoadMore(), - txs: newResponse.combinedTxs()) + txs: newResponse.combinedTxs()) } } } @@ -47,9 +49,10 @@ private extension WalletTransactionsService { try await withThrowingTaskGroup(of: [WalletTransactionsPerChainResponse].self) { group in for response in responses { - if let cursor = response.cursor { + if let cursor = response.cursor, + let chain = response.resolveBlockchainType() { group.addTask { - try await self.fetchTransactions(for: wallet, cursor: cursor, chain: response.chain, forceRefresh: false) + try await self.fetchTransactions(for: wallet, cursor: cursor, chains: [chain], forceRefresh: false) } } else { newResponses.append(response) @@ -65,17 +68,18 @@ private extension WalletTransactionsService { return finalResult } - func fetchAndCacheOnlyTransactions(for wallet: HexAddress) async throws -> [WalletTransactionsPerChainResponse] { - let newResponse = try await fetchTransactions(for: wallet, cursor: nil, chain: nil, forceRefresh: true) + func fetchAndCacheOnlyTransactions(for wallet: HexAddress, + chains: [BlockchainType]?) async throws -> [WalletTransactionsPerChainResponse] { + let newResponse = try await fetchTransactions(for: wallet, cursor: nil, chains: chains, forceRefresh: true) await cache.setTransactionsToCache(newResponse, for: wallet) return newResponse } func fetchTransactions(for wallet: HexAddress, cursor: String?, - chain: String?, + chains: [BlockchainType]?, forceRefresh: Bool) async throws -> [WalletTransactionsPerChainResponse] { - try await networkService.getTransactionsFor(wallet: wallet, cursor: cursor, chain: chain, forceRefresh: forceRefresh) + try await networkService.getTransactionsFor(wallet: wallet, cursor: cursor, chains: chains, forceRefresh: forceRefresh) } func mergeResponsesWithLocalCache(_ newResponses: [WalletTransactionsPerChainResponse], for wallet: HexAddress) async -> [WalletTransactionsPerChainResponse] { diff --git a/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsServiceProtocol.swift b/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsServiceProtocol.swift index 041e6d9b0..f093e5edd 100644 --- a/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsServiceProtocol.swift +++ b/unstoppable-ios-app/domains-manager-ios/Services/WalletTransactionsService/WalletTransactionsServiceProtocol.swift @@ -8,5 +8,7 @@ import Foundation protocol WalletTransactionsServiceProtocol { - func getTransactionsFor(wallet: HexAddress, forceReload: Bool) async throws -> WalletTransactionsResponse + func getTransactionsFor(wallet: HexAddress, + chains: [BlockchainType]?, + forceReload: Bool) async throws -> WalletTransactionsResponse } diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/AppDelegate.swift b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/AppDelegate.swift index d6b616f9c..52dc6730c 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/AppDelegate.swift +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/AppDelegate.swift @@ -32,7 +32,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate { #if DEBUG - Debugger.setAllowedTopicsSet(.debugDefault) + Debugger.setAllowedTopicsSet(.debugNetwork) // CoreDataMessagingStorageService(decrypterService: AESMessagingContentDecrypterService()).clear() // MessagingFilesService(decrypterService: AESMessagingContentDecrypterService()).clear() // clearImagesCache() diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/baseIcon.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/baseIcon.imageset/Contents.json index 349cefea9..679abe13b 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/baseIcon.imageset/Contents.json +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/baseIcon.imageset/Contents.json @@ -10,6 +10,6 @@ "version" : 1 }, "properties" : { - "template-rendering-intent" : "original" + "template-rendering-intent" : "template" } } diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/filter.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/filter.imageset/Contents.json new file mode 100644 index 000000000..a5f91380c --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/filter.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "filter.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/filter.imageset/filter.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/filter.imageset/filter.svg new file mode 100644 index 000000000..d265ac469 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/filter.imageset/filter.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/layoutGridTwo.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/layoutGridTwo.imageset/Contents.json new file mode 100644 index 000000000..d89731850 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/layoutGridTwo.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "layoutGridTwo.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/layoutGridTwo.imageset/layoutGridTwo.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/layoutGridTwo.imageset/layoutGridTwo.svg new file mode 100644 index 000000000..99a947934 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/layoutGridTwo.imageset/layoutGridTwo.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/solanaIcon.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/solanaIcon.imageset/Contents.json index 1d31706ad..4d0a3df7d 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/solanaIcon.imageset/Contents.json +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/solanaIcon.imageset/Contents.json @@ -8,5 +8,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/twoCoinsIcon.imageset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/twoCoinsIcon.imageset/Contents.json new file mode 100644 index 000000000..87aa865a8 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/twoCoinsIcon.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "twoCoinsIcon.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/twoCoinsIcon.imageset/twoCoinsIcon.svg b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/twoCoinsIcon.imageset/twoCoinsIcon.svg new file mode 100644 index 000000000..4dd3a156c --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Assets.xcassets/Common/twoCoinsIcon.imageset/twoCoinsIcon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Colors.xcassets/New/Background/backgroundAccentMuted.colorset/Contents.json b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Colors.xcassets/New/Background/backgroundAccentMuted.colorset/Contents.json new file mode 100644 index 000000000..c37912562 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Colors.xcassets/New/Background/backgroundAccentMuted.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.080", + "blue" : "0xFE", + "green" : "0x67", + "red" : "0x0D" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "0.080", + "blue" : "0xFE", + "green" : "0x7F", + "red" : "0x34" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.strings b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.strings index 5e28295a7..d9dc52b44 100644 --- a/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.strings +++ b/unstoppable-ios-app/domains-manager-ios/SupportingFiles/Localization/en.lproj/Localizable.strings @@ -183,6 +183,12 @@ "SETUP" = "Setup"; "NETWORK_FEE" = "Network Fee"; "RECONNECT" = "Reconnect"; +"INCOME" = "Income"; +"OUTCOME" = "Outcome"; +"FILTER" = "Filter"; +"RESET" = "Reset"; +"CHAINS" = "Chains"; +"ACTIVITY_WITH" = "Activity with"; /* ONBOARDING */ // Tutorial screens diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/ListVGrid.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/ListVGrid.swift index 30d9fdaf8..c141e4cff 100644 --- a/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/ListVGrid.swift +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/ListVGrid.swift @@ -17,7 +17,7 @@ struct ListVGrid: View { @ViewBuilder var content: (Element) -> Row private let sideOffsets: CGFloat = 32 private var numberOfColumnsForData: Int { - (data.count / 2) + 1 + (data.count / numberOfColumns) + 1 } var body: some View { ForEach(0..: View { } private func findElement(column: Int, row: Int) -> Element? { - let n = (column * 2) + row + let n = (column * numberOfColumns) + row if n <= data.count - 1 { return data[n] } diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/SelectionPopoverView/SelectionPopoverView.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/SelectionPopoverView/SelectionPopoverView.swift new file mode 100644 index 000000000..000544127 --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/SelectionPopoverView/SelectionPopoverView.swift @@ -0,0 +1,120 @@ +// +// SelectionPopoverView.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 18.07.2024. +// + +import SwiftUI + +struct SelectionPopoverView: View { + + let items: [Item] + @Binding var selectedItems: [Item] + var isMultipleSelectionAllowed: Bool = false + @ViewBuilder var label: () -> Content + + @State private var showingSelectionList = false + + var body: some View { + Button { + UDVibration.buttonTap.vibrate() + showingSelectionList = true + } label: { + label() + } + .alwaysPopover(isPresented: $showingSelectionList) { + SelectionListView(items: items, + selectedItems: $selectedItems, + isMultipleSelectionAllowed: isMultipleSelectionAllowed) + } + } + + struct SelectionListView: View { + + @Environment(\.dismiss) var dismiss + + let items: [Item] + @Binding var selectedItems: [Item] + let isMultipleSelectionAllowed: Bool + + var body: some View { + ScrollView { + VStack(spacing: 0) { + ForEach(items, id: \.self) { item in + selectionRowView(item: item) + if item != items.last { + separatorView() + } + } + } + } + .scrollDisabled(true) + .frame(minWidth: 250) + .background(.thinMaterial) + .animation(.default, value: UUID()) + } + + @ViewBuilder + func selectionRowView(item: Item) -> some View { + Button { + UDVibration.buttonTap.vibrate() + if let i = selectedItems.firstIndex(where: { $0 == item }) { + selectedItems.remove(at: i) + } else { + if isMultipleSelectionAllowed { + selectedItems.append(item) + } else { + selectedItems = [item] + } + } + + if !isMultipleSelectionAllowed { + dismiss() + } + } label: { + Text("") + } + .buttonStyle(ControllableButtonStyle(state: .init(isEnabled: true), change: { state in + HStack { + Image(systemName: isItemSelected(item) ? "checkmark" : "") + .resizable() + .squareFrame(12) + .bold() + Text(item.selectionTitle) + .font(.callout) + Spacer() + } + .padding(.horizontal, 12) + .frame(height: 40) + .frame(minWidth: 200) + .background(state.pressed ? Color(uiColor: .label).opacity(0.1) : Color.clear) + .listRowInsets(.init(horizontal: 0, vertical: 0)) + .contentShape(Rectangle()) + })) + } + + func isItemSelected(_ item: Item) -> Bool { + selectedItems.contains(item) + } + + @ViewBuilder + func separatorView() -> some View { + Line(direction: .horizontal) + .stroke(lineWidth: 1) + .frame(height: 1) + .foregroundStyle(Color(uiColor: .separator).opacity(0.6)) + } + } + +} + + +#Preview { + SelectionPopoverView(items: BlockchainType.allCases, + selectedItems: .constant([.Ethereum]), + isMultipleSelectionAllowed: true, + label: { + Text("Popover") + }) +} diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/SelectionPopoverView/SelectionPopoverViewItem.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/SelectionPopoverView/SelectionPopoverViewItem.swift new file mode 100644 index 000000000..1b21ca56e --- /dev/null +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/SelectionPopoverView/SelectionPopoverViewItem.swift @@ -0,0 +1,16 @@ +// +// SelectionPopoverViewItem.swift +// domains-manager-ios +// +// Created by Oleg Kuplin on 18.07.2024. +// + +import Foundation + +protocol SelectionPopoverViewItem: Hashable { + var selectionTitle: String { get } +} + +extension BlockchainType: SelectionPopoverViewItem { + var selectionTitle: String { shortCode } +} diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/UDSegmentedControlView.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/UDSegmentedControlView.swift index 97cd42309..fed108e7a 100644 --- a/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/UDSegmentedControlView.swift +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/CommonViews/UDSegmentedControlView.swift @@ -14,6 +14,7 @@ struct UDSegmentedControlView: View, ViewAnal @Binding var selection: Selection let items: [Selection] + var height: CGFloat = 36 var customSegmentLabel: ((Selection) -> any View)? = nil var body: some View { @@ -23,10 +24,11 @@ struct UDSegmentedControlView: View, ViewAnal selectedBackgroundView(width: proxy.size.width / CGFloat(items.count)) viewForItems(items) } - .frame(height: 36) + .frame(height: height) .animation(.easeInOut(duration: 0.25), value: selection) .frame(maxWidth: .infinity) } + .frame(height: height) } } @@ -47,7 +49,7 @@ private extension UDSegmentedControlView { func selectedBackgroundView(width: CGFloat) -> some View { Color.white .clipShape(RoundedRectangle(cornerRadius: 8)) - .frame(width: width, height: 28) + .frame(width: width, height: height - 8) .offset(x: selectedIndexXOffset + selectedIndexOffset(width: width), y: 0) } diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Color.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Color.swift index eb762af85..ba7736471 100644 --- a/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Color.swift +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Color.swift @@ -41,6 +41,7 @@ extension Color { static let backgroundEmphasisOpacity = Color(uiColor: .backgroundEmphasisOpacity) static let backgroundEmphasisOpacity2 = Color(uiColor: .backgroundEmphasisOpacity2) static let backgroundAccent = Color(uiColor: .backgroundAccent) + static let backgroundAccentMuted = Color(uiColor: .backgroundAccentMuted) static let backgroundAccentEmphasis = Color(uiColor: .backgroundAccentEmphasis) static let backgroundAccentEmphasis2 = Color(uiColor: .backgroundAccentEmphasis2) static let backgroundSuccess = Color(uiColor: .backgroundSuccess) diff --git a/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Image.swift b/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Image.swift index b79e5be7c..5b6c7b36e 100644 --- a/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Image.swift +++ b/unstoppable-ios-app/domains-manager-ios/SwiftUI/Extensions/Image.swift @@ -131,6 +131,9 @@ extension Image { static let lensIcon = Image("lensIcon") static let farcasterIcon = Image("farcasterIcon") static let walletAddressesIcon = Image("walletAddressesIcon") + static let layoutGridTwo = Image("layoutGridTwo") + static let twoCoinsIcon = Image("twoCoinsIcon") + static let filter = Image("filter") static let systemDocOnDoc = Image(systemName: "doc.on.doc") static let systemAppBadgeCheckmark = Image(systemName: "app.badge.checkmark") diff --git a/unstoppable-ios-app/domains-manager-iosTests/WalletTransactionsServiceTests.swift b/unstoppable-ios-app/domains-manager-iosTests/WalletTransactionsServiceTests.swift index 1523cde1a..02b9fc14f 100644 --- a/unstoppable-ios-app/domains-manager-iosTests/WalletTransactionsServiceTests.swift +++ b/unstoppable-ios-app/domains-manager-iosTests/WalletTransactionsServiceTests.swift @@ -29,7 +29,7 @@ final class WalletTransactionsServiceTests: XCTestCase, WalletDataValidator { extension WalletTransactionsServiceTests { func testCanLoadMoreIfEmptyResponse() async throws { networkService.expectedResponse = [] - let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, forceReload: false) + let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: false) XCTAssertEqual(transactionsResponse.canLoadMore, false) } @@ -40,7 +40,7 @@ extension WalletTransactionsServiceTests { WalletTransactionsPerChainResponse(chain: "MATIC", cursor: nil, txs: []) ] networkService.expectedResponse = expectedResponse - let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, forceReload: false) + let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: false) XCTAssertEqual(transactionsResponse.canLoadMore, false) } @@ -51,7 +51,7 @@ extension WalletTransactionsServiceTests { WalletTransactionsPerChainResponse(chain: "MATIC", cursor: nil, txs: []) ] networkService.expectedResponse = expectedResponse - let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, forceReload: false) + let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: false) XCTAssertEqual(transactionsResponse.canLoadMore, true) } @@ -60,21 +60,21 @@ extension WalletTransactionsServiceTests { // MARK: - Network requests extension WalletTransactionsServiceTests { func testNumberOfRequestsForFirstLoad() async throws { - _ = try await service.getTransactionsFor(wallet: wallet, forceReload: false) + _ = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: false) - XCTAssertEqual(networkService.requests, [.init(wallet: wallet, cursor: nil, chain: nil)]) + XCTAssertEqual(networkService.requests, [.init(wallet: wallet, cursor: nil, chains: nil)]) } func testRequestsWhenNoCacheForceReload() async throws { - _ = try await service.getTransactionsFor(wallet: wallet, forceReload: true) + _ = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: true) - XCTAssertEqual(networkService.requests, [.init(wallet: wallet, cursor: nil, chain: nil)]) + XCTAssertEqual(networkService.requests, [.init(wallet: wallet, cursor: nil, chains: nil)]) } func testRequestsWhenNoCacheNotForceReload() async throws { - _ = try await service.getTransactionsFor(wallet: wallet, forceReload: false) + _ = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: false) - XCTAssertEqual(networkService.requests, [.init(wallet: wallet, cursor: nil, chain: nil)]) + XCTAssertEqual(networkService.requests, [.init(wallet: wallet, cursor: nil, chains: nil)]) } func testRequestsWhenHasCachedNoCursorForceReload() async throws { @@ -83,9 +83,9 @@ extension WalletTransactionsServiceTests { WalletTransactionsPerChainResponse(chain: "MATIC", cursor: nil, txs: []) ] cache.cache[wallet] = expectedResponse - _ = try await service.getTransactionsFor(wallet: wallet, forceReload: true) + _ = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: true) - XCTAssertEqual(networkService.requests, [.init(wallet: wallet, cursor: nil, chain: nil)]) + XCTAssertEqual(networkService.requests, [.init(wallet: wallet, cursor: nil, chains: nil)]) } func testRequestsWhenHasCachedNoCursorNotForceReload() async throws { @@ -94,7 +94,7 @@ extension WalletTransactionsServiceTests { WalletTransactionsPerChainResponse(chain: "MATIC", cursor: nil, txs: []) ] cache.cache[wallet] = expectedResponse - _ = try await service.getTransactionsFor(wallet: wallet, forceReload: false) + _ = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: false) XCTAssertEqual(networkService.requests, []) } @@ -105,9 +105,9 @@ extension WalletTransactionsServiceTests { WalletTransactionsPerChainResponse(chain: "MATIC", cursor: nil, txs: []) ] cache.cache[wallet] = expectedResponse - _ = try await service.getTransactionsFor(wallet: wallet, forceReload: true) + _ = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: true) - XCTAssertEqual(networkService.requests, [.init(wallet: wallet, cursor: nil, chain: nil)]) + XCTAssertEqual(networkService.requests, [.init(wallet: wallet, cursor: nil, chains: nil)]) } func testRequestsWhenHasCachedWithCursorNotForceReload() async throws { @@ -116,9 +116,9 @@ extension WalletTransactionsServiceTests { WalletTransactionsPerChainResponse(chain: "MATIC", cursor: nil, txs: []) ] cache.cache[wallet] = expectedResponse - _ = try await service.getTransactionsFor(wallet: wallet, forceReload: false) + _ = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: false) - XCTAssertEqual(networkService.requests, [.init(wallet: wallet, cursor: "1", chain: "ETH")]) + XCTAssertEqual(networkService.requests, [.init(wallet: wallet, cursor: "1", chains: [.Ethereum])]) } } @@ -130,7 +130,7 @@ extension WalletTransactionsServiceTests { WalletTransactionsPerChainResponse(chain: "ETH", cursor: "1", txs: txs) ] networkService.expectedResponse = expectedResponse - let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, forceReload: false) + let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: false) isSameTxs(txs, transactionsResponse.txs) } @@ -140,7 +140,7 @@ extension WalletTransactionsServiceTests { WalletTransactionsPerChainResponse(chain: "ETH", cursor: nil, txs: txs) ] cache.cache[wallet] = expectedResponse - let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, forceReload: false) + let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: false) isSameTxs(txs, transactionsResponse.txs) } @@ -156,7 +156,7 @@ extension WalletTransactionsServiceTests { ] networkService.expectedResponse = networkResponse - let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, forceReload: false) + let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: false) isSameTxs(txs + newTxs, transactionsResponse.txs) } @@ -172,7 +172,7 @@ extension WalletTransactionsServiceTests { ] networkService.expectedResponse = networkResponse - let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, forceReload: true) + let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: true) isSameTxs(newTxs, transactionsResponse.txs) } @@ -188,7 +188,7 @@ extension WalletTransactionsServiceTests { ] networkService.expectedResponse = networkResponse - let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, forceReload: false) + let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: false) isSameTxsIds((1...4).map { String($0)}, transactionsResponse.txs.map { $0.id }) } @@ -206,7 +206,7 @@ extension WalletTransactionsServiceTests { ] networkService.expectedResponse = networkResponse - let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, forceReload: false) + let transactionsResponse = try await service.getTransactionsFor(wallet: wallet, chains: nil, forceReload: false) isSameTxs(txs + newTxs + otherChaintxs, transactionsResponse.txs) let updatedCache = await cache.fetchTransactionsFromCache(wallet: wallet)! XCTAssertEqual(updatedCache.count, 2) @@ -237,9 +237,9 @@ private final class MockNetworkService: WalletTransactionsNetworkServiceProtocol func getTransactionsFor(wallet: HexAddress, cursor: String?, - chain: String?, + chains: [BlockchainType]?, forceRefresh: Bool) async throws -> [WalletTransactionsPerChainResponse] { - requests.append(.init(wallet: wallet, cursor: cursor, chain: chain)) + requests.append(.init(wallet: wallet, cursor: cursor, chains: chains)) try failIfNeeded() return expectedResponse } @@ -247,7 +247,7 @@ private final class MockNetworkService: WalletTransactionsNetworkServiceProtocol struct Request: Hashable { let wallet: HexAddress let cursor: String? - let chain: String? + let chains: [BlockchainType]? } }