Fetch, store and display CrypoKitties using https://opensea.io

pull/581/head
Hwee-Boon Yar 6 years ago
parent bf31e1ceaa
commit dd1ce49675
  1. 34
      AlphaWallet.xcodeproj/project.pbxproj
  2. 1
      AlphaWallet/AssetDefinition/AssetAttribute.swift
  3. 2
      AlphaWallet/Core/Initializers/MigrationInitializer.swift
  4. 2
      AlphaWallet/Localization/en.lproj/Localizable.strings
  5. 2
      AlphaWallet/Localization/es.lproj/Localizable.strings
  6. 2
      AlphaWallet/Localization/zh-Hans.lproj/Localizable.strings
  7. 19
      AlphaWallet/Market/ViewModels/ImportTicketViewControllerViewModel.swift
  8. 13
      AlphaWallet/Redeem/ViewControllers/RedeemTicketsQuantitySelectionViewController.swift
  9. 23
      AlphaWallet/Redeem/ViewControllers/RedeemTicketsViewController.swift
  10. 13
      AlphaWallet/Redeem/ViewControllers/TicketRedemptionViewController.swift
  11. 9
      AlphaWallet/Redeem/ViewModels/BaseTicketTableViewCellViewModel.swift
  12. 20
      AlphaWallet/Redeem/ViewModels/TicketRowViewModel.swift
  13. 121
      AlphaWallet/Redeem/ViewModels/TokenListFormatRowViewModel.swift
  14. 6
      AlphaWallet/Redeem/Views/TicketRowView.swift
  15. 227
      AlphaWallet/Redeem/Views/TokenListFormatRowView.swift
  16. 10
      AlphaWallet/Redeem/Views/TokenListFormatTableViewCellWithCheckbox.swift
  17. 14
      AlphaWallet/Redeem/Views/TokenRowView.swift
  18. 13
      AlphaWallet/Sell/ViewControllers/EnterSellTicketsPriceQuantityViewController.swift
  19. 23
      AlphaWallet/Sell/ViewControllers/SellTicketsViewController.swift
  20. 27
      AlphaWallet/Sell/ViewControllers/SetSellTicketsExpiryDateViewController.swift
  21. 28
      AlphaWallet/Sell/ViewModels/SetSellTicketsExpiryDateViewControllerViewModel.swift
  22. 16
      AlphaWallet/Settings/Types/Constants.swift
  23. 74
      AlphaWallet/Tokens/Helpers/TokenAdaptor.swift
  24. 22
      AlphaWallet/Tokens/Types/CryptoKitty.swift
  25. 25
      AlphaWallet/Tokens/Types/CryptoKittyHandling.swift
  26. 32
      AlphaWallet/Tokens/Types/Ticket.swift
  27. 15
      AlphaWallet/Tokens/Types/TokenHolder.swift
  28. 72
      AlphaWallet/Tokens/Types/TokensDataStore.swift
  29. 1
      AlphaWallet/Tokens/ViewControllers/NewTokenViewController.swift
  30. 22
      AlphaWallet/Tokens/ViewControllers/TicketsViewController.swift
  31. 17
      AlphaWallet/Tokens/ViewModels/TicketTableViewCellViewModel.swift
  32. 66
      AlphaWallet/Tokens/Views/BaseTokenListFormatTableViewCell.swift
  33. 10
      AlphaWallet/Tokens/Views/TokenListFormatTableViewCellWithoutCheckbox.swift
  34. 25
      AlphaWallet/Transactions/Coordinators/TicketsCoordinator.swift
  35. 13
      AlphaWallet/Transfer/ViewControllers/ChooseTicketTransferModeViewController.swift
  36. 13
      AlphaWallet/Transfer/ViewControllers/SetTransferTicketsExpiryDateViewController.swift
  37. 15
      AlphaWallet/Transfer/ViewControllers/TransferTicketsQuantitySelectionViewController.swift
  38. 13
      AlphaWallet/Transfer/ViewControllers/TransferTicketsViaWalletAddressViewController.swift
  39. 30
      AlphaWallet/Transfer/ViewControllers/TransferTicketsViewController.swift

