Commit a01594f0 authored by Jose Blaya's avatar Jose Blaya
Browse files

PIAX. Confirm VPN Plan

parent 6bf49655
......@@ -262,6 +262,7 @@
DD31498F21834B3F008E26E8 /* GetStartedViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD31498E21834B3F008E26E8 /* GetStartedViewController.swift */; };
DD314990218350D1008E26E8 /* SwiftGen+ScenesStoryboards.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC812472176166500CB290C /* SwiftGen+ScenesStoryboards.swift */; };
DD314991218350D1008E26E8 /* SwiftGen+SeguesStoryboards.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDC81249217617F900CB290C /* SwiftGen+SeguesStoryboards.swift */; };
DD8BF3CB219C6BAA0041357C /* ConfirmVPNPlanViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD8BF3CA219C6BAA0041357C /* ConfirmVPNPlanViewController.swift */; };
DDD824E32189969400151709 /* Preset.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD824E22189969400151709 /* Preset.swift */; };
DDD824E5218996CD00151709 /* Pages.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD824E4218996CD00151709 /* Pages.swift */; };
DDD824E72189C0E800151709 /* BrandableNavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDD824E62189C0E800151709 /* BrandableNavigationBar.swift */; };
......@@ -519,6 +520,7 @@
DA1A1A4FDD6B854C1227A5F0 /* Pods_PIALibrary_PIALibraryHost_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_PIALibrary_PIALibraryHost_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; };
DD0AC78F218715B8009B576B /* PIAButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PIAButton.swift; sourceTree = "<group>"; };
DD31498E21834B3F008E26E8 /* GetStartedViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetStartedViewController.swift; sourceTree = "<group>"; };
DD8BF3CA219C6BAA0041357C /* ConfirmVPNPlanViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfirmVPNPlanViewController.swift; sourceTree = "<group>"; };
DDC812472176166500CB290C /* SwiftGen+ScenesStoryboards.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftGen+ScenesStoryboards.swift"; sourceTree = "<group>"; };
DDC81249217617F900CB290C /* SwiftGen+SeguesStoryboards.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SwiftGen+SeguesStoryboards.swift"; sourceTree = "<group>"; };
DDD824E22189969400151709 /* Preset.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preset.swift; sourceTree = "<group>"; };
......@@ -1059,6 +1061,7 @@
0EB8C05A1F9CD38A005857E4 /* SignupSuccessViewController.swift */,
0EB8C05B1F9CD38A005857E4 /* WelcomePageViewController.swift */,
DD31498E21834B3F008E26E8 /* GetStartedViewController.swift */,
DD8BF3CA219C6BAA0041357C /* ConfirmVPNPlanViewController.swift */,
);
path = ViewControllers;
sourceTree = "<group>";
......@@ -1681,6 +1684,7 @@
0EB8C0641F9CD38B005857E4 /* ActivityButton.swift in Sources */,
0ED2B5131F82444E00C9DB2B /* Client+Preferences.swift in Sources */,
0EE78AF61F81880E002E4CDD /* Credentials.swift in Sources */,
DD8BF3CB219C6BAA0041357C /* ConfirmVPNPlanViewController.swift in Sources */,
0E53A83E1FE5A4C8000C2A18 /* Client+Daemons.swift in Sources */,
0E4D4EA01FA4CA7A007DA6DA /* Validator.swift in Sources */,
0E0E5B111F8297D200022CD0 /* UserDefaultsStore.swift in Sources */,
......
......@@ -23,14 +23,16 @@
"purchase.title" = "Select a VPN plan";
"purchase.subtitle" = "7-day money back guarantee";
"purchase.email.placeholder" = "Email address";
"purchase.submit" = "Continue";
"purchase.continue" = "Continue";
"purchase.login.footer" = "Already have an account?";
"purchase.login.button" = "Sign in";
"purchase.error.title" = "Purchase";
"purchase.error.validation" = "You must enter an email address.";
"purchase.error.connectivity.title" = "Connection Failure";
"purchase.error.connectivity.description" = "We are unable to reach Private Internet Access. This may due to poor internet or our service is blocked in your country.";
"purchase.confirm.form.email" = "Enter your email address";
"purchase.confirm.plan" = "Your are purchasing the %@ plan";
"purchase.submit" = "Submit";
"redeem.title" = "Redeem gift card";
"redeem.subtitle" = "Type in your email address and the %lu digit PIN from your gift card or trial card below.";
......
......@@ -25,6 +25,8 @@ internal enum Asset {
internal static let iconWarning = ImageAsset(name: "icon-warning")
internal static let logoDark = ImageAsset(name: "logo-dark")
internal static let logoLight = ImageAsset(name: "logo-light")
internal static let planSelected = ImageAsset(name: "plan-selected")
internal static let planUnselected = ImageAsset(name: "plan-unselected")
internal static let closeIcon = ImageAsset(name: "close-icon")
internal static let imageAccountFailed = ImageAsset(name: "image-account-failed")
internal static let imageNoInternet = ImageAsset(name: "image-no-internet")
......
......@@ -20,6 +20,7 @@ internal enum StoryboardSegue {
case unwindInternetUnreachableSegueIdentifier = "UnwindInternetUnreachableSegueIdentifier"
}
internal enum Welcome: String, SegueType {
case confirmPurchaseVPNPlanSegue = "ConfirmPurchaseVPNPlanSegue"
case purchaseVPNPlanSegue = "PurchaseVPNPlanSegue"
case redeemGiftCardSegue = "RedeemGiftCardSegue"
case signupQRCameraScannerSegue = "SignupQRCameraScannerSegue"
......
......@@ -204,11 +204,23 @@ internal enum L10n {
}
internal enum Purchase {
/// Continue
internal static let `continue` = L10n.tr("Welcome", "purchase.continue")
/// Submit
internal static let submit = L10n.tr("Welcome", "purchase.submit")
/// 7-day money back guarantee
internal static let subtitle = L10n.tr("Welcome", "purchase.subtitle")
/// Select a VPN plan
internal static let title = L10n.tr("Welcome", "purchase.title")
internal enum Confirm {
/// Your are purchasing the %@ plan
internal static func plan(_ p1: String) -> String {
return L10n.tr("Welcome", "purchase.confirm.plan", p1)
}
internal enum Form {
/// Enter your email address
internal static let email = L10n.tr("Welcome", "purchase.confirm.form.email")
}
}
internal enum Email {
/// Email address
internal static let placeholder = L10n.tr("Welcome", "purchase.email.placeholder")
......
......@@ -17,6 +17,13 @@ public protocol ModalController: class {
func dismissModal()
}
/// Enum used to determinate the status of the view controller and apply effects over the UI elements
public enum ViewControllerStatus {
case initial
case restore(element: UIView)
case error(element: UIView)
}
/// Base view controller with dynamic constraints and restyling support.
///
/// - Seealso: `Theme`
......@@ -32,6 +39,12 @@ open class AutolayoutViewController: UIViewController, ModalController, Restylab
return Theme.current.statusBarAppearance(for: self)
}
/// The initial status of the view controller. Every time the var changes the value, we reload the UI of the form element given as parameter.
/// Example of use: self.status = .error(element: textEmail)
open var status: ViewControllerStatus = .initial {
didSet { reloadFormElements() }
}
deinit {
NotificationCenter.default.removeObserver(self)
}
......@@ -94,4 +107,28 @@ open class AutolayoutViewController: UIViewController, ModalController, Restylab
}
setNeedsStatusBarAppearanceUpdate()
}
private func reloadFormElements() {
switch status {
case .initial:
break
case .restore(let element):
restoreFormElementBorder(element)
case .error(let element):
updateFormElementBorder(element)
}
}
private func restoreFormElementBorder(_ element: UIView) {
if let element = element as? UITextField {
Theme.current.applyInput(element)
}
}
private func updateFormElementBorder(_ element: UIView) {
if let element = element as? UITextField {
Theme.current.applyInputError(element)
}
}
}
//
// ConfirmVPNPlanViewController.swift
// PIALibrary-iOS
//
// Created by Jose Antonio Blaya Garcia on 14/11/2018.
// Copyright © 2018 London Trust Media. All rights reserved.
//
import UIKit
import SwiftyBeaver
private let log = SwiftyBeaver.self
public class ConfirmVPNPlanViewController: AutolayoutViewController, BrandableNavigationBar, WelcomeChild {
@IBOutlet private weak var buttonConfirm: PIAButton!
@IBOutlet private weak var textEmail: BorderedTextField!
@IBOutlet private weak var textAgreement: UITextView!
@IBOutlet private weak var labelTitle: UILabel!
@IBOutlet private weak var labelSubtitle: UILabel!
private var signupEmail: String?
private var signupTransaction: InAppTransaction?
weak var completionDelegate: WelcomeCompletionDelegate?
var omitsSiblingLink = false
var preset: Preset?
private var allPlans: [PurchasePlan] = [.dummy, .dummy]
private var selectedPlanIndex: Int?
deinit {
NotificationCenter.default.removeObserver(self)
}
override public func viewDidLoad() {
super.viewDidLoad()
guard let preset = self.preset else {
fatalError("Preset not propagated")
}
guard let planIndex = selectedPlanIndex else {
return
}
self.navigationItem.leftBarButtonItem = UIBarButtonItem(
image: Theme.current.palette.navigationBarBackIcon?.withRenderingMode(.alwaysOriginal),
style: .plain,
target: self,
action: #selector(back(_:))
)
self.navigationItem.leftBarButtonItem?.accessibilityLabel = L10n.Welcome.Redeem.Accessibility.back
labelTitle.text = L10n.Welcome.Purchase.Confirm.Form.email
let plan = allPlans[planIndex]
labelSubtitle.text = L10n.Welcome.Purchase.Confirm.plan(plan.title.lowercased())
textAgreement.attributedText = Theme.current.agreementText(
withMessage: L10n.Welcome.Agreement.message,
tos: L10n.Welcome.Agreement.Message.tos,
tosUrl: Client.configuration.tosUrl,
privacy: L10n.Welcome.Agreement.Message.privacy,
privacyUrl: Client.configuration.privacyUrl
)
textEmail.placeholder = L10n.Welcome.Purchase.Email.placeholder
textEmail.text = preset.purchaseEmail
self.styleConfirmButton()
}
/// Populate the view with the values from PurchaseViewController
/// - Parameters:
/// - plans: The available plans.
/// - selectedIndex: The selected plan from the previous screen.
func populateViewWith(plans: [PurchasePlan], andSelectedPlanIndex selectedIndex: Int) {
self.allPlans = plans
self.selectedPlanIndex = selectedIndex
}
@IBAction private func signUp(_ sender: Any?) {
guard let planIndex = selectedPlanIndex else {
return
}
guard let email = textEmail.text?.trimmed(), Validator.validate(email: email) else {
signupEmail = nil
Macros.displayImageNote(withImage: Asset.iconWarning.image,
message: L10n.Welcome.Purchase.Error.validation)
self.status = .error(element: textEmail)
return
}
self.status = .restore(element: textEmail)
let plan = allPlans[planIndex]
guard !plan.isDummy else {
Macros.displayImageNote(withImage: Asset.iconWarning.image,
message: L10n.Welcome.Iap.Error.Message.unavailable)
return
}
disableInteractions()
preset?.accountProvider.isAPIEndpointAvailable({ [weak self] (isAvailable, error) in
self?.enableInteractions()
guard let isAvailable = isAvailable,
isAvailable else {
Macros.displayImageNote(withImage: Asset.iconWarning.image,
message: L10n.Welcome.Purchase.Error.Connectivity.description)
return
}
self?.startPurchaseProcessWithEmail(email,
andPlan: plan)
})
}
@objc private func back(_ sender: Any?) {
self.navigationController?.popViewController(animated: true)
}
private func disableInteractions() {
parent?.view.isUserInteractionEnabled = false
}
private func enableInteractions() {
parent?.view.isUserInteractionEnabled = true
}
private func startPurchaseProcessWithEmail(_ email: String,
andPlan plan: PurchasePlan) {
//textEmail.text = email
log.debug("Will purchase plan: \(plan.product)")
disableInteractions()
preset?.accountProvider.purchase(plan: plan.plan) { (transaction, error) in
self.enableInteractions()
guard let transaction = transaction else {
if let error = error {
log.error("Purchase failed (error: \(error))")
Macros.displayImageNote(withImage: Asset.iconWarning.image,
message: error.localizedDescription)
} else {
log.warning("Cancelled purchase")
}
return
}
log.debug("Purchased with transaction: \(transaction)")
self.signupEmail = email
self.signupTransaction = transaction
self.perform(segue: StoryboardSegue.Welcome.signupViaPurchaseSegue)
}
}
override public func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == StoryboardSegue.Welcome.signupViaPurchaseSegue.rawValue) {
let nav = segue.destination as! UINavigationController
let vc = nav.topViewController as! SignupInProgressViewController
guard let email = signupEmail else {
fatalError("Signing up and signupEmail is not set")
}
var metadata = SignupMetadata(email: email)
metadata.title = L10n.Signup.InProgress.title
metadata.bodySubtitle = L10n.Signup.InProgress.message
vc.metadata = metadata
vc.signupRequest = SignupRequest(email: email, transaction: signupTransaction)
vc.preset = preset
vc.completionDelegate = completionDelegate
}
}
// MARK: Restylable
override public func viewShouldRestyle() {
super.viewShouldRestyle()
navigationItem.titleView = NavigationLogoView()
Theme.current.applyNavigationBarStyle(to: self)
Theme.current.applyLightBackground(view)
Theme.current.applyInput(textEmail)
Theme.current.applyTitle(labelTitle, appearance: .dark)
Theme.current.applySubtitle(labelSubtitle)
Theme.current.applyLinkAttributes(textAgreement)
}
private func styleConfirmButton() {
buttonConfirm.setRounded()
buttonConfirm.style(style: TextStyle.Buttons.piaGreenButton)
buttonConfirm.setTitle(L10n.Welcome.Purchase.submit.uppercased(),
for: [])
}
}
......@@ -12,6 +12,7 @@ import SwiftyBeaver
private let log = SwiftyBeaver.self
class PurchaseViewController: AutolayoutViewController, WelcomeChild {
private struct Cells {
static let plan = "PlanCell"
}
......@@ -20,8 +21,6 @@ class PurchaseViewController: AutolayoutViewController, WelcomeChild {
@IBOutlet private weak var labelTitle: UILabel!
@IBOutlet private weak var labelSubtitle: UILabel!
//@IBOutlet private weak var textEmail: BorderedTextField!
@IBOutlet private weak var collectionPlans: UICollectionView!
......@@ -30,18 +29,12 @@ class PurchaseViewController: AutolayoutViewController, WelcomeChild {
@IBOutlet private weak var buttonPurchase: PIAButton!
var preset: Preset?
var omitsSiblingLink = false
weak var completionDelegate: WelcomeCompletionDelegate?
var omitsSiblingLink = false
private var allPlans: [PurchasePlan] = [.dummy, .dummy]
private var selectedPlanIndex: Int?
private var signupEmail: String?
private var signupTransaction: InAppTransaction?
deinit {
NotificationCenter.default.removeObserver(self)
......@@ -50,7 +43,7 @@ class PurchaseViewController: AutolayoutViewController, WelcomeChild {
override func viewDidLoad() {
super.viewDidLoad()
guard let preset = self.preset else {
guard let _ = self.preset else {
fatalError("Preset not propagated")
}
......@@ -58,7 +51,6 @@ class PurchaseViewController: AutolayoutViewController, WelcomeChild {
labelTitle.text = L10n.Welcome.Purchase.title
labelSubtitle.text = L10n.Welcome.Purchase.subtitle
//textEmail.placeholder = L10n.Welcome.Purchase.Email.placeholder
textAgreement.attributedText = Theme.current.agreementText(
withMessage: L10n.Welcome.Agreement.message,
tos: L10n.Welcome.Agreement.Message.tos,
......@@ -66,7 +58,6 @@ class PurchaseViewController: AutolayoutViewController, WelcomeChild {
privacy: L10n.Welcome.Agreement.Message.privacy,
privacyUrl: Client.configuration.privacyUrl
)
//textEmail.text = preset.purchaseEmail
let nc = NotificationCenter.default
nc.addObserver(self, selector: #selector(productsDidFetch(notification:)), name: .__InAppDidFetchProducts, object: nil)
......@@ -94,6 +85,17 @@ class PurchaseViewController: AutolayoutViewController, WelcomeChild {
scrollView.isScrollEnabled = (traitCollection.verticalSizeClass == .compact)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == StoryboardSegue.Welcome.confirmPurchaseVPNPlanSegue.rawValue) {
if let vc = segue.destination as? ConfirmVPNPlanViewController,
let selectedIndex = selectedPlanIndex {
vc.preset = preset
vc.populateViewWith(plans: allPlans,
andSelectedPlanIndex: selectedIndex)
}
}
}
// MARK: Actions
private func refreshPlans(_ plans: [Plan: InAppProduct]) {
......@@ -131,104 +133,6 @@ class PurchaseViewController: AutolayoutViewController, WelcomeChild {
collectionPlans.selectItem(at: IndexPath(row: selectedPlanIndex!, section: 0), animated: false, scrollPosition: [])
}
@IBAction private func signUp(_ sender: Any?) {
guard let planIndex = selectedPlanIndex else {
return
}
let errorTitle = L10n.Welcome.Purchase.Error.title
let errorMessage = L10n.Welcome.Purchase.Error.validation
/*
guard let email = textEmail.text?.trimmed(), Validator.validate(email: email) else {
signupEmail = nil
let alert = Macros.alert(errorTitle, errorMessage)
alert.addCancelAction(L10n.Ui.Global.ok)
present(alert, animated: true, completion: nil)
return
}
*/
let plan = allPlans[planIndex]
guard !plan.isDummy else {
let alert = Macros.alert(
L10n.Welcome.Iap.Error.title,
L10n.Welcome.Iap.Error.Message.unavailable
)
alert.addCancelAction(L10n.Ui.Global.close)
present(alert, animated: true, completion: nil)
return
}
preset?.accountProvider.isAPIEndpointAvailable({ [weak self] (isAvailable, error) in
guard let isAvailable = isAvailable,
isAvailable else {
let alert = Macros.alert(
L10n.Welcome.Purchase.Error.Connectivity.title,
L10n.Welcome.Purchase.Error.Connectivity.description
)
alert.addCancelAction(L10n.Ui.Global.close)
self?.present(alert, animated: true, completion: nil)
return
}
//self?.startPurchaseProcessWithEmail(email,
// andPlan: plan)
})
}
private func startPurchaseProcessWithEmail(_ email: String,
andPlan plan: PurchasePlan) {
//textEmail.text = email
log.debug("Will purchase plan: \(plan.product)")
disableInteractions(fully: true)
preset?.accountProvider.purchase(plan: plan.plan) { (transaction, error) in
self.enableInteractions()
guard let transaction = transaction else {
if let error = error {
log.error("Purchase failed (error: \(error))")
let alert = Macros.alert(
L10n.Welcome.Iap.Error.title,
error.localizedDescription
)
alert.addCancelAction(L10n.Ui.Global.close)
self.present(alert, animated: true, completion: nil)
} else {
log.warning("Cancelled purchase")
}
return
}
log.debug("Purchased with transaction: \(transaction)")
self.signupEmail = email
self.signupTransaction = transaction
self.perform(segue: StoryboardSegue.Welcome.signupViaPurchaseSegue)
}
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if (segue.identifier == StoryboardSegue.Welcome.signupViaPurchaseSegue.rawValue) {
let nav = segue.destination as! UINavigationController
let vc = nav.topViewController as! SignupInProgressViewController
guard let email = signupEmail else {
fatalError("Signing up and signupEmail is not set")
}
var metadata = SignupMetadata(email: email)
metadata.title = L10n.Signup.InProgress.title
metadata.bodySubtitle = L10n.Signup.InProgress.message
vc.metadata = metadata
vc.signupRequest = SignupRequest(email: email, transaction: signupTransaction)
vc.preset = preset
vc.completionDelegate = completionDelegate
}
}
private func disableInteractions(fully: Bool) {
collectionPlans.isUserInteractionEnabled = false
if fully {
......@@ -245,7 +149,6 @@ class PurchaseViewController: AutolayoutViewController, WelcomeChild {
@objc private func productsDidFetch(notification: Notification) {
let products: [Plan: InAppProduct] = notification.userInfo(for: .products)
refreshPlans(products)
enableInteractions()
}
......@@ -259,14 +162,13 @@ class PurchaseViewController: AutolayoutViewController, WelcomeChild {
Theme.current.applyLightBackground(collectionPlans)
Theme.current.applyTitle(labelTitle, appearance: .dark)
Theme.current.applySubtitle(labelSubtitle)
//Theme.current.applyInput(textEmail)
Theme.current.applyLinkAttributes(textAgreement)
}
private func stylePurchaseButton() {
buttonPurchase.setRounded()
buttonPurchase.style(style: TextStyle.Buttons.piaGreenButton)
buttonPurchase.setTitle(L10n.Welcome.Purchase.submit.uppercased(),
buttonPurchase.setTitle(L10n.Welcome.Purchase.continue.uppercased(),
for: [])
}
......
......@@ -18,12 +18,6 @@ protocol RedeemScannerDelegate: class {
func errorFound()
}
enum RedeemViewStatus {
case initial
case restore(element: UIView)
case error(element: UIView)
}
class RedeemViewController: AutolayoutViewController, WelcomeChild {
private static let codeInvalidSet = CharacterSet.decimalDigits.inverted
......@@ -73,10 +67,6 @@ class RedeemViewController: AutolayoutViewController, WelcomeChild {
}
}