diff --git a/Passepartout-iOS/AppDelegate.swift b/Passepartout-iOS/AppDelegate.swift
index f77b31a8..cfbbd94c 100644
--- a/Passepartout-iOS/AppDelegate.swift
+++ b/Passepartout-iOS/AppDelegate.swift
@@ -97,8 +97,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UISplitViewControllerDele
guard let root = window?.rootViewController else {
fatalError("No window.rootViewController?")
}
-
let topmost = root.presentedViewController ?? root
+ if TransientStore.shared.service.hasReachedMaximumNumberOfHosts {
+ guard ProductManager.shared.isEligible(forFeature: .unlimitedHosts) else {
+ topmost.presentPurchaseScreen(forProduct: .unlimitedHosts)
+ return false
+ }
+ }
return tryParseURL(url, passphrase: nil, target: topmost)
}
diff --git a/Passepartout-iOS/Base.lproj/Purchase.storyboard b/Passepartout-iOS/Base.lproj/Purchase.storyboard
new file mode 100644
index 00000000..ac1989bc
--- /dev/null
+++ b/Passepartout-iOS/Base.lproj/Purchase.storyboard
@@ -0,0 +1,94 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Passepartout-iOS/Global/Macros.swift b/Passepartout-iOS/Global/Macros.swift
index a984e45d..088bcd29 100644
--- a/Passepartout-iOS/Global/Macros.swift
+++ b/Passepartout-iOS/Global/Macros.swift
@@ -62,6 +62,19 @@ extension UIColor {
}
}
+extension UIViewController {
+ func presentPurchaseScreen(forProduct product: Product) {
+ let nav = StoryboardScene.Purchase.initialScene.instantiate()
+ let vc = nav.topViewController as? PurchaseViewController
+ vc?.feature = product
+
+ // enforce pre iOS 13 behavior
+ nav.modalPresentationStyle = .fullScreen
+
+ present(nav, animated: true, completion: nil)
+ }
+}
+
func delay(_ block: @escaping () -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
block()
diff --git a/Passepartout-iOS/Global/Product.swift b/Passepartout-iOS/Global/Product.swift
new file mode 100644
index 00000000..5558c5ee
--- /dev/null
+++ b/Passepartout-iOS/Global/Product.swift
@@ -0,0 +1,115 @@
+//
+// Product.swift
+// Passepartout-iOS
+//
+// Created by Davide De Rosa on 10/11/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 Foundation
+import PassepartoutCore
+
+enum Product: String {
+
+ // MARK: Donations
+
+ case tinyDonation = "com.algoritmico.ios.Passepartout.donations.Tiny"
+
+ case smallDonation = "com.algoritmico.ios.Passepartout.donations.Small"
+
+ case mediumDonation = "com.algoritmico.ios.Passepartout.donations.Medium"
+
+ case bigDonation = "com.algoritmico.ios.Passepartout.donations.Big"
+
+ case hugeDonation = "com.algoritmico.ios.Passepartout.donations.Huge"
+
+ case maxiDonation = "com.algoritmico.ios.Passepartout.donations.Maxi"
+
+ static let allDonations: [Product] = [
+ .tinyDonation,
+ .smallDonation,
+ .mediumDonation,
+ .bigDonation,
+ .hugeDonation,
+ .maxiDonation
+ ]
+
+ // MARK: Features
+
+ case unlimitedHosts = "com.algoritmico.ios.Passepartout.features.unlimited_hosts"
+
+ case trustedNetworks = "com.algoritmico.ios.Passepartout.features.trusted_networks"
+
+ case siriShortcuts = "com.algoritmico.ios.Passepartout.features.siri"
+
+ case fullVersion = "com.algoritmico.ios.Passepartout.features.full_version"
+
+ static let allFeatures: [Product] = [
+ .unlimitedHosts,
+ .trustedNetworks,
+ .siriShortcuts,
+ .fullVersion
+ ]
+
+ // MARK: Providers
+
+ case mullvad = "com.algoritmico.ios.Passepartout.providers.Mullvad"
+
+ case nordVPN = "com.algoritmico.ios.Passepartout.providers.NordVPN"
+
+ case pia = "com.algoritmico.ios.Passepartout.providers.PIA"
+
+ case protonVPN = "com.algoritmico.ios.Passepartout.providers.ProtonVPN"
+
+ case tunnelBear = "com.algoritmico.ios.Passepartout.providers.TunnelBear"
+
+ case vyprVPN = "com.algoritmico.ios.Passepartout.providers.VyprVPN"
+
+ case windscribe = "com.algoritmico.ios.Passepartout.providers.Windscribe"
+
+ static let allProviders: [Product] = [
+ .mullvad,
+ .nordVPN,
+ .pia,
+ .protonVPN,
+ .tunnelBear,
+ .vyprVPN,
+ .windscribe
+ ]
+
+ // MARK: All
+
+ static let all: [Product] = allDonations + allFeatures + allProviders
+}
+
+extension Infrastructure.Name {
+ var product: Product {
+ guard let product = Product(rawValue: "com.algoritmico.ios.Passepartout.providers.\(rawValue)") else {
+ fatalError("Product not found for provider \(rawValue)")
+ }
+ return product
+ }
+}
+
+extension AppConstants {
+ struct InApp {
+ public static let limitedNumberOfHosts = 2
+ }
+}
diff --git a/Passepartout-iOS/Global/ProductManager.swift b/Passepartout-iOS/Global/ProductManager.swift
index d836ef3a..143360fa 100644
--- a/Passepartout-iOS/Global/ProductManager.swift
+++ b/Passepartout-iOS/Global/ProductManager.swift
@@ -26,14 +26,40 @@
import Foundation
import StoreKit
import Convenience
+import SwiftyBeaver
+import Kvitto
+import PassepartoutCore
+
+private let log = SwiftyBeaver.self
+
+class ProductManager: NSObject {
+ private static let lastFullVersionBuild = 2016 // 1.8.1
-struct ProductManager {
static let shared = ProductManager()
- private let inApp: InApp
+ private let inApp: InApp
- private init() {
+ private var purchasedAppBuild: Int?
+
+ private(set) var purchasedFeatures: Set
+
+ private var refreshRequest: SKReceiptRefreshRequest?
+
+ private var restoreCompletionHandler: ((Error?) -> Void)?
+
+ private override init() {
inApp = InApp()
+ purchasedAppBuild = nil
+ purchasedFeatures = []
+
+ super.init()
+
+ reloadReceipt()
+ SKPaymentQueue.default().add(self)
+ }
+
+ deinit {
+ SKPaymentQueue.default().remove(self)
}
func listProducts(completionHandler: (([SKProduct]) -> Void)?) {
@@ -41,12 +67,125 @@ struct ProductManager {
completionHandler?(inApp.products)
return
}
- inApp.requestProducts(withIdentifiers: Donation.all) { _ in
+ inApp.requestProducts(withIdentifiers: Product.all) { _ in
completionHandler?(self.inApp.products)
}
}
+ func product(withIdentifier identifier: Product) -> SKProduct? {
+ return inApp.product(withIdentifier: identifier)
+ }
+
func purchase(_ product: SKProduct, completionHandler: @escaping (InAppPurchaseResult, Error?) -> Void) {
- inApp.purchase(product: product, completionHandler: completionHandler)
+ inApp.purchase(product: product) {
+ if $0 == .success {
+ self.reloadReceipt()
+ }
+ completionHandler($0, $1)
+ }
+ }
+
+ func restorePurchases(completionHandler: @escaping (Error?) -> Void) {
+ restoreCompletionHandler = completionHandler
+ refreshRequest = SKReceiptRefreshRequest()
+ refreshRequest?.delegate = self
+ refreshRequest?.start()
+ }
+
+ // MARK: In-app eligibility
+
+ private func reloadReceipt() {
+ guard let url = Bundle.main.appStoreReceiptURL else {
+ log.warning("No App Store receipt found!")
+ return
+ }
+ guard let receipt = Receipt(contentsOfURL: url) else {
+ log.error("Could not parse App Store receipt!")
+ return
+ }
+
+ if let originalAppVersion = receipt.originalAppVersion, let buildNumber = Int(originalAppVersion) {
+ purchasedAppBuild = buildNumber
+ }
+ purchasedFeatures.removeAll()
+
+ if let buildNumber = purchasedAppBuild {
+ log.debug("Original purchased build: \(buildNumber)")
+
+ // treat former purchases as full versions
+ if buildNumber <= ProductManager.lastFullVersionBuild {
+ purchasedFeatures.insert(.fullVersion)
+ }
+ }
+ if let iapReceipts = receipt.inAppPurchaseReceipts {
+ log.debug("In-app receipts:")
+ iapReceipts.forEach {
+ guard let pid = $0.productIdentifier, let date = $0.originalPurchaseDate else {
+ return
+ }
+ log.debug("\t\(pid) [\(date)]")
+ }
+ for r in iapReceipts {
+ guard let pid = r.productIdentifier, let product = Product(rawValue: pid) else {
+ continue
+ }
+ purchasedFeatures.insert(product)
+ }
+ }
+ log.info("Purchased features: \(purchasedFeatures)")
+ }
+
+ func isFullVersion() -> Bool {
+ guard !AppConstants.Flags.isBeta else {
+ return true
+ }
+ return purchasedFeatures.contains(.fullVersion)
+ }
+
+ func isEligible(forFeature feature: Product) -> Bool {
+ guard !isFullVersion() else {
+ return true
+ }
+ return purchasedFeatures.contains(feature)
+ }
+
+ func isEligible(forProvider name: Infrastructure.Name) -> Bool {
+ guard !isFullVersion() else {
+ return true
+ }
+ return purchasedFeatures.contains {
+ return $0.rawValue.hasSuffix("providers.\(name.rawValue)")
+ }
+ }
+}
+
+extension ConnectionService {
+ var hasReachedMaximumNumberOfHosts: Bool {
+ let numberOfHosts = ids(forContext: .host).count
+ return numberOfHosts >= AppConstants.InApp.limitedNumberOfHosts
+ }
+}
+
+extension ProductManager: SKPaymentTransactionObserver {
+ func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
+ reloadReceipt()
+ }
+}
+
+extension ProductManager: SKRequestDelegate {
+ func requestDidFinish(_ request: SKRequest) {
+ reloadReceipt()
+ inApp.restorePurchases { [weak self] (finished, _, error) in
+ guard finished else {
+ return
+ }
+ self?.restoreCompletionHandler?(error)
+ self?.restoreCompletionHandler = nil
+ }
+ }
+
+ func request(_ request: SKRequest, didFailWithError error: Error) {
+ restoreCompletionHandler?(error)
+ restoreCompletionHandler = nil
}
}
diff --git a/Passepartout-iOS/Global/SwiftGen+Scenes.swift b/Passepartout-iOS/Global/SwiftGen+Scenes.swift
index c019e041..999e5fce 100644
--- a/Passepartout-iOS/Global/SwiftGen+Scenes.swift
+++ b/Passepartout-iOS/Global/SwiftGen+Scenes.swift
@@ -41,6 +41,11 @@ internal enum StoryboardScene {
internal static let wizardHostIdentifier = SceneType(storyboard: Organizer.self, identifier: "WizardHostIdentifier")
}
+ internal enum Purchase: StoryboardType {
+ internal static let storyboardName = "Purchase"
+
+ internal static let initialScene = InitialSceneType(storyboard: Purchase.self)
+ }
internal enum Shortcuts: StoryboardType {
internal static let storyboardName = "Shortcuts"
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/Organizer/DonationViewController.swift b/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift
index 015d3242..0cdb8359 100644
--- a/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift
+++ b/Passepartout-iOS/Scenes/Organizer/DonationViewController.swift
@@ -29,7 +29,7 @@ import PassepartoutCore
import Convenience
class DonationViewController: UITableViewController, StrongTableHost {
- private var donationList: [Donation] = []
+ private var donationList: [Product] = []
private var productsByIdentifier: [String: SKProduct] = [:]
@@ -62,12 +62,7 @@ class DonationViewController: UITableViewController, StrongTableHost {
return
}
- for row in Donation.all {
- guard let _ = productsByIdentifier[row.rawValue] else {
- continue
- }
- donationList.append(row)
- }
+ donationList.append(contentsOf: Product.allDonations.filter { productsByIdentifier[$0.rawValue] != nil })
model.set(.donation, count: donationList.count, forSection: .oneTime)
if isPurchasing {
diff --git a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift
index 225bc24b..86bb59b1 100644
--- a/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift
+++ b/Passepartout-iOS/Scenes/Organizer/ImportedHostsViewController.swift
@@ -71,7 +71,7 @@ class ImportedHostsViewController: UITableViewController {
// MARK: Actions
@IBAction private func openConfigurationFile() {
- let picker = UIDocumentPickerViewController(documentTypes: ["public.content", "public.data"], in: .import)
+ let picker = UIDocumentPickerViewController(documentTypes: AppConstants.URLs.filetypes, in: .import)
picker.allowsMultipleSelection = false
picker.delegate = self
present(picker, animated: true, completion: nil)
diff --git a/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift b/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift
index f690b69c..6e07f843 100644
--- a/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift
+++ b/Passepartout-iOS/Scenes/Organizer/OrganizerViewController.swift
@@ -204,10 +204,20 @@ class OrganizerViewController: UITableViewController, StrongTableHost {
}
private func addNewHost() {
+ if TransientStore.shared.service.hasReachedMaximumNumberOfHosts {
+ guard ProductManager.shared.isEligible(forFeature: .unlimitedHosts) else {
+ presentPurchaseScreen(forProduct: .unlimitedHosts)
+ return
+ }
+ }
perform(segue: StoryboardSegue.Organizer.showImportedHostsSegueIdentifier)
}
private func addShortcuts() {
+ guard ProductManager.shared.isEligible(forFeature: .siriShortcuts) else {
+ presentPurchaseScreen(forProduct: .siriShortcuts)
+ return
+ }
perform(segue: StoryboardSegue.Organizer.siriShortcutsSegueIdentifier)
}
diff --git a/Passepartout-iOS/Scenes/Organizer/WizardProviderViewController.swift b/Passepartout-iOS/Scenes/Organizer/WizardProviderViewController.swift
index 02553939..18e12271 100644
--- a/Passepartout-iOS/Scenes/Organizer/WizardProviderViewController.swift
+++ b/Passepartout-iOS/Scenes/Organizer/WizardProviderViewController.swift
@@ -38,6 +38,11 @@ class WizardProviderViewController: UITableViewController {
}
private func next(withName name: Infrastructure.Name) {
+ guard ProductManager.shared.isEligible(forProvider: name) else {
+ presentPurchaseScreen(forProduct: name.product)
+ return
+ }
+
let profile = ProviderConnectionProfile(name: name)
createdProfile = profile
diff --git a/Passepartout-iOS/Global/Donation.swift b/Passepartout-iOS/Scenes/Purchase/PurchaseTableViewCell.swift
similarity index 50%
rename from Passepartout-iOS/Global/Donation.swift
rename to Passepartout-iOS/Scenes/Purchase/PurchaseTableViewCell.swift
index 5eef090e..8efbab02 100644
--- a/Passepartout-iOS/Global/Donation.swift
+++ b/Passepartout-iOS/Scenes/Purchase/PurchaseTableViewCell.swift
@@ -1,8 +1,8 @@
//
-// Donation.swift
+// PurchaseTableViewCell.swift
// Passepartout-iOS
//
-// Created by Davide De Rosa on 10/11/19.
+// Created by Davide De Rosa on 10/30/19.
// Copyright (c) 2019 Davide De Rosa. All rights reserved.
//
// https://github.com/passepartoutvpn
@@ -23,27 +23,33 @@
// along with Passepartout. If not, see .
//
-import Foundation
+import UIKit
+import StoreKit
-enum Donation: String {
- case tiny = "com.algoritmico.ios.Passepartout.donations.Tiny"
+class PurchaseTableViewCell: UITableViewCell {
+ @IBOutlet private weak var labelTitle: UILabel?
- case small = "com.algoritmico.ios.Passepartout.donations.Small"
+ @IBOutlet private weak var labelDescription: UILabel?
- case medium = "com.algoritmico.ios.Passepartout.donations.Medium"
+ 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)."
+ )
+ }
- case big = "com.algoritmico.ios.Passepartout.donations.Big"
-
- case huge = "com.algoritmico.ios.Passepartout.donations.Huge"
-
- case maxi = "com.algoritmico.ios.Passepartout.donations.Maxi"
-
- static let all: [Donation] = [
- .tiny,
- .small,
- .medium,
- .big,
- .huge,
- .maxi
- ]
+ 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
new file mode 100644
index 00000000..224251aa
--- /dev/null
+++ b/Passepartout-iOS/Scenes/Purchase/PurchaseViewController.swift
@@ -0,0 +1,200 @@
+//
+// PurchaseViewController.swift
+// Passepartout-iOS
+//
+// Created by Davide De Rosa on 10/27/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
+import SwiftyBeaver
+import Convenience
+
+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
+ 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-iOS/Scenes/ServiceViewController.swift b/Passepartout-iOS/Scenes/ServiceViewController.swift
index 2f1d95e5..8d206faf 100644
--- a/Passepartout-iOS/Scenes/ServiceViewController.swift
+++ b/Passepartout-iOS/Scenes/ServiceViewController.swift
@@ -347,6 +347,14 @@ class ServiceViewController: UIViewController, StrongTableHost {
}
private func trustMobileNetwork(cell: ToggleTableViewCell) {
+ guard ProductManager.shared.isEligible(forFeature: .trustedNetworks) else {
+ delay {
+ cell.setOn(false, animated: true)
+ }
+ presentPurchaseScreen(forProduct: .trustedNetworks)
+ return
+ }
+
if #available(iOS 12, *) {
IntentDispatcher.donateTrustCellularNetwork()
IntentDispatcher.donateUntrustCellularNetwork()
@@ -356,6 +364,11 @@ class ServiceViewController: UIViewController, StrongTableHost {
}
private func trustCurrentWiFi() {
+ guard ProductManager.shared.isEligible(forFeature: .trustedNetworks) else {
+ presentPurchaseScreen(forProduct: .trustedNetworks)
+ return
+ }
+
if #available(iOS 13, *) {
let auth = CLLocationManager.authorizationStatus()
switch auth {
@@ -400,6 +413,14 @@ class ServiceViewController: UIViewController, StrongTableHost {
}
private func toggleTrustWiFi(cell: ToggleTableViewCell, at row: Int) {
+ guard ProductManager.shared.isEligible(forFeature: .trustedNetworks) else {
+ delay {
+ cell.setOn(false, animated: true)
+ }
+ presentPurchaseScreen(forProduct: .trustedNetworks)
+ return
+ }
+
if cell.isOn {
trustedNetworks.enableWifi(at: row)
} else {
diff --git a/Passepartout.xcodeproj/project.pbxproj b/Passepartout.xcodeproj/project.pbxproj
index 39d6ab50..dbb037c5 100644
--- a/Passepartout.xcodeproj/project.pbxproj
+++ b/Passepartout.xcodeproj/project.pbxproj
@@ -57,7 +57,7 @@
0E3152DB223FA05800F61841 /* ProfileKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E79D14021919F5600BB5FB2 /* ProfileKey.swift */; };
0E3152DC223FA05800F61841 /* ProviderConnectionProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBE3AA4213DC1B000BFA2F5 /* ProviderConnectionProfile.swift */; };
0E3262D9235EE8DA00B5E470 /* HostImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3262D8235EE8DA00B5E470 /* HostImporter.swift */; };
- 0E3419AD2350815E00419E18 /* Donation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3419AC2350815E00419E18 /* Donation.swift */; };
+ 0E3419AD2350815E00419E18 /* Product.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3419AC2350815E00419E18 /* Product.swift */; };
0E3586FE225BD34800509A4D /* ActivityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E3586FD225BD34800509A4D /* ActivityTableViewCell.swift */; };
0E36D24D2240234B006AF062 /* ShortcutsAddViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E36D24C2240234B006AF062 /* ShortcutsAddViewController.swift */; };
0E36D25822403469006AF062 /* Shortcuts.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E36D25A22403469006AF062 /* Shortcuts.storyboard */; };
@@ -68,6 +68,8 @@
0E45E6E422BD799700F19312 /* SwiftGen+Strings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E45E6E322BD799700F19312 /* SwiftGen+Strings.swift */; };
0E45E6FA22BD8FC500F19312 /* Core.strings in Resources */ = {isa = PBXBuildFile; fileRef = 0E3CAF98229AAE760008E5C8 /* Core.strings */; };
0E45E71022BE108100F19312 /* OpenVPNOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E45E70F22BE108100F19312 /* OpenVPNOptions.swift */; };
+ 0E4B0D6B2366E3C100C890B4 /* PurchaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E4B0D6A2366E3C000C890B4 /* PurchaseViewController.swift */; };
+ 0E4B0D742366E6C800C890B4 /* Purchase.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E4B0D762366E6C800C890B4 /* Purchase.storyboard */; };
0E4C9CBB20DCF0D600A0C59C /* DestructiveTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E4C9CBA20DCF0D600A0C59C /* DestructiveTableViewCell.swift */; };
0E4FD7F120D58618002221FF /* Macros.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E4FD7F020D58618002221FF /* Macros.swift */; };
0E533B162258E03B00EF94FC /* PoolGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E533B152258E03B00EF94FC /* PoolGroup.swift */; };
@@ -76,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 */; };
@@ -180,7 +183,7 @@
0E31529D223F9EF500F61841 /* PassepartoutCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PassepartoutCore.h; sourceTree = ""; };
0E31529E223F9EF500F61841 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
0E3262D8235EE8DA00B5E470 /* HostImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostImporter.swift; sourceTree = ""; };
- 0E3419AC2350815E00419E18 /* Donation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Donation.swift; sourceTree = ""; };
+ 0E3419AC2350815E00419E18 /* Product.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Product.swift; sourceTree = ""; };
0E3586FD225BD34800509A4D /* ActivityTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityTableViewCell.swift; sourceTree = ""; };
0E36D24C2240234B006AF062 /* ShortcutsAddViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsAddViewController.swift; sourceTree = ""; };
0E36D25B224034AD006AF062 /* ShortcutsConnectToViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShortcutsConnectToViewController.swift; sourceTree = ""; };
@@ -208,6 +211,8 @@
0E45E6F822BD898A00F19312 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/App.strings; sourceTree = ""; };
0E45E6F922BD898B00F19312 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/App.strings; sourceTree = ""; };
0E45E70F22BE108100F19312 /* OpenVPNOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenVPNOptions.swift; sourceTree = ""; };
+ 0E4B0D6A2366E3C000C890B4 /* PurchaseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseViewController.swift; sourceTree = ""; };
+ 0E4B0D752366E6C800C890B4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Purchase.storyboard; sourceTree = ""; };
0E4C9CB820DB9BC600A0C59C /* TrustedNetworks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedNetworks.swift; sourceTree = ""; };
0E4C9CBA20DCF0D600A0C59C /* DestructiveTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveTableViewCell.swift; sourceTree = ""; };
0E4FD7DD20D3E49A002221FF /* StandardVPNProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardVPNProvider.swift; sourceTree = ""; };
@@ -223,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 = ""; };
@@ -391,6 +397,15 @@
path = Resources;
sourceTree = "";
};
+ 0E4B0D6C2366E53C00C890B4 /* Purchase */ = {
+ isa = PBXGroup;
+ children = (
+ 0E6268932369AD0600355F75 /* PurchaseTableViewCell.swift */,
+ 0E4B0D6A2366E3C000C890B4 /* PurchaseViewController.swift */,
+ );
+ path = Purchase;
+ sourceTree = "";
+ };
0E4FD7D920D3E43F002221FF /* VPN */ = {
isa = PBXGroup;
children = (
@@ -448,6 +463,7 @@
0E24273C225950450064A1A3 /* About.storyboard */,
0E57F63F20C83FC5008323CF /* Main.storyboard */,
0ED38ADC213F44D00004D387 /* Organizer.storyboard */,
+ 0E4B0D762366E6C800C890B4 /* Purchase.storyboard */,
0E36D25A22403469006AF062 /* Shortcuts.storyboard */,
0E57F64220C83FC7008323CF /* Assets.xcassets */,
0E9CD788225746B300D033B4 /* Flags.xcassets */,
@@ -533,10 +549,10 @@
isa = PBXGroup;
children = (
0E45E6E222BD793800F19312 /* App.strings */,
- 0E3419AC2350815E00419E18 /* Donation.swift */,
0E3262D8235EE8DA00B5E470 /* HostImporter.swift */,
0EFD943D215BE10800529B64 /* IssueReporter.swift */,
0E4FD7F020D58618002221FF /* Macros.swift */,
+ 0E3419AC2350815E00419E18 /* Product.swift */,
0E24273F225951B00064A1A3 /* ProductManager.swift */,
0ECC60DD2256B6890020BEAC /* SwiftGen+Assets.swift */,
0EDE8DE320C89028004C739C /* SwiftGen+Scenes.swift */,
@@ -580,6 +596,7 @@
children = (
0E24273D225950CC0064A1A3 /* About */,
0E89DFCC213EEDE700741BA1 /* Organizer */,
+ 0E4B0D6C2366E53C00C890B4 /* Purchase */,
0E24273E225950ED0064A1A3 /* Shortcuts */,
0ED31C2820CF2A340027975F /* AccountViewController.swift */,
0ED38AEB2141260D0004D387 /* ConfigurationModificationDelegate.swift */,
@@ -812,6 +829,7 @@
0E9CD7872257462800D033B4 /* Providers.xcassets in Resources */,
0E9CD789225746B300D033B4 /* Flags.xcassets in Resources */,
0E57F64120C83FC5008323CF /* Main.storyboard in Resources */,
+ 0E4B0D742366E6C800C890B4 /* Purchase.storyboard in Resources */,
0E2AC24522EC3AC10037B4B0 /* Settings.bundle in Resources */,
0E45E6E022BD793800F19312 /* App.strings in Resources */,
);
@@ -980,11 +998,13 @@
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 */,
0E05C5D620D1645F006EE732 /* SwiftGen+Scenes.swift in Sources */,
0E773BF8224BF37600CDDC8E /* ShortcutsViewController.swift in Sources */,
- 0E3419AD2350815E00419E18 /* Donation.swift in Sources */,
+ 0E3419AD2350815E00419E18 /* Product.swift in Sources */,
0E9CDB6723604AD5006733B4 /* ServerNetworkViewController.swift in Sources */,
0E3262D9235EE8DA00B5E470 /* HostImporter.swift in Sources */,
0EFD9440215BED8E00529B64 /* LabelViewController.swift in Sources */,
@@ -1106,6 +1126,14 @@
name = App.strings;
sourceTree = "";
};
+ 0E4B0D762366E6C800C890B4 /* Purchase.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 0E4B0D752366E6C800C890B4 /* Base */,
+ );
+ name = Purchase.storyboard;
+ sourceTree = "";
+ };
0E57F63F20C83FC5008323CF /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
diff --git a/Podfile b/Podfile
index f4f6a9af..94c5667c 100644
--- a/Podfile
+++ b/Podfile
@@ -14,13 +14,14 @@ def shared_pods
pod 'SSZipArchive'
for spec in ['About', 'Alerts', 'Dialogs', 'InApp', 'Misc', 'Options', 'Persistence', 'Reviewer', 'Tables'] do
- pod "Convenience/#{spec}", :git => 'https://github.com/keeshux/convenience', :commit => 'b990a8c'
+ pod "Convenience/#{spec}", :git => 'https://github.com/keeshux/convenience', :commit => '22778d5'
#pod "Convenience/#{spec}", :path => '../../personal/convenience'
end
end
target 'PassepartoutCore-iOS' do
shared_pods
+ pod 'Kvitto'
end
target 'Passepartout-iOS' do
diff --git a/Podfile.lock b/Podfile.lock
index 27769c28..e5abed72 100644
--- a/Podfile.lock
+++ b/Podfile.lock
@@ -10,6 +10,14 @@ PODS:
- Convenience/Persistence (0.0.1)
- Convenience/Reviewer (0.0.1)
- Convenience/Tables (0.0.1)
+ - DTFoundation/Core (1.7.14)
+ - DTFoundation/DTASN1 (1.7.14):
+ - DTFoundation/Core
+ - Kvitto (1.0.3):
+ - DTFoundation/DTASN1 (~> 1.7.13)
+ - Kvitto/Core (= 1.0.3)
+ - Kvitto/Core (1.0.3):
+ - DTFoundation/DTASN1 (~> 1.7.13)
- MBProgressHUD (1.1.0)
- OpenSSL-Apple (1.1.0l.4)
- SSZipArchive (2.2.2)
@@ -26,15 +34,16 @@ PODS:
- TunnelKit/Core
DEPENDENCIES:
- - Convenience/About (from `https://github.com/keeshux/convenience`, commit `b990a8c`)
- - Convenience/Alerts (from `https://github.com/keeshux/convenience`, commit `b990a8c`)
- - Convenience/Dialogs (from `https://github.com/keeshux/convenience`, commit `b990a8c`)
- - Convenience/InApp (from `https://github.com/keeshux/convenience`, commit `b990a8c`)
- - Convenience/Misc (from `https://github.com/keeshux/convenience`, commit `b990a8c`)
- - Convenience/Options (from `https://github.com/keeshux/convenience`, commit `b990a8c`)
- - Convenience/Persistence (from `https://github.com/keeshux/convenience`, commit `b990a8c`)
- - Convenience/Reviewer (from `https://github.com/keeshux/convenience`, commit `b990a8c`)
- - Convenience/Tables (from `https://github.com/keeshux/convenience`, commit `b990a8c`)
+ - Convenience/About (from `https://github.com/keeshux/convenience`, commit `22778d5`)
+ - Convenience/Alerts (from `https://github.com/keeshux/convenience`, commit `22778d5`)
+ - Convenience/Dialogs (from `https://github.com/keeshux/convenience`, commit `22778d5`)
+ - Convenience/InApp (from `https://github.com/keeshux/convenience`, commit `22778d5`)
+ - Convenience/Misc (from `https://github.com/keeshux/convenience`, commit `22778d5`)
+ - Convenience/Options (from `https://github.com/keeshux/convenience`, commit `22778d5`)
+ - Convenience/Persistence (from `https://github.com/keeshux/convenience`, commit `22778d5`)
+ - Convenience/Reviewer (from `https://github.com/keeshux/convenience`, commit `22778d5`)
+ - Convenience/Tables (from `https://github.com/keeshux/convenience`, commit `22778d5`)
+ - Kvitto
- MBProgressHUD
- SSZipArchive
- TunnelKit/Extra/LZO (from `https://github.com/passepartoutvpn/tunnelkit`, commit `4d930d3`)
@@ -42,6 +51,8 @@ DEPENDENCIES:
SPEC REPOS:
https://github.com/cocoapods/specs.git:
+ - DTFoundation
+ - Kvitto
- MBProgressHUD
- OpenSSL-Apple
- SSZipArchive
@@ -49,7 +60,7 @@ SPEC REPOS:
EXTERNAL SOURCES:
Convenience:
- :commit: b990a8c
+ :commit: 22778d5
:git: https://github.com/keeshux/convenience
TunnelKit:
:commit: 4d930d3
@@ -57,20 +68,22 @@ EXTERNAL SOURCES:
CHECKOUT OPTIONS:
Convenience:
- :commit: b990a8c
+ :commit: 22778d5
:git: https://github.com/keeshux/convenience
TunnelKit:
:commit: 4d930d3
:git: https://github.com/passepartoutvpn/tunnelkit
SPEC CHECKSUMS:
- Convenience: c2bc96be4ca77c7018f85b2c63b95c2a44c05c5a
+ Convenience: c4240c936b2119752ffa0841d40a4bc6a0ba8a5d
+ DTFoundation: 25aa19bb7c6e225b1dfae195604fb8cf1da0ab4c
+ Kvitto: d451f893f84ad669850b7cb7d3f8781363e14232
MBProgressHUD: e7baa36a220447d8aeb12769bf0585582f3866d9
OpenSSL-Apple: f3d1668588ea8f06b076dcfa6177cd90452e3800
SSZipArchive: fa16b8cc4cdeceb698e5e5d9f67e9558532fbf23
SwiftyBeaver: aaf2ebd7dac2e952991f46a82ed24ad642867ae2
TunnelKit: 0743f0306be0869d51118ac33e274e7507a93537
-PODFILE CHECKSUM: f45a3fd3744e646a5513e3e25d447d1550c9fefa
+PODFILE CHECKSUM: 8099389f0a709d4f175b81d077f47699a651fa42
COCOAPODS: 1.8.4
diff --git a/README.md b/README.md
index fd506231..cae82223 100644
--- a/README.md
+++ b/README.md
@@ -130,12 +130,13 @@ The logo is taken from the awesome Circle Icons set by Nick Roach.
The country flags are taken from:
+- Kvitto - © 2015 Oliver Drobnik
+- lzo - © 1996-2017 Markus F.X.J. Oberhumer
- MBProgressHUD - © 2009-2016 Matej Bukovinski
- PIATunnel - © 2018-Present Private Internet Access
- SSZipArchive - © 2010-2012 Sam Soffes
- SwiftGen - © 2018 SwiftGen
- SwiftyBeaver - © 2015 Sebastian Kreutzberger
-- lzo - © 1996-2017 Markus F.X.J. Oberhumer
This product includes software developed by the OpenSSL Project for use in the OpenSSL Toolkit. ([https://www.openssl.org/][dep-openssl])
diff --git a/Submodules/Core b/Submodules/Core
index 4fb30619..22f214e9 160000
--- a/Submodules/Core
+++ b/Submodules/Core
@@ -1 +1 @@
-Subproject commit 4fb306195bbb71973adf4d03290952226c2b5dab
+Subproject commit 22f214e9ab03da3087a5818882e3704b98711bc4
diff --git a/swiftgen.yml b/swiftgen.yml
index 855481ab..fe9ab66e 100644
--- a/swiftgen.yml
+++ b/swiftgen.yml
@@ -11,6 +11,7 @@ ib:
- Passepartout-iOS/Base.lproj/About.storyboard
- Passepartout-iOS/Base.lproj/Main.storyboard
- Passepartout-iOS/Base.lproj/Organizer.storyboard
+ - Passepartout-iOS/Base.lproj/Purchase.storyboard
- Passepartout-iOS/Base.lproj/Shortcuts.storyboard
outputs:
- templateName: scenes-swift4