@ -277,6 +277,7 @@
5E7C701BFF4469B35A074EB9 /* RequestViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C767497AD8DEE83F384D7 /* RequestViewModel.swift */; };
5E7C70AE62DBB193399C7F5E /* ServerViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CDB0BAD5D27D2F24F57 /* ServerViewCell.swift */; };
5E7C70BE9AE35408038E1971 /* HelpContentsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B089FD4C96810DD10FD /* HelpContentsViewController.swift */; };
5E7C70E4E194FEA5DA2F610C /* CryptoKitty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7382EAC8B9CE5EE0668D /* CryptoKitty.swift */; };
5E7C70FF17622C0FFD45A542 /* AlphaWalletSettingPushRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D2AAB777BF35B8B56BD /* AlphaWalletSettingPushRow.swift */; };
5E7C710331196CD591B51785 /* LockCreatePasscodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C741196D9D9C9C3EE5E30 /* LockCreatePasscodeViewController.swift */; };
5E7C713ACE8C72642B1C9F93 /* SendHeaderViewViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B7A45EDFA8ED1E25863 /* SendHeaderViewViewModel.swift */; };
@ -290,6 +291,7 @@
5E7C71DAA5DAFF764F92587D /* SetTransferTicketsExpiryDateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C727433F7B8E322B3C68A /* SetTransferTicketsExpiryDateViewController.swift */; };
5E7C71F8050CCF990539B293 /* LockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C79D674D45A07E694CE31 /* LockView.swift */; };
5E7C7208A83399C27AE57E44 /* ImportWalletHelpBubbleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C70CC85B337061151724E /* ImportWalletHelpBubbleView.swift */; };
5E7C724638271FD2FA0EB93C /* BaseTokenListFormatTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C734D61C0347C1638A1F7 /* BaseTokenListFormatTableViewCell.swift */; };
5E7C72670E16AFB8DAF64673 /* OnboardingPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7E24936CC2190D2A16C2 /* OnboardingPageViewModel.swift */; };
5E7C728CDF33FBDBA47F71A6 /* MarketplaceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C794F8EBAEE5E8F2821C2 /* MarketplaceViewController.swift */; };
5E7C72AF95DCE8BC65490BCA /* StatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B82CC07F290B9CAA4E4 /* StatusViewController.swift */; };
@ -308,6 +310,7 @@
5E7C73FD5BD75D90C8D0EF3C /* WalletFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C58586099F082973073 /* WalletFilterView.swift */; };
5E7C7402B29A987B0AF7061D /* VerifiableStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CC48CA7A1EA7D539C87 /* VerifiableStatusViewController.swift */; };
5E7C741353DDF87133054FCC /* DeletedContract.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72BEB789700C49FF64A6 /* DeletedContract.swift */; };
5E7C7420D72B8DF96A60C56A /* TokenListFormatRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7669A1A6973BBF2F8620 /* TokenListFormatRowViewModel.swift */; };
5E7C745A423BD10CFDED9A81 /* ServersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7CBEBF984CFCA29D6866 /* ServersViewModel.swift */; };
5E7C745C725F3F34037DCC68 /* SetTransferTicketsExpiryDateViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C754C0E2E57F32A61F9A3 /* SetTransferTicketsExpiryDateViewControllerViewModel.swift */; };
5E7C745DACB5FCCEBCEB49CA /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C793E23E2364B73C4D813 /* WelcomeViewController.swift */; };
@ -317,6 +320,7 @@
5E7C74B99922D0CAB635970E /* PasscodeCharacterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B9220E616F82EDA956F /* PasscodeCharacterView.swift */; };
5E7C74BD08801CABF9695853 /* LocaleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C79778E4BFE1322711EA6 /* LocaleViewModel.swift */; };
5E7C74DBAE43954C185057B3 /* ChooseTicketTransferModeViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7BA578BE5FB0E613A6D6 /* ChooseTicketTransferModeViewControllerViewModel.swift */; };
5E7C75216FDB686D7D4BB06F /* TokenRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C946B522B15BB860DE1 /* TokenRowView.swift */; };
5E7C7567A690B6B8F889AE83 /* SendViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C70088832B2D161EB4AAB /* SendViewController.swift */; };
5E7C75B37912E6B68C30D689 /* AssetDefinitionDiskBackingStoreWithOverrides.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C017A044087BEA30CC0 /* AssetDefinitionDiskBackingStoreWithOverrides.swift */; };
5E7C75C99B9F595F26EDC405 /* LockPasscodeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7D5F3CAE69CF932AB236 /* LockPasscodeViewController.swift */; };
@ -326,6 +330,7 @@
5E7C75F80A7E178B49830BCD /* TicketsViewControllerHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C796039C0F47CDCA236C0 /* TicketsViewControllerHeader.swift */; };
5E7C760C7D55C97424F55138 /* TicketTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75F877B2F2E24C7EF258 /* TicketTableViewCellViewModel.swift */; };
5E7C760F9D7C178E17C75C52 /* ImportTicketViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7487BDF72352446E1266 /* ImportTicketViewControllerTests.swift */; };
5E7C7613C97327C740D7CF5A /* TokenListFormatTableViewCellWithCheckbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7F00DE77B4FD5BAB1A10 /* TokenListFormatTableViewCellWithCheckbox.swift */; };
5E7C764D3C130AAB26E80EC1 /* AmountTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C73617E3A4C0B9A90A5F8 /* AmountTextField.swift */; };
5E7C76696EF7F27EC0788CDD /* GenerateTransferMagicLinkViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7EEAAE9C23B68419E9F5 /* GenerateTransferMagicLinkViewControllerViewModel.swift */; };
5E7C7669BBE6255A2377E070 /* SetSellTicketsExpiryDateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7962AE417E12F13FF58E /* SetSellTicketsExpiryDateViewController.swift */; };
@ -347,13 +352,15 @@
5E7C77A8425E0AFAB11F1FCD /* PromptBackupCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7ADD0FBE8708A6E98AF8 /* PromptBackupCoordinator.swift */; };
5E7C77AD9FAAC18211B6F355 /* TransferTicketsQuantitySelectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7419F47CC8B2996AA8F9 /* TransferTicketsQuantitySelectionViewController.swift */; };
5E7C77E844D710D7AFBC58D4 /* RequestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74DCC21272EC231A20E2 /* RequestViewController.swift */; };
5E7C7813019A111443A542CA /* TokenListFormatTableViewCellWithoutCheckbox.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C781CCE43B6451671B9 /* TokenListFormatTableViewCellWithoutCheckbox.swift */; };
5E7C782410321CE6CEE68275 /* AssetDefinitionDiskBackingStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7BF5551BF64D2AE8AD66 /* AssetDefinitionDiskBackingStore.swift */; };
5E7C783B4784DE76971EEBB4 /* StatusViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7BD9B4BDAFC2D9EBD741 /* StatusViewControllerViewModel.swift */; };
5E7C78407F6DCB0EDD562DF6 /* TicketTokenViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C731B6F01534683227123 /* TicketTokenViewCellViewModel.swift */; };
5E7C786AD8E4877C36D3B14A /* TicketAdaptorTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C775FD95FE80B0F1CEA33 /* TicketAdaptorTest.swift */; };
5E7C78B3FD5CA87E395E1861 /* OnboardingPageStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AF9A592D7224ED58016 /* OnboardingPageStyle.swift */; };
5E7C78F1D29280E3FF4EAF5E /* RoundedBackground.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75918317E13AD540DCA7 /* RoundedBackground.swift */; };
5E7C790F42EAB5BC9CC9FFFE /* AssetAttributeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C759D3BBDDFB39AD262AA /* AssetAttributeTests.swift */; };
5E7C790F42EAB5BC9CC9FFFE /* PBXBuildFile */ = {isa = PBXBuildFile; fileRef = 5E7C759D3BBDDFB39AD262AA /* AssetAttributeTests.swift */; };
5E7C7924A51816692D36DA18 /* TokenListFormatRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C788BA3AB345D4E7D7C8E /* TokenListFormatRowView.swift */; };
5E7C793F7E346402CDAF771F /* AssetDefinitionStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7FE30D58E4022AF04E48 /* AssetDefinitionStoreTests.swift */; };
5E7C797BE2C8DB7EF6F217B3 /* OnboardingPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7103135DCCCAB96EE5FC /* OnboardingPage.swift */; };
5E7C798E5F5EE00D405B91AE /* TicketRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7ACB94CEE493AC37487F /* TicketRowView.swift */; };
@ -369,6 +376,7 @@
5E7C7AE2EF04A23EC7C5ADFD /* ImportTicketViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7535095323B035CA47C0 /* ImportTicketViewController.swift */; };
5E7C7B0367CFB413C6885474 /* GenerateSellMagicLinkViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7624D6F7EA55F6F167B3 /* GenerateSellMagicLinkViewControllerViewModel.swift */; };
5E7C7B3E08EEA63C5B68B9C4 /* TicketRedemptionInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C778F20D32B70D7FF2135 /* TicketRedemptionInfoViewController.swift */; };
5E7C7B414FA8A428798D73EF /* CryptoKittyHandling.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C758EEBD945A3451C96C8 /* CryptoKittyHandling.swift */; };
5E7C7B4E3DEA90147A5A9E0A /* TokensDataStoreTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C71E355BD14E975AF7491 /* TokensDataStoreTest.swift */; };
5E7C7C0FAC500A6651E663FD /* TransferTicketsQuantitySelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C703BA1D0E9ACB7399155 /* TransferTicketsQuantitySelectionViewModel.swift */; };
5E7C7C21E5CAF122AA4F6617 /* HowDoIGetMyMoneyInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C78B001F9F95F404D5FEF /* HowDoIGetMyMoneyInfoViewController.swift */; };
@ -841,7 +849,9 @@
5E7C72D0E7CA03ADE5CFAE7A /* ImportTicketViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportTicketViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C731B6F01534683227123 /* TicketTokenViewCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TicketTokenViewCellViewModel.swift; sourceTree = "<group>"; };
5E7C73495E0C0A207152EC25 /* LockEnterPasscodeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LockEnterPasscodeViewController.swift; path = AlphaWallet/AlphaWalletLock/ViewControllers/LockEnterPasscodeViewController.swift; sourceTree = SOURCE_ROOT; };
5E7C734D61C0347C1638A1F7 /* BaseTokenListFormatTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTokenListFormatTableViewCell.swift; sourceTree = "<group>"; };
5E7C73617E3A4C0B9A90A5F8 /* AmountTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmountTextField.swift; sourceTree = "<group>"; };
5E7C7382EAC8B9CE5EE0668D /* CryptoKitty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoKitty.swift; sourceTree = "<group>"; };
5E7C7393B08745287A266000 /* StringExtensionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = StringExtensionTests.swift; path = Extensions/StringExtensionTests.swift; sourceTree = "<group>"; };
5E7C73D26F24C4AAE981E2F2 /* ImportWalletTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletTab.swift; sourceTree = "<group>"; };
5E7C73DF5FBFE756097D32B1 /* EthCurrencyHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EthCurrencyHelper.swift; sourceTree = "<group>"; };
@ -862,6 +872,7 @@
5E7C754C0E2E57F32A61F9A3 /* SetTransferTicketsExpiryDateViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetTransferTicketsExpiryDateViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C755132D9B6F95080A1BE /* TransferTicketsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferTicketsCoordinator.swift; sourceTree = "<group>"; };
5E7C7564AF453BAB0BDAAA57 /* SettingsAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAction.swift; sourceTree = "<group>"; };
5E7C758EEBD945A3451C96C8 /* CryptoKittyHandling.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CryptoKittyHandling.swift; sourceTree = "<group>"; };
5E7C75918317E13AD540DCA7 /* RoundedBackground.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoundedBackground.swift; sourceTree = "<group>"; };
5E7C7596408BA84E95C90ADA /* AssetAttribute.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetAttribute.swift; sourceTree = "<group>"; };
5E7C759D3BBDDFB39AD262AA /* AssetAttributeTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetAttributeTests.swift; sourceTree = "<group>"; };
@ -876,6 +887,7 @@
5E7C764B98F526271E4C2A6A /* StaticHTMLViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaticHTMLViewController.swift; sourceTree = "<group>"; };
5E7C764DD34226D5639672B7 /* FetchAssetDefinitionsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FetchAssetDefinitionsCoordinator.swift; sourceTree = "<group>"; };
5E7C765E0DC0174E9788CCF9 /* EnterSellTicketsPriceQuantityViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnterSellTicketsPriceQuantityViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C7669A1A6973BBF2F8620 /* TokenListFormatRowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenListFormatRowViewModel.swift; sourceTree = "<group>"; };
5E7C767497AD8DEE83F384D7 /* RequestViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestViewModel.swift; sourceTree = "<group>"; };
5E7C76AF81B8DFF605558499 /* UniversalLinkCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniversalLinkCoordinator.swift; sourceTree = "<group>"; };
5E7C76C895E7BFA47233068C /* LocalesCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LocalesCoordinator.swift; sourceTree = "<group>"; };
@ -890,6 +902,7 @@
5E7C7821694C489D5114DB18 /* TicketsViewControllerTitleHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TicketsViewControllerTitleHeader.swift; sourceTree = "<group>"; };
5E7C7828BD821B6F04B71C00 /* SendHeaderView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SendHeaderView.swift; sourceTree = "<group>"; };
5E7C783E3ADA4CF9554A0E7D /* TicketTokenViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TicketTokenViewCell.swift; sourceTree = "<group>"; };
5E7C788BA3AB345D4E7D7C8E /* TokenListFormatRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenListFormatRowView.swift; sourceTree = "<group>"; };
5E7C78B001F9F95F404D5FEF /* HowDoIGetMyMoneyInfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HowDoIGetMyMoneyInfoViewController.swift; sourceTree = "<group>"; };
5E7C78B63FDE2FAF25389260 /* TransferTicketsViaWalletAddressViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferTicketsViaWalletAddressViewController.swift; sourceTree = "<group>"; };
5E7C793E23E2364B73C4D813 /* WelcomeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeViewController.swift; sourceTree = "<group>"; };
@ -931,6 +944,8 @@
5E7C7C077372C3F2A4349FA1 /* TokenViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenViewCell.swift; sourceTree = "<group>"; };
5E7C7C2872E213BBB05D55BA /* ImportWalletTabBarViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletTabBarViewModel.swift; sourceTree = "<group>"; };
5E7C7C58586099F082973073 /* WalletFilterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletFilterView.swift; sourceTree = "<group>"; };
5E7C7C781CCE43B6451671B9 /* TokenListFormatTableViewCellWithoutCheckbox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenListFormatTableViewCellWithoutCheckbox.swift; sourceTree = "<group>"; };
5E7C7C946B522B15BB860DE1 /* TokenRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenRowView.swift; sourceTree = "<group>"; };
5E7C7C963C42DE81F82732E5 /* AssetDefinitionBackingStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AssetDefinitionBackingStore.swift; sourceTree = "<group>"; };
5E7C7CBEBF984CFCA29D6866 /* ServersViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServersViewModel.swift; sourceTree = "<group>"; };
5E7C7CC48CA7A1EA7D539C87 /* VerifiableStatusViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerifiableStatusViewController.swift; sourceTree = "<group>"; };
@ -950,6 +965,7 @@
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>"; };
5E7C7F00DE77B4FD5BAB1A10 /* TokenListFormatTableViewCellWithCheckbox.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenListFormatTableViewCellWithCheckbox.swift; sourceTree = "<group>"; };
5E7C7F3DD81D44A996789FC4 /* UniversalLinkInPasteboardCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniversalLinkInPasteboardCoordinator.swift; sourceTree = "<group>"; };
5E7C7F5C10E3895E805EA7E0 /* BaseTicketTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseTicketTableViewCell.swift; sourceTree = "<group>"; };
5E7C7F610139D24D947B1625 /* EnterSellTicketsPriceQuantityViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EnterSellTicketsPriceQuantityViewController.swift; sourceTree = "<group>"; };
@ -1713,6 +1729,8 @@
5E7C70CC85B337061151724E /* ImportWalletHelpBubbleView.swift */,
5E7C7F5C10E3895E805EA7E0 /* BaseTicketTableViewCell.swift */,
5E7C7CFDE7DEA8C06C4100AF /* TextField.swift */,
5E7C734D61C0347C1638A1F7 /* BaseTokenListFormatTableViewCell.swift */,
5E7C7C781CCE43B6451671B9 /* TokenListFormatTableViewCellWithoutCheckbox.swift */,
);
path = Views;
sourceTree = "<group>";
@ -1764,6 +1782,8 @@
442FCFEB2D7443C4E0B889B0 /* TokenHolder.swift */,
5E7C74B9EB81C51E956566E7 /* TokensDataStore.swift */,
5E7C72BEB789700C49FF64A6 /* DeletedContract.swift */,
5E7C7382EAC8B9CE5EE0668D /* CryptoKitty.swift */,
5E7C758EEBD945A3451C96C8 /* CryptoKittyHandling.swift */,
);
path = Types;
sourceTree = "<group>";
@ -2307,6 +2327,9 @@
442FCD4311FCAFE6FB288A5E /* TicketTableViewCellWithCheckbox.swift */,
5E7C7821694C489D5114DB18 /* TicketsViewControllerTitleHeader.swift */,
5E7C7ACB94CEE493AC37487F /* TicketRowView.swift */,
5E7C7F00DE77B4FD5BAB1A10 /* TokenListFormatTableViewCellWithCheckbox.swift */,
5E7C788BA3AB345D4E7D7C8E /* TokenListFormatRowView.swift */,
5E7C7C946B522B15BB860DE1 /* TokenRowView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -2319,6 +2342,7 @@
442FCBDB86579889BC773540 /* TicketRedemptionViewModel.swift */,
5E7C77061BEF269BCE358086 /* BaseTicketTableViewCellViewModel.swift */,
5E7C7742709724B3BD0C2A0D /* TicketRowViewModel.swift */,
5E7C7669A1A6973BBF2F8620 /* TokenListFormatRowViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
@ -3706,6 +3730,14 @@
5E7C79DE8864702C51C0A7CC /* ResultResult.swift in Sources */,
5E7C7AE2EF04A23EC7C5ADFD /* ImportTicketViewController.swift in Sources */,
5E7C700A0B11504AC44718DD /* CanScanQRCode.swift in Sources */,
5E7C70E4E194FEA5DA2F610C /* CryptoKitty.swift in Sources */,
5E7C724638271FD2FA0EB93C /* BaseTokenListFormatTableViewCell.swift in Sources */,
5E7C7813019A111443A542CA /* TokenListFormatTableViewCellWithoutCheckbox.swift in Sources */,
5E7C7613C97327C740D7CF5A /* TokenListFormatTableViewCellWithCheckbox.swift in Sources */,
5E7C7924A51816692D36DA18 /* TokenListFormatRowView.swift in Sources */,
5E7C7420D72B8DF96A60C56A /* TokenListFormatRowViewModel.swift in Sources */,
5E7C7B414FA8A428798D73EF /* CryptoKittyHandling.swift in Sources */,
5E7C75216FDB686D7D4BB06F /* TokenRowView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

@ -8,6 +8,7 @@ protocol AssetAttributeValue {}
extension String: AssetAttributeValue {}
extension Int: AssetAttributeValue {}
extension GeneralisedTime: AssetAttributeValue {}
extension Array: AssetAttributeValue {}
enum AssetAttributeSyntax: String {
case directoryString = "1.3.6.1.4.1.1466.115.121.1.15"

@ -20,7 +20,7 @@ class MigrationInitializer: Initializer {
}
func perform() {
config.schemaVersion = 45
config.schemaVersion = 46
config.migrationBlock = { migration, oldSchemaVersion in
switch oldSchemaVersion {
case 0...32:

@ -279,3 +279,5 @@
"camera.qrCode.denied.prompt.title" = "Camera Access Required to Scan QR Code";
"camera.qrCode.denied.prompt.message" = "Your privacy settings are preventing us from accessing your camera for QR code scanning. Fix this by:\n\n1. Tap the Open Settings button below to open the Settings app.\n\n2. Tap to enable the Camera on.\n\n3. Launch this app again.";
"camera.qrCode.denied.prompt.button" = "Open Settings";
"cryptoKitties.url.open" = "Open on CryptoKitties";
"cryptoKitties.cat.name" = "CryptoKitty #%@";

@ -279,3 +279,5 @@
"camera.qrCode.denied.prompt.title" = "Camera Access Required to Scan QR Code";
"camera.qrCode.denied.prompt.message" = "Your privacy settings are preventing us from accessing your camera for QR code scanning. Fix this by:\n\n1. Tap the Open Settings button below to open the Settings app.\n\n2. Tap to enable the Camera on.\n\n3. Launch this app again.";
"camera.qrCode.denied.prompt.button" = "Open Settings";
"cryptoKitties.url.open" = "Open on CryptoKitties";
"cryptoKitties.cat.name" = "CryptoKitty #%@";

@ -279,3 +279,5 @@
"camera.qrCode.denied.prompt.title" = "Camera Access Required to Scan QR Code";
"camera.qrCode.denied.prompt.message" = "Your privacy settings are preventing us from accessing your camera for QR code scanning. Fix this by:\n\n1. Tap the Open Settings button below to open the Settings app.\n\n2. Tap to enable the Camera on.\n\n3. Launch this app again.";
"camera.qrCode.denied.prompt.button" = "Open Settings";
"cryptoKitties.url.open" = "Open on CryptoKitties";
"cryptoKitties.cat.name" = "CryptoKitty #%@";

@ -75,7 +75,7 @@ struct ImportTicketViewControllerViewModel {
if case .validating = state {
return ""
} else {
return ticketHolder.city
return ticketHolder.values["locality"] as? String ?? "N/A"
}
}
@ -84,7 +84,7 @@ struct ImportTicketViewControllerViewModel {
if case .validating = state {
return ""
} else {
return String(ticketHolder.category)
return ticketHolder.values["category"] as? String ?? "N/A"
}
}
@ -93,7 +93,8 @@ struct ImportTicketViewControllerViewModel {
if case .validating = state {
return ""
} else {
return ticketHolder.date.format("hh:mm")
let value = ticketHolder.values["time"] as? GeneralisedTime ?? GeneralisedTime()
return value.format("hh:mm")
}
}
@ -102,13 +103,16 @@ struct ImportTicketViewControllerViewModel {
if case .validating = state {
return ""
} else {
return R.string.localizable.aWalletTicketTokenMatchVs(ticketHolder.countryA, ticketHolder.countryB)
let countryA = ticketHolder.values["countryA"] as? String ?? ""
let countryB = ticketHolder.values["countryB"] as? String ?? ""
return R.string.localizable.aWalletTicketTokenMatchVs(countryA, countryB)
}
}
var match: String {
guard let ticketHolder = ticketHolder else { return "" }
return "M\(ticketHolder.match)"
let value = ticketHolder.values["match"] as? Int ?? 0
return "M\(value)"
}
var venue: String {
@ -116,7 +120,7 @@ struct ImportTicketViewControllerViewModel {
if case .validating = state {
return ""
} else {
return ticketHolder.venue
return ticketHolder.values["venue"] as? String ?? "N/A"
}
}
@ -125,7 +129,8 @@ struct ImportTicketViewControllerViewModel {
if case .validating = state {
return ""
} else {
return ticketHolder.date.format("dd MMM yyyy")
let value = ticketHolder.values["time"] as? GeneralisedTime ?? GeneralisedTime()
return value.format("dd MMM yyyy")
}
}

@ -25,7 +25,7 @@ class RedeemTicketsQuantitySelectionViewController: UIViewController, TicketVeri
let header = TicketsViewControllerTitleHeader()
let subtitleLabel = UILabel()
var quantityStepper = NumberStepper()
let ticketView = TicketRowView()
let ticketView: TokenRowView & UIView
let nextButton = UIButton(type: .system)
var viewModel: RedeemTicketsQuantitySelectionViewModel
weak var delegate: RedeemTicketsQuantitySelectionViewControllerDelegate?
@ -34,6 +34,15 @@ class RedeemTicketsQuantitySelectionViewController: UIViewController, TicketVeri
self.config = config
self.token = token
self.viewModel = viewModel
let tokenType = CryptoKittyHandling(address: token.address)
switch tokenType {
case .cryptoKitty:
ticketView = TokenListFormatRowView()
case .otherNonFungibleToken:
ticketView = TicketRowView()
}
super.init(nibName: nil, bundle: nil)
updateNavigationRightBarButtons(isVerified: true)
@ -142,7 +151,7 @@ class RedeemTicketsQuantitySelectionViewController: UIViewController, TicketVeri
subtitleLabel.font = viewModel.subtitleFont
subtitleLabel.text = viewModel.subtitleText
ticketView.configure(viewModel: .init(ticketHolder: viewModel.ticketHolder))
ticketView.configure(tokenHolder: viewModel.ticketHolder)
quantityStepper.borderWidth = 1
quantityStepper.clipsToBounds = true

@ -12,6 +12,7 @@ protocol RedeemTicketsViewControllerDelegate: class {
func didSelectTicketHolder(token: TokenObject, ticketHolder: TokenHolder, in viewController: RedeemTicketsViewController)
func didPressViewInfo(in viewController: RedeemTicketsViewController)
func didPressViewContractWebPage(in viewController: RedeemTicketsViewController)
func didTapURL(url: URL, in viewController: RedeemTicketsViewController)
}
class RedeemTicketsViewController: UIViewController, TicketVerifiableStatusViewController {
@ -42,6 +43,7 @@ class RedeemTicketsViewController: UIViewController, TicketVerifiableStatusViewC
view.addSubview(roundedBackground)
tableView.register(TicketTableViewCellWithCheckbox.self, forCellReuseIdentifier: TicketTableViewCellWithCheckbox.identifier)
tableView.register(TokenListFormatTableViewCellWithCheckbox.self, forCellReuseIdentifier: TokenListFormatTableViewCellWithCheckbox.identifier)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.separatorStyle = .none
@ -139,10 +141,19 @@ extension RedeemTicketsViewController: UITableViewDelegate, UITableViewDataSourc
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TicketTableViewCellWithCheckbox.identifier, for: indexPath) as! TicketTableViewCellWithCheckbox
let ticketHolder = viewModel.item(for: indexPath)
cell.configure(viewModel: .init(ticketHolder: ticketHolder))
return cell
let tokenType = CryptoKittyHandling(contract: ticketHolder.contractAddress)
switch tokenType {
case .cryptoKitty:
let cell = tableView.dequeueReusableCell(withIdentifier: TokenListFormatTableViewCellWithCheckbox.identifier, for: indexPath) as! TokenListFormatTableViewCellWithCheckbox
cell.delegate = self
cell.configure(viewModel: .init(ticketHolder: ticketHolder))
return cell
case .otherNonFungibleToken:
let cell = tableView.dequeueReusableCell(withIdentifier: TicketTableViewCellWithCheckbox.identifier, for: indexPath) as! TicketTableViewCellWithCheckbox
cell.configure(viewModel: .init(ticketHolder: ticketHolder))
return cell
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
@ -150,3 +161,9 @@ extension RedeemTicketsViewController: UITableViewDelegate, UITableViewDataSourc
animateRowHeightChanges(for: changedIndexPaths, in: tableView)
}
}
extension RedeemTicketsViewController: BaseTokenListFormatTableViewCellDelegate {
func didTapURL(url: URL) {
delegate?.didTapURL(url: url, in: self)
}
}

@ -17,7 +17,7 @@ class TicketRedemptionViewController: UIViewController, TicketVerifiableStatusVi
var viewModel: TicketRedemptionViewModel
var titleLabel = UILabel()
let imageView = UIImageView()
let ticketView = TicketRowView()
let ticketView: TokenRowView & UIView
var timer: Timer!
var session: WalletSession
private let token: TokenObject
@ -28,6 +28,15 @@ class TicketRedemptionViewController: UIViewController, TicketVerifiableStatusVi
self.session = session
self.token = token
self.viewModel = viewModel
let tokenType = CryptoKittyHandling(address: token.address)
switch tokenType {
case .cryptoKitty:
ticketView = TokenListFormatRowView()
case .otherNonFungibleToken:
ticketView = TicketRowView()
}
super.init(nibName: nil, bundle: nil)
updateNavigationRightBarButtons(isVerified: true)
@ -160,7 +169,7 @@ class TicketRedemptionViewController: UIViewController, TicketVerifiableStatusVi
configureUI()
ticketView.configure(viewModel: .init(ticketHolder: viewModel.ticketHolder))
ticketView.configure(tokenHolder: viewModel.ticketHolder)
ticketView.stateLabel.isHidden = true
}

@ -19,15 +19,6 @@ struct BaseTicketTableViewCellViewModel {
return ""
}
var cellHeight: CGFloat {
let detailsHeight = CGFloat(34)
if ticketHolder.areDetailsVisible {
return 120 + detailsHeight
} else {
return 120
}
}
var checkboxImage: UIImage {
if ticketHolder.isSelected {
return R.image.ticket_bundle_checked()!

@ -68,36 +68,42 @@ struct TicketRowViewModel {
var city: String {
guard let ticketHolder = ticketHolder else { return "" }
return ", \(ticketHolder.city)"
let value = ticketHolder.values["locality"] ?? "N/A"
return ", \(value)"
}
var category: String {
guard let ticketHolder = ticketHolder else { return "" }
return String(ticketHolder.category)
return ticketHolder.values["category"] as? String ?? "N/A"
}
var teams: String {
guard let ticketHolder = ticketHolder else { return "" }
return R.string.localizable.aWalletTicketTokenMatchVs(ticketHolder.countryA, ticketHolder.countryB)
let countryA = ticketHolder.values["countryA"] as? String ?? ""
let countryB = ticketHolder.values["countryB"] as? String ?? ""
return R.string.localizable.aWalletTicketTokenMatchVs(countryA, countryB)
}
var match: String {
guard let ticketHolder = ticketHolder else { return "" }
return "M\(ticketHolder.match)"
let value = ticketHolder.values["match"] as? Int ?? 0
return "M\(value)"
}
var venue: String {
guard let ticketHolder = ticketHolder else { return "" }
return ticketHolder.venue
return ticketHolder.values["venue"] as? String ?? "N/A"
}
var date: String {
guard let ticketHolder = ticketHolder else { return "" }
return ticketHolder.date.formatAsShortDateString()
let value = ticketHolder.values["time"] as? GeneralisedTime ?? GeneralisedTime()
return value.formatAsShortDateString()
}
var time: String {
guard let ticketHolder = ticketHolder else { return "" }
return ticketHolder.date.format("h:mm a")
let value = ticketHolder.values["time"] as? GeneralisedTime ?? GeneralisedTime()
return value.format("h:mm a")
}
}

@ -0,0 +1,121 @@
// Copyright © 2018 Stormbird PTE. LTD.
import UIKit
struct TokenListFormatRowViewModel {
var ticketHolder: TokenHolder
var contentsBackgroundColor: UIColor {
return Colors.appWhite
}
var titleColor: UIColor {
return Colors.appText
}
var countColor: UIColor {
return Colors.appHighlightGreen
}
var subtitleColor: UIColor {
return UIColor(red: 112, green: 112, blue: 112)
}
var ticketCountFont: UIFont {
return Fonts.bold(size: 21)!
}
var titleFont: UIFont {
return Fonts.light(size: 21)!
}
var descriptionFont: UIFont {
return Fonts.light(size: 16)!
}
var stateBackgroundColor: UIColor {
return UIColor(red: 151, green: 151, blue: 151)
}
var stateColor: UIColor {
return .white
}
var subtitleFont: UIFont {
if ScreenChecker().isNarrowScreen() {
return Fonts.semibold(size: 12)!
} else {
return Fonts.semibold(size: 15)!
}
}
var detailsFont: UIFont {
return Fonts.light(size: 16)!
}
var urlButtonColor: UIColor {
return Colors.appText
}
var urlButtonFont: UIFont {
return Fonts.light(size: 25)!
}
var urlButtonText: String {
return R.string.localizable.cryptoKittiesUrlOpen()
}
var ticketCount: String {
return "x\(ticketHolder.tickets.count)"
}
var title: String {
let tokenId = ticketHolder.values["tokenId"] as? String ?? ""
return R.string.localizable.cryptoKittiesCatName(tokenId)
}
var subtitle: String {
let traits = ticketHolder.values["traits"] as? [CryptoKittyTrait] ?? []
let generationText: String
let cooldownText: String
if let generation = traits.first(where: { $0.type == "generation" }) {
generationText = "Gen \(generation.value)"
} else {
generationText = ""
}
if let cooldown = traits.first(where: { $0.type == "cooldown_index" }), let cooldownIndex = Int(cooldown.value) {
let cooldownValue = Constants.cryptoKittiesCooldowns[cooldownIndex]
cooldownText = "\(cooldownValue) Cooldown"
} else {
cooldownText = ""
}
if generationText.isEmpty {
return cooldownText
} else if cooldownText.isEmpty {
return generationText
} else {
return "\(generationText) . \(cooldownText)"
}
}
var description: String {
return ticketHolder.values["description"] as? String ?? ""
}
var thumbnailImageUrl: URL? {
guard let url = ticketHolder.values["thumbnailUrl"] as? String else { return nil }
return URL(string: url)
}
var externalLink: URL? {
guard let url = ticketHolder.values["externalLink"] as? String else { return nil }
return URL(string: url)
}
//TODO using CryptoKitty struct here, not good
var details: [String] {
let traits = ticketHolder.values["traits"] as? [CryptoKittyTrait] ?? []
let withoutGenerationAndCooldownIndex = traits.filter { $0.type != "generation" && $0.type != "cooldown_index" }
return withoutGenerationAndCooldownIndex.map { "\($0.type): \($0.value)" }
}
}

@ -163,3 +163,9 @@ class TicketRowView: UIView {
matchLabel.text = viewModel.match
}
}
extension TicketRowView: TokenRowView {
func configure(tokenHolder: TokenHolder) {
configure(viewModel: .init(ticketHolder: tokenHolder))
}
}

@ -0,0 +1,227 @@
// Copyright © 2018 Stormbird PTE. LTD.
import UIKit
import Alamofire
protocol TokenListFormatRowViewDelegate: class {
func didTapURL(url: URL)
}
class TokenListFormatRowView: UIView {
let checkboxImageView = UIImageView(image: R.image.ticket_bundle_unchecked())
weak var delegate: TokenListFormatRowViewDelegate?
let background = UIView()
let stateLabel = UILabel()
//TODO We don't display this for now. Maybe should have flag to show/hide it
let ticketCountLabel = UILabel()
private let thumbnailImageView = UIImageView()
//TODO this imageView is not used yet
private let imageView = UIImageView()
private let titleLabel = UILabel()
private let subtitleLabel = UILabel()
private let descriptionLabel = UILabel()
private var detailLabels = [UILabel]()
private let detailsRowStack: UIStackView
private let detailsRowWrapperStack: UIStackView
private let urlButton = UIButton(type: .system)
private let showCheckbox: Bool
private var viewModel: TokenListFormatRowViewModel?
var areDetailsVisible = false {
didSet {
if areDetailsVisible {
descriptionLabel.numberOfLines = 0
detailsRowWrapperStack.isHidden = false
urlButton.isHidden = false
} else {
descriptionLabel.numberOfLines = 1
detailsRowWrapperStack.isHidden = true
urlButton.isHidden = true
}
}
}
init(showCheckbox: Bool = false) {
self.showCheckbox = showCheckbox
checkboxImageView.translatesAutoresizingMaskIntoConstraints = false
background.translatesAutoresizingMaskIntoConstraints = false
let topRowStack = [titleLabel].asStackView(spacing: 15, contentHuggingPriority: .required)
detailsRowStack = [].asStackView(axis: .vertical, contentHuggingPriority: .required)
detailsRowWrapperStack = [
.spacer(height: 10),
detailsRowStack,
].asStackView(axis: .vertical, contentHuggingPriority: .required)
detailsRowWrapperStack.isHidden = true
urlButton.isHidden = true
super.init(frame: .zero)
if showCheckbox {
addSubview(checkboxImageView)
}
addSubview(background)
urlButton.addTarget(self, action: #selector(tappedUrl), for: .touchUpInside)
let col0 = [
stateLabel,
topRowStack,
subtitleLabel,
descriptionLabel,
].asStackView(axis: .vertical, contentHuggingPriority: .required)
col0.translatesAutoresizingMaskIntoConstraints = false
col0.alignment = .leading
let topStack = [
col0,
thumbnailImageView,
].asStackView(axis: .horizontal, contentHuggingPriority: .required)
topStack.translatesAutoresizingMaskIntoConstraints = false
topStack.alignment = .leading
let stackView = [
topStack,
.spacer(height: 10),
detailsRowWrapperStack,
urlButton,
].asStackView(axis: .vertical, contentHuggingPriority: .required)
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.alignment = .leading
background.addSubview(stackView)
// TODO extract constant. Maybe StyleLayout.sideMargin
let xMargin = CGFloat(7)
let yMargin = CGFloat(5)
var checkboxRelatedConstraints = [NSLayoutConstraint]()
if showCheckbox {
checkboxRelatedConstraints.append(checkboxImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: xMargin))
checkboxRelatedConstraints.append(checkboxImageView.centerYAnchor.constraint(equalTo: centerYAnchor))
checkboxRelatedConstraints.append(background.leadingAnchor.constraint(equalTo: checkboxImageView.trailingAnchor, constant: xMargin))
if ScreenChecker().isNarrowScreen() {
checkboxRelatedConstraints.append(checkboxImageView.widthAnchor.constraint(equalToConstant: 20))
checkboxRelatedConstraints.append(checkboxImageView.heightAnchor.constraint(equalToConstant: 20))
} else {
//Have to be hardcoded and not rely on the image's size because different string lengths for the text fields can force the checkbox to shrink
checkboxRelatedConstraints.append(checkboxImageView.widthAnchor.constraint(equalToConstant: 28))
checkboxRelatedConstraints.append(checkboxImageView.heightAnchor.constraint(equalToConstant: 28))
}
} else {
checkboxRelatedConstraints.append(background.leadingAnchor.constraint(equalTo: leadingAnchor, constant: xMargin))
}
NSLayoutConstraint.activate([
stackView.leadingAnchor.constraint(equalTo: background.leadingAnchor, constant: 21),
stackView.trailingAnchor.constraint(equalTo: background.trailingAnchor, constant: -21),
stackView.topAnchor.constraint(equalTo: background.topAnchor, constant: 16),
stackView.bottomAnchor.constraint(lessThanOrEqualTo: background.bottomAnchor, constant: -16),
background.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -xMargin),
background.topAnchor.constraint(equalTo: topAnchor, constant: yMargin),
background.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -yMargin),
thumbnailImageView.widthAnchor.constraint(equalToConstant: 44),
thumbnailImageView.widthAnchor.constraint(equalTo: thumbnailImageView.heightAnchor),
stateLabel.heightAnchor.constraint(equalToConstant: 22),
] + checkboxRelatedConstraints)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tappedUrl() {
guard let url = viewModel?.externalLink else { return }
delegate?.didTapURL(url: url)
}
func configure(viewModel: TokenListFormatRowViewModel) {
self.viewModel = viewModel
background.backgroundColor = viewModel.contentsBackgroundColor
background.layer.cornerRadius = 20
background.layer.shadowRadius = 3
background.layer.shadowColor = UIColor.black.cgColor
background.layer.shadowOffset = CGSize(width: 0, height: 0)
background.layer.shadowOpacity = 0.14
background.layer.borderColor = UIColor.black.cgColor
stateLabel.backgroundColor = viewModel.stateBackgroundColor
stateLabel.layer.cornerRadius = 8
stateLabel.clipsToBounds = true
stateLabel.textColor = viewModel.stateColor
stateLabel.font = viewModel.subtitleFont
ticketCountLabel.textColor = viewModel.countColor
ticketCountLabel.font = viewModel.ticketCountFont
descriptionLabel.textColor = viewModel.titleColor
descriptionLabel.font = viewModel.descriptionFont
titleLabel.textColor = viewModel.titleColor
titleLabel.font = viewModel.titleFont
subtitleLabel.textColor = viewModel.titleColor
subtitleLabel.font = viewModel.titleFont
urlButton.titleLabel?.font = viewModel.urlButtonFont
urlButton.setTitleColor(viewModel.urlButtonColor, for: .normal)
urlButton.setTitle(viewModel.urlButtonText, for: .normal)
displayDetails(viewModel: viewModel)
ticketCountLabel.text = viewModel.ticketCount
descriptionLabel.text = viewModel.description
titleLabel.text = viewModel.title
subtitleLabel.text = viewModel.subtitle
//TODO cancel the request if we reuse the cell before it's finished downloading
if let url = viewModel.thumbnailImageUrl {
var request = URLRequest(url: url)
request.httpMethod = "GET"
request.cachePolicy = .returnCacheDataElseLoad
Alamofire.request(request).response { response in
if let data = response.data, let image = UIImage(data: data) {
if url == viewModel.thumbnailImageUrl {
self.thumbnailImageView.image = image
}
}
}
}
}
///We only add labels, not remove them. For performance
private func displayDetails(viewModel: TokenListFormatRowViewModel) {
for each in detailLabels {
each.text = ""
}
let labelsToAddCount = viewModel.details.count - detailLabels.count
if labelsToAddCount > 0 {
for i in 1...labelsToAddCount {
let label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
label.textColor = viewModel.subtitleColor
label.font = viewModel.detailsFont
detailLabels.append(label)
detailsRowStack.addArrangedSubview(label)
}
}
for (i, each) in viewModel.details.enumerated() {
let label = detailLabels[i]
label.text = each
}
}
}
extension TokenListFormatRowView: TokenRowView {
func configure(tokenHolder: TokenHolder) {
configure(viewModel: .init(ticketHolder: tokenHolder))
}
}

@ -0,0 +1,10 @@
// Copyright © 2018 Stormbird PTE. LTD.
import UIKit
//TODO might be unnecessary in the future. Full-text search for TokenRowViewProtocol
class TokenListFormatTableViewCellWithCheckbox: BaseTokenListFormatTableViewCell {
override func showCheckbox() -> Bool {
return true
}
}

@ -0,0 +1,14 @@
// Copyright © 2018 Stormbird PTE. LTD.
import UIKit
//TODO make TicketTableViewCellWithoutCheckbox, etc cell classes be generic (with TokenRowView as the type parameter). Unfortunately: https://bugs.swift.org/browse/SR-6977 is only fixed in Swift 4.2, aka Xcode 10.
protocol TokenRowView: class {
//var checkboxImageView: UIImageView { get }
//var areDetailsVisible: Bool { get set }
func configure(tokenHolder: TokenHolder)
//TODO getting rid of these will be good
var background: UIView { get }
var stateLabel: UILabel { get }
}

@ -26,7 +26,7 @@ class EnterSellTicketsPriceQuantityViewController: UIViewController, TicketVerif
let ethCostLabel = UILabel()
let dollarCostLabelLabel = UILabel()
let dollarCostLabel = PaddedLabel()
let ticketView = TicketRowView()
let ticketView: TokenRowView & UIView
let nextButton = UIButton(type: .system)
var viewModel: EnterSellTicketsPriceQuantityViewControllerViewModel
var paymentFlow: PaymentFlow
@ -62,6 +62,15 @@ class EnterSellTicketsPriceQuantityViewController: UIViewController, TicketVerif
self.paymentFlow = paymentFlow
self.ethPrice = ethPrice
self.viewModel = viewModel
let tokenType = CryptoKittyHandling(address: viewModel.token.address)
switch tokenType {
case .cryptoKitty:
ticketView = TokenListFormatRowView()
case .otherNonFungibleToken:
ticketView = TicketRowView()
}
super.init(nibName: nil, bundle: nil)
updateNavigationRightBarButtons(isVerified: true)
@ -269,7 +278,7 @@ class EnterSellTicketsPriceQuantityViewController: UIViewController, TicketVerif
header.configure(title: viewModel.headerTitle)
ticketView.configure(viewModel: .init(ticketHolder: viewModel.ticketHolder))
ticketView.configure(tokenHolder: viewModel.ticketHolder)
pricePerTicketLabel.textAlignment = .center
pricePerTicketLabel.textColor = viewModel.choiceLabelColor

@ -6,6 +6,7 @@ protocol SellTicketsViewControllerDelegate: class {
func didSelectTicketHolder(ticketHolder: TokenHolder, in viewController: SellTicketsViewController)
func didPressViewInfo(in viewController: SellTicketsViewController)
func didPressViewContractWebPage(in viewController: SellTicketsViewController)
func didTapURL(url: URL, in viewController: SellTicketsViewController)
}
class SellTicketsViewController: UIViewController, TicketVerifiableStatusViewController {
@ -36,6 +37,7 @@ class SellTicketsViewController: UIViewController, TicketVerifiableStatusViewCon
view.addSubview(roundedBackground)
tableView.register(TicketTableViewCellWithCheckbox.self, forCellReuseIdentifier: TicketTableViewCellWithCheckbox.identifier)
tableView.register(TokenListFormatTableViewCellWithCheckbox.self, forCellReuseIdentifier: TokenListFormatTableViewCellWithCheckbox.identifier)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.separatorStyle = .none
@ -133,10 +135,19 @@ extension SellTicketsViewController: UITableViewDelegate, UITableViewDataSource
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TicketTableViewCellWithCheckbox.identifier, for: indexPath) as! TicketTableViewCellWithCheckbox
let ticketHolder = viewModel.item(for: indexPath)
cell.configure(viewModel: .init(ticketHolder: ticketHolder))
return cell
let tokenType = CryptoKittyHandling(contract: ticketHolder.contractAddress)
switch tokenType {
case .cryptoKitty:
let cell = tableView.dequeueReusableCell(withIdentifier: TokenListFormatTableViewCellWithCheckbox.identifier, for: indexPath) as! TokenListFormatTableViewCellWithCheckbox
cell.delegate = self
cell.configure(viewModel: .init(ticketHolder: ticketHolder))
return cell
case .otherNonFungibleToken:
let cell = tableView.dequeueReusableCell(withIdentifier: TicketTableViewCellWithCheckbox.identifier, for: indexPath) as! TicketTableViewCellWithCheckbox
cell.configure(viewModel: .init(ticketHolder: ticketHolder))
return cell
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
@ -144,3 +155,9 @@ extension SellTicketsViewController: UITableViewDelegate, UITableViewDataSource
animateRowHeightChanges(for: changedIndexPaths, in: tableView)
}
}
extension SellTicketsViewController: BaseTokenListFormatTableViewCellDelegate {
func didTapURL(url: URL) {
delegate?.didTapURL(url: url, in: self)
}
}

@ -29,7 +29,7 @@ class SetSellTicketsExpiryDateViewController: UIViewController, TicketVerifiable
let noteTitleLabel = UILabel()
let noteLabel = UILabel()
let noteBorderView = UIView()
let ticketView = TicketRowView()
let ticketView: TokenRowView & UIView
let nextButton = UIButton(type: .system)
var datePicker = UIDatePicker()
var timePicker = UIDatePicker()
@ -53,6 +53,15 @@ class SetSellTicketsExpiryDateViewController: UIViewController, TicketVerifiable
self.ticketHolder = ticketHolder
self.ethCost = ethCost
self.viewModel = viewModel
let tokenType = CryptoKittyHandling(contract: ticketHolder.contractAddress)
switch tokenType {
case .cryptoKitty:
ticketView = TokenListFormatRowView()
case .otherNonFungibleToken:
ticketView = TicketRowView()
}
super.init(nibName: nil, bundle: nil)
updateNavigationRightBarButtons(isVerified: true)
@ -259,7 +268,7 @@ class SetSellTicketsExpiryDateViewController: UIViewController, TicketVerifiable
header.configure(title: viewModel.headerTitle)
ticketView.configure(viewModel: .init())
ticketView.configure(tokenHolder: viewModel.ticketHolder)
linkExpiryDateLabel.textAlignment = .center
linkExpiryDateLabel.textColor = viewModel.choiceLabelColor
@ -309,20 +318,6 @@ class SetSellTicketsExpiryDateViewController: UIViewController, TicketVerifiable
ticketView.stateLabel.isHidden = true
ticketView.ticketCountLabel.text = viewModel.ticketCountString
ticketView.venueLabel.text = viewModel.venue
ticketView.dateLabel.text = viewModel.date
ticketView.cityLabel.text = viewModel.city
ticketView.categoryLabel.text = viewModel.category
ticketView.teamsLabel.text = viewModel.teams
ticketView.matchLabel.text = viewModel.match
nextButton.setTitleColor(viewModel.buttonTitleColor, for: .normal)
nextButton.backgroundColor = viewModel.buttonBackgroundColor
nextButton.titleLabel?.font = viewModel.buttonFont

@ -37,34 +37,6 @@ struct SetSellTicketsExpiryDateViewControllerViewModel {
return Fonts.regular(size: 10)!
}
var ticketCountString: String {
return "x\(ticketHolder.tickets.count)"
}
var city: String {
return ticketHolder.city
}
var category: String {
return String(ticketHolder.category)
}
var teams: String {
return R.string.localizable.aWalletTicketTokenMatchVs(ticketHolder.countryA, ticketHolder.countryB)
}
var match: String {
return "M\(ticketHolder.match)"
}
var venue: String {
return ticketHolder.venue
}
var date: String {
return ticketHolder.date.formatAsShortDateString()
}
var linkExpiryDateLabelText: String {
return R.string.localizable.aWalletTicketTokenSellLinkExpiryDateTitle()
}

@ -55,8 +55,24 @@ public struct Constants {
//OpenSea links for erc721 assets
public static let openseaAPI = "https://api.opensea.io/"
public static let cryptoKittiesCooldowns = [
"Fast",
"Swift",
"Swift",
"Snappy",
"Snappy",
"Brisk",
"Brisk",
"Ploddy",
"Ploddy",
"Slow",
"Slow",
"Sluggish"
]
//contract addresses that are compatible with opensea
//See comment in CryptoKittyHandling enum
public static let cryptoKittiesContractAddress = "0x06012c8cf97bead5deae237070f9587f8e7a266d"
}

@ -17,6 +17,21 @@ class TokenAdaptor {
}
public func getTicketHolders() -> [TokenHolder] {
switch token.type {
case .ether, .erc20, .erc875:
return getNonCryptoKittyTicketHolders()
case .erc721:
let tokenType = CryptoKittyHandling(address: token.address)
switch tokenType {
case .cryptoKitty:
return getCryptoKittyTicketHolders()
case .otherNonFungibleToken:
return getNonCryptoKittyTicketHolders()
}
}
}
private func getNonCryptoKittyTicketHolders() -> [TokenHolder] {
let balance = token.balance
var tickets = [Ticket]()
for (index, item) in balance.enumerated() {
@ -32,6 +47,19 @@ class TokenAdaptor {
return bundle(tickets: tickets)
}
private func getCryptoKittyTicketHolders() -> [TokenHolder] {
let balance = token.balance
var tickets = [Ticket]()
for (index, item) in balance.enumerated() {
let jsonString = item.balance
if let ticket = getTicketForCryptoKitty(forJSONString: jsonString, in: token) {
tickets.append(ticket)
}
}
return bundle(tickets: tickets)
}
func bundle(tickets: [Ticket]) -> [TokenHolder] {
switch token.type {
case .ether, .erc20, .erc875:
@ -52,14 +80,22 @@ class TokenAdaptor {
}
private func sortBundlesUpcomingFirst(bundles: [TokenHolder]) -> [TokenHolder] {
return bundles.sorted { $0.date < $1.date }
return bundles.sorted {
let d0 = $0.values["time"] as? GeneralisedTime ?? GeneralisedTime()
let d1 = $1.values["time"] as? GeneralisedTime ?? GeneralisedTime()
return d0 < d1
}
}
//If sequential or have the same seat number, add them together
///e.g 21, 22, 25 is broken up into 2 bundles: 21-22 and 25.
///e.g 21, 21, 22, 25 is broken up into 2 bundles: (21,21-22) and 25.
private func breakBundlesFurtherToHaveContinuousSeatRange(tickets: [Ticket]) -> [[Ticket]] {
let tickets = tickets.sorted { $0.seatId <= $1.seatId }
let tickets = tickets.sorted {
let s0 = $0.values["numero"] as? Int ?? 0
let s1 = $1.values["numero"] as? Int ?? 0
return s0 <= s1
}
return tickets.reduce([[Ticket]]()) { results, ticket in
var results = results
if var previousRange = results.last, let previousTicket = previousRange.last, (previousTicket.seatId + 1 == ticket.seatId || previousTicket.seatId == ticket.seatId) {
@ -77,7 +113,15 @@ class TokenAdaptor {
private func groupTicketsByFields(tickets: [Ticket]) -> Dictionary<String, [Ticket]>.Values {
var dictionary = [String: [Ticket]]()
for each in tickets {
let hash = "\(each.city),\(each.venue),\(each.date),\(each.countryA),\(each.countryB),\(each.match),\(each.category)"
let city = each.values["locality"] as? String ?? "N/A"
let venue = each.values["venue"] as? String ?? "N/A"
let date = each.values["time"] as? GeneralisedTime ?? GeneralisedTime()
let countryA = each.values["countryA"] as? String ?? ""
let countryB = each.values["countryB"] as? String ?? ""
let match = each.values["match"] as? Int ?? 0
let category = each.values["category"] as? String ?? "N/A"
let hash = "\(city),\(venue),\(date),\(countryA),\(countryB),\(match),\(category)"
var group = dictionary[hash] ?? []
group.append(each)
dictionary[hash] = group
@ -90,6 +134,23 @@ class TokenAdaptor {
return XMLHandler(contract: token.contract).getFifaInfoForTicket(tokenId: id, index: index)
}
private func getTicketForCryptoKitty(forJSONString jsonString: String, in token: TokenObject) -> Ticket? {
guard let data = jsonString.data(using: .utf8), let cat = try? JSONDecoder().decode(CryptoKitty.self, from: data) else { return nil }
var values = [String: AssetAttributeValue]()
values["tokenId"] = cat.tokenId
values["description"] = cat.description
values["imageUrl"] = cat.imageUrl
values["thumbnailUrl"] = cat.thumbnailUrl
values["externalLink"] = cat.externalLink
values["traits"] = cat.traits
return Ticket(
id: 0,
index: 0,
name: "name",
values: values
)
}
private func getTicketHolder(for tickets: [Ticket]) -> TokenHolder {
return TokenHolder(
tickets: tickets,
@ -99,3 +160,10 @@ class TokenAdaptor {
}
}
extension Ticket {
//TODO Convenience-only. (Look for references). Should remove once we generalize things further and not hardcode the use of seatId
var seatId: Int {
return values["numero"] as? Int ?? 0
}
}

@ -0,0 +1,22 @@
// Copyright © 2018 Stormbird PTE. LTD.
import Foundation
struct CryptoKitty: Codable {
let tokenId: String
let description: String
let thumbnailUrl: String
let imageUrl: String
let externalLink: String
let traits: [CryptoKittyTrait]
}
struct CryptoKittyTrait: Codable {
let count: Int
let type: String
let value: String
}
struct CryptoKittyError: Error {
var localizedDescription: String
}

@ -0,0 +1,25 @@
// Copyright © 2018 Stormbird PTE. LTD.
import Foundation
import TrustKeystore
///Use this enum to "mark" where we do special handling for CryptoKitty instead of accessing the crypto kitty contract access directly
///If there are other special casing for CryptoKitty that doesn't fit this model, create another enum type (not case)
enum CryptoKittyHandling {
case cryptoKitty
case otherNonFungibleToken
init(contract: String) {
self = {
if contract.sameContract(as: Constants.cryptoKittiesContractAddress) {
return .cryptoKitty
} else {
return .otherNonFungibleToken
}
}()
}
init(address: Address) {
self.init(contract: address.eip55String)
}
}

@ -15,38 +15,6 @@ struct Ticket {
var name: String
let values: [String: AssetAttributeValue]
var city: String {
return values["locality"] as? String ?? "N/A"
}
var venue: String {
return values["venue"] as? String ?? "N/A"
}
var match: Int {
return values["match"] as? Int ?? 0
}
var date: GeneralisedTime {
return values["time"] as? GeneralisedTime ?? .init()
}
var seatId: Int {
return values["numero"] as? Int ?? 0
}
var category: String {
return values["category"] as? String ?? "N/A"
}
var countryA: String {
return values["countryA"] as? String ?? ""
}
var countryB: String {
return values["countryB"] as? String ?? ""
}
static var empty: Ticket {
return Ticket(
id: Constants.nullTicketBigUInt,

@ -14,14 +14,7 @@ class TokenHolder {
}
var tickets: [Ticket]
var city: String { return tickets[0].city }
var name: String { return tickets[0].name }
var venue: String { return tickets[0].venue }
var match: Int { return tickets[0].match }
var date: GeneralisedTime { return tickets[0].date }
var category: String { return tickets[0].category }
var countryA: String { return tickets[0].countryA }
var countryB: String { return tickets[0].countryB }
var values: [String: AssetAttributeValue] { return tickets[0].values }
var status: TicketHolderStatus
var isSelected = false
@ -34,14 +27,6 @@ class TokenHolder {
self.contractAddress = contractAddress
}
var seatRange: String {
let seatIds = tickets.map { $0.seatId }
if seatIds.count == 1 {
return seatIds.first!.toString()
}
return seatIds.min()!.toString() + "-" + seatIds.max()!.toString()
}
var count: Int {
return tickets.count
}

@ -1,11 +1,13 @@
// Copyright © 2018 Stormbird PTE. LTD.
import Foundation
import Alamofire
import Result
import APIKit
import RealmSwift
import BigInt
import Moya
import SwiftyJSON
import TrustKeystore
enum TokenError: Error {
@ -13,7 +15,7 @@ enum TokenError: Error {
}
protocol TokensDataStoreDelegate: class {
func didUpdate(result: Result<TokensViewModel, TokenError>)
func didUpdate(result: ResultResult<TokensViewModel, TokenError>.t)
}
class TokensDataStore {
@ -149,7 +151,7 @@ class TokensDataStore {
}
func getContractName(for addressString: String,
completion: @escaping (Result<String, AnyError>) -> Void) {
completion: @escaping (ResultResult<String, AnyError>.t) -> Void) {
let address = Address(string: addressString)
getNameCoordinator.getName(for: address!) { (result) in
completion(result)
@ -157,14 +159,14 @@ class TokensDataStore {
}
func getContractSymbol(for addressString: String,
completion: @escaping (Result<String, AnyError>) -> Void) {
completion: @escaping (ResultResult<String, AnyError>.t) -> Void) {
let address = Address(string: addressString)
getSymbolCoordinator.getSymbol(for: address!) { result in
completion(result)
}
}
func getDecimals(for addressString: String,
completion: @escaping (Result<UInt8, AnyError>) -> Void) {
completion: @escaping (ResultResult<UInt8, AnyError>.t) -> Void) {
let address = Address(string: addressString)
getDecimalsCoordinator.getDecimals(for: address!) { result in
completion(result)
@ -172,7 +174,7 @@ class TokensDataStore {
}
func getERC875Balance(for addressString: String,
completion: @escaping (Result<[String], AnyError>) -> Void) {
completion: @escaping (ResultResult<[String], AnyError>.t) -> Void) {
let address = Address(string: addressString)
getERC875BalanceCoordinator.getERC875TokenBalance(for: account.address, contract: address!) { result in
completion(result)
@ -180,7 +182,7 @@ class TokensDataStore {
}
func getIsERC875Contract(for addressString: String,
completion: @escaping (Result<Bool, AnyError>) -> Void) {
completion: @escaping (ResultResult<Bool, AnyError>.t) -> Void) {
let address = Address(string: addressString)
getIsERC875ContractCoordinator.getIsERC875Contract(for: address!) { result in
completion(result)
@ -188,18 +190,72 @@ class TokensDataStore {
}
func getERC721Balance(for addressString: String,
completion: @escaping (Result<[String], AnyError>) -> Void) {
completion: @escaping (ResultResult<[String], AnyError>.t) -> Void) {
let tokenType = CryptoKittyHandling(contract: addressString)
switch tokenType {
case .cryptoKitty:
getCryptoKittyBalance(for: addressString, completion: completion)
case .otherNonFungibleToken:
getGenericERC721Balance(for: addressString, completion: completion)
}
}
private func getGenericERC721Balance(for addressString: String,
completion: @escaping (ResultResult<[String], AnyError>.t) -> Void) {
let address = Address(string: addressString)
getERC721BalanceCoordinator.getERC721TokenBalance(for: account.address, contract: address!) { result in
switch result {
case .success(let ints):
completion(.success(ints.map { MarketQueueHandler.bytesToHexa($0.data.array) }))
completion(.success(ints.map {
MarketQueueHandler.bytesToHexa($0.data.array)
}))
case .failure(let error):
completion(.failure(error))
}
}
}
private func getCryptoKittyBalance(for addressString: String,
completion: @escaping (ResultResult<[String], AnyError>.t) -> Void) {
guard let url = URL(string: "\(Constants.openseaAPI)api/v1/assets/?owner=\(account.address.eip55String)&order_by=current_price&order_direction=asc") else {
completion(.failure(AnyError(CryptoKittyError(localizedDescription: "Error calling \(Constants.openseaAPI) API"))))
return
}
Alamofire.request(
url,
method: .get
).responseJSON { response in
guard let data = response.data, let json = try? JSON(data: data) else {
completion(.failure(AnyError(CryptoKittyError(localizedDescription: "Error calling \(Constants.openseaAPI) API"))))
return
}
var results = [String]()
for (index, each): (String, JSON) in json["assets"] where each["asset_contract"]["address"].stringValue.sameContract(as: Constants.cryptoKittiesContractAddress) {
let tokenId = each["token_id"].stringValue
let description = each["description"].stringValue
let thumbnailUrl = each["image_thumbnail_url"].stringValue
let imageUrl = each["image_url"].stringValue
let externalLink = each["external_link"].stringValue
var traits = [CryptoKittyTrait]()
for each in each["traits"].arrayValue {
let traitCount = each["trait_count"].intValue
let traitType = each["trait_type"].stringValue
let traitValue = each["value"].stringValue
let trait = CryptoKittyTrait(count: traitCount, type: traitType, value: traitValue)
traits.append(trait)
}
let cat = CryptoKitty(tokenId: tokenId, description: description, thumbnailUrl: thumbnailUrl, imageUrl: imageUrl, externalLink: externalLink, traits: traits)
if let encodedJson = try? JSONEncoder().encode(cat), let jsonString = String(data: encodedJson, encoding: .utf8) {
results.append(jsonString)
} else {
completion(.failure(AnyError(CryptoKittyError(localizedDescription: "Error converting JSON to CryptoKitty"))))
return
}
}
completion(.success(results))
}
}
func getTokenType(for addressString: String,
completion: @escaping (TokenType) -> Void) {
let address = Address(string: addressString)

@ -186,6 +186,7 @@ class NewTokenViewController: UIViewController, CanScanQRCode {
//int is 64 bits, if this proves not enough later we can convert to BigUInt
public func updateBalanceValue(_ balance: [String]) {
//TODO this happens to work for CryptoKitty now because of how isNonZeroBalance() is implemented. But should fix
let filteredTokens = balance.filter { isNonZeroBalance($0) }
viewModel.ERC875TokenBalance = filteredTokens
balanceTextField.value = viewModel.ERC875TokenBalanceAmount.description

@ -18,6 +18,7 @@ protocol TicketsViewControllerDelegate: class {
func didCancel(in viewController: TicketsViewController)
func didPressViewRedemptionInfo(in viewController: TicketsViewController)
func didPressViewContractWebPage(in viewController: TicketsViewController)
func didTapURL(url: URL, in viewController: TicketsViewController)
}
class TicketsViewController: UIViewController, TicketVerifiableStatusViewController {
@ -57,6 +58,7 @@ class TicketsViewController: UIViewController, TicketVerifiableStatusViewControl
view.addSubview(roundedBackground)
tableView.register(TicketTableViewCellWithoutCheckbox.self, forCellReuseIdentifier: TicketTableViewCellWithoutCheckbox.identifier)
tableView.register(TokenListFormatTableViewCellWithoutCheckbox.self, forCellReuseIdentifier: TokenListFormatTableViewCellWithoutCheckbox.identifier)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.separatorStyle = .none
@ -215,10 +217,19 @@ extension TicketsViewController: UITableViewDelegate, UITableViewDataSource {
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TicketTableViewCellWithoutCheckbox.identifier, for: indexPath) as! TicketTableViewCellWithoutCheckbox
let ticketHolder = viewModel.item(for: indexPath)
cell.configure(viewModel: .init(ticketHolder: ticketHolder))
return cell
let tokenType = CryptoKittyHandling(contract: ticketHolder.contractAddress)
switch tokenType {
case .cryptoKitty:
let cell = tableView.dequeueReusableCell(withIdentifier: TokenListFormatTableViewCellWithoutCheckbox.identifier, for: indexPath) as! TokenListFormatTableViewCellWithoutCheckbox
cell.delegate = self
cell.configure(viewModel: .init(ticketHolder: ticketHolder))
return cell
case .otherNonFungibleToken:
let cell = tableView.dequeueReusableCell(withIdentifier: TicketTableViewCellWithoutCheckbox.identifier, for: indexPath) as! TicketTableViewCellWithoutCheckbox
cell.configure(viewModel: .init(ticketHolder: ticketHolder))
return cell
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
@ -227,3 +238,8 @@ extension TicketsViewController: UITableViewDelegate, UITableViewDataSource {
}
}
extension TicketsViewController: BaseTokenListFormatTableViewCellDelegate {
func didTapURL(url: URL) {
delegate?.didTapURL(url: url, in: self)
}
}

@ -29,21 +29,4 @@ struct TicketTableViewCellViewModel {
return R.string.localizable.aWalletTicketTokenBundleStatusRedeemedTitle()
}
}
var cellHeight: CGFloat {
let detailsHeight = CGFloat(34)
if status.isEmpty {
if ticketHolder.areDetailsVisible {
return 120 + detailsHeight
} else {
return 120
}
} else {
if ticketHolder.areDetailsVisible {
return 150 + detailsHeight
} else {
return 150
}
}
}
}

@ -0,0 +1,66 @@
// Copyright © 2018 Stormbird PTE. LTD.
import UIKit
protocol BaseTokenListFormatTableViewCellDelegate: class {
func didTapURL(url: URL)
}
//TODO might be unnecessary in the future. Full-text search for TokenRowViewProtocol
// Override showCheckbox() to return true or false
class BaseTokenListFormatTableViewCell: UITableViewCell {
static let identifier = "BaseTokenListFormatTableViewCell"
lazy var rowView: TokenListFormatRowView = {
let result = TokenListFormatRowView(showCheckbox: showCheckbox())
result.delegate = self
return result
}()
weak var delegate: BaseTokenListFormatTableViewCellDelegate?
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
rowView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(rowView)
NSLayoutConstraint.activate([
rowView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
rowView.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
rowView.topAnchor.constraint(equalTo: contentView.topAnchor),
rowView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(viewModel: BaseTicketTableViewCellViewModel) {
selectionStyle = .none
backgroundColor = viewModel.backgroundColor
contentView.backgroundColor = viewModel.backgroundColor
rowView.configure(viewModel: .init(ticketHolder: viewModel.ticketHolder))
if showCheckbox() {
rowView.checkboxImageView.image = viewModel.checkboxImage
}
rowView.stateLabel.text = " \(viewModel.status) "
rowView.stateLabel.isHidden = viewModel.status.isEmpty
rowView.areDetailsVisible = viewModel.areDetailsVisible
}
func showCheckbox() -> Bool {
return true
}
}
extension BaseTokenListFormatTableViewCell: TokenListFormatRowViewDelegate {
func didTapURL(url: URL) {
delegate?.didTapURL(url: url)
}
}

@ -0,0 +1,10 @@
// Copyright © 2018 Stormbird PTE. LTD.
import UIKit
//TODO might be unnecessary in the future. Full-text search for TokenRowViewProtocol
class TokenListFormatTableViewCellWithoutCheckbox: BaseTokenListFormatTableViewCell {
override func showCheckbox() -> Bool {
return false
}
}

@ -8,6 +8,7 @@
import Foundation
import UIKit
import Result
import SafariServices
import TrustKeystore
import MessageUI
import BigInt
@ -380,6 +381,12 @@ extension TicketsCoordinator: TicketsViewControllerDelegate {
func didPressViewContractWebPage(in viewController: TicketsViewController) {
delegate?.didPressViewContractWebPage(for: viewController.viewModel.token, in: viewController)
}
func didTapURL(url: URL, in viewController: TicketsViewController) {
let controller = SFSafariViewController(url: url)
// Don't attempt to change tint colors for SFSafariViewController. It doesn't well correctly especially because the controller sets more than 1 color for the title
viewController.present(controller, animated: true, completion: nil)
}
}
extension TicketsCoordinator: RedeemTicketsViewControllerDelegate {
@ -394,6 +401,12 @@ extension TicketsCoordinator: RedeemTicketsViewControllerDelegate {
func didPressViewContractWebPage(in viewController: RedeemTicketsViewController) {
delegate?.didPressViewContractWebPage(for: viewController.viewModel.token, in: viewController)
}
func didTapURL(url: URL, in viewController: RedeemTicketsViewController) {
let controller = SFSafariViewController(url: url)
// Don't attempt to change tint colors for SFSafariViewController. It doesn't well correctly especially because the controller sets more than 1 color for the title
viewController.present(controller, animated: true, completion: nil)
}
}
extension TicketsCoordinator: RedeemTicketsQuantitySelectionViewControllerDelegate {
@ -422,6 +435,12 @@ extension TicketsCoordinator: SellTicketsViewControllerDelegate {
func didPressViewContractWebPage(in viewController: SellTicketsViewController) {
delegate?.didPressViewContractWebPage(for: viewController.viewModel.token, in: viewController)
}
func didTapURL(url: URL, in viewController: SellTicketsViewController) {
let controller = SFSafariViewController(url: url)
// Don't attempt to change tint colors for SFSafariViewController. It doesn't well correctly especially because the controller sets more than 1 color for the title
viewController.present(controller, animated: true, completion: nil)
}
}
extension TicketsCoordinator: TransferTicketsQuantitySelectionViewControllerDelegate {
@ -478,6 +497,12 @@ extension TicketsCoordinator: TransferTicketsViewControllerDelegate {
func didPressViewContractWebPage(in viewController: TransferTicketsViewController) {
delegate?.didPressViewContractWebPage(for: viewController.viewModel.token, in: viewController)
}
func didTapURL(url: URL, in viewController: TransferTicketsViewController) {
let controller = SFSafariViewController(url: url)
// Don't attempt to change tint colors for SFSafariViewController. It doesn't well correctly especially because the controller sets more than 1 color for the title
viewController.present(controller, animated: true, completion: nil)
}
}
extension TicketsCoordinator: TransferTicketsCoordinatorDelegate {

@ -18,7 +18,7 @@ class ChooseTicketTransferModeViewController: UIViewController, TicketVerifiable
}
let roundedBackground = RoundedBackground()
let header = TicketsViewControllerTitleHeader()
let ticketView = TicketRowView()
let ticketView: TokenRowView & UIView
let generateMagicLinkButton = UIButton(type: .system)
let transferNowButton = UIButton(type: .system)
var viewModel: ChooseTicketTransferModeViewControllerViewModel
@ -36,6 +36,15 @@ class ChooseTicketTransferModeViewController: UIViewController, TicketVerifiable
self.ticketHolder = ticketHolder
self.paymentFlow = paymentFlow
self.viewModel = viewModel
let tokenType = CryptoKittyHandling(contract: ticketHolder.contractAddress)
switch tokenType {
case .cryptoKitty:
ticketView = TokenListFormatRowView()
case .otherNonFungibleToken:
ticketView = TicketRowView()
}
super.init(nibName: nil, bundle: nil)
updateNavigationRightBarButtons(isVerified: true)
@ -133,7 +142,7 @@ class ChooseTicketTransferModeViewController: UIViewController, TicketVerifiable
header.configure(title: viewModel.headerTitle)
ticketView.configure(viewModel: .init(ticketHolder: ticketHolder))
ticketView.configure(tokenHolder: ticketHolder)
ticketView.stateLabel.isHidden = true

@ -17,7 +17,7 @@ class SetTransferTicketsExpiryDateViewController: UIViewController, TicketVerifi
let roundedBackground = RoundedBackground()
let scrollView = UIScrollView()
let header = TicketsViewControllerTitleHeader()
let ticketView = TicketRowView()
let ticketView: TokenRowView & UIView
let linkExpiryDateLabel = UILabel()
let linkExpiryDateField = DateEntryField()
let linkExpiryTimeLabel = UILabel()
@ -44,6 +44,15 @@ class SetTransferTicketsExpiryDateViewController: UIViewController, TicketVerifi
self.ticketHolder = ticketHolder
self.paymentFlow = paymentFlow
self.viewModel = viewModel
let tokenType = CryptoKittyHandling(contract: ticketHolder.contractAddress)
switch tokenType {
case .cryptoKitty:
ticketView = TokenListFormatRowView()
case .otherNonFungibleToken:
ticketView = TicketRowView()
}
super.init(nibName: nil, bundle: nil)
updateNavigationRightBarButtons(isVerified: true)
@ -254,7 +263,7 @@ class SetTransferTicketsExpiryDateViewController: UIViewController, TicketVerifi
header.configure(title: viewModel.headerTitle)
ticketView.configure(viewModel: .init(ticketHolder: ticketHolder))
ticketView.configure(tokenHolder: ticketHolder)
linkExpiryDateLabel.textAlignment = .center
linkExpiryDateLabel.textColor = viewModel.choiceLabelColor

@ -18,7 +18,7 @@ class TransferTicketsQuantitySelectionViewController: UIViewController, TicketVe
let header = TicketsViewControllerTitleHeader()
let subtitleLabel = UILabel()
var quantityStepper = NumberStepper()
let ticketView = TicketRowView()
let ticketView: TokenRowView & UIView
let nextButton = UIButton(type: .system)
var viewModel: TransferTicketsQuantitySelectionViewModel
var paymentFlow: PaymentFlow
@ -35,6 +35,15 @@ class TransferTicketsQuantitySelectionViewController: UIViewController, TicketVe
self.paymentFlow = paymentFlow
self.token = token
self.viewModel = viewModel
let tokenType = CryptoKittyHandling(contract: token.contract)
switch tokenType {
case .cryptoKitty:
ticketView = TokenListFormatRowView()
case .otherNonFungibleToken:
ticketView = TicketRowView()
}
super.init(nibName: nil, bundle: nil)
updateNavigationRightBarButtons(isVerified: true)
@ -100,7 +109,7 @@ class TransferTicketsQuantitySelectionViewController: UIViewController, TicketVe
footerBar.bottomAnchor.constraint(equalTo: view.bottomAnchor),
] + roundedBackground.createConstraintsWithContainer(view: view))
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@ -142,7 +151,7 @@ class TransferTicketsQuantitySelectionViewController: UIViewController, TicketVe
subtitleLabel.font = viewModel.subtitleFont
subtitleLabel.text = viewModel.subtitleText
ticketView.configure(viewModel: .init(ticketHolder: viewModel.ticketHolder))
ticketView.configure(tokenHolder: viewModel.ticketHolder)
quantityStepper.borderWidth = 1
quantityStepper.clipsToBounds = true

@ -17,7 +17,7 @@ class TransferTicketsViaWalletAddressViewController: UIViewController, TicketVer
private let token: TokenObject
let roundedBackground = RoundedBackground()
let header = TicketsViewControllerTitleHeader()
let ticketView = TicketRowView()
let ticketView: TokenRowView & UIView
let targetAddressTextField = AddressTextField()
let nextButton = UIButton(type: .system)
var viewModel: TransferTicketsViaWalletAddressViewControllerViewModel
@ -37,6 +37,15 @@ class TransferTicketsViaWalletAddressViewController: UIViewController, TicketVer
self.ticketHolder = ticketHolder
self.paymentFlow = paymentFlow
self.viewModel = viewModel
let tokenType = CryptoKittyHandling(contract: ticketHolder.contractAddress)
switch tokenType {
case .cryptoKitty:
ticketView = TokenListFormatRowView()
case .otherNonFungibleToken:
ticketView = TicketRowView()
}
super.init(nibName: nil, bundle: nil)
updateNavigationRightBarButtons(isVerified: true)
@ -131,7 +140,7 @@ class TransferTicketsViaWalletAddressViewController: UIViewController, TicketVer
header.configure(title: viewModel.headerTitle)
ticketView.configure(viewModel: .init(ticketHolder: ticketHolder))
ticketView.configure(tokenHolder: ticketHolder)
ticketView.stateLabel.isHidden = true

@ -6,6 +6,7 @@ protocol TransferTicketsViewControllerDelegate: class {
func didSelectTicketHolder(token: TokenObject, ticketHolder: TokenHolder, in viewController: TransferTicketsViewController)
func didPressViewInfo(in viewController: TransferTicketsViewController)
func didPressViewContractWebPage(in viewController: TransferTicketsViewController)
func didTapURL(url: URL, in viewController: TransferTicketsViewController)
}
class TransferTicketsViewController: UIViewController, TicketVerifiableStatusViewController {
@ -38,11 +39,13 @@ class TransferTicketsViewController: UIViewController, TicketVerifiableStatusVie
view.addSubview(roundedBackground)
tableView.register(TicketTableViewCellWithCheckbox.self, forCellReuseIdentifier: TicketTableViewCellWithCheckbox.identifier)
tableView.register(TokenListFormatTableViewCellWithCheckbox.self, forCellReuseIdentifier: TokenListFormatTableViewCellWithCheckbox.identifier)
tableView.translatesAutoresizingMaskIntoConstraints = false
tableView.delegate = self
tableView.separatorStyle = .none
tableView.backgroundColor = Colors.appWhite
tableView.tableHeaderView = header
tableView.rowHeight = UITableViewAutomaticDimension
roundedBackground.addSubview(tableView)
nextButton.setTitle(R.string.localizable.aWalletNextButtonTitle(), for: .normal)
@ -134,16 +137,19 @@ extension TransferTicketsViewController: UITableViewDelegate, UITableViewDataSou
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: TicketTableViewCellWithCheckbox.identifier, for: indexPath) as! TicketTableViewCellWithCheckbox
let ticketHolder = viewModel.item(for: indexPath)
cell.configure(viewModel: .init(ticketHolder: ticketHolder))
return cell
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let ticketHolder = viewModel.item(for: indexPath)
let cellViewModel = BaseTicketTableViewCellViewModel(ticketHolder: ticketHolder)
return cellViewModel.cellHeight
let tokenType = CryptoKittyHandling(contract: ticketHolder.contractAddress)
switch tokenType {
case .cryptoKitty:
let cell = tableView.dequeueReusableCell(withIdentifier: TokenListFormatTableViewCellWithCheckbox.identifier, for: indexPath) as! TokenListFormatTableViewCellWithCheckbox
cell.delegate = self
cell.configure(viewModel: .init(ticketHolder: ticketHolder))
return cell
case .otherNonFungibleToken:
let cell = tableView.dequeueReusableCell(withIdentifier: TicketTableViewCellWithCheckbox.identifier, for: indexPath) as! TicketTableViewCellWithCheckbox
cell.configure(viewModel: .init(ticketHolder: ticketHolder))
return cell
}
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
@ -151,3 +157,9 @@ extension TransferTicketsViewController: UITableViewDelegate, UITableViewDataSou
animateRowHeightChanges(for: changedIndexPaths, in: tableView)
}
}
extension TransferTicketsViewController: BaseTokenListFormatTableViewCellDelegate {
func didTapURL(url: URL) {
delegate?.didTapURL(url: url, in: self)
}
}

Loading…
Cancel
Save