Split fungible token view controller tabs to separate view controllers, add fungible token coordinator #5852

pull/5858/head
Krypto Pank 2 years ago
parent 264ee4ebd6
commit e2ac16d3a3
  1. 56
      AlphaWallet.xcodeproj/project.pbxproj
  2. 9
      AlphaWallet/Alerts/Coordinators/EditPriceAlertCoordinator.swift
  3. 30
      AlphaWallet/Alerts/ViewControllers/EditPriceAlertViewController.swift
  4. 172
      AlphaWallet/Alerts/ViewControllers/PriceAlertsViewController.swift
  5. 14
      AlphaWallet/Alerts/ViewModels/EditPriceAlertViewModel.swift
  6. 27
      AlphaWallet/Alerts/ViewModels/PriceAlertTableViewCellViewModel.swift
  7. 73
      AlphaWallet/Alerts/ViewModels/PriceAlertsPageViewModel.swift
  8. 77
      AlphaWallet/Alerts/Views/PriceAlertTableViewCell.swift
  9. 260
      AlphaWallet/Alerts/Views/PriceAlertsPageView.swift
  10. 142
      AlphaWallet/Common/ViewControllers/TopTabBarViewController.swift
  11. 8
      AlphaWallet/Common/Views/EmptyView.swift
  12. 6
      AlphaWallet/Tokens/Collectibles/ViewModels/NFTAssetViewModel.swift
  13. 105
      AlphaWallet/Tokens/Collectibles/Views/TokenInfoPageView.swift
  14. 212
      AlphaWallet/Tokens/Coordinators/FungibleTokenCoordinator.swift
  15. 86
      AlphaWallet/Tokens/Coordinators/SingleChainTokenCoordinator.swift
  16. 1
      AlphaWallet/Tokens/Coordinators/TokensCoordinator.swift
  17. 187
      AlphaWallet/Tokens/ViewControllers/FungibleTokenDetailsViewController.swift
  18. 94
      AlphaWallet/Tokens/ViewControllers/FungibleTokenTabViewController.swift
  19. 248
      AlphaWallet/Tokens/ViewControllers/FungibleTokenViewController.swift
  20. 300
      AlphaWallet/Tokens/ViewModels/FungibleTokenDetailsViewModel.swift
  21. 79
      AlphaWallet/Tokens/ViewModels/FungibleTokenTabViewModel.swift
  22. 208
      AlphaWallet/Tokens/ViewModels/FungibleTokenViewModel.swift
  23. 188
      AlphaWallet/Tokens/ViewModels/TokenInfoPageViewModel.swift
  24. 27
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Alerts/PriceAlertService.swift
  25. 52
      modules/AlphaWalletFoundation/AlphaWalletFoundation/Alerts/Types/PriceAlert.swift
  26. 4
      modules/AlphaWalletFoundation/AlphaWalletFoundation/CoinTicker/BaseCoinTickersFetcher.swift

