diff --git a/Passepartout-iOS/Base.lproj/Purchase.storyboard b/Passepartout-iOS/Base.lproj/Purchase.storyboard
index 747d3afb..ac1989bc 100644
--- a/Passepartout-iOS/Base.lproj/Purchase.storyboard
+++ b/Passepartout-iOS/Base.lproj/Purchase.storyboard
@@ -4,7 +4,6 @@
-
@@ -17,7 +16,7 @@
-
+
@@ -25,20 +24,71 @@
-
+
-
-
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/Passepartout-iOS/Global/Macros.swift b/Passepartout-iOS/Global/Macros.swift
index 68282102..990cc961 100644
--- a/Passepartout-iOS/Global/Macros.swift
+++ b/Passepartout-iOS/Global/Macros.swift
@@ -64,7 +64,10 @@ extension UIColor {
extension UIViewController {
func presentPurchaseScreen(forProduct product: Product) {
- present(StoryboardScene.Purchase.initialScene.instantiate(), animated: true, completion: nil)
+ let nav = StoryboardScene.Purchase.initialScene.instantiate()
+ let vc = nav.topViewController as? PurchaseViewController
+ vc?.feature = product
+ present(nav, animated: true, completion: nil)
}
}
diff --git a/Passepartout-iOS/Global/SwiftGen+Strings.swift b/Passepartout-iOS/Global/SwiftGen+Strings.swift
index 067b35a5..1ec505c6 100644
--- a/Passepartout-iOS/Global/SwiftGen+Strings.swift
+++ b/Passepartout-iOS/Global/SwiftGen+Strings.swift
@@ -78,6 +78,18 @@ internal enum L10n {
}
}
}
+ internal enum Purchase {
+ /// Purchase
+ internal static let title = L10n.tr("App", "purchase.title")
+ internal enum Cells {
+ internal enum Restore {
+ /// If you bought this app or feature in the past, you can restore your purchases and this screen won't show again.
+ internal static let description = L10n.tr("App", "purchase.cells.restore.description")
+ /// Restore purchases
+ internal static let title = L10n.tr("App", "purchase.cells.restore.title")
+ }
+ }
+ }
internal enum Service {
internal enum Alerts {
internal enum Location {
diff --git a/Passepartout-iOS/Global/Theme.swift b/Passepartout-iOS/Global/Theme.swift
index de07270a..16f84761 100644
--- a/Passepartout-iOS/Global/Theme.swift
+++ b/Passepartout-iOS/Global/Theme.swift
@@ -130,6 +130,10 @@ extension UILabel {
func applyLight(_ theme: Theme) {
textColor = theme.palette.primaryLightText
}
+
+ func applyAccent(_ theme: Theme) {
+ textColor = theme.palette.accent1
+ }
}
extension UIButton {
diff --git a/Passepartout-iOS/Global/en.lproj/App.strings b/Passepartout-iOS/Global/en.lproj/App.strings
index 99907bb3..ec407710 100644
--- a/Passepartout-iOS/Global/en.lproj/App.strings
+++ b/Passepartout-iOS/Global/en.lproj/App.strings
@@ -59,3 +59,7 @@
"shortcuts.edit.title" = "Manage shortcuts";
"shortcuts.edit.cells.add_shortcut.caption" = "Add shortcut";
+
+"purchase.title" = "Purchase";
+"purchase.cells.restore.title" = "Restore purchases";
+"purchase.cells.restore.description" = "If you bought this app or feature in the past, you can restore your purchases and this screen won't show again.";
diff --git a/Passepartout-iOS/Scenes/Purchase/PurchaseTableViewCell.swift b/Passepartout-iOS/Scenes/Purchase/PurchaseTableViewCell.swift
new file mode 100644
index 00000000..8efbab02
--- /dev/null
+++ b/Passepartout-iOS/Scenes/Purchase/PurchaseTableViewCell.swift
@@ -0,0 +1,55 @@
+//
+// PurchaseTableViewCell.swift
+// Passepartout-iOS
+//
+// Created by Davide De Rosa on 10/30/19.
+// Copyright (c) 2019 Davide De Rosa. All rights reserved.
+//
+// https://github.com/passepartoutvpn
+//
+// This file is part of Passepartout.
+//
+// Passepartout is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Passepartout is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Passepartout. If not, see .
+//
+
+import UIKit
+import StoreKit
+
+class PurchaseTableViewCell: UITableViewCell {
+ @IBOutlet private weak var labelTitle: UILabel?
+
+ @IBOutlet private weak var labelDescription: UILabel?
+
+ override func awakeFromNib() {
+ super.awakeFromNib()
+
+ labelTitle?.applyAccent(.current)
+ }
+
+ func fill(product: SKProduct) {
+ var title = product.localizedTitle
+ if let price = product.localizedPrice {
+ title += " @ \(price)"
+ }
+ fill(
+ title: title,
+ description: "\(product.localizedDescription)."
+ )
+ }
+
+ func fill(title: String, description: String) {
+ labelTitle?.text = title
+ labelDescription?.text = description
+ }
+}
diff --git a/Passepartout-iOS/Scenes/Purchase/PurchaseViewController.swift b/Passepartout-iOS/Scenes/Purchase/PurchaseViewController.swift
index 63079fa0..10b029dc 100644
--- a/Passepartout-iOS/Scenes/Purchase/PurchaseViewController.swift
+++ b/Passepartout-iOS/Scenes/Purchase/PurchaseViewController.swift
@@ -24,7 +24,182 @@
//
import UIKit
+import StoreKit
+import SwiftyBeaver
+import Convenience
-class PurchaseViewController: UIViewController {
+private let log = SwiftyBeaver.self
+
+class PurchaseViewController: UITableViewController, StrongTableHost {
+ private var isLoading = true
+
+ var feature: Product!
+
+ private var skFeature: SKProduct?
+
+ private var skFullVersion: SKProduct?
+
+ // MARK: StrongTableHost
+ var model: StrongTableModel = StrongTableModel()
+
+ func reloadModel() {
+ model.clear()
+ model.add(.products)
+
+ var rows: [RowType] = []
+ let pm = ProductManager.shared
+ if let skFeature = pm.product(withIdentifier: feature) {
+ self.skFeature = skFeature
+ rows.append(.feature)
+ }
+ if let skFullVersion = pm.product(withIdentifier: .fullVersion) {
+ self.skFullVersion = skFullVersion
+ rows.append(.fullVersion)
+ }
+ rows.append(.restore)
+ model.set(rows, forSection: .products)
+ }
+
+ // MARK: UIViewController
+
+ override func viewDidLoad() {
+ super.viewDidLoad()
+
+ guard let _ = feature else {
+ fatalError("No feature set for purchase")
+ }
+
+ title = L10n.App.Purchase.title
+
+ // enforce pre iOS 13 behavior
+ if #available(iOS 13, *) {
+ isModalInPresentation = true
+ }
+ navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(close))
+
+ isLoading = true
+ tableView.reloadData()
+
+ let hud = HUD(view: view)
+ ProductManager.shared.listProducts { [weak self] _ in
+ self?.reloadModel()
+ self?.isLoading = false
+ self?.tableView.reloadData()
+ hud.hide()
+ }
+ }
+
+ // MARK: Actions
+
+ private func purchaseFeature() {
+ guard let sk = skFeature else {
+ return
+ }
+ purchase(sk)
+ }
+
+ private func purchaseFullVersion() {
+ guard let sk = skFullVersion else {
+ return
+ }
+ purchase(sk)
+ }
+
+ private func restorePurchases() {
+ let hud = HUD(view: view)
+ ProductManager.shared.restorePurchases { [weak self] in
+ hud.hide()
+ guard $0 == nil else {
+ return
+ }
+ self?.dismiss(animated: true, completion: nil)
+ }
+ }
+
+ private func purchase(_ skProduct: SKProduct) {
+ let hud = HUD(view: view)
+ ProductManager.shared.purchase(skProduct) { [weak self] in
+ hud.hide()
+ guard $0 == .success else {
+ if let error = $1 {
+ self?.reportPurchaseError(withProduct: skProduct, error: error)
+ }
+ return
+ }
+ self?.dismiss(animated: true, completion: nil)
+ }
+ }
+
+ private func reportPurchaseError(withProduct product: SKProduct, error: Error) {
+ log.error("Unable to purchase \(product): \(error)")
+
+ let alert = UIAlertController.asAlert(product.localizedTitle, error.localizedDescription)
+ alert.addCancelAction(L10n.Core.Global.ok)
+ present(alert, animated: true, completion: nil)
+ }
+
+ @objc private func close() {
+ dismiss(animated: true, completion: nil)
+ }
+}
+
+extension PurchaseViewController {
+ enum SectionType {
+ case products
+ }
+
+ enum RowType {
+ case feature
+
+ case fullVersion
+
+ case restore
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ guard !isLoading else {
+ return 0
+ }
+ return model.numberOfRows(forSection: section)
+ }
+
+ override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+ let cell = tableView.dequeueReusableCell(withIdentifier: "PurchaseTableViewCell", for: indexPath) as! PurchaseTableViewCell
+ switch model.row(at: indexPath) {
+ case .feature:
+ guard let product = skFeature else {
+ fatalError("Loaded feature cell, yet no corresponding product?")
+ }
+ cell.fill(product: product)
+
+ case .fullVersion:
+ guard let product = skFullVersion else {
+ fatalError("Loaded full version cell, yet no corresponding product?")
+ }
+ cell.fill(product: product)
+
+ case .restore:
+ cell.fill(
+ title: L10n.App.Purchase.Cells.Restore.title,
+ description: L10n.App.Purchase.Cells.Restore.description
+ )
+ }
+ return cell
+ }
+
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ tableView.deselectRow(at: indexPath, animated: true)
+
+ switch model.row(at: indexPath) {
+ case .feature:
+ purchaseFeature()
+
+ case .fullVersion:
+ purchaseFullVersion()
+
+ case .restore:
+ restorePurchases()
+ }
+ }
}
diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj
index 3ea1f019..dbb037c5 100644
--- a/Passepartout.xcodeproj/project.pbxproj
+++ b/Passepartout.xcodeproj/project.pbxproj
@@ -78,6 +78,7 @@
0E57F64120C83FC5008323CF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E57F63F20C83FC5008323CF /* Main.storyboard */; };
0E57F64320C83FC7008323CF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0E57F64220C83FC7008323CF /* Assets.xcassets */; };
0E57F64620C83FC7008323CF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E57F64420C83FC7008323CF /* LaunchScreen.storyboard */; };
+ 0E6268942369AD0600355F75 /* PurchaseTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6268932369AD0600355F75 /* PurchaseTableViewCell.swift */; };
0E66A270225FE25800F9C779 /* PoolCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E66A26F225FE25800F9C779 /* PoolCategory.swift */; };
0E6BE13F20CFBAB300A6DD36 /* DebugLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6BE13E20CFBAB300A6DD36 /* DebugLogViewController.swift */; };
0E773BF8224BF37600CDDC8E /* ShortcutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E773BF7224BF37600CDDC8E /* ShortcutsViewController.swift */; };
@@ -227,6 +228,7 @@
0E5E5DDE215119AF00E318A3 /* VPNStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNStatus.swift; sourceTree = ""; };
0E5E5DE1215119DD00E318A3 /* VPNConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNConfiguration.swift; sourceTree = ""; };
0E5E5DE421511C5F00E318A3 /* GracefulVPN.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GracefulVPN.swift; sourceTree = ""; };
+ 0E6268932369AD0600355F75 /* PurchaseTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseTableViewCell.swift; sourceTree = ""; };
0E66A26F225FE25800F9C779 /* PoolCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PoolCategory.swift; path = ../Model/Profiles/PoolCategory.swift; sourceTree = ""; };
0E6ACB7722B1A57C001B3C99 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Intents.strings; sourceTree = ""; };
0E6ACB7822B1A5BB001B3C99 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Core.strings; sourceTree = ""; };
@@ -398,6 +400,7 @@
0E4B0D6C2366E53C00C890B4 /* Purchase */ = {
isa = PBXGroup;
children = (
+ 0E6268932369AD0600355F75 /* PurchaseTableViewCell.swift */,
0E4B0D6A2366E3C000C890B4 /* PurchaseViewController.swift */,
);
path = Purchase;
@@ -995,6 +998,7 @@
0E776642229D0DAE0023FA76 /* Intents.intentdefinition in Sources */,
0ECEE45020E1182E00A6BB43 /* Theme+Cells.swift in Sources */,
0E242740225951B00064A1A3 /* ProductManager.swift in Sources */,
+ 0E6268942369AD0600355F75 /* PurchaseTableViewCell.swift in Sources */,
0E1066C920E0F84A004F98B7 /* Cells.swift in Sources */,
0E4B0D6B2366E3C100C890B4 /* PurchaseViewController.swift in Sources */,
0EF56BBB2185AC8500B0C8AB /* SwiftGen+Segues.swift in Sources */,