diff --git a/novawallet.xcodeproj/project.pbxproj b/novawallet.xcodeproj/project.pbxproj index 3f9ff941b6..fad48bb3e1 100644 --- a/novawallet.xcodeproj/project.pbxproj +++ b/novawallet.xcodeproj/project.pbxproj @@ -24,6 +24,7 @@ 044BCF706211CF2AA2D28CAE /* CloudBackupReviewChangesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E1AE1C4FF054023B98969F0 /* CloudBackupReviewChangesViewController.swift */; }; 049DA9A36A72CB6F8401769C /* WalletsListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F404EE82BC45BFE0F42E0A4 /* WalletsListWireframe.swift */; }; 04B85867D67D56994D99FF14 /* NftListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89CFED2E01AB638656E251AF /* NftListProtocols.swift */; }; + 04D6122369889C927BB3D13F /* SwapRouteDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 461EFCB1DAACD7D5DBBB713C /* SwapRouteDetailsViewController.swift */; }; 04D86D5341406305E60F6D18 /* ReferendumVoteSetupInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024B7E67C0603C53981EC394 /* ReferendumVoteSetupInteractor.swift */; }; 054C4BCDEC29ED5F74A36E8B /* ExportMnemonicPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61EBE466BDCF77E65FDCDF81 /* ExportMnemonicPresenter.swift */; }; 0566845B5E1D65E3632C54CE /* NotificationWalletListViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC56B91B4C4091E2429AC010 /* NotificationWalletListViewLayout.swift */; }; @@ -53,6 +54,13 @@ 0BB2E3FF30B1700D321C526A /* TransferNetworkSelectionViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA463769D0F411429780D7D /* TransferNetworkSelectionViewFactory.swift */; }; 0C0387512D066474000A2F24 /* AssetExchageUsdtConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387502D066474000A2F24 /* AssetExchageUsdtConverter.swift */; }; 0C03877C2D09A1AA000A2F24 /* MockAssetExchangePathCostEstimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03877B2D09A1AA000A2F24 /* MockAssetExchangePathCostEstimator.swift */; }; + 0C03877F2D0A1043000A2F24 /* SwapRouteDetailsItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03877E2D0A1043000A2F24 /* SwapRouteDetailsItemView.swift */; }; + 0C0387812D0A1283000A2F24 /* AssetAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387802D0A1283000A2F24 /* AssetAmountView.swift */; }; + 0C0387832D0A1878000A2F24 /* AssetAmountRouteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387822D0A1878000A2F24 /* AssetAmountRouteItemView.swift */; }; + 0C0387852D0A1C0A000A2F24 /* LabelRouteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387842D0A1C0A000A2F24 /* LabelRouteItemView.swift */; }; + 0C0387872D0A24E6000A2F24 /* SwapRouteDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387862D0A24E6000A2F24 /* SwapRouteDetailsView.swift */; }; + 0C03878A2D0A2785000A2F24 /* SwapRouteDetailsViewModelFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0387892D0A2785000A2F24 /* SwapRouteDetailsViewModelFactory.swift */; }; + 0C03878C2D0B2686000A2F24 /* LinePatternView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03878B2D0B2686000A2F24 /* LinePatternView.swift */; }; 0C053ECB2BA2FA5C003063A0 /* StorageLocationMigrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C053ECA2BA2FA5C003063A0 /* StorageLocationMigrationTests.swift */; }; 0C0CB37F2AC540B200EAC516 /* AssetConversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB37E2AC540B200EAC516 /* AssetConversion.swift */; }; 0C0CB3822AC545A800EAC516 /* AssetConversionExtrinsicService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0CB3812AC545A800EAC516 /* AssetConversionExtrinsicService.swift */; }; @@ -899,6 +907,7 @@ 106CC4BFC48B6BFFF31434A9 /* LedgerWalletConfirmPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BC1402B34E341312ABB378 /* LedgerWalletConfirmPresenter.swift */; }; 109512489F8CB32C2430808E /* ManualBackupKeyListViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29CD41F03830F4967EF06F91 /* ManualBackupKeyListViewLayout.swift */; }; 10DD08A4E459DB4757809318 /* ManualBackupWalletListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08D63836373E5AB433A04596 /* ManualBackupWalletListWireframe.swift */; }; + 1116E062DCFC5E1353B9B4F8 /* SwapRouteDetailsProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2BD7CE1EF80FCD3569BD44 /* SwapRouteDetailsProtocols.swift */; }; 1180349875F35B4D4DD88A4C /* StakingTypeViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4752D80077E85563CF3AD5D /* StakingTypeViewFactory.swift */; }; 11C6F4CD5B167DE4E9E7F654 /* DAppPhishingWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518305BB475DE40E94DCBD5D /* DAppPhishingWireframe.swift */; }; 1232A714A96F937330FC0AFA /* GovernanceDelegateConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = B718CE9C51158F87D37894BB /* GovernanceDelegateConfirmViewFactory.swift */; }; @@ -1344,6 +1353,7 @@ 4040F1394A73AC2FE598242C /* ControllerAccountConfirmationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04EF69DFE142600FF2708A13 /* ControllerAccountConfirmationViewController.swift */; }; 4083005E0DC01C9BB57BEBAE /* UsernameSetupViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74A2166B054240BD5D925B6 /* UsernameSetupViewFactory.swift */; }; 4097A50CF5E5794092354758 /* ParaStkCollatorInfoWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7944C833DD6EAB3100F50B2 /* ParaStkCollatorInfoWireframe.swift */; }; + 40A9CF698CC8CD2D83B6D564 /* SwapRouteDetailsWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516EEC84B1D075B8C965ACF6 /* SwapRouteDetailsWireframe.swift */; }; 40E5A7D9511B8760A2CCEB06 /* BackupAttentionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2D8A1ACC06F8EA9CE6C982B /* BackupAttentionViewController.swift */; }; 411E74233593A329298C6405 /* MessageSheetPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6C3AD523C06AD4FD58B7B3CC /* MessageSheetPresenter.swift */; }; 413CCB7C7B22831147B8E815 /* ParaStkRedeemPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E2F6581450E9DB4C18C4984 /* ParaStkRedeemPresenter.swift */; }; @@ -1526,6 +1536,7 @@ 692114AAB4AC5E1C58A21FCD /* GovernanceTracksSettingsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40C01C724F616836BACFBE8B /* GovernanceTracksSettingsInteractor.swift */; }; 6927967A758F8FB354C62F49 /* CloudBackupRemindViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F65C2DCBA74EBD7DE89CF02C /* CloudBackupRemindViewController.swift */; }; 694FC2B5A9C40461F74763B5 /* AssetListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D529B9CA8D6E2E6F375FC260 /* AssetListProtocols.swift */; }; + 6962A0D96A2F40C7D50FFFA7 /* SwapRouteDetailsViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEDD7BBAB8B408BC84477468 /* SwapRouteDetailsViewLayout.swift */; }; 6A2B6DF2D6AE912D5FA62D94 /* ParitySignerAddressesWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8B10C37813EFE7D7663605E /* ParitySignerAddressesWireframe.swift */; }; 6A2B815B3AA8B31CF77023A7 /* NetworkNodeViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C5CE1E167B357490396AAC5 /* NetworkNodeViewFactory.swift */; }; 6A776A53FEC109C875113B38 /* ParaStkCollatorFiltersViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED010772D7CE0450BDF30707 /* ParaStkCollatorFiltersViewFactory.swift */; }; @@ -1585,6 +1596,7 @@ 75B99AA8D71430BACFAEF755 /* DAppWalletAuthWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03622BA8CF4657307B8F9B97 /* DAppWalletAuthWireframe.swift */; }; 75DAB313623E900EC475E215 /* LedgerTxConfirmViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = CCBCB7C3ABB6C06CD4681D44 /* LedgerTxConfirmViewFactory.swift */; }; 75E689BC8D16786DF2674171 /* AssetListViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81F4883B898928C77D17C824 /* AssetListViewLayout.swift */; }; + 766794D7BD30D0A4AABC67D3 /* SwapRouteDetailsInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA78AF786BFB1FC050738206 /* SwapRouteDetailsInteractor.swift */; }; 766FE2FAB8509BF0F56EA3C0 /* ParaStkCollatorInfoProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7F3B8502E5BF8CDD7ACE2DD0 /* ParaStkCollatorInfoProtocols.swift */; }; 76A79A11227196CC5AF21D22 /* NetworksListWireframe.swift in Sources */ = {isa = PBXBuildFile; fileRef = 932EBC486896C9ECB60977C4 /* NetworksListWireframe.swift */; }; 76B0B7147181747A7CEDDDF6 /* GovernanceUnavailableTracksViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30E25CF67173500E0AC19387 /* GovernanceUnavailableTracksViewController.swift */; }; @@ -4922,6 +4934,7 @@ D264B2A8A516396051016CAB /* AssetReceivePresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3934C46625930FA8D171D3E7 /* AssetReceivePresenter.swift */; }; D274117F06B12F955073D35B /* DelegationReferendumVotersViewLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7059B3F1E8DC94D36733B4C7 /* DelegationReferendumVotersViewLayout.swift */; }; D344C6DAC1F8BB6152BA8DD0 /* RecommendedValidatorListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C6573C52692E4A56E35FF9 /* RecommendedValidatorListProtocols.swift */; }; + D381A3467D7C07331DBEB68F /* SwapRouteDetailsViewFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A3DA31DE8C8AD1D82D66DEF /* SwapRouteDetailsViewFactory.swift */; }; D39B1873B761440E4E0EA749 /* AdvancedExportPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8EB218244A61B9743D9ACF69 /* AdvancedExportPresenter.swift */; }; D3B48F82A875E301D749AC0B /* StakingUnbondConfirmViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5674162035C7D9F226FA9964 /* StakingUnbondConfirmViewController.swift */; }; D3B74ED2525DE12423722DE2 /* AssetReceiveInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B243F7A096241F329224A18E /* AssetReceiveInteractor.swift */; }; @@ -5065,6 +5078,7 @@ EEDDE41F8445C0CB2E99AFE4 /* ParaStkYieldBoostStartPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3089A0A7C992300CE839A050 /* ParaStkYieldBoostStartPresenter.swift */; }; EFEB65B229DB34B4B526003B /* ParaStkStakeConfirmInteractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CDC7A44F6B01FE389F34C3A /* ParaStkStakeConfirmInteractor.swift */; }; EFF8F905CE4E8A212FE79EE4 /* ParaStkYourCollatorsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5E099C1E3DC3730DD503BE /* ParaStkYourCollatorsViewController.swift */; }; + F007ED524D9B187C5159C5DA /* SwapRouteDetailsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 399E91BE2E289FE301D7846A /* SwapRouteDetailsPresenter.swift */; }; F022F1444E0F75CCA42F4648 /* YourValidatorListProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A31780E84948D7FE632ECB02 /* YourValidatorListProtocols.swift */; }; F040165DFF9C8D7C5CC47581 /* AddDelegationProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85C12FF06C3E4D642221EDCD /* AddDelegationProtocols.swift */; }; F041E02C4EE5E899B7F153B5 /* CloudBackupSettingsPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18AFA9612CE9D32E08F6DBED /* CloudBackupSettingsPresenter.swift */; }; @@ -5336,6 +5350,13 @@ 0C029E292CE4D2AB00649C28 /* SubstrateDataModel34.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = SubstrateDataModel34.xcdatamodel; sourceTree = ""; }; 0C0387502D066474000A2F24 /* AssetExchageUsdtConverter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchageUsdtConverter.swift; sourceTree = ""; }; 0C03877B2D09A1AA000A2F24 /* MockAssetExchangePathCostEstimator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAssetExchangePathCostEstimator.swift; sourceTree = ""; }; + 0C03877E2D0A1043000A2F24 /* SwapRouteDetailsItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsItemView.swift; sourceTree = ""; }; + 0C0387802D0A1283000A2F24 /* AssetAmountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetAmountView.swift; sourceTree = ""; }; + 0C0387822D0A1878000A2F24 /* AssetAmountRouteItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetAmountRouteItemView.swift; sourceTree = ""; }; + 0C0387842D0A1C0A000A2F24 /* LabelRouteItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelRouteItemView.swift; sourceTree = ""; }; + 0C0387862D0A24E6000A2F24 /* SwapRouteDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsView.swift; sourceTree = ""; }; + 0C0387892D0A2785000A2F24 /* SwapRouteDetailsViewModelFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsViewModelFactory.swift; sourceTree = ""; }; + 0C03878B2D0B2686000A2F24 /* LinePatternView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinePatternView.swift; sourceTree = ""; }; 0C053ECA2BA2FA5C003063A0 /* StorageLocationMigrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageLocationMigrationTests.swift; sourceTree = ""; }; 0C0CB37E2AC540B200EAC516 /* AssetConversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversion.swift; sourceTree = ""; }; 0C0CB3812AC545A800EAC516 /* AssetConversionExtrinsicService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetConversionExtrinsicService.swift; sourceTree = ""; }; @@ -5449,6 +5470,7 @@ 0C2B18B32BFEFD2A00206EDE /* CloudBackupConflictsResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudBackupConflictsResolver.swift; sourceTree = ""; }; 0C2B3C9875FDA7EE8D168900 /* ParaStkYieldBoostSetupWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkYieldBoostSetupWireframe.swift; sourceTree = ""; }; 0C2B583DB30C6C818B0F952D /* ParaStkRebondWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParaStkRebondWireframe.swift; sourceTree = ""; }; + 0C2BD7CE1EF80FCD3569BD44 /* SwapRouteDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsProtocols.swift; sourceTree = ""; }; 0C2DA8992CC21419001F79C8 /* GraphQuotableEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQuotableEdge.swift; sourceTree = ""; }; 0C2DA89B2CC215D0001F79C8 /* AssetExchangeGraphEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AssetExchangeGraphEdge.swift; sourceTree = ""; }; 0C2DA89D2CC21853001F79C8 /* CrosschainExchangeEdge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrosschainExchangeEdge.swift; sourceTree = ""; }; @@ -6647,6 +6669,7 @@ 397F057FD5B16A58E5F30F07 /* NPoolsUnstakeConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NPoolsUnstakeConfirmWireframe.swift; sourceTree = ""; }; 39907750D40A8DD7FE1288C8 /* CreateWatchOnlyViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateWatchOnlyViewController.swift; sourceTree = ""; }; 399700B22225DD916DFACAF9 /* DelegateVotedReferendaViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DelegateVotedReferendaViewFactory.swift; sourceTree = ""; }; + 399E91BE2E289FE301D7846A /* SwapRouteDetailsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsPresenter.swift; sourceTree = ""; }; 3A46EE888D60C1538A0A3EFC /* NftDetailsProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftDetailsProtocols.swift; sourceTree = ""; }; 3A7235097E09C94005B091B4 /* CommonDelegationTracksPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CommonDelegationTracksPresenter.swift; sourceTree = ""; }; 3A76BDAB14EEA1C4E23B884E /* ParitySignerTxQrViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxQrViewController.swift; sourceTree = ""; }; @@ -6706,6 +6729,7 @@ 45C0B1C175A2470AAA50DAC5 /* DAppSettingsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppSettingsViewController.swift; sourceTree = ""; }; 461A9E7672D247C9CCF0B45D /* GovernanceRevokeDelegationTracksWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceRevokeDelegationTracksWireframe.swift; sourceTree = ""; }; 461B7FAD84690F82070E4431 /* StakingRebagConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRebagConfirmPresenter.swift; sourceTree = ""; }; + 461EFCB1DAACD7D5DBBB713C /* SwapRouteDetailsViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsViewController.swift; sourceTree = ""; }; 466D36DE48F51DC3023E3C5E /* StakingProxyManagementViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingProxyManagementViewController.swift; sourceTree = ""; }; 46BDF9947BE6366712E454DD /* LedgerDiscoverWalletCreateWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerDiscoverWalletCreateWireframe.swift; sourceTree = ""; }; 47042BBA5082B1EC1D017B56 /* AssetReceiveProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AssetReceiveProtocols.swift; sourceTree = ""; }; @@ -6754,6 +6778,7 @@ 513A449CCF5A417B67B7067D /* GovernanceUnlockConfirmPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnlockConfirmPresenter.swift; sourceTree = ""; }; 5147BFCC44EB3938D50EE8D9 /* DAppPhishingPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppPhishingPresenter.swift; sourceTree = ""; }; 5159EA2661A6CBE123CCF891 /* ReferendumVoteSetupProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumVoteSetupProtocols.swift; sourceTree = ""; }; + 516EEC84B1D075B8C965ACF6 /* SwapRouteDetailsWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsWireframe.swift; sourceTree = ""; }; 518305BB475DE40E94DCBD5D /* DAppPhishingWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppPhishingWireframe.swift; sourceTree = ""; }; 525813EB768E636A397C00BB /* WalletsChoosePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = WalletsChoosePresenter.swift; sourceTree = ""; }; 5278A5F4178922A240590334 /* DAppBrowserViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DAppBrowserViewLayout.swift; sourceTree = ""; }; @@ -9785,6 +9810,7 @@ 899686C7351A2600FFA08371 /* TransferConfirmOnChainViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TransferConfirmOnChainViewFactory.swift; sourceTree = ""; }; 89AAAF09837D225A05769A7D /* ProxySignValidationPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProxySignValidationPresenter.swift; sourceTree = ""; }; 89CFED2E01AB638656E251AF /* NftListProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NftListProtocols.swift; sourceTree = ""; }; + 8A3DA31DE8C8AD1D82D66DEF /* SwapRouteDetailsViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsViewFactory.swift; sourceTree = ""; }; 8B0BF8DFAA80B405D4A5D891 /* GovernanceUnavailableTracksViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GovernanceUnavailableTracksViewFactory.swift; sourceTree = ""; }; 8B4C1B5D56DB69BA0AECF731 /* ExportSeedWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportSeedWireframe.swift; sourceTree = ""; }; 8B56BDC7E6221DE292498D3A /* ParitySignerTxQrViewFactory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ParitySignerTxQrViewFactory.swift; sourceTree = ""; }; @@ -10283,6 +10309,7 @@ DA086DFCCA0976489FD38B95 /* MoonbeamTermsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MoonbeamTermsInteractor.swift; sourceTree = ""; }; DA3918C1B96EB07100C74815 /* CloudBackupRemindProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CloudBackupRemindProtocols.swift; sourceTree = ""; }; DA576BBCED647C22E93F0202 /* NominationPoolBondMoreConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NominationPoolBondMoreConfirmWireframe.swift; sourceTree = ""; }; + DA78AF786BFB1FC050738206 /* SwapRouteDetailsInteractor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsInteractor.swift; sourceTree = ""; }; DA9DD724F02DA0A174D875A8 /* CreateWatchOnlyViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreateWatchOnlyViewLayout.swift; sourceTree = ""; }; DB7F5F9B54BE4234C5682BDE /* StakingRedeemViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = StakingRedeemViewController.swift; sourceTree = ""; }; DB8939D2E509D6FF66B7E117 /* InAppUpdatesProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = InAppUpdatesProtocols.swift; sourceTree = ""; }; @@ -10376,6 +10403,7 @@ EEB918ED70D77D1832094A8A /* LedgerTxConfirmWireframe.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LedgerTxConfirmWireframe.swift; sourceTree = ""; }; EEBBEC474F607DD9F2A0F4FD /* AccountCreatePresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AccountCreatePresenter.swift; sourceTree = ""; }; EED9939B17C4224C8E153F8A /* SelectValidatorsStartProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SelectValidatorsStartProtocols.swift; sourceTree = ""; }; + EEDD7BBAB8B408BC84477468 /* SwapRouteDetailsViewLayout.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SwapRouteDetailsViewLayout.swift; sourceTree = ""; }; EEEF1360E04CD27CCC13E472 /* ReferendumsFiltersPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ReferendumsFiltersPresenter.swift; sourceTree = ""; }; EF3AD755B2B3DCFB3D14DF91 /* ExportMnemonicProtocols.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ExportMnemonicProtocols.swift; sourceTree = ""; }; EF4ED0EE2A3EF620DC51870B /* CrowdloanYourContributionsPresenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CrowdloanYourContributionsPresenter.swift; sourceTree = ""; }; @@ -10812,6 +10840,23 @@ path = Amount; sourceTree = ""; }; + 0C03877D2D0A1007000A2F24 /* View */ = { + isa = PBXGroup; + children = ( + 0C03877E2D0A1043000A2F24 /* SwapRouteDetailsItemView.swift */, + 0C0387862D0A24E6000A2F24 /* SwapRouteDetailsView.swift */, + ); + path = View; + sourceTree = ""; + }; + 0C0387882D0A2770000A2F24 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 0C0387892D0A2785000A2F24 /* SwapRouteDetailsViewModelFactory.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; 0C0CB37C2AC5408000EAC516 /* AssetConversion */ = { isa = PBXGroup; children = ( @@ -12492,6 +12537,8 @@ children = ( 0CF3A6C82CE9423000F93C49 /* RouteView.swift */, 0CF3A6CA2CE949ED00F93C49 /* SwapRouteView.swift */, + 0C0387822D0A1878000A2F24 /* AssetAmountRouteItemView.swift */, + 0C0387842D0A1C0A000A2F24 /* LabelRouteItemView.swift */, ); path = View; sourceTree = ""; @@ -14135,6 +14182,22 @@ path = StakingRewardFilters; sourceTree = ""; }; + 5F08F57C053DB946BD47EFE5 /* RouteDetails */ = { + isa = PBXGroup; + children = ( + 0C0387882D0A2770000A2F24 /* ViewModel */, + 0C03877D2D0A1007000A2F24 /* View */, + 0C2BD7CE1EF80FCD3569BD44 /* SwapRouteDetailsProtocols.swift */, + 516EEC84B1D075B8C965ACF6 /* SwapRouteDetailsWireframe.swift */, + 399E91BE2E289FE301D7846A /* SwapRouteDetailsPresenter.swift */, + DA78AF786BFB1FC050738206 /* SwapRouteDetailsInteractor.swift */, + 461EFCB1DAACD7D5DBBB713C /* SwapRouteDetailsViewController.swift */, + EEDD7BBAB8B408BC84477468 /* SwapRouteDetailsViewLayout.swift */, + 8A3DA31DE8C8AD1D82D66DEF /* SwapRouteDetailsViewFactory.swift */, + ); + path = RouteDetails; + sourceTree = ""; + }; 5F9F36C10D734E30870F81AB /* KnownNetworksList */ = { isa = PBXGroup; children = ( @@ -14801,6 +14864,7 @@ 29BD7DA0076BA8BC3411221A /* Setup */, 7E5E800395DC908962C169CF /* Confirm */, E53BE56E5726646BC073E502 /* Execution */, + 5F08F57C053DB946BD47EFE5 /* RouteDetails */, ); path = Swaps; sourceTree = ""; @@ -18940,6 +19004,8 @@ 0CAC03052B4D86E000DDEC3A /* LocalizableControllerBackedView.swift */, 0C11F0272CEFD99C008D19D2 /* CountdownLoadingView.swift */, 0CF976762CF08EF9001D2801 /* OperationExecutionProgressView.swift */, + 0C0387802D0A1283000A2F24 /* AssetAmountView.swift */, + 0C03878B2D0B2686000A2F24 /* LinePatternView.swift */, ); path = View; sourceTree = ""; @@ -25702,6 +25768,7 @@ 77A0B2F52A3CA39D00CBF653 /* StakingMoreOptionCollectionViewCell.swift in Sources */, 88F34FDB28FFE6AA00712BDE /* RequestedAmountRow.swift in Sources */, 880CC0AA29E7F151008C7F65 /* EquilibriumLocksSubscription.swift in Sources */, + 0C03878A2D0A2785000A2F24 /* SwapRouteDetailsViewModelFactory.swift in Sources */, 8490145324A93FD1008F705E /* NovaLoadingViewFactory.swift in Sources */, 8472976C260B1CAD009B86D0 /* InitiatedBondingConfirmInteractor.swift in Sources */, 0C6390912BF091610015D467 /* CloudBackupUpdateCalculationFactory.swift in Sources */, @@ -25942,6 +26009,7 @@ 0C9D87AE2AC708070095FE8C /* AssetHubTokensConverter.swift in Sources */, 84B8AA7529F8FD2400347A37 /* DAppInteractionFactory.swift in Sources */, 0C38B5072B7B3B4B00882A8B /* HydraOmnipool+Events.swift in Sources */, + 0C03878C2D0B2686000A2F24 /* LinePatternView.swift in Sources */, 844EFB65265FD61D0090ACB1 /* CrowdloanContributeConfirmViewModel.swift in Sources */, AEE5FAFF26415E0C002B8FDC /* StakingRebondSetupPresenter.swift in Sources */, 84A3B8A02836D74B00DE2669 /* StorageKeyDecodingProtocol.swift in Sources */, @@ -26053,6 +26121,7 @@ 77A0B2F32A3CA37E00CBF653 /* StakingMoreOptionsViewModelFactory.swift in Sources */, 849014B924AA87E3008F705E /* PinSetupViewController.swift in Sources */, 84F98D8A25E3DD3F0040418E /* StorageCodingPath.swift in Sources */, + 0C0387852D0A1C0A000A2F24 /* LabelRouteItemView.swift in Sources */, 8846F74229D759DD00B8B776 /* Data+proquint.swift in Sources */, 842D8B782A4098C300660005 /* ShimmeringLabel.swift in Sources */, 0C7A3FCE2B6A874500764603 /* HydraSwapRemoteState.swift in Sources */, @@ -26685,6 +26754,7 @@ F402BC83273ACDC30075F803 /* AstarBonusService.swift in Sources */, 888797B429F11EF90078633F /* SettingsBaseTableViewCell.swift in Sources */, 84205897260C795B007D26C6 /* NominatorState+Status.swift in Sources */, + 0C0387812D0A1283000A2F24 /* AssetAmountView.swift in Sources */, AE8B883226733BAD00AB0AA9 /* CustomValidatorListHeaderView.swift in Sources */, 8488D5DB298167D10019B388 /* GovernanceDelegateTypeView.swift in Sources */, 84CC726228AF8C7A003429E7 /* LedgerApplicationRequest.swift in Sources */, @@ -26844,6 +26914,7 @@ 84F30EE425FFAC0800039D09 /* StreamableProviderOptions+Substrate.swift in Sources */, 845B811228F429BB0040CE84 /* SupportPallet.swift in Sources */, 0C3205C62A877594002EB914 /* EvmGasPriceWithFallbackProvider.swift in Sources */, + 0C0387872D0A24E6000A2F24 /* SwapRouteDetailsView.swift in Sources */, 8470D6D4253E35F0009E9A5D /* StorageUpdate.swift in Sources */, 8460E715284AC0AA002896E9 /* ParaStkBaseUnstakeInteractor.swift in Sources */, 84355CEE28B614A7004E5C5E /* MessageSheetImageGraphicsView.swift in Sources */, @@ -28330,6 +28401,7 @@ 841E553C282D44BA00C8438F /* ParachainStakingAccountSubscriptionService.swift in Sources */, 84C479C129309E58003DF82B /* BasePolkassemblyOperationFactory.swift in Sources */, 0CA5BDF32C4476A3000A4CDD /* ConnectionCreationParams.swift in Sources */, + 0C03877F2D0A1043000A2F24 /* SwapRouteDetailsItemView.swift in Sources */, 770ABB8C2B85587200132465 /* PushNotificationTopicSettings.swift in Sources */, 0C846B892BE50BF2000EBFC2 /* VaraRewardEngine.swift in Sources */, 84300B2C26C10C9B00D64514 /* ConnectionStateReporting.swift in Sources */, @@ -29558,6 +29630,7 @@ 1232A714A96F937330FC0AFA /* GovernanceDelegateConfirmViewFactory.swift in Sources */, 4B1FA597B618713C75917816 /* GovernanceYourDelegationsProtocols.swift in Sources */, 15B079FA97C96327FD4A2E16 /* GovernanceYourDelegationsWireframe.swift in Sources */, + 0C0387832D0A1878000A2F24 /* AssetAmountRouteItemView.swift in Sources */, 77740BC42AD8145500E8C06F /* PercentInputView.swift in Sources */, 75249684C6F3EE4E553DABA1 /* GovernanceYourDelegationsPresenter.swift in Sources */, EB20C6B406155664B981BA94 /* GovernanceYourDelegationsInteractor.swift in Sources */, @@ -30120,6 +30193,13 @@ D1D2709811B89D89F8A08FAF /* SwapExecutionViewController.swift in Sources */, 8BB2AB5B4E97FAE0D27A4B60 /* SwapExecutionViewLayout.swift in Sources */, D010C2D055F79587881692F3 /* SwapExecutionViewFactory.swift in Sources */, + 1116E062DCFC5E1353B9B4F8 /* SwapRouteDetailsProtocols.swift in Sources */, + 40A9CF698CC8CD2D83B6D564 /* SwapRouteDetailsWireframe.swift in Sources */, + F007ED524D9B187C5159C5DA /* SwapRouteDetailsPresenter.swift in Sources */, + 766794D7BD30D0A4AABC67D3 /* SwapRouteDetailsInteractor.swift in Sources */, + 04D6122369889C927BB3D13F /* SwapRouteDetailsViewController.swift in Sources */, + 6962A0D96A2F40C7D50FFFA7 /* SwapRouteDetailsViewLayout.swift in Sources */, + D381A3467D7C07331DBEB68F /* SwapRouteDetailsViewFactory.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/novawallet/Assets.xcassets/colors/background/colorRouteNumberBackground.colorset/Contents.json b/novawallet/Assets.xcassets/colors/background/colorRouteNumberBackground.colorset/Contents.json new file mode 100644 index 0000000000..c14dc7cc8b --- /dev/null +++ b/novawallet/Assets.xcassets/colors/background/colorRouteNumberBackground.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x3B", + "green" : "0x2D", + "red" : "0x2B" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/novawallet/Common/View/AssetAmountView.swift b/novawallet/Common/View/AssetAmountView.swift new file mode 100644 index 0000000000..4de6e558dd --- /dev/null +++ b/novawallet/Common/View/AssetAmountView.swift @@ -0,0 +1,16 @@ +import UIKit + +class AssetAmountView: GenericPairValueView { + var assetIconView: AssetIconView { fView } + var amountLabel: UILabel { sView } + + override init(frame: CGRect) { + super.init(frame: frame) + + configure() + } + + private func configure() { + setHorizontalAndSpacing(1) + } +} diff --git a/novawallet/Common/View/LinePatternView.swift b/novawallet/Common/View/LinePatternView.swift new file mode 100644 index 0000000000..c28e86cf6d --- /dev/null +++ b/novawallet/Common/View/LinePatternView.swift @@ -0,0 +1,51 @@ +import Foundation +import SoraUI + +final class LinePatternView: UIView { + var style: Style = .defaultStyle { + didSet { + setNeedsDisplay() + } + } + + override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { + return + } + + context.setLineWidth(style.lineWidth) + context.setStrokeColor(style.color.cgColor) + + if let pattern = style.pattern { + context.setLineDash(phase: pattern.phase, lengths: pattern.segments) + } + + context.move(to: CGPoint(x: rect.midX, y: 0)) + context.addLine(to: CGPoint(x: rect.midX, y: rect.maxY)) + context.drawPath(using: .stroke) + } +} + +extension LinePatternView { + struct Pattern { + let segments: [CGFloat] + let phase: CGFloat + } + + struct Style { + let color: UIColor + let lineWidth: CGFloat + let pattern: Pattern? + + static var defaultStyle: Style { + Style( + color: R.color.colorIconSecondary()!, + lineWidth: 1, + pattern: LinePatternView.Pattern( + segments: [2, 3], + phase: 0 + ) + ) + } + } +} diff --git a/novawallet/Modules/AppearanceSettings/AssetIconViewModelFactory.swift b/novawallet/Modules/AppearanceSettings/AssetIconViewModelFactory.swift index de8022b448..be9b7232bf 100644 --- a/novawallet/Modules/AppearanceSettings/AssetIconViewModelFactory.swift +++ b/novawallet/Modules/AppearanceSettings/AssetIconViewModelFactory.swift @@ -21,6 +21,13 @@ extension AssetIconViewModelFactoryProtocol { defaultURL: defaultURL ) } + + func createAssetIconViewModel(from assetDisplayInfo: AssetBalanceDisplayInfo) -> ImageViewModelProtocol { + createAssetIconViewModel( + for: assetDisplayInfo.icon?.getPath(), + defaultURL: assetDisplayInfo.icon?.getURL() + ) + } } class AssetIconViewModelFactory { diff --git a/novawallet/Modules/AssetExchange/Common/AssetExchangeOperationFee.swift b/novawallet/Modules/AssetExchange/Common/AssetExchangeOperationFee.swift index c78e7f28e9..8a9cfc1313 100644 --- a/novawallet/Modules/AssetExchange/Common/AssetExchangeOperationFee.swift +++ b/novawallet/Modules/AssetExchange/Common/AssetExchangeOperationFee.swift @@ -22,6 +22,10 @@ struct AssetExchangeOperationFee: Equatable { func totalAmountIn(asset: ChainAssetId) -> Balance { self.asset == asset ? amount : 0 } + + func addAmount(to store: inout [ChainAssetId: Balance]) { + store[asset] = (store[asset] ?? 0) + amount + } } struct Submission: Equatable { @@ -53,6 +57,10 @@ struct AssetExchangeOperationFee: Equatable { return amountWithAsset.totalAmountIn(asset: asset) } + + func addAmount(to store: inout [ChainAssetId: Balance]) { + amountWithAsset.addAmount(to: &store) + } } struct AmountByPayer: Equatable { @@ -81,6 +89,10 @@ struct AssetExchangeOperationFee: Equatable { return amountWithAsset.totalAmountIn(asset: asset) } + + func addAmount(to store: inout [ChainAssetId: Balance]) { + amountWithAsset.addAmount(to: &store) + } } struct PostSubmission: Equatable { @@ -161,6 +173,11 @@ struct AssetExchangeOperationFee: Equatable { return totalByAccount + totalFromAmount } + + func addAmount(to store: inout [ChainAssetId: Balance]) { + paidByAccount.forEach { $0.amountWithAsset.addAmount(to: &store) } + paidFromAmount.forEach { $0.addAmount(to: &store) } + } } /** @@ -227,6 +244,15 @@ extension AssetExchangeOperationFee { return submissionTotal + postSubmissionTotal } + + func groupedAmountByAsset() -> [ChainAssetId: Balance] { + var store: [ChainAssetId: Balance] = [:] + + submissionFee.addAmount(to: &store) + postSubmissionFee.addAmount(to: &store) + + return store + } } extension AssetExchangeOperationFee.Submission: ExtrinsicFeeProtocol { diff --git a/novawallet/Modules/InAppUpdates/InAppUpdatesStyles.swift b/novawallet/Modules/InAppUpdates/InAppUpdatesStyles.swift index 7d06767d42..8f14e8dc16 100644 --- a/novawallet/Modules/InAppUpdates/InAppUpdatesStyles.swift +++ b/novawallet/Modules/InAppUpdates/InAppUpdatesStyles.swift @@ -13,6 +13,19 @@ extension BorderedLabelView { } extension BorderedLabelView.Style { + static let stepNumber = BorderedLabelView.Style( + text: .init( + textColor: R.color.colorTextSecondary()!, + font: .semiBoldCaps1 + ), + background: .init( + shadowOpacity: 0, + strokeWidth: 0, + fillColor: R.color.colorRouteNumberBackground()!, + highlightedFillColor: R.color.colorRouteNumberBackground()! + ) + ) + static let chipsText = BorderedLabelView.Style( text: .init( textColor: R.color.colorChipText()!, diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift index 7b5adc96dd..3841eda8c5 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmPresenter.swift @@ -381,6 +381,18 @@ extension SwapConfirmPresenter: SwapConfirmPresenterProtocol { ) } + func showRouteDetails() { + guard let fee else { + return + } + + wireframe.showRouteDetails( + from: view, + quote: initState.quote, + fee: fee + ) + } + func confirm() { guard let swapModel = getSwapModel() else { return diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift index d4533834e8..bd59d07ca5 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmProtocols.swift @@ -22,6 +22,7 @@ protocol SwapConfirmPresenterProtocol: AnyObject { func showPriceDifferenceInfo() func showSlippageInfo() func showNetworkFeeInfo() + func showRouteDetails() func showAddressOptions() func confirm() } @@ -34,4 +35,10 @@ protocol SwapConfirmWireframeProtocol: SwapBaseWireframeProtocol, AddressOptions from view: SwapConfirmViewProtocol?, model: SwapExecutionModel ) + + func showRouteDetails( + from view: ControllerBackedProtocol?, + quote: AssetExchangeQuote, + fee: AssetExchangeFee + ) } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift index b8fb44a1b6..5d41756a5e 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmViewController.swift @@ -42,6 +42,7 @@ final class SwapConfirmViewController: UIViewController, ViewHolder { private func setupHandlers() { rootView.rateCell.addTarget(self, action: #selector(rateAction), for: .touchUpInside) + rootView.routeCell.addTarget(self, action: #selector(routeAction), for: .touchUpInside) rootView.priceDifferenceCell.addTarget(self, action: #selector(priceDifferenceAction), for: .touchUpInside) rootView.slippageCell.addTarget(self, action: #selector(slippageAction), for: .touchUpInside) rootView.networkFeeCell.addTarget(self, action: #selector(networkFeeAction), for: .touchUpInside) @@ -65,6 +66,10 @@ final class SwapConfirmViewController: UIViewController, ViewHolder { presenter.showNetworkFeeInfo() } + @objc private func routeAction() { + presenter.showRouteDetails() + } + @objc private func addressAction() { presenter.showAddressOptions() } diff --git a/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift b/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift index b1cabee475..5fdf720570 100644 --- a/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift +++ b/novawallet/Modules/Swaps/Confirm/SwapConfirmWireframe.swift @@ -31,4 +31,23 @@ final class SwapConfirmWireframe: SwapConfirmWireframeProtocol { presenter?.present(swapExecutionView.controller, animated: true) } } + + func showRouteDetails( + from view: ControllerBackedProtocol?, + quote: AssetExchangeQuote, + fee: AssetExchangeFee + ) { + guard + let routeDetailsView = SwapRouteDetailsViewFactory.createView( + for: quote, + fee: fee, + state: flowState + ) else { + return + } + + let navigationController = NovaNavigationController(rootViewController: routeDetailsView.controller) + + view?.controller.present(navigationController, animated: true) + } } diff --git a/novawallet/Modules/Swaps/Execution/SwapExecutionPresenter.swift b/novawallet/Modules/Swaps/Execution/SwapExecutionPresenter.swift index 8b28837b4c..3cc65146da 100644 --- a/novawallet/Modules/Swaps/Execution/SwapExecutionPresenter.swift +++ b/novawallet/Modules/Swaps/Execution/SwapExecutionPresenter.swift @@ -251,6 +251,14 @@ extension SwapExecutionPresenter: SwapExecutionPresenterProtocol { wireframe.showRateInfo(from: view) } + func showRouteDetails() { + wireframe.showRouteDetails( + from: view, + quote: model.quote, + fee: model.fee + ) + } + func showPriceDifferenceInfo() { let title = LocalizableResource { R.string.localizable.swapsSetupPriceDifference( diff --git a/novawallet/Modules/Swaps/Execution/SwapExecutionProtocols.swift b/novawallet/Modules/Swaps/Execution/SwapExecutionProtocols.swift index d91f6f5ad5..691f357ec0 100644 --- a/novawallet/Modules/Swaps/Execution/SwapExecutionProtocols.swift +++ b/novawallet/Modules/Swaps/Execution/SwapExecutionProtocols.swift @@ -16,6 +16,7 @@ protocol SwapExecutionPresenterProtocol: AnyObject { func showPriceDifferenceInfo() func showSlippageInfo() func showTotalFeeInfo() + func showRouteDetails() func activateDone() func activateTryAgain() } @@ -42,4 +43,10 @@ protocol SwapExecutionWireframeProtocol: ShortTextInfoPresentable, MessageSheetP payChainAsset: ChainAsset, receiveChainAsset: ChainAsset ) + + func showRouteDetails( + from view: ControllerBackedProtocol?, + quote: AssetExchangeQuote, + fee: AssetExchangeFee + ) } diff --git a/novawallet/Modules/Swaps/Execution/SwapExecutionViewController.swift b/novawallet/Modules/Swaps/Execution/SwapExecutionViewController.swift index 2f734bcf5b..f5af5326e3 100644 --- a/novawallet/Modules/Swaps/Execution/SwapExecutionViewController.swift +++ b/novawallet/Modules/Swaps/Execution/SwapExecutionViewController.swift @@ -44,6 +44,7 @@ final class SwapExecutionViewController: UIViewController, ViewHolder { rootView.detailsView.delegate = self rootView.rateCell.addTarget(self, action: #selector(rateAction), for: .touchUpInside) + rootView.routeCell.addTarget(self, action: #selector(routeAction), for: .touchUpInside) rootView.priceDifferenceCell.addTarget(self, action: #selector(priceDifferenceAction), for: .touchUpInside) rootView.slippageCell.addTarget(self, action: #selector(slippageAction), for: .touchUpInside) rootView.totalFeeCell.addTarget(self, action: #selector(totalFeeAction), for: .touchUpInside) @@ -61,6 +62,10 @@ final class SwapExecutionViewController: UIViewController, ViewHolder { presenter.showPriceDifferenceInfo() } + @objc private func routeAction() { + presenter.showRouteDetails() + } + @objc private func slippageAction() { presenter.showSlippageInfo() } diff --git a/novawallet/Modules/Swaps/Execution/SwapExecutionWireframe.swift b/novawallet/Modules/Swaps/Execution/SwapExecutionWireframe.swift index fd64055621..8400a91ed6 100644 --- a/novawallet/Modules/Swaps/Execution/SwapExecutionWireframe.swift +++ b/novawallet/Modules/Swaps/Execution/SwapExecutionWireframe.swift @@ -44,4 +44,23 @@ final class SwapExecutionWireframe: SwapExecutionWireframeProtocol { presenter?.present(navigationController, animated: true) } } + + func showRouteDetails( + from view: ControllerBackedProtocol?, + quote: AssetExchangeQuote, + fee: AssetExchangeFee + ) { + guard + let routeDetailsView = SwapRouteDetailsViewFactory.createView( + for: quote, + fee: fee, + state: flowState + ) else { + return + } + + let navigationController = NovaNavigationController(rootViewController: routeDetailsView.controller) + + view?.controller.present(navigationController, animated: true) + } } diff --git a/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsInteractor.swift b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsInteractor.swift new file mode 100644 index 0000000000..a8e71ea760 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsInteractor.swift @@ -0,0 +1,7 @@ +import UIKit + +final class SwapRouteDetailsInteractor { + weak var presenter: SwapRouteDetailsInteractorOutputProtocol? +} + +extension SwapRouteDetailsInteractor: SwapRouteDetailsInteractorInputProtocol {} diff --git a/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsPresenter.swift b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsPresenter.swift new file mode 100644 index 0000000000..0e39fbdc7b --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsPresenter.swift @@ -0,0 +1,62 @@ +import Foundation +import SoraFoundation + +final class SwapRouteDetailsPresenter { + weak var view: SwapRouteDetailsViewProtocol? + let wireframe: SwapRouteDetailsWireframeProtocol + let interactor: SwapRouteDetailsInteractorInputProtocol + + let quote: AssetExchangeQuote + let fee: AssetExchangeFee + let prices: [ChainAssetId: PriceData] + let viewModelFactory: SwapRouteDetailsViewModelFactoryProtocol + + init( + interactor: SwapRouteDetailsInteractorInputProtocol, + wireframe: SwapRouteDetailsWireframeProtocol, + quote: AssetExchangeQuote, + fee: AssetExchangeFee, + prices: [ChainAssetId: PriceData], + viewModelFactory: SwapRouteDetailsViewModelFactoryProtocol, + localizationManager: LocalizationManagerProtocol + ) { + self.interactor = interactor + self.wireframe = wireframe + self.quote = quote + self.fee = fee + self.prices = prices + self.viewModelFactory = viewModelFactory + self.localizationManager = localizationManager + } + + private func provideViewModel() { + let viewModel = quote.metaOperations.enumerated().map { index, operation in + let fee = fee.operationFees[index] + + return viewModelFactory.createViewModel( + for: operation, + fee: fee, + prices: prices, + locale: selectedLocale + ) + } + + view?.didReceive(viewModel: viewModel) + } +} + +extension SwapRouteDetailsPresenter: SwapRouteDetailsPresenterProtocol { + func setup() { + provideViewModel() + } +} + +extension SwapRouteDetailsPresenter: SwapRouteDetailsInteractorOutputProtocol {} + +extension SwapRouteDetailsPresenter: Localizable { + func applyLocalization() { + if let view, view.isSetup { + provideViewModel() + } + } +} diff --git a/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsProtocols.swift b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsProtocols.swift new file mode 100644 index 0000000000..c69a97de82 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsProtocols.swift @@ -0,0 +1,13 @@ +protocol SwapRouteDetailsViewProtocol: ControllerBackedProtocol { + func didReceive(viewModel: SwapRouteDetailsViewModel) +} + +protocol SwapRouteDetailsPresenterProtocol: AnyObject { + func setup() +} + +protocol SwapRouteDetailsInteractorInputProtocol: AnyObject {} + +protocol SwapRouteDetailsInteractorOutputProtocol: AnyObject {} + +protocol SwapRouteDetailsWireframeProtocol: AnyObject {} diff --git a/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewController.swift b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewController.swift new file mode 100644 index 0000000000..284f290ea6 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewController.swift @@ -0,0 +1,57 @@ +import UIKit +import SoraFoundation + +final class SwapRouteDetailsViewController: UIViewController, ViewHolder { + typealias RootViewType = SwapRouteDetailsViewLayout + + let presenter: SwapRouteDetailsPresenterProtocol + + init(presenter: SwapRouteDetailsPresenterProtocol, localizationManager: LocalizationManagerProtocol) { + self.presenter = presenter + super.init(nibName: nil, bundle: nil) + + self.localizationManager = localizationManager + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + view = SwapRouteDetailsViewLayout() + } + + override func viewDidLoad() { + super.viewDidLoad() + + setupLocalization() + + presenter.setup() + } + + private func setupLocalization() { + rootView.titleView.bind( + topValue: R.string.localizable.swapsDetailsRoute( + preferredLanguages: selectedLocale.rLanguages + ), + bottomValue: R.string.localizable.swapRouteDetailsSubtitle( + preferredLanguages: selectedLocale.rLanguages + ) + ) + } +} + +extension SwapRouteDetailsViewController: SwapRouteDetailsViewProtocol { + func didReceive(viewModel: SwapRouteDetailsViewModel) { + rootView.routeDetailsView.bind(viewModel: viewModel) + } +} + +extension SwapRouteDetailsViewController: Localizable { + func applyLocalization() { + if isViewLoaded { + setupLocalization() + } + } +} diff --git a/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewFactory.swift b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewFactory.swift new file mode 100644 index 0000000000..eeacd18f34 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewFactory.swift @@ -0,0 +1,43 @@ +import Foundation +import SoraFoundation + +struct SwapRouteDetailsViewFactory { + static func createView( + for quote: AssetExchangeQuote, + fee: AssetExchangeFee, + state: SwapTokensFlowStateProtocol + ) -> SwapRouteDetailsViewProtocol? { + guard let currencyManager = CurrencyManager.shared else { return nil } + + let priceInfoFactory = PriceAssetInfoFactory(currencyManager: currencyManager) + + let interactor = SwapRouteDetailsInteractor() + let wireframe = SwapRouteDetailsWireframe() + + let prices = (try? state.assetListObservable.state.value.priceResult?.get()) ?? [:] + + let viewModelFactory = SwapRouteDetailsViewModelFactory( + priceAssetInfoFactory: PriceAssetInfoFactory(currencyManager: currencyManager) + ) + + let presenter = SwapRouteDetailsPresenter( + interactor: interactor, + wireframe: wireframe, + quote: quote, + fee: fee, + prices: prices, + viewModelFactory: viewModelFactory, + localizationManager: LocalizationManager.shared + ) + + let view = SwapRouteDetailsViewController( + presenter: presenter, + localizationManager: LocalizationManager.shared + ) + + presenter.view = view + interactor.presenter = presenter + + return view + } +} diff --git a/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewLayout.swift b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewLayout.swift new file mode 100644 index 0000000000..c8e4dd0fc3 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsViewLayout.swift @@ -0,0 +1,21 @@ +import UIKit + +final class SwapRouteDetailsViewLayout: ScrollableContainerLayoutView { + let titleView: MultiValueView = .create { view in + view.valueTop.apply(style: .boldTitle1Primary) + view.valueTop.textAlignment = .left + view.valueBottom.textAlignment = .left + view.valueBottom.apply(style: .regularSubhedlineSecondary) + view.valueBottom.numberOfLines = 0 + view.spacing = 8 + } + + let routeDetailsView = SwapRouteDetailsView() + + override func setupLayout() { + super.setupLayout() + + addArrangedSubview(titleView, spacingAfter: 24) + addArrangedSubview(routeDetailsView) + } +} diff --git a/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsWireframe.swift b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsWireframe.swift new file mode 100644 index 0000000000..fa9e9b95b2 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/SwapRouteDetailsWireframe.swift @@ -0,0 +1,3 @@ +import Foundation + +final class SwapRouteDetailsWireframe: SwapRouteDetailsWireframeProtocol {} diff --git a/novawallet/Modules/Swaps/RouteDetails/View/SwapRouteDetailsItemView.swift b/novawallet/Modules/Swaps/RouteDetails/View/SwapRouteDetailsItemView.swift new file mode 100644 index 0000000000..61c06b7721 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/View/SwapRouteDetailsItemView.swift @@ -0,0 +1,110 @@ +import UIKit + +final class SwapRouteDetailsItemView: GenericBorderedView { + override init(frame: CGRect) { + super.init(frame: frame) + + configure() + } + + private func configure() { + contentInsets = UIEdgeInsets(verticalInset: 12, horizontalInset: 16) + backgroundView.cornerRadius = 12 + } +} + +final class SwapRouteDetailsItemContent: GenericMultiValueView< + GenericPairValueView< + RouteView, + GenericTitleValueView> + > +> { + var titleLabel: UILabel { valueTop } + + var amountView: RouteView { valueBottom.fView } + + var feeView: UILabel { valueBottom.sView.titleView } + + var networkView: RouteView { valueBottom.sView.valueView } + + override init(frame: CGRect) { + super.init(frame: frame) + + configure() + } + + private func configure() { + titleLabel.apply(style: .regularSubhedlinePrimary) + titleLabel.textAlignment = .left + + feeView.apply(style: .caption1Secondary) + + spacing = 12 + valueBottom.setVerticalAndSpacing(12) + } + + private func configureAmountItemsConstraints(_ items: [AssetAmountRouteItemView]) { + items.dropLast().forEach { itemView in + itemView.amountLabel.setContentHuggingPriority(.high, for: .horizontal) + itemView.amountLabel.setContentCompressionResistancePriority(.high, for: .horizontal) + } + + guard let lastView = items.last else { return } + + lastView.amountLabel.setContentHuggingPriority(.low, for: .horizontal) + lastView.amountLabel.setContentCompressionResistancePriority(.low, for: .horizontal) + } + + private func configureAmountSeparatorConstraints(_ separators: [SwapRouteSeparatorView]) { + separators.forEach { separator in + separator.contentMode = .center + separator.setContentHuggingPriority(.required, for: .horizontal) + separator.setContentCompressionResistancePriority(.required, for: .horizontal) + } + } + + private func configureNetworkSeparatorConstraints(_ separators: [SwapRouteSeparatorView]) { + separators.forEach { separator in + separator.contentMode = .scaleAspectFit + + separator.snp.remakeConstraints { make in + make.width.equalTo(12) + } + } + } + + func bind(viewModel: ViewModel) { + titleLabel.text = viewModel.type + + amountView.bind( + items: viewModel.amountItems, + itemStyle: AssetAmountRouteItemView.Style( + imageSize: 24, + amountStyle: .semiboldBodyPrimary, + spacing: 4 + ), + separatorStyle: R.image.iconForward() + ) + + feeView.text = viewModel.fee + + networkView.bind( + items: viewModel.networkItems, + itemStyle: .caption1Secondary, + separatorStyle: R.image.iconForward() + ) + + configureAmountItemsConstraints(amountView.getItems()) + configureAmountSeparatorConstraints(amountView.getSeparators()) + configureNetworkSeparatorConstraints(networkView.getSeparators()) + } +} + +extension SwapRouteDetailsItemContent { + struct ViewModel { + let type: String + let amountItems: [AssetAmountRouteItemView.ViewModel] + let fee: String + let networkItems: [LabelRouteItemView.ViewModel] + } +} diff --git a/novawallet/Modules/Swaps/RouteDetails/View/SwapRouteDetailsView.swift b/novawallet/Modules/Swaps/RouteDetails/View/SwapRouteDetailsView.swift new file mode 100644 index 0000000000..4d9ca167ba --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/View/SwapRouteDetailsView.swift @@ -0,0 +1,102 @@ +import UIKit + +final class SwapRouteDetailsView: UIView { + private var itemListView: UIStackView? + private var stepsLineView: LinePatternView? + private var stepViews: [BorderedLabelView] = [] + + func bind(viewModel: SwapRouteDetailsViewModel) { + updateItemsView(for: viewModel) + updateLineView() + updateStepViews() + } +} + +private extension SwapRouteDetailsView { + func updateItemsView(for itemViewModels: SwapRouteDetailsViewModel) { + itemListView?.removeFromSuperview() + + let itemViews = itemViewModels.map { viewModel in + let itemView = SwapRouteDetailsItemView() + itemView.contentView.bind(viewModel: viewModel) + return itemView + } + + let itemsView = UIView.vStack( + alignment: .fill, + distribution: .fill, + spacing: 12, + margins: nil, + itemViews + ) + + addSubview(itemsView) + + itemsView.snp.makeConstraints { make in + make.top.bottom.equalToSuperview() + make.leading.equalToSuperview().inset(Constants.stepsContainerWidth + Constants.itemsHorOffset) + make.trailing.equalToSuperview() + } + + itemListView = itemsView + } + + func updateLineView() { + stepsLineView?.removeFromSuperview() + stepsLineView = nil + + guard + let items = itemListView?.arrangedSubviews, + let itemFirst = items.first, + let itemLast = items.last else { + return + } + + let lineView = LinePatternView() + addSubview(lineView) + + lineView.snp.makeConstraints { make in + make.leading.equalToSuperview() + make.width.equalTo(Constants.stepsContainerWidth) + make.top.equalTo(itemFirst.snp.top).offset(Constants.stepsTopOffset) + make.bottom.equalTo(itemLast.snp.top).offset(Constants.stepsTopOffset) + } + + stepsLineView = lineView + } + + func updateStepViews() { + stepViews.forEach { $0.removeFromSuperview() } + stepViews = [] + + guard let stepsLineView, let itemListView else { return } + + itemListView.arrangedSubviews.enumerated().forEach { index, itemView in + let stepView = BorderedLabelView() + stepView.apply(style: .stepNumber) + stepView.backgroundView.cornerRadius = Constants.stepWidth / 2 + stepView.titleLabel.text = String(index + 1) + stepView.titleLabel.textAlignment = .center + stepView.contentInsets = .zero + addSubview(stepView) + stepViews.append(stepView) + + stepView.snp.makeConstraints { make in + make.centerX.equalTo(stepsLineView) + make.width.height.equalTo(Constants.stepWidth) + make.top.equalTo(itemView).offset(Constants.stepsTopOffset) + } + } + } +} + +extension SwapRouteDetailsView { + enum Constants { + static let stepsContainerWidth: CGFloat = 24 + static let stepWidth: CGFloat = 20 + static let stepsTopOffset: CGFloat = 11 + static let itemsHorOffset: CGFloat = 18 + } +} + +typealias SwapRouteDetailsViewModel = [SwapRouteDetailsItemContent.ViewModel] diff --git a/novawallet/Modules/Swaps/RouteDetails/ViewModel/SwapRouteDetailsViewModelFactory.swift b/novawallet/Modules/Swaps/RouteDetails/ViewModel/SwapRouteDetailsViewModelFactory.swift new file mode 100644 index 0000000000..49f87006b9 --- /dev/null +++ b/novawallet/Modules/Swaps/RouteDetails/ViewModel/SwapRouteDetailsViewModelFactory.swift @@ -0,0 +1,163 @@ +import Foundation + +protocol SwapRouteDetailsViewModelFactoryProtocol { + func createViewModel( + for operation: AssetExchangeMetaOperationProtocol, + fee: AssetExchangeOperationFee, + prices: [ChainAssetId: PriceData], + locale: Locale + ) -> SwapRouteDetailsItemContent.ViewModel +} + +final class SwapRouteDetailsViewModelFactory { + let assetIconViewModelFactory: AssetIconViewModelFactoryProtocol + let balanceViewModelFacade: BalanceViewModelFactoryFacadeProtocol + let priceAssetInfoFactory: PriceAssetInfoFactoryProtocol + + init( + priceAssetInfoFactory: PriceAssetInfoFactoryProtocol, + assetIconViewModelFactory: AssetIconViewModelFactoryProtocol = AssetIconViewModelFactory() + ) { + self.priceAssetInfoFactory = priceAssetInfoFactory + balanceViewModelFacade = BalanceViewModelFactoryFacade(priceAssetInfoFactory: priceAssetInfoFactory) + self.assetIconViewModelFactory = assetIconViewModelFactory + } +} + +private extension SwapRouteDetailsViewModelFactory { + func createType( + from operation: AssetExchangeMetaOperationProtocol, + locale: Locale + ) -> String { + switch operation.label { + case .swap: + R.string.localizable.swapsLabelSwap(preferredLanguages: locale.rLanguages) + case .transfer: + R.string.localizable.swapsLabelTransfer(preferredLanguages: locale.rLanguages) + } + } + + func createAmountItem( + from chainAsset: ChainAsset, + amount: Balance, + locale: Locale + ) -> AssetAmountRouteItemView.ViewModel { + let assetDisplayInfo = chainAsset.assetDisplayInfo + + let imageViewModel = assetIconViewModelFactory.createAssetIconViewModel( + from: assetDisplayInfo + ) + + let amount = balanceViewModelFacade.amountFromValue( + targetAssetInfo: assetDisplayInfo, + value: amount.decimal(assetInfo: assetDisplayInfo) + ).value(for: locale) + + return AssetAmountRouteItemView.ViewModel(imageViewModel: imageViewModel, amount: amount) + } + + func createAmountItems( + from operation: AssetExchangeMetaOperationProtocol, + locale: Locale + ) -> [AssetAmountRouteItemView.ViewModel] { + switch operation.label { + case .swap: + [ + createAmountItem( + from: operation.assetIn, + amount: operation.amountIn, + locale: locale + ), + createAmountItem( + from: operation.assetOut, + amount: operation.amountOut, + locale: locale + ) + ] + case .transfer: + [ + createAmountItem( + from: operation.assetOut, + amount: operation.amountOut, + locale: locale + ) + ] + } + } + + func createNetworkItems(from operation: AssetExchangeMetaOperationProtocol) -> [LabelRouteItemView.ViewModel] { + switch operation.label { + case .swap: + [ + operation.assetIn.chain.name + ] + case .transfer: + [ + operation.assetIn.chain.name, + operation.assetOut.chain.name + ] + } + } + + func createFee( + from fee: AssetExchangeOperationFee, + chain: ChainModel, + prices: [ChainAssetId: PriceData], + locale: Locale + ) -> String { + let amounts = fee.groupedAmountByAsset() + + let totalAmountInFiat = amounts + .map { keyValue in + guard + keyValue.key.chainId == chain.chainId, + let chainAssetInfo = chain.chainAsset(for: keyValue.key.assetId)?.assetDisplayInfo else { + return 0 + } + + return Decimal.fiatValue( + from: keyValue.value, + price: prices[keyValue.key], + precision: chainAssetInfo.assetPrecision + ) + } + .reduce(Decimal(0)) { $1 + $0 } + + let assetDisplayInfo = priceAssetInfoFactory.createAssetBalanceDisplayInfo( + from: prices.first?.value.currencyId + ) + + let amount = balanceViewModelFacade.amountFromValue( + targetAssetInfo: assetDisplayInfo, + value: totalAmountInFiat + ).value(for: locale) + + return R.string.localizable.commonFeeAmountPrefixed( + amount, + preferredLanguages: locale.rLanguages + ) + } +} + +extension SwapRouteDetailsViewModelFactory: SwapRouteDetailsViewModelFactoryProtocol { + func createViewModel( + for operation: AssetExchangeMetaOperationProtocol, + fee: AssetExchangeOperationFee, + prices: [ChainAssetId: PriceData], + locale: Locale + ) -> SwapRouteDetailsItemContent.ViewModel { + let fee = createFee( + from: fee, + chain: operation.assetIn.chain, + prices: prices, + locale: locale + ) + + return SwapRouteDetailsItemContent.ViewModel( + type: createType(from: operation, locale: locale), + amountItems: createAmountItems(from: operation, locale: locale), + fee: fee, + networkItems: createNetworkItems(from: operation) + ) + } +} diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift index 29e01cf3d0..2d5ceb4208 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupPresenter.swift @@ -767,6 +767,18 @@ extension SwapSetupPresenter: SwapSetupPresenterProtocol { wireframe.showRateInfo(from: view) } + func showRouteDetails() { + guard let quote, let fee else { + return + } + + wireframe.showRouteDetails( + from: view, + quote: quote, + fee: fee + ) + } + func proceed() { guard let swapModel = getSwapModel() else { return diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift index a49b03d434..6ac6d604ee 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupProtocols.swift @@ -34,6 +34,7 @@ protocol SwapSetupPresenterProtocol: AnyObject { func showFeeInfo() func showRateInfo() func showSettings() + func showRouteDetails() func selectMaxPayAmount() func depositInsufficientToken() } @@ -86,6 +87,12 @@ protocol SwapSetupWireframeProtocol: SwapBaseWireframeProtocol, destinationChainAsset: ChainAsset, locale: Locale ) + + func showRouteDetails( + from view: ControllerBackedProtocol?, + quote: AssetExchangeQuote, + fee: AssetExchangeFee + ) } enum SwapSetupViewIssue: Equatable { diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift index f6c472fb22..c3e14035d9 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupViewController.swift @@ -76,6 +76,11 @@ final class SwapSetupViewController: UIViewController, ViewHolder { action: #selector(rateInfoAction), for: .touchUpInside ) + rootView.routeCell.addTarget( + self, + action: #selector(routeDetailsAction), + for: .touchUpInside + ) rootView.networkFeeCell.addTarget( self, action: #selector(networkFeeInfoAction), @@ -161,6 +166,10 @@ final class SwapSetupViewController: UIViewController, ViewHolder { presenter.showRateInfo() } + @objc private func routeDetailsAction() { + presenter.showRouteDetails() + } + @objc private func payMaxAction() { presenter.selectMaxPayAmount() } diff --git a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift index 0ddf3d8f7d..d3a480d11e 100644 --- a/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift +++ b/novawallet/Modules/Swaps/Setup/SwapSetupWireframe.swift @@ -179,4 +179,23 @@ final class SwapSetupWireframe: SwapSetupWireframeProtocol { view?.controller.present(navigationController, animated: true) } + + func showRouteDetails( + from view: ControllerBackedProtocol?, + quote: AssetExchangeQuote, + fee: AssetExchangeFee + ) { + guard + let routeDetailsView = SwapRouteDetailsViewFactory.createView( + for: quote, + fee: fee, + state: state + ) else { + return + } + + let navigationController = NovaNavigationController(rootViewController: routeDetailsView.controller) + + view?.controller.present(navigationController, animated: true) + } } diff --git a/novawallet/Modules/Swaps/View/AssetAmountRouteItemView.swift b/novawallet/Modules/Swaps/View/AssetAmountRouteItemView.swift new file mode 100644 index 0000000000..cbbb549c3a --- /dev/null +++ b/novawallet/Modules/Swaps/View/AssetAmountRouteItemView.swift @@ -0,0 +1,61 @@ +import Foundation +import SoraUI + +final class AssetAmountRouteItemView: AssetAmountView { + typealias ViewModel = AssetAmountRouteItemView.ItemViewModel + typealias Style = AssetAmountRouteItemView.ItemStyle + + private var imageSize: CGFloat = 24 { + didSet { + if imageSize != oldValue { + updateAssetIconLayout() + } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + + updateAssetIconLayout() + } +} + +private extension AssetAmountRouteItemView { + private func updateAssetIconLayout() { + assetIconView.snp.remakeConstraints { make in + make.size.equalTo(imageSize) + } + } +} + +extension AssetAmountRouteItemView { + struct ItemViewModel { + let imageViewModel: ImageViewModelProtocol? + let amount: String + } + + struct ItemStyle { + let imageSize: CGFloat + let amountStyle: UILabel.Style + let spacing: CGFloat + } +} + +extension AssetAmountRouteItemView: RouteItemViewProtocol { + func bind(routeItemViewModel: ViewModel) { + assetIconView.bind( + viewModel: routeItemViewModel.imageViewModel, size: CGSize(width: imageSize, height: imageSize) + ) + + amountLabel.text = routeItemViewModel.amount + } + + func apply(routeItemStyle: Style) { + imageSize = routeItemStyle.imageSize + + assetIconView.backgroundView.cornerRadius = imageSize / 2 + amountLabel.apply(style: routeItemStyle.amountStyle) + + setHorizontalAndSpacing(routeItemStyle.spacing) + } +} diff --git a/novawallet/Modules/Swaps/View/LabelRouteItemView.swift b/novawallet/Modules/Swaps/View/LabelRouteItemView.swift new file mode 100644 index 0000000000..2ee71d7fef --- /dev/null +++ b/novawallet/Modules/Swaps/View/LabelRouteItemView.swift @@ -0,0 +1,13 @@ +import UIKit + +final class LabelRouteItemView: UILabel, RouteItemViewProtocol { + typealias ViewModel = String + + func bind(routeItemViewModel: ViewModel) { + text = routeItemViewModel + } + + func apply(routeItemStyle: Style) { + apply(style: routeItemStyle) + } +} diff --git a/novawallet/Modules/Swaps/View/RouteView.swift b/novawallet/Modules/Swaps/View/RouteView.swift index 336bc25362..c4ecf6892e 100644 --- a/novawallet/Modules/Swaps/View/RouteView.swift +++ b/novawallet/Modules/Swaps/View/RouteView.swift @@ -97,6 +97,14 @@ final class RouteView: UIView { separatorViews.forEach { $0.apply(routeSeparatorStyle: separatorStyle) } } + func getItems() -> [I] { + itemViews + } + + func getSeparators() -> [S] { + separatorViews + } + private func setupLayout() { addSubview(stackView) stackView.snp.makeConstraints { make in diff --git a/novawallet/en.lproj/Localizable.strings b/novawallet/en.lproj/Localizable.strings index af332bd44a..f298fcbd2c 100644 --- a/novawallet/en.lproj/Localizable.strings +++ b/novawallet/en.lproj/Localizable.strings @@ -1779,4 +1779,7 @@ "swaps.execution.swap.failure" = "Failed on operation #%@ (%@)"; "swaps.label.crosschain" = "Cross-chain transfer"; "swaps.label.swap" = "Swap"; +"swaps.label.transfer" = "Transfer"; "swap.intermediate.too.low.amount.to.stay.abow.ed.message" = "During swap execution intermediate receive amount is %@ which is less than minimum balance of %@. Try specifying larger swap amount."; +"common.fee.amount.prefixed" = "Fee: %@"; +"swap.route.details.subtitle" = "The way that your token will take through different networks to get the desired token.";