@ -177,7 +177,7 @@
5E7C712B11D9D9AAA02C022F /* WalletConnectSessionCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7DC4B06C1A623788EEED /* WalletConnectSessionCellViewModel.swift */; };
5E7C7131E338A806132D989B /* DateEntryField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CD1FB7D353704EF3389 /* DateEntryField.swift */; };
5E7C7133FCB97894647E2628 /* SeedPhraseBackupIntroductionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C78CF45AA54EF8647C44B /* SeedPhraseBackupIntroductionViewController.swift */; };
5E7C713ACE8C72642B1C9F93 /* TokenInfoPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B7A45EDFA8ED1E25863 /* TokenInfoPageViewModel.swift */; };
5E7C713ACE8C72642B1C9F93 /* FungibleTokenDetailsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B7A45EDFA8ED1E25863 /* FungibleTokenDetailsViewModel.swift */; };
5E7C71570B651B3B56CAA1CC /* StringExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76D132F4BEA5CE4FFD0A /* StringExtensionTests.swift */; };
5E7C715B5921162B65A9A706 /* PromptViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C71ADDDAB65DEF4096A8D /* PromptViewController.swift */; };
5E7C71628CC710DE80590FF0 /* WalletQrCodeDonation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F4F9F57A4B1834EB723 /* WalletQrCodeDonation.swift */; };
@ -372,7 +372,6 @@
5E7C7D7F197980329CFB502D /* DappCommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75FF98048AC1BA905580 /* DappCommandTests.swift */; };
5E7C7D8173CB1089D622DA38 /* HelpViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7646352F10C96B5FC6F6 /* HelpViewCell.swift */; };
5E7C7DB7B431837C0D7031B1 /* ChooseSendPrivateTransactionsProviderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C73BF5CE15E6D7AFC3F0C /* ChooseSendPrivateTransactionsProviderViewModel.swift */; };
5E7C7DC485AD4401A3F6D071 /* FungibleTokenViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7EC53B2B5DFAAC7965EC /* FungibleTokenViewController.swift */; };
5E7C7DCE5242D2AC0A8DA65C /* TokenCardRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CAA3D0C19444005EA83 /* TokenCardRowViewModel.swift */; };
5E7C7E02785866606FF298F3 /* OpenSeaNonFungibleTokenViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72FBC0D2787AAA804098 /* OpenSeaNonFungibleTokenViewCellViewModel.swift */; };
5E7C7E04D4DDD7D8881A2AB1 /* ImportMagicLinkCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76AF81B8DFF605558499 /* ImportMagicLinkCoordinator.swift */; };
@ -389,7 +388,6 @@
5E7C7EAED92E4AE8B99217AB /* TransferTokensCardQuantitySelectionViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7021EE19C4B81CAAF3C0 /* TransferTokensCardQuantitySelectionViewControllerTests.swift */; };
5E7C7EB80D32A2F366E79140 /* SettingsHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75D384C0D727BB43305E /* SettingsHeaderView.swift */; };
5E7C7EB845B0EE96CC8DCF43 /* ServerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7FCE2427A30ACD860DF8 /* ServerViewModel.swift */; };
5E7C7EC8E473526629825ADA /* FungibleTokenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76B386CFE74302944BB0 /* FungibleTokenViewModel.swift */; };
5E7C7ECE164289A89734B4EF /* LocalesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76C895E7BFA47233068C /* LocalesCoordinator.swift */; };
5E7C7ED4612686DAD9B9D093 /* TokensCardViewControllerTitleHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C71C2C110B621EFDE336F /* TokensCardViewControllerTitleHeader.swift */; };
5E7C7EDA1BB781A45C1C19CD /* ImportWalletTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C73D26F24C4AAE981E2F2 /* ImportWalletTab.swift */; };
@ -615,6 +613,9 @@
8762002C266E150B0059B05A /* WalletTokenViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8762002B266E150B0059B05A /* WalletTokenViewCellViewModel.swift */; };
8762002E266E15310059B05A /* PopularTokenViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8762002D266E15310059B05A /* PopularTokenViewCellViewModel.swift */; };
8764BA4D292825D00054F574 /* SupportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8764BA4C292825D00054F574 /* SupportCoordinator.swift */; };
8764BA512929180E0054F574 /* PriceAlertTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8764BA502929180E0054F574 /* PriceAlertTableViewCell.swift */; };
8764BA53292918410054F574 /* PriceAlertTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8764BA52292918410054F574 /* PriceAlertTableViewCellViewModel.swift */; };
8764BA5529291B520054F574 /* FungibleTokenDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8764BA5429291B520054F574 /* FungibleTokenDetailsViewController.swift */; };
8765D6E1282BAD3A00529F45 /* FakeMultiWalletBalanceService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8765D6E0282BAD3A00529F45 /* FakeMultiWalletBalanceService.swift */; };
8765D6E3282BAD6300529F45 /* FakeNftProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8765D6E2282BAD6300529F45 /* FakeNftProvider.swift */; };
8765D6E9282BD64400529F45 /* FakeTokenSwapperNetworkProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8765D6E8282BD64400529F45 /* FakeTokenSwapperNetworkProvider.swift */; };
@ -626,9 +627,8 @@
8769BCA8256D15BF0095EA5B /* BlockieImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8769BCA7256D15BF0095EA5B /* BlockieImageView.swift */; };
876C80CD2673940B00B16595 /* SwitchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 876C80CC2673940B00B16595 /* SwitchView.swift */; };
87713EB0264BAB2500B1B9CB /* TokenPagesContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87713EAF264BAB2500B1B9CB /* TokenPagesContainerView.swift */; };
87713EB2264BAB4700B1B9CB /* TokenInfoPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87713EB1264BAB4700B1B9CB /* TokenInfoPageView.swift */; };
87713EB4264BAB5A00B1B9CB /* ActivityPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87713EB3264BAB5A00B1B9CB /* ActivityPageView.swift */; };
87713EB6264BAB6E00B1B9CB /* PriceAlertsPageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87713EB5264BAB6E00B1B9CB /* PriceAlertsPageView.swift */; };
87713EB6264BAB6E00B1B9CB /* PriceAlertsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87713EB5264BAB6E00B1B9CB /* PriceAlertsViewController.swift */; };
877239CB28F7DCE00062DC14 /* GoogleService-InfoTests.plist in Resources */ = {isa = PBXBuildFile; fileRef = 877239CA28F7DCE00062DC14 /* GoogleService-InfoTests.plist */; };
877239CE28F806820062DC14 /* WalletConnectSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 877239CD28F806820062DC14 /* WalletConnectSwift */; };
8772D77028D1EFB300615803 /* ServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8772D76F28D1EFB200615803 /* ServiceProvider.swift */; };
@ -707,6 +707,9 @@
87B1B64128CB16D50072A5E2 /* ScreenChecker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B1B64028CB16D50072A5E2 /* ScreenChecker.swift */; };
87B1B64428CB180A0072A5E2 /* ConfigureImageStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B1B64328CB180A0072A5E2 /* ConfigureImageStorage.swift */; };
87B1B64628CB19BD0072A5E2 /* KingfisherImageFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B1B64528CB19BD0072A5E2 /* KingfisherImageFetcher.swift */; };
87B3CC132929710C008DBA51 /* FungibleTokenCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B3CC122929710C008DBA51 /* FungibleTokenCoordinator.swift */; };
87B3CC15292A1017008DBA51 /* FungibleTokenTabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B3CC14292A1017008DBA51 /* FungibleTokenTabViewController.swift */; };
87B3CC17292A103E008DBA51 /* FungibleTokenTabViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B3CC16292A103E008DBA51 /* FungibleTokenTabViewModel.swift */; };
87B651F7256D4BFE000EF927 /* ClaimPaidOrderCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87B651F6256D4BFE000EF927 /* ClaimPaidOrderCoordinator.swift */; };
87B93B0E2726A46500F6EA73 /* BrowserStorageSubscription.js in Resources */ = {isa = PBXBuildFile; fileRef = 87B93B0D2726A46500F6EA73 /* BrowserStorageSubscription.js */; };
87B93B0F2726C59D00F6EA73 /* content.js in Resources */ = {isa = PBXBuildFile; fileRef = 8703FC0A270366DA0062C416 /* content.js */; };
@ -734,6 +737,7 @@
87C237DC26DE5303003CA387 /* UIToolbar+Custom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C237DB26DE5303003CA387 /* UIToolbar+Custom.swift */; };
87C237DE26DE5389003CA387 /* SelectAssetAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C237DD26DE5389003CA387 /* SelectAssetAmountView.swift */; };
87C650C325F2408E007B02CB /* ServerUnavailableCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C650C225F2408E007B02CB /* ServerUnavailableCoordinator.swift */; };
87C7B5CC29373FFB0012CCA7 /* TopTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C7B5CB29373FFB0012CCA7 /* TopTabBarViewController.swift */; };
87C8018C24350174007648CF /* AddHideTokenSectionHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87C8018B24350174007648CF /* AddHideTokenSectionHeaderViewModel.swift */; };
87CA8495253DDFF200BF8443 /* TransitionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CA8490253DDFF100BF8443 /* TransitionButton.swift */; };
87D163A2242CD811002662D2 /* AddHideTokensViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87D1639F242CD811002662D2 /* AddHideTokensViewModel.swift */; };
@ -1137,7 +1141,6 @@
5E7C768D3C3F400E0B88B575 /* ETH.tsml */ = {isa = PBXFileReference; lastKnownFileType = file.tsml; path = ETH.tsml; sourceTree = "<group>"; };
5E7C7695F7C45A31C7EAF97F /* AssetDefinitionsOverridesViewCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionsOverridesViewCellViewModel.swift; sourceTree = "<group>"; };
5E7C76AF81B8DFF605558499 /* ImportMagicLinkCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportMagicLinkCoordinator.swift; sourceTree = "<group>"; };
5E7C76B386CFE74302944BB0 /* FungibleTokenViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FungibleTokenViewModel.swift; sourceTree = "<group>"; };
5E7C76B3FD690DC23263DE26 /* DefaultActivityItemViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultActivityItemViewCell.swift; sourceTree = "<group>"; };
5E7C76C895E7BFA47233068C /* LocalesCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalesCoordinator.swift; sourceTree = "<group>"; };
5E7C76D132F4BEA5CE4FFD0A /* StringExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringExtensionTests.swift; sourceTree = "<group>"; };
@ -1208,7 +1211,7 @@
5E7C7B6380E6EB88AF8810CD /* ConfirmSignMessageViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConfirmSignMessageViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C7B6849D43348C5109712 /* DefaultActivityCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefaultActivityCellViewModel.swift; sourceTree = "<group>"; };
5E7C7B6A80324059B3EDA38C /* EnabledServersViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnabledServersViewController.swift; sourceTree = "<group>"; };
5E7C7B7A45EDFA8ED1E25863 /* TokenInfoPageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenInfoPageViewModel.swift; sourceTree = "<group>"; };
5E7C7B7A45EDFA8ED1E25863 /* FungibleTokenDetailsViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FungibleTokenDetailsViewModel.swift; sourceTree = "<group>"; };
5E7C7B82CC07F290B9CAA4E4 /* StatusViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusViewController.swift; sourceTree = "<group>"; };
5E7C7B8EB45ABD915B9BFF61 /* LocalPopularTokensCollectionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalPopularTokensCollectionTests.swift; sourceTree = "<group>"; };
5E7C7B8FD1E2BCC325DF4EE4 /* ServersCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServersCoordinator.swift; sourceTree = "<group>"; };
@ -1260,7 +1263,6 @@
5E7C7E9A5E7D36AA3BC108A4 /* EnsResolverTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnsResolverTests.swift; sourceTree = "<group>"; };
5E7C7EA385280B0BAB6F0745 /* TransactionViewModelTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionViewModelTests.swift; sourceTree = "<group>"; };
5E7C7EA7AD39A9F6703189FD /* ActiveWalletSessionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActiveWalletSessionView.swift; sourceTree = "<group>"; };
5E7C7EC53B2B5DFAAC7965EC /* FungibleTokenViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FungibleTokenViewController.swift; sourceTree = "<group>"; };
5E7C7EE374A74F2B00013C18 /* EthTokenViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EthTokenViewCell.swift; sourceTree = "<group>"; };
5E7C7EE467A7F5F2E5B1F660 /* TokensViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensViewModel.swift; sourceTree = "<group>"; };
5E7C7EEAAE9C23B68419E9F5 /* GenerateTransferMagicLinkViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GenerateTransferMagicLinkViewControllerViewModel.swift; sourceTree = "<group>"; };
@ -1489,6 +1491,9 @@
8762002B266E150B0059B05A /* WalletTokenViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletTokenViewCellViewModel.swift; sourceTree = "<group>"; };
8762002D266E15310059B05A /* PopularTokenViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PopularTokenViewCellViewModel.swift; sourceTree = "<group>"; };
8764BA4C292825D00054F574 /* SupportCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupportCoordinator.swift; sourceTree = "<group>"; };
8764BA502929180E0054F574 /* PriceAlertTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceAlertTableViewCell.swift; sourceTree = "<group>"; };
8764BA52292918410054F574 /* PriceAlertTableViewCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceAlertTableViewCellViewModel.swift; sourceTree = "<group>"; };
8764BA5429291B520054F574 /* FungibleTokenDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FungibleTokenDetailsViewController.swift; sourceTree = "<group>"; };
8765D6E0282BAD3A00529F45 /* FakeMultiWalletBalanceService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeMultiWalletBalanceService.swift; sourceTree = "<group>"; };
8765D6E2282BAD6300529F45 /* FakeNftProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeNftProvider.swift; sourceTree = "<group>"; };
8765D6E8282BD64400529F45 /* FakeTokenSwapperNetworkProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeTokenSwapperNetworkProvider.swift; sourceTree = "<group>"; };
@ -1500,9 +1505,8 @@
8769BCA7256D15BF0095EA5B /* BlockieImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockieImageView.swift; sourceTree = "<group>"; };
876C80CC2673940B00B16595 /* SwitchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwitchView.swift; sourceTree = "<group>"; };
87713EAF264BAB2500B1B9CB /* TokenPagesContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenPagesContainerView.swift; sourceTree = "<group>"; };
87713EB1264BAB4700B1B9CB /* TokenInfoPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TokenInfoPageView.swift; sourceTree = "<group>"; };
87713EB3264BAB5A00B1B9CB /* ActivityPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityPageView.swift; sourceTree = "<group>"; };
87713EB5264BAB6E00B1B9CB /* PriceAlertsPageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceAlertsPageView.swift; sourceTree = "<group>"; };
87713EB5264BAB6E00B1B9CB /* PriceAlertsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceAlertsViewController.swift; sourceTree = "<group>"; };
877239CA28F7DCE00062DC14 /* GoogleService-InfoTests.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "GoogleService-InfoTests.plist"; sourceTree = "<group>"; };
8772D76F28D1EFB200615803 /* ServiceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceProvider.swift; sourceTree = "<group>"; };
8772D77128D1F03600615803 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
@ -1582,6 +1586,9 @@
87B1B64028CB16D50072A5E2 /* ScreenChecker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScreenChecker.swift; sourceTree = "<group>"; };
87B1B64328CB180A0072A5E2 /* ConfigureImageStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigureImageStorage.swift; sourceTree = "<group>"; };
87B1B64528CB19BD0072A5E2 /* KingfisherImageFetcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KingfisherImageFetcher.swift; sourceTree = "<group>"; };
87B3CC122929710C008DBA51 /* FungibleTokenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FungibleTokenCoordinator.swift; sourceTree = "<group>"; };
87B3CC14292A1017008DBA51 /* FungibleTokenTabViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FungibleTokenTabViewController.swift; sourceTree = "<group>"; };
87B3CC16292A103E008DBA51 /* FungibleTokenTabViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FungibleTokenTabViewModel.swift; sourceTree = "<group>"; };
87B651F6256D4BFE000EF927 /* ClaimPaidOrderCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ClaimPaidOrderCoordinator.swift; sourceTree = "<group>"; };
87B93B0D2726A46500F6EA73 /* BrowserStorageSubscription.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = BrowserStorageSubscription.js; sourceTree = "<group>"; };
87B93B122726C75900F6EA73 /* helpers.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = helpers.js; sourceTree = "<group>"; };
@ -1604,6 +1611,7 @@
87C237DB26DE5303003CA387 /* UIToolbar+Custom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIToolbar+Custom.swift"; sourceTree = "<group>"; };
87C237DD26DE5389003CA387 /* SelectAssetAmountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectAssetAmountView.swift; sourceTree = "<group>"; };
87C650C225F2408E007B02CB /* ServerUnavailableCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerUnavailableCoordinator.swift; sourceTree = "<group>"; };
87C7B5CB29373FFB0012CCA7 /* TopTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TopTabBarViewController.swift; sourceTree = "<group>"; };
87C8018B24350174007648CF /* AddHideTokenSectionHeaderViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddHideTokenSectionHeaderViewModel.swift; sourceTree = "<group>"; };
87CA8490253DDFF100BF8443 /* TransitionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransitionButton.swift; sourceTree = "<group>"; };
87D1639F242CD811002662D2 /* AddHideTokensViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddHideTokensViewModel.swift; sourceTree = "<group>"; };
@ -2242,9 +2250,10 @@
5E7C778F20D32B70D7FF2135 /* TokenCardRedemptionInfoViewController.swift */,
5E7C7B3302309706CA0F972A /* TokensViewController.swift */,
5E7C7CC48CA7A1EA7D539C87 /* VerifiableStatusViewController.swift */,
5E7C7EC53B2B5DFAAC7965EC /* FungibleTokenViewController.swift */,
5E7C741DF75781BBB24CE6D0 /* TokenInstanceActionViewController.swift */,
874DED1624C1BB0E006C8FCE /* SelectTokenViewController.swift */,
8764BA5429291B520054F574 /* FungibleTokenDetailsViewController.swift */,
87B3CC14292A1017008DBA51 /* FungibleTokenTabViewController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
@ -2252,7 +2261,7 @@
2977CAF21F7E0C3A009682A0 /* ViewModels */ = {
isa = PBXGroup;
children = (
5E7C7B7A45EDFA8ED1E25863 /* TokenInfoPageViewModel.swift */,
5E7C7B7A45EDFA8ED1E25863 /* FungibleTokenDetailsViewModel.swift */,
87D1639F242CD811002662D2 /* AddHideTokensViewModel.swift */,
29E9CFCC1FE7343C00017744 /* NewTokenViewModel.swift */,
5E7C79ED9F842D3FC102AC54 /* FungibleTokenViewCellViewModel.swift */,
@ -2260,7 +2269,6 @@
5E7C7EE467A7F5F2E5B1F660 /* TokensViewModel.swift */,
5E7C74B82783A94091A43470 /* EthTokenViewCellViewModel.swift */,
5E7C72A3369125C89C869EE7 /* OpenSea */,
5E7C76B386CFE74302944BB0 /* FungibleTokenViewModel.swift */,
8782035E2431FBC300792F12 /* ShowAddHideTokensViewModel.swift */,
87C8018B24350174007648CF /* AddHideTokenSectionHeaderViewModel.swift */,
87D1757924ADAF07002130D2 /* BlockchainTagLabelViewModel.swift */,
@ -2269,6 +2277,7 @@
8762002D266E15310059B05A /* PopularTokenViewCellViewModel.swift */,
87757285282D486900EAD907 /* TokenCardWebViewModel.swift */,
87E1A5EE2837ACC700E98555 /* TokenHistoryChartViewModel.swift */,
87B3CC16292A103E008DBA51 /* FungibleTokenTabViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
@ -2547,6 +2556,7 @@
5E7C7A3D7408DC690C0F601C /* SingleChainTokenCoordinator.swift */,
874DED1424C1BAFF006C8FCE /* SelectTokenCoordinator.swift */,
87ED843A24C564B5001A3747 /* NewTokenCoordinator.swift */,
87B3CC122929710C008DBA51 /* FungibleTokenCoordinator.swift */,
);
path = Coordinators;
sourceTree = "<group>";
@ -3004,6 +3014,7 @@
5E7C764B98F526271E4C2A6A /* StaticHTMLViewController.swift */,
5E7C71ADDDAB65DEF4096A8D /* PromptViewController.swift */,
87509A5F26F8D63600D3EE85 /* ModalViewController.swift */,
87C7B5CB29373FFB0012CCA7 /* TopTabBarViewController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
@ -3606,6 +3617,7 @@
8712A37C26F314EF0009C376 /* ViewControllers */ = {
isa = PBXGroup;
children = (
87713EB5264BAB6E00B1B9CB /* PriceAlertsViewController.swift */,
8712A37E26F315190009C376 /* EditPriceAlertViewController.swift */,
);
path = ViewControllers;
@ -3614,7 +3626,7 @@
8712A38026F315420009C376 /* Views */ = {
isa = PBXGroup;
children = (
87713EB5264BAB6E00B1B9CB /* PriceAlertsPageView.swift */,
8764BA502929180E0054F574 /* PriceAlertTableViewCell.swift */,
);
path = Views;
sourceTree = "<group>";
@ -3624,6 +3636,7 @@
children = (
8712A38C26F476540009C376 /* EditPriceAlertViewModel.swift */,
8712A39126F476EF0009C376 /* PriceAlertsPageViewModel.swift */,
8764BA52292918410054F574 /* PriceAlertTableViewCellViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
@ -3830,7 +3843,6 @@
5E7C7828BD821B6F04B71C00 /* FungibleTokenHeaderView.swift */,
8727149B2822489200901B3E /* ContainerCollectionViewCell.swift */,
87713EAF264BAB2500B1B9CB /* TokenPagesContainerView.swift */,
87713EB1264BAB4700B1B9CB /* TokenInfoPageView.swift */,
879E1E3F264BDD5C006CD362 /* TokenHistoryChartView.swift */,
879E1E41264BFB9B006CD362 /* TokenHistoryPeriodSelectorView.swift */,
879E1E45264C072D006CD362 /* TokenInfoHeaderView.swift */,
@ -4994,6 +5006,7 @@
293B8B451F70A20200356286 /* TransactionViewCell.swift in Sources */,
872A98812864606700196EA3 /* SwapTransactionViewModel.swift in Sources */,
29F1C85D2003698A003780D8 /* WellDoneViewController.swift in Sources */,
87B3CC132929710C008DBA51 /* FungibleTokenCoordinator.swift in Sources */,
875CA8DA28BDEC030020FA48 /* UITraitCollection.swift in Sources */,
87F4D41C26C26C2000EFB9BC /* DropDownViewModel.swift in Sources */,
87897B9E25EF7C50006E3C75 /* ShowSeedPhraseIntroductionViewModel.swift in Sources */,
@ -5001,6 +5014,7 @@
296421951F70C1EC00EB363B /* LoadingView.swift in Sources */,
879F184426E73BFF000602F2 /* SingleNFTAssetSelectionView.swift in Sources */,
2996F14D1F6CA743005C33AE /* UIViewController.swift in Sources */,
87C7B5CC29373FFB0012CCA7 /* TopTabBarViewController.swift in Sources */,
875CA92428BF8B9E0020FA48 /* DeepLinkRequesterViewModel.swift in Sources */,
8793A4A0291A5DA100BCF849 /* CurrencyTableViewCellViewModel.swift in Sources */,
87E2555824F52EBF00F025F7 /* GasSpeedTableViewCellViewModel.swift in Sources */,
@ -5139,7 +5153,8 @@
87F21B89292232C400592706 /* KeyboardInitializer.swift in Sources */,
442FCE0DAE5527A93F54022C /* RedeemTokenCardQuantitySelectionViewController.swift in Sources */,
442FC0B59B23C0F3068621C0 /* NumberStepper.swift in Sources */,
87713EB6264BAB6E00B1B9CB /* PriceAlertsPageView.swift in Sources */,
8764BA53292918410054F574 /* PriceAlertTableViewCellViewModel.swift in Sources */,
87713EB6264BAB6E00B1B9CB /* PriceAlertsViewController.swift in Sources */,
442FCE2BEE8D475C7DEB39C1 /* RedeemTokenCardQuantitySelectionViewModel.swift in Sources */,
8739BBC726CD29510045CFED /* NFTAssetSelectionViewController.swift in Sources */,
442FCB9CF5BC243F0705F4FE /* TokenCardRedemptionViewController.swift in Sources */,
@ -5171,6 +5186,7 @@
879F184F26E73ECB000602F2 /* SendSemiFungibleTokenViewController.swift in Sources */,
87ED8FA92488E4430005C69B /* SendViewSectionHeader.swift in Sources */,
5E7C76F8CB67466725C590CE /* FungibleTokenViewCellViewModel.swift in Sources */,
8764BA5529291B520054F574 /* FungibleTokenDetailsViewController.swift in Sources */,
8722F86D25F79A4700293D89 /* UITableViewCell.swift in Sources */,
5E7C78407F6DCB0EDD562DF6 /* NonFungibleTokenViewCellViewModel.swift in Sources */,
5E7C7CE5CA19183FCED8C907 /* TokensViewModel.swift in Sources */,
@ -5199,10 +5215,11 @@
5E7C7C0FAC500A6651E663FD /* TransferTokensCardQuantitySelectionViewModel.swift in Sources */,
5E7C77AD9FAAC18211B6F355 /* TransferTokensCardQuantitySelectionViewController.swift in Sources */,
5E7C7EEE563D81793CB96FA0 /* TransferNFTCoordinator.swift in Sources */,
87B3CC17292A103E008DBA51 /* FungibleTokenTabViewModel.swift in Sources */,
5E7C7567A690B6B8F889AE83 /* SendViewController.swift in Sources */,
5E7C72E1D4B4B4C8443F3DA1 /* FungibleTokenHeaderView.swift in Sources */,
875E257E28CB1DEC001976F8 /* Coordinator.swift in Sources */,
5E7C713ACE8C72642B1C9F93 /* TokenInfoPageViewModel.swift in Sources */,
5E7C713ACE8C72642B1C9F93 /* FungibleTokenDetailsViewModel.swift in Sources */,
5E7C7376B566E5A59CC8F463 /* ImportMagicTokenViewControllerViewModel.swift in Sources */,
5E7C75E81F85353844CACECC /* EnterSellTokensCardPriceQuantityViewController.swift in Sources */,
5E7C7FCC321493B41C1083C1 /* EnterSellTokensCardPriceQuantityViewControllerViewModel.swift in Sources */,
@ -5277,8 +5294,8 @@
874DED1524C1BAFF006C8FCE /* SelectTokenCoordinator.swift in Sources */,
023286E2277D7960007D33C5 /* AddMultipleCustomRpcView.swift in Sources */,
5E7C77D5AD018763345D8DD5 /* CanOpenContractWebPage.swift in Sources */,
87B3CC15292A1017008DBA51 /* FungibleTokenTabViewController.swift in Sources */,
5E7C7F1B297CE042114EF095 /* LockEnterPasscodeViewController.swift in Sources */,
87713EB2264BAB4700B1B9CB /* TokenInfoPageView.swift in Sources */,
5E7C71DC13B2040F5408BF3C /* ImportMagicTokenCardRowViewModel.swift in Sources */,
5E7C77BFA252C7AA63BA5B90 /* TokenCardRowView.swift in Sources */,
87C650C325F2408E007B02CB /* ServerUnavailableCoordinator.swift in Sources */,
@ -5286,6 +5303,7 @@
879E1E40264BDD5C006CD362 /* TokenHistoryChartView.swift in Sources */,
87E2555424F52E9500F025F7 /* GasSpeedTableViewHeaderView.swift in Sources */,
5E7C74C1C2AB84F9AFAC630E /* TokenCardRowViewModelProtocol.swift in Sources */,
8764BA512929180E0054F574 /* PriceAlertTableViewCell.swift in Sources */,
872A987D2864601A00196EA3 /* SpeedupTransactionViewModel.swift in Sources */,
87F2F0C128448EB700CE3B94 /* WalletConnectV2Storage.swift in Sources */,
874D099825EE33E000A58EF2 /* SelfResizedTextView.swift in Sources */,
@ -5375,8 +5393,6 @@
5E7C76D28BB14C7685296BEF /* DappsHomeEmptyViewModel.swift in Sources */,
5E7C741CA88EFAC66756DE7F /* EditBookmarkViewModel.swift in Sources */,
87584B4125EF911D0070063B /* ShowSeedPhraseCoordinator.swift in Sources */,
5E7C7DC485AD4401A3F6D071 /* FungibleTokenViewController.swift in Sources */,
5E7C7EC8E473526629825ADA /* FungibleTokenViewModel.swift in Sources */,
87ED8F93248534E30005C69B /* SwitchTableViewCell.swift in Sources */,
8728FFDF27428FEA008E5524 /* TokenAttributeViewModel.swift in Sources */,
5E7C72EECA8154CEB7D9F46C /* ContainerViewWithShadow.swift in Sources */,

@ -10,17 +10,17 @@ import AlphaWalletFoundation
protocol EditPriceAlertCoordinatorDelegate: class {
func didClose(in coordinator: EditPriceAlertCoordinator)
func didUpdateAlert(in coordinator: EditPriceAlertCoordinator)
}
class EditPriceAlertCoordinator: Coordinator {
var coordinators: [Coordinator] = []
private let configuration: EditPriceAlertViewModel.Configuration
private let navigationController: UINavigationController
private let token: Token
private let session: WalletSession
private let alertService: PriceAlertServiceType
private let tokensService: TokenViewModelState
var coordinators: [Coordinator] = []
weak var delegate: EditPriceAlertCoordinatorDelegate?
init(navigationController: UINavigationController, configuration: EditPriceAlertViewModel.Configuration, token: Token, session: WalletSession, tokensService: TokenViewModelState, alertService: PriceAlertServiceType) {
@ -37,6 +37,7 @@ class EditPriceAlertCoordinator: Coordinator {
let viewController = EditPriceAlertViewController(viewModel: viewModel)
viewController.delegate = self
viewController.hidesBottomBarWhenPushed = true
viewController.navigationItem.largeTitleDisplayMode = .never
navigationController.pushViewController(viewController, animated: true)
}
@ -50,9 +51,7 @@ extension EditPriceAlertCoordinator: EditPriceAlertViewControllerDelegate {
func didUpdateAlert(in viewController: EditPriceAlertViewController) {
navigationController.popViewController(animated: true)
delegate?.didUpdateAlert(in: self)
delegate?.didClose(in: self)
}
}

@ -18,7 +18,7 @@ class EditPriceAlertViewController: UIViewController {
private lazy var headerView: SendViewSectionHeader = {
let view = SendViewSectionHeader()
view.configure(viewModel: .init(text: viewModel.headerTitle))
view.configure(viewModel: .init(text: R.string.localizable.priceAlertEnterTargetPrice().uppercased()))
return view
}()
@ -41,15 +41,21 @@ class EditPriceAlertViewController: UIViewController {
return textField
}()
private let buttonsBar = HorizontalButtonsBar(configuration: .primary(buttons: 1))
private let buttonsBar: HorizontalButtonsBar = {
let buttonsBar = HorizontalButtonsBar(configuration: .primary(buttons: 1))
buttonsBar.configure()
return buttonsBar
}()
private lazy var containerView: ScrollableStackView = {
let view = ScrollableStackView()
return view
}()
private let viewModel: EditPriceAlertViewModel
private let appear = PassthroughSubject<Void, Never>()
private let willAppear = PassthroughSubject<Void, Never>()
private var cancelable = Set<AnyCancellable>()
private var saveButton: UIButton { return buttonsBar.buttons[0] }
weak var delegate: EditPriceAlertViewControllerDelegate?
@ -78,26 +84,26 @@ class EditPriceAlertViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
buttonsBar.configure()
buttonsBar.buttons[0].setTitle(viewModel.setAlertTitle, for: .normal)
saveButton.setTitle(R.string.localizable.priceAlertSet(), for: .normal)
view.backgroundColor = Configuration.Color.Semantic.defaultViewBackground
bind(viewModel: viewModel)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
appear.send(())
willAppear.send(())
}
private func bind(viewModel: EditPriceAlertViewModel) {
view.backgroundColor = viewModel.backgroundColor
navigationItem.title = viewModel.title
containerView.configure(viewModel: .init(backgroundColor: viewModel.backgroundColor))
let save = buttonsBar.buttons[0].publisher(forEvent: .touchUpInside).eraseToAnyPublisher()
let input = EditPriceAlertViewModelInput(
willAppear: willAppear.eraseToAnyPublisher(),
save: saveButton.publisher(forEvent: .touchUpInside).eraseToAnyPublisher(),
cryptoValue: amountTextField.cryptoValuePublisher)
let input = EditPriceAlertViewModelInput(appear: appear.eraseToAnyPublisher(), save: save, cryptoValue: amountTextField.cryptoValuePublisher)
let output = viewModel.transform(input: input)
output.cryptoToFiatRate
@ -113,7 +119,7 @@ class EditPriceAlertViewController: UIViewController {
.store(in: &cancelable)
output.isEnabled
.sink { [weak buttonsBar] in buttonsBar?.buttons[0].isEnabled = $0 }
.sink { [weak self] in self?.saveButton.isEnabled = $0 }
.store(in: &cancelable)
output.createOrUpdatePriceAlert

@ -0,0 +1,172 @@
//
// PriceAlertsViewController.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 12.05.2021.
//
import UIKit
import AlphaWalletFoundation
import StatefulViewController
import Combine
protocol PriceAlertsViewControllerDelegate: class {
func editAlertSelected(in viewController: PriceAlertsViewController, alert: PriceAlert)
func addAlertSelected(in viewController: PriceAlertsViewController)
}
class PriceAlertsViewController: UIViewController {
private let viewModel: PriceAlertsViewModel
private lazy var dataSource = makeDataSource()
private lazy var tableView: UITableView = {
var tableView = UITableView.grouped
tableView.register(PriceAlertTableViewCell.self)
tableView.delegate = self
return tableView
}()
private let updateAlert = PassthroughSubject<(value: Bool, indexPath: IndexPath), Never>()
private let removeAlert = PassthroughSubject<IndexPath, Never>()
private var cancelable = Set<AnyCancellable>()
private lazy var addNotificationView: AddHideTokensView = {
let view = AddHideTokensView()
view.delegate = self
return view
}()
weak var delegate: PriceAlertsViewControllerDelegate?
init(viewModel: PriceAlertsViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
let stackView = [
addNotificationView,
UIView.separator(),
tableView
].asStackView(axis: .vertical)
stackView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stackView)
NSLayoutConstraint.activate([
stackView.anchorsConstraint(to: view),
addNotificationView.heightAnchor.constraint(equalToConstant: DataEntry.Metric.Tokens.Filter.height)
])
emptyView = EmptyView.priceAlertsEmptyView()
}
required init?(coder: NSCoder) {
return nil
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Configuration.Color.Semantic.defaultViewBackground
bind(viewModel: viewModel)
}
private func bind(viewModel: PriceAlertsViewModel) {
let input = PriceAlertsViewModelInput(
updateAlert: updateAlert.eraseToAnyPublisher(),
removeAlert: removeAlert.eraseToAnyPublisher())
let outout = viewModel.transform(input: input)
outout.viewState
.sink { [weak self, dataSource, addNotificationView] viewState in
dataSource.apply(viewState.snapshot, animatingDifferences: viewState.animatingDifferences)
addNotificationView.configure(viewModel: viewState.addNewAlertViewModel)
self?.endLoading()
}.store(in: &cancelable)
}
}
extension PriceAlertsViewController: StatefulViewController {
func hasContent() -> Bool {
return dataSource.snapshot().numberOfItems > 0
}
}
extension PriceAlertsViewController {
private func makeDataSource() -> PriceAlertsViewModel.DataSource {
PriceAlertsViewModel.DataSource(tableView: tableView) { tableView, indexPath, viewModel -> PriceAlertTableViewCell in
let cell: PriceAlertTableViewCell = tableView.dequeueReusableCell(for: indexPath)
cell.delegate = self
cell.configure(viewModel: viewModel)
return cell
}
}
}
extension PriceAlertsViewController: PriceAlertTableViewCellDelegate {
func cell(_ cell: PriceAlertTableViewCell, didToggle value: Bool, indexPath: IndexPath) {
updateAlert.send((value, indexPath))
}
}
extension PriceAlertsViewController: UITableViewDelegate {
//Hide the footer
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
.leastNormalMagnitude
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
nil
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
.leastNormalMagnitude
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
nil
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
delegate?.editAlertSelected(in: self, alert: dataSource.item(at: indexPath).alert)
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
return trailingSwipeActionsConfiguration(forRowAt: indexPath)
}
func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .delete
}
private func trailingSwipeActionsConfiguration(forRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let title = R.string.localizable.delete()
let hideAction = UIContextualAction(style: .destructive, title: title) { [removeAlert, dataSource] (_, _, completion) in
var snapshot = dataSource.snapshot()
let item = snapshot.itemIdentifiers(inSection: snapshot.sectionIdentifiers[indexPath.section])[indexPath.row]
snapshot.deleteItems([item])
dataSource.apply(snapshot, animatingDifferences: true)
removeAlert.send(indexPath)
completion(true)
}
hideAction.backgroundColor = R.color.danger()
hideAction.image = R.image.hideToken()
let configuration = UISwipeActionsConfiguration(actions: [hideAction])
configuration.performsFirstActionWithFullSwipe = true
return configuration
}
}
extension PriceAlertsViewController: AddHideTokensViewDelegate {
func view(_ view: AddHideTokensView, didSelectAddHideTokensButton sender: UIButton) {
delegate?.addAlertSelected(in: self)
}
}

@ -10,7 +10,7 @@ import AlphaWalletFoundation
import Combine
struct EditPriceAlertViewModelInput {
let appear: AnyPublisher<Void, Never>
let willAppear: AnyPublisher<Void, Never>
let save: AnyPublisher<Void, Never>
let cryptoValue: AnyPublisher<String, Never>
}
@ -31,10 +31,7 @@ final class EditPriceAlertViewModel {
private let alertService: PriceAlertServiceType
private var cancelable = Set<AnyCancellable>()
var backgroundColor: UIColor = Colors.appWhite
var title: String { configuration.title }
var headerTitle: String = R.string.localizable.priceAlertEnterTargetPrice().uppercased()
var setAlertTitle: String = R.string.localizable.priceAlertSet()
let token: Token
init(configuration: EditPriceAlertViewModel.Configuration, token: Token, tokensService: TokenViewModelState, alertService: PriceAlertServiceType) {
@ -71,12 +68,12 @@ final class EditPriceAlertViewModel {
switch configuration {
case .create:
let alert: PriceAlert = .init(type: .init(value: pair.crypto, marketPrice: pair.marketPrice), token: token, isEnabled: true)
alertService.add(alert: alert)
guard alertService.add(alert: alert) else { return .failure(.alertAlreadyExists) }
return .success(())
case .edit(let alert):
alertService.update(alert: alert, update: .value(value: pair.crypto, marketPrice: pair.marketPrice))
return .success(())
}
return .success(())
}.eraseToAnyPublisher()
let marketPrice = cryptoRate
@ -87,7 +84,7 @@ final class EditPriceAlertViewModel {
.map { $0 != nil }
.eraseToAnyPublisher()
let cryptoInitial = input.appear
let cryptoInitial = input.willAppear
.map { _ in self.configuration.value }
.eraseToAnyPublisher()
@ -98,6 +95,7 @@ final class EditPriceAlertViewModel {
extension EditPriceAlertViewModel {
enum EditPriceAlertError: Error {
case cryptoOrMarketPriceNotFound
case alertAlreadyExists
}
enum Configuration {

@ -0,0 +1,27 @@
//
// PriceAlertTableViewCellViewModel.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 19.11.2022.
//
import UIKit
import AlphaWalletFoundation
struct PriceAlertTableViewCellViewModel: Hashable {
let alert: PriceAlert
let titleAttributedString: NSAttributedString
let icon: UIImage?
let isSelected: Bool
init(alert: PriceAlert) {
self.alert = alert
titleAttributedString = .init(string: alert.title, attributes: [
.font: Fonts.regular(size: 17),
.foregroundColor: Configuration.Color.Semantic.defaultForegroundText
])
icon = alert.icon
isSelected = alert.isEnabled
}
}

@ -1,5 +1,5 @@
//
// PriceAlertsPageViewModel.swift
// PriceAlertsViewModel.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 17.09.2021.
@ -7,23 +7,74 @@
import UIKit
import AlphaWalletFoundation
import Combine
struct PriceAlertsPageViewModel {
var title: String { return R.string.localizable.priceAlertNavigationTitle() }
struct PriceAlertsViewModelInput {
let updateAlert: AnyPublisher<(value: Bool, indexPath: IndexPath), Never>
let removeAlert: AnyPublisher<IndexPath, Never>
}
struct PriceAlertsViewModelOutput {
let viewState: AnyPublisher<PriceAlertsViewModel.ViewState, Never>
}
class PriceAlertsViewModel {
private let alertService: PriceAlertServiceType
private let token: Token
private var cancelable = Set<AnyCancellable>()
init(alertService: PriceAlertServiceType, token: Token) {
self.alertService = alertService
self.token = token
}
func transform(input: PriceAlertsViewModelInput) -> PriceAlertsViewModelOutput {
input.removeAlert
.sink { [alertService] in alertService.remove(indexPath: $0) }
.store(in: &cancelable)
input.updateAlert
.sink { [alertService] in alertService.update(indexPath: $0.indexPath, update: .enabled($0.value)) }
.store(in: &cancelable)
let viewState = alertService.alertsPublisher(forStrategy: .token(token))
.map { $0.map { PriceAlertTableViewCellViewModel(alert: $0) } }
.map { [SectionViewModel(section: .alerts, views: $0)] }
.map { ViewState(snapshot: self.buildSnapshot(for: $0)) }
.eraseToAnyPublisher()
return .init(viewState: viewState)
}
private func buildSnapshot(for viewModels: [SectionViewModel]) -> PriceAlertsViewModel.Snapshot {
var snapshot = PriceAlertsViewModel.Snapshot()
let sections = viewModels.map { $0.section }
snapshot.appendSections(sections)
for each in viewModels {
snapshot.appendItems(each.views, toSection: each.section)
}
return snapshot
}
}
var backgroundColor: UIColor = Colors.appWhite
var alerts: [PriceAlert]
extension PriceAlertsViewModel {
class DataSource: UITableViewDiffableDataSource<PriceAlertsViewModel.Section, PriceAlertTableViewCellViewModel> {}
typealias Snapshot = NSDiffableDataSourceSnapshot<PriceAlertsViewModel.Section, PriceAlertTableViewCellViewModel>
init(alerts: [PriceAlert]?) {
self.alerts = alerts ?? []
enum Section: Int, Hashable, CaseIterable {
case alerts
}
var addNewAlertViewModel: ShowAddHideTokensViewModel {
return .init(addHideTokensIcon: R.image.add_hide_tokens(), addHideTokensTitle: R.string.localizable.priceAlertNewAlert(), backgroundColor: R.color.alabaster()!, badgeText: nil)
struct SectionViewModel {
let section: Section
let views: [PriceAlertTableViewCellViewModel]
}
mutating func removeAlert(indexPath: IndexPath) {
alerts.remove(at: indexPath.row)
struct ViewState {
let animatingDifferences: Bool = false
let snapshot: Snapshot
let addNewAlertViewModel = ShowAddHideTokensViewModel(addHideTokensIcon: R.image.add_hide_tokens(), addHideTokensTitle: R.string.localizable.priceAlertNewAlert(), backgroundColor: Configuration.Color.Semantic.tableViewHeaderBackground, badgeText: nil)
}
}

@ -0,0 +1,77 @@
//
// PriceAlertTableViewCell.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 19.11.2022.
//
import UIKit
protocol PriceAlertTableViewCellDelegate: class {
func cell(_ cell: PriceAlertTableViewCell, didToggle value: Bool, indexPath: IndexPath)
}
class PriceAlertTableViewCell: UITableViewCell {
private lazy var iconImageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private lazy var switchButton: UISwitch = {
let button = UISwitch()
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
weak var delegate: PriceAlertTableViewCellDelegate?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
separatorInset = .zero
let stackView = [
.spacerWidth(16),
iconImageView,
.spacerWidth(16),
titleLabel,
.spacerWidth(16, flexible: true),
switchButton,
.spacerWidth(16),
].asStackView(axis: .horizontal, alignment: .center)
stackView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stackView)
NSLayoutConstraint.activate([
iconImageView.heightAnchor.constraint(equalToConstant: 18),
iconImageView.widthAnchor.constraint(equalToConstant: 18),
stackView.anchorsConstraint(to: contentView, edgeInsets: .init(top: 14, left: 0, bottom: 14, right: 0))
])
switchButton.addTarget(self, action: #selector(toggleSelectionState), for: .valueChanged)
}
required init?(coder: NSCoder) {
return nil
}
func configure(viewModel: PriceAlertTableViewCellViewModel) {
iconImageView.image = viewModel.icon
titleLabel.attributedText = viewModel.titleAttributedString
switchButton.isEnabled = viewModel.isSelected
}
@objc private func toggleSelectionState(_ sender: UISwitch) {
guard let indexPath = indexPath else { return }
delegate?.cell(self, didToggle: sender.isEnabled, indexPath: indexPath)
}
}

@ -1,260 +0,0 @@
//
// PriceAlertsPageView.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 12.05.2021.
//
import UIKit
import AlphaWalletFoundation
protocol PriceAlertsPageViewDelegate: class {
func editAlertSelected(in view: PriceAlertsPageView, alert: PriceAlert)
func addAlertSelected(in view: PriceAlertsPageView)
func removeAlert(in view: PriceAlertsPageView, indexPath: IndexPath)
func updateAlert(in view: PriceAlertsPageView, value: Bool, indexPath: IndexPath)
}
class PriceAlertsPageView: UIView, PageViewType {
var title: String { viewModel.title }
var rightBarButtonItem: UIBarButtonItem?
private var viewModel: PriceAlertsPageViewModel
private lazy var tableView: UITableView = {
var tableView = UITableView.grouped
tableView.register(PriceAlertTableViewCell.self)
tableView.delegate = self
tableView.dataSource = self
return tableView
}()
private lazy var statefulView: StatefulView<UITableView> = {
return .init(subview: tableView)
}()
private lazy var addNotificationView: AddHideTokensView = {
let view = AddHideTokensView()
view.delegate = self
view.configure(viewModel: viewModel.addNewAlertViewModel)
return view
}()
weak var delegate: PriceAlertsPageViewDelegate?
init(viewModel: PriceAlertsPageViewModel) {
self.viewModel = viewModel
super.init(frame: .zero)
translatesAutoresizingMaskIntoConstraints = false
let stackView = [
addNotificationView.embededWithSeparator(top: 0),
statefulView
].asStackView(axis: .vertical)
stackView.translatesAutoresizingMaskIntoConstraints = false
addSubview(stackView)
NSLayoutConstraint.activate([
stackView.anchorsConstraintSafeArea(to: self),
addNotificationView.heightAnchor.constraint(equalToConstant: DataEntry.Metric.Tokens.Filter.height)
])
statefulView.emptyView = EmptyView.activitiesEmptyView()
}
required init?(coder: NSCoder) {
return nil
}
func configure(viewModel: PriceAlertsPageViewModel) {
self.viewModel = viewModel
reloadData()
statefulView.endLoading()
}
deinit {
statefulView.resetStatefulStateToReleaseObjectToAvoidMemoryLeak()
}
func reloadData() {
tableView.reloadData()
}
}
extension PriceAlertsPageView: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
//Hide the footer
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
.leastNormalMagnitude
}
func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
nil
}
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
.leastNormalMagnitude
}
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
nil
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.alerts.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: PriceAlertTableViewCell = tableView.dequeueReusableCell(for: indexPath)
cell.delegate = self
cell.configure(viewModel: .init(alert: viewModel.alerts[indexPath.row]))
return cell
}
}
extension PriceAlertsPageView: PriceAlertTableViewCellDelegate {
func cell(_ cell: PriceAlertTableViewCell, didToggle value: Bool, indexPath: IndexPath) {
delegate?.updateAlert(in: self, value: value, indexPath: indexPath)
}
}
extension PriceAlertsPageView: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
let alert = viewModel.alerts[indexPath.row]
delegate?.editAlertSelected(in: self, alert: alert)
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
return trailingSwipeActionsConfiguration(forRowAt: indexPath)
}
private func trailingSwipeActionsConfiguration(forRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
let title = R.string.localizable.delete()
let hideAction = UIContextualAction(style: .destructive, title: title) { [weak self] (_, _, completionHandler) in
guard let strongSelf = self else { return }
strongSelf.viewModel.removeAlert(indexPath: indexPath)
strongSelf.tableView.deleteRows(at: [indexPath], with: .automatic)
strongSelf.statefulView.endLoading()
// NOTE: small delay for correct remove animation
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
strongSelf.delegate?.removeAlert(in: strongSelf, indexPath: indexPath)
}
completionHandler(true)
}
hideAction.backgroundColor = R.color.danger()
hideAction.image = R.image.hideToken()
let configuration = UISwipeActionsConfiguration(actions: [hideAction])
configuration.performsFirstActionWithFullSwipe = true
return configuration
}
}
extension PriceAlertsPageView: AddHideTokensViewDelegate {
func view(_ view: AddHideTokensView, didSelectAddHideTokensButton sender: UIButton) {
delegate?.addAlertSelected(in: self)
}
}
protocol PriceAlertTableViewCellDelegate: class {
func cell(_ cell: PriceAlertsPageView.PriceAlertTableViewCell, didToggle value: Bool, indexPath: IndexPath)
}
extension PriceAlertsPageView {
struct PriceAlertTableViewCellViewModel {
let titleAttributedString: NSAttributedString
let icon: UIImage?
let isSelected: Bool
init(alert: PriceAlert) {
titleAttributedString = .init(string: alert.title, attributes: [
.font: Fonts.regular(size: 17),
.foregroundColor: Colors.black
])
icon = alert.icon
isSelected = alert.isEnabled
}
}
class PriceAlertTableViewCell: UITableViewCell {
private lazy var iconImageView: UIImageView = {
let imageView = UIImageView()
imageView.translatesAutoresizingMaskIntoConstraints = false
return imageView
}()
private lazy var titleLabel: UILabel = {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
return label
}()
private lazy var switchButton: UISwitch = {
let button = UISwitch()
button.translatesAutoresizingMaskIntoConstraints = false
return button
}()
weak var delegate: PriceAlertTableViewCellDelegate?
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
separatorInset = .zero
let stackView = [
.spacerWidth(16),
iconImageView,
.spacerWidth(16),
titleLabel,
.spacerWidth(16, flexible: true),
switchButton,
.spacerWidth(16),
].asStackView(axis: .horizontal, alignment: .center)
stackView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(stackView)
NSLayoutConstraint.activate([
iconImageView.heightAnchor.constraint(equalToConstant: 18),
iconImageView.widthAnchor.constraint(equalToConstant: 18),
stackView.anchorsConstraint(to: contentView, edgeInsets: .init(top: 14, left: 0, bottom: 14, right: 0))
])
switchButton.addTarget(self, action: #selector(toggleSelectionState), for: .valueChanged)
}
required init?(coder: NSCoder) {
return nil
}
func configure(viewModel: PriceAlertTableViewCellViewModel) {
iconImageView.image = viewModel.icon
titleLabel.attributedText = viewModel.titleAttributedString
switchButton.isEnabled = viewModel.isSelected
}
@objc private func toggleSelectionState(_ sender: UISwitch) {
guard let indexPath = indexPath else { return }
delegate?.cell(self, didToggle: sender.isEnabled, indexPath: indexPath)
}
}
}

@ -0,0 +1,142 @@
//
// TopTabBarViewController.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 30.11.2022.
//
import UIKit
protocol TopTabBarViewControllerDelegate: class {
func viewController(_ viewController: TopTabBarViewController, didSelectPage index: Int)
}
class TopTabBarViewController: UIViewController {
private lazy var tabBar: ScrollableSegmentedControl = {
let cellConfiguration = Style.ScrollableSegmentedControlCell.configuration
let controlConfiguration = Style.ScrollableSegmentedControl.configuration
let cells = titles.map { title in
ScrollableSegmentedControlCell(frame: .zero, title: title, configuration: cellConfiguration)
}
let control = ScrollableSegmentedControl(cells: cells, configuration: controlConfiguration)
control.setSelection(cellIndex: selectedIndex)
control.addTarget(self, action: #selector(didTapSegment), for: .touchUpInside)
return control
}()
private lazy var scrollView: UIScrollView = {
let scrollView = UIScrollView()
scrollView.translatesAutoresizingMaskIntoConstraints = false
scrollView.isPagingEnabled = true
scrollView.isScrollEnabled = false
return scrollView
}()
private let stackView: UIStackView = {
let stackView = UIStackView()
stackView.axis = .horizontal
stackView.spacing = 0
stackView.translatesAutoresizingMaskIntoConstraints = false
return stackView
}()
weak var navigationDelegate: TopTabBarViewControllerDelegate?
var selection: ControlSelection {
return tabBar.selectedSegment
}
private (set) var bottomAnchorConstraints: [NSLayoutConstraint] = []
private let selectedIndex: Int
private let titles: [String]
init(titles: [String], selectedIndex: Int = 0) {
self.titles = titles
self.selectedIndex = selectedIndex
super.init(nibName: nil, bundle: nil)
view.addSubview(tabBar)
view.addSubview(scrollView)
scrollView.addSubview(stackView)
bottomAnchorConstraints = [
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
stackView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
]
NSLayoutConstraint.activate([
tabBar.heightAnchor.constraint(equalToConstant: DataEntry.Metric.TabBar.height),
tabBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
tabBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
tabBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
scrollView.topAnchor.constraint(equalTo: tabBar.bottomAnchor),
stackView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
stackView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
stackView.topAnchor.constraint(equalTo: tabBar.bottomAnchor),
] + bottomAnchorConstraints)
}
required init?(coder aDecoder: NSCoder) {
return nil
}
//NOTE: need to triggle initial selection state when view layout its subviews for first time
private var didLayoutSubviewsAtFirstTime: Bool = true
func set(viewControllers: [UIViewController]) {
viewControllers.forEach { addChild($0) }
let views = viewControllers.compactMap { $0.view }
stackView.addArrangedSubviews(views)
let viewsHeights = views.flatMap { each -> [NSLayoutConstraint] in
return [
each.widthAnchor.constraint(equalTo: view.widthAnchor),
each.heightAnchor.constraint(equalTo: scrollView.heightAnchor)
]
}
NSLayoutConstraint.activate([
viewsHeights
])
viewControllers.forEach { $0.didMove(toParent: self) }
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
guard scrollView.bounds.width != .zero && didLayoutSubviewsAtFirstTime else {
return
}
didLayoutSubviewsAtFirstTime = false
selectTab(selection: tabBar.selectedSegment, animated: false)
}
@objc private func didTapSegment(_ control: ScrollableSegmentedControl) {
selectTab(selection: control.selectedSegment, animated: true)
}
private func selectTab(selection: ControlSelection, animated: Bool) {
let index: Int
switch selection {
case .selected(let value):
index = Int(value)
case .unselected:
index = 0
}
let offset = CGPoint(x: CGFloat(index) * scrollView.bounds.width, y: 0)
scrollView.setContentOffset(offset, animated: animated)
navigationDelegate?.viewController(self, didSelectPage: index)
}
}

@ -190,12 +190,12 @@ extension EmptyView {
.configure(insets: .zero)
}
static func priceAlertsEmpryView() -> EmptyView {
EmptyView()
static func priceAlertsEmptyView() -> EmptyView {
EmptyView(placement: FilterTokensHoldersEmptyViewDefaultPlacement(verticalOffset: -DataEntry.Metric.Tokens.Filter.height))
.configure(image: R.image.iconsIllustrationsAlert2())
.configure(title: R.string.localizable.activityEmpty())
.configure(title: "Alerts will appear here")
.configure(spacing: 0)
.configure(insets: .zero)
.configure(insets: .init(top: DataEntry.Metric.Tokens.Filter.height, left: 0, bottom: 0, right: 0))
}
static func filterTokensEmptyView(completion: @escaping () -> Void) -> EmptyView {

@ -294,7 +294,7 @@ class NFTAssetViewModel {
}
}
func tokenScriptWarningMessage(for action: TokenInstanceAction) -> FungibleTokenViewModel.TokenScriptWarningMessage? {
func tokenScriptWarningMessage(for action: TokenInstanceAction) -> FungibleTokenDetailsViewModel.TokenScriptWarningMessage? {
if let selection = action.activeExcludingSelection(selectedTokenHolders: [tokenHolder], forWalletAddress: session.account.address) {
if let denialMessage = selection.denial {
return .warning(string: denialMessage)
@ -307,8 +307,8 @@ class NFTAssetViewModel {
}
}
func buttonState(for action: TokenInstanceAction) -> FungibleTokenViewModel.ActionButtonState {
func _configButton(action: TokenInstanceAction) -> FungibleTokenViewModel.ActionButtonState {
func buttonState(for action: TokenInstanceAction) -> FungibleTokenDetailsViewModel.ActionButtonState {
func _configButton(action: TokenInstanceAction) -> FungibleTokenDetailsViewModel.ActionButtonState {
if let selection = action.activeExcludingSelection(selectedTokenHolders: [tokenHolder], forWalletAddress: session.account.address) {
if selection.denial == nil {
return .isDisplayed(false)

@ -1,105 +0,0 @@
//
// InfoPageView.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 12.05.2021.
//
import UIKit
import Combine
import AlphaWalletFoundation
protocol TokenInfoPageViewDelegate: class {
func didPressViewContractWebPage(forContract contract: AlphaWallet.Address, in tokenInfoPageView: TokenInfoPageView)
}
class TokenInfoPageView: ScrollableStackView, PageViewType {
private lazy var headerView = FungibleTokenHeaderView(viewModel: viewModel.headerViewModel)
private lazy var chartView: TokenHistoryChartView = {
let chartView = TokenHistoryChartView(viewModel: viewModel.chartViewModel)
return chartView
}()
private let viewModel: TokenInfoPageViewModel
private var cancelable = Set<AnyCancellable>()
private let appear = PassthroughSubject<Void, Never>()
weak var delegate: TokenInfoPageViewDelegate?
var rightBarButtonItem: UIBarButtonItem?
var title: String { return viewModel.tabTitle }
init(viewModel: TokenInfoPageViewModel) {
self.viewModel = viewModel
super.init()
headerView.delegate = self
bind(viewModel: viewModel)
}
func viewWillAppear() {
appear.send(())
}
private func generateSubviews(for viewTypes: [TokenInfoPageViewModel.ViewType]) {
stackView.removeAllArrangedSubviews()
stackView.addArrangedSubview(headerView)
for each in viewTypes {
switch each {
case .testnet:
stackView.addArrangedSubview(UIView.spacer(height: 40))
stackView.addArrangedSubview(UIView.spacer(backgroundColor: Configuration.Color.Semantic.tableViewSeparator))
let view = TestnetTokenInfoView()
view.configure(viewModel: .init())
stackView.addArrangedSubview(view)
case .charts:
stackView.addArrangedSubview(chartView)
stackView.addArrangedSubview(UIView.spacer(height: 10))
stackView.addArrangedSubview(UIView.spacer(backgroundColor: Configuration.Color.Semantic.tableViewSeparator))
stackView.addArrangedSubview(UIView.spacer(height: 10))
case .field(let viewModel):
let indexPath = IndexPath(row: 0, section: 0)
let view = TokenAttributeView(indexPath: indexPath)
view.delegate = self
view.configure(viewModel: viewModel)
stackView.addArrangedSubview(view)
case .header(let viewModel):
let view = TokenInfoHeaderView()
view.configure(viewModel: viewModel)
stackView.addArrangedSubview(view)
}
}
}
private func bind(viewModel: TokenInfoPageViewModel) {
let input = TokenInfoPageViewModelInput(appear: appear.eraseToAnyPublisher())
let output = viewModel.transform(input: input)
output.viewState
.sink { [weak self] state in
self?.generateSubviews(for: state.views)
}.store(in: &cancelable)
}
required init?(coder: NSCoder) {
return nil
}
}
extension TokenInfoPageView: TokenAttributeViewDelegate {
func didSelect(in view: TokenAttributeView) {
//no-op
}
}
extension TokenInfoPageView: FungibleTokenHeaderViewDelegate {
func didPressViewContractWebPage(inHeaderView: FungibleTokenHeaderView) {
delegate?.didPressViewContractWebPage(forContract: viewModel.token.contractAddress, in: self)
}
}

@ -0,0 +1,212 @@
//
// FungibleTokenCoordinator.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 19.11.2022.
//
import Foundation
import AlphaWalletFoundation
import PromiseKit
import Combine
protocol FungibleTokenCoordinatorDelegate: AnyObject, CanOpenURL {
func didTapSwap(swapTokenFlow: SwapTokenFlow, in coordinator: FungibleTokenCoordinator)
func didTapBridge(transactionType: TransactionType, service: TokenActionProvider, in coordinator: FungibleTokenCoordinator)
func didTapBuy(transactionType: TransactionType, service: TokenActionProvider, in coordinator: FungibleTokenCoordinator)
func didPress(for type: PaymentFlow, viewController: UIViewController, in coordinator: FungibleTokenCoordinator)
func didTap(transaction: TransactionInstance, viewController: UIViewController, in coordinator: FungibleTokenCoordinator)
func didTap(activity: Activity, viewController: UIViewController, in coordinator: FungibleTokenCoordinator)
func didClose(in coordinator: FungibleTokenCoordinator)
}
class FungibleTokenCoordinator: Coordinator {
private let keystore: Keystore
private let assetDefinitionStore: AssetDefinitionStore
private let analytics: AnalyticsLogger
private let tokenActionsProvider: SupportedTokenActionsProvider
private let coinTickersFetcher: CoinTickersFetcher
private let activitiesService: ActivitiesServiceType
private let sessions: ServerDictionary<WalletSession>
private let session: WalletSession
private let alertService: PriceAlertServiceType
private let tokensService: TokenBalanceRefreshable & TokenViewModelState & TokenHolderState
private let token: Token
private let navigationController: UINavigationController
private var cancelable = Set<AnyCancellable>()
private lazy var rootViewController: FungibleTokenTabViewController = {
let viewModel = FungibleTokenTabViewModel(token: token, session: session, tokensService: tokensService, assetDefinitionStore: assetDefinitionStore)
let viewController = FungibleTokenTabViewController(viewModel: viewModel)
let viewControlers = viewModel.tabBarItems.map { buildViewController(tabBarItem: $0) }
viewController.set(viewControllers: viewControlers)
viewController.delegate = self
return viewController
}()
var coordinators: [Coordinator] = []
weak var delegate: FungibleTokenCoordinatorDelegate?
init(token: Token,
navigationController: UINavigationController,
session: WalletSession,
keystore: Keystore,
assetDefinitionStore: AssetDefinitionStore,
analytics: AnalyticsLogger,
tokenActionsProvider: SupportedTokenActionsProvider,
coinTickersFetcher: CoinTickersFetcher,
activitiesService: ActivitiesServiceType,
alertService: PriceAlertServiceType,
tokensService: TokenBalanceRefreshable & TokenViewModelState & TokenHolderState,
sessions: ServerDictionary<WalletSession>) {
self.token = token
self.navigationController = navigationController
self.sessions = sessions
self.tokensService = tokensService
self.session = session
self.keystore = keystore
self.assetDefinitionStore = assetDefinitionStore
self.analytics = analytics
self.tokenActionsProvider = tokenActionsProvider
self.coinTickersFetcher = coinTickersFetcher
self.activitiesService = activitiesService
self.alertService = alertService
}
func start() {
rootViewController.hidesBottomBarWhenPushed = true
rootViewController.navigationItem.largeTitleDisplayMode = .never
navigationController.pushViewController(rootViewController, animated: true)
}
private func buildViewController(tabBarItem: FungibleTokenTabViewModel.TabBarItem) -> UIViewController {
switch tabBarItem {
case .details:
return buildDetailsViewController()
case .activities:
return buildActivitiesViewController()
case .alerts:
return buildAlertsViewController()
}
}
private func buildActivitiesViewController() -> UIViewController {
let viewController = ActivitiesViewController(analytics: analytics, keystore: keystore, wallet: session.account, viewModel: .init(collection: .init(activities: [])), sessions: sessions, assetDefinitionStore: assetDefinitionStore)
viewController.delegate = self
//FIXME: replace later with moving it to `ActivitiesViewController`
activitiesService.activitiesPublisher
.map { ActivityPageViewModel(activitiesViewModel: .init(collection: .init(activities: $0))) }
.receive(on: RunLoop.main)
.sink { [viewController] in
viewController.configure(viewModel: $0.activitiesViewModel)
}.store(in: &cancelable)
activitiesService.start()
return viewController
}
private func buildAlertsViewController() -> UIViewController {
let viewModel = PriceAlertsViewModel(alertService: alertService, token: token)
let viewController = PriceAlertsViewController(viewModel: viewModel)
viewController.delegate = self
return viewController
}
private func buildDetailsViewController() -> UIViewController {
lazy var viewModel = FungibleTokenDetailsViewModel(token: token, coinTickersFetcher: coinTickersFetcher, tokensService: tokensService, session: session, assetDefinitionStore: assetDefinitionStore, tokenActionsProvider: tokenActionsProvider)
let viewController = FungibleTokenDetailsViewController(viewModel: viewModel)
viewController.delegate = self
return viewController
}
}
extension FungibleTokenCoordinator: FungibleTokenDetailsViewControllerDelegate {
func didTapSwap(swapTokenFlow: SwapTokenFlow, in viewController: FungibleTokenDetailsViewController) {
delegate?.didTapSwap(swapTokenFlow: swapTokenFlow, in: self)
}
func didTapBridge(for token: Token, service: TokenActionProvider, in viewController: FungibleTokenDetailsViewController) {
delegate?.didTapBridge(transactionType: .init(fungibleToken: token), service: service, in: self)
}
func didTapBuy(for token: Token, service: TokenActionProvider, in viewController: FungibleTokenDetailsViewController) {
delegate?.didTapBuy(transactionType: .init(fungibleToken: token), service: service, in: self)
}
func didTapSend(for token: Token, in viewController: FungibleTokenDetailsViewController) {
delegate?.didPress(for: .send(type: .transaction(.init(fungibleToken: token))), viewController: viewController, in: self)
}
func didTapReceive(for token: Token, in viewController: FungibleTokenDetailsViewController) {
delegate?.didPress(for: .request, viewController: viewController, in: self)
}
func didTap(action: TokenInstanceAction, token: Token, in viewController: FungibleTokenDetailsViewController) {
guard let navigationController = viewController.navigationController else { return }
let tokenHolder = token.getTokenHolder(assetDefinitionStore: assetDefinitionStore, forWallet: session.account)
delegate?.didPress(for: .send(type: .tokenScript(action: action, token: token, tokenHolder: tokenHolder)), viewController: navigationController, in: self)
}
func didPressViewContractWebPage(forContract contract: AlphaWallet.Address, server: RPCServer, in viewController: UIViewController) {
delegate?.didPressViewContractWebPage(forContract: contract, server: server, in: viewController)
}
func didPressViewContractWebPage(_ url: URL, in viewController: UIViewController) {
delegate?.didPressOpenWebPage(url, in: viewController)
}
func didPressOpenWebPage(_ url: URL, in viewController: UIViewController) {
delegate?.didPressOpenWebPage(url, in: viewController)
}
}
extension FungibleTokenCoordinator: PriceAlertsViewControllerDelegate {
func editAlertSelected(in viewController: PriceAlertsViewController, alert: PriceAlert) {
let coordinator = EditPriceAlertCoordinator(navigationController: navigationController, configuration: .edit(alert), token: token, session: session, tokensService: tokensService, alertService: alertService)
addCoordinator(coordinator)
coordinator.delegate = self
coordinator.start()
}
func addAlertSelected(in viewController: PriceAlertsViewController) {
let coordinator = EditPriceAlertCoordinator(navigationController: navigationController, configuration: .create, token: token, session: session, tokensService: tokensService, alertService: alertService)
addCoordinator(coordinator)
coordinator.delegate = self
coordinator.start()
}
}
extension FungibleTokenCoordinator: ActivitiesViewControllerDelegate {
func didPressActivity(activity: AlphaWalletFoundation.Activity, in viewController: ActivitiesViewController) {
delegate?.didTap(activity: activity, viewController: viewController, in: self)
}
func didPressTransaction(transaction: AlphaWalletFoundation.TransactionInstance, in viewController: ActivitiesViewController) {
delegate?.didTap(transaction: transaction, viewController: viewController, in: self)
}
}
extension FungibleTokenCoordinator: EditPriceAlertCoordinatorDelegate {
func didClose(in coordinator: EditPriceAlertCoordinator) {
removeCoordinator(coordinator)
}
}
extension FungibleTokenCoordinator: FungibleTokenTabViewControllerDelegate {
func didClose(in viewController: FungibleTokenTabViewController) {
delegate?.didClose(in: self)
}
func open(url: URL) {
delegate?.didPressOpenWebPage(url, in: rootViewController)
}
}

@ -32,6 +32,9 @@ class SingleChainTokenCoordinator: Coordinator {
private let coinTickersFetcher: CoinTickersFetcher
private let activitiesService: ActivitiesServiceType
private let sessions: ServerDictionary<WalletSession>
private let alertService: PriceAlertServiceType
private let tokensService: TokenBalanceRefreshable & TokenViewModelState & TokenHolderState
let session: WalletSession
weak var delegate: SingleChainTokenCoordinatorDelegate?
var coordinators: [Coordinator] = []
@ -39,8 +42,6 @@ class SingleChainTokenCoordinator: Coordinator {
var server: RPCServer {
session.server
}
private let alertService: PriceAlertServiceType
private let tokensService: TokenBalanceRefreshable & TokenViewModelState & TokenHolderState
init(
session: WalletSession,
@ -102,87 +103,68 @@ class SingleChainTokenCoordinator: Coordinator {
//NOTE: create half mutable copy of `activitiesService` to configure it for fetching activities for specific token
let activitiesFilterStrategy = token.activitiesFilterStrategy
let activitiesService = self.activitiesService.copy(activitiesFilterStrategy: activitiesFilterStrategy, transactionsFilterStrategy: TransactionDataStore.functional.transactionsFilter(for: activitiesFilterStrategy, token: token))
let viewModel = FungibleTokenViewModel(activitiesService: activitiesService, alertService: alertService, token: token, session: session, assetDefinitionStore: assetDefinitionStore, tokenActionsProvider: tokenActionsProvider, coinTickersFetcher: coinTickersFetcher, tokensService: tokensService)
let viewController = FungibleTokenViewController(keystore: keystore, analytics: analytics, viewModel: viewModel, activitiesService: activitiesService, sessions: sessions)
viewController.delegate = self
navigationController.pushViewController(viewController, animated: true)
let coordinator = FungibleTokenCoordinator(token: token, navigationController: navigationController, session: session, keystore: keystore, assetDefinitionStore: assetDefinitionStore, analytics: analytics, tokenActionsProvider: tokenActionsProvider, coinTickersFetcher: coinTickersFetcher, activitiesService: activitiesService, alertService: alertService, tokensService: tokensService, sessions: sessions)
addCoordinator(coordinator)
coordinator.delegate = self
coordinator.start()
}
private func showTokenInstanceActionView(forAction action: TokenInstanceAction, fungibleTokenObject token: Token, navigationController: UINavigationController) {
let tokenHolder = token.getTokenHolder(assetDefinitionStore: assetDefinitionStore, forWallet: session.account)
delegate?.didPress(for: .send(type: .tokenScript(action: action, token: token, tokenHolder: tokenHolder)), viewController: navigationController, in: self)
}
func didClose(in viewController: FungibleTokenViewController) {
//no-op
}
}
extension SingleChainTokenCoordinator: NFTCollectionCoordinatorDelegate {
func didTap(transaction: TransactionInstance, in coordinator: NFTCollectionCoordinator) {
delegate?.didTap(transaction: transaction, viewController: coordinator.rootViewController, in: self)
}
func didTap(activity: Activity, in coordinator: NFTCollectionCoordinator) {
delegate?.didTap(activity: activity, viewController: coordinator.rootViewController, in: self)
}
func didPress(for type: PaymentFlow, inViewController viewController: UIViewController, in coordinator: NFTCollectionCoordinator) {
delegate?.didPress(for: type, viewController: viewController, in: self)
}
func didClose(in coordinator: NFTCollectionCoordinator) {
removeCoordinator(coordinator)
extension SingleChainTokenCoordinator: FungibleTokenCoordinatorDelegate {
func didTapSwap(swapTokenFlow: SwapTokenFlow, in coordinator: FungibleTokenCoordinator) {
delegate?.didTapSwap(swapTokenFlow: swapTokenFlow, in: self)
}
func didPostTokenScriptTransaction(_ transaction: SentTransaction, in coordinator: NFTCollectionCoordinator) {
delegate?.didPostTokenScriptTransaction(transaction, in: self)
func didTapBridge(transactionType: TransactionType, service: TokenActionProvider, in coordinator: FungibleTokenCoordinator) {
delegate?.didTapBridge(transactionType: transactionType, service: service, in: self)
}
}
extension SingleChainTokenCoordinator: FungibleTokenViewControllerDelegate {
func didTapAddAlert(for token: Token, in viewController: FungibleTokenViewController) {
delegate?.didTapAddAlert(for: token, in: self)
func didTapBuy(transactionType: TransactionType, service: TokenActionProvider, in coordinator: FungibleTokenCoordinator) {
delegate?.didTapBuy(transactionType: transactionType, service: service, in: self)
}
func didTapEditAlert(for token: Token, alert: PriceAlert, in viewController: FungibleTokenViewController) {
delegate?.didTapEditAlert(for: token, alert: alert, in: self)
func didPress(for type: PaymentFlow, viewController: UIViewController, in coordinator: FungibleTokenCoordinator) {
delegate?.didPress(for: type, viewController: viewController, in: self)
}
func didTapSwap(swapTokenFlow: SwapTokenFlow, in viewController: FungibleTokenViewController) {
delegate?.didTapSwap(swapTokenFlow: swapTokenFlow, in: self)
func didTap(transaction: TransactionInstance, viewController: UIViewController, in coordinator: FungibleTokenCoordinator) {
delegate?.didTap(transaction: transaction, viewController: viewController, in: self)
}
func didTapBridge(for token: Token, service: TokenActionProvider, in viewController: FungibleTokenViewController) {
delegate?.didTapBridge(transactionType: .init(fungibleToken: token), service: service, in: self)
func didTap(activity: Activity, viewController: UIViewController, in coordinator: FungibleTokenCoordinator) {
delegate?.didTap(activity: activity, viewController: viewController, in: self)
}
func didTapBuy(for token: Token, service: TokenActionProvider, in viewController: FungibleTokenViewController) {
delegate?.didTapBuy(transactionType: .init(fungibleToken: token), service: service, in: self)
func didClose(in coordinator: FungibleTokenCoordinator) {
removeCoordinator(coordinator)
}
}
func didTapSend(for token: Token, in viewController: FungibleTokenViewController) {
delegate?.didPress(for: .send(type: .transaction(.init(fungibleToken: token))), viewController: viewController, in: self)
extension SingleChainTokenCoordinator: NFTCollectionCoordinatorDelegate {
func didTap(transaction: TransactionInstance, in coordinator: NFTCollectionCoordinator) {
delegate?.didTap(transaction: transaction, viewController: coordinator.rootViewController, in: self)
}
func didTapReceive(for token: Token, in viewController: FungibleTokenViewController) {
delegate?.didPress(for: .request, viewController: viewController, in: self)
func didTap(activity: Activity, in coordinator: NFTCollectionCoordinator) {
delegate?.didTap(activity: activity, viewController: coordinator.rootViewController, in: self)
}
func didTap(activity: Activity, in viewController: FungibleTokenViewController) {
delegate?.didTap(activity: activity, viewController: viewController, in: self)
func didPress(for type: PaymentFlow, inViewController viewController: UIViewController, in coordinator: NFTCollectionCoordinator) {
delegate?.didPress(for: type, viewController: viewController, in: self)
}
func didTap(transaction: TransactionInstance, in viewController: FungibleTokenViewController) {
delegate?.didTap(transaction: transaction, viewController: viewController, in: self)
func didClose(in coordinator: NFTCollectionCoordinator) {
removeCoordinator(coordinator)
}
func didTap(action: TokenInstanceAction, token: Token, in viewController: FungibleTokenViewController) {
guard let navigationController = viewController.navigationController else { return }
showTokenInstanceActionView(forAction: action, fungibleTokenObject: token, navigationController: navigationController)
func didPostTokenScriptTransaction(_ transaction: SentTransaction, in coordinator: NFTCollectionCoordinator) {
delegate?.didPostTokenScriptTransaction(transaction, in: self)
}
}

@ -410,7 +410,6 @@ extension TokensCoordinator: EditPriceAlertCoordinatorDelegate {
}
extension TokensCoordinator: SingleChainTokenCoordinatorDelegate {
func didSendTransaction(_ transaction: SentTransaction, inCoordinator coordinator: TransactionConfirmationCoordinator) {
delegate?.didSendTransaction(transaction, inCoordinator: coordinator)
}

@ -0,0 +1,187 @@
//
// FungibleTokenDetailsViewController.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 19.11.2022.
//
import UIKit
import Combine
import AlphaWalletFoundation
protocol FungibleTokenDetailsViewControllerDelegate: AnyObject, CanOpenURL {
func didTapSwap(swapTokenFlow: SwapTokenFlow, in viewController: FungibleTokenDetailsViewController)
func didTapBridge(for token: Token, service: TokenActionProvider, in viewController: FungibleTokenDetailsViewController)
func didTapBuy(for token: Token, service: TokenActionProvider, in viewController: FungibleTokenDetailsViewController)
func didTapSend(for token: Token, in viewController: FungibleTokenDetailsViewController)
func didTapReceive(for token: Token, in viewController: FungibleTokenDetailsViewController)
func didTap(action: TokenInstanceAction, token: Token, in viewController: FungibleTokenDetailsViewController)
}
class FungibleTokenDetailsViewController: UIViewController {
private let containerView: ScrollableStackView = ScrollableStackView()
private let buttonsBar = HorizontalButtonsBar(configuration: .combined(buttons: 2))
private lazy var headerView: FungibleTokenHeaderView = {
let view = FungibleTokenHeaderView(viewModel: viewModel.headerViewModel)
view.delegate = self
return view
}()
private lazy var chartView: TokenHistoryChartView = {
let chartView = TokenHistoryChartView(viewModel: viewModel.chartViewModel)
return chartView
}()
private let viewModel: FungibleTokenDetailsViewModel
private var cancelable = Set<AnyCancellable>()
private let willAppear = PassthroughSubject<Void, Never>()
weak var delegate: FungibleTokenDetailsViewControllerDelegate?
init(viewModel: FungibleTokenDetailsViewModel) {
self.viewModel = viewModel
super.init(nibName: nil, bundle: nil)
let footerBar = ButtonsBarBackgroundView(buttonsBar: buttonsBar)
view.addSubview(footerBar)
view.addSubview(containerView)
NSLayoutConstraint.activate([
containerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
containerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
containerView.topAnchor.constraint(equalTo: view.topAnchor),
containerView.bottomAnchor.constraint(equalTo: footerBar.topAnchor),
footerBar.anchorsConstraint(to: view)
])
buttonsBar.viewController = self
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Configuration.Color.Semantic.defaultViewBackground
bind(viewModel: viewModel)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
willAppear.send(())
}
private func buildSubviews(for viewTypes: [FungibleTokenDetailsViewModel.ViewType]) -> [UIView] {
var subviews: [UIView] = []
subviews += [headerView]
for each in viewTypes {
switch each {
case .testnet:
subviews += [UIView.spacer(height: 40)]
subviews += [UIView.spacer(backgroundColor: Configuration.Color.Semantic.tableViewSeparator)]
let view = TestnetTokenInfoView()
view.configure(viewModel: .init())
subviews += [view]
case .charts:
subviews += [chartView]
subviews += [UIView.spacer(height: 10)]
subviews += [UIView.spacer(backgroundColor: Configuration.Color.Semantic.tableViewSeparator)]
subviews += [UIView.spacer(height: 10)]
case .field(let viewModel):
let view = TokenAttributeView(indexPath: IndexPath(row: 0, section: 0))
view.configure(viewModel: viewModel)
subviews += [view]
case .header(let viewModel):
let view = TokenInfoHeaderView()
view.configure(viewModel: viewModel)
subviews += [view]
}
}
return subviews
}
private func layoutSubviews(_ subviews: [UIView]) {
containerView.stackView.removeAllArrangedSubviews()
containerView.stackView.addArrangedSubviews(subviews)
}
private func bind(viewModel: FungibleTokenDetailsViewModel) {
let input = FungibleTokenDetailsViewModelInput(willAppear: willAppear.eraseToAnyPublisher())
let output = viewModel.transform(input: input)
output.viewState
.sink { [weak self] viewState in
guard let strongSelf = self else { return }
strongSelf.layoutSubviews(strongSelf.buildSubviews(for: viewState.views))
strongSelf.configureActionButtons(with: viewState.actions)
}.store(in: &cancelable)
}
required init?(coder: NSCoder) {
return nil
}
private func configureActionButtons(with actions: [TokenInstanceAction]) {
buttonsBar.configure(.combined(buttons: actions.count))
for (action, button) in zip(actions, buttonsBar.buttons) {
button.setTitle(action.name, for: .normal)
button.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside)
switch viewModel.buttonState(for: action) {
case .isEnabled(let isEnabled):
button.isEnabled = isEnabled
case .isDisplayed(let isDisplayed):
button.displayButton = isDisplayed
case .noOption:
continue
}
}
}
@objc private func actionButtonTapped(sender: UIButton) {
for (action, button) in zip(viewModel.actions, buttonsBar.buttons) where button == sender {
switch action.type {
case .swap:
delegate?.didTapSwap(swapTokenFlow: .swapToken(token: viewModel.token), in: self)
case .erc20Send:
delegate?.didTapSend(for: viewModel.token, in: self)
case .erc20Receive:
delegate?.didTapReceive(for: viewModel.token, in: self)
case .nftRedeem, .nftSell, .nonFungibleTransfer:
break
case .tokenScript:
if let message = viewModel.tokenScriptWarningMessage(for: action) {
guard case .warning(let string) = message else { return }
show(message: string)
} else {
delegate?.didTap(action: action, token: viewModel.token, in: self)
}
case .bridge(let service):
delegate?.didTapBridge(for: viewModel.token, service: service, in: self)
case .buy(let service):
delegate?.didTapBuy(for: viewModel.token, service: service, in: self)
}
break
}
}
private func show(message: String) {
UIAlertController.alert(message: message, alertButtonTitles: [R.string.localizable.oK()], alertButtonStyles: [.default], viewController: self)
}
}
extension FungibleTokenDetailsViewController: FungibleTokenHeaderViewDelegate {
func didPressViewContractWebPage(inHeaderView: FungibleTokenHeaderView) {
delegate?.didPressViewContractWebPage(forContract: viewModel.token.contractAddress, server: viewModel.token.server, in: self)
}
}

@ -0,0 +1,94 @@
//
// FungibleTokenTabViewController.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 20.11.2022.
//
import UIKit
import Combine
import AlphaWalletFoundation
protocol FungibleTokenTabViewControllerDelegate: AnyObject, CanOpenURL2 {
func didClose(in viewController: FungibleTokenTabViewController)
}
class FungibleTokenTabViewController: TopTabBarViewController {
private let viewModel: FungibleTokenTabViewModel
private var cancelable = Set<AnyCancellable>()
private let willAppear = PassthroughSubject<Void, Never>()
weak var delegate: FungibleTokenTabViewControllerDelegate?
init(viewModel: FungibleTokenTabViewModel) {
self.viewModel = viewModel
super.init(titles: viewModel.tabBarItems.map { $0.description })
}
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = Configuration.Color.Semantic.defaultViewBackground
updateNavigationRightBarButtons(tokenScriptFileStatusHandler: viewModel.tokenScriptFileStatusHandler)
bind(viewModel: viewModel)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
hideNavigationBarTopSeparatorLine()
willAppear.send(())
}
private func bind(viewModel: FungibleTokenTabViewModel) {
let input = FungibleTokenTabViewModelInput(willAppear: willAppear.eraseToAnyPublisher())
let output = viewModel.transform(input: input)
output.viewState
.map { $0.title }
.assign(to: \.title, on: navigationItem, ownership: .weak)
.store(in: &cancelable)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func updateNavigationRightBarButtons(tokenScriptFileStatusHandler xmlHandler: XMLHandler) {
if Features.default.isAvailable(.isTokenScriptSignatureStatusEnabled) {
let tokenScriptStatusPromise = xmlHandler.tokenScriptStatus
if tokenScriptStatusPromise.isPending {
let label: UIBarButtonItem = .init(title: R.string.localizable.tokenScriptVerifying(), style: .plain, target: nil, action: nil)
navigationItem.rightBarButtonItem = label
tokenScriptStatusPromise.done { [weak self] _ in
self?.updateNavigationRightBarButtons(tokenScriptFileStatusHandler: xmlHandler)
}.cauterize()
}
if let server = xmlHandler.server, let status = tokenScriptStatusPromise.value, server.matches(server: viewModel.session.server) {
switch status {
case .type0NoTokenScript:
navigationItem.rightBarButtonItem = nil
case .type1GoodTokenScriptSignatureGoodOrOptional, .type2BadTokenScript:
let button = createTokenScriptFileStatusButton(withStatus: status, urlOpener: self)
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: button)
}
} else {
navigationItem.rightBarButtonItem = nil
}
} else {
//no-op
}
}
}
extension FungibleTokenTabViewController: PopNotifiable {
func didPopViewController(animated: Bool) {
delegate?.didClose(in: self)
}
}
extension FungibleTokenTabViewController: CanOpenURL2 {
func open(url: URL) {
delegate?.open(url: url)
}
}

@ -1,248 +0,0 @@
// Copyright © 2018 Stormbird PTE. LTD.
import UIKit
import Combine
import AlphaWalletFoundation
protocol FungibleTokenViewControllerDelegate: class, CanOpenURL {
func didTapSwap(swapTokenFlow: SwapTokenFlow, in viewController: FungibleTokenViewController)
func didTapBridge(for token: Token, service: TokenActionProvider, in viewController: FungibleTokenViewController)
func didTapBuy(for token: Token, service: TokenActionProvider, in viewController: FungibleTokenViewController)
func didTapSend(for token: Token, in viewController: FungibleTokenViewController)
func didTapReceive(for token: Token, in viewController: FungibleTokenViewController)
func didTap(transaction: TransactionInstance, in viewController: FungibleTokenViewController)
func didTap(activity: Activity, in viewController: FungibleTokenViewController)
func didTap(action: TokenInstanceAction, token: Token, in viewController: FungibleTokenViewController)
func didTapAddAlert(for token: Token, in viewController: FungibleTokenViewController)
func didTapEditAlert(for token: Token, alert: PriceAlert, in viewController: FungibleTokenViewController)
func didClose(in viewController: FungibleTokenViewController)
}
class FungibleTokenViewController: UIViewController {
private var viewModel: FungibleTokenViewModel
private let buttonsBar = HorizontalButtonsBar(configuration: .combined(buttons: 2))
private lazy var tokenInfoPageView: TokenInfoPageView = {
let view = TokenInfoPageView(viewModel: viewModel.tokenInfoPageViewModel)
view.delegate = self
return view
}()
private lazy var activitiesPageView: ActivitiesPageView = {
return ActivitiesPageView(analytics: analytics, keystore: keystore, wallet: viewModel.wallet, viewModel: .init(activitiesViewModel: .init(collection: .init())), sessions: sessions, assetDefinitionStore: viewModel.assetDefinitionStore)
}()
private lazy var alertsPageView: PriceAlertsPageView = {
return PriceAlertsPageView(viewModel: .init(alerts: []))
}()
private let activitiesService: ActivitiesServiceType
private let analytics: AnalyticsLogger
private let keystore: Keystore
private var cancelable = Set<AnyCancellable>()
private let appear = PassthroughSubject<Void, Never>()
private let updateAlert = PassthroughSubject<(value: Bool, indexPath: IndexPath), Never>()
private let removeAlert = PassthroughSubject<IndexPath, Never>()
private let sessions: ServerDictionary<WalletSession>
weak var delegate: FungibleTokenViewControllerDelegate?
init(keystore: Keystore, analytics: AnalyticsLogger, viewModel: FungibleTokenViewModel, activitiesService: ActivitiesServiceType, sessions: ServerDictionary<WalletSession>) {
self.sessions = sessions
self.viewModel = viewModel
self.keystore = keystore
self.activitiesService = activitiesService
self.analytics = analytics
super.init(nibName: nil, bundle: nil)
hidesBottomBarWhenPushed = true
let footerBar = ButtonsBarBackgroundView(buttonsBar: buttonsBar)
let pageWithFooter = PageViewWithFooter(pageView: tokenInfoPageView, footerBar: footerBar)
let pages: [PageViewType]
if Features.default.isAvailable(.isAlertsEnabled) && viewModel.hasCoinTicker {
pages = [pageWithFooter, activitiesPageView, alertsPageView]
} else {
pages = [pageWithFooter, activitiesPageView]
}
let containerView = PagesContainerView(pages: pages)
view.addSubview(containerView)
NSLayoutConstraint.activate([containerView.anchorsConstraint(to: view)])
navigationItem.largeTitleDisplayMode = .never
}
required init?(coder aDecoder: NSCoder) {
return nil
}
override func viewDidLoad() {
super.viewDidLoad()
activitiesPageView.delegate = self
alertsPageView.delegate = self
buttonsBar.viewController = self
bind(viewModel: viewModel)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
hideNavigationBarTopSeparatorLine()
appear.send(())
tokenInfoPageView.viewWillAppear()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
showNavigationBarTopSeparatorLine()
}
private func bind(viewModel: FungibleTokenViewModel) {
view.backgroundColor = viewModel.backgroundColor
updateNavigationRightBarButtons(tokenScriptFileStatusHandler: viewModel.tokenScriptFileStatusHandler)
let input = FungibleTokenViewModelInput(appear: appear.eraseToAnyPublisher(),
updateAlert: updateAlert.eraseToAnyPublisher(),
removeAlert: removeAlert.eraseToAnyPublisher())
let output = viewModel.transform(input: input)
output.viewState
.sink { [weak self, navigationItem] state in
navigationItem.title = state.title
self?.configureActionButtons(with: state.actions)
}.store(in: &cancelable)
output.activities
.sink { [weak activitiesPageView] in activitiesPageView?.configure(viewModel: $0) }
.store(in: &cancelable)
output.alerts
.sink { [weak alertsPageView] in alertsPageView?.configure(viewModel: $0) }
.store(in: &cancelable)
}
private func configureActionButtons(with actions: [TokenInstanceAction]) {
buttonsBar.configure(.combined(buttons: actions.count))
for (action, button) in zip(actions, buttonsBar.buttons) {
button.setTitle(action.name, for: .normal)
button.addTarget(self, action: #selector(actionButtonTapped), for: .touchUpInside)
switch viewModel.buttonState(for: action) {
case .isEnabled(let isEnabled):
button.isEnabled = isEnabled
case .isDisplayed(let isDisplayed):
button.displayButton = isDisplayed
case .noOption:
continue
}
}
}
private func updateNavigationRightBarButtons(tokenScriptFileStatusHandler xmlHandler: XMLHandler) {
if Features.default.isAvailable(.isTokenScriptSignatureStatusEnabled) {
let tokenScriptStatusPromise = xmlHandler.tokenScriptStatus
if tokenScriptStatusPromise.isPending {
let label: UIBarButtonItem = .init(title: R.string.localizable.tokenScriptVerifying(), style: .plain, target: nil, action: nil)
navigationItem.rightBarButtonItem = label
tokenScriptStatusPromise.done { [weak self] _ in
self?.updateNavigationRightBarButtons(tokenScriptFileStatusHandler: xmlHandler)
}.cauterize()
}
if let server = xmlHandler.server, let status = tokenScriptStatusPromise.value, server.matches(server: viewModel.session.server) {
switch status {
case .type0NoTokenScript:
navigationItem.rightBarButtonItem = nil
case .type1GoodTokenScriptSignatureGoodOrOptional, .type2BadTokenScript:
let button = createTokenScriptFileStatusButton(withStatus: status, urlOpener: self)
navigationItem.rightBarButtonItem = UIBarButtonItem(customView: button)
}
} else {
navigationItem.rightBarButtonItem = nil
}
} else {
//no-op
}
}
@objc private func actionButtonTapped(sender: UIButton) {
let actions = viewModel.actions
for (action, button) in zip(actions, buttonsBar.buttons) where button == sender {
switch action.type {
case .swap:
delegate?.didTapSwap(swapTokenFlow: .swapToken(token: viewModel.token), in: self)
case .erc20Send:
delegate?.didTapSend(for: viewModel.token, in: self)
case .erc20Receive:
delegate?.didTapReceive(for: viewModel.token, in: self)
case .nftRedeem, .nftSell, .nonFungibleTransfer:
break
case .tokenScript:
if let message = viewModel.tokenScriptWarningMessage(for: action) {
guard case .warning(let string) = message else { return }
show(message: string)
} else {
delegate?.didTap(action: action, token: viewModel.token, in: self)
}
case .bridge(let service):
delegate?.didTapBridge(for: viewModel.token, service: service, in: self)
case .buy(let service):
delegate?.didTapBuy(for: viewModel.token, service: service, in: self)
}
break
}
}
private func show(message: String) {
UIAlertController.alert(message: message, alertButtonTitles: [R.string.localizable.oK()], alertButtonStyles: [.default], viewController: self)
}
}
extension FungibleTokenViewController: CanOpenURL2 {
func open(url: URL) {
delegate?.didPressOpenWebPage(url, in: self)
}
}
extension FungibleTokenViewController: TokenInfoPageViewDelegate {
func didPressViewContractWebPage(forContract contract: AlphaWallet.Address, in tokenInfoPageView: TokenInfoPageView) {
delegate?.didPressViewContractWebPage(forContract: contract, server: viewModel.session.server, in: self)
}
}
extension FungibleTokenViewController: PriceAlertsPageViewDelegate {
func addAlertSelected(in view: PriceAlertsPageView) {
delegate?.didTapAddAlert(for: viewModel.token, in: self)
}
func editAlertSelected(in view: PriceAlertsPageView, alert: PriceAlert) {
delegate?.didTapEditAlert(for: viewModel.token, alert: alert, in: self)
}
func removeAlert(in view: PriceAlertsPageView, indexPath: IndexPath) {
removeAlert.send(indexPath)
}
func updateAlert(in view: PriceAlertsPageView, value: Bool, indexPath: IndexPath) {
updateAlert.send((value: value, indexPath: indexPath))
}
}
extension FungibleTokenViewController: ActivitiesPageViewDelegate {
func didTap(activity: Activity, in view: ActivitiesPageView) {
delegate?.didTap(activity: activity, in: self)
}
func didTap(transaction: TransactionInstance, in view: ActivitiesPageView) {
delegate?.didTap(transaction: transaction, in: self)
}
}
extension FungibleTokenViewController: PopNotifiable {
func didPopViewController(animated: Bool) {
delegate?.didClose(in: self)
}
}

@ -0,0 +1,300 @@
// Copyright © 2018 Stormbird PTE. LTD.
import UIKit
import Combine
import AlphaWalletFoundation
struct FungibleTokenDetailsViewModelInput {
let willAppear: AnyPublisher<Void, Never>
}
struct FungibleTokenDetailsViewModelOutput {
let viewState: AnyPublisher<FungibleTokenDetailsViewModel.ViewState, Never>
}
final class FungibleTokenDetailsViewModel {
private var chartHistoriesSubject: CurrentValueSubject<[ChartHistoryPeriod: ChartHistory], Never> = .init([:])
private let coinTickersFetcher: CoinTickersFetcher
private let tokensService: TokenViewModelState
private var cancelable = Set<AnyCancellable>()
private var chartHistories: [ChartHistoryPeriod: ChartHistory] { chartHistoriesSubject.value }
private lazy var coinTicker: AnyPublisher<CoinTicker?, Never> = {
return tokensService.tokenViewModelPublisher(for: token)
.map { $0?.balance.ticker }
.eraseToAnyPublisher()
}()
private lazy var tokenHolder: TokenHolder = token.getTokenHolder(assetDefinitionStore: assetDefinitionStore, forWallet: session.account)
private let session: WalletSession
private let assetDefinitionStore: AssetDefinitionStore
private let tokenActionsProvider: SupportedTokenActionsProvider
private (set) var actions: [TokenInstanceAction] = []
let token: Token
lazy var chartViewModel = TokenHistoryChartViewModel(chartHistories: chartHistoriesSubject.eraseToAnyPublisher(), coinTicker: coinTicker)
lazy var headerViewModel = FungibleTokenHeaderViewModel(token: token, tokensService: tokensService)
var wallet: Wallet { session.account }
init(token: Token, coinTickersFetcher: CoinTickersFetcher, tokensService: TokenViewModelState, session: WalletSession, assetDefinitionStore: AssetDefinitionStore, tokenActionsProvider: SupportedTokenActionsProvider) {
self.tokenActionsProvider = tokenActionsProvider
self.session = session
self.assetDefinitionStore = assetDefinitionStore
self.tokensService = tokensService
self.coinTickersFetcher = coinTickersFetcher
self.token = token
}
func transform(input: FungibleTokenDetailsViewModelInput) -> FungibleTokenDetailsViewModelOutput {
input.willAppear.flatMapLatest { [coinTickersFetcher, token] _ in
coinTickersFetcher.fetchChartHistories(for: .init(token: token), force: false, periods: ChartHistoryPeriod.allCases)
}.print("xxx.chartHistories").assign(to: \.value, on: chartHistoriesSubject)
.store(in: &cancelable)
let viewTypes = Publishers.CombineLatest(coinTicker, chartHistoriesSubject)
.compactMap { [weak self] ticker, _ in self?.buildViewTypes(for: ticker) }
let viewState = Publishers.CombineLatest(tokenActionsPublisher(), viewTypes)
.map { FungibleTokenDetailsViewModel.ViewState(actions: $0, views: $1) }
.eraseToAnyPublisher()
return .init(viewState: viewState)
}
private func tokenActionsPublisher() -> AnyPublisher<[TokenInstanceAction], Never> {
let whenTokenHolderHasChanged = tokenHolder.objectWillChange
.map { [tokensService, token] _ in tokensService.tokenViewModel(for: token) }
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
let whenTokenActionsHasChanged = tokenActionsProvider.objectWillChange
.map { [tokensService, token] _ in tokensService.tokenViewModel(for: token) }
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
let tokenViewModel = tokensService.tokenViewModelPublisher(for: token)
return Publishers.MergeMany(tokenViewModel, whenTokenHolderHasChanged, whenTokenActionsHasChanged)
.compactMap { _ in self.buildTokenActions() }
.handleEvents(receiveOutput: { self.actions = $0 })
.eraseToAnyPublisher()
}
private func buildTokenActions() -> [TokenInstanceAction] {
let xmlHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore)
let actionsFromTokenScript = xmlHandler.actions
infoLog("[TokenScript] actions names: \(actionsFromTokenScript.map(\.name))")
if actionsFromTokenScript.isEmpty {
switch token.type {
case .erc875, .erc721, .erc721ForTickets, .erc1155:
return []
case .erc20, .nativeCryptocurrency:
let actions: [TokenInstanceAction] = [
.init(type: .erc20Send),
.init(type: .erc20Receive)
]
return actions + tokenActionsProvider.actions(token: token)
}
} else {
switch token.type {
case .erc875, .erc721, .erc721ForTickets, .erc1155:
return []
case .erc20:
return actionsFromTokenScript + tokenActionsProvider.actions(token: token)
case .nativeCryptocurrency:
//TODO we should support retrieval of XML (and XMLHandler) based on address + server. For now, this is only important for native cryptocurrency. So might be ok to check like this for now
if let server = xmlHandler.server, server.matches(server: token.server) {
return actionsFromTokenScript + tokenActionsProvider.actions(token: token)
} else {
//TODO .erc20Send and .erc20Receive names aren't appropriate
let actions: [TokenInstanceAction] = [
.init(type: .erc20Send),
.init(type: .erc20Receive)
]
return actions + tokenActionsProvider.actions(token: token)
}
}
}
}
private func buildViewTypes(for ticker: CoinTicker?) -> [FungibleTokenDetailsViewModel.ViewType] {
var views: [FungibleTokenDetailsViewModel.ViewType] = []
if token.server.isTestnet {
views = [
.testnet
]
} else {
views = [
.charts,
.header(viewModel: .init(title: R.string.localizable.tokenInfoHeaderPerformance())),
.field(viewModel: dayViewModel),
.field(viewModel: weekViewModel),
.field(viewModel: monthViewModel),
.field(viewModel: yearViewModel),
.header(viewModel: .init(title: R.string.localizable.tokenInfoHeaderStats())),
.field(viewModel: markerCapViewModel(for: ticker)),
.field(viewModel: yearLowViewModel),
.field(viewModel: yearHighViewModel)
]
}
return views
}
func tokenScriptWarningMessage(for action: TokenInstanceAction) -> TokenScriptWarningMessage? {
let fungibleBalance = tokensService.tokenViewModel(for: token)?.balance.value
if let selection = action.activeExcludingSelection(selectedTokenHolders: [tokenHolder], forWalletAddress: wallet.address, fungibleBalance: fungibleBalance) {
if let denialMessage = selection.denial {
return .warning(string: denialMessage)
} else {
//no-op shouldn't have reached here since the button should be disabled. So just do nothing to be safe
return .undefined
}
} else {
return nil
}
}
func buttonState(for action: TokenInstanceAction) -> ActionButtonState {
func _configButton(action: TokenInstanceAction) -> ActionButtonState {
let fungibleBalance = tokensService.tokenViewModel(for: token)?.balance.value
if let selection = action.activeExcludingSelection(selectedTokenHolders: [tokenHolder], forWalletAddress: wallet.address, fungibleBalance: fungibleBalance) {
if selection.denial == nil {
return .isDisplayed(false)
}
}
return .noOption
}
switch wallet.type {
case .real:
return _configButton(action: action)
case .watch:
if session.config.development.shouldPretendIsRealWallet {
return _configButton(action: action)
} else {
return .isEnabled(false)
}
}
}
private func markerCapViewModel(for ticker: CoinTicker?) -> TokenAttributeViewModel {
let value: String = ticker?.market_cap.flatMap { StringFormatter().largeNumberFormatter(for: $0, currency: "USD") } ?? "-"
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value)
return .init(title: R.string.localizable.tokenInfoFieldStatsMarket_cap(), attributedValue: attributedValue)
}
private func totalSupplyViewModel(for ticker: CoinTicker?) -> TokenAttributeViewModel {
let value: String = ticker?.total_supply.flatMap { String($0) } ?? "-"
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value)
return .init(title: R.string.localizable.tokenInfoFieldStatsTotal_supply(), attributedValue: attributedValue)
}
private func maxSupplyViewModel(for ticker: CoinTicker?) -> TokenAttributeViewModel {
let value: String = ticker?.max_supply.flatMap { Formatter.usd.string(from: $0) } ?? "-"
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value)
return .init(title: R.string.localizable.tokenInfoFieldStatsMax_supply(), attributedValue: attributedValue)
}
private var yearLowViewModel: TokenAttributeViewModel {
let value: String = {
let history = chartHistories[ChartHistoryPeriod.year]
if let min = HistoryHelper(history: history).minMax?.min, let value = Formatter.usd.string(from: min) {
return value
} else {
return "-"
}
}()
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value)
return .init(title: R.string.localizable.tokenInfoFieldPerformanceYearLow(), attributedValue: attributedValue)
}
private var yearHighViewModel: TokenAttributeViewModel {
let value: String = {
let history = chartHistories[ChartHistoryPeriod.year]
if let max = HistoryHelper(history: history).minMax?.max, let value = Formatter.usd.string(from: max) {
return value
} else {
return "-"
}
}()
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value)
return .init(title: R.string.localizable.tokenInfoFieldPerformanceYearHigh(), attributedValue: attributedValue)
}
private var yearViewModel: TokenAttributeViewModel {
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.year)
return .init(title: R.string.localizable.tokenInfoFieldStatsYear(), attributedValue: attributedValue)
}
private var monthViewModel: TokenAttributeViewModel {
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.month)
return .init(title: R.string.localizable.tokenInfoFieldStatsMonth(), attributedValue: attributedValue)
}
private var weekViewModel: TokenAttributeViewModel {
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.week)
return .init(title: R.string.localizable.tokenInfoFieldStatsWeek(), attributedValue: attributedValue)
}
private var dayViewModel: TokenAttributeViewModel {
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.day)
return .init(title: R.string.localizable.tokenInfoFieldStatsDay(), attributedValue: attributedValue)
}
private func attributedHistoryValue(period: ChartHistoryPeriod) -> NSAttributedString {
let result: (string: String, foregroundColor: UIColor) = {
let result = HistoryHelper(history: chartHistories[period])
switch result.change {
case .appreciate(let percentage, let value):
let p = Formatter.percent.string(from: percentage) ?? "-"
let v = Formatter.usd.string(from: value) ?? "-"
return ("\(v) (\(p)%)", Style.value.appreciated)
case .depreciate(let percentage, let value):
let p = Formatter.percent.string(from: percentage) ?? "-"
let v = Formatter.usd.string(from: value) ?? "-"
return ("\(v) (\(p)%)", Style.value.depreciated)
case .none:
return ("-", Colors.black)
}
}()
return TokenAttributeViewModel.attributedString(result.string, alignment: .right, font: Fonts.regular(size: 17), foregroundColor: result.foregroundColor, lineBreakMode: .byTruncatingTail)
}
var backgroundColor: UIColor {
return Screen.TokenCard.Color.background
}
}
extension FungibleTokenDetailsViewModel {
enum TokenScriptWarningMessage {
case warning(string: String)
case undefined
}
enum ActionButtonState {
case isDisplayed(Bool)
case isEnabled(Bool)
case noOption
}
enum ViewType {
case charts
case testnet
case header(viewModel: TokenInfoHeaderViewModel)
case field(viewModel: TokenAttributeViewModel)
}
struct ViewState {
let actions: [TokenInstanceAction]
let views: [FungibleTokenDetailsViewModel.ViewType]
}
}

@ -0,0 +1,79 @@
//
// FungibleTokenTabViewModel.swift
// AlphaWallet
//
// Created by Vladyslav Shepitko on 20.11.2022.
//
import UIKit
import AlphaWalletFoundation
import Combine
struct FungibleTokenTabViewModelInput {
let willAppear: AnyPublisher<Void, Never>
}
struct FungibleTokenTabViewModelOutput {
let viewState: AnyPublisher<FungibleTokenTabViewModel.ViewState, Never>
}
class FungibleTokenTabViewModel {
private let token: Token
private let tokensService: TokenBalanceRefreshable & TokenViewModelState
private let assetDefinitionStore: AssetDefinitionStore
private var cancelable = Set<AnyCancellable>()
lazy var tokenScriptFileStatusHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore)
let session: WalletSession
let tabBarItems: [TabBarItem]
init(token: Token, session: WalletSession, tokensService: TokenBalanceRefreshable & TokenViewModelState, assetDefinitionStore: AssetDefinitionStore) {
self.tokensService = tokensService
self.assetDefinitionStore = assetDefinitionStore
self.token = token
self.session = session
let hasTicker = tokensService.tokenViewModel(for: token)?.balance.ticker != nil
if Features.default.isAvailable(.isAlertsEnabled) && hasTicker {
tabBarItems = [.details, .activities, .alerts]
} else {
tabBarItems = [.details, .activities]
}
}
func transform(input: FungibleTokenTabViewModelInput) -> FungibleTokenTabViewModelOutput {
input.willAppear
.sink { [tokensService, token] _ in
tokensService.refreshBalance(updatePolicy: .token(token: token))
}.store(in: &cancelable)
let viewState = tokensService
.tokenViewModelPublisher(for: token)
.map { $0?.tokenScriptOverrides?.titleInPluralForm }
.map { FungibleTokenTabViewModel.ViewState(title: $0) }
.eraseToAnyPublisher()
return .init(viewState: viewState)
}
}
extension FungibleTokenTabViewModel {
enum TabBarItem: CustomStringConvertible {
case details
case activities
case alerts
var description: String {
switch self {
case .details: return R.string.localizable.tokenTabInfo()
case .activities: return R.string.localizable.tokenTabActivity()
case .alerts: return R.string.localizable.priceAlertNavigationTitle()
}
}
}
struct ViewState {
let title: String?
}
}

@ -1,208 +0,0 @@
// Copyright © 2018 Stormbird PTE. LTD.
import Foundation
import UIKit
import BigInt
import PromiseKit
import Combine
import AlphaWalletFoundation
struct FungibleTokenViewModelInput {
let appear: AnyPublisher<Void, Never>
let updateAlert: AnyPublisher<(value: Bool, indexPath: IndexPath), Never>
let removeAlert: AnyPublisher<IndexPath, Never>
}
struct FungibleTokenViewModelOutput {
let viewState: AnyPublisher<FungibleTokenViewModel.ViewState, Never>
let activities: AnyPublisher<ActivityPageViewModel, Never>
let alerts: AnyPublisher<PriceAlertsPageViewModel, Never>
}
final class FungibleTokenViewModel {
private var cancelable = Set<AnyCancellable>()
private let coinTickersFetcher: CoinTickersFetcher
private let tokenActionsProvider: SupportedTokenActionsProvider
private let tokensService: TokenViewModelState & TokenBalanceRefreshable
private let activitiesService: ActivitiesServiceType
private let alertService: PriceAlertServiceType
private lazy var tokenHolder: TokenHolder = token.getTokenHolder(assetDefinitionStore: assetDefinitionStore, forWallet: session.account)
private (set) var actions: [TokenInstanceAction] = []
let session: WalletSession
let assetDefinitionStore: AssetDefinitionStore
var wallet: Wallet { session.account }
lazy var tokenScriptFileStatusHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore)
let token: Token
var tokenScriptStatus: Promise<TokenLevelTokenScriptDisplayStatus> {
let xmlHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore)
return xmlHandler.tokenScriptStatus
}
var hasCoinTicker: Bool {
return tokensService.tokenViewModel(for: token)?.balance.ticker != nil
}
lazy var tokenInfoPageViewModel = TokenInfoPageViewModel(token: token, coinTickersFetcher: coinTickersFetcher, tokensService: tokensService)
let backgroundColor: UIColor = Colors.appBackground
let sendButtonTitle: String = R.string.localizable.send()
let receiveButtonTitle: String = R.string.localizable.receive()
init(activitiesService: ActivitiesServiceType, alertService: PriceAlertServiceType, token: Token, session: WalletSession, assetDefinitionStore: AssetDefinitionStore, tokenActionsProvider: SupportedTokenActionsProvider, coinTickersFetcher: CoinTickersFetcher, tokensService: TokenViewModelState & TokenBalanceRefreshable) {
self.activitiesService = activitiesService
self.alertService = alertService
self.token = token
self.session = session
self.assetDefinitionStore = assetDefinitionStore
self.tokenActionsProvider = tokenActionsProvider
self.coinTickersFetcher = coinTickersFetcher
self.tokensService = tokensService
}
func tokenScriptWarningMessage(for action: TokenInstanceAction) -> TokenScriptWarningMessage? {
let fungibleBalance = tokensService.tokenViewModel(for: token)?.balance.value
if let selection = action.activeExcludingSelection(selectedTokenHolders: [tokenHolder], forWalletAddress: wallet.address, fungibleBalance: fungibleBalance) {
if let denialMessage = selection.denial {
return .warning(string: denialMessage)
} else {
//no-op shouldn't have reached here since the button should be disabled. So just do nothing to be safe
return .undefined
}
} else {
return nil
}
}
func buttonState(for action: TokenInstanceAction) -> ActionButtonState {
func _configButton(action: TokenInstanceAction) -> ActionButtonState {
let fungibleBalance = tokensService.tokenViewModel(for: token)?.balance.value
if let selection = action.activeExcludingSelection(selectedTokenHolders: [tokenHolder], forWalletAddress: wallet.address, fungibleBalance: fungibleBalance) {
if selection.denial == nil {
return .isDisplayed(false)
}
}
return .noOption
}
switch wallet.type {
case .real:
return _configButton(action: action)
case .watch:
if session.config.development.shouldPretendIsRealWallet {
return _configButton(action: action)
} else {
return .isEnabled(false)
}
}
}
func transform(input: FungibleTokenViewModelInput) -> FungibleTokenViewModelOutput {
input.appear.receive(on: RunLoop.main)
.sink { [tokensService, token] _ in tokensService.refreshBalance(updatePolicy: .token(token: token)) }
.store(in: &cancelable)
input.removeAlert
.sink { [alertService] in alertService.remove(indexPath: $0) }
.store(in: &cancelable)
input.updateAlert
.sink { [alertService] in alertService.update(indexPath: $0.indexPath, update: .enabled($0.value)) }
.store(in: &cancelable)
activitiesService.start()
let whenTokenHolderHasChanged = tokenHolder.objectWillChange
.map { [tokensService, token] _ in tokensService.tokenViewModel(for: token) }
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
let whenTokenActionsHasChanged = tokenActionsProvider.objectWillChange
.map { [tokensService, token] _ in tokensService.tokenViewModel(for: token) }
.receive(on: RunLoop.main)
.eraseToAnyPublisher()
let tokenViewModel = tokensService.tokenViewModelPublisher(for: token)
let actions = Publishers.MergeMany(tokenViewModel, whenTokenHolderHasChanged, whenTokenActionsHasChanged)
.compactMap { _ in self.buildTokenActions() }
.handleEvents(receiveOutput: { self.actions = $0 })
let title = tokenViewModel.compactMap { $0?.tokenScriptOverrides?.titleInPluralForm }
let activities = activitiesService.activitiesPublisher
.map { ActivityPageViewModel(activitiesViewModel: .init(collection: .init(activities: $0))) }
.receive(on: RunLoop.main)
let alerts = alertService.alertsPublisher(forStrategy: .token(token))
.map { PriceAlertsPageViewModel(alerts: $0) }
.receive(on: RunLoop.main)
let viewState = Publishers.CombineLatest(actions, title)
.map { actions, title in FungibleTokenViewModel.ViewState(title: title, actions: actions) }
return .init(viewState: viewState.eraseToAnyPublisher(),
activities: activities.eraseToAnyPublisher(),
alerts: alerts.eraseToAnyPublisher())
}
private func buildTokenActions() -> [TokenInstanceAction] {
let xmlHandler = XMLHandler(token: token, assetDefinitionStore: assetDefinitionStore)
let actionsFromTokenScript = xmlHandler.actions
infoLog("[TokenScript] actions names: \(actionsFromTokenScript.map(\.name))")
if actionsFromTokenScript.isEmpty {
switch token.type {
case .erc875, .erc721, .erc721ForTickets, .erc1155:
return []
case .erc20, .nativeCryptocurrency:
let actions: [TokenInstanceAction] = [
.init(type: .erc20Send),
.init(type: .erc20Receive)
]
return actions + tokenActionsProvider.actions(token: token)
}
} else {
switch token.type {
case .erc875, .erc721, .erc721ForTickets, .erc1155:
return []
case .erc20:
return actionsFromTokenScript + tokenActionsProvider.actions(token: token)
case .nativeCryptocurrency:
//TODO we should support retrieval of XML (and XMLHandler) based on address + server. For now, this is only important for native cryptocurrency. So might be ok to check like this for now
if let server = xmlHandler.server, server.matches(server: token.server) {
return actionsFromTokenScript + tokenActionsProvider.actions(token: token)
} else {
//TODO .erc20Send and .erc20Receive names aren't appropriate
let actions: [TokenInstanceAction] = [
.init(type: .erc20Send),
.init(type: .erc20Receive)
]
return actions + tokenActionsProvider.actions(token: token)
}
}
}
}
}
extension FungibleTokenViewModel {
enum TokenScriptWarningMessage {
case warning(string: String)
case undefined
}
enum ActionButtonState {
case isDisplayed(Bool)
case isEnabled(Bool)
case noOption
}
struct ViewState {
let title: String
let actions: [TokenInstanceAction]
}
}

@ -1,188 +0,0 @@
// Copyright © 2018 Stormbird PTE. LTD.
import UIKit
import Combine
import AlphaWalletFoundation
struct TokenInfoPageViewModelInput {
let appear: AnyPublisher<Void, Never>
}
struct TokenInfoPageViewModelOutput {
let viewState: AnyPublisher<TokenInfoPageViewModel.ViewState, Never>
}
final class TokenInfoPageViewModel {
private var chartHistoriesSubject: CurrentValueSubject<[ChartHistoryPeriod: ChartHistory], Never> = .init([:])
private let coinTickersFetcher: CoinTickersFetcher
private var ticker: CoinTicker?
private let tokensService: TokenViewModelState
private var cancelable = Set<AnyCancellable>()
private var chartHistories: [ChartHistoryPeriod: ChartHistory] { chartHistoriesSubject.value }
private lazy var coinTicker: AnyPublisher<CoinTicker?, Never> = {
return tokensService.tokenViewModelPublisher(for: token)
.map { $0?.balance.ticker }
.eraseToAnyPublisher()
}()
var tabTitle: String { return R.string.localizable.tokenTabInfo() }
let token: Token
lazy var chartViewModel: TokenHistoryChartViewModel = .init(chartHistories: chartHistoriesSubject.eraseToAnyPublisher(), coinTicker: coinTicker)
lazy var headerViewModel: FungibleTokenHeaderViewModel = .init(token: token, tokensService: tokensService)
init(token: Token, coinTickersFetcher: CoinTickersFetcher, tokensService: TokenViewModelState) {
self.tokensService = tokensService
self.coinTickersFetcher = coinTickersFetcher
self.token = token
}
func transform(input: TokenInfoPageViewModelInput) -> TokenInfoPageViewModelOutput {
input.appear.flatMapLatest { [coinTickersFetcher, token] _ in
coinTickersFetcher.fetchChartHistories(for: .init(token: token), force: false, periods: ChartHistoryPeriod.allCases)
}.assign(to: \.value, on: chartHistoriesSubject)
.store(in: &cancelable)
let coinTicker = coinTicker.handleEvents(receiveOutput: { [weak self] in self?.ticker = $0 }).map { _ in }
let chartHistories = chartHistoriesSubject.map { _ in }
let viewTypes = Publishers.Merge(coinTicker, chartHistories)
.compactMap { [weak self] _ in self?.buildViewTypes() }
let viewState = viewTypes
.map { TokenInfoPageViewModel.ViewState(views: $0) }
.eraseToAnyPublisher()
return .init(viewState: viewState)
}
private func buildViewTypes() -> [TokenInfoPageViewModel.ViewType] {
var views: [TokenInfoPageViewModel.ViewType] = []
if token.server.isTestnet {
views = [
.testnet
]
} else {
views = [
.charts,
.header(viewModel: .init(title: R.string.localizable.tokenInfoHeaderPerformance())),
.field(viewModel: dayViewModel),
.field(viewModel: weekViewModel),
.field(viewModel: monthViewModel),
.field(viewModel: yearViewModel),
.header(viewModel: .init(title: R.string.localizable.tokenInfoHeaderStats())),
.field(viewModel: markerCapViewModel),
.field(viewModel: yearLowViewModel),
.field(viewModel: yearHighViewModel)
]
}
return views
}
private var markerCapViewModel: TokenAttributeViewModel {
let value: String = ticker?.market_cap.flatMap { StringFormatter().largeNumberFormatter(for: $0, currency: "USD") } ?? "-"
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value)
return .init(title: R.string.localizable.tokenInfoFieldStatsMarket_cap(), attributedValue: attributedValue)
}
private var totalSupplyViewModel: TokenAttributeViewModel {
let value: String = ticker?.total_supply.flatMap { String($0) } ?? "-"
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value)
return .init(title: R.string.localizable.tokenInfoFieldStatsTotal_supply(), attributedValue: attributedValue)
}
private var maxSupplyViewModel: TokenAttributeViewModel {
let value: String = ticker?.max_supply.flatMap { Formatter.usd.string(from: $0) } ?? "-"
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value)
return .init(title: R.string.localizable.tokenInfoFieldStatsMax_supply(), attributedValue: attributedValue)
}
private var yearLowViewModel: TokenAttributeViewModel {
let value: String = {
let history = chartHistories[ChartHistoryPeriod.year]
if let min = HistoryHelper(history: history).minMax?.min, let value = Formatter.usd.string(from: min) {
return value
} else {
return "-"
}
}()
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value)
return .init(title: R.string.localizable.tokenInfoFieldPerformanceYearLow(), attributedValue: attributedValue)
}
private var yearHighViewModel: TokenAttributeViewModel {
let value: String = {
let history = chartHistories[ChartHistoryPeriod.year]
if let max = HistoryHelper(history: history).minMax?.max, let value = Formatter.usd.string(from: max) {
return value
} else {
return "-"
}
}()
let attributedValue = TokenAttributeViewModel.defaultValueAttributedString(value)
return .init(title: R.string.localizable.tokenInfoFieldPerformanceYearHigh(), attributedValue: attributedValue)
}
private var yearViewModel: TokenAttributeViewModel {
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.year)
return .init(title: R.string.localizable.tokenInfoFieldStatsYear(), attributedValue: attributedValue)
}
private var monthViewModel: TokenAttributeViewModel {
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.month)
return .init(title: R.string.localizable.tokenInfoFieldStatsMonth(), attributedValue: attributedValue)
}
private var weekViewModel: TokenAttributeViewModel {
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.week)
return .init(title: R.string.localizable.tokenInfoFieldStatsWeek(), attributedValue: attributedValue)
}
private var dayViewModel: TokenAttributeViewModel {
let attributedValue: NSAttributedString = attributedHistoryValue(period: ChartHistoryPeriod.day)
return .init(title: R.string.localizable.tokenInfoFieldStatsDay(), attributedValue: attributedValue)
}
private func attributedHistoryValue(period: ChartHistoryPeriod) -> NSAttributedString {
let result: (string: String, foregroundColor: UIColor) = {
let result = HistoryHelper(history: chartHistories[period])
switch result.change {
case .appreciate(let percentage, let value):
let p = Formatter.percent.string(from: percentage) ?? "-"
let v = Formatter.usd.string(from: value) ?? "-"
return ("\(v) (\(p)%)", Style.value.appreciated)
case .depreciate(let percentage, let value):
let p = Formatter.percent.string(from: percentage) ?? "-"
let v = Formatter.usd.string(from: value) ?? "-"
return ("\(v) (\(p)%)", Style.value.depreciated)
case .none:
return ("-", Colors.black)
}
}()
return TokenAttributeViewModel.attributedString(result.string, alignment: .right, font: Fonts.regular(size: 17), foregroundColor: result.foregroundColor, lineBreakMode: .byTruncatingTail)
}
var backgroundColor: UIColor {
return Screen.TokenCard.Color.background
}
}
extension TokenInfoPageViewModel {
enum ViewType {
case charts
case testnet
case header(viewModel: TokenInfoHeaderViewModel)
case field(viewModel: TokenAttributeViewModel)
}
struct ViewState {
let views: [TokenInfoPageViewModel.ViewType]
}
}

@ -17,7 +17,7 @@ public protocol PriceAlertServiceType: class {
func alertsPublisher(forStrategy strategy: PriceAlertsFilterStrategy) -> AnyPublisher<[PriceAlert], Never>
func alerts(forStrategy strategy: PriceAlertsFilterStrategy) -> [PriceAlert]
func start()
func add(alert: PriceAlert)
func add(alert: PriceAlert) -> Bool
func update(alert: PriceAlert, update: PriceAlertUpdates)
func update(indexPath: IndexPath, update: PriceAlertUpdates)
func remove(indexPath: IndexPath)
@ -41,9 +41,9 @@ public class PriceAlertService: PriceAlertServiceType {
datastore.alertsPublisher.map { alerts -> [PriceAlert] in
switch strategy {
case .token(let token):
return alerts.filter { $0.addressAndRPCServer == token.addressAndRPCServer }
return alerts.filter { $0.addressAndRPCServer == token.addressAndRPCServer }.uniqued()
case .all:
return alerts
return alerts.uniqued()
}
}.eraseToAnyPublisher()
}
@ -51,14 +51,17 @@ public class PriceAlertService: PriceAlertServiceType {
public func alerts(forStrategy strategy: PriceAlertsFilterStrategy) -> [PriceAlert] {
switch strategy {
case .token(let token):
return datastore.alerts.filter { $0.addressAndRPCServer == token.addressAndRPCServer }
return datastore.alerts.filter { $0.addressAndRPCServer == token.addressAndRPCServer }.uniqued()
case .all:
return datastore.alerts
return datastore.alerts.uniqued()
}
}
public func add(alert: PriceAlert) {
public func add(alert: PriceAlert) -> Bool {
guard !datastore.alerts.contains(where: { $0 == alert }) else { return false }
datastore.add(alert: alert)
return true
}
public func update(alert: PriceAlert, update: PriceAlertUpdates) {
@ -73,3 +76,15 @@ public class PriceAlertService: PriceAlertServiceType {
datastore.remove(indexPath: indexPath)
}
}
extension Sequence where Element: Hashable {
public func uniqued() -> [Element] {
var elements: [Element] = []
for value in self {
if !elements.contains(value) {
elements.append(value)
}
}
return elements
}
}

@ -12,7 +12,34 @@ public enum PriceTarget: String, Codable {
case below
}
public enum AlertType: Codable {
public enum AlertType {
case price(priceTarget: PriceTarget, value: Double)
public init(value: Double, marketPrice: Double) {
let priceTarget: PriceTarget = value > marketPrice ? .above : .below
self = .price(priceTarget: priceTarget, value: value)
}
}
public struct PriceAlert {
public var type: AlertType
public var isEnabled: Bool
public let addressAndRPCServer: AddressAndRPCServer
}
extension PriceAlert: Codable, Equatable, Hashable {
public init(type: AlertType, token: Token, isEnabled: Bool) {
self.addressAndRPCServer = token.addressAndRPCServer
self.type = type
self.isEnabled = isEnabled
}
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.addressAndRPCServer == rhs.addressAndRPCServer && lhs.isEnabled == rhs.isEnabled && lhs.type == rhs.type
}
}
extension AlertType: Codable, Hashable {
private enum CodingKeys: String, CodingKey {
case priceTarget
case value
@ -36,27 +63,4 @@ public enum AlertType: Codable {
self = .price(priceTarget: priceTarget, value: value)
}
case price(priceTarget: PriceTarget, value: Double)
public init(value: Double, marketPrice: Double) {
let priceTarget: PriceTarget = value > marketPrice ? .above : .below
self = .price(priceTarget: priceTarget, value: value)
}
}
public struct PriceAlert: Codable, Equatable {
public var type: AlertType
public var isEnabled: Bool
public let addressAndRPCServer: AddressAndRPCServer
public init(type: AlertType, token: Token, isEnabled: Bool) {
self.addressAndRPCServer = token.addressAndRPCServer
self.type = type
self.isEnabled = isEnabled
}
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.addressAndRPCServer == rhs.addressAndRPCServer
}
}

@ -87,7 +87,7 @@ public class BaseCoinTickersFetcher {
let publishers = periods.map { fetchChartHistory(force: force, period: $0, for: token, currency: Currency.USD.rawValue) }
return Publishers.MergeMany(publishers).collect()
.map { $0.reorder(by: periods)/*.map { $0.history }*/ }
.map { $0.reorder(by: periods) }
.map { mapped -> [ChartHistoryPeriod: ChartHistory] in
var values: [ChartHistoryPeriod: ChartHistory] = [:]
for each in mapped {
@ -133,6 +133,8 @@ public class BaseCoinTickersFetcher {
}
private func hasExpired(history mappedChartHistory: MappedChartHistory, for period: ChartHistoryPeriod) -> Bool {
guard ReachabilityManager().isReachable else { return false }
let hasCacheExpired: Bool
switch period {
case .day:

Loading…
Cancel
Save