diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 1fb4c0cb2..a90267000 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -1086,6 +1086,9 @@ 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 */; }; + 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 */; }; @@ -6509,6 +6512,9 @@ 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 = ""; }; + 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 = ""; }; @@ -13404,6 +13410,7 @@ children = ( 2D4431E82D0369EB00017951 /* DAppListViewModelFactoryProtocol.swift */, 842BA36C27B64F1000D31EEF /* DAppListViewModelFactory.swift */, + 2D20F1C12D353495003E9CF2 /* DAppIconViewModelFactory.swift */, ); path = DAppListViewModelFactory; sourceTree = ""; @@ -16221,6 +16228,7 @@ 842BA36A27B64F1000D31EEF /* DAppViewModel.swift */, 2D3EA7DF2CFEDC0F0033AFD2 /* DAppCategoryViewModelFactory.swift */, 2D4431E12D01130E00017951 /* DAppListSection.swift */, + 2D20F1BD2D351936003E9CF2 /* DAppListBannerViewModel.swift */, 2D4431EA2D036A0400017951 /* DAppListViewModelFactory */, ); path = ViewModel; @@ -21588,6 +21596,7 @@ children = ( 84EE780327C4CF9E0027357F /* DAppListItemsLoadingView.swift */, 84DA03DA275A31B500E8B326 /* DAppListHeaderView.swift */, + 2D20F1BF2D351B28003E9CF2 /* DAppListBannerView.swift */, 2D4431DC2D01094B00017951 /* DAppItemView */, 84720731277C370600F593DD /* DAppCategoriesView.swift */, 8448D5B7277DA4C200FAEEBC /* DAppListLoadingView.swift */, @@ -26134,6 +26143,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 +28047,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 */, @@ -30093,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/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) } } 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/DAppBrowser/DAppBrowserPresenter.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift index f65bf3054..633a36c49 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserPresenter.swift @@ -156,11 +156,17 @@ 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) + } + + func willDismissInteractive(stateRender: DAppBrowserTabRenderProtocol) { + view?.didDecideClose() interactor.saveLastTabState(render: stateRender) } @@ -270,6 +276,7 @@ extension DAppBrowserPresenter: DAppAuthDelegate { extension DAppBrowserPresenter: DAppPhishingViewDelegate { func dappPhishingViewDidHide() { + view?.didDecideClose() wireframe.close(view: view) } } 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/DAppBrowserTabList/DAppBrowserTabListPresenter.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabList/DAppBrowserTabListPresenter.swift index e4b556180..597042be6 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) { @@ -84,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() } 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? diff --git a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift index 315906f95..b94a9a4be 100644 --- a/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift +++ b/novawallet/Modules/DApp/DAppBrowser/DAppBrowserTabTransition.swift @@ -11,6 +11,26 @@ enum DAppBrowserTabTransition { guard let tabId else { return } let options = UIViewController.Transition.ZoomOptions() + 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 willDismiss { + destinationController.willBeDismissedInteractively() + } + + return willDismiss + } + options.alignmentRectProvider = { context in guard let destinationController = context.zoomedViewController as? DAppBrowserViewController else { return .zero @@ -45,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 e7a79f7ee..163570433 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,17 @@ final class DAppBrowserViewController: UIViewController, ViewHolder { presenter.setup() } + + func canBeDismissedInteractively() -> Bool { + !isLandscape + } + + func willBeDismissedInteractively() { + snapshotWebView { [weak self] image in + let render = DAppBrowserTabRender(for: image) + self?.presenter.willDismissInteractive(stateRender: render) + } + } } // MARK: Private @@ -175,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) @@ -557,13 +566,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/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/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/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/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..31a47113c --- /dev/null +++ b/novawallet/Modules/DApp/DAppList/View/DAppListBannerView.swift @@ -0,0 +1,76 @@ +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) { + 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 new file mode 100644 index 000000000..34255d19a --- /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: ImageViewModelProtocol +} + +// 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/DAppIconViewModelFactory.swift b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppIconViewModelFactory.swift new file mode 100644 index 000000000..eb93150a4 --- /dev/null +++ b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppIconViewModelFactory.swift @@ -0,0 +1,104 @@ +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 faviconAPIFormat: String + + init(faviconAPIFormat: String = Constants.ddgFaviconAPIFormat) { + self.faviconAPIFormat = faviconAPIFormat + } +} + +// MARK: Private + +private extension DAppIconViewModelFactory { + func createFaviconURL(for pageURL: URL) -> URL? { + let regex = try? NSRegularExpression(pattern: "(?:https?://)?([^/\\?]+)") + let urlString = pageURL.absoluteString + + 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 } + + let domain = urlString[range] + + let resultURLString = String( + format: faviconAPIFormat, + String(domain) + ) + + 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 + ) + } + + 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" + } +} diff --git a/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift b/novawallet/Modules/DApp/DAppList/ViewModel/DAppListViewModelFactory/DAppListViewModelFactory.swift index b9b5736aa..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] { @@ -96,14 +88,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 +105,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 ) @@ -217,6 +214,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 @@ -273,26 +294,15 @@ 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 availableCategories = Set(dAppsByQuery.flatMap(\.dapp.categories)) + let favoritesByQuery = search(by: query, in: favorites) 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 @@ -301,9 +311,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 }) @@ -355,6 +366,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/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/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/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 - ) - } - ) - } } 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 + } + } + } } diff --git a/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift b/novawallet/Modules/NovaMainAppContainer/NovaMainAppContainerViewController.swift index b0fe306d7..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 minimizedWidgetHeight = Constants.minimizedWidgetHeight(for: view) return DAppBrowserLayoutTransitionDependencies( layoutClosure: { [weak self] in - self?.browserWidget?.view.snp.updateConstraints { make in - make.bottom.equalToSuperview().inset(-topContainerBottomOffset) - make.height.equalTo(minimizedWidgetHeight) + 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 = [] @@ -82,14 +84,16 @@ private extension NovaMainAppContainerViewController { return DAppBrowserLayoutTransitionDependencies( layoutClosure: { [weak self] in - self?.browserWidget?.view.snp.updateConstraints { make in - make.bottom.equalToSuperview() - make.height.equalTo(minimizedWidgetHeight) + 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 = [ @@ -101,14 +105,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.bottom.equalToSuperview() + self?.browserWidget?.view.snp.remakeConstraints { make in + make.edges.equalToSuperview() } self?.topContainerBottomConstraint?.constant = -topContainerBottomOffset @@ -127,6 +129,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 @@ -161,13 +187,12 @@ extension NovaMainAppContainerViewController { rootView.addSubview(bottomView) - let topContainerBottomOffset = Constants.topContainerBottomOffset(for: view) let minimizedWidgetHeight = Constants.minimizedWidgetHeight(for: view) bottomView.snp.makeConstraints { make in make.leading.trailing.equalToSuperview() make.bottom.equalToSuperview().inset(-topContainerBottomOffset) - make.height.equalTo(minimizedWidgetHeight) + make.top.equalTo(rootView.snp.bottom) } } } @@ -184,30 +209,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 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], 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(