From 5534f35d843951a45740519b24a846026a9b926b Mon Sep 17 00:00:00 2001 From: Hwee-Boon Yar Date: Mon, 16 Apr 2018 11:57:13 +0800 Subject: [PATCH 1/2] Update import ticket UI --- Trust.xcodeproj/project.pbxproj | 8 + Trust/AppDelegate.swift | 14 +- .../Localization/en.lproj/Localizable.strings | 9 +- .../UniversalLinkCoordinator.swift | 107 ++++--- .../ImportTicketViewController.swift | 268 ++++++++++++++++++ .../ImportTicketViewControllerViewModel.swift | 264 +++++++++++++++++ 6 files changed, 630 insertions(+), 40 deletions(-) create mode 100644 Trust/Market/ViewControllers/ImportTicketViewController.swift create mode 100644 Trust/Market/ViewModels/ImportTicketViewControllerViewModel.swift diff --git a/Trust.xcodeproj/project.pbxproj b/Trust.xcodeproj/project.pbxproj index 147488be2..9525c87a4 100644 --- a/Trust.xcodeproj/project.pbxproj +++ b/Trust.xcodeproj/project.pbxproj @@ -303,6 +303,7 @@ 5E7C731B88842C036A74A039 /* AlphaWalletSettingsButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C71EBD4C95AD4E11F3352 /* AlphaWalletSettingsButtonRow.swift */; }; 5E7C73305DF984B99E94D9F9 /* TransferModeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7717D829205D1E254AC1 /* TransferModeButton.swift */; }; 5E7C733638D7596F93DEE2A9 /* OnboardingCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75CE3F1D6B7993E7A840 /* OnboardingCollectionViewController.swift */; }; + 5E7C7376B566E5A59CC8F463 /* ImportTicketViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72D0E7CA03ADE5CFAE7A /* ImportTicketViewControllerViewModel.swift */; }; 5E7C73FC3990D110C474C3D6 /* WalletFilterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75CC640BAFFE0E789F44 /* WalletFilterViewModel.swift */; }; 5E7C73FD5BD75D90C8D0EF3C /* WalletFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7C58586099F082973073 /* WalletFilterView.swift */; }; 5E7C745DACB5FCCEBCEB49CA /* WelcomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C793E23E2364B73C4D813 /* WelcomeViewController.swift */; }; @@ -314,6 +315,7 @@ 5E7C75E3C4BAE885746BD1B3 /* TransferTicketViaWalletAddressViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C72B37551451352EBB9F9 /* TransferTicketViaWalletAddressViewController.swift */; }; 5E7C75F80A7E178B49830BCD /* TicketsViewControllerHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C796039C0F47CDCA236C0 /* TicketsViewControllerHeader.swift */; }; 5E7C760C7D55C97424F55138 /* TicketTableViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C75F877B2F2E24C7EF258 /* TicketTableViewCellViewModel.swift */; }; + 5E7C76605A5102FBD376F32A /* ImportTicketViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7535095323B035CA47C0 /* ImportTicketViewController.swift */; }; 5E7C76A0365D128B7F19A0C2 /* ProtectionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74BEC095303B66FB4B1E /* ProtectionCoordinator.swift */; }; 5E7C76A65C14D0F11AF7848F /* TicketRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7742709724B3BD0C2A0D /* TicketRowViewModel.swift */; }; 5E7C76B917517C93D1E26B0A /* LockEnterPasscodeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7981AB6584B25C72D46B /* LockEnterPasscodeCoordinator.swift */; }; @@ -793,6 +795,7 @@ 5E7C71EBD4C95AD4E11F3352 /* AlphaWalletSettingsButtonRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AlphaWalletSettingsButtonRow.swift; path = Views/AlphaWalletSettingsButtonRow.swift; sourceTree = ""; }; 5E7C72142D5817EF8FA8CADA /* PrivacyPolicyViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrivacyPolicyViewController.swift; sourceTree = ""; }; 5E7C72B37551451352EBB9F9 /* TransferTicketViaWalletAddressViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferTicketViaWalletAddressViewController.swift; sourceTree = ""; }; + 5E7C72D0E7CA03ADE5CFAE7A /* ImportTicketViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportTicketViewControllerViewModel.swift; sourceTree = ""; }; 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; }; 5E7C741196D9D9C9C3EE5E30 /* LockCreatePasscodeViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockCreatePasscodeViewController.swift; sourceTree = ""; }; @@ -804,6 +807,7 @@ 5E7C74DCC21272EC231A20E2 /* RequestViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestViewController.swift; sourceTree = ""; }; 5E7C74FABE14B7B1BEEC4F5E /* WhyUseEthereumInfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhyUseEthereumInfoViewController.swift; sourceTree = ""; }; 5E7C7534FB6BF4D199643246 /* AlphaWalletSettingsSwitchRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AlphaWalletSettingsSwitchRow.swift; path = Views/AlphaWalletSettingsSwitchRow.swift; sourceTree = ""; }; + 5E7C7535095323B035CA47C0 /* ImportTicketViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportTicketViewController.swift; sourceTree = ""; }; 5E7C755132D9B6F95080A1BE /* TransferTicketsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransferTicketsCoordinator.swift; sourceTree = ""; }; 5E7C7564AF453BAB0BDAAA57 /* SettingsAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAction.swift; sourceTree = ""; }; 5E7C75BE23CDD9CD271EC30C /* ChooseTicketTransferModeViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ChooseTicketTransferModeViewControllerViewModel.swift; sourceTree = ""; }; @@ -2272,6 +2276,7 @@ isa = PBXGroup; children = ( 5E7C7B82CC07F290B9CAA4E4 /* StatusViewController.swift */, + 5E7C7535095323B035CA47C0 /* ImportTicketViewController.swift */, ); path = ViewControllers; sourceTree = ""; @@ -2305,6 +2310,7 @@ isa = PBXGroup; children = ( 5E7C7BD9B4BDAFC2D9EBD741 /* StatusViewControllerViewModel.swift */, + 5E7C72D0E7CA03ADE5CFAE7A /* ImportTicketViewControllerViewModel.swift */, ); path = ViewModels; sourceTree = ""; @@ -3514,6 +3520,8 @@ 5E7C7567A690B6B8F889AE83 /* SendViewController.swift in Sources */, 5E7C72E1D4B4B4C8443F3DA1 /* SendHeaderView.swift in Sources */, 5E7C713ACE8C72642B1C9F93 /* SendHeaderViewViewModel.swift in Sources */, + 5E7C7376B566E5A59CC8F463 /* ImportTicketViewControllerViewModel.swift in Sources */, + 5E7C76605A5102FBD376F32A /* ImportTicketViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Trust/AppDelegate.swift b/Trust/AppDelegate.swift index 83783a210..a2bda528b 100644 --- a/Trust/AppDelegate.swift +++ b/Trust/AppDelegate.swift @@ -9,6 +9,8 @@ import RealmSwift class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate { var window: UIWindow? var coordinator: AppCoordinator! + // Need to retain while still processing + var universalLinkCoordinator: UniversalLinkCoordinator! //This is separate coordinator for the protection of the sensitive information. lazy var protectionCoordinator: ProtectionCoordinator = { return ProtectionCoordinator() @@ -82,10 +84,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele Branch.getInstance().continue(userActivity) let url = userActivity.webpageURL - let coordinator = UniversalLinkCoordinator() - coordinator.delegate = self - coordinator.start() - let handled = coordinator.handleUniversalLink(url: url) + universalLinkCoordinator = UniversalLinkCoordinator() + universalLinkCoordinator.delegate = self + universalLinkCoordinator.start() + let handled = universalLinkCoordinator.handleUniversalLink(url: url) //TODO: if we handle other types of URLs, check if handled==false, then we pass the url to another handlers return true @@ -103,4 +105,8 @@ extension AppDelegate: UniversalLinkCoordinatorDelegate { return nil } } + + func completed(in coordinator: UniversalLinkCoordinator) { + universalLinkCoordinator = nil + } } diff --git a/Trust/Localization/en.lproj/Localizable.strings b/Trust/Localization/en.lproj/Localizable.strings index 2927156c4..6a5338c52 100644 --- a/Trust/Localization/en.lproj/Localizable.strings +++ b/Trust/Localization/en.lproj/Localizable.strings @@ -248,11 +248,18 @@ "a.welcome.onboarding.createwallet.button.title" = "GET STARTED"; "a.settings.advanced.label.title" = "Advanced"; "a.marketplace.tabbar.item.title" = "Marketplace"; +"a.claim.ticket.title" = "Importing tickets"; "a.claim.ticket.success.title" = "Your ticket has been transferred and the balance will be updated shortly"; "a.claim.ticket.failed.title" = "Invalid ticket link"; -"a.claim.ticket.inProgress.title" = "Importing ticket..."; +"a.claim.ticket.validating.title" = "Processing..."; +"a.claim.ticket.promptImport.title" = "Import?"; +"a.claim.ticket.inProgress.title" = "Importing tickets..."; "a.claim.ticket.done.button.title" = "Done"; "a.claim.ticket.import.button.title" = "Import"; +"a.claim.ticket.purchase.button.title" = "Import"; +"a.claim.ticket.ethCostLabel.title" = "Total Cost"; +"a.claim.ticket.ethCost.free.title" = "Free Import"; +"a.claim.ticket.dollarCostLabel.title" = "EQUIVALENT IN USD"; "a.setup.reminder.text.text" = "Please set up text messaging to send a text."; "a.setup.reminder.email.text" = "Please set up a Mail account in order to send email."; "a.send.receive.button.title" = "Send/Receive"; diff --git a/Trust/Market/Coordinators/UniversalLinkCoordinator.swift b/Trust/Market/Coordinators/UniversalLinkCoordinator.swift index 054f93ec5..ab798da43 100644 --- a/Trust/Market/Coordinators/UniversalLinkCoordinator.swift +++ b/Trust/Market/Coordinators/UniversalLinkCoordinator.swift @@ -5,14 +5,16 @@ import Alamofire protocol UniversalLinkCoordinatorDelegate: class { func viewControllerForPresenting(in coordinator: UniversalLinkCoordinator) -> UIViewController? + func completed(in coordinator: UniversalLinkCoordinator) } class UniversalLinkCoordinator: Coordinator { var coordinators: [Coordinator] = [] weak var delegate: UniversalLinkCoordinatorDelegate? - var statusViewController: StatusViewController? + var importTicketViewController: ImportTicketViewController? func start() { + preparingToImportUniversalLink() } //Returns true if handled @@ -22,7 +24,7 @@ class UniversalLinkCoordinator: Coordinator { return false } let keystore = try! EtherKeystore() - let signedOrder = UniversalLinkHandler().parseUniversalLink(url: (url?.absoluteString)!) + let signedOrder = UniversalLinkHandler().parseUniversalLink(url: (url?.absoluteString)!) let signature = signedOrder.signature.substring(from: 2) // form the json string out of the order for the paymaster server @@ -35,50 +37,79 @@ class UniversalLinkCoordinator: Coordinator { } //cut off last comma indicesStringEncoded = indicesStringEncoded.substring(to: indicesStringEncoded.count - 1) - let address = (keystore.recentlyUsedWallet?.address.eip55String)! + let address = (keystore.recentlyUsedWallet?.address.eip55String)! let parameters: Parameters = [ - "address": address, + "address": address, "indices": indicesStringEncoded, - "expiry": signedOrder.order.expiry.description, + "expiry": signedOrder.order.expiry.description, "v": signature.substring(from: 128), "r": "0x" + signature.substring(with: Range(uncheckedBounds: (0, 64))), "s": "0x" + signature.substring(with: Range(uncheckedBounds: (64, 128))) ] - let query = UniversalLinkHandler.paymentServer + let query = UniversalLinkHandler.paymentServer //TODO check if URL is valid or not by validating signature, low priority - //TODO localize if signature.count > 128 { - if let viewController = delegate?.viewControllerForPresenting(in: self) { - UIAlertController.alert(title: nil, message: "Import Link?", alertButtonTitles: [R.string.localizable.aClaimTicketImportButtonTitle(), R.string.localizable.cancel()], alertButtonStyles: [.default, .cancel], viewController: viewController) { - if $0 == 0 { - self.importUniversalLink(query: query, parameters: parameters) - } - } - } + //TODO create Ticket instances and 1 TicketHolder instance and compute cost from link's information + let ticket = Ticket(id: 1, index: 1, zone: "", name: "", venue: "", date: Date(), seatId: 1) + let ticketHolder = TicketHolder( + tickets: [ticket], + zone: "ABC", + name: "Applying for mortages (APM)", + venue: "XYZ Stadium", + date: Date(), + status: .available + ) + //nil or "" implies free + let ethCost = "0.00001" + let dollarCost = "0.004" + self.promptImportUniversalLink(ticketHolder: ticketHolder, ethCost: ethCost, dollarCost: dollarCost) } else { - return true + //TODO Pass in error message + self.showImportError(errorMessage: R.string.localizable.aClaimTicketFailedTitle()) } return true } - private func importUniversalLink(query: String, parameters: Parameters) { + private func preparingToImportUniversalLink() { if let viewController = delegate?.viewControllerForPresenting(in: self) { - statusViewController = StatusViewController() - if let vc = statusViewController { + importTicketViewController = ImportTicketViewController() + if let vc = importTicketViewController { vc.delegate = self - vc.configure(viewModel: .init( - state: .processing, - inProgressText: R.string.localizable.aClaimTicketInProgressTitle(), - succeededTextText: R.string.localizable.aClaimTicketSuccessTitle(), - failedText: R.string.localizable.aClaimTicketFailedTitle() - )) - vc.modalPresentationStyle = .overCurrentContext - viewController.present(vc, animated: true) + vc.configure(viewModel: .init(state: .validating)) + viewController.present(UINavigationController(rootViewController: vc), animated: true) + } + } + } + + private func updateImportTicketController(with state: ImportTicketViewControllerViewModel.State, ticketHolder: TicketHolder? = nil, ethCost: String? = nil, dollarCost: String? = nil) { + if let vc = importTicketViewController, var viewModel = vc.viewModel { + viewModel.state = state + if let ticketHolder = ticketHolder, let ethCost = ethCost, let dollarCost = dollarCost { + viewModel.ticketHolder = ticketHolder + viewModel.ethCost = ethCost + viewModel.dollarCost = dollarCost } + vc.configure(viewModel: viewModel) } + } + + private func promptImportUniversalLink(ticketHolder: TicketHolder, ethCost: String, dollarCost: String) { + updateImportTicketController(with: .promptImport, ticketHolder: ticketHolder, ethCost: ethCost, dollarCost: dollarCost) + } + + private func showImportSuccessful() { + updateImportTicketController(with: .succeeded) + } + + private func showImportError(errorMessage: String) { + updateImportTicketController(with: .failed(errorMessage: errorMessage)) + } + + private func importUniversalLink(query: String, parameters: Parameters) { + updateImportTicketController(with: .processing) Alamofire.request( query, @@ -93,17 +124,16 @@ class UniversalLinkCoordinator: Coordinator { successful = true } } - - if let vc = self.statusViewController { + + if let vc = self.importTicketViewController { // TODO handle http response print(result) - if let vc = self.statusViewController, var viewModel = vc.viewModel { + if let vc = self.importTicketViewController, var viewModel = vc.viewModel { if successful { - viewModel.state = .succeeded - vc.configure(viewModel: viewModel) + self.showImportSuccessful() } else { - viewModel.state = .failed - vc.configure(viewModel: viewModel) + //TODO Pass in error message + self.showImportError(errorMessage: R.string.localizable.aClaimTicketFailedTitle()) } } } @@ -112,8 +142,15 @@ class UniversalLinkCoordinator: Coordinator { } -extension UniversalLinkCoordinator: StatusViewControllerDelegate { - func didPressDone(in viewController: StatusViewController) { +extension UniversalLinkCoordinator: ImportTicketViewControllerDelegate { + func didPressDone(in viewController: ImportTicketViewController) { viewController.dismiss(animated: true) + delegate?.completed(in: self) + } + + func didPressImport(in viewController: ImportTicketViewController) { + if let query = viewController.query, let parameters = viewController.parameters { + importUniversalLink(query: query, parameters: parameters) + } } } diff --git a/Trust/Market/ViewControllers/ImportTicketViewController.swift b/Trust/Market/ViewControllers/ImportTicketViewController.swift new file mode 100644 index 000000000..00fb07b47 --- /dev/null +++ b/Trust/Market/ViewControllers/ImportTicketViewController.swift @@ -0,0 +1,268 @@ +// Copyright © 2018 Stormbird PTE. LTD. import Foundation + +import UIKit +import Alamofire + +protocol ImportTicketViewControllerDelegate: class { + func didPressDone(in viewController: ImportTicketViewController) + func didPressImport(in viewController: ImportTicketViewController) +} + +class ImportTicketViewController: UIViewController { + weak var delegate: ImportTicketViewControllerDelegate? + //roundedBackground is used to achieve the top 2 rounded corners-only effect since maskedCorners to not round bottom corners is not available in iOS 10 + let roundedBackground = UIView() + let header = TicketsViewControllerTitleHeader() + let ticketView = TicketRowView() + let statusLabel = UILabel() + let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge) + let costStackView = UIStackView() + let ethCostLabelLabel = UILabel() + let ethCostLabel = UILabel() + let dollarCostLabelLabel = UILabel() + let dollarCostLabel = PaddedLabel() + let buttonSeparator = UIView() + let actionButton = UIButton(type: .system) + let cancelButton = UIButton(type: .system) + var viewModel: ImportTicketViewControllerViewModel? + var query: String? + var parameters: Parameters? + + init() { + super.init(nibName: nil, bundle: nil) + view.backgroundColor = .clear + + ticketView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(ticketView) + + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + activityIndicator.hidesWhenStopped = true + + roundedBackground.translatesAutoresizingMaskIntoConstraints = false + roundedBackground.backgroundColor = Colors.appWhite + roundedBackground.cornerRadius = 20 + view.addSubview(roundedBackground) + + ethCostLabelLabel.translatesAutoresizingMaskIntoConstraints = false + ethCostLabel.translatesAutoresizingMaskIntoConstraints = false + dollarCostLabelLabel.translatesAutoresizingMaskIntoConstraints = false + dollarCostLabel.translatesAutoresizingMaskIntoConstraints = false + + let separator1 = UIView() + separator1.backgroundColor = UIColor(red: 230, green: 230, blue: 230) + + let separator2 = UIView() + separator2.backgroundColor = UIColor(red: 230, green: 230, blue: 230) + + costStackView.addArrangedSubview(ethCostLabelLabel) + costStackView.addArrangedSubview(.spacer(height: 7)) + costStackView.addArrangedSubview(separator1) + costStackView.addArrangedSubview(.spacer(height: 7)) + costStackView.addArrangedSubview(ethCostLabel) + costStackView.addArrangedSubview(.spacer(height: 7)) + costStackView.addArrangedSubview(separator2) + costStackView.addArrangedSubview(.spacer(height: 7)) + costStackView.addArrangedSubview(dollarCostLabelLabel) + costStackView.addArrangedSubview(.spacer(height: 3)) + costStackView.addArrangedSubview(dollarCostLabel) + costStackView.translatesAutoresizingMaskIntoConstraints = false + costStackView.axis = .vertical + costStackView.spacing = 0 + costStackView.distribution = .fill + costStackView.alignment = .center + + actionButton.addTarget(self, action: #selector(actionTapped), for: .touchUpInside) + + cancelButton.addTarget(self, action: #selector(cancel), for: .touchUpInside) + + let buttonsStackView = UIStackView(arrangedSubviews: [actionButton, cancelButton]) + buttonsStackView.translatesAutoresizingMaskIntoConstraints = false + buttonsStackView.axis = .horizontal + buttonsStackView.spacing = 0 + buttonsStackView.distribution = .fillEqually + buttonsStackView.setContentHuggingPriority(UILayoutPriority.required, for: .horizontal) + + let stackView = UIStackView(arrangedSubviews: [ + header, + .spacer(height: 1), + ticketView, + .spacer(height: 1), + activityIndicator, + .spacer(height: 14), + statusLabel, + .spacer(height: 20), + costStackView, + ]) + stackView.translatesAutoresizingMaskIntoConstraints = false + stackView.axis = .vertical + stackView.spacing = 0 + stackView.distribution = .fill + stackView.alignment = .center + roundedBackground.addSubview(stackView) + + let marginToHideBottomRoundedCorners = CGFloat(30) + let footerBar = UIView() + footerBar.translatesAutoresizingMaskIntoConstraints = false + footerBar.backgroundColor = Colors.appHighlightGreen + roundedBackground.addSubview(footerBar) + + let buttonsHeight = CGFloat(60) + footerBar.addSubview(buttonsStackView) + + buttonSeparator.translatesAutoresizingMaskIntoConstraints = false + buttonSeparator.backgroundColor = Colors.appLightButtonSeparator + footerBar.addSubview(buttonSeparator) + + let separatorThickness = CGFloat(1) + + NSLayoutConstraint.activate([ + header.heightAnchor.constraint(equalToConstant: 90), + + ticketView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + ticketView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + separator1.heightAnchor.constraint(equalToConstant: 1), + separator1.leadingAnchor.constraint(equalTo: ticketView.background.leadingAnchor), + separator1.trailingAnchor.constraint(equalTo: ticketView.background.trailingAnchor), + + separator2.heightAnchor.constraint(equalToConstant: 1), + separator2.leadingAnchor.constraint(equalTo: ticketView.background.leadingAnchor), + separator2.trailingAnchor.constraint(equalTo: ticketView.background.trailingAnchor), + + buttonsStackView.leadingAnchor.constraint(equalTo: footerBar.leadingAnchor), + buttonsStackView.trailingAnchor.constraint(equalTo: footerBar.trailingAnchor), + buttonsStackView.topAnchor.constraint(equalTo: footerBar.topAnchor), + buttonsStackView.heightAnchor.constraint(equalToConstant: buttonsHeight), + + buttonSeparator.leadingAnchor.constraint(equalTo: actionButton.trailingAnchor, constant: -separatorThickness / 2), + buttonSeparator.trailingAnchor.constraint(equalTo: cancelButton.leadingAnchor, constant: separatorThickness / 2), + buttonSeparator.topAnchor.constraint(equalTo: buttonsStackView.topAnchor, constant: 8), + buttonSeparator.bottomAnchor.constraint(equalTo: buttonsStackView.bottomAnchor, constant: -8), + + footerBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), + footerBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), + footerBar.heightAnchor.constraint(equalToConstant: buttonsHeight), + footerBar.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + statusLabel.widthAnchor.constraint(equalTo: ticketView.widthAnchor, constant: -20), + + stackView.leadingAnchor.constraint(equalTo: roundedBackground.leadingAnchor), + stackView.trailingAnchor.constraint(equalTo: roundedBackground.trailingAnchor), + stackView.topAnchor.constraint(equalTo: roundedBackground.topAnchor), + + roundedBackground.leadingAnchor.constraint(equalTo: view.leadingAnchor), + roundedBackground.trailingAnchor.constraint(equalTo: view.trailingAnchor), + roundedBackground.topAnchor.constraint(equalTo: view.topAnchor), + roundedBackground.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: marginToHideBottomRoundedCorners), + ]) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(viewModel: ImportTicketViewControllerViewModel) { + self.viewModel = viewModel + if let viewModel = self.viewModel { + view.backgroundColor = viewModel.backgroundColor + + header.configure(title: viewModel.headerTitle) + + ticketView.configure(viewModel: .init()) + + ticketView.isHidden = !viewModel.showTicketRow + + ticketView.stateLabel.isHidden = true + + ticketView.ticketCountLabel.text = viewModel.ticketCount + ticketView.titleLabel.text = viewModel.title + ticketView.venueLabel.text = viewModel.venue + ticketView.dateLabel.text = viewModel.date + ticketView.seatRangeLabel.text = viewModel.seatRange + ticketView.zoneNameLabel.text = viewModel.zoneName + + ticketView.dateImageView.isHidden = !viewModel.showTicketRowIcons + ticketView.seatRangeImageView.isHidden = !viewModel.showTicketRowIcons + ticketView.zoneNameImageView.isHidden = !viewModel.showTicketRowIcons + + statusLabel.textColor = viewModel.statusColor + statusLabel.font = viewModel.statusFont + statusLabel.textAlignment = .center + statusLabel.text = viewModel.statusText + statusLabel.numberOfLines = 0 + + costStackView.isHidden = !viewModel.showCost + + ethCostLabelLabel.textColor = viewModel.ethCostLabelLabelColor + ethCostLabelLabel.font = viewModel.ethCostLabelLabelFont + ethCostLabelLabel.textAlignment = .center + ethCostLabelLabel.text = viewModel.ethCostLabelLabelText + + ethCostLabel.textColor = viewModel.ethCostLabelColor + ethCostLabel.font = viewModel.ethCostLabelFont + ethCostLabel.textAlignment = .center + ethCostLabel.text = viewModel.ethCostLabelText + + dollarCostLabelLabel.textColor = viewModel.dollarCostLabelLabelColor + dollarCostLabelLabel.font = viewModel.dollarCostLabelLabelFont + dollarCostLabelLabel.textAlignment = .center + dollarCostLabelLabel.text = viewModel.dollarCostLabelLabelText + + dollarCostLabel.textColor = viewModel.dollarCostLabelColor + dollarCostLabel.font = viewModel.dollarCostLabelFont + dollarCostLabel.textAlignment = .center + dollarCostLabel.text = viewModel.dollarCostLabelText + dollarCostLabel.backgroundColor = viewModel.dollarCostLabelBackgroundColor + dollarCostLabel.layer.masksToBounds = true + dollarCostLabel.isHidden = !viewModel.showDollarCostLabel + + activityIndicator.color = viewModel.activityIndicatorColor + + if viewModel.showActivityIndicator { + activityIndicator.startAnimating() + } else { + activityIndicator.stopAnimating() + } + + actionButton.setTitleColor(viewModel.buttonTitleColor, for: .normal) + actionButton.backgroundColor = viewModel.buttonBackgroundColor + actionButton.titleLabel?.font = viewModel.buttonFont + actionButton.setTitle(viewModel.actionButtonTitle, for: .normal) + + cancelButton.setTitleColor(viewModel.buttonTitleColor, for: .normal) + cancelButton.backgroundColor = viewModel.buttonBackgroundColor + cancelButton.titleLabel?.font = viewModel.buttonFont + cancelButton.setTitle(viewModel.cancelButtonTitle, for: .normal) + + actionButton.isHidden = !viewModel.showActionButton + buttonSeparator.isHidden = !viewModel.showActionButton + } + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + //We can't use height / 2 because for some unknown reason, dollarCostLabel still has a zero height here +// dollarCostLabel.layer.cornerRadius = dollarCostLabel.frame.size.height / 2 + dollarCostLabel.layer.cornerRadius = 18 + } + + @objc func actionTapped() { + delegate?.didPressImport(in: self) + } + + @objc func cancel() { + if let delegate = delegate { + delegate.didPressDone(in: self) + } else { + dismiss(animated: true) + } + } + + class PaddedLabel: UILabel { + override var intrinsicContentSize: CGSize { + let size = super.intrinsicContentSize + return CGSize(width: size.width + 30, height: size.height + 10) + } + } +} + diff --git a/Trust/Market/ViewModels/ImportTicketViewControllerViewModel.swift b/Trust/Market/ViewModels/ImportTicketViewControllerViewModel.swift new file mode 100644 index 000000000..89e8667fd --- /dev/null +++ b/Trust/Market/ViewModels/ImportTicketViewControllerViewModel.swift @@ -0,0 +1,264 @@ +// Copyright © 2018 Stormbird PTE. LTD. + +import UIKit + +struct ImportTicketViewControllerViewModel { + enum State { + case validating + case promptImport + case processing + case succeeded + case failed(errorMessage: String) + } + var state: State + var ticketHolder: TicketHolder? + var ethCost: String? + var dollarCost: String? + + var backgroundColor: UIColor { + return Colors.appBackground + } + + var contentsBackgroundColor: UIColor { + return Colors.appWhite + } + + var headerTitle: String { + return R.string.localizable.aClaimTicketTitle() + } + + var activityIndicatorColor: UIColor { + return Colors.appBackground + } + var showActivityIndicator: Bool { + switch state { + case .validating, .processing: + return true + case .promptImport, .succeeded, .failed: + return false + } + } + + var showTicketRow: Bool { + switch state { + case .validating: + return false + case .processing, .promptImport, .succeeded, .failed: + return true + } + } + + var ticketCount: String { + guard let ticketHolder = ticketHolder else { return "" } + if case let .validating = state { + return "" + } else { + return "x\(ticketHolder.tickets.count)" + } + } + + var title: String { + guard let ticketHolder = ticketHolder else { return "" } + if case let .validating = state { + return "" + } else { + return ticketHolder.name + } + } + + var seatRange: String { + guard let ticketHolder = ticketHolder else { return "" } + if case let .validating = state { + return "" + } else { + return ticketHolder.seatRange + } + } + + var zoneName: String { + guard let ticketHolder = ticketHolder else { return "" } + if case let .validating = state { + return "" + } else { + return ticketHolder.zone + } + } + + var venue: String { + guard let ticketHolder = ticketHolder else { return "" } + if case let .validating = state { + return "" + } else { + return ticketHolder.venue + } + } + + var date: String { + guard let ticketHolder = ticketHolder else { return "" } + if case let .validating = state { + return "" + } else { + //TODO Should format be localized? + return ticketHolder.date.format("dd MMM yyyy") + } + } + + var showTicketRowIcons: Bool { + if case let .validating = state { + return false + } else { + return true + } + } + + var statusText: String { + switch state { + case .validating: + return R.string.localizable.aClaimTicketValidatingTitle() + case .promptImport: + return R.string.localizable.aClaimTicketPromptImportTitle() + case .processing: + return R.string.localizable.aClaimTicketInProgressTitle() + case .succeeded: + return R.string.localizable.aClaimTicketSuccessTitle() + case .failed: + return R.string.localizable.aClaimTicketFailedTitle() + } + } + + var statusColor: UIColor { + return UIColor(red: 20, green: 20, blue: 20) + } + + var statusFont: UIFont { + return Fonts.semibold(size: 25)! + } + + var showCost: Bool { + return showTicketRow + } + + var ethCostLabelLabelText: String { + return R.string.localizable.aClaimTicketEthCostLabelTitle() + } + + var ethCostLabelLabelColor: UIColor { + return UIColor(red: 155, green: 155, blue: 155) + } + + var ethCostLabelLabelFont: UIFont { + return Fonts.semibold(size: 21)! + } + + var ethCostLabelText: String { + guard let ethCost = ethCost else { return R.string.localizable.aClaimTicketEthCostFreeTitle() } + if ethCost.isEmpty { + return R.string.localizable.aClaimTicketEthCostFreeTitle() + } else { + return "\(ethCost) ETH" + } + } + + var ethCostLabelColor: UIColor { + return Colors.appBackground + } + + var ethCostLabelFont: UIFont { + return Fonts.semibold(size: 21)! + } + + var dollarCostLabelLabelText: String { + return R.string.localizable.aClaimTicketDollarCostLabelTitle() + } + + var dollarCostLabelLabelColor: UIColor { + return UIColor(red: 155, green: 155, blue: 155) + } + + var dollarCostLabelLabelFont: UIFont { + return Fonts.regular(size: 10)! + } + + var dollarCostLabelText: String { + guard let dollarCost = dollarCost else { return "" } + if dollarCost.isEmpty { + return "" + } else { + return "$\(dollarCost)" + } + } + + var showDollarCostLabel: Bool { + return !transactionIsFree + } + + var dollarCostLabelBackgroundColor: UIColor { + return UIColor(red: 236, green: 236, blue: 236) + } + + var dollarCostLabelColor: UIColor { + return Colors.darkGray + } + + var dollarCostLabelFont: UIFont { + return Fonts.light(size: 21)! + } + + var buttonTitleColor: UIColor { + return Colors.appWhite + } + + var buttonBackgroundColor: UIColor { + return Colors.appHighlightGreen + } + + var buttonFont: UIFont { + return Fonts.regular(size: 20)! + } + + var showActionButton: Bool { + switch state { + case .promptImport: + return true + case .validating, .processing, .succeeded, .failed: + return false + } + } + + var actionButtonTitle: String { + switch state { + case .validating: + return "" + case .promptImport: + if transactionIsFree { + return R.string.localizable.aClaimTicketImportButtonTitle() + } else { + return R.string.localizable.aClaimTicketPurchaseButtonTitle() + } + case .processing: + return "" + case .succeeded: + return "" + case .failed: + return "" + } + } + + var cancelButtonTitle: String { + switch state { + case .validating, .promptImport, .processing: + return R.string.localizable.cancel() + case .succeeded, .failed: + return R.string.localizable.aClaimTicketDoneButtonTitle() + } + } + + var transactionIsFree: Bool { + guard let ethCost = ethCost else { return true } + return ethCost.isEmpty + } + + init(state: State) { + self.state = state + } +} From 004d7aa43576d91d98eb8e22c1d7a3e156d71ee1 Mon Sep 17 00:00:00 2001 From: James Sangalli Date: Mon, 16 Apr 2018 14:01:10 +0800 Subject: [PATCH 2/2] intergrated query handling --- Trust/Market/Coordinators/UniversalLinkCoordinator.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Trust/Market/Coordinators/UniversalLinkCoordinator.swift b/Trust/Market/Coordinators/UniversalLinkCoordinator.swift index ab798da43..f9d764cdd 100644 --- a/Trust/Market/Coordinators/UniversalLinkCoordinator.swift +++ b/Trust/Market/Coordinators/UniversalLinkCoordinator.swift @@ -64,6 +64,10 @@ class UniversalLinkCoordinator: Coordinator { //nil or "" implies free let ethCost = "0.00001" let dollarCost = "0.004" + if let vc = importTicketViewController { + vc.query = query + vc.parameters = parameters + } self.promptImportUniversalLink(ticketHolder: ticketHolder, ethCost: ethCost, dollarCost: dollarCost) } else { //TODO Pass in error message