Merge pull request #133 from James-Sangalli/add-import-ticket-from-universal-link-ui

Add UI for import ticket from universal link: prompt, success, failure
pull/139/head
James Sangalli 7 years ago committed by GitHub
commit 8c76de5802
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      Trust.xcodeproj/project.pbxproj
  2. 46
      Trust/AppDelegate.swift
  3. 5
      Trust/Localization/en.lproj/Localizable.strings
  4. 102
      Trust/Market/Coordinators/UniversalLinkCoordinator.swift
  5. 129
      Trust/Market/ViewControllers/TicketImportStatusViewController.swift
  6. 65
      Trust/Market/ViewModels/TicketImportStatusViewControllerViewModel.swift

@ -294,6 +294,7 @@
5E7C7248A9A732452BDC27D7 /* AdvancedSettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C78581AA28CA5C3CBC468 /* AdvancedSettingsViewController.swift */; };
5E7C72670E16AFB8DAF64673 /* OnboardingPageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7E24936CC2190D2A16C2 /* OnboardingPageViewModel.swift */; };
5E7C728CDF33FBDBA47F71A6 /* MarketplaceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C794F8EBAEE5E8F2821C2 /* MarketplaceViewController.swift */; };
5E7C72AF95DCE8BC65490BCA /* TicketImportStatusViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B82CC07F290B9CAA4E4 /* TicketImportStatusViewController.swift */; };
5E7C72B0A10A92E591696E48 /* ContactUsBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AE6FAE0DF969B4F52E9 /* ContactUsBannerView.swift */; };
5E7C72C8A15397C5A40BFE76 /* WhatIsEthereumInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C774BCA281E4B077DBBFA /* WhatIsEthereumInfoViewController.swift */; };
5E7C731B88842C036A74A039 /* AlphaWalletSettingsButtonRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C71EBD4C95AD4E11F3352 /* AlphaWalletSettingsButtonRow.swift */; };
@ -315,6 +316,7 @@
5E7C774B5332AC0DC19C5B1B /* EthTokenViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74B82783A94091A43470 /* EthTokenViewCellViewModel.swift */; };
5E7C7793AB6B577906F2BCA3 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AFE9AF9FE6B58C925D4 /* SettingsViewController.swift */; };
5E7C77E844D710D7AFBC58D4 /* RequestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74DCC21272EC231A20E2 /* RequestViewController.swift */; };
5E7C783B4784DE76971EEBB4 /* TicketImportStatusViewControllerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7BD9B4BDAFC2D9EBD741 /* TicketImportStatusViewControllerViewModel.swift */; };
5E7C78407F6DCB0EDD562DF6 /* TicketTokenViewCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C731B6F01534683227123 /* TicketTokenViewCellViewModel.swift */; };
5E7C78B3FD5CA87E395E1861 /* OnboardingPageStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7AF9A592D7224ED58016 /* OnboardingPageStyle.swift */; };
5E7C78D6C94739B1ADDFBB5B /* WhyUseEthereumInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74FABE14B7B1BEEC4F5E /* WhyUseEthereumInfoViewController.swift */; };
@ -336,6 +338,7 @@
5E7C7D71D3184F44C397FFE7 /* HelpViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C715F395B973FB61056CF /* HelpViewController.swift */; };
5E7C7D8173CB1089D622DA38 /* HelpViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7646352F10C96B5FC6F6 /* HelpViewCell.swift */; };
5E7C7D8AFC9BA1E8C1D05167 /* TicketSellInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C74A2C738BF2412D412A7 /* TicketSellInfoViewController.swift */; };
5E7C7E04D4DDD7D8881A2AB1 /* UniversalLinkCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C76AF81B8DFF605558499 /* UniversalLinkCoordinator.swift */; };
5E7C7E2C03099D78A9D911D7 /* WhatIsASeedPhraseInfoViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7B0BE9EE3B198AE7D92D /* WhatIsASeedPhraseInfoViewController.swift */; };
5E7C7E4B4054AAD41C5BE3EC /* SettingsAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C7564AF453BAB0BDAAA57 /* SettingsAction.swift */; };
5E7C7E5C30EFDC70DF1E00C1 /* TicketsViewControllerHeaderViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E7C77316522DF2B256F1F92 /* TicketsViewControllerHeaderViewModel.swift */; };
@ -787,6 +790,7 @@
5E7C7646352F10C96B5FC6F6 /* HelpViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HelpViewCell.swift; sourceTree = "<group>"; };
5E7C764B98F526271E4C2A6A /* StaticHTMLViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaticHTMLViewController.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>"; };
5E7C77061BEF269BCE358086 /* RedeemTicketTableViewCellViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RedeemTicketTableViewCellViewModel.swift; sourceTree = "<group>"; };
5E7C77316522DF2B256F1F92 /* TicketsViewControllerHeaderViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TicketsViewControllerHeaderViewModel.swift; sourceTree = "<group>"; };
5E7C7742709724B3BD0C2A0D /* TicketRowViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TicketRowViewModel.swift; sourceTree = "<group>"; };
@ -812,7 +816,9 @@
5E7C7B0BE9EE3B198AE7D92D /* WhatIsASeedPhraseInfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WhatIsASeedPhraseInfoViewController.swift; sourceTree = "<group>"; };
5E7C7B1FB2702A2A8A4EBD76 /* SettingsCoordinator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = "<group>"; };
5E7C7B3302309706CA0F972A /* TokensViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokensViewController.swift; sourceTree = "<group>"; };
5E7C7B82CC07F290B9CAA4E4 /* TicketImportStatusViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TicketImportStatusViewController.swift; sourceTree = "<group>"; };
5E7C7B9220E616F82EDA956F /* PasscodeCharacterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeCharacterView.swift; sourceTree = "<group>"; };
5E7C7BD9B4BDAFC2D9EBD741 /* TicketImportStatusViewControllerViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TicketImportStatusViewControllerViewModel.swift; sourceTree = "<group>"; };
5E7C7C077372C3F2A4349FA1 /* TokenViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TokenViewCell.swift; sourceTree = "<group>"; };
5E7C7C58586099F082973073 /* WalletFilterView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WalletFilterView.swift; sourceTree = "<group>"; };
5E7C7D2AAB777BF35B8B56BD /* AlphaWalletSettingPushRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AlphaWalletSettingPushRow.swift; path = Views/AlphaWalletSettingPushRow.swift; sourceTree = "<group>"; };
@ -2209,6 +2215,22 @@
path = ViewControllers;
sourceTree = "<group>";
};
5E7C714506B5E3AED3C410FC /* ViewControllers */ = {
isa = PBXGroup;
children = (
5E7C7B82CC07F290B9CAA4E4 /* TicketImportStatusViewController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
};
5E7C71D254F51DCFBA549722 /* Coordinators */ = {
isa = PBXGroup;
children = (
5E7C76AF81B8DFF605558499 /* UniversalLinkCoordinator.swift */,
);
path = Coordinators;
sourceTree = "<group>";
};
5E7C7254740FFAE67AE9ECE6 /* ViewControllers */ = {
isa = PBXGroup;
children = (
@ -2226,6 +2248,14 @@
path = Views;
sourceTree = "<group>";
};
5E7C755521B5CAF98176AB84 /* ViewModels */ = {
isa = PBXGroup;
children = (
5E7C7BD9B4BDAFC2D9EBD741 /* TicketImportStatusViewControllerViewModel.swift */,
);
path = ViewModels;
sourceTree = "<group>";
};
5E7C7ACB32FB112CD7D92977 /* AlphaWalletHelp */ = {
isa = PBXGroup;
children = (
@ -2343,6 +2373,9 @@
76F1DADFD07E2941897FD2E1 /* OrderHandler.swift */,
B1DC375C203AEAE100C9756D /* MarketQueueHandler.swift */,
76F1DCD54618349AC91C6DF8 /* UniversalLinkHandler.swift */,
5E7C71D254F51DCFBA549722 /* Coordinators */,
5E7C714506B5E3AED3C410FC /* ViewControllers */,
5E7C755521B5CAF98176AB84 /* ViewModels */,
);
path = Market;
sourceTree = "<group>";
@ -3389,6 +3422,9 @@
5E7C76A65C14D0F11AF7848F /* TicketRowViewModel.swift in Sources */,
5E7C71A7D2BD6FCE3980CC51 /* ImportWalletHelpBubbleViewViewModel.swift in Sources */,
5E7C7208A83399C27AE57E44 /* ImportWalletHelpBubbleView.swift in Sources */,
5E7C7E04D4DDD7D8881A2AB1 /* UniversalLinkCoordinator.swift in Sources */,
5E7C72AF95DCE8BC65490BCA /* TicketImportStatusViewController.swift in Sources */,
5E7C783B4784DE76971EEBB4 /* TicketImportStatusViewControllerViewModel.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

@ -4,7 +4,6 @@ import UIKit
import Lokalise
import Branch
import RealmSwift
import Alamofire
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDelegate {
@ -83,43 +82,18 @@ 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)
//TODO: if we handle other types of URLs, check if handled==false, then we pass the url to another handlers
if(url?.description.contains(UniversalLinkHandler().urlPrefix))!
{
let keystore = try! EtherKeystore()
let signedOrder = UniversalLinkHandler().parseURL(url: (url?.description)!)
let signature = signedOrder.signature.substring(from: 2)
// form the json string out of the order for the paymaster server
// James S. wrote
let indices = signedOrder.order.indices
var indicesStringEncoded = ""
for i in 0...indices.count - 1 {
indicesStringEncoded += String(indices[i]) + ","
}
//cut off last comma
indicesStringEncoded = indicesStringEncoded.substring(from: indicesStringEncoded.count - 1)
let parameters: Parameters = [
"address" : keystore.recentlyUsedWallet?.address.description,
"indices": indicesStringEncoded,
"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
Alamofire.request(
query,
method: .post,
parameters: parameters
).responseJSON {
result in
// TODO handle http response
print(result)
return true
}
}
return true
extension AppDelegate: UniversalLinkCoordinatorDelegate {
func viewControllerForPresenting(in coordinator: UniversalLinkCoordinator) -> UIViewController? {
return window?.rootViewController
}
}

@ -228,3 +228,8 @@
"a.welcome.onboarding.createwallet.button.title" = "GET STARTED";
"a.settings.advanced.label.title" = "Advanced";
"a.marketplace.tabbar.item.title" = "Marketplace";
"a.claim.ticket.success.title" = "Your ticket has been imported and should be available shortly";
"a.claim.ticket.failed.title" = "Your ticket was not imported";
"a.claim.ticket.inProgress.title" = "Importing ticket...";
"a.claim.ticket.done.button.title" = "Done";
"a.claim.ticket.import.button.title" = "Import";

@ -0,0 +1,102 @@
// Copyright © 2018 Stormbird PTE. LTD.
import Foundation
import Alamofire
protocol UniversalLinkCoordinatorDelegate: class {
func viewControllerForPresenting(in coordinator: UniversalLinkCoordinator) -> UIViewController?
}
class UniversalLinkCoordinator: Coordinator {
var coordinators: [Coordinator] = []
weak var delegate: UniversalLinkCoordinatorDelegate?
var statusViewController: TicketImportStatusViewController?
func start() {
}
//Returns true if handled
func handleUniversalLink(url: URL?) -> Bool {
let matchedPrefix = (url?.description.contains(UniversalLinkHandler().urlPrefix))!
guard matchedPrefix else {
return false
}
let keystore = try! EtherKeystore()
let signedOrder = UniversalLinkHandler().parseURL(url: (url?.description)!)
let signature = signedOrder.signature.substring(from: 2)
// form the json string out of the order for the paymaster server
// James S. wrote
let indices = signedOrder.order.indices
var indicesStringEncoded = ""
for i in 0...indices.count - 1 {
indicesStringEncoded += String(indices[i]) + ","
}
//cut off last comma
indicesStringEncoded = indicesStringEncoded.substring(from: indicesStringEncoded.count - 1)
let parameters: Parameters = [
"address": keystore.recentlyUsedWallet?.address.description,
"indices": indicesStringEncoded,
"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
//TODO check if URL is valid or not. Price?
let validURL = true
if validURL {
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)
}
}
}
} else {
return true
}
return true
}
private func importUniversalLink(query: String, parameters: Parameters) {
if let viewController = delegate?.viewControllerForPresenting(in: self) {
statusViewController = TicketImportStatusViewController()
if let vc = statusViewController {
vc.delegate = self
vc.configure(viewModel: .init(state: .processing))
vc.modalPresentationStyle = .overCurrentContext
viewController.present(vc, animated: true)
}
}
Alamofire.request(
query,
method: .post,
parameters: parameters
).responseJSON {
result in
// TODO handle http response
print(result)
//TODO handle successful or not. Pass an error (message?) to the view model if we have one
let successful = true
if let vc = self.statusViewController {
if successful {
vc.configure(viewModel: .init(state: .succeeded))
} else {
vc.configure(viewModel: .init(state: .failed))
}
}
}
}
}
extension UniversalLinkCoordinator: TicketImportStatusViewControllerDelegate {
func didPressDone(in viewController: TicketImportStatusViewController) {
viewController.dismiss(animated: true)
}
}

@ -0,0 +1,129 @@
// Copyright © 2018 Stormbird PTE. LTD.
import Foundation
import UIKit
protocol TicketImportStatusViewControllerDelegate: class {
func didPressDone(in viewController: TicketImportStatusViewController)
}
class TicketImportStatusViewController: UIViewController {
weak var delegate: TicketImportStatusViewControllerDelegate?
let background = UIView()
let imageView = UIImageView()
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .whiteLarge)
let titleLabel = UILabel()
let actionButton = UIButton()
init() {
super.init(nibName: nil, bundle: nil)
view.backgroundColor = .clear
let visualEffectView = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
visualEffectView.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(visualEffectView, at: 0)
let imageHolder = UIView()
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
activityIndicator.hidesWhenStopped = true
imageHolder.addSubview(activityIndicator)
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.isHidden = true
imageHolder.addSubview(imageView)
view.addSubview(background)
background.translatesAutoresizingMaskIntoConstraints = false
actionButton.addTarget(self, action: #selector(done), for: .touchUpInside)
let stackView = UIStackView(arrangedSubviews: [
.spacer(height: 20),
imageHolder,
.spacer(height: 20),
titleLabel,
.spacer(height: 20),
actionButton,
.spacer(height: 18)
])
stackView.translatesAutoresizingMaskIntoConstraints = false
stackView.axis = .vertical
stackView.spacing = 0
stackView.distribution = .fill
background.addSubview(stackView)
NSLayoutConstraint.activate([
visualEffectView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
visualEffectView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
visualEffectView.topAnchor.constraint(equalTo: view.topAnchor),
visualEffectView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
imageView.widthAnchor.constraint(equalToConstant: 70),
imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor),
imageView.centerXAnchor.constraint(equalTo: imageHolder.centerXAnchor),
imageView.centerYAnchor.constraint(equalTo: imageHolder.centerYAnchor),
activityIndicator.centerXAnchor.constraint(equalTo: imageHolder.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: imageHolder.centerYAnchor),
imageHolder.heightAnchor.constraint(equalTo: imageView.heightAnchor),
actionButton.heightAnchor.constraint(equalToConstant: 47),
stackView.leadingAnchor.constraint(equalTo: background.leadingAnchor, constant: 40),
stackView.trailingAnchor.constraint(equalTo: background.trailingAnchor, constant: -40),
stackView.topAnchor.constraint(equalTo: background.topAnchor, constant: 16),
stackView.bottomAnchor.constraint(equalTo: background.bottomAnchor, constant: -16),
background.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 42),
background.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -42),
background.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func configure(viewModel: TicketImportStatusViewControllerViewModel) {
background.backgroundColor = viewModel.contentsBackgroundColor
background.layer.cornerRadius = 20
activityIndicator.color = viewModel.activityIndicatorColor
if viewModel.showActivityIndicator {
activityIndicator.startAnimating()
} else {
activityIndicator.stopAnimating()
}
imageView.isHidden = viewModel.showActivityIndicator
imageView.image = viewModel.image
titleLabel.numberOfLines = 0
titleLabel.textColor = viewModel.titleColor
titleLabel.font = viewModel.titleFont
titleLabel.textAlignment = .center
titleLabel.text = viewModel.titleLabelText
actionButton.setTitleColor(viewModel.actionButtonTitleColor, for: .normal)
actionButton.setBackgroundColor(viewModel.actionButtonBackgroundColor, forState: .normal)
actionButton.titleLabel?.font = viewModel.actionButtonTitleFont
actionButton.setTitle(viewModel.actionButtonTitle, for: .normal)
actionButton.layer.masksToBounds = true
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
actionButton.layer.cornerRadius = actionButton.frame.size.height / 2
}
@objc func done() {
if let delegate = delegate {
delegate.didPressDone(in: self)
} else {
dismiss(animated: true)
}
}
}

@ -0,0 +1,65 @@
// Copyright © 2018 Stormbird PTE. LTD.
import UIKit
struct TicketImportStatusViewControllerViewModel {
enum State {
case processing
case succeeded
case failed
}
let state: State
var contentsBackgroundColor: UIColor {
return Colors.appWhite
}
var image: UIImage? {
switch state {
case .processing:
return nil
case .succeeded:
return R.image.onboarding_complete()
case .failed:
//TODO return a failed version
return R.image.onboarding_complete()
}
}
var titleColor: UIColor {
return Colors.appText
}
var titleFont: UIFont {
return Fonts.light(size: 25)!
}
var activityIndicatorColor: UIColor {
return Colors.appBackground
}
var actionButtonTitleColor: UIColor {
return Colors.appWhite
}
var actionButtonBackgroundColor: UIColor {
return Colors.appBackground
}
var actionButtonTitleFont: UIFont {
return Fonts.regular(size: 20)!
}
var titleLabelText: String {
switch state {
case .processing:
return R.string.localizable.aClaimTicketInProgressTitle()
case .succeeded:
return R.string.localizable.aClaimTicketSuccessTitle()
case .failed:
return R.string.localizable.aClaimTicketFailedTitle()
}
}
var actionButtonTitle: String {
return R.string.localizable.aClaimTicketDoneButtonTitle()
}
var showActivityIndicator: Bool {
return state == .processing
}
init(state: State) {
self.state = state
}
}
Loading…
Cancel
Save