diff --git a/Trust.xcodeproj/project.pbxproj b/Trust.xcodeproj/project.pbxproj index 36c3b0224..37e8979ec 100644 --- a/Trust.xcodeproj/project.pbxproj +++ b/Trust.xcodeproj/project.pbxproj @@ -344,11 +344,13 @@ 5E7C7A4384A8E3F22D3F8249 /* SetSellTicketsExpiryDateViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C700CD3E43689E88FBE9B /* SetSellTicketsExpiryDateViewControllerViewModel.swift */; }; 5E7C7AB2ECFB589632F2A26C /* WalletFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7E2DCCE0D775ECF83088 /* WalletFilter.swift */; }; 5E7C7AB6950E43BD6E8D0CBE /* TokensViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B3302309706CA0F972A /* TokensViewController.swift */; }; + 5E7C7AE1389D3179239249F0 /* ImportWalletTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C743172FCBDCD362C03A6 /* ImportWalletTabBar.swift */; }; 5E7C7B0367CFB413C6885474 /* GenerateSellMagicLinkViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7624D6F7EA55F6F167B3 /* GenerateSellMagicLinkViewControllerViewModel.swift */; }; 5E7C7B3E08EEA63C5B68B9C4 /* TicketRedemptionInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C778F20D32B70D7FF2135 /* TicketRedemptionInfoViewController.swift */; }; 5E7C7C0FAC500A6651E663FD /* TransferTicketsQuantitySelectionViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C703BA1D0E9ACB7399155 /* TransferTicketsQuantitySelectionViewModel.swift */; }; 5E7C7C21E5CAF122AA4F6617 /* HowDoIGetMyMoneyInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C78B001F9F95F404D5FEF /* HowDoIGetMyMoneyInfoViewController.swift */; }; 5E7C7C658D619C70F1E3DE59 /* AdvancedSettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C719B717E002583B1E2E9 /* AdvancedSettingsViewModel.swift */; }; + 5E7C7C7142C4519873B2BB4E /* ImportWalletTabBarViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C2872E213BBB05D55BA /* ImportWalletTabBarViewModel.swift */; }; 5E7C7C98EAF40E8110241DBD /* TicketTokenViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C783E3ADA4CF9554A0E7D /* TicketTokenViewCell.swift */; }; 5E7C7C9E89056069C8FEFA76 /* AlphaWalletSettingsSwitchRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7534FB6BF4D199643246 /* AlphaWalletSettingsSwitchRow.swift */; }; 5E7C7CCA357CB7BF12E1F2B4 /* UIStackView+Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C73ED9226646D562B5A3C /* UIStackView+Array.swift */; }; @@ -367,11 +369,13 @@ 5E7C7E04D4DDD7D8881A2AB1 /* UniversalLinkCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76AF81B8DFF605558499 /* UniversalLinkCoordinator.swift */; }; 5E7C7E1B18EC7F7FD6D64439 /* SellTicketsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7FF84A4377FC395772C3 /* SellTicketsViewController.swift */; }; 5E7C7E2F558A1DFF078B61F9 /* TransferTicketsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7011D8E5C9FFE0E59D55 /* TransferTicketsViewController.swift */; }; + 5E7C7E47C3C412A52DED7380 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AC5A210D034DBC75FB0 /* TextView.swift */; }; 5E7C7E4B4054AAD41C5BE3EC /* SettingsAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7564AF453BAB0BDAAA57 /* SettingsAction.swift */; }; 5E7C7E5C30EFDC70DF1E00C1 /* TicketsViewControllerHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C77316522DF2B256F1F92 /* TicketsViewControllerHeaderViewModel.swift */; }; 5E7C7E68425E20834B898D06 /* AppLocale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B29A9E728402D144C05 /* AppLocale.swift */; }; 5E7C7EAEBB435F3909DA36FB /* TransferTicketsViaWalletAddressViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76D3CFA12C2236E73E10 /* TransferTicketsViaWalletAddressViewControllerViewModel.swift */; }; 5E7C7EAED92E4AE8B99217AB /* TransferTicketsQuantitySelectionViewControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7021EE19C4B81CAAF3C0 /* TransferTicketsQuantitySelectionViewControllerTests.swift */; }; + 5E7C7EDA1BB781A45C1C19CD /* ImportWalletTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C73D26F24C4AAE981E2F2 /* ImportWalletTab.swift */; }; 5E7C7EEE563D81793CB96FA0 /* TransferTicketsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C755132D9B6F95080A1BE /* TransferTicketsCoordinator.swift */; }; 5E7C7FAF2A07E7AE21BF09AF /* AlphaWalletSettingsTextRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C71684B93F60206992E10 /* AlphaWalletSettingsTextRow.swift */; }; 5E7C7FC0770A411DB09F8C09 /* TokenViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C077372C3F2A4349FA1 /* TokenViewCell.swift */; }; @@ -806,10 +810,12 @@ 5E7C731B6F01534683227123 /* TicketTokenViewCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TicketTokenViewCellViewModel.swift; sourceTree = ""; }; 5E7C73495E0C0A207152EC25 /* LockEnterPasscodeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = LockEnterPasscodeViewController.swift; path = Trust/AlphaWalletLock/ViewControllers/LockEnterPasscodeViewController.swift; sourceTree = SOURCE_ROOT; }; 5E7C73617E3A4C0B9A90A5F8 /* AmountTextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmountTextField.swift; sourceTree = ""; }; + 5E7C73D26F24C4AAE981E2F2 /* ImportWalletTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletTab.swift; sourceTree = ""; }; 5E7C73DF5FBFE756097D32B1 /* EthCurrencyHelper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EthCurrencyHelper.swift; sourceTree = ""; }; 5E7C73ED9226646D562B5A3C /* UIStackView+Array.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIStackView+Array.swift"; sourceTree = ""; }; 5E7C741196D9D9C9C3EE5E30 /* LockCreatePasscodeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockCreatePasscodeViewController.swift; sourceTree = ""; }; 5E7C7419F47CC8B2996AA8F9 /* TransferTicketsQuantitySelectionViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferTicketsQuantitySelectionViewController.swift; sourceTree = ""; }; + 5E7C743172FCBDCD362C03A6 /* ImportWalletTabBar.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletTabBar.swift; sourceTree = ""; }; 5E7C74A2C738BF2412D412A7 /* TicketSellInfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TicketSellInfoViewController.swift; sourceTree = ""; }; 5E7C74B82783A94091A43470 /* EthTokenViewCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EthTokenViewCellViewModel.swift; sourceTree = ""; }; 5E7C74B9EB81C51E956566E7 /* TokensDataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensDataStore.swift; sourceTree = ""; }; @@ -857,6 +863,7 @@ 5E7C7A16ABC8BD5D508AA641 /* ImportWalletHelpBubbleViewViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletHelpBubbleViewViewModel.swift; sourceTree = ""; }; 5E7C7A65F6033318F7C8AEB0 /* DateEntryField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DateEntryField.swift; sourceTree = ""; }; 5E7C7AB3440C01136DF4F3E9 /* LockCreatePasscodeCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockCreatePasscodeCoordinator.swift; sourceTree = ""; }; + 5E7C7AC5A210D034DBC75FB0 /* TextView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = TextView.swift; path = Views/TextView.swift; sourceTree = ""; }; 5E7C7ACB94CEE493AC37487F /* TicketRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TicketRowView.swift; sourceTree = ""; }; 5E7C7ADD0FBE8708A6E98AF8 /* PromptBackupCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PromptBackupCoordinator.swift; sourceTree = ""; }; 5E7C7AE6FAE0DF969B4F52E9 /* ContactUsBannerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContactUsBannerView.swift; sourceTree = ""; }; @@ -872,6 +879,7 @@ 5E7C7BA578BE5FB0E613A6D6 /* ChooseTicketTransferModeViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChooseTicketTransferModeViewControllerViewModel.swift; sourceTree = ""; }; 5E7C7BD9B4BDAFC2D9EBD741 /* StatusViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StatusViewControllerViewModel.swift; sourceTree = ""; }; 5E7C7C077372C3F2A4349FA1 /* TokenViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenViewCell.swift; sourceTree = ""; }; + 5E7C7C2872E213BBB05D55BA /* ImportWalletTabBarViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletTabBarViewModel.swift; sourceTree = ""; }; 5E7C7C58586099F082973073 /* WalletFilterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletFilterView.swift; sourceTree = ""; }; 5E7C7CD7ABB18C1121D5776F /* LiveLocaleSwitcherBundle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveLocaleSwitcherBundle.swift; sourceTree = ""; }; 5E7C7CFDE7DEA8C06C4100AF /* TextField.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; @@ -1302,6 +1310,7 @@ 442FC575B6A4A50B0555E1B0 /* NumberStepper.swift */, 5E7C75918317E13AD540DCA7 /* RoundedBackground.swift */, 5E7C75B5AF76279A71395FC7 /* AddressTextField.swift */, + 5E7C7AC5A210D034DBC75FB0 /* TextView.swift */, ); path = UI; sourceTree = ""; @@ -1406,6 +1415,7 @@ children = ( 291A1B641F974E8600ADEC80 /* WalletEntryPoint.swift */, 29F114EF1FA6D53700114A29 /* ImportSelectionType.swift */, + 5E7C73D26F24C4AAE981E2F2 /* ImportWalletTab.swift */, ); path = Types; sourceTree = ""; @@ -1619,6 +1629,7 @@ 2996F1421F6C96FF005C33AE /* ImportWalletViewModel.swift */, 771AA961200D5EC700D25403 /* PassphraseViewModel.swift */, 77872D26202505C00032D687 /* EnterPasswordViewModel.swift */, + 5E7C7C2872E213BBB05D55BA /* ImportWalletTabBarViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -2618,6 +2629,7 @@ 771AA95F200D5E8800D25403 /* PassphraseView.swift */, 771AA963200D5EDB00D25403 /* WordCollectionViewCell.swift */, 771AA965200D5F1900D25403 /* WordCollectionViewCell.xib */, + 5E7C743172FCBDCD362C03A6 /* ImportWalletTabBar.swift */, ); path = Views; sourceTree = ""; @@ -3612,6 +3624,10 @@ 5E7C7488D5CAE24B7462815A /* LiveLocaleSwitcherBundle.swift in Sources */, 5E7C79D78AA5E774119BE49B /* TextField.swift in Sources */, 5E7C7788984F7ADCFE5B4DE0 /* AddressTextField.swift in Sources */, + 5E7C7E47C3C412A52DED7380 /* TextView.swift in Sources */, + 5E7C7AE1389D3179239249F0 /* ImportWalletTabBar.swift in Sources */, + 5E7C7EDA1BB781A45C1C19CD /* ImportWalletTab.swift in Sources */, + 5E7C7C7142C4519873B2BB4E /* ImportWalletTabBarViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Trust/Assets.xcassets/create_wallet.imageset/Contents.json b/Trust/Assets.xcassets/create_wallet.imageset/Contents.json deleted file mode 100644 index d50dcdba8..000000000 --- a/Trust/Assets.xcassets/create_wallet.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "create_wallet@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Trust/Assets.xcassets/create_wallet.imageset/create_wallet@3x.png b/Trust/Assets.xcassets/create_wallet.imageset/create_wallet@3x.png deleted file mode 100644 index 3a1f8f288..000000000 Binary files a/Trust/Assets.xcassets/create_wallet.imageset/create_wallet@3x.png and /dev/null differ diff --git a/Trust/Assets.xcassets/create_wallet_import.imageset/Contents.json b/Trust/Assets.xcassets/create_wallet_import.imageset/Contents.json deleted file mode 100644 index 8710bc6c7..000000000 --- a/Trust/Assets.xcassets/create_wallet_import.imageset/Contents.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "import (1).png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} \ No newline at end of file diff --git a/Trust/Assets.xcassets/create_wallet_import.imageset/import (1).png b/Trust/Assets.xcassets/create_wallet_import.imageset/import (1).png deleted file mode 100644 index c7bcbec95..000000000 Binary files a/Trust/Assets.xcassets/create_wallet_import.imageset/import (1).png and /dev/null differ diff --git a/Trust/Localization/en.lproj/Localizable.strings b/Trust/Localization/en.lproj/Localizable.strings index 5ed7bb21a..bb510236d 100644 --- a/Trust/Localization/en.lproj/Localizable.strings +++ b/Trust/Localization/en.lproj/Localizable.strings @@ -66,7 +66,6 @@ "wallet.create.button.title" = "Create Wallet"; "wallet.create.inProgress" = "Creating wallet..."; "wallet.import.button.title" = "Import Wallet"; -"wallet.import.subtitle" = "Importing wallet as easy as creating"; "wallets.backup.alertSheet.title" = "Backup Keystore"; "transactions.tabbar.item.title" = "My Transactions"; "transaction.navigation.title" = "Transaction"; diff --git a/Trust/Localization/es.lproj/Localizable.strings b/Trust/Localization/es.lproj/Localizable.strings index c4552d8dd..924ca3515 100644 --- a/Trust/Localization/es.lproj/Localizable.strings +++ b/Trust/Localization/es.lproj/Localizable.strings @@ -73,7 +73,6 @@ "wallet.create.button.title" = "Create Wallet"; "wallet.create.inProgress" = "Creating wallet..."; "wallet.import.button.title" = "Import Wallet"; -"wallet.import.subtitle" = "Importing wallet as easy as creating"; "wallets.backup.alertSheet.title" = "Hacer copia de seguridad del almacén de claves"; "transactions.tabbar.item.title" = "Actas"; "import.navigation.title" = "Importando cartera"; diff --git a/Trust/Localization/zh-Hans.lproj/Localizable.strings b/Trust/Localization/zh-Hans.lproj/Localizable.strings index 904397c6e..0c9b0d228 100644 --- a/Trust/Localization/zh-Hans.lproj/Localizable.strings +++ b/Trust/Localization/zh-Hans.lproj/Localizable.strings @@ -112,7 +112,6 @@ "wallet.import.button.title" = "导入钱包"; "browser.home.button.title" = "主页"; "browser.reload.button.title" = "刷新"; -"wallet.import.subtitle" = "导入钱包与创建一样简单"; "configureTransaction.gasPriceGwei.label.title" = "Gas费用 (Gwei)"; "enterPassword.confirmPassword.textField.placeholder" = "确认密码"; "enterPassword.navigation.title" = "备份密码"; diff --git a/Trust/Settings/Models/LiveLocaleSwitcherBundle.swift b/Trust/Settings/Models/LiveLocaleSwitcherBundle.swift index 65e2cd972..0ffa73934 100644 --- a/Trust/Settings/Models/LiveLocaleSwitcherBundle.swift +++ b/Trust/Settings/Models/LiveLocaleSwitcherBundle.swift @@ -15,7 +15,8 @@ class LiveLocaleSwitcherBundle: Bundle { } override func url(forResource name: String?, withExtension ext: String?) -> URL? { - if let languageBundle = objc_getAssociatedObject(self, &liveLocaleSwitcherBundleKey) as? Bundle { + //We want to match "html", but exclude "nib" (for "xib"). Safe to whitelist instead of blacklist + if ext == "html", let languageBundle = objc_getAssociatedObject(self, &liveLocaleSwitcherBundleKey) as? Bundle { return languageBundle.url(forResource: name, withExtension: ext) } else { return super.url(forResource: name, withExtension: ext) diff --git a/Trust/UI/Views/TextView.swift b/Trust/UI/Views/TextView.swift new file mode 100644 index 000000000..6c6b40253 --- /dev/null +++ b/Trust/UI/Views/TextView.swift @@ -0,0 +1,124 @@ +// Copyright © 2018 Stormbird PTE. LTD. + +import UIKit + +protocol TextViewDelegate: class { + func shouldReturn(in textView: TextView) -> Bool + func doneButtonTapped(for textView: TextView) + func nextButtonTapped(for textView: TextView) +} + +class TextView: UIControl { + enum InputAccessoryButtonType { + case done + case next + case none + } + + let label = UILabel() + let textView = UITextView() + var value: String { + get { + return textView.text ?? "" + } + set { + textView.text = newValue + } + } + var inputAccessoryButtonType = InputAccessoryButtonType.none { + didSet { + switch inputAccessoryButtonType { + case .done: + textView.inputAccessoryView = makeToolbarWithDoneButton() + case .next: + textView.inputAccessoryView = makeToolbarWithNextButton() + case .none: + textView.inputAccessoryView = nil + } + } + } + private var isConfigured = false + weak var delegate: TextViewDelegate? + + init() { + super.init(frame: .zero) + + translatesAutoresizingMaskIntoConstraints = false + + textView.translatesAutoresizingMaskIntoConstraints = false + textView.delegate = self + textView.textContainerInset = .init(top: 10, left: 12, bottom: 10, right: 12) + addSubview(textView) + + NSLayoutConstraint.activate([ + textView.leadingAnchor.constraint(equalTo: leadingAnchor), + textView.trailingAnchor.constraint(equalTo: trailingAnchor), + textView.topAnchor.constraint(equalTo: topAnchor), + textView.bottomAnchor.constraint(equalTo: bottomAnchor), + ]) + } + + func configureOnce() { + guard !isConfigured else { return } + isConfigured = true + + label.font = Fonts.regular(size: 10)! + label.textColor = Colors.appGrayLabelColor + + textView.textColor = Colors.appBackground + textView.font = Fonts.bold(size: 21) + textView.layer.borderColor = Colors.appBackground.cgColor + textView.layer.borderWidth = 1 + textView.layer.cornerRadius = 20 + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func makeToolbarWithDoneButton() -> UIToolbar { + //Frame needed, but actual values aren't that important + let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 100, height: 40)) + toolbar.barStyle = .default + + let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + let done = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(doneButtonTapped)) + + toolbar.items = [flexSpace, done] + toolbar.sizeToFit() + + return toolbar + } + + private func makeToolbarWithNextButton() -> UIToolbar { + //Frame needed, but actual values aren't that important + let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: 100, height: 40)) + toolbar.barStyle = .default + + let flexSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + let next = UIBarButtonItem(title: R.string.localizable.next(), style: .plain, target: self, action: #selector(nextButtonTapped)) + toolbar.items = [flexSpace, next] + toolbar.sizeToFit() + + return toolbar + } + + @objc func doneButtonTapped() { + delegate?.doneButtonTapped(for: self) + } + + @objc func nextButtonTapped() { + delegate?.nextButtonTapped(for: self) + } +} + +extension TextView: UITextViewDelegate { + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if text == "\n" { + guard let delegate = delegate else { return true } + return delegate.shouldReturn(in: self) + } else { + return true + } + } +} diff --git a/Trust/Wallet/Types/ImportWalletTab.swift b/Trust/Wallet/Types/ImportWalletTab.swift new file mode 100644 index 000000000..d6d49dab1 --- /dev/null +++ b/Trust/Wallet/Types/ImportWalletTab.swift @@ -0,0 +1,10 @@ +// Copyright © 2018 Stormbird PTE. LTD. + +import Foundation + +enum ImportWalletTab { + case keystore + case privateKey + case watch +} + diff --git a/Trust/Wallet/ViewControllers/ImportWalletViewController.swift b/Trust/Wallet/ViewControllers/ImportWalletViewController.swift index 5074de9d3..f51c0eadf 100644 --- a/Trust/Wallet/ViewControllers/ImportWalletViewController.swift +++ b/Trust/Wallet/ViewControllers/ImportWalletViewController.swift @@ -1,7 +1,5 @@ // Copyright SIX DAY LLC. All rights reserved. - import UIKit -import Eureka import BonMot import TrustKeystore import QRCodeReaderViewController @@ -10,57 +8,160 @@ protocol ImportWalletViewControllerDelegate: class { func didImportAccount(account: Wallet, in viewController: ImportWalletViewController) } -class ImportWalletViewController: FormViewController { +class ImportWalletViewController: UIViewController { + struct ValidationError: LocalizedError { + var msg: String + var errorDescription: String? { + return msg + } + } let keystore: Keystore private let viewModel = ImportWalletViewModel() - struct Values { - static let segment = "segment" - static let keystore = "keystore" - static let privateKey = "privateKey" - static let password = "password" - static let watch = "watch" - static let mnemonic = "mnemonic" - } - - var segmentRow: SegmentedRow? { - return form.rowBy(tag: Values.segment) - } - - var keystoreRow: TextAreaRow? { - return form.rowBy(tag: Values.keystore) - } - - var mnemonicRow: TextAreaRow? { - return form.rowBy(tag: Values.mnemonic) - } - - var privateKeyRow: TextAreaRow? { - return form.rowBy(tag: Values.privateKey) - } + //We don't actually use the rounded corner here, but it's a useful "content" view here + let roundedBackground = RoundedBackground() + let scrollView = UIScrollView() + let footerBar = UIView() + let tabBar = ImportWalletTabBar() + let keystoreJSONTextView = TextView() + let passwordTextField = TextField() + let privateKeyTextView = TextView() + let watchAddressTextField = AddressTextField() - var passwordRow: TextFloatLabelRow? { - return form.rowBy(tag: Values.password) - } + var keystoreJSONControlsStackView: UIStackView! + var privateKeyControlsStackView: UIStackView! + var watchControlsStackView: UIStackView! - var watchRow: TextFloatLabelRow? { - return form.rowBy(tag: Values.watch) - } + let importButton = UIButton(type: .system) weak var delegate: ImportWalletViewControllerDelegate? - init( - keystore: Keystore - ) { + init(keystore: Keystore) { self.keystore = keystore - super.init(nibName: nil, bundle: nil) - } - override func viewDidLoad() { - super.viewDidLoad() + super.init(nibName: nil, bundle: nil) title = viewModel.title + + roundedBackground.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(roundedBackground) + + scrollView.translatesAutoresizingMaskIntoConstraints = false + roundedBackground.addSubview(scrollView) + + tabBar.delegate = self + tabBar.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tabBar) + + keystoreJSONTextView.label.translatesAutoresizingMaskIntoConstraints = false + keystoreJSONTextView.delegate = self + keystoreJSONTextView.translatesAutoresizingMaskIntoConstraints = false + keystoreJSONTextView.textView.returnKeyType = .next + + passwordTextField.label.translatesAutoresizingMaskIntoConstraints = false + passwordTextField.delegate = self + passwordTextField.translatesAutoresizingMaskIntoConstraints = false + passwordTextField.textField.returnKeyType = .done + passwordTextField.textField.isSecureTextEntry = true + + privateKeyTextView.label.translatesAutoresizingMaskIntoConstraints = false + privateKeyTextView.delegate = self + privateKeyTextView.translatesAutoresizingMaskIntoConstraints = false + privateKeyTextView.textView.returnKeyType = .done + + watchAddressTextField.translatesAutoresizingMaskIntoConstraints = false + watchAddressTextField.delegate = self + watchAddressTextField.textField.returnKeyType = .done + + keystoreJSONControlsStackView = [ + keystoreJSONTextView.label, + .spacer(height: 4), + keystoreJSONTextView, + .spacer(height: 10), + passwordTextField.label, + .spacer(height: 4), + passwordTextField, + ].asStackView(axis: .vertical) + keystoreJSONControlsStackView.translatesAutoresizingMaskIntoConstraints = false + + privateKeyControlsStackView = [ + privateKeyTextView.label, + .spacer(height: 4), + privateKeyTextView, + ].asStackView(axis: .vertical) + privateKeyControlsStackView.translatesAutoresizingMaskIntoConstraints = false + + watchControlsStackView = [ + watchAddressTextField.label, + .spacer(height: 4), + watchAddressTextField, + ].asStackView(axis: .vertical) + watchControlsStackView.translatesAutoresizingMaskIntoConstraints = false + + let stackView = [ + tabBar, + .spacer(height: 10), + keystoreJSONControlsStackView, + privateKeyControlsStackView, + watchControlsStackView, + ].asStackView(axis: .vertical, alignment: .center) + stackView.translatesAutoresizingMaskIntoConstraints = false + scrollView.addSubview(stackView) + + importButton.setTitle(R.string.localizable.importWalletImportButtonTitle(), for: .normal) + importButton.addTarget(self, action: #selector(importWallet), for: .touchUpInside) + + let buttonsStackView = [importButton].asStackView(distribution: .fillEqually, contentHuggingPriority: .required) + buttonsStackView.translatesAutoresizingMaskIntoConstraints = false + + footerBar.translatesAutoresizingMaskIntoConstraints = false + footerBar.backgroundColor = Colors.appHighlightGreen + roundedBackground.addSubview(footerBar) + + let buttonsHeight = CGFloat(60) + footerBar.addSubview(buttonsStackView) + + let xMargin = CGFloat(7) + let heightThatFitsPrivateKeyNicely = CGFloat(100) + NSLayoutConstraint.activate([ + keystoreJSONTextView.heightAnchor.constraint(equalToConstant: heightThatFitsPrivateKeyNicely), + privateKeyTextView.heightAnchor.constraint(equalToConstant: heightThatFitsPrivateKeyNicely), + + tabBar.leadingAnchor.constraint(equalTo: stackView.leadingAnchor), + tabBar.trailingAnchor.constraint(equalTo: stackView.trailingAnchor), + + keystoreJSONControlsStackView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: xMargin), + keystoreJSONControlsStackView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -xMargin), + privateKeyControlsStackView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: xMargin), + privateKeyControlsStackView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -xMargin), + watchControlsStackView.leadingAnchor.constraint(equalTo: stackView.leadingAnchor, constant: xMargin), + watchControlsStackView.trailingAnchor.constraint(equalTo: stackView.trailingAnchor, constant: -xMargin), + + stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + stackView.topAnchor.constraint(equalTo: scrollView.topAnchor), + stackView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + + buttonsStackView.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor), + buttonsStackView.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor), + buttonsStackView.topAnchor.constraint(equalTo: footerBar.topAnchor), + buttonsStackView.heightAnchor.constraint(equalToConstant: buttonsHeight), + + footerBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), + footerBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), + footerBar.heightAnchor.constraint(equalToConstant: buttonsHeight), + footerBar.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + scrollView.topAnchor.constraint(equalTo: view.topAnchor), + scrollView.bottomAnchor.constraint(equalTo: footerBar.topAnchor), + ] + roundedBackground.createConstraintsWithContainer(view: view)) + + configure() + showKeystoreControlsOnly() + navigationItem.rightBarButtonItems = [ UIBarButtonItem(image: R.image.import_options(), style: .done, target: self, action: #selector(importOptions)), UIBarButtonItem(image: R.image.qr_code_icon(), style: .done, target: self, action: #selector(openReader)), @@ -71,126 +172,97 @@ class ImportWalletViewController: FormViewController { self.demo() } } + } - let recipientRightView = FieldAppereance.addressFieldRightView( - pasteAction: { [unowned self] in self.pasteAddressAction() }, - qrAction: { [unowned self] in self.openReader() } - ) - - form - +++ Section { - var header = HeaderFooterView(.class) - header.height = { 90 } - header.onSetupView = { (view, section) -> Void in - view.label.attributedText = R.string.localizable.walletImportSubtitle().styled( - with: - .color(UIColor(hex: "6e6e72")), - .font(Fonts.regular(size: 16)!), - .lineHeightMultiple(1.25) - ) - view.logoImageView.image = R.image.create_wallet_import() - } - $0.header = header - } - - <<< SegmentedRow(Values.segment) { - $0.options = [ - //ImportSelectionType.mnemonic.title, - ImportSelectionType.keystore.title, - ImportSelectionType.privateKey.title, - ImportSelectionType.watch.title, - ] - $0.value = ImportSelectionType.keystore.title - } + func configure() { + view.backgroundColor = viewModel.backgroundColor - <<< AppFormAppearance.textArea(tag: Values.mnemonic) { - $0.placeholder = R.string.localizable.mnemonic() - $0.textAreaHeight = .fixed(cellHeight: 140) - $0.add(rule: RuleRequired()) + keystoreJSONTextView.configureOnce() + keystoreJSONTextView.label.textAlignment = .center + keystoreJSONTextView.label.text = viewModel.keystoreJSONLabel - $0.hidden = Eureka.Condition.function([Values.segment], { _ in - return self.segmentRow?.value != ImportSelectionType.mnemonic.title - }) - } + passwordTextField.configureOnce() + passwordTextField.label.textAlignment = .center + passwordTextField.label.text = viewModel.passwordLabel - <<< AppFormAppearance.textArea(tag: Values.keystore) { - $0.placeholder = R.string.localizable.keystoreJSON() - $0.textAreaHeight = .fixed(cellHeight: 140) - $0.add(rule: RuleRequired()) + privateKeyTextView.configureOnce() + privateKeyTextView.label.textAlignment = .center + privateKeyTextView.label.text = viewModel.privateKeyLabel - $0.hidden = Eureka.Condition.function([Values.segment], { _ in - return self.segmentRow?.value != ImportSelectionType.keystore.title - }) - } + watchAddressTextField.configureOnce() + watchAddressTextField.label.textAlignment = .center + watchAddressTextField.label.text = viewModel.watchAddressLabel - <<< AppFormAppearance.textArea(tag: Values.privateKey) { - $0.placeholder = R.string.localizable.privateKey() - $0.textAreaHeight = .fixed(cellHeight: 140) - $0.add(rule: RuleRequired()) - $0.add(rule: PrivateKeyRule()) - $0.hidden = Eureka.Condition.function([Values.segment], { _ in - return self.segmentRow?.value != ImportSelectionType.privateKey.title - }) - } + importButton.setTitleColor(viewModel.buttonTitleColor, for: .normal) + importButton.backgroundColor = viewModel.buttonBackgroundColor + importButton.titleLabel?.font = viewModel.buttonFont + } - <<< AppFormAppearance.textFieldFloat(tag: Values.watch) { - $0.add(rule: RuleRequired()) - $0.add(rule: EthereumAddressRule()) - $0.hidden = Eureka.Condition.function([Values.segment], { _ in - return self.segmentRow?.value != ImportSelectionType.watch.title - }) - }.cellUpdate { cell, _ in - cell.textField.placeholder = R.string.localizable.ethereumAddress() - cell.textField.rightView = recipientRightView - cell.textField.rightViewMode = .always - } + func didImport(account: Wallet) { + delegate?.didImportAccount(account: account, in: self) + } - <<< AppFormAppearance.textFieldFloat(tag: Values.password) { - $0.validationOptions = .validatesOnDemand - $0.hidden = Eureka.Condition.function([Values.segment], { _ in - return self.segmentRow?.value != ImportSelectionType.keystore.title - }) - }.cellUpdate { cell, _ in - cell.textField.isSecureTextEntry = true - cell.textField.textAlignment = .left - cell.textField.placeholder = R.string.localizable.password() - } + ///Returns true only if valid + private func validate() -> Bool { + switch tabBar.tab { + case .keystore: + return validateKeystore() + case .privateKey: + return validatePrivateKey() + case .watch: + return validateWatch() + default: + return true + } + } - +++ Section("") + ///Returns true only if valid + private func validateKeystore() -> Bool { + if keystoreJSONTextView.value.isEmpty { + displayError(title: viewModel.keystoreJSONLabel, error: ValidationError(msg: R.string.localizable.warningFieldRequired())) + return false + } + if passwordTextField.value.isEmpty { + displayError(title: viewModel.passwordLabel, error: ValidationError(msg: R.string.localizable.warningFieldRequired())) + return false + } + return true + } - <<< ButtonRow(R.string.localizable.importWalletImportButtonTitle()) { - $0.title = $0.tag - }.onCellSelection { [unowned self] _, _ in - self.importWallet() - } + ///Returns true only if valid + private func validatePrivateKey() -> Bool { + if let validationError = PrivateKeyRule().isValid(value: passwordTextField.value) { + displayError(error: ValidationError(msg: validationError.msg)) + return false + } + return true } - func didImport(account: Wallet) { - delegate?.didImportAccount(account: account, in: self) + ///Returns true only if valid + private func validateWatch() -> Bool { + if let validationError = EthereumAddressRule().isValid(value: watchAddressTextField.value) { + displayError(error: ValidationError(msg: validationError.msg)) + return false + } + return true } - func importWallet() { - let validatedError = keystoreRow?.section?.form?.validate() - guard let errors = validatedError, errors.isEmpty else { return } + @objc func importWallet() { + guard validate() else { return } - let keystoreInput = keystoreRow?.value?.trimmed ?? "" - let privateKeyInput = privateKeyRow?.value?.trimmed ?? "" - let password = passwordRow?.value ?? "" - let watchInput = watchRow?.value?.trimmed ?? "" - let mnemonicInput = mnemonicRow?.value?.trimmed ?? "" - let words = mnemonicInput.components(separatedBy: " ").map { $0.trimmed } + let keystoreInput = keystoreJSONTextView.value.trimmed + let privateKeyInput = privateKeyTextView.value.trimmed + let password = passwordTextField.value.trimmed + let watchInput = watchAddressTextField.value.trimmed displayLoading(text: R.string.localizable.importWalletImportingIndicatorLabelTitle(), animated: false) - let type = ImportSelectionType(title: segmentRow?.value) let importType: ImportType = { - switch type { + switch tabBar.tab { case .keystore: return .keystore(string: keystoreInput, password: password) case .privateKey: return .privateKey(privateKey: privateKeyInput) - case .mnemonic: - return .mnemonic(words: words, password: password) case .watch: let address = Address(string: watchInput)! // Address validated by form view. return .watch(address: address) @@ -246,40 +318,60 @@ class ImportWalletViewController: FormViewController { } func setValueForCurrentField(string: String) { - let type = ImportSelectionType(title: segmentRow?.value) - switch type { + switch tabBar.tab { case .keystore: - keystoreRow?.value = string - keystoreRow?.reload() + keystoreJSONTextView.value = string case .privateKey: - privateKeyRow?.value = string - privateKeyRow?.reload() + privateKeyTextView.value = string case .watch: - watchRow?.value = string - watchRow?.reload() - case .mnemonic: - mnemonicRow?.value = string - mnemonicRow?.reload() + watchAddressTextField.value = string + default: + return } } - @objc func pasteAddressAction() { - let value = UIPasteboard.general.string?.trimmed - watchRow?.value = value - watchRow?.reload() - } - required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + private func showKeystoreControlsOnly() { + keystoreJSONControlsStackView.isHidden = false + privateKeyControlsStackView.isHidden = true + watchControlsStackView.isHidden = true + } + private func showPrivateKeyControlsOnly() { + keystoreJSONControlsStackView.isHidden = true + privateKeyControlsStackView.isHidden = false + watchControlsStackView.isHidden = true + } + private func showWatchControlsOnly() { + keystoreJSONControlsStackView.isHidden = true + privateKeyControlsStackView.isHidden = true + watchControlsStackView.isHidden = false + } + + private func moveFocusToTextEntryField(after textInput: UIView) { + switch textInput { + case keystoreJSONTextView.textView: + passwordTextField.textField.becomeFirstResponder() + case passwordTextField.textField: + view.endEditing(true) + case privateKeyTextView.textView: + view.endEditing(true) + case watchAddressTextField.textField: + view.endEditing(true) + default: + break + } + } } extension ImportWalletViewController: UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentAt url: URL) { - if controller.documentPickerMode == UIDocumentPickerMode.import { - let text = try? String(contentsOfFile: url.path) - keystoreRow?.value = text - keystoreRow?.reload() + guard controller.documentPickerMode == UIDocumentPickerMode.import else { return } + let text = try? String(contentsOfFile: url.path) + if let text = text { + keystoreJSONTextView.value = text } } } @@ -295,3 +387,71 @@ extension ImportWalletViewController: QRCodeReaderDelegate { reader.dismiss(animated: true) } } + +extension ImportWalletViewController: TextFieldDelegate { + func shouldReturn(in textField: TextField) -> Bool { + moveFocusToTextEntryField(after: textField.textField) + return false + } + + func doneButtonTapped(for textField: TextField) { + view.endEditing(true) + } + + func nextButtonTapped(for textField: TextField) { + moveFocusToTextEntryField(after: textField.textField) + } +} + +extension ImportWalletViewController: TextViewDelegate { + func shouldReturn(in textView: TextView) -> Bool { + moveFocusToTextEntryField(after: textView.textView) + return false + } + + func doneButtonTapped(for textView: TextView) { + view.endEditing(true) + } + + func nextButtonTapped(for textView: TextView) { + moveFocusToTextEntryField(after: textView.textView) + } +} + +extension ImportWalletViewController: AddressTextFieldDelegate { + func displayError(error: Error, for textField: AddressTextField) { + displayError(error: error) + } + + func openQRCodeReader(for textField: AddressTextField) { + openReader() + } + + func didPaste(in textField: AddressTextField) { + view.endEditing(true) + } + + func shouldReturn(in textField: AddressTextField) -> Bool { + moveFocusToTextEntryField(after: textField.textField) + return false + } + + func shouldChange(in range: NSRange, to string: String, in textField: AddressTextField) -> Bool { + return true + } +} + +extension ImportWalletViewController: ImportWalletTabBarDelegate { + func didPressImportWalletTab(tab: ImportWalletTab, in tabBar: ImportWalletTabBar) { + switch tab { + case .keystore: + showKeystoreControlsOnly() + case .privateKey: + showPrivateKeyControlsOnly() + case .watch: + showWatchControlsOnly() + default: + break + } + } +} diff --git a/Trust/Wallet/ViewModels/ImportWalletTabBarViewModel.swift b/Trust/Wallet/ViewModels/ImportWalletTabBarViewModel.swift new file mode 100644 index 000000000..15c92f915 --- /dev/null +++ b/Trust/Wallet/ViewModels/ImportWalletTabBarViewModel.swift @@ -0,0 +1,36 @@ +// Copyright © 2018 Stormbird PTE. LTD. + +import UIKit + +struct ImportWalletTabBarViewModel { + var currentTab: ImportWalletTab + + init(tab: ImportWalletTab) { + currentTab = tab + } + + var backgroundColor: UIColor { + return Colors.appBackground + } + + func titleColor(for tab: ImportWalletTab) -> UIColor { + if currentTab == tab { + return Colors.appBackground + } else { + return Colors.appWhite + } + } + + var font: UIFont { + return Fonts.regular(size: 14)! + } + + var barUnhighlightedColor: UIColor { + return UIColor(red: 122, green: 197, blue: 225) + } + + var barHighlightedColor: UIColor { + return Colors.appWhite + } +} + diff --git a/Trust/Wallet/ViewModels/ImportWalletViewModel.swift b/Trust/Wallet/ViewModels/ImportWalletViewModel.swift index eb1aa9c02..9f875ae53 100644 --- a/Trust/Wallet/ViewModels/ImportWalletViewModel.swift +++ b/Trust/Wallet/ViewModels/ImportWalletViewModel.swift @@ -1,10 +1,42 @@ // Copyright SIX DAY LLC. All rights reserved. -import Foundation +import UIKit struct ImportWalletViewModel { + var backgroundColor: UIColor { + return Colors.appBackground + } var title: String { return R.string.localizable.importNavigationTitle() } + + var buttonTitleColor: UIColor { + return Colors.appWhite + } + + var buttonBackgroundColor: UIColor { + return Colors.appHighlightGreen + } + + var buttonFont: UIFont { + return Fonts.regular(size: 20)! + } + + var keystoreJSONLabel: String { + return R.string.localizable.keystoreJSON().uppercased() + } + + var passwordLabel: String { + return R.string.localizable.password().uppercased() + } + + var privateKeyLabel: String { + return R.string.localizable.privateKey().uppercased() + } + + var watchAddressLabel: String { + return R.string.localizable.ethereumAddress().uppercased() + } + } diff --git a/Trust/Wallet/Views/ImportWalletTabBar.swift b/Trust/Wallet/Views/ImportWalletTabBar.swift new file mode 100644 index 000000000..f97f2f8c1 --- /dev/null +++ b/Trust/Wallet/Views/ImportWalletTabBar.swift @@ -0,0 +1,138 @@ +// Copyright © 2018 Stormbird PTE. LTD. + +import UIKit + +protocol ImportWalletTabBarDelegate: class { + func didPressImportWalletTab(tab: ImportWalletTab, in tabBar: ImportWalletTabBar) +} + +class ImportWalletTabBar: UIView { + let keystoreButton = UIButton(type: .system) + let privateKeyButton = UIButton(type: .system) + let watchButton = UIButton(type: .system) + let tabHighlightView = UIView() + var tab: ImportWalletTab = .keystore { + didSet { + viewModel.currentTab = tab + delegate?.didPressImportWalletTab(tab: tab, in: self) + configure() + } + } + var highlightBarHorizontalConstraints: [NSLayoutConstraint]? + weak var delegate: ImportWalletTabBarDelegate? + lazy var viewModel = ImportWalletTabBarViewModel(tab: tab) + + override init(frame: CGRect) { + super.init(frame: frame) + + backgroundColor = viewModel.backgroundColor + + keystoreButton.setTitle(ImportSelectionType.keystore.title.uppercased(), for: .normal) + keystoreButton.titleLabel?.font = viewModel.font + keystoreButton.addTarget(self, action: #selector(showKeystoreTab), for: .touchUpInside) + + privateKeyButton.setTitle(ImportSelectionType.privateKey.title.uppercased(), for: .normal) + privateKeyButton.titleLabel?.font = viewModel.font + privateKeyButton.addTarget(self, action: #selector(showPrivateKeyTab), for: .touchUpInside) + + watchButton.setTitle(ImportSelectionType.watch.title.uppercased(), for: .normal) + watchButton.titleLabel?.font = viewModel.font + watchButton.addTarget(self, action: #selector(showWatchTab), for: .touchUpInside) + + let fullWidthBar = UIView() + fullWidthBar.translatesAutoresizingMaskIntoConstraints = false + fullWidthBar.backgroundColor = .clear + fullWidthBar.isUserInteractionEnabled = false + addSubview(fullWidthBar) + + tabHighlightView.translatesAutoresizingMaskIntoConstraints = false + tabHighlightView.backgroundColor = viewModel.barHighlightedColor + fullWidthBar.addSubview(tabHighlightView) + + let buttonsStackView = [keystoreButton, privateKeyButton, watchButton].asStackView(spacing: 20) + buttonsStackView.translatesAutoresizingMaskIntoConstraints = false + addSubview(buttonsStackView) + + let barHeightConstraint = fullWidthBar.heightAnchor.constraint(equalToConstant: 44) + barHeightConstraint.priority = .defaultHigh + let stackViewLeadingConstraint = buttonsStackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 17) + stackViewLeadingConstraint.priority = .defaultHigh + let stackViewTrailingConstraint = buttonsStackView.widthAnchor.constraint(lessThanOrEqualTo: widthAnchor, constant: -17) + stackViewTrailingConstraint.priority = .defaultHigh + NSLayoutConstraint.activate([ + stackViewLeadingConstraint, + stackViewTrailingConstraint, + buttonsStackView.topAnchor.constraint(equalTo: topAnchor), + buttonsStackView.bottomAnchor.constraint(equalTo: bottomAnchor), + + keystoreButton.widthAnchor.constraint(equalTo: privateKeyButton.widthAnchor), + keystoreButton.widthAnchor.constraint(equalTo: watchButton.widthAnchor), + + fullWidthBar.leadingAnchor.constraint(equalTo: leadingAnchor), + fullWidthBar.trailingAnchor.constraint(equalTo: trailingAnchor), + barHeightConstraint, + fullWidthBar.topAnchor.constraint(equalTo: topAnchor), + fullWidthBar.bottomAnchor.constraint(equalTo: bottomAnchor), + + tabHighlightView.topAnchor.constraint(equalTo: fullWidthBar.topAnchor), + tabHighlightView.bottomAnchor.constraint(equalTo: fullWidthBar.bottomAnchor, constant: 20), + ]) + + configure() + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func showKeystoreTab() { + tab = .keystore + } + + @objc func showPrivateKeyTab() { + tab = .privateKey + } + + @objc func showWatchTab() { + tab = .watch + } + + private func configure() { + configureButtonColors() + configureHighlightedBar() + } + + private func configureButtonColors() { + keystoreButton.setTitleColor(viewModel.titleColor(for: .keystore), for: .normal) + privateKeyButton.setTitleColor(viewModel.titleColor(for: .privateKey), for: .normal) + watchButton.setTitleColor(viewModel.titleColor(for: .watch), for: .normal) + } + + private func configureHighlightedBar() { + tabHighlightView.cornerRadius = 14 + + var button: UIButton + switch tab { + case .keystore: + button = keystoreButton + case .privateKey: + button = privateKeyButton + case .watch: + button = watchButton + } + + if let previousConstraints = highlightBarHorizontalConstraints { + NSLayoutConstraint.deactivate(previousConstraints) + } + highlightBarHorizontalConstraints = [ + tabHighlightView.leadingAnchor.constraint(equalTo: button.leadingAnchor, constant: -10), + tabHighlightView.trailingAnchor.constraint(equalTo: button.trailingAnchor, constant: 10), + ] + if let constraints = highlightBarHorizontalConstraints { + NSLayoutConstraint.activate(constraints) + } + UIView.animate(withDuration: 0.3) { + self.layoutIfNeeded() + } + } +} diff --git a/TrustTests/Settings/ConfigTests.swift b/TrustTests/Settings/ConfigTests.swift index c57eabd44..e36ff313e 100644 --- a/TrustTests/Settings/ConfigTests.swift +++ b/TrustTests/Settings/ConfigTests.swift @@ -51,5 +51,17 @@ class ConfigTests: XCTestCase { //Must change this back to system, otherwise other tests will break either immediately or the next run config.locale = AppLocale.system.id } -} + func testNibsAccessAfterSwitchingLocale() { + var config: Config = .make() + + config.locale = AppLocale.english.id + config.locale = AppLocale.simplifiedChinese.id + let controller = AccountsViewController(keystore: FakeKeystore(), balanceCoordinator: FakeGetBalanceCoordinator()) + let _ = controller.view + XCTAssertNoThrow(controller.tableView.dequeueReusableCell(withIdentifier: R.nib.accountViewCell.name, for: .init(row: 0, section: 0))) + + //Must change this back to system, otherwise other tests will break either immediately or the next run + config.locale = AppLocale.system.id + } +}