From 63965553197da6297a0859e18e7b6a0b437b6ea9 Mon Sep 17 00:00:00 2001 From: svojsu Date: Wed, 8 Jan 2025 19:24:26 +0200 Subject: [PATCH 01/13] Show dialog when closing multiple tabs on tabs manager screen --- .../DAppBrowserTabListPresenter.swift | 11 ++++++++++- .../DAppBrowserTabListProtocols.swift | 3 ++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabList/DAppBrowserTabListPresenter.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabList/DAppBrowserTabListPresenter.swift index e4b556180..85e375f80 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabList/DAppBrowserTabListPresenter.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabList/DAppBrowserTabListPresenter.swift @@ -68,7 +68,16 @@ extension DAppBrowserTabListPresenter: DAppBrowserTabListPresenterProtocol { } func closeAllTabs() { - interactor.closeAllTabs() + if tabs.count > 1 { + wireframe.presentCloseTabsAlert( + from: view, + with: localizationManager.selectedLocale + ) { [weak self] in + self?.interactor.closeAllTabs() + } + } else { + interactor.closeAllTabs() + } } func closeTab(with id: UUID) { diff --git a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabList/DAppBrowserTabListProtocols.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabList/DAppBrowserTabListProtocols.swift index 2df9f9bdd..8c174e7ca 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabList/DAppBrowserTabListProtocols.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabList/DAppBrowserTabListProtocols.swift @@ -33,7 +33,8 @@ protocol DAppBrowserTabListInteractorOutputProtocol: AnyObject { protocol DAppBrowserTabListWireframeProtocol: AlertPresentable, ErrorPresentable, - DAppBrowserSearchPresentable { + DAppBrowserSearchPresentable, + DAppBrowserTabsClosePresentable { func showTab( _ tab: DAppBrowserTab, from view: ControllerBackedProtocol? From 88cb57d6ec2b08bfb8c361e0c2fba5f459c4dd41 Mon Sep 17 00:00:00 2001 From: svojsu Date: Wed, 8 Jan 2025 19:37:51 +0200 Subject: [PATCH 02/13] close tabs manager screen on close all --- .../DAppBrowserTabList/DAppBrowserTabListPresenter.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabList/DAppBrowserTabListPresenter.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabList/DAppBrowserTabListPresenter.swift index 85e375f80..597042be6 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabList/DAppBrowserTabListPresenter.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabList/DAppBrowserTabListPresenter.swift @@ -93,8 +93,12 @@ extension DAppBrowserTabListPresenter: DAppBrowserTabListPresenterProtocol { extension DAppBrowserTabListPresenter: DAppBrowserTabListInteractorOutputProtocol { func didReceiveTabs(_ models: [DAppBrowserTab]) { - tabs = models + if !tabs.isEmpty, models.isEmpty { + wireframe.close(from: view) + return + } + tabs = models provideTabs() } From 095ab95fe16c1acdb084537754f5c956256dea21 Mon Sep 17 00:00:00 2001 From: svojsu Date: Wed, 8 Jan 2025 19:58:39 +0200 Subject: [PATCH 03/13] don't filter categories with dApp search query --- .../DAppListViewModelFactory.swift | 3 - .../DAppSearch/DAppSearchViewController.swift | 2 +- .../DAppSearch/DAppSearchViewLayout.swift | 73 +------------------ 3 files changed, 3 insertions(+), 75 deletions(-) diff --git a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift index b9b5736aa..3f027b2a4 100644 --- a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift +++ b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift @@ -289,10 +289,7 @@ extension DAppListViewModelFactory: DAppListViewModelFactoryProtocol { return name.localizedCaseInsensitiveContains(query) } - let availableCategories = Set(dAppsByQuery.flatMap(\.dapp.categories)) - let categoryViewModels = dAppList.categories - .filter { availableCategories.contains($0.identifier) } .map { dappCategoriesViewModelFactory.createViewModel(for: $0) } let categoriesById = dAppList.categories.reduce(into: [String: DAppCategory]()) { result, category in diff --git a/novawallet/Modules/DApp/DAppSearch/DAppSearchViewController.swift b/novawallet/Modules/DApp/DAppSearch/DAppSearchViewController.swift index 6519eb13b..9d136ddcd 100644 --- a/novawallet/Modules/DApp/DAppSearch/DAppSearchViewController.swift +++ b/novawallet/Modules/DApp/DAppSearch/DAppSearchViewController.swift @@ -274,7 +274,7 @@ extension DAppSearchViewController: DAppSearchViewProtocol { func didReceive(viewModel: DAppListViewModel?) { self.viewModel = viewModel - rootView.updateCategoriesView(with: viewModel?.categories ?? []) + rootView.categoriesView.bind(categories: viewModel?.categories ?? []) rootView.categoriesView.setSelectedIndex( viewModel?.selectedCategoryIndex, diff --git a/novawallet/Modules/DApp/DAppSearch/DAppSearchViewLayout.swift b/novawallet/Modules/DApp/DAppSearch/DAppSearchViewLayout.swift index 989e4d2f8..03a322215 100644 --- a/novawallet/Modules/DApp/DAppSearch/DAppSearchViewLayout.swift +++ b/novawallet/Modules/DApp/DAppSearch/DAppSearchViewLayout.swift @@ -4,9 +4,7 @@ import SoraUI final class DAppSearchViewLayout: UIView { let searchBar = CustomSearchBar() - let categoriesView: DAppCategoriesView = .create { view in - view.alpha = 0.0 - } + let categoriesView = DAppCategoriesView() let topContainerView = UIView() @@ -29,22 +27,6 @@ final class DAppSearchViewLayout: UIView { return item }() - private let appearanceAnimator: ViewAnimatorProtocol = FadeAnimator( - from: 0.0, - to: 1.0, - duration: 0.15 - ) - private let disappearanceAnimator: ViewAnimatorProtocol = FadeAnimator( - from: 1.0, - to: 0.0, - duration: 0.15 - ) - private let blockAnimator: BlockViewAnimatorProtocol = BlockViewAnimator( - duration: 0.2, - delay: 0.0, - options: [.curveLinear] - ) - override init(frame: CGRect) { super.init(frame: frame) @@ -65,7 +47,7 @@ final class DAppSearchViewLayout: UIView { categoriesView.snp.makeConstraints { make in make.top.equalTo(safeAreaLayoutGuide) - make.height.equalTo(0.0) + make.height.equalTo(DAppCategoriesView.preferredHeight) make.leading.trailing.equalToSuperview() } @@ -79,55 +61,4 @@ final class DAppSearchViewLayout: UIView { make.leading.trailing.bottom.equalToSuperview() } } - - func updateCategoriesView(with viewModels: [DAppCategoryViewModel]) { - guard viewModels.isEmpty != categoriesView.viewModels.isEmpty else { - categoriesView.bind(categories: viewModels) - - return - } - - if viewModels.isEmpty { - hideCategoriesView { [weak self] in - self?.categoriesView.bind(categories: viewModels) - } - } else { - categoriesView.bind(categories: viewModels) - showCategoriesView() - } - } - - func hideCategoriesView(updateClosure: @escaping () -> Void) { - categoriesView.snp.updateConstraints { make in - make.height.equalTo(0.0) - } - - disappearanceAnimator.animate( - view: categoriesView, - completionBlock: nil - ) - - blockAnimator.animate( - block: { [weak self] in self?.layoutIfNeeded() }, - completionBlock: { _ in updateClosure() } - ) - } - - func showCategoriesView() { - categoriesView.snp.updateConstraints { make in - make.height.equalTo(DAppCategoriesView.preferredHeight) - } - - blockAnimator.animate( - block: { [weak self] in self?.layoutIfNeeded() }, - completionBlock: { [weak self] _ in - guard let self else { return } - - appearanceAnimator.animate( - view: categoriesView, - completionBlock: nil - ) - } - ) - } } From e8570d724de12ec930778abe41733cad0d256bda Mon Sep 17 00:00:00 2001 From: svojsu Date: Wed, 8 Jan 2025 21:01:56 +0200 Subject: [PATCH 04/13] query and selected category filtering logic for favorites --- .../DAppListViewModelFactory.swift | 32 +++++++++---------- .../DAppSearch/DAppSearchingByQuery.swift | 31 +++++++++++++++++- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift index 3f027b2a4..2c31de4c3 100644 --- a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift +++ b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift @@ -96,14 +96,13 @@ private extension DAppListViewModelFactory { merging dapps: [IndexedDApp], filteredFavorites: [String: DAppFavorite], allFavorites: [String: DAppFavorite], - categoriesDict: [String: DAppCategory] + categoriesDict: [String: DAppCategory], + selectedCategory: String? = nil ) -> [DAppViewModel] { let knownDApps: [String: DApp] = dapps.reduce(into: [:]) { acc, indexedDApp in acc[indexedDApp.dapp.identifier] = indexedDApp.dapp } - let knownIdentifiers = Set(knownDApps.keys) - let knownViewModels: [DAppViewModel] = dapps.map { indexedDapp in let favorite = allFavorites[indexedDapp.dapp.identifier] != nil return createDAppViewModel( @@ -114,14 +113,20 @@ private extension DAppListViewModelFactory { ) } - let filteredFavorites = filteredFavorites.values.filter { - !knownIdentifiers.contains($0.identifier) + let favoritePages: [DAppFavorite] + + if selectedCategory == nil { + favoritePages = filteredFavorites.values.filter { + knownDApps[$0.identifier] == nil + } + } else { + favoritePages = [] } let sortedKnownViewModels = sortedDAppViewModels(from: knownViewModels) let sortedFavoriteViewModels = createFavorites( - from: filteredFavorites, + from: favoritePages, knownDApps: knownDApps, categories: categoriesDict ) @@ -273,21 +278,13 @@ extension DAppListViewModelFactory: DAppListViewModelFactoryProtocol { favorites: [String: DAppFavorite] ) -> DAppListViewModel { let dAppsByQuery: [IndexedDApp] = search(by: query, in: dAppList) - let actualDApps: [IndexedDApp] = dAppsByQuery.filter { indexedDApp in guard let category else { return true } return indexedDApp.dapp.categories.contains(category) } - let filteredFavorites = favorites.filter { keyValue in - guard let query = query, !query.isEmpty else { - return true - } - - let name = createFavoriteDAppName(from: keyValue.value) - return name.localizedCaseInsensitiveContains(query) - } + let favoritesByQuery = search(by: query, in: favorites) let categoryViewModels = dAppList.categories .map { dappCategoriesViewModelFactory.createViewModel(for: $0) } @@ -298,9 +295,10 @@ extension DAppListViewModelFactory: DAppListViewModelFactoryProtocol { let dappViewModels = createViewModels( merging: actualDApps, - filteredFavorites: filteredFavorites, + filteredFavorites: favoritesByQuery, allFavorites: favorites, - categoriesDict: categoriesById + categoriesDict: categoriesById, + selectedCategory: category ) let selectedCategoryIndex = categoryViewModels.firstIndex(where: { $0.identifier == category }) diff --git a/novawallet/Modules/DApp/DAppSearch/DAppSearchingByQuery.swift b/novawallet/Modules/DApp/DAppSearch/DAppSearchingByQuery.swift index 4ef816e25..ba911c76d 100644 --- a/novawallet/Modules/DApp/DAppSearch/DAppSearchingByQuery.swift +++ b/novawallet/Modules/DApp/DAppSearch/DAppSearchingByQuery.swift @@ -5,6 +5,11 @@ protocol DAppSearchingByQuery { by query: String?, in dAppList: DAppList? ) -> [IndexedDApp] + + func search( + by query: String?, + in favorites: [String: DAppFavorite] + ) -> [String: DAppFavorite] } extension DAppSearchingByQuery { @@ -15,7 +20,7 @@ extension DAppSearchingByQuery { guard let dAppList else { return [] } return dAppList.dApps.enumerated().compactMap { valueIndex in - guard let query = query, !query.isEmpty else { + guard let query, !query.isEmpty else { return IndexedDApp(index: valueIndex.offset, dapp: valueIndex.element) } @@ -28,4 +33,28 @@ extension DAppSearchingByQuery { } } } + + func search( + by query: String?, + in favorites: [String: DAppFavorite] + ) -> [String: DAppFavorite] { + favorites.filter { dApp in + guard let query, !query.isEmpty else { + return true + } + + if + let name = dApp.value.label, + name.localizedCaseInsensitiveContains(query) { + return true + } else if + let queryURL = URL(string: query), + let dAppURL = URL(string: dApp.value.identifier), + dAppURL.host == queryURL.host { + return true + } else { + return false + } + } + } } From 76503498e3422012729cb26e4316927989f05002 Mon Sep 17 00:00:00 2001 From: svojsu Date: Sun, 12 Jan 2025 22:01:32 +0200 Subject: [PATCH 05/13] fix browser container layout to support both portrait and landscape --- .../DAppBrowser/DAppBrowserTabTransition.swift | 8 ++++++++ .../DAppBrowser/DAppBrowserViewController.swift | 12 +++++++----- .../NovaMainAppContainerViewController.swift | 14 +++++++------- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift index 315906f95..ea4b5d30f 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift @@ -11,6 +11,14 @@ enum DAppBrowserTabTransition { guard let tabId else { return } let options = UIViewController.Transition.ZoomOptions() + options.interactiveDismissShouldBegin = { _ in + guard let destinationController = destController as? DAppBrowserViewController else { + return true + } + + return destinationController.canBeDismissedInteractively() + } + options.alignmentRectProvider = { context in guard let destinationController = context.zoomedViewController as? DAppBrowserViewController else { return .zero diff --git a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserViewController.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserViewController.swift index e7a79f7ee..96bbaf6cd 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserViewController.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserViewController.swift @@ -36,7 +36,7 @@ final class DAppBrowserViewController: UIViewController, ViewHolder { } var isLandscape: Bool { - view.frame.size.width > view.frame.size.height + traitCollection.verticalSizeClass == .compact } init( @@ -103,6 +103,10 @@ final class DAppBrowserViewController: UIViewController, ViewHolder { presenter.setup() } + + func canBeDismissedInteractively() -> Bool { + !isLandscape + } } // MARK: Private @@ -557,13 +561,11 @@ extension DAppBrowserViewController: UIScrollViewDelegate { if isScrollingUp { hideBars() - - scrollYOffset = scrollView.contentOffset.y } else if isScrollingDown { showBars() - - scrollYOffset = scrollView.contentOffset.y } + + scrollYOffset = scrollView.contentOffset.y } } diff --git a/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift b/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift index b0fe306d7..46bce160a 100644 --- a/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift +++ b/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift @@ -56,13 +56,13 @@ private extension NovaMainAppContainerViewController { func browserCloseLayoutDependencies() -> DAppBrowserLayoutTransitionDependencies { let topContainerBottomOffset = Constants.topContainerBottomOffset(for: view) - let minimizedWidgetHeight = Constants.minimizedWidgetHeight(for: view) + let widgetTopConstraintInset = view.bounds.height return DAppBrowserLayoutTransitionDependencies( layoutClosure: { [weak self] in self?.browserWidget?.view.snp.updateConstraints { make in make.bottom.equalToSuperview().inset(-topContainerBottomOffset) - make.height.equalTo(minimizedWidgetHeight) + make.top.equalToSuperview().inset(widgetTopConstraintInset) } self?.topContainerBottomConstraint?.constant = 0 @@ -79,12 +79,13 @@ private extension NovaMainAppContainerViewController { func browserMinimizeLayoutDependencies() -> DAppBrowserLayoutTransitionDependencies { let topContainerBottomOffset = Constants.topContainerBottomOffset(for: view) let minimizedWidgetHeight = Constants.minimizedWidgetHeight(for: view) + let widgetTopConstraintInset = view.bounds.height - minimizedWidgetHeight return DAppBrowserLayoutTransitionDependencies( layoutClosure: { [weak self] in self?.browserWidget?.view.snp.updateConstraints { make in make.bottom.equalToSuperview() - make.height.equalTo(minimizedWidgetHeight) + make.top.equalToSuperview().inset(widgetTopConstraintInset) } self?.topContainerBottomConstraint?.constant = -topContainerBottomOffset @@ -101,13 +102,12 @@ private extension NovaMainAppContainerViewController { } func browserMaximizeLayoutDependencies() -> DAppBrowserLayoutTransitionDependencies { - let fullHeight = view.frame.size.height let topContainerBottomOffset = Constants.topContainerBottomOffset(for: view) return DAppBrowserLayoutTransitionDependencies( layoutClosure: { [weak self] in self?.browserWidget?.view.snp.updateConstraints { make in - make.height.equalTo(fullHeight) + make.top.equalToSuperview() make.bottom.equalToSuperview() } @@ -162,12 +162,12 @@ extension NovaMainAppContainerViewController { rootView.addSubview(bottomView) let topContainerBottomOffset = Constants.topContainerBottomOffset(for: view) - let minimizedWidgetHeight = Constants.minimizedWidgetHeight(for: view) + let widgetTopConstraintInset = view.bounds.height bottomView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.bottom.equalToSuperview().inset(-topContainerBottomOffset) - make.height.equalTo(minimizedWidgetHeight) + make.top.equalToSuperview().inset(widgetTopConstraintInset) } } } From d59e2cf6e466e19751574be590a833ab8f209168 Mon Sep 17 00:00:00 2001 From: svojsu Date: Mon, 13 Jan 2025 13:18:54 +0200 Subject: [PATCH 06/13] fix widget frame calculation and banner section for dapp list --- novawallet.xcodeproj/project.pbxproj | 8 +++ .../DAppListViewController+DataSource.swift | 21 ++++++ .../DAppList/DAppListViewController.swift | 1 + .../DApp/DAppList/DAppListViewLayout.swift | 32 ++++++++- .../DAppList/View/DAppListBannerView.swift | 71 +++++++++++++++++++ .../ViewModel/DAppListBannerViewModel.swift | 23 ++++++ .../DAppList/ViewModel/DAppListSection.swift | 7 ++ .../DAppListViewModelFactory.swift | 31 ++++++++ .../NovaMainAppContainerViewController.swift | 2 +- 9 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 novawallet/Modules/DApp/DAppList/View/DAppListBannerView.swift create mode 100644 novawallet/Modules/DApp/DAppList/ViewModel/DAppListBannerViewModel.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 1fb4c0cb2..533987d31 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -1086,6 +1086,8 @@ 2D1D66022CD82330009C6C2F /* KingfisherIconRetrieveOperationFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1D66012CD82330009C6C2F /* KingfisherIconRetrieveOperationFactory.swift */; }; 2D1D66042CD89573009C6C2F /* AssetListStyleSwitcherView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1D66032CD89573009C6C2F /* AssetListStyleSwitcherView.swift */; }; 2D1D66062CD92209009C6C2F /* QRDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1D66052CD92209009C6C2F /* QRDisplayView.swift */; }; + 2D20F1BE2D351936003E9CF2 /* DAppListBannerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D20F1BD2D351936003E9CF2 /* DAppListBannerViewModel.swift */; }; + 2D20F1C02D351B28003E9CF2 /* DAppListBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D20F1BF2D351B28003E9CF2 /* DAppListBannerView.swift */; }; 2D2F6F522C50E52D005020EF /* VotingCurveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D2F6F512C50E52D005020EF /* VotingCurveTests.swift */; }; 2D31D3062D149F74004BF46B /* ModalCardPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D31D3052D149F74004BF46B /* ModalCardPresentationController.swift */; }; 2D32BE122C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32BE052C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift */; }; @@ -6509,6 +6511,8 @@ 2D1D66012CD82330009C6C2F /* KingfisherIconRetrieveOperationFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KingfisherIconRetrieveOperationFactory.swift; sourceTree = ""; }; 2D1D66032CD89573009C6C2F /* AssetListStyleSwitcherView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetListStyleSwitcherView.swift; sourceTree = ""; }; 2D1D66052CD92209009C6C2F /* QRDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRDisplayView.swift; sourceTree = ""; }; + 2D20F1BD2D351936003E9CF2 /* DAppListBannerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAppListBannerViewModel.swift; sourceTree = ""; }; + 2D20F1BF2D351B28003E9CF2 /* DAppListBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAppListBannerView.swift; sourceTree = ""; }; 2D2F6F512C50E52D005020EF /* VotingCurveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingCurveTests.swift; sourceTree = ""; }; 2D31D3052D149F74004BF46B /* ModalCardPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalCardPresentationController.swift; sourceTree = ""; }; 2D32BE052C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtrinsicAssetConversionFeeEstimator.swift; sourceTree = ""; }; @@ -16221,6 +16225,7 @@ 842BA36A27B64F1000D31EEF /* DAppViewModel.swift */, 2D3EA7DF2CFEDC0F0033AFD2 /* DAppCategoryViewModelFactory.swift */, 2D4431E12D01130E00017951 /* DAppListSection.swift */, + 2D20F1BD2D351936003E9CF2 /* DAppListBannerViewModel.swift */, 2D4431EA2D036A0400017951 /* DAppListViewModelFactory */, ); path = ViewModel; @@ -21588,6 +21593,7 @@ children = ( 84EE780327C4CF9E0027357F /* DAppListItemsLoadingView.swift */, 84DA03DA275A31B500E8B326 /* DAppListHeaderView.swift */, + 2D20F1BF2D351B28003E9CF2 /* DAppListBannerView.swift */, 2D4431DC2D01094B00017951 /* DAppItemView */, 84720731277C370600F593DD /* DAppCategoriesView.swift */, 8448D5B7277DA4C200FAEEBC /* DAppListLoadingView.swift */, @@ -26134,6 +26140,7 @@ 0CE1A0522BE9E995008206ED /* CloudBackupSettingsView.swift in Sources */, 84E9A05028F000AB00551DC4 /* ReferendumMetadataLocal.swift in Sources */, 84821E84275F93C700ADC8D2 /* TitleMultiValueView+Style.swift in Sources */, + 2D20F1BE2D351936003E9CF2 /* DAppListBannerViewModel.swift in Sources */, 84DA03D12758AA6800E8B326 /* BaseAccountImportWireframe.swift in Sources */, 0C38B50E2B7DC42500882A8B /* KodaDotNftOperationFactory.swift in Sources */, 84E8BA1F29FFB38600FD9F40 /* EthereumOperationFactory.swift in Sources */, @@ -28037,6 +28044,7 @@ 844304622A28C9FA00DE36DE /* MultistakingSyncState.swift in Sources */, 0C626D1F2A92AA0F00CDAF4E /* NominationPoolsDataProviding.swift in Sources */, 8446F5EC2817107D00B7A86C /* TitleAmountView.swift in Sources */, + 2D20F1C02D351B28003E9CF2 /* DAppListBannerView.swift in Sources */, 84CEAAF726D7B8010021B881 /* SettingsMigrator.swift in Sources */, 84ABB32B2A16146600B5E95A /* HistoryItemTableViewCell.swift in Sources */, F4D0546B2729949100210294 /* MoonbeamMakeSignatureResponse.swift in Sources */, diff --git a/novawallet/Modules/DApp/DAppList/DAppListViewController+DataSource.swift b/novawallet/Modules/DApp/DAppList/DAppListViewController+DataSource.swift index 9937dc4fd..665224123 100644 --- a/novawallet/Modules/DApp/DAppList/DAppListViewController+DataSource.swift +++ b/novawallet/Modules/DApp/DAppList/DAppListViewController+DataSource.swift @@ -30,6 +30,12 @@ extension DAppListViewController { categoriess: models, indexPath: indexPath ) + case let .banner(model): + setupBannerView( + using: collectionView, + banner: model, + indexPath: indexPath + ) case let .favorites(model, _): setupDAppView( using: collectionView, @@ -119,6 +125,21 @@ private extension DAppListViewController { return cell } + func setupBannerView( + using collectionView: UICollectionView, + banner: DAppListBannerViewModel, + indexPath: IndexPath + ) -> UICollectionViewCell { + let cell: DAppListBannerView = collectionView.dequeueReusableCellWithType( + DAppListBannerView.self, + for: indexPath + )! + + cell.bind(viewModel: banner) + + return cell + } + func setupDAppView( using collectionView: UICollectionView, dApp: DAppViewModel, diff --git a/novawallet/Modules/DApp/DAppList/DAppListViewController.swift b/novawallet/Modules/DApp/DAppList/DAppListViewController.swift index e0c9687ff..efa4872e4 100644 --- a/novawallet/Modules/DApp/DAppList/DAppListViewController.swift +++ b/novawallet/Modules/DApp/DAppList/DAppListViewController.swift @@ -70,6 +70,7 @@ private extension DAppListViewController { func configureCollectionView() { rootView.collectionView.registerCellClass(DAppListHeaderView.self) rootView.collectionView.registerCellClass(DAppCategoriesViewCell.self) + rootView.collectionView.registerCellClass(DAppListBannerView.self) rootView.collectionView.registerCellClass(DAppListErrorView.self) rootView.collectionView.registerCellClass(DAppItemCollectionViewCell.self) rootView.collectionView.registerCellClass(DAppListLoadingView.self) diff --git a/novawallet/Modules/DApp/DAppList/DAppListViewLayout.swift b/novawallet/Modules/DApp/DAppList/DAppListViewLayout.swift index d887af6e4..75a1e788d 100644 --- a/novawallet/Modules/DApp/DAppList/DAppListViewLayout.swift +++ b/novawallet/Modules/DApp/DAppList/DAppListViewLayout.swift @@ -69,7 +69,10 @@ private extension DAppListViewLayout { fixedHeight: DAppCategoriesView.preferredHeight, scrollingBehavior: .none ) - contentInsets.bottom = 8 + contentInsets.bottom = 12 + case .banners: + section = bannersSectionLayout() + contentInsets.bottom = 24 case .favorites: section = dAppFavoritesSectionLayout() contentInsets.bottom = 24 @@ -103,11 +106,36 @@ private extension DAppListViewLayout { } } + func bannersSectionLayout() -> NSCollectionLayoutSection { + let item = NSCollectionLayoutItem( + layoutSize: NSCollectionLayoutSize( + widthDimension: .fractionalWidth(1), + heightDimension: .fractionalHeight(1) + ) + ) + let group = NSCollectionLayoutGroup.horizontal( + layoutSize: NSCollectionLayoutSize( + widthDimension: .fractionalWidth(0.915), + heightDimension: .absolute(120.0) + ), + subitem: item, + count: 1 + ) + + let section = NSCollectionLayoutSection(group: group) + section.orthogonalScrollingBehavior = .groupPaging + section.interGroupSpacing = UIConstants.horizontalInset / 2 + section.contentInsets = .zero + section.contentInsets.leading = UIConstants.horizontalInset + + return section + } + func dAppFavoritesSectionLayout() -> NSCollectionLayoutSection { let item = NSCollectionLayoutItem( layoutSize: NSCollectionLayoutSize( widthDimension: .fractionalWidth(1), - heightDimension: .fractionalWidth(1) + heightDimension: .fractionalHeight(1) ) ) let group = NSCollectionLayoutGroup.horizontal( diff --git a/novawallet/Modules/DApp/DAppList/View/DAppListBannerView.swift b/novawallet/Modules/DApp/DAppList/View/DAppListBannerView.swift new file mode 100644 index 000000000..a8546d3c1 --- /dev/null +++ b/novawallet/Modules/DApp/DAppList/View/DAppListBannerView.swift @@ -0,0 +1,71 @@ +import Foundation +import UIKit + +class DAppListBannerView: UICollectionViewCell { + let decorationView: UIImageView = .create { view in + view.contentMode = .scaleAspectFill + view.layer.cornerRadius = 12.0 + view.clipsToBounds = true + } + + let decorationTitleLabel: UILabel = .create { view in + view.textColor = R.color.colorTextPrimary() + view.font = .semiBoldTitle3 + view.textAlignment = .left + view.numberOfLines = 0 + } + + let decorationSubtitleLabel: UILabel = .create { view in + view.textColor = R.color.colorTextSecondary() + view.font = .caption1 + view.textAlignment = .left + view.numberOfLines = 0 + } + + override init(frame: CGRect) { + super.init(frame: frame) + + setupLayout() + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +// MARK: Private + +private extension DAppListBannerView { + func setupLayout() { + contentView.addSubview(decorationView) + decorationView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + + decorationView.addSubview(decorationTitleLabel) + decorationTitleLabel.snp.makeConstraints { make in + make.leading.equalToSuperview().inset(UIConstants.horizontalInset) + make.width.equalTo(200) + make.top.equalToSuperview().inset(12.0) + } + + decorationView.addSubview(decorationSubtitleLabel) + decorationSubtitleLabel.snp.makeConstraints { make in + make.leading.equalToSuperview().inset(UIConstants.horizontalInset) + make.width.equalTo(200) + make.top.equalTo(decorationTitleLabel.snp.bottom).offset(8.0) + make.bottom.equalToSuperview().inset(12.0) + } + } +} + +// MARK: Internal + +extension DAppListBannerView { + func bind(viewModel: DAppListBannerViewModel) { + decorationView.image = viewModel.imageViewModel.image + decorationTitleLabel.text = viewModel.title + decorationSubtitleLabel.text = viewModel.subtitle + } +} diff --git a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListBannerViewModel.swift b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListBannerViewModel.swift new file mode 100644 index 000000000..8c4784ec4 --- /dev/null +++ b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListBannerViewModel.swift @@ -0,0 +1,23 @@ +import Foundation + +struct DAppListBannerViewModel { + let title: String + let subtitle: String + + let imageViewModel: StaticImageViewModel +} + +// MARK: Hashable + +extension DAppListBannerViewModel: Hashable { + func hash(into hasher: inout Hasher) { + hasher.combine(title) + } + + static func == ( + lhs: DAppListBannerViewModel, + rhs: DAppListBannerViewModel + ) -> Bool { + lhs.title == rhs.title + } +} diff --git a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListSection.swift b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListSection.swift index e25a6631f..cb58cce07 100644 --- a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListSection.swift +++ b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListSection.swift @@ -8,6 +8,7 @@ enum DAppListViewState: Equatable { enum DAppListSectionViewModel: Equatable { case header(DAppListSection) case categorySelect(DAppListSection) + case banners(DAppListSection) case favorites(DAppListSection) case category(DAppListSection) case notLoaded(DAppListSection) @@ -19,6 +20,7 @@ enum DAppListSectionViewModel: Equatable { let .favorites(model), let .header(model), let .categorySelect(model), + let .banners(model), let .notLoaded(model), let .error(model): return model @@ -34,6 +36,7 @@ struct DAppListSection: Hashable, SectionProtocol { enum DAppListItem: Hashable { case header(WalletSwitchViewModel) case categorySelect([DAppCategoryViewModel]) + case banner(DAppListBannerViewModel) case favorites(model: DAppViewModel, categoryName: String) case category(model: DAppViewModel, categoryName: String) case notLoaded @@ -48,6 +51,8 @@ enum DAppListItem: Hashable { hasher.combine(model) case let .categorySelect(models): hasher.combine(models) + case let .banner(model): + hasher.combine(model) default: break } @@ -59,6 +64,8 @@ enum DAppListItem: Hashable { lhsModel == rhsModel && lhsCategoryName == rhsCategoryName case let (.favorites(lhsModel, lhsCategoryName), .favorites(rhsModel, rhsCategoryName)): lhsModel == rhsModel && lhsCategoryName == rhsCategoryName + case let (.banner(lhsModel), .banner(rhsModel)): + lhsModel == rhsModel case let (.header(lhsModel), .header(rhsModel)): lhsModel == rhsModel case let (.categorySelect(lhsModel), .categorySelect(rhsModel)): diff --git a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift index 2c31de4c3..19a606ad3 100644 --- a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift +++ b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift @@ -222,6 +222,30 @@ private extension DAppListViewModelFactory { ) } + func bannersSection( + from dAppList: DAppList, + locale: Locale + ) -> DAppListSection? { + guard !dAppList.dApps.isEmpty else { return nil } + + let title = R.string.localizable.dappDecorationTitle(preferredLanguages: locale.rLanguages) + let subtitle = R.string.localizable.dappsDecorationSubtitle(preferredLanguages: locale.rLanguages) + let image = R.image.imageDapps() + + let imageViewModel = StaticImageViewModel(image: image!) + + let bannerViewModel = DAppListBannerViewModel( + title: title, + subtitle: subtitle, + imageViewModel: imageViewModel + ) + + return DAppListSection( + title: nil, + cells: [.banner(bannerViewModel)] + ) + } + func headerSection( for wallet: MetaAccountModel, hasWalletsListUpdates: Bool @@ -350,6 +374,13 @@ extension DAppListViewModelFactory: DAppListViewModelFactoryProtocol { viewModels.append(.categorySelect(categorySelectSection)) } + if let bannersSection = bannersSection( + from: dAppList, + locale: locale + ) { + viewModels.append(.banners(bannersSection)) + } + if let favoritesSection = favoritesSection( from: favorites, dAppList: dAppList, diff --git a/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift b/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift index 46bce160a..29e8b1ad6 100644 --- a/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift +++ b/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift @@ -162,7 +162,7 @@ extension NovaMainAppContainerViewController { rootView.addSubview(bottomView) let topContainerBottomOffset = Constants.topContainerBottomOffset(for: view) - let widgetTopConstraintInset = view.bounds.height + let widgetTopConstraintInset = UIScreen.main.bounds.height bottomView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() From 19af0e9a74813e9ec544d8fbb16d8d3eb3e0020e Mon Sep 17 00:00:00 2001 From: svojsu Date: Mon, 13 Jan 2025 14:14:32 +0200 Subject: [PATCH 07/13] favicon fetch logic for dapps --- novawallet.xcodeproj/project.pbxproj | 4 ++ .../DAppFavoritesViewFactory.swift | 4 +- .../DApp/DAppList/DAppListViewFactory.swift | 5 +- .../DAppIconViewModelFactory.swift | 71 +++++++++++++++++++ .../DAppListViewModelFactory.swift | 24 +++---- .../DAppSearch/DAppSearchViewFactory.swift | 3 +- .../DApps/DAppList/DAppListTests.swift | 5 +- .../DApps/DAppSearch/DAppSearchTests.swift | 3 +- 8 files changed, 93 insertions(+), 26 deletions(-) create mode 100644 novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppIconViewModelFactory.swift diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 533987d31..a90267000 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -1088,6 +1088,7 @@ 2D1D66062CD92209009C6C2F /* QRDisplayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1D66052CD92209009C6C2F /* QRDisplayView.swift */; }; 2D20F1BE2D351936003E9CF2 /* DAppListBannerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D20F1BD2D351936003E9CF2 /* DAppListBannerViewModel.swift */; }; 2D20F1C02D351B28003E9CF2 /* DAppListBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D20F1BF2D351B28003E9CF2 /* DAppListBannerView.swift */; }; + 2D20F1C22D353495003E9CF2 /* DAppIconViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D20F1C12D353495003E9CF2 /* DAppIconViewModelFactory.swift */; }; 2D2F6F522C50E52D005020EF /* VotingCurveTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D2F6F512C50E52D005020EF /* VotingCurveTests.swift */; }; 2D31D3062D149F74004BF46B /* ModalCardPresentationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D31D3052D149F74004BF46B /* ModalCardPresentationController.swift */; }; 2D32BE122C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D32BE052C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift */; }; @@ -6513,6 +6514,7 @@ 2D1D66052CD92209009C6C2F /* QRDisplayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRDisplayView.swift; sourceTree = ""; }; 2D20F1BD2D351936003E9CF2 /* DAppListBannerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAppListBannerViewModel.swift; sourceTree = ""; }; 2D20F1BF2D351B28003E9CF2 /* DAppListBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAppListBannerView.swift; sourceTree = ""; }; + 2D20F1C12D353495003E9CF2 /* DAppIconViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DAppIconViewModelFactory.swift; sourceTree = ""; }; 2D2F6F512C50E52D005020EF /* VotingCurveTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VotingCurveTests.swift; sourceTree = ""; }; 2D31D3052D149F74004BF46B /* ModalCardPresentationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalCardPresentationController.swift; sourceTree = ""; }; 2D32BE052C6A49900047F520 /* ExtrinsicAssetConversionFeeEstimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExtrinsicAssetConversionFeeEstimator.swift; sourceTree = ""; }; @@ -13408,6 +13410,7 @@ children = ( 2D4431E82D0369EB00017951 /* DAppListViewModelFactoryProtocol.swift */, 842BA36C27B64F1000D31EEF /* DAppListViewModelFactory.swift */, + 2D20F1C12D353495003E9CF2 /* DAppIconViewModelFactory.swift */, ); path = DAppListViewModelFactory; sourceTree = ""; @@ -30101,6 +30104,7 @@ AD877F7F77F9CB862DC7D5B3 /* DelegationReferendumVotersWireframe.swift in Sources */, 0DD3DB85B0E7FD5692F58787 /* DelegationReferendumVotersPresenter.swift in Sources */, 8A2486E62C3915CB6D1FDED8 /* DelegationReferendumVotersInteractor.swift in Sources */, + 2D20F1C22D353495003E9CF2 /* DAppIconViewModelFactory.swift in Sources */, 77740BBE2AD4A7F500E8C06F /* SwapDetailsView.swift in Sources */, 0F073C8161B9852DEB1D40CD /* DelegationReferendumVotersViewController.swift in Sources */, D274117F06B12F955073D35B /* DelegationReferendumVotersViewLayout.swift in Sources */, diff --git a/novawallet/Modules/DApp/DAppFavorites/DAppFavoritesViewFactory.swift b/novawallet/Modules/DApp/DAppFavorites/DAppFavoritesViewFactory.swift index 691682d86..6b13ae573 100644 --- a/novawallet/Modules/DApp/DAppFavorites/DAppFavoritesViewFactory.swift +++ b/novawallet/Modules/DApp/DAppFavorites/DAppFavoritesViewFactory.swift @@ -28,9 +28,9 @@ struct DAppFavoritesViewFactory { let wireframe = DAppFavoritesWireframe() - let categoriesViewModelFactory = DAppCategoryViewModelFactory() let viewModelFactory = DAppListViewModelFactory( - dappCategoriesViewModelFactory: categoriesViewModelFactory + dappCategoriesViewModelFactory: DAppCategoryViewModelFactory(), + dappIconViewModelFactory: DAppIconViewModelFactory() ) let wallet: MetaAccountModel = SelectedWalletSettings.shared.value diff --git a/novawallet/Modules/DApp/DAppList/DAppListViewFactory.swift b/novawallet/Modules/DApp/DAppList/DAppListViewFactory.swift index 87419a3ff..9eed0ed95 100644 --- a/novawallet/Modules/DApp/DAppList/DAppListViewFactory.swift +++ b/novawallet/Modules/DApp/DAppList/DAppListViewFactory.swift @@ -45,10 +45,9 @@ struct DAppListViewFactory { let localizationManager = LocalizationManager.shared - let categoryViewModelFactory = DAppCategoryViewModelFactory() - let viewModelFactory = DAppListViewModelFactory( - dappCategoriesViewModelFactory: categoryViewModelFactory + dappCategoriesViewModelFactory: DAppCategoryViewModelFactory(), + dappIconViewModelFactory: DAppIconViewModelFactory() ) let presenter = DAppListPresenter( diff --git a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppIconViewModelFactory.swift b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppIconViewModelFactory.swift new file mode 100644 index 000000000..75eb347e4 --- /dev/null +++ b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppIconViewModelFactory.swift @@ -0,0 +1,71 @@ +import Foundation + +protocol DAppIconViewModelFactoryProtocol { + func createIconViewModel(for favorite: DAppFavorite) -> ImageViewModelProtocol + func createIconViewModel(for dApp: DApp) -> ImageViewModelProtocol +} + +class DAppIconViewModelFactory { + private let faviconAPIURLString: String = "https://icons.duckduckgo.com/ip3" + private let faviconExtension: String = "ico" +} + +// MARK: Private + +private extension DAppIconViewModelFactory { + func createFaviconURL(for pageURL: URL) -> URL? { + guard let domain = pageURL.host else { return nil } + + let resultURLString = [ + [ + faviconAPIURLString, + domain + ].joined(with: .slash), + + faviconExtension + ].joined(with: .dot) + + return URL(string: resultURLString) + } + + func createImageViewModel( + for iconURL: URL?, + dAppURL: URL + ) -> ImageViewModelProtocol { + if let iconURL { + RemoteImageViewModel(url: iconURL) + } else if let faviconURL = createFaviconURL(for: dAppURL) { + RemoteImageViewModel(url: faviconURL) + } else { + StaticImageViewModel(image: R.image.iconDefaultDapp()!) + } + } +} + +// MARK: DAppIconViewModelFactoryProtocol + +extension DAppIconViewModelFactory: DAppIconViewModelFactoryProtocol { + func createIconViewModel(for favorite: DAppFavorite) -> ImageViewModelProtocol { + guard let pageURL = URL(string: favorite.identifier) else { + return StaticImageViewModel(image: R.image.iconDefaultDapp()!) + } + + let iconURL: URL? = if let iconURLString = favorite.icon { + URL(string: iconURLString) + } else { + nil + } + + return createImageViewModel( + for: iconURL, + dAppURL: pageURL + ) + } + + func createIconViewModel(for dApp: DApp) -> ImageViewModelProtocol { + createImageViewModel( + for: dApp.icon, + dAppURL: dApp.url + ) + } +} diff --git a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift index 19a606ad3..2179a15bf 100644 --- a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift +++ b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift @@ -6,9 +6,14 @@ typealias IndexedDApp = (index: Int, dapp: DApp) final class DAppListViewModelFactory: DAppSearchingByQuery { private let dappCategoriesViewModelFactory: DAppCategoryViewModelFactoryProtocol private let walletSwitchViewModelFactory = WalletSwitchViewModelFactory() + private let dappIconViewModelFactory: DAppIconViewModelFactoryProtocol - init(dappCategoriesViewModelFactory: DAppCategoryViewModelFactoryProtocol) { + init( + dappCategoriesViewModelFactory: DAppCategoryViewModelFactoryProtocol, + dappIconViewModelFactory: DAppIconViewModelFactoryProtocol + ) { self.dappCategoriesViewModelFactory = dappCategoriesViewModelFactory + self.dappIconViewModelFactory = dappIconViewModelFactory } } @@ -21,13 +26,7 @@ private extension DAppListViewModelFactory { categories: [String: DAppCategory], favorite: Bool ) -> DAppViewModel { - let imageViewModel: ImageViewModelProtocol - - if let iconUrl = model.icon { - imageViewModel = RemoteImageViewModel(url: iconUrl) - } else { - imageViewModel = StaticImageViewModel(image: R.image.iconDefaultDapp()!) - } + let imageViewModel = dappIconViewModelFactory.createIconViewModel(for: model) let details = model.categories.map { categories[$0]?.name ?? $0 @@ -64,14 +63,7 @@ private extension DAppListViewModelFactory { knownDApps: [String: DApp], categoriesDict: [String: DAppCategory] ) -> DAppViewModel { - let imageViewModel: ImageViewModelProtocol - - if let icon = model.icon, let url = URL(string: icon) { - imageViewModel = RemoteImageViewModel(url: url) - } else { - imageViewModel = StaticImageViewModel(image: R.image.iconDefaultDapp()!) - } - + let imageViewModel = dappIconViewModelFactory.createIconViewModel(for: model) let name = createFavoriteDAppName(from: model) let details = if let knownDApp = knownDApps[model.identifier] { diff --git a/novawallet/Modules/DApp/DAppSearch/DAppSearchViewFactory.swift b/novawallet/Modules/DApp/DAppSearch/DAppSearchViewFactory.swift index 3ab455758..e2e61b1ed 100644 --- a/novawallet/Modules/DApp/DAppSearch/DAppSearchViewFactory.swift +++ b/novawallet/Modules/DApp/DAppSearch/DAppSearchViewFactory.swift @@ -21,7 +21,8 @@ struct DAppSearchViewFactory { let wireframe = DAppSearchWireframe() let viewModelFactory = DAppListViewModelFactory( - dappCategoriesViewModelFactory: DAppCategoryViewModelFactory() + dappCategoriesViewModelFactory: DAppCategoryViewModelFactory(), + dappIconViewModelFactory: DAppIconViewModelFactory() ) let presenter = DAppSearchPresenter( diff --git a/novawalletTests/Modules/DApps/DAppList/DAppListTests.swift b/novawalletTests/Modules/DApps/DAppList/DAppListTests.swift index b7678eb19..97bfda5bd 100644 --- a/novawalletTests/Modules/DApps/DAppList/DAppListTests.swift +++ b/novawalletTests/Modules/DApps/DAppList/DAppListTests.swift @@ -70,10 +70,9 @@ class DAppListTests: XCTestCase { logger: Logger.shared ) - let dAppCategoryViewModelFactory = DAppCategoryViewModelFactory() - let viewModelFactory = DAppListViewModelFactory( - dappCategoriesViewModelFactory: dAppCategoryViewModelFactory + dappCategoriesViewModelFactory: DAppCategoryViewModelFactory(), + dappIconViewModelFactory: DAppIconViewModelFactory() ) let presenter = DAppListPresenter( diff --git a/novawalletTests/Modules/DApps/DAppSearch/DAppSearchTests.swift b/novawalletTests/Modules/DApps/DAppSearch/DAppSearchTests.swift index b3041d789..1205013af 100644 --- a/novawalletTests/Modules/DApps/DAppSearch/DAppSearchTests.swift +++ b/novawalletTests/Modules/DApps/DAppSearch/DAppSearchTests.swift @@ -29,7 +29,8 @@ class DAppSearchTests: XCTestCase { ) let viewModelFactory = DAppListViewModelFactory( - dappCategoriesViewModelFactory: DAppCategoryViewModelFactory() + dappCategoriesViewModelFactory: DAppCategoryViewModelFactory(), + dappIconViewModelFactory: DAppIconViewModelFactory() ) let presenter = DAppSearchPresenter( From 58930737c11158b76272645e518a24fd1ae25dff Mon Sep 17 00:00:00 2001 From: svojsu Date: Mon, 13 Jan 2025 14:38:24 +0200 Subject: [PATCH 08/13] fix dapp matching when adding to favorites --- .../DAppAddFavorite/DAppAddFavoriteInteractor.swift | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/novawallet/Modules/DApp/DAppAddFavorite/DAppAddFavoriteInteractor.swift b/novawallet/Modules/DApp/DAppAddFavorite/DAppAddFavoriteInteractor.swift index 114f09e3e..3e8945c33 100644 --- a/novawallet/Modules/DApp/DAppAddFavorite/DAppAddFavoriteInteractor.swift +++ b/novawallet/Modules/DApp/DAppAddFavorite/DAppAddFavoriteInteractor.swift @@ -48,7 +48,17 @@ final class DAppAddFavoriteInteractor { if dApps.count == 1, let dApp = dApps.first { provideProposedModelWithMatchedDApp(dApp) } else { - provideProposedModelWithMatchedDApp(nil) + let path = browserPage.url + .pathComponents + .first { $0 != "/" } + + let dApp = dApps.first { + $0.url + .pathComponents + .first { $0 != "/" } == path + } + + provideProposedModelWithMatchedDApp(dApp) } } From 999ac8c95e666ae2d67e1dc28adea2125d7006e6 Mon Sep 17 00:00:00 2001 From: svojsu Date: Mon, 13 Jan 2025 15:21:19 +0200 Subject: [PATCH 09/13] disable landscape on browser close --- .../Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift | 4 +++- .../DApp/DAppBrowser/DAppBrowserTabTransition.swift | 8 +++++++- .../DApp/DAppBrowser/DAppBrowserViewController.swift | 7 +++++++ .../NovaMainAppContainerViewController.swift | 9 ++++++++- 4 files changed, 25 insertions(+), 3 deletions(-) diff --git a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift index f65bf3054..f4fb3169e 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift @@ -156,11 +156,12 @@ extension DAppBrowserPresenter: DAppBrowserPresenterProtocol { func close(stateRender: DAppBrowserTabRenderProtocol) { interactor.process(stateRender: stateRender) - + view?.didDecideClose() wireframe.close(view: view) } func showTabs(stateRender: DAppBrowserTabRenderProtocol) { + view?.didDecideClose() interactor.saveLastTabState(render: stateRender) } @@ -270,6 +271,7 @@ extension DAppBrowserPresenter: DAppAuthDelegate { extension DAppBrowserPresenter: DAppPhishingViewDelegate { func dappPhishingViewDidHide() { + view?.didDecideClose() wireframe.close(view: view) } } diff --git a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift index ea4b5d30f..bd63dbb5a 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift @@ -16,7 +16,13 @@ enum DAppBrowserTabTransition { return true } - return destinationController.canBeDismissedInteractively() + let canBeDismissedInteractively: Bool = destinationController.canBeDismissedInteractively() + + if canBeDismissedInteractively { + destinationController.willBeDismissedInteractively() + } + + return canBeDismissedInteractively } options.alignmentRectProvider = { context in diff --git a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserViewController.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserViewController.swift index 96bbaf6cd..e02d0bdf5 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserViewController.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserViewController.swift @@ -107,6 +107,13 @@ final class DAppBrowserViewController: UIViewController, ViewHolder { func canBeDismissedInteractively() -> Bool { !isLandscape } + + func willBeDismissedInteractively() { + if #available(iOS 16.0, *) { + deviceOrientationManager.disableLandscape() + setNeedsUpdateOfSupportedInterfaceOrientations() + } + } } // MARK: Private diff --git a/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift b/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift index 29e8b1ad6..edb60555b 100644 --- a/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift +++ b/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift @@ -79,7 +79,14 @@ private extension NovaMainAppContainerViewController { func browserMinimizeLayoutDependencies() -> DAppBrowserLayoutTransitionDependencies { let topContainerBottomOffset = Constants.topContainerBottomOffset(for: view) let minimizedWidgetHeight = Constants.minimizedWidgetHeight(for: view) - let widgetTopConstraintInset = view.bounds.height - minimizedWidgetHeight + + // We do this in cases when minimizing started before the device orientation + // finished transitioning from landscape to portrait + let widgetTopConstraintInset = if view.bounds.height > view.bounds.width { + view.bounds.height - minimizedWidgetHeight + } else { + view.bounds.width - minimizedWidgetHeight + } return DAppBrowserLayoutTransitionDependencies( layoutClosure: { [weak self] in From 8d6ccc57a72960fa75559ccb8029bdf161922b42 Mon Sep 17 00:00:00 2001 From: svojsu Date: Mon, 13 Jan 2025 16:46:42 +0200 Subject: [PATCH 10/13] move makeTransition method to private extension --- .../NovaMainAppContainerViewController.swift | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift b/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift index edb60555b..681ffbe67 100644 --- a/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift +++ b/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift @@ -134,6 +134,30 @@ private extension NovaMainAppContainerViewController { presentedViewController = presentedViewController?.presentedViewController } } + + func makeTransition( + for state: DAppBrowserWidgetState, + using transitionBuilder: DAppBrowserWidgetTransitionBuilder? + ) { + let builder = if let transitionBuilder { + transitionBuilder + } else { + DAppBrowserWidgetTransitionBuilder() + } + + let widgetLayout = DAppBrowserWidgetLayout(from: state) + let transitionDependencies = createTransitionLayoutDependencies(for: widgetLayout) + + builder.setWidgetLayout(transitionDependencies) + + do { + let transition = try builder.build(for: widgetLayout) + + transition.start() + } catch { + logger.error("Failed to build transition: \(error)") + } + } } // MARK: Internal @@ -191,30 +215,6 @@ extension NovaMainAppContainerViewController: DAppBrowserWidgetParentControllerP using: transitionBuilder ) } - - func makeTransition( - for state: DAppBrowserWidgetState, - using transitionBuilder: DAppBrowserWidgetTransitionBuilder? - ) { - let builder = if let transitionBuilder { - transitionBuilder - } else { - DAppBrowserWidgetTransitionBuilder() - } - - let widgetLayout = DAppBrowserWidgetLayout(from: state) - let transitionDependencies = createTransitionLayoutDependencies(for: widgetLayout) - - builder.setWidgetLayout(transitionDependencies) - - do { - let transition = try builder.build(for: widgetLayout) - - transition.start() - } catch { - logger.error("Failed to build transition: \(error)") - } - } } // MARK: NovaMainAppContainerViewProtocol From 17d6aa089dbd6808dedd910f27ff20177b1a7e81 Mon Sep 17 00:00:00 2001 From: svojsu Date: Mon, 13 Jan 2025 17:45:22 +0200 Subject: [PATCH 11/13] add dapp icon view model factory for dapp auth request flow --- .../DAppAddFavoritePresenter.swift | 12 ++--- .../DAppAddFavoriteViewFactory.swift | 3 +- .../DAppAuthConfirmViewFactory.swift | 6 ++- .../ViewModel/DAppAuthViewModelFactory.swift | 14 ++--- .../DAppIconViewModelFactory.swift | 53 +++++++++++++++---- 5 files changed, 61 insertions(+), 27 deletions(-) diff --git a/novawallet/Modules/DApp/DAppAddFavorite/DAppAddFavoritePresenter.swift b/novawallet/Modules/DApp/DAppAddFavorite/DAppAddFavoritePresenter.swift index e8101f9c3..6a11bd534 100644 --- a/novawallet/Modules/DApp/DAppAddFavorite/DAppAddFavoritePresenter.swift +++ b/novawallet/Modules/DApp/DAppAddFavorite/DAppAddFavoritePresenter.swift @@ -5,6 +5,7 @@ final class DAppAddFavoritePresenter { weak var view: DAppAddFavoriteViewProtocol? let wireframe: DAppAddFavoriteWireframeProtocol let interactor: DAppAddFavoriteInteractorInputProtocol + let iconViewModelFactory: DAppIconViewModelFactoryProtocol let localizationManager: LocalizationManagerProtocol private(set) var titleViewModel: InputViewModelProtocol? @@ -15,10 +16,12 @@ final class DAppAddFavoritePresenter { init( interactor: DAppAddFavoriteInteractorInputProtocol, wireframe: DAppAddFavoriteWireframeProtocol, + iconViewModelFactory: DAppIconViewModelFactoryProtocol, localizationManager: LocalizationManagerProtocol ) { self.interactor = interactor self.wireframe = wireframe + self.iconViewModelFactory = iconViewModelFactory self.localizationManager = localizationManager } @@ -27,14 +30,7 @@ final class DAppAddFavoritePresenter { return } - let iconViewModel: ImageViewModelProtocol - - if let icon = proposedModel.icon, let url = URL(string: icon) { - iconViewModel = RemoteImageViewModel(url: url) - } else { - let defaultIcon = R.image.iconDefaultDapp()! - iconViewModel = StaticImageViewModel(image: defaultIcon) - } + let iconViewModel = iconViewModelFactory.createIconViewModel(for: proposedModel) view?.didReceive(iconViewModel: iconViewModel) diff --git a/novawallet/Modules/DApp/DAppAddFavorite/DAppAddFavoriteViewFactory.swift b/novawallet/Modules/DApp/DAppAddFavorite/DAppAddFavoriteViewFactory.swift index c164a62c6..4f6dcf86f 100644 --- a/novawallet/Modules/DApp/DAppAddFavorite/DAppAddFavoriteViewFactory.swift +++ b/novawallet/Modules/DApp/DAppAddFavorite/DAppAddFavoriteViewFactory.swift @@ -20,12 +20,13 @@ struct DAppAddFavoriteViewFactory { ) let wireframe = DAppAddFavoriteWireframe() - let localizationManager = LocalizationManager.shared + let iconViewModelFactory = DAppIconViewModelFactory() let presenter = DAppAddFavoritePresenter( interactor: interactor, wireframe: wireframe, + iconViewModelFactory: iconViewModelFactory, localizationManager: localizationManager ) diff --git a/novawallet/Modules/DApp/DAppAuthConfirm/DAppAuthConfirmViewFactory.swift b/novawallet/Modules/DApp/DAppAuthConfirm/DAppAuthConfirmViewFactory.swift index 7a23de3c2..90e7abadd 100644 --- a/novawallet/Modules/DApp/DAppAuthConfirm/DAppAuthConfirmViewFactory.swift +++ b/novawallet/Modules/DApp/DAppAuthConfirm/DAppAuthConfirmViewFactory.swift @@ -10,11 +10,15 @@ struct DAppAuthConfirmViewFactory { let localizationManager = LocalizationManager.shared + let viewModelFactory = DAppAuthViewModelFactory( + iconViewModelFactory: DAppIconViewModelFactory() + ) + let presenter = DAppAuthConfirmPresenter( wireframe: wireframe, request: request, delegate: delegate, - viewModelFactory: DAppAuthViewModelFactory() + viewModelFactory: viewModelFactory ) let view = DAppAuthConfirmViewController( diff --git a/novawallet/Modules/DApp/DAppAuthConfirm/ViewModel/DAppAuthViewModelFactory.swift b/novawallet/Modules/DApp/DAppAuthConfirm/ViewModel/DAppAuthViewModelFactory.swift index db802c3b4..e89dc0c35 100644 --- a/novawallet/Modules/DApp/DAppAuthConfirm/ViewModel/DAppAuthViewModelFactory.swift +++ b/novawallet/Modules/DApp/DAppAuthConfirm/ViewModel/DAppAuthViewModelFactory.swift @@ -6,16 +6,16 @@ protocol DAppAuthViewModelFactoryProtocol { } final class DAppAuthViewModelFactory: DAppAuthViewModelFactoryProtocol { + private let iconViewModelFactory: DAppIconViewModelFactoryProtocol + + init(iconViewModelFactory: DAppIconViewModelFactoryProtocol) { + self.iconViewModelFactory = iconViewModelFactory + } + func createViewModel(from request: DAppAuthRequest) -> DAppAuthViewModel { let sourceViewModel = StaticImageViewModel(image: R.image.iconDappExtension()!) - let destinationViewModel: ImageViewModelProtocol - - if let iconUrl = request.dAppIcon { - destinationViewModel = RemoteImageViewModel(url: iconUrl) - } else { - destinationViewModel = StaticImageViewModel(image: R.image.iconDefaultDapp()!) - } + let destinationViewModel = iconViewModelFactory.createIconViewModel(for: request) let iconGenerator = NovaIconGenerator() diff --git a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppIconViewModelFactory.swift b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppIconViewModelFactory.swift index 75eb347e4..eb93150a4 100644 --- a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppIconViewModelFactory.swift +++ b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppIconViewModelFactory.swift @@ -3,27 +3,41 @@ import Foundation protocol DAppIconViewModelFactoryProtocol { func createIconViewModel(for favorite: DAppFavorite) -> ImageViewModelProtocol func createIconViewModel(for dApp: DApp) -> ImageViewModelProtocol + func createIconViewModel(for dAppAuthRequest: DAppAuthRequest) -> ImageViewModelProtocol } class DAppIconViewModelFactory { - private let faviconAPIURLString: String = "https://icons.duckduckgo.com/ip3" - private let faviconExtension: String = "ico" + private let faviconAPIFormat: String + + init(faviconAPIFormat: String = Constants.ddgFaviconAPIFormat) { + self.faviconAPIFormat = faviconAPIFormat + } } // MARK: Private private extension DAppIconViewModelFactory { func createFaviconURL(for pageURL: URL) -> URL? { - guard let domain = pageURL.host else { return nil } + let regex = try? NSRegularExpression(pattern: "(?:https?://)?([^/\\?]+)") + let urlString = pageURL.absoluteString - let resultURLString = [ - [ - faviconAPIURLString, - domain - ].joined(with: .slash), + guard + let match = regex?.firstMatch( + in: urlString, + range: NSRange(urlString.startIndex..., in: urlString) + ), + let range = Range( + match.range(at: 1), + in: urlString + ) + else { return nil } - faviconExtension - ].joined(with: .dot) + let domain = urlString[range] + + let resultURLString = String( + format: faviconAPIFormat, + String(domain) + ) return URL(string: resultURLString) } @@ -68,4 +82,23 @@ extension DAppIconViewModelFactory: DAppIconViewModelFactoryProtocol { dAppURL: dApp.url ) } + + func createIconViewModel(for dAppAuthRequest: DAppAuthRequest) -> ImageViewModelProtocol { + guard let dAppURL = URL(string: dAppAuthRequest.dApp) else { + return StaticImageViewModel(image: R.image.iconDefaultDapp()!) + } + + return createImageViewModel( + for: dAppAuthRequest.dAppIcon, + dAppURL: dAppURL + ) + } +} + +// MARK: Constants + +private extension DAppIconViewModelFactory { + enum Constants { + static let ddgFaviconAPIFormat: String = "https://icons.duckduckgo.com/ip3/%@.ico" + } } From 15c8f57474abd40e5ef1df163e101b54f6860bb6 Mon Sep 17 00:00:00 2001 From: svojsu Date: Mon, 13 Jan 2025 18:37:59 +0200 Subject: [PATCH 12/13] fix tests --- novawalletTests/Mocks/ModuleMocks.swift | 18 +++++++++--------- .../DAppAuthConfirm/DAppAuthConfirmTests.swift | 6 +++++- .../DApps/DAppBrowser/DAppBrowserTests.swift | 5 ++++- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/novawalletTests/Mocks/ModuleMocks.swift b/novawalletTests/Mocks/ModuleMocks.swift index 30a8b8651..05fd25eb8 100644 --- a/novawalletTests/Mocks/ModuleMocks.swift +++ b/novawalletTests/Mocks/ModuleMocks.swift @@ -4332,9 +4332,9 @@ import Operation_iOS - func didReceiveTabsCount(viewModel: String) { + func didReceiveTabsCount(viewModel: DAppBrowserTabsButtonViewModel) { - return cuckoo_manager.call("didReceiveTabsCount(viewModel: String)", + return cuckoo_manager.call("didReceiveTabsCount(viewModel: DAppBrowserTabsButtonViewModel)", parameters: (viewModel), escapingParameters: (viewModel), superclassCall: @@ -4489,9 +4489,9 @@ import Operation_iOS return .init(stub: cuckoo_manager.createStub(for: MockDAppBrowserViewProtocol.self, method: "didReceive(viewModel: DAppBrowserModel)", parameterMatchers: matchers)) } - func didReceiveTabsCount(viewModel: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(String)> where M1.MatchedType == String { - let matchers: [Cuckoo.ParameterMatcher<(String)>] = [wrap(matchable: viewModel) { $0 }] - return .init(stub: cuckoo_manager.createStub(for: MockDAppBrowserViewProtocol.self, method: "didReceiveTabsCount(viewModel: String)", parameterMatchers: matchers)) + func didReceiveTabsCount(viewModel: M1) -> Cuckoo.ProtocolStubNoReturnFunction<(DAppBrowserTabsButtonViewModel)> where M1.MatchedType == DAppBrowserTabsButtonViewModel { + let matchers: [Cuckoo.ParameterMatcher<(DAppBrowserTabsButtonViewModel)>] = [wrap(matchable: viewModel) { $0 }] + return .init(stub: cuckoo_manager.createStub(for: MockDAppBrowserViewProtocol.self, method: "didReceiveTabsCount(viewModel: DAppBrowserTabsButtonViewModel)", parameterMatchers: matchers)) } func didReceive(response: M1, forTransport name: M2) -> Cuckoo.ProtocolStubNoReturnFunction<(DAppScriptResponse, String)> where M1.MatchedType == DAppScriptResponse, M2.MatchedType == String { @@ -4567,9 +4567,9 @@ import Operation_iOS } @discardableResult - func didReceiveTabsCount(viewModel: M1) -> Cuckoo.__DoNotUse<(String), Void> where M1.MatchedType == String { - let matchers: [Cuckoo.ParameterMatcher<(String)>] = [wrap(matchable: viewModel) { $0 }] - return cuckoo_manager.verify("didReceiveTabsCount(viewModel: String)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) + func didReceiveTabsCount(viewModel: M1) -> Cuckoo.__DoNotUse<(DAppBrowserTabsButtonViewModel), Void> where M1.MatchedType == DAppBrowserTabsButtonViewModel { + let matchers: [Cuckoo.ParameterMatcher<(DAppBrowserTabsButtonViewModel)>] = [wrap(matchable: viewModel) { $0 }] + return cuckoo_manager.verify("didReceiveTabsCount(viewModel: DAppBrowserTabsButtonViewModel)", callMatcher: callMatcher, parameterMatchers: matchers, sourceLocation: sourceLocation) } @discardableResult @@ -4655,7 +4655,7 @@ import Operation_iOS - func didReceiveTabsCount(viewModel: String) { + func didReceiveTabsCount(viewModel: DAppBrowserTabsButtonViewModel) { return DefaultValueRegistry.defaultValue(for: (Void).self) } diff --git a/novawalletTests/Modules/DApps/DAppAuthConfirm/DAppAuthConfirmTests.swift b/novawalletTests/Modules/DApps/DAppAuthConfirm/DAppAuthConfirmTests.swift index 5bf07eaac..dd48141af 100644 --- a/novawalletTests/Modules/DApps/DAppAuthConfirm/DAppAuthConfirmTests.swift +++ b/novawalletTests/Modules/DApps/DAppAuthConfirm/DAppAuthConfirmTests.swift @@ -114,12 +114,16 @@ class DAppAuthConfirmTests: XCTestCase { requiredChains: .init(), optionalChains: nil ) + + let viewModelFactory = DAppAuthViewModelFactory( + iconViewModelFactory: DAppIconViewModelFactory() + ) let presenter = DAppAuthConfirmPresenter( wireframe: wireframe, request: request, delegate: delegate, - viewModelFactory: DAppAuthViewModelFactory() + viewModelFactory: viewModelFactory ) presenter.view = view diff --git a/novawalletTests/Modules/DApps/DAppBrowser/DAppBrowserTests.swift b/novawalletTests/Modules/DApps/DAppBrowser/DAppBrowserTests.swift index 97f86bda8..990e5454b 100644 --- a/novawalletTests/Modules/DApps/DAppBrowser/DAppBrowserTests.swift +++ b/novawalletTests/Modules/DApps/DAppBrowser/DAppBrowserTests.swift @@ -66,7 +66,10 @@ class DAppBrowserTests: XCTestCase { let tabManager = DAppBrowserTabManager.shared - let tab = DAppBrowserTab(from: dAppURL)! + let tab = DAppBrowserTab( + from: dAppURL, + metaId: walletSettings.value.metaId + )! let interactor = DAppBrowserInteractor( transports: [transport], From 6af6608ee7d43f678f4d1e7c2dc34e3b93a6c721 Mon Sep 17 00:00:00 2001 From: svojsu Date: Tue, 14 Jan 2025 13:21:13 +0200 Subject: [PATCH 13/13] review fixes --- .../DAppBrowser/DAppBrowserPresenter.swift | 5 ++ .../DAppBrowser/DAppBrowserProtocols.swift | 1 + .../DAppBrowserTabTransition.swift | 23 ++++++++-- .../DAppBrowserViewController.swift | 8 ++-- .../DAppList/View/DAppListBannerView.swift | 7 ++- .../ViewModel/DAppListBannerViewModel.swift | 2 +- .../NovaMainAppContainerViewController.swift | 46 ++++++++----------- 7 files changed, 56 insertions(+), 36 deletions(-) diff --git a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift index f4fb3169e..633a36c49 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift @@ -165,6 +165,11 @@ extension DAppBrowserPresenter: DAppBrowserPresenterProtocol { interactor.saveLastTabState(render: stateRender) } + func willDismissInteractive(stateRender: DAppBrowserTabRenderProtocol) { + view?.didDecideClose() + interactor.saveLastTabState(render: stateRender) + } + func didLoadPage() { interactor.saveTabIfNeeded() } diff --git a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserProtocols.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserProtocols.swift index b26f83927..cec7b83f8 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserProtocols.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserProtocols.swift @@ -41,6 +41,7 @@ protocol DAppBrowserPresenterProtocol: AnyObject { func showSettings(using isDesktop: Bool) func close(stateRender: DAppBrowserTabRenderProtocol) func showTabs(stateRender: DAppBrowserTabRenderProtocol) + func willDismissInteractive(stateRender: DAppBrowserTabRenderProtocol) } protocol DAppBrowserInteractorInputProtocol: AnyObject { diff --git a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift index bd63dbb5a..b94a9a4be 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift @@ -11,18 +11,24 @@ enum DAppBrowserTabTransition { guard let tabId else { return } let options = UIViewController.Transition.ZoomOptions() - options.interactiveDismissShouldBegin = { _ in + options.interactiveDismissShouldBegin = { context in guard let destinationController = destController as? DAppBrowserViewController else { return true } + let shouldDismiss = atViewEdge( + context.location, + view: destinationController.view + ) + let canBeDismissedInteractively: Bool = destinationController.canBeDismissedInteractively() + let willDismiss = shouldDismiss && canBeDismissedInteractively - if canBeDismissedInteractively { + if willDismiss { destinationController.willBeDismissedInteractively() } - return canBeDismissedInteractively + return willDismiss } options.alignmentRectProvider = { context in @@ -59,4 +65,15 @@ enum DAppBrowserTabTransition { false } } + + static func atViewEdge( + _ point: CGPoint, + view: UIView, + edgeWidth: CGFloat = 20 + ) -> Bool { + let atLeftEdge = point.x <= edgeWidth + let atRightEdge = point.x >= view.bounds.width - edgeWidth + + return atLeftEdge || atRightEdge + } } diff --git a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserViewController.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserViewController.swift index e02d0bdf5..163570433 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserViewController.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserViewController.swift @@ -109,9 +109,9 @@ final class DAppBrowserViewController: UIViewController, ViewHolder { } func willBeDismissedInteractively() { - if #available(iOS 16.0, *) { - deviceOrientationManager.disableLandscape() - setNeedsUpdateOfSupportedInterfaceOrientations() + snapshotWebView { [weak self] image in + let render = DAppBrowserTabRender(for: image) + self?.presenter.willDismissInteractive(stateRender: render) } } } @@ -186,8 +186,6 @@ private extension DAppBrowserViewController { } func makeStateRender() { - guard let webView = rootView.webView else { return } - snapshotWebView { [weak self] image in let render = DAppBrowserTabRender(for: image) self?.presenter.process(stateRender: render) diff --git a/novawallet/Modules/DApp/DAppList/View/DAppListBannerView.swift b/novawallet/Modules/DApp/DAppList/View/DAppListBannerView.swift index a8546d3c1..31a47113c 100644 --- a/novawallet/Modules/DApp/DAppList/View/DAppListBannerView.swift +++ b/novawallet/Modules/DApp/DAppList/View/DAppListBannerView.swift @@ -64,7 +64,12 @@ private extension DAppListBannerView { extension DAppListBannerView { func bind(viewModel: DAppListBannerViewModel) { - decorationView.image = viewModel.imageViewModel.image + viewModel.imageViewModel.loadImage( + on: decorationView, + targetSize: decorationView.bounds.size, + animated: true + ) + decorationTitleLabel.text = viewModel.title decorationSubtitleLabel.text = viewModel.subtitle } diff --git a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListBannerViewModel.swift b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListBannerViewModel.swift index 8c4784ec4..34255d19a 100644 --- a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListBannerViewModel.swift +++ b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListBannerViewModel.swift @@ -4,7 +4,7 @@ struct DAppListBannerViewModel { let title: String let subtitle: String - let imageViewModel: StaticImageViewModel + let imageViewModel: ImageViewModelProtocol } // MARK: Hashable diff --git a/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift b/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift index 681ffbe67..1d273d3e1 100644 --- a/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift +++ b/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift @@ -55,19 +55,21 @@ private extension NovaMainAppContainerViewController { } func browserCloseLayoutDependencies() -> DAppBrowserLayoutTransitionDependencies { - let topContainerBottomOffset = Constants.topContainerBottomOffset(for: view) - let widgetTopConstraintInset = view.bounds.height + let minimizedWidgetHeight = Constants.minimizedWidgetHeight(for: view) return DAppBrowserLayoutTransitionDependencies( layoutClosure: { [weak self] in - self?.browserWidget?.view.snp.updateConstraints { make in - make.bottom.equalToSuperview().inset(-topContainerBottomOffset) - make.top.equalToSuperview().inset(widgetTopConstraintInset) + guard let self else { return nil } + + browserWidget?.view.snp.remakeConstraints { make in + make.bottom.equalToSuperview().inset(-minimizedWidgetHeight) + make.top.equalTo(self.rootView.snp.bottom) + make.leading.trailing.equalToSuperview() } - self?.topContainerBottomConstraint?.constant = 0 + topContainerBottomConstraint?.constant = 0 - return self?.rootView + return rootView }, animatableClosure: { [weak self] in self?.tabBar?.view.layer.maskedCorners = [] @@ -80,24 +82,18 @@ private extension NovaMainAppContainerViewController { let topContainerBottomOffset = Constants.topContainerBottomOffset(for: view) let minimizedWidgetHeight = Constants.minimizedWidgetHeight(for: view) - // We do this in cases when minimizing started before the device orientation - // finished transitioning from landscape to portrait - let widgetTopConstraintInset = if view.bounds.height > view.bounds.width { - view.bounds.height - minimizedWidgetHeight - } else { - view.bounds.width - minimizedWidgetHeight - } - return DAppBrowserLayoutTransitionDependencies( layoutClosure: { [weak self] in - self?.browserWidget?.view.snp.updateConstraints { make in - make.bottom.equalToSuperview() - make.top.equalToSuperview().inset(widgetTopConstraintInset) + guard let self else { return nil } + + browserWidget?.view.snp.remakeConstraints { make in + make.top.equalTo(self.rootView.snp.bottom).inset(minimizedWidgetHeight) + make.bottom.leading.trailing.equalToSuperview() } - self?.topContainerBottomConstraint?.constant = -topContainerBottomOffset + topContainerBottomConstraint?.constant = -topContainerBottomOffset - return self?.rootView + return rootView }, animatableClosure: { [weak self] in self?.tabBar?.view.layer.maskedCorners = [ @@ -113,9 +109,8 @@ private extension NovaMainAppContainerViewController { return DAppBrowserLayoutTransitionDependencies( layoutClosure: { [weak self] in - self?.browserWidget?.view.snp.updateConstraints { make in - make.top.equalToSuperview() - make.bottom.equalToSuperview() + self?.browserWidget?.view.snp.remakeConstraints { make in + make.edges.equalToSuperview() } self?.topContainerBottomConstraint?.constant = -topContainerBottomOffset @@ -192,13 +187,12 @@ extension NovaMainAppContainerViewController { rootView.addSubview(bottomView) - let topContainerBottomOffset = Constants.topContainerBottomOffset(for: view) - let widgetTopConstraintInset = UIScreen.main.bounds.height + let minimizedWidgetHeight = Constants.minimizedWidgetHeight(for: view) bottomView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.bottom.equalToSuperview().inset(-topContainerBottomOffset) - make.top.equalToSuperview().inset(widgetTopConstraintInset) + make.top.equalTo(rootView.snp.bottom) } } }