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

New login receipt button

Retrieve username from /account response
Purchase without email
WIP
parent 9a3ae603
This diff is collapsed.
......@@ -66,6 +66,8 @@ protocol PlainStore: class {
var isPersistentConnection: Bool? { get set }
var gdprTermsAccepted: Bool? { get set }
var shouldConnectForAllNetworks: Bool? { get set }
var useWiFiProtection: Bool? { get set }
......
......@@ -27,6 +27,9 @@ public struct AccountInfo {
/// The linked email address if any.
public internal(set) var email: String?
/// PIA username
public let username: String
/// The currently subscribed `Plan`.
public let plan: Plan
......
......@@ -167,6 +167,8 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
return
}
self.updateToken(token)
self.webServices.info(token: token) { (accountInfo, error) in
guard let accountInfo = accountInfo else {
callback?(nil, error)
......@@ -174,7 +176,7 @@ class DefaultAccountProvider: AccountProvider, ConfigurationAccess, DatabaseAcce
}
self.updateDatabaseWith(token,
andUsername: "PIA")
andUsername: accountInfo.username)
//Save after confirm the login was successful.
self.accessedDatabase.plain.accountInfo = accountInfo
......
......@@ -30,6 +30,8 @@ private protocol PreferencesStore: class {
var isPersistentConnection: Bool { get set }
var gdprTermsAccepted: Bool { get set }
var mace: Bool { get set }
var useWiFiProtection: Bool { get set }
......@@ -69,6 +71,7 @@ private extension PreferencesStore {
func load(from source: PreferencesStore) {
preferredServer = source.preferredServer
gdprTermsAccepted = source.gdprTermsAccepted
isPersistentConnection = source.isPersistentConnection
mace = source.mace
useWiFiProtection = source.useWiFiProtection
......@@ -118,6 +121,16 @@ extension Client {
}
}
/// GDPR treatment data agreement.
public fileprivate(set) var gdprTermsAccepted: Bool {
get {
return accessedDatabase.plain.gdprTermsAccepted ?? defaults.gdprTermsAccepted
}
set {
accessedDatabase.plain.gdprTermsAccepted = newValue
}
}
/// Enables automatic VPN reconnection.
public fileprivate(set) var isPersistentConnection: Bool {
get {
......@@ -317,6 +330,7 @@ extension Client.Preferences {
fileprivate init() {
preferredServer = nil
isPersistentConnection = true
gdprTermsAccepted = false
mace = false
useWiFiProtection = true
trustCellularData = false
......@@ -361,6 +375,9 @@ extension Client.Preferences {
/// :nodoc:
public var isPersistentConnection: Bool
/// :nodoc:
public var gdprTermsAccepted: Bool
/// :nodoc:
public var mace: Bool
......
......@@ -54,6 +54,8 @@ class UserDefaultsStore: PlainStore, ConfigurationAccess {
static let persistentConnection = "PersistentConnection" // legacy
static let gdprTermsAccepted = "GDPRTermsAccepted"
static let mace = "MACE" // legacy
static let visibleTiles = "VisibleTiles"
......@@ -356,6 +358,18 @@ class UserDefaultsStore: PlainStore, ConfigurationAccess {
}
}
var gdprTermsAccepted: Bool? {
get {
guard let value = backend.object(forKey: Entries.gdprTermsAccepted) as? Bool else {
return nil
}
return value
}
set {
backend.set(newValue, forKey: Entries.gdprTermsAccepted)
}
}
var useWiFiProtection: Bool? {
get {
guard let value = backend.object(forKey: Entries.useWiFiProtection) as? Bool else {
......
......@@ -28,6 +28,7 @@ class GlossAccountInfo: GlossParser {
required init?(json: JSON) {
let email: String? = "email" <~~ json
let username: String = "username" <~~ json ?? ""
let productId: String? = "product_id" <~~ json
let plan: Plan = "plan" <~~ json ?? .other
let canInvite: Bool = "can_invite" <~~ json ?? false
......@@ -52,6 +53,7 @@ class GlossAccountInfo: GlossParser {
parsed = AccountInfo(
email: email,
username: username,
plan: plan,
productId: productId,
isRenewable: isRenewable,
......@@ -69,6 +71,7 @@ extension AccountInfo: JSONEncodable {
public func toJSON() -> JSON? {
return jsonify([
"email" ~~> email,
"username" ~~> username,
"product_id" ~~> productId,
"plan" ~~> plan.rawValue,
"renewable" ~~> isRenewable,
......
......@@ -25,12 +25,16 @@ import Gloss
extension Signup: JSONEncodable {
func toJSON() -> JSON? {
return jsonify([
var json = jsonify([
"email" ~~> email,
"receipt" ~~> receipt.base64EncodedString(),
"marketing" ~~> marketing,
"debug" ~~> debug,
"store" ~~> "apple_app_store"
])
if email == "" {
json?.removeValue(forKey: "email")
}
return json
}
}
......@@ -108,6 +108,7 @@ public class MockAccountProvider: AccountProvider, WebServicesConsumer {
webServices.accountInfo = {
return AccountInfo(
email: self.mockEmail,
username: "p0000000",
plan: self.mockPlan,
productId: self.mockProductId,
isRenewable: self.mockIsRenewable,
......
......@@ -37,7 +37,6 @@ public class ConfirmVPNPlanViewController: AutolayoutViewController, BrandableNa
private var signupTransaction: InAppTransaction?
weak var completionDelegate: WelcomeCompletionDelegate?
var omitsSiblingLink = false
var termsAndConditionsAgreed = false
var preset: Preset?
private var allPlans: [PurchasePlan] = [.dummy, .dummy]
......@@ -110,13 +109,6 @@ public class ConfirmVPNPlanViewController: AutolayoutViewController, BrandableNa
return
}
guard termsAndConditionsAgreed else {
//present term and conditions
self.performSegue(withIdentifier: StoryboardSegue.Welcome.presentGDPRTermsSegue.rawValue,
sender: nil)
return
}
self.status = .restore(element: textEmail)
let plan = allPlans[planIndex]
......@@ -180,7 +172,7 @@ public class ConfirmVPNPlanViewController: AutolayoutViewController, BrandableNa
guard let transaction = transaction else {
if let error = error {
var message = error.localizedDescription
let message = error.localizedDescription
log.error("Purchase failed (error: \(error))")
Macros.displayImageNote(withImage: Asset.iconWarning.image,
message: message)
......@@ -214,11 +206,6 @@ public class ConfirmVPNPlanViewController: AutolayoutViewController, BrandableNa
vc.signupRequest = SignupRequest(email: email, transaction: signupTransaction)
vc.preset = preset
vc.completionDelegate = completionDelegate
} else if (segue.identifier == StoryboardSegue.Welcome.presentGDPRTermsSegue.rawValue) {
let gdprViewController = segue.destination as! GDPRViewController
gdprViewController.delegate = self
}
}
......@@ -242,16 +229,3 @@ public class ConfirmVPNPlanViewController: AutolayoutViewController, BrandableNa
}
}
extension ConfirmVPNPlanViewController: GDPRDelegate {
public func gdprViewWasAccepted() {
self.termsAndConditionsAgreed = true
self.signUp(nil)
}
public func gdprViewWasRejected() {
self.termsAndConditionsAgreed = false
}
}
......@@ -22,7 +22,7 @@
import UIKit
public class GetStartedViewController: AutolayoutViewController, ConfigurationAccess {
public class GetStartedViewController: PIAWelcomeViewController {
private static let smallDeviceMaxViewHeight: CGFloat = 520
private static let maxViewHeight: CGFloat = 500
......@@ -48,15 +48,17 @@ public class GetStartedViewController: AutolayoutViewController, ConfigurationAc
@IBOutlet private weak var textAgreement: UITextView!
@IBOutlet weak var visualEffectView: UIVisualEffectView!
private var allPlans: [PurchasePlan] = [.dummy, .dummy]
private var selectedPlanIndex: Int?
private var signupEmail: String?
private var signupTransaction: InAppTransaction?
private var isPurchasing = false
var preset = Preset()
private weak var delegate: PIAWelcomeViewControllerDelegate?
weak var completionDelegate: WelcomeCompletionDelegate?
@IBOutlet private weak var buttonViewConstraintHeight: NSLayoutConstraint!
@IBOutlet private weak var hiddenButtonsConstraintHeight: NSLayoutConstraint!
var termsWerePresentedOnce = false
private var buttonViewIsExpanded = false {
didSet {
self.updateButtonView()
......@@ -95,6 +97,8 @@ public class GetStartedViewController: AutolayoutViewController, ConfigurationAc
override public func viewDidLoad() {
allPlans = [.dummy, .dummy]
completionDelegate = self
subscribeNowTitle.text = L10n.Signup.Purchase.Trials.intro
subscribeNowDescription.text = L10n.Signup.Purchase.Trials.Price.after("")
......@@ -156,25 +160,63 @@ public class GetStartedViewController: AutolayoutViewController, ConfigurationAc
}
// MARK: Actions
@IBAction private func scrollPage(_ sender: UIPageControl) {
scrollToPage(sender.currentPage, animated: true)
@IBAction func confirmPlan() {
if let index = selectedPlanIndex,
let plans = allPlans {
let plan = plans[index]
self.startPurchaseProcessWithEmail("", andPlan: plan)
}
}
/**
Creates a wrapped `GetStartedViewController` ready for presentation.
- Parameter preset: The optional `Preset` to configure this controller with
- Parameter delegate: The `PIAWelcomeViewControllerDelegate` to handle raised events
*/
public static func with(preset: Preset? = nil, delegate: PIAWelcomeViewControllerDelegate? = nil) -> UIViewController {
let nav = StoryboardScene.Welcome.initialScene.instantiate()
let vc = nav.topViewController as! GetStartedViewController
if let customPreset = preset {
vc.preset = customPreset
private func startPurchaseProcessWithEmail(_ email: String,
andPlan plan: PurchasePlan) {
guard !Client.store.hasUncreditedTransactions else {
let alert = Macros.alert(
nil,
L10n.Signup.Purchase.Uncredited.Alert.message
)
alert.addCancelAction(L10n.Signup.Purchase.Uncredited.Alert.Button.cancel)
alert.addActionWithTitle(L10n.Signup.Purchase.Uncredited.Alert.Button.recover) {
self.navigationController?.popToRootViewController(animated: true)
Macros.postNotification(.PIARecoverAccount)
}
present(alert, animated: true, completion: nil)
return
}
//textEmail.text = email
isPurchasing = true
disableInteractions(fully: true)
self.showLoadingAnimation()
preset.accountProvider.purchase(plan: plan.plan) { (transaction, error) in
self.isPurchasing = false
self.enableInteractions()
self.hideLoadingAnimation()
guard let transaction = transaction else {
if let error = error {
let message = error.localizedDescription
Macros.displayImageNote(withImage: Asset.iconWarning.image,
message: message)
}
return
}
self.signupEmail = email
self.signupTransaction = transaction
self.perform(segue: StoryboardSegue.Welcome.signupViaPurchaseSegue)
}
vc.delegate = delegate
return nav
}
@IBAction private func scrollPage(_ sender: UIPageControl) {
scrollToPage(sender.currentPage, animated: true)
}
public static func withPurchase(preset: Preset? = nil, delegate: PIAWelcomeViewControllerDelegate? = nil) -> UIViewController {
......@@ -191,6 +233,25 @@ public class GetStartedViewController: AutolayoutViewController, ConfigurationAc
public override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == StoryboardSegue.Welcome.presentGDPRTermsSegue.rawValue {
let gdprViewController = segue.destination as! GDPRViewController
gdprViewController.delegate = self
} else 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
}
guard let vc = segue.destination as? PIAWelcomeViewController else {
return
}
......@@ -201,10 +262,6 @@ public class GetStartedViewController: AutolayoutViewController, ConfigurationAc
switch segue.identifier {
case StoryboardSegue.Welcome.purchaseVPNPlanSegue.rawValue:
vc.preset.pages = .purchase
case StoryboardSegue.Welcome.subscribeNowVPNPlanSegue.rawValue:
vc.preset.pages = .directPurchase
vc.allPlans = allPlans
vc.selectedPlanIndex = 0
case StoryboardSegue.Welcome.loginAccountSegue.rawValue:
vc.preset.pages = .login
case StoryboardSegue.Welcome.restorePurchaseSegue.rawValue:
......@@ -212,7 +269,7 @@ public class GetStartedViewController: AutolayoutViewController, ConfigurationAc
default:
break
}
}
// MARK: Orientation
......@@ -230,14 +287,28 @@ public class GetStartedViewController: AutolayoutViewController, ConfigurationAc
/// :nodoc:
public override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
UIDevice.current.setValue(Int(UIInterfaceOrientation.portrait.rawValue), forKey: "orientation")
self.navigationController?.setNavigationBarHidden(true, animated: true)
super.viewWillAppear(animated)
if let products = preset.accountProvider.planProducts {
refreshPlans(products)
} else {
disableInteractions(fully: false)
}
guard Client.preferences.gdprTermsAccepted else {
if !termsWerePresentedOnce {
//present term and conditions
termsWerePresentedOnce = true
self.performSegue(withIdentifier: StoryboardSegue.Welcome.presentGDPRTermsSegue.rawValue,
sender: nil)
return
}
return
}
}
private func styleButtons() {
......@@ -287,8 +358,10 @@ public class GetStartedViewController: AutolayoutViewController, ConfigurationAc
}
private func enableInteractions() {
self.subscribeNowButton.isEnabled = true
self.spinner.stopAnimating()
if !isPurchasing { //dont reenable the screen if we are still purchasing
self.subscribeNowButton.isEnabled = true
self.spinner.stopAnimating()
}
}
......@@ -410,7 +483,7 @@ public class GetStartedViewController: AutolayoutViewController, ConfigurationAc
}
}
allPlans[0] = purchase
allPlans?[0] = purchase
selectedPlanIndex = 0
}
}
......@@ -424,3 +497,19 @@ extension GetStartedViewController: UIScrollViewDelegate {
updateButtonsToCurrentPage()
}
}
extension GetStartedViewController: GDPRDelegate {
public func gdprViewWasAccepted() {
let preferences = Client.preferences.editable()
preferences.gdprTermsAccepted = true
preferences.commit()
}
public func gdprViewWasRejected() {
let preferences = Client.preferences.editable()
preferences.gdprTermsAccepted = false
preferences.commit()
}
}
......@@ -50,6 +50,10 @@ class PurchaseViewController: AutolayoutViewController, BrandableNavigationBar,
var selectedPlanIndex: Int?
private var signupEmail: String?
private var signupTransaction: InAppTransaction?
private var isPurchasing = false
deinit {
NotificationCenter.default.removeObserver(self)
}
......@@ -118,7 +122,22 @@ class PurchaseViewController: AutolayoutViewController, BrandableNavigationBar,
vc.termsAndConditionsTitle = L10n.Welcome.Agreement.Trials.title
vc.termsAndConditions = L10n.Welcome.Agreement.Trials.message
}
} else 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
}
}
/// Populate the view with the values from GetStartedView
......@@ -132,11 +151,72 @@ class PurchaseViewController: AutolayoutViewController, BrandableNavigationBar,
// MARK: Actions
func confirmPlan() {
@IBAction func confirmPlan() {
/**
self.performSegue(withIdentifier: StoryboardSegue.Welcome.confirmPurchaseVPNPlanSegue.rawValue,
sender: nil)
**/
if let index = selectedPlanIndex {
let plan = allPlans[index]
self.startPurchaseProcessWithEmail("", andPlan: plan)
}
}
private func startPurchaseProcessWithEmail(_ email: String,
andPlan plan: PurchasePlan) {
guard !Client.store.hasUncreditedTransactions else {
let alert = Macros.alert(
nil,
L10n.Signup.Purchase.Uncredited.Alert.message
)
alert.addCancelAction(L10n.Signup.Purchase.Uncredited.Alert.Button.cancel)
alert.addActionWithTitle(L10n.Signup.Purchase.Uncredited.Alert.Button.recover) {
self.navigationController?.popToRootViewController(animated: true)
Macros.postNotification(.PIARecoverAccount)
}
present(alert, animated: true, completion: nil)
return
}
//textEmail.text = email
log.debug("Will purchase plan: \(plan.product)")
isPurchasing = true
disableInteractions(fully: true)
self.showLoadingAnimation()
preset?.accountProvider.purchase(plan: plan.plan) { (transaction, error) in
self.isPurchasing = false
self.enableInteractions()
self.hideLoadingAnimation()
guard let transaction = transaction else {
if let error = error {
let message = error.localizedDescription
log.error("Purchase failed (error: \(error))")
Macros.displayImageNote(withImage: Asset.iconWarning.image,
message: message)
} else {
log.warning("Cancelled purchase")
}
return
}
log.debug("Purchased with transaction: \(transaction)")
self.signupEmail = email
self.signupTransaction = transaction
self.perform(segue: StoryboardSegue.Welcome.signupViaPurchaseSegue)
}
}
private func refreshPlans(_ plans: [Plan: InAppProduct]) {
if let yearly = plans[.yearly] {
let purchase = PurchasePlan(
......@@ -192,11 +272,13 @@ class PurchaseViewController: AutolayoutViewController, BrandableNavigationBar,
}
private func enableInteractions() {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.hideLoadingAnimation()
if !isPurchasing { //dont reenable the screen if we are still purchasing
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.hideLoadingAnimation()
}
collectionPlans.isUserInteractionEnabled = true
parent?.view.isUserInteractionEnabled = true