Merge branch 'freemium-model'
This commit is contained in:
commit
b82abcb257
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="fEC-GT-W4O">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="njF-92-dZH">
|
||||
<objects>
|
||||
<navigationController id="fEC-GT-W4O" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="hZC-Bv-5pP">
|
||||
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="bQc-2A-qWz" kind="relationship" relationship="rootViewController" id="0da-C5-Txv"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cwJ-TA-B3C" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-781" y="113"/>
|
||||
</scene>
|
||||
<!--Purchase View Controller-->
|
||||
<scene sceneID="93l-dg-vRI">
|
||||
<objects>
|
||||
<tableViewController id="bQc-2A-qWz" customClass="PurchaseViewController" customModule="Passepartout" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="5WE-4Q-uDQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="PurchaseTableViewCell" id="0XE-hK-4Ro" customClass="PurchaseTableViewCell" customModule="Passepartout" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="28" width="414" height="105"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0XE-hK-4Ro" id="Fe6-eH-sBT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="105"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Dit-R5-2h7">
|
||||
<rect key="frame" x="20" y="20" width="374" height="65"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Title" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Hf0-aN-JRs">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="24.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="20"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Description" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gB1-nL-LEc">
|
||||
<rect key="frame" x="0.0" y="44.5" width="374" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="gB1-nL-LEc" secondAttribute="trailing" id="062-HW-sKW"/>
|
||||
<constraint firstItem="gB1-nL-LEc" firstAttribute="leading" secondItem="Dit-R5-2h7" secondAttribute="leading" id="3je-C8-e0v"/>
|
||||
<constraint firstItem="Hf0-aN-JRs" firstAttribute="leading" secondItem="Dit-R5-2h7" secondAttribute="leading" id="7cX-Z8-Z9p"/>
|
||||
<constraint firstItem="Hf0-aN-JRs" firstAttribute="top" secondItem="Dit-R5-2h7" secondAttribute="top" id="FOS-1V-gEv"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Hf0-aN-JRs" secondAttribute="trailing" id="MQl-hH-lvW"/>
|
||||
<constraint firstItem="gB1-nL-LEc" firstAttribute="top" secondItem="Hf0-aN-JRs" secondAttribute="bottom" constant="20" id="dKT-d6-56g"/>
|
||||
<constraint firstAttribute="bottom" secondItem="gB1-nL-LEc" secondAttribute="bottom" id="h1s-nk-2kw"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="Dit-R5-2h7" secondAttribute="trailing" constant="20" id="Kzi-2M-P4i"/>
|
||||
<constraint firstItem="Dit-R5-2h7" firstAttribute="leading" secondItem="Fe6-eH-sBT" secondAttribute="leading" constant="20" id="M5n-u9-S3P"/>
|
||||
<constraint firstItem="Dit-R5-2h7" firstAttribute="top" secondItem="Fe6-eH-sBT" secondAttribute="top" constant="20" id="xZt-JQ-Zyk"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Dit-R5-2h7" secondAttribute="bottom" constant="20" id="zfx-7a-jXF"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="labelDescription" destination="gB1-nL-LEc" id="zIn-g5-Rl0"/>
|
||||
<outlet property="labelTitle" destination="Hf0-aN-JRs" id="E65-5C-zZR"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<sections/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="bQc-2A-qWz" id="WNX-QW-EjN"/>
|
||||
<outlet property="delegate" destination="bQc-2A-qWz" id="oqL-qc-xkY"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" id="azV-cT-GZs"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="VLq-IA-G4Y" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="87" y="113"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
|
@ -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()
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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<Donation>
|
||||
private let inApp: InApp<Product>
|
||||
|
||||
private init() {
|
||||
private var purchasedAppBuild: Int?
|
||||
|
||||
private(set) var purchasedFeatures: Set<Product>
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,11 @@ internal enum StoryboardScene {
|
|||
|
||||
internal static let wizardHostIdentifier = SceneType<UIKit.UINavigationController>(storyboard: Organizer.self, identifier: "WizardHostIdentifier")
|
||||
}
|
||||
internal enum Purchase: StoryboardType {
|
||||
internal static let storyboardName = "Purchase"
|
||||
|
||||
internal static let initialScene = InitialSceneType<UIKit.UINavigationController>(storyboard: Purchase.self)
|
||||
}
|
||||
internal enum Shortcuts: StoryboardType {
|
||||
internal static let storyboardName = "Shortcuts"
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -130,6 +130,10 @@ extension UILabel {
|
|||
func applyLight(_ theme: Theme) {
|
||||
textColor = theme.palette.primaryLightText
|
||||
}
|
||||
|
||||
func applyAccent(_ theme: Theme) {
|
||||
textColor = theme.palette.accent1
|
||||
}
|
||||
}
|
||||
|
||||
extension UIButton {
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
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<SectionType, RowType> = 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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
0E31529E223F9EF500F61841 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
0E3262D8235EE8DA00B5E470 /* HostImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HostImporter.swift; sourceTree = "<group>"; };
|
||||
0E3419AC2350815E00419E18 /* Donation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Donation.swift; sourceTree = "<group>"; };
|
||||
0E3419AC2350815E00419E18 /* Product.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Product.swift; sourceTree = "<group>"; };
|
||||
0E3586FD225BD34800509A4D /* ActivityTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityTableViewCell.swift; sourceTree = "<group>"; };
|
||||
0E36D24C2240234B006AF062 /* ShortcutsAddViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsAddViewController.swift; sourceTree = "<group>"; };
|
||||
0E36D25B224034AD006AF062 /* ShortcutsConnectToViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ShortcutsConnectToViewController.swift; sourceTree = "<group>"; };
|
||||
|
@ -208,6 +211,8 @@
|
|||
0E45E6F822BD898A00F19312 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/App.strings; sourceTree = "<group>"; };
|
||||
0E45E6F922BD898B00F19312 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/App.strings; sourceTree = "<group>"; };
|
||||
0E45E70F22BE108100F19312 /* OpenVPNOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenVPNOptions.swift; sourceTree = "<group>"; };
|
||||
0E4B0D6A2366E3C000C890B4 /* PurchaseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseViewController.swift; sourceTree = "<group>"; };
|
||||
0E4B0D752366E6C800C890B4 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Purchase.storyboard; sourceTree = "<group>"; };
|
||||
0E4C9CB820DB9BC600A0C59C /* TrustedNetworks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrustedNetworks.swift; sourceTree = "<group>"; };
|
||||
0E4C9CBA20DCF0D600A0C59C /* DestructiveTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DestructiveTableViewCell.swift; sourceTree = "<group>"; };
|
||||
0E4FD7DD20D3E49A002221FF /* StandardVPNProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardVPNProvider.swift; sourceTree = "<group>"; };
|
||||
|
@ -223,6 +228,7 @@
|
|||
0E5E5DDE215119AF00E318A3 /* VPNStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNStatus.swift; sourceTree = "<group>"; };
|
||||
0E5E5DE1215119DD00E318A3 /* VPNConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNConfiguration.swift; sourceTree = "<group>"; };
|
||||
0E5E5DE421511C5F00E318A3 /* GracefulVPN.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GracefulVPN.swift; sourceTree = "<group>"; };
|
||||
0E6268932369AD0600355F75 /* PurchaseTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseTableViewCell.swift; sourceTree = "<group>"; };
|
||||
0E66A26F225FE25800F9C779 /* PoolCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PoolCategory.swift; path = ../Model/Profiles/PoolCategory.swift; sourceTree = "<group>"; };
|
||||
0E6ACB7722B1A57C001B3C99 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Intents.strings; sourceTree = "<group>"; };
|
||||
0E6ACB7822B1A5BB001B3C99 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Core.strings; sourceTree = "<group>"; };
|
||||
|
@ -391,6 +397,15 @@
|
|||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0E4B0D6C2366E53C00C890B4 /* Purchase */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0E6268932369AD0600355F75 /* PurchaseTableViewCell.swift */,
|
||||
0E4B0D6A2366E3C000C890B4 /* PurchaseViewController.swift */,
|
||||
);
|
||||
path = Purchase;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
0E4B0D762366E6C800C890B4 /* Purchase.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
0E4B0D752366E6C800C890B4 /* Base */,
|
||||
);
|
||||
name = Purchase.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0E57F63F20C83FC5008323CF /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
|
|
3
Podfile
3
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
|
||||
|
|
39
Podfile.lock
39
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
|
||||
|
|
|
@ -130,12 +130,13 @@ The logo is taken from the awesome Circle Icons set by Nick Roach.
|
|||
|
||||
The country flags are taken from: <https://github.com/lipis/flag-icon-css/>
|
||||
|
||||
- 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])
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 4fb306195bbb71973adf4d03290952226c2b5dab
|
||||
Subproject commit 22f214e9ab03da3087a5818882e3704b98711bc4
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue