Merge branch 'restrict-macos-features'

This commit is contained in:
Davide De Rosa 2021-02-04 15:21:35 +01:00
commit 95449149d3
42 changed files with 776 additions and 137 deletions

View File

@ -175,9 +175,12 @@
0E57F64620C83FC7008323CF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E57F64420C83FC7008323CF /* LaunchScreen.storyboard */; }; 0E57F64620C83FC7008323CF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E57F64420C83FC7008323CF /* LaunchScreen.storyboard */; };
0E6268942369AD0600355F75 /* PurchaseTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6268932369AD0600355F75 /* PurchaseTableViewCell.swift */; }; 0E6268942369AD0600355F75 /* PurchaseTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6268932369AD0600355F75 /* PurchaseTableViewCell.swift */; };
0E66A270225FE25800F9C779 /* PoolCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E66A26F225FE25800F9C779 /* PoolCategory.swift */; }; 0E66A270225FE25800F9C779 /* PoolCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E66A26F225FE25800F9C779 /* PoolCategory.swift */; };
0E6BA54B25C9EE3A000CDFAC /* Purchase.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0E6BA54D25C9EE3A000CDFAC /* Purchase.storyboard */; };
0E6BE13F20CFBAB300A6DD36 /* DebugLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6BE13E20CFBAB300A6DD36 /* DebugLogViewController.swift */; }; 0E6BE13F20CFBAB300A6DD36 /* DebugLogViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6BE13E20CFBAB300A6DD36 /* DebugLogViewController.swift */; };
0E773BF8224BF37600CDDC8E /* ShortcutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E773BF7224BF37600CDDC8E /* ShortcutsViewController.swift */; }; 0E773BF8224BF37600CDDC8E /* ShortcutsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E773BF7224BF37600CDDC8E /* ShortcutsViewController.swift */; };
0E776642229D0DAE0023FA76 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 0E3CAFAD229AAE760008E5C8 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; }; 0E776642229D0DAE0023FA76 /* Intents.intentdefinition in Sources */ = {isa = PBXBuildFile; fileRef = 0E3CAFAD229AAE760008E5C8 /* Intents.intentdefinition */; settings = {ATTRIBUTES = (no_codegen, ); }; };
0E79D2C825C9F1B300D12964 /* PurchaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E79D2C725C9F1B300D12964 /* PurchaseViewController.swift */; };
0E79D31E25CC0CF600D12964 /* PurchaseProductView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E79D31D25CC0CF600D12964 /* PurchaseProductView.swift */; };
0E89DFCE213EEDFA00741BA1 /* WizardProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFCD213EEDFA00741BA1 /* WizardProviderViewController.swift */; }; 0E89DFCE213EEDFA00741BA1 /* WizardProviderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E89DFCD213EEDFA00741BA1 /* WizardProviderViewController.swift */; };
0E9AA978259F756A003FAFF1 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9AA977259F756A003FAFF1 /* PacketTunnelProvider.swift */; }; 0E9AA978259F756A003FAFF1 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9AA977259F756A003FAFF1 /* PacketTunnelProvider.swift */; };
0E9AA979259F756A003FAFF1 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9AA977259F756A003FAFF1 /* PacketTunnelProvider.swift */; }; 0E9AA979259F756A003FAFF1 /* PacketTunnelProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9AA977259F756A003FAFF1 /* PacketTunnelProvider.swift */; };
@ -485,6 +488,7 @@
0E66A26F225FE25800F9C779 /* PoolCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PoolCategory.swift; path = ../Model/Profiles/PoolCategory.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>"; }; 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>"; }; 0E6ACB7822B1A5BB001B3C99 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Core.strings; sourceTree = "<group>"; };
0E6BA54C25C9EE3A000CDFAC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Purchase.storyboard; sourceTree = "<group>"; };
0E6BE13920CFB76800A6DD36 /* ApplicationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationError.swift; sourceTree = "<group>"; }; 0E6BE13920CFB76800A6DD36 /* ApplicationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationError.swift; sourceTree = "<group>"; };
0E6BE13E20CFBAB300A6DD36 /* DebugLogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugLogViewController.swift; sourceTree = "<group>"; }; 0E6BE13E20CFBAB300A6DD36 /* DebugLogViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DebugLogViewController.swift; sourceTree = "<group>"; };
0E773BF7224BF37600CDDC8E /* ShortcutsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsViewController.swift; sourceTree = "<group>"; }; 0E773BF7224BF37600CDDC8E /* ShortcutsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShortcutsViewController.swift; sourceTree = "<group>"; };
@ -500,6 +504,8 @@
0E776640229D0DA80023FA76 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Intents.strings; sourceTree = "<group>"; }; 0E776640229D0DA80023FA76 /* es */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = es; path = es.lproj/Intents.strings; sourceTree = "<group>"; };
0E79D13E21919EC900BB5FB2 /* PlaceholderConnectionProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderConnectionProfile.swift; sourceTree = "<group>"; }; 0E79D13E21919EC900BB5FB2 /* PlaceholderConnectionProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceholderConnectionProfile.swift; sourceTree = "<group>"; };
0E79D14021919F5600BB5FB2 /* ProfileKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileKey.swift; sourceTree = "<group>"; }; 0E79D14021919F5600BB5FB2 /* ProfileKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileKey.swift; sourceTree = "<group>"; };
0E79D2C725C9F1B300D12964 /* PurchaseViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PurchaseViewController.swift; sourceTree = "<group>"; };
0E79D31D25CC0CF600D12964 /* PurchaseProductView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseProductView.swift; sourceTree = "<group>"; };
0E89DFC4213DF7AE00741BA1 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; }; 0E89DFC4213DF7AE00741BA1 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
0E89DFC7213E8FC500741BA1 /* SessionProxy+Communication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProxy+Communication.swift"; sourceTree = "<group>"; }; 0E89DFC7213E8FC500741BA1 /* SessionProxy+Communication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionProxy+Communication.swift"; sourceTree = "<group>"; };
0E89DFCD213EEDFA00741BA1 /* WizardProviderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardProviderViewController.swift; sourceTree = "<group>"; }; 0E89DFCD213EEDFA00741BA1 /* WizardProviderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardProviderViewController.swift; sourceTree = "<group>"; };
@ -771,6 +777,7 @@
0E569F7D259F41690022DFB8 /* Providers.xcassets */, 0E569F7D259F41690022DFB8 /* Providers.xcassets */,
0E569F85259F41690022DFB8 /* Main.storyboard */, 0E569F85259F41690022DFB8 /* Main.storyboard */,
0E569F81259F41690022DFB8 /* Preferences.storyboard */, 0E569F81259F41690022DFB8 /* Preferences.storyboard */,
0E6BA54D25C9EE3A000CDFAC /* Purchase.storyboard */,
0E569F83259F41690022DFB8 /* Service.storyboard */, 0E569F83259F41690022DFB8 /* Service.storyboard */,
); );
path = macOS; path = macOS;
@ -801,6 +808,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0E569F65259F41690022DFB8 /* Preferences */, 0E569F65259F41690022DFB8 /* Preferences */,
0E6BA54125C9ED91000CDFAC /* Purchase */,
0E569F6C259F41690022DFB8 /* Service */, 0E569F6C259F41690022DFB8 /* Service */,
0E569F69259F41690022DFB8 /* OrganizerProfileTableView.swift */, 0E569F69259F41690022DFB8 /* OrganizerProfileTableView.swift */,
0E569F6A259F41690022DFB8 /* OrganizerViewController.swift */, 0E569F6A259F41690022DFB8 /* OrganizerViewController.swift */,
@ -932,6 +940,15 @@
path = iOS; path = iOS;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
0E6BA54125C9ED91000CDFAC /* Purchase */ = {
isa = PBXGroup;
children = (
0E79D31D25CC0CF600D12964 /* PurchaseProductView.swift */,
0E79D2C725C9F1B300D12964 /* PurchaseViewController.swift */,
);
path = Purchase;
sourceTree = "<group>";
};
0E89DFCC213EEDE700741BA1 /* Organizer */ = { 0E89DFCC213EEDE700741BA1 /* Organizer */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1486,6 +1503,7 @@
0E52031D259F58BF00CBAB56 /* Providers.xcassets in Resources */, 0E52031D259F58BF00CBAB56 /* Providers.xcassets in Resources */,
0E52047D259F642600CBAB56 /* Preferences.storyboard in Resources */, 0E52047D259F642600CBAB56 /* Preferences.storyboard in Resources */,
0E52038F259F593F00CBAB56 /* App.strings in Resources */, 0E52038F259F593F00CBAB56 /* App.strings in Resources */,
0E6BA54B25C9EE3A000CDFAC /* Purchase.storyboard in Resources */,
0E520385259F593B00CBAB56 /* Credits.html in Resources */, 0E520385259F593B00CBAB56 /* Credits.html in Resources */,
0E52047C259F642600CBAB56 /* Service.storyboard in Resources */, 0E52047C259F642600CBAB56 /* Service.storyboard in Resources */,
0E52032B259F58DD00CBAB56 /* TextTableView.xib in Resources */, 0E52032B259F58DD00CBAB56 /* TextTableView.xib in Resources */,
@ -1882,6 +1900,7 @@
0E294AA225AE2B0B00CB4908 /* Descriptible.swift in Sources */, 0E294AA225AE2B0B00CB4908 /* Descriptible.swift in Sources */,
0E520356259F590600CBAB56 /* PreferencesGeneralViewController.swift in Sources */, 0E520356259F590600CBAB56 /* PreferencesGeneralViewController.swift in Sources */,
0E520348259F58FE00CBAB56 /* MTUViewController.swift in Sources */, 0E520348259F58FE00CBAB56 /* MTUViewController.swift in Sources */,
0E79D31E25CC0CF600D12964 /* PurchaseProductView.swift in Sources */,
0E52037E259F593B00CBAB56 /* SwiftGen+Segues.swift in Sources */, 0E52037E259F593B00CBAB56 /* SwiftGen+Segues.swift in Sources */,
0E52037C259F593B00CBAB56 /* Theme.swift in Sources */, 0E52037C259F593B00CBAB56 /* Theme.swift in Sources */,
0E52035E259F591300CBAB56 /* StatusMenu.swift in Sources */, 0E52035E259F591300CBAB56 /* StatusMenu.swift in Sources */,
@ -1895,6 +1914,7 @@
0E520354259F590600CBAB56 /* PreferencesViewController.swift in Sources */, 0E520354259F590600CBAB56 /* PreferencesViewController.swift in Sources */,
0E520343259F58FE00CBAB56 /* ProfileCustomizationViewController.swift in Sources */, 0E520343259F58FE00CBAB56 /* ProfileCustomizationViewController.swift in Sources */,
0E520346259F58FE00CBAB56 /* TrustedNetworksViewController.swift in Sources */, 0E520346259F58FE00CBAB56 /* TrustedNetworksViewController.swift in Sources */,
0E79D2C825C9F1B300D12964 /* PurchaseViewController.swift in Sources */,
0E520338259F58F500CBAB56 /* ProviderServiceView.swift in Sources */, 0E520338259F58F500CBAB56 /* ProviderServiceView.swift in Sources */,
0E520347259F58FE00CBAB56 /* ProxyViewController.swift in Sources */, 0E520347259F58FE00CBAB56 /* ProxyViewController.swift in Sources */,
0E52037B259F593B00CBAB56 /* IssueReporter.swift in Sources */, 0E52037B259F593B00CBAB56 /* IssueReporter.swift in Sources */,
@ -2247,6 +2267,14 @@
name = LaunchScreen.storyboard; name = LaunchScreen.storyboard;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
0E6BA54D25C9EE3A000CDFAC /* Purchase.storyboard */ = {
isa = PBXVariantGroup;
children = (
0E6BA54C25C9EE3A000CDFAC /* Base */,
);
name = Purchase.storyboard;
sourceTree = "<group>";
};
0ED38ADC213F44D00004D387 /* Organizer.storyboard */ = { 0ED38ADC213F44D00004D387 /* Organizer.storyboard */ = {
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (

View File

@ -95,30 +95,6 @@ internal enum L10n {
} }
} }
} }
internal enum Purchase {
/// Purchase
internal static let title = L10n.tr("App", "purchase.title")
internal enum Cells {
internal enum FullVersion {
/// - All providers (including future ones)\n%@
internal static func extraDescription(_ p1: Any) -> String {
return L10n.tr("App", "purchase.cells.full_version.extra_description", String(describing: p1))
}
}
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 Sections {
internal enum Products {
/// Every product is a one-time purchase. Provider purchases do not include a VPN subscription.
internal static let footer = L10n.tr("App", "purchase.sections.products.footer")
}
}
}
internal enum Service { internal enum Service {
internal enum Alerts { internal enum Alerts {
internal enum Location { internal enum Location {
@ -765,6 +741,30 @@ internal enum L10n {
} }
} }
} }
internal enum Purchase {
/// Purchase
internal static let title = L10n.tr("Core", "purchase.title")
internal enum Cells {
internal enum FullVersion {
/// - All providers (including future ones)\n%@
internal static func extraDescription(_ p1: Any) -> String {
return L10n.tr("Core", "purchase.cells.full_version.extra_description", String(describing: p1))
}
}
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("Core", "purchase.cells.restore.description")
/// Restore purchases
internal static let title = L10n.tr("Core", "purchase.cells.restore.title")
}
}
internal enum Sections {
internal enum Products {
/// Every product is a one-time purchase. Provider purchases do not include a VPN subscription.
internal static let footer = L10n.tr("Core", "purchase.sections.products.footer")
}
}
}
internal enum Reddit { internal enum Reddit {
/// Did you know that Passepartout has a subreddit? Subscribe for updates or to discuss issues, features, new platforms or whatever you like.\n\nIt's also a great way to show you care about this project. /// Did you know that Passepartout has a subreddit? Subscribe for updates or to discuss issues, features, new platforms or whatever you like.\n\nIt's also a great way to show you care about this project.
internal static let message = L10n.tr("Core", "reddit.message") internal static let message = L10n.tr("Core", "reddit.message")

View File

@ -43,9 +43,13 @@ class PurchaseViewController: UITableViewController, StrongTableHost {
weak var delegate: PurchaseViewControllerDelegate? weak var delegate: PurchaseViewControllerDelegate?
private var skFeature: SKProduct? private var skFeature: SKProduct?
private var skPlatformVersion: SKProduct?
private var skFullVersion: SKProduct? private var skFullVersion: SKProduct?
private var platformVersionExtra: String?
private var fullVersionExtra: String? private var fullVersionExtra: String?
// MARK: StrongTableHost // MARK: StrongTableHost
@ -55,10 +59,14 @@ class PurchaseViewController: UITableViewController, StrongTableHost {
func reloadModel() { func reloadModel() {
model.clear() model.clear()
model.add(.products) model.add(.products)
model.setFooter(L10n.App.Purchase.Sections.Products.footer, forSection: .products) model.setFooter(L10n.Core.Purchase.Sections.Products.footer, forSection: .products)
var rows: [RowType] = [] var rows: [RowType] = []
let pm = ProductManager.shared let pm = ProductManager.shared
if let skPlatformVersion = pm.product(withIdentifier: .fullVersion_iOS) {
self.skPlatformVersion = skPlatformVersion
rows.append(.platformVersion)
}
if let skFullVersion = pm.product(withIdentifier: .fullVersion) { if let skFullVersion = pm.product(withIdentifier: .fullVersion) {
self.skFullVersion = skFullVersion self.skFullVersion = skFullVersion
rows.append(.fullVersion) rows.append(.fullVersion)
@ -70,11 +78,17 @@ class PurchaseViewController: UITableViewController, StrongTableHost {
rows.append(.restore) rows.append(.restore)
model.set(rows, forSection: .products) model.set(rows, forSection: .products)
let featureBulletsList: [String] = ProductManager.shared.featureProducts(excluding: [.fullVersion, .fullVersion_iOS]).map { let platformBulletsList: [String] = ProductManager.shared.featureProducts(excluding: [.fullVersion, .fullVersion_iOS, .fullVersion_macOS]).map {
return "- \($0.localizedTitle)" return "- \($0.localizedTitle)"
}.sortedCaseInsensitive() }.sortedCaseInsensitive()
let featureBullets = featureBulletsList.joined(separator: "\n") let platformBullets = platformBulletsList.joined(separator: "\n")
fullVersionExtra = L10n.App.Purchase.Cells.FullVersion.extraDescription(featureBullets) platformVersionExtra = "- \(L10n.Core.Purchase.Cells.FullVersion.extraDescription(platformBullets))"
let fullBulletsList: [String] = ProductManager.shared.featureProducts(excluding: [.fullVersion, .fullVersion_iOS]).map {
return "- \($0.localizedTitle)"
}.sortedCaseInsensitive()
let fullBullets = fullBulletsList.joined(separator: "\n")
fullVersionExtra = "- \(L10n.Core.Purchase.Cells.FullVersion.extraDescription(fullBullets))"
} }
// MARK: UIViewController // MARK: UIViewController
@ -86,7 +100,7 @@ class PurchaseViewController: UITableViewController, StrongTableHost {
fatalError("No feature set for purchase") fatalError("No feature set for purchase")
} }
title = L10n.App.Purchase.title title = L10n.Core.Purchase.title
navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(close)) navigationItem.leftBarButtonItem = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(close))
isLoading = true isLoading = true
@ -113,6 +127,13 @@ class PurchaseViewController: UITableViewController, StrongTableHost {
} }
purchase(sk) purchase(sk)
} }
private func purchasePlatformVersion() {
guard let sk = skPlatformVersion else {
return
}
purchase(sk)
}
private func purchaseFullVersion() { private func purchaseFullVersion() {
guard let sk = skFullVersion else { guard let sk = skFullVersion else {
@ -174,8 +195,10 @@ extension PurchaseViewController {
enum RowType { enum RowType {
case feature case feature
case fullVersion case platformVersion
case fullVersion
case restore case restore
} }
@ -203,6 +226,12 @@ extension PurchaseViewController {
} }
cell.fill(product: product) cell.fill(product: product)
case .platformVersion:
guard let product = skPlatformVersion else {
fatalError("Loaded platform version cell, yet no corresponding product?")
}
cell.fill(product: product, customDescription: platformVersionExtra)
case .fullVersion: case .fullVersion:
guard let product = skFullVersion else { guard let product = skFullVersion else {
fatalError("Loaded full version cell, yet no corresponding product?") fatalError("Loaded full version cell, yet no corresponding product?")
@ -211,8 +240,8 @@ extension PurchaseViewController {
case .restore: case .restore:
cell.fill( cell.fill(
title: L10n.App.Purchase.Cells.Restore.title, title: L10n.Core.Purchase.Cells.Restore.title,
description: L10n.App.Purchase.Cells.Restore.description description: L10n.Core.Purchase.Cells.Restore.description
) )
} }
return cell return cell
@ -225,6 +254,9 @@ extension PurchaseViewController {
case .feature: case .feature:
purchaseFeature() purchaseFeature()
case .platformVersion:
purchasePlatformVersion()
case .fullVersion: case .fullVersion:
purchaseFullVersion() purchaseFullVersion()

View File

@ -63,9 +63,3 @@
"shortcuts.edit.title" = "Kurzbefehle bearbeiten"; "shortcuts.edit.title" = "Kurzbefehle bearbeiten";
"shortcuts.edit.cells.add_shortcut.caption" = "Kurzbefehl hinzufügen"; "shortcuts.edit.cells.add_shortcut.caption" = "Kurzbefehl hinzufügen";
"purchase.title" = "Kaufen";
"purchase.sections.products.footer" = "Jedes Produkt ist ein einmaliger Kauf. Der Kauf eines Providers beinhaltet kein VPN-Abonnement.";
"purchase.cells.full_version.extra_description" = "- Alle Anbieter (inklusive Zukünftige)\n%@";
"purchase.cells.restore.title" = "Einkäufe wiederherstellen";
"purchase.cells.restore.description" = "Wenn Sie diese App oder Funktion in der Vergangenheit gekauft haben, können Sie Ihre Einkäufe wiederherstellen und dieser Bildschirm wird nicht mehr angezeigt.";

View File

@ -63,9 +63,3 @@
"shortcuts.edit.title" = "Διαχείριση συντομεύσεων"; "shortcuts.edit.title" = "Διαχείριση συντομεύσεων";
"shortcuts.edit.cells.add_shortcut.caption" = "Προσθήκη Συντόμευσης"; "shortcuts.edit.cells.add_shortcut.caption" = "Προσθήκη Συντόμευσης";
"purchase.title" = "Αγορά";
"purchase.sections.products.footer" = "Κάθε προϊόν είναι μια αγορά. Οι αγορές παρόχων δεν περιλαμβάνουν τη συνδρομή VPN.";
"purchase.cells.full_version.extra_description" = "- Όλοι οι πάροχοι (περιλαμβάνονται και οι μελλοντικοί)\n%@";
"purchase.cells.restore.title" = "Επαναφορά Αγορών";
"purchase.cells.restore.description" = "Εαν αγοράσατε την εφαρμογή στο παρελθόν, μπορείτε να κάνετε επαναφορά αγορών και αυτή η οθόνη δε θα εμφανιστεί ξανά.";

View File

@ -63,9 +63,3 @@
"shortcuts.edit.title" = "Manage shortcuts"; "shortcuts.edit.title" = "Manage shortcuts";
"shortcuts.edit.cells.add_shortcut.caption" = "Add shortcut"; "shortcuts.edit.cells.add_shortcut.caption" = "Add shortcut";
"purchase.title" = "Purchase";
"purchase.sections.products.footer" = "Every product is a one-time purchase. Provider purchases do not include a VPN subscription.";
"purchase.cells.full_version.extra_description" = "- All providers (including future ones)\n%@";
"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.";

View File

@ -63,9 +63,3 @@
"shortcuts.edit.title" = "Gestionar atajos"; "shortcuts.edit.title" = "Gestionar atajos";
"shortcuts.edit.cells.add_shortcut.caption" = "Añadir atajo"; "shortcuts.edit.cells.add_shortcut.caption" = "Añadir atajo";
"purchase.title" = "Compra";
"purchase.sections.products.footer" = "Cada producto es una compra única y no recurrente. La compra de un proveedor no incluye una suscripción al servicio.";
"purchase.cells.full_version.extra_description" = "- Todos los proveedores (incluye los futuros)\n%@";
"purchase.cells.restore.title" = "Restaurar compras";
"purchase.cells.restore.description" = "Si compraste esta aplicación o funcionalidad anteriormente, puedes restaurar tus compras y esta pantalla no volverá a aparecer.";

View File

@ -63,9 +63,3 @@
"shortcuts.edit.title" = "Gérer les raccourcis"; "shortcuts.edit.title" = "Gérer les raccourcis";
"shortcuts.edit.cells.add_shortcut.caption" = "Ajouter un raccourcis"; "shortcuts.edit.cells.add_shortcut.caption" = "Ajouter un raccourcis";
"purchase.title" = "Acheter";
"purchase.sections.products.footer" = "Chaque produit est un achat unique. Les achats n'incluent pas une souscription à un service de VPN.";
"purchase.cells.full_version.extra_description" = "- Tous les fournisseurs (incluant les prochains)\n%@";
"purchase.cells.restore.title" = "Restaurer les achats";
"purchase.cells.restore.description" = "Si vous avez acheté l'application ou une fonctionnalité dans le passé, vous pouvez restaurer les achats et ce message ne s'affichera plus.";

View File

@ -63,9 +63,3 @@
"shortcuts.edit.title" = "Gestisci comandi rapidi"; "shortcuts.edit.title" = "Gestisci comandi rapidi";
"shortcuts.edit.cells.add_shortcut.caption" = "Aggiungi comando rapido"; "shortcuts.edit.cells.add_shortcut.caption" = "Aggiungi comando rapido";
"purchase.title" = "Acquista";
"purchase.sections.products.footer" = "Ogni prodotto è un acquisto unico e non ricorrente. L'acquisto di un provider non include una sottoscrizione.";
"purchase.cells.full_version.extra_description" = "- Tutti i provider (inclusi quelli futuri)\n%@";
"purchase.cells.restore.title" = "Ripristina acquisti";
"purchase.cells.restore.description" = "Se hai comprato quest'applicazione o funzionalità in precedenza, puoi ripristinare i tuoi acquisti in modo che questa schermata non compaia più.";

View File

@ -63,9 +63,3 @@
"shortcuts.edit.title" = "Beheer snelkoppelingen"; "shortcuts.edit.title" = "Beheer snelkoppelingen";
"shortcuts.edit.cells.add_shortcut.caption" = "Voeg snelkoppeling toe"; "shortcuts.edit.cells.add_shortcut.caption" = "Voeg snelkoppeling toe";
"purchase.title" = "Aanschaffen";
"purchase.sections.products.footer" = "Elk product is een eenmalige aankoop. Aankopen van providers bevatten geen VPN-abonnement.";
"purchase.cells.full_version.extra_description" = "- Alle providers (inclusief toekomstige)\n%@";
"purchase.cells.restore.title" = "Herstel Aankopen";
"purchase.cells.restore.description" = "Als u deze app of functie in het verleden heeft gekocht, kunt u uw aankopen herstellen en wordt dit scherm niet meer getoond.";

View File

@ -63,9 +63,3 @@
"shortcuts.edit.title" = "Zarządzaj skrótami"; "shortcuts.edit.title" = "Zarządzaj skrótami";
"shortcuts.edit.cells.add_shortcut.caption" = "Dodaj skrót"; "shortcuts.edit.cells.add_shortcut.caption" = "Dodaj skrót";
"purchase.title" = "Kup";
"purchase.sections.products.footer" = "Każdy produkt to zakup jednorazowy. Kuipno usługodawcy nie zawiera subskrypcji VPN.";
"purchase.cells.full_version.extra_description" = "- Wszyscy usługodawcy (włączając przyszłych)\n%@";
"purchase.cells.restore.title" = "Przywróć zakup";
"purchase.cells.restore.description" = "Jeśli kupiłeś tą aplikację lub funkcję wcześniej, możesz przywrócić swoje zakupy i ten ekran nie będzie wyświetlony ponownie.";

View File

@ -63,9 +63,3 @@
"shortcuts.edit.title" = "Configuração de atalhos"; "shortcuts.edit.title" = "Configuração de atalhos";
"shortcuts.edit.cells.add_shortcut.caption" = "Adicionar atalho"; "shortcuts.edit.cells.add_shortcut.caption" = "Adicionar atalho";
"purchase.title" = "Comprar";
"purchase.sections.products.footer" = "Todo produto é uma compra única. As compras do fornecedor não incluem uma assinatura VPN.";
"purchase.cells.full_version.extra_description" = "- Todos os provedores (incluindo os futuros)\n%@";
"purchase.cells.restore.title" = "Restaurar compras";
"purchase.cells.restore.description" = "Se você comprou este aplicativo ou recurso no passado, pode restaurar suas compras e essa tela não será exibida novamente.";

View File

@ -63,9 +63,3 @@
"shortcuts.edit.title" = "Управлять командами"; "shortcuts.edit.title" = "Управлять командами";
"shortcuts.edit.cells.add_shortcut.caption" = "Создать команду"; "shortcuts.edit.cells.add_shortcut.caption" = "Создать команду";
"purchase.title" = "Покупка";
"purchase.sections.products.footer" = "Каждый продукт является разовой покупкой. Покупка провайдера не включает подписку на VPN.";
"purchase.cells.full_version.extra_description" = "- Все провайдеры (включая добавленных в будущем)\n%@";
"purchase.cells.restore.title" = "Восстановить покупки";
"purchase.cells.restore.description" = "Если Вы купили это приложение или совершили встроенные покупки в прошлом, вы можете восстановить ваши покупки, и этот баннер больше не появится.";

View File

@ -63,9 +63,3 @@
"shortcuts.edit.title" = "Hantera genvägar"; "shortcuts.edit.title" = "Hantera genvägar";
"shortcuts.edit.cells.add_shortcut.caption" = "Lägg till genväg"; "shortcuts.edit.cells.add_shortcut.caption" = "Lägg till genväg";
"purchase.title" = "Köp";
"purchase.sections.products.footer" = "Varje produkt är ett engångsköp. Leverantörsköp inkluderar inte ett VPN-abonnemang.";
"purchase.cells.full_version.extra_description" = "- Alla leverantörer (inklusive framtida)\n%@";
"purchase.cells.restore.title" = "Återställ köp";
"purchase.cells.restore.description" = "Om du köpte den här appen eller funktionen tidigare kan du återställa dina inköp och den här skärmen visas inte igen.";

View File

@ -63,9 +63,3 @@
"shortcuts.edit.title" = "管理捷径"; "shortcuts.edit.title" = "管理捷径";
"shortcuts.edit.cells.add_shortcut.caption" = "添加捷径"; "shortcuts.edit.cells.add_shortcut.caption" = "添加捷径";
"purchase.title" = "购买";
"purchase.sections.products.footer" = "每件产品都是一次性的购买。 购买的提供商并不包含VPN订阅。";
"purchase.cells.full_version.extra_description" = "- 所有的提供商(包括未来添加的)\n%@";
"purchase.cells.restore.title" = "恢复购买";
"purchase.cells.restore.description" = "如果你购买过此应用或其特征, 你可以恢复购买,此页面将不在显示。";

View File

@ -57,6 +57,7 @@ class AppDelegate: NSObject, NSApplicationDelegate {
NSApp.mainMenu = loadMainMenu() NSApp.mainMenu = loadMainMenu()
StatusMenu.shared.install() StatusMenu.shared.install()
ProductManager.shared.reviewPurchases()
// if let appCenterSecret = appCenterSecret, !appCenterSecret.isEmpty { // if let appCenterSecret = appCenterSecret, !appCenterSecret.isEmpty {
// AppCenter.start(withAppSecret: appCenterSecret, services: [Analytics.self, Crashes.self]) // AppCenter.start(withAppSecret: appCenterSecret, services: [Analytics.self, Crashes.self])

View File

@ -0,0 +1,187 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="Rv5-Zx-TH3">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Purchase View Controller-->
<scene sceneID="9TJ-0E-yCE">
<objects>
<viewController id="Rv5-Zx-TH3" customClass="PurchaseViewController" customModule="Passepartout" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" id="8QZ-37-H7U">
<rect key="frame" x="0.0" y="0.0" width="442" height="500"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<scrollView autohidesScrollers="YES" horizontalLineScroll="80" horizontalPageScroll="10" verticalLineScroll="80" verticalPageScroll="10" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WTb-Wq-BLo">
<rect key="frame" x="20" y="120" width="402" height="360"/>
<clipView key="contentView" id="dfi-7t-ces">
<rect key="frame" x="1" y="1" width="400" height="358"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView verticalHuggingPriority="750" allowsExpansionToolTips="YES" columnAutoresizingStyle="lastColumnOnly" columnSelection="YES" multipleSelection="NO" autosaveColumns="NO" rowHeight="80" rowSizeStyle="automatic" viewBased="YES" id="9w7-b6-jXh">
<rect key="frame" x="0.0" y="0.0" width="400" height="358"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<size key="intercellSpacing" width="17" height="0.0"/>
<color key="backgroundColor" name="controlBackgroundColor" catalog="System" colorSpace="catalog"/>
<color key="gridColor" name="gridColor" catalog="System" colorSpace="catalog"/>
<tableColumns>
<tableColumn width="388" minWidth="40" maxWidth="1000" id="GRZ-an-Pd1">
<tableHeaderCell key="headerCell" lineBreakMode="truncatingTail" borderStyle="border">
<color key="textColor" name="headerTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="headerColor" catalog="System" colorSpace="catalog"/>
</tableHeaderCell>
<textFieldCell key="dataCell" lineBreakMode="truncatingTail" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" title="Text" id="gwL-5E-ehb">
<font key="font" metaFont="system"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<tableColumnResizingMask key="resizingMask" resizeWithTable="YES" userResizable="YES"/>
<prototypeCellViews>
<customView identifier="ProductCellIdentifier" misplaced="YES" id="pnP-i1-QiM" customClass="PurchaseProductView" customModule="Passepartout" customModuleProvider="target">
<rect key="frame" x="8" y="0.0" width="383" height="80"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<subviews>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="q6k-22-i5F">
<rect key="frame" x="18" y="44" width="278" height="20"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="&lt;title&gt;" id="3zZ-WV-GIN">
<font key="font" metaFont="systemMedium" size="17"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xYH-wg-hUh">
<rect key="frame" x="18" y="20" width="347" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="&lt;description&gt;" id="bqY-NM-eTC">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="kRO-hG-JPd">
<rect key="frame" x="300" y="44" width="65" height="20"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="&lt;price&gt;" id="YYL-xk-bbo">
<font key="font" metaFont="systemMedium" size="17"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="xYH-wg-hUh" secondAttribute="bottom" constant="20" symbolic="YES" id="1EP-V1-gsU"/>
<constraint firstAttribute="trailing" secondItem="xYH-wg-hUh" secondAttribute="trailing" constant="20" symbolic="YES" id="Des-Zp-REX"/>
<constraint firstAttribute="trailing" secondItem="kRO-hG-JPd" secondAttribute="trailing" constant="20" symbolic="YES" id="OY4-cV-sdC"/>
<constraint firstItem="kRO-hG-JPd" firstAttribute="leading" secondItem="q6k-22-i5F" secondAttribute="trailing" constant="8" symbolic="YES" id="Olt-aL-NpR"/>
<constraint firstItem="xYH-wg-hUh" firstAttribute="top" secondItem="q6k-22-i5F" secondAttribute="bottom" constant="8" symbolic="YES" id="R2x-ie-ECU"/>
<constraint firstItem="q6k-22-i5F" firstAttribute="leading" secondItem="pnP-i1-QiM" secondAttribute="leading" constant="20" symbolic="YES" id="TQm-gM-KGy"/>
<constraint firstItem="q6k-22-i5F" firstAttribute="top" secondItem="pnP-i1-QiM" secondAttribute="top" constant="20" symbolic="YES" id="dO0-4G-6TX"/>
<constraint firstItem="kRO-hG-JPd" firstAttribute="centerY" secondItem="q6k-22-i5F" secondAttribute="centerY" id="f9p-8K-1bF"/>
<constraint firstItem="xYH-wg-hUh" firstAttribute="leading" secondItem="pnP-i1-QiM" secondAttribute="leading" constant="20" symbolic="YES" id="nZu-CX-k7N"/>
</constraints>
<connections>
<outlet property="labelDescription" destination="xYH-wg-hUh" id="TSs-OW-1hh"/>
<outlet property="labelPrice" destination="kRO-hG-JPd" id="QE3-Yk-pxQ"/>
<outlet property="labelTitle" destination="q6k-22-i5F" id="0IP-Gv-Wh0"/>
</connections>
</customView>
</prototypeCellViews>
</tableColumn>
</tableColumns>
<connections>
<outlet property="dataSource" destination="Rv5-Zx-TH3" id="QpY-8y-snp"/>
<outlet property="delegate" destination="Rv5-Zx-TH3" id="vc2-tf-15J"/>
</connections>
</tableView>
</subviews>
</clipView>
<scroller key="horizontalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="YES" id="CeE-dS-NyP">
<rect key="frame" x="1" y="329" width="400" height="16"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
<scroller key="verticalScroller" hidden="YES" wantsLayer="YES" verticalHuggingPriority="750" horizontal="NO" id="2Ip-OW-X0v">
<rect key="frame" x="224" y="17" width="15" height="102"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="TKV-I9-3N8">
<rect key="frame" x="19" y="96" width="404" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="400" id="uyO-So-B3k"/>
</constraints>
<textFieldCell key="cell" title="&lt;footer&gt;" id="thS-No-Evy">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<progressIndicator hidden="YES" maxValue="100" displayedWhenStopped="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="2hB-Jy-QEw">
<rect key="frame" x="213" y="22" width="16" height="16"/>
</progressIndicator>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="XMI-x0-24k">
<rect key="frame" x="230" y="13" width="94" height="32"/>
<buttonCell key="cell" type="push" title="&lt;restore&gt;" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="Tce-5t-Hj1">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="doRestorePurchases:" target="Rv5-Zx-TH3" id="5Nz-PR-cvY"/>
</connections>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="lBO-a6-qnj">
<rect key="frame" x="322" y="13" width="107" height="32"/>
<buttonCell key="cell" type="push" title="&lt;purchase&gt;" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="uxN-5f-y4i">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="doPurchase:" target="Rv5-Zx-TH3" id="e5O-mz-tRo"/>
</connections>
</button>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="72F-hB-DPP">
<rect key="frame" x="19" y="60" width="404" height="16"/>
<constraints>
<constraint firstAttribute="width" constant="400" id="ejH-6r-pkl"/>
</constraints>
<textFieldCell key="cell" title="&lt;restore&gt;" id="VuC-Wl-HpD">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<constraints>
<constraint firstItem="72F-hB-DPP" firstAttribute="top" secondItem="TKV-I9-3N8" secondAttribute="bottom" constant="20" id="8kK-yQ-UGc"/>
<constraint firstAttribute="bottom" secondItem="lBO-a6-qnj" secondAttribute="bottom" constant="20" symbolic="YES" id="ENp-WT-Z1C"/>
<constraint firstAttribute="trailing" secondItem="WTb-Wq-BLo" secondAttribute="trailing" constant="20" symbolic="YES" id="Ed7-QK-8qI"/>
<constraint firstItem="lBO-a6-qnj" firstAttribute="leading" secondItem="XMI-x0-24k" secondAttribute="trailing" constant="12" symbolic="YES" id="HGl-z7-xCS"/>
<constraint firstItem="XMI-x0-24k" firstAttribute="centerY" secondItem="lBO-a6-qnj" secondAttribute="centerY" id="Lkd-1j-rt9"/>
<constraint firstItem="lBO-a6-qnj" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="9w7-b6-jXh" secondAttribute="leading" id="PAx-YO-dub"/>
<constraint firstItem="TKV-I9-3N8" firstAttribute="trailing" secondItem="9w7-b6-jXh" secondAttribute="trailing" id="QMH-Dc-Vbo"/>
<constraint firstItem="XMI-x0-24k" firstAttribute="leading" secondItem="2hB-Jy-QEw" secondAttribute="trailing" constant="8" symbolic="YES" id="Rlz-6Y-GTq"/>
<constraint firstItem="WTb-Wq-BLo" firstAttribute="leading" secondItem="8QZ-37-H7U" secondAttribute="leading" constant="20" symbolic="YES" id="UZT-z0-YQI"/>
<constraint firstItem="72F-hB-DPP" firstAttribute="leading" secondItem="TKV-I9-3N8" secondAttribute="leading" id="ZI9-mi-dTu"/>
<constraint firstAttribute="trailing" secondItem="lBO-a6-qnj" secondAttribute="trailing" constant="20" symbolic="YES" id="Ztx-Qh-mHp"/>
<constraint firstItem="lBO-a6-qnj" firstAttribute="top" secondItem="72F-hB-DPP" secondAttribute="bottom" constant="20" id="agT-yo-xZF"/>
<constraint firstItem="TKV-I9-3N8" firstAttribute="top" secondItem="WTb-Wq-BLo" secondAttribute="bottom" constant="8" symbolic="YES" id="kzd-et-rve"/>
<constraint firstItem="72F-hB-DPP" firstAttribute="trailing" secondItem="TKV-I9-3N8" secondAttribute="trailing" id="m9x-V7-HTG"/>
<constraint firstItem="2hB-Jy-QEw" firstAttribute="centerY" secondItem="lBO-a6-qnj" secondAttribute="centerY" id="mX6-db-4Tp"/>
<constraint firstItem="TKV-I9-3N8" firstAttribute="leading" secondItem="9w7-b6-jXh" secondAttribute="leading" id="pNd-TR-JzE"/>
<constraint firstItem="WTb-Wq-BLo" firstAttribute="top" secondItem="8QZ-37-H7U" secondAttribute="top" constant="20" symbolic="YES" id="q88-zV-efL"/>
</constraints>
</view>
<connections>
<outlet property="activityPurchase" destination="2hB-Jy-QEw" id="WcK-o8-BZZ"/>
<outlet property="buttonPurchase" destination="lBO-a6-qnj" id="m5t-5u-goQ"/>
<outlet property="buttonRestore" destination="XMI-x0-24k" id="Xhd-Ph-uUY"/>
<outlet property="labelFooter" destination="TKV-I9-3N8" id="DTN-XY-TSD"/>
<outlet property="labelRestore" destination="72F-hB-DPP" id="rnt-Zy-gEz"/>
<outlet property="tableView" destination="9w7-b6-jXh" id="0Ju-MH-sbN"/>
</connections>
</viewController>
<customObject id="GVf-vI-DWL" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="428" y="-9"/>
</scene>
</scenes>
</document>

View File

@ -1220,7 +1220,7 @@ DQ
<font key="font" metaFont="system"/> <font key="font" metaFont="system"/>
</buttonCell> </buttonCell>
<connections> <connections>
<segue destination="KOf-Ss-PtI" kind="sheet" id="131-KX-EPr"/> <segue destination="KOf-Ss-PtI" kind="sheet" identifier="TrustedNetworkAddSegueIdentifier" id="131-KX-EPr"/>
</connections> </connections>
</button> </button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="C9Y-vu-Z9f"> <button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="C9Y-vu-Z9f">

View File

@ -24,6 +24,7 @@
// //
import Cocoa import Cocoa
import PassepartoutCore
class Macros { class Macros {
static func warning(_ title: String, _ message: String) -> NSAlert { static func warning(_ title: String, _ message: String) -> NSAlert {
@ -89,6 +90,15 @@ extension NSAlert {
} }
} }
extension NSViewController {
func presentPurchaseScreen(forProduct product: Product, delegate: PurchaseViewControllerDelegate? = nil) {
let vc = StoryboardScene.Purchase.initialScene.instantiate()
vc.feature = product
vc.delegate = delegate
presentAsModalWindow(vc)
}
}
extension NSView { extension NSView {
static func get<T: NSView>() -> T { static func get<T: NSView>() -> T {
let name = String(describing: T.self) let name = String(describing: T.self)

View File

@ -24,6 +24,11 @@ internal enum StoryboardScene {
internal static let initialScene = InitialSceneType<PreferencesViewController>(storyboard: Preferences.self) internal static let initialScene = InitialSceneType<PreferencesViewController>(storyboard: Preferences.self)
} }
internal enum Purchase: StoryboardType {
internal static let storyboardName = "Purchase"
internal static let initialScene = InitialSceneType<PurchaseViewController>(storyboard: Purchase.self)
}
internal enum Service: StoryboardType { internal enum Service: StoryboardType {
internal static let storyboardName = "Service" internal static let storyboardName = "Service"

View File

@ -19,6 +19,7 @@ internal enum StoryboardSegue {
internal enum Service: String, SegueType { internal enum Service: String, SegueType {
case accountSegueIdentifier = "AccountSegueIdentifier" case accountSegueIdentifier = "AccountSegueIdentifier"
case customizeSegueIdentifier = "CustomizeSegueIdentifier" case customizeSegueIdentifier = "CustomizeSegueIdentifier"
case trustedNetworkAddSegueIdentifier = "TrustedNetworkAddSegueIdentifier"
} }
} }
// swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name // swiftlint:enable explicit_type_interface identifier_name line_length type_body_length type_name

View File

@ -763,6 +763,30 @@ internal enum L10n {
} }
} }
} }
internal enum Purchase {
/// Purchase
internal static let title = L10n.tr("Core", "purchase.title")
internal enum Cells {
internal enum FullVersion {
/// - All providers (including future ones)\n%@
internal static func extraDescription(_ p1: Any) -> String {
return L10n.tr("Core", "purchase.cells.full_version.extra_description", String(describing: p1))
}
}
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("Core", "purchase.cells.restore.description")
/// Restore purchases
internal static let title = L10n.tr("Core", "purchase.cells.restore.title")
}
}
internal enum Sections {
internal enum Products {
/// Every product is a one-time purchase. Provider purchases do not include a VPN subscription.
internal static let footer = L10n.tr("Core", "purchase.sections.products.footer")
}
}
}
internal enum Reddit { internal enum Reddit {
/// Did you know that Passepartout has a subreddit? Subscribe for updates or to discuss issues, features, new platforms or whatever you like.\n\nIt's also a great way to show you care about this project. /// Did you know that Passepartout has a subreddit? Subscribe for updates or to discuss issues, features, new platforms or whatever you like.\n\nIt's also a great way to show you care about this project.
internal static let message = L10n.tr("Core", "reddit.message") internal static let message = L10n.tr("Core", "reddit.message")

View File

@ -131,28 +131,32 @@ class StatusMenu: NSObject {
let menuSupport = NSMenu() let menuSupport = NSMenu()
let itemCommunity = NSMenuItem(title: L10n.Core.Organizer.Cells.JoinCommunity.caption.asContinuation, action: #selector(joinCommunity), keyEquivalent: "") let itemCommunity = NSMenuItem(title: L10n.Core.Organizer.Cells.JoinCommunity.caption.asContinuation, action: #selector(joinCommunity), keyEquivalent: "")
let itemReview = NSMenuItem(title: L10n.Core.Organizer.Cells.WriteReview.caption.asContinuation, action: #selector(writeReview), keyEquivalent: "")
// let itemDonate = NSMenuItem(title: L10n.Core.Organizer.Cells.Donate.caption.asContinuation, action: #selector(showDonations), keyEquivalent: "") // let itemDonate = NSMenuItem(title: L10n.Core.Organizer.Cells.Donate.caption.asContinuation, action: #selector(showDonations), keyEquivalent: "")
// let itemGitHubSponsors = NSMenuItem(title: L10n.Core.Organizer.Cells.GithubSponsors.caption.asContinuation, action: #selector(seeGitHubSponsors), keyEquivalent: "") // let itemGitHubSponsors = NSMenuItem(title: L10n.Core.Organizer.Cells.GithubSponsors.caption.asContinuation, action: #selector(seeGitHubSponsors), keyEquivalent: "")
// let itemTranslate = NSMenuItem(title: L10n.Core.Organizer.Cells.Translate.caption.asContinuation, action: #selector(offerToTranslate), keyEquivalent: "") // let itemTranslate = NSMenuItem(title: L10n.Core.Organizer.Cells.Translate.caption.asContinuation, action: #selector(offerToTranslate), keyEquivalent: "")
let itemFAQ = NSMenuItem(title: L10n.Core.About.Cells.Faq.caption.asContinuation, action: #selector(visitFAQ), keyEquivalent: "") let itemFAQ = NSMenuItem(title: L10n.Core.About.Cells.Faq.caption.asContinuation, action: #selector(visitFAQ), keyEquivalent: "")
let itemReport = NSMenuItem(title: L10n.Core.Service.Cells.ReportIssue.caption.asContinuation, action: #selector(reportConnectivityIssue), keyEquivalent: "")
itemCommunity.target = self itemCommunity.target = self
itemReview.target = self
// itemDonate.target = self // itemDonate.target = self
// itemGitHubSponsors.target = self // itemGitHubSponsors.target = self
// itemTranslate.target = self // itemTranslate.target = self
itemFAQ.target = self itemFAQ.target = self
itemReport.target = self
// menuSupport.addItem(itemDonate) // menuSupport.addItem(itemDonate)
menuSupport.addItem(itemCommunity) menuSupport.addItem(itemCommunity)
// menuSupport.addItem(.separator()) // menuSupport.addItem(.separator())
// menuSupport.addItem(itemGitHubSponsors) // menuSupport.addItem(itemGitHubSponsors)
// menuSupport.addItem(itemTranslate) // menuSupport.addItem(itemTranslate)
menuSupport.addItem(itemReview) if ProductManager.shared.isEligibleForFeedback() {
let itemReview = NSMenuItem(title: L10n.Core.Organizer.Cells.WriteReview.caption.asContinuation, action: #selector(writeReview), keyEquivalent: "")
itemReview.target = self
menuSupport.addItem(itemReview)
}
menuSupport.addItem(.separator()) menuSupport.addItem(.separator())
menuSupport.addItem(itemFAQ) menuSupport.addItem(itemFAQ)
menuSupport.addItem(itemReport) if ProductManager.shared.isEligibleForFeedback() {
let itemReport = NSMenuItem(title: L10n.Core.Service.Cells.ReportIssue.caption.asContinuation, action: #selector(reportConnectivityIssue), keyEquivalent: "")
itemReport.target = self
menuSupport.addItem(itemReport)
}
let itemSupport = NSMenuItem(title: L10n.App.Menu.Support.title, action: nil, keyEquivalent: "") let itemSupport = NSMenuItem(title: L10n.App.Menu.Support.title, action: nil, keyEquivalent: "")
menu.setSubmenu(menuSupport, for: itemSupport) menu.setSubmenu(menuSupport, for: itemSupport)
menu.addItem(itemSupport) menu.addItem(itemSupport)

View File

@ -88,6 +88,12 @@ class OrganizerViewController: NSViewController {
guard let item = sender as? NSMenuItem, let metadata = item.representedObject as? Infrastructure.Metadata else { guard let item = sender as? NSMenuItem, let metadata = item.representedObject as? Infrastructure.Metadata else {
return return
} }
do {
try ProductManager.shared.verifyEligible(forProvider: metadata)
} catch {
presentPurchaseScreen(forProduct: metadata.product)
return
}
perform(segue: StoryboardSegue.Main.enterAccountSegueIdentifier, sender: metadata.name) perform(segue: StoryboardSegue.Main.enterAccountSegueIdentifier, sender: metadata.name)
} }

View File

@ -0,0 +1,49 @@
//
// PurchaseProductView.swift
// Passepartout
//
// Created by Davide De Rosa on 2/4/21.
// Copyright (c) 2021 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 Cocoa
import StoreKit
class PurchaseProductView: NSView {
@IBOutlet private weak var labelTitle: NSTextField?
@IBOutlet private weak var labelPrice: NSTextField?
@IBOutlet private weak var labelDescription: NSTextField?
func fill(product: SKProduct, customDescription: String? = nil) {
fill(
title: product.localizedTitle,
description: customDescription ?? "\(product.localizedDescription)."
)
labelPrice?.stringValue = product.localizedPrice ?? ""
}
func fill(title: String, description: String) {
labelTitle?.stringValue = title
labelDescription?.stringValue = description
labelPrice?.stringValue = ""
}
}

View File

@ -0,0 +1,274 @@
//
// PurchaseViewController.swift
// Passepartout
//
// Created by Davide De Rosa on 2/2/21.
// Copyright (c) 2021 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 Cocoa
import StoreKit
import PassepartoutCore
import SwiftyBeaver
import Convenience
private let log = SwiftyBeaver.self
protocol PurchaseViewControllerDelegate: class {
func purchaseController(_ purchaseController: PurchaseViewController, didPurchase product: Product)
}
class PurchaseViewController: NSViewController {
private struct Columns {
static let product = NSUserInterfaceItemIdentifier("ProductCellIdentifier")
}
@IBOutlet private weak var tableView: NSTableView!
@IBOutlet private weak var labelFooter: NSTextField!
@IBOutlet private weak var labelRestore: NSTextField!
@IBOutlet private weak var activityPurchase: NSProgressIndicator!
@IBOutlet private weak var buttonPurchase: NSButton!
@IBOutlet private weak var buttonRestore: NSButton!
var feature: Product!
weak var delegate: PurchaseViewControllerDelegate?
private var skFeature: SKProduct?
private var skPlatformVersion: SKProduct?
private var skFullVersion: SKProduct?
private var platformVersionExtra: String?
private var fullVersionExtra: String?
var rows: [RowType] = []
func reloadModel() {
rows = []
let pm = ProductManager.shared
if let skPlatformVersion = pm.product(withIdentifier: .fullVersion_macOS) {
self.skPlatformVersion = skPlatformVersion
rows.append(.platformVersion)
}
if let skFullVersion = pm.product(withIdentifier: .fullVersion) {
self.skFullVersion = skFullVersion
rows.append(.fullVersion)
}
if let skFeature = pm.product(withIdentifier: feature) {
self.skFeature = skFeature
rows.append(.feature)
}
let platformBulletsList: [String] = ProductManager.shared.featureProducts(excluding: [.fullVersion, .fullVersion_iOS, .fullVersion_macOS]).map {
return $0.localizedTitle
}.sortedCaseInsensitive()
let platformBullets = platformBulletsList.joined(separator: "\n")
platformVersionExtra = L10n.Core.Purchase.Cells.FullVersion.extraDescription(platformBullets)
let fullBulletsList: [String] = ProductManager.shared.featureProducts(excluding: [.fullVersion, .fullVersion_macOS]).map {
return $0.localizedTitle
}.sortedCaseInsensitive()
let fullBullets = fullBulletsList.joined(separator: "\n")
fullVersionExtra = L10n.Core.Purchase.Cells.FullVersion.extraDescription(fullBullets)
}
// MARK: NSViewController
override func viewDidLoad() {
super.viewDidLoad()
title = L10n.Core.Purchase.title
labelFooter.stringValue = L10n.Core.Purchase.Sections.Products.footer
labelRestore.stringValue = L10n.Core.Purchase.Cells.Restore.description
buttonPurchase.title = L10n.Core.Purchase.title
buttonRestore.title = L10n.Core.Purchase.Cells.Restore.title
guard let _ = feature else {
fatalError("No feature set for purchase")
}
tableView.usesAutomaticRowHeights = true
tableView.reloadData()
}
override func viewWillAppear() {
super.viewWillAppear()
view.window?.styleMask = [.closable, .titled]
}
override func viewDidAppear() {
super.viewDidAppear()
startWaiting()
ProductManager.shared.listProducts { [weak self] (_, _) in
self?.reloadModel()
self?.tableView.reloadData()
self?.stopWaiting()
}
}
// MARK: Actions
@IBAction private func doPurchase(_ sender: Any) {
guard tableView.selectedRow != -1 else {
return
}
switch rows[tableView.selectedRow] {
case .feature:
purchaseFeature()
case .platformVersion:
purchasePlatformVersion()
case .fullVersion:
purchaseFullVersion()
}
}
@IBAction private func doRestorePurchases(_ sender: Any) {
startWaiting()
ProductManager.shared.restorePurchases { [weak self] in
self?.stopWaiting()
guard $0 == nil else {
return
}
self?.dismiss(nil)
}
}
private func purchaseFeature() {
guard let sk = skFeature else {
return
}
purchase(sk)
}
private func purchasePlatformVersion() {
guard let sk = skPlatformVersion else {
return
}
purchase(sk)
}
private func purchaseFullVersion() {
guard let sk = skFullVersion else {
return
}
purchase(sk)
}
private func purchase(_ skProduct: SKProduct) {
startWaiting()
ProductManager.shared.purchase(skProduct) { [weak self] in
self?.stopWaiting()
guard $0 == .success else {
if let error = $1 {
self?.reportPurchaseError(withProduct: skProduct, error: error)
}
return
}
guard let weakSelf = self else {
return
}
let product = weakSelf.feature.matchesStoreKitProduct(skProduct) ? weakSelf.feature! : .fullVersion
weakSelf.delegate?.purchaseController(weakSelf, didPurchase: product)
self?.dismiss(nil)
}
}
private func reportPurchaseError(withProduct product: SKProduct, error: Error) {
log.error("Unable to purchase \(product): \(error)")
let alert = Macros.warning(product.localizedTitle, error.localizedDescription)
_ = alert.presentModally(withOK: L10n.Core.Global.ok, cancel: nil)
}
@objc private func close() {
dismiss(nil)
}
// MARK: Helpers
private func startWaiting() {
tableView.isEnabled = false
buttonPurchase.isEnabled = false
buttonRestore.isEnabled = false
activityPurchase.isHidden = false
activityPurchase.startAnimation(nil)
}
private func stopWaiting() {
activityPurchase.stopAnimation(nil)
tableView.isEnabled = true
buttonPurchase.isEnabled = true
buttonRestore.isEnabled = true
}
}
extension PurchaseViewController: NSTableViewDataSource, NSTableViewDelegate {
enum RowType {
case feature
case platformVersion
case fullVersion
}
func numberOfRows(in tableView: NSTableView) -> Int {
return rows.count
}
func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
guard let view = tableView.makeView(withIdentifier: Columns.product, owner: nil) as? PurchaseProductView else {
return nil
}
switch rows[row] {
case .feature:
guard let product = skFeature else {
fatalError("Loaded feature cell, yet no corresponding product?")
}
view.fill(product: product)
case .platformVersion:
guard let product = skPlatformVersion else {
fatalError("Loaded platform version cell, yet no corresponding product?")
}
view.fill(product: product, customDescription: platformVersionExtra)
case .fullVersion:
guard let product = skFullVersion else {
fatalError("Loaded full version cell, yet no corresponding product?")
}
view.fill(product: product, customDescription: fullVersionExtra)
}
return view
}
}

View File

@ -107,6 +107,13 @@ class TrustedNetworksViewController: NSViewController, ProfileCustomization {
} }
@IBAction private func toggleTrustEthernet(_ sender: Any?) { @IBAction private func toggleTrustEthernet(_ sender: Any?) {
do {
try ProductManager.shared.verifyEligibleForTrustedNetworks()
} catch {
checkTrustEthernet.state = .off
presentPurchaseScreen(forProduct: .fullVersion_macOS)
return
}
trustedNetworks.includesEthernet = (checkTrustEthernet.state == .on) trustedNetworks.includesEthernet = (checkTrustEthernet.state == .on)
delegate?.profileCustomization(self, didUpdateTrustedNetworks: trustedNetworks) delegate?.profileCustomization(self, didUpdateTrustedNetworks: trustedNetworks)
@ -122,6 +129,18 @@ class TrustedNetworksViewController: NSViewController, ProfileCustomization {
delegate?.profileCustomization(self, didUpdateTrustedNetworks: trustedNetworks) delegate?.profileCustomization(self, didUpdateTrustedNetworks: trustedNetworks)
} }
override func shouldPerformSegue(withIdentifier identifier: NSStoryboardSegue.Identifier, sender: Any?) -> Bool {
if identifier == StoryboardSegue.Service.trustedNetworkAddSegueIdentifier.rawValue {
do {
try ProductManager.shared.verifyEligibleForTrustedNetworks()
} catch {
presentPurchaseScreen(forProduct: .fullVersion_macOS)
return false
}
}
return true
}
override func prepare(for segue: NSStoryboardSegue, sender: Any?) { override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
if let addVC = segue.destinationController as? TrustedNetworksAddViewController { if let addVC = segue.destinationController as? TrustedNetworksAddViewController {
addVC.delegate = self addVC.delegate = self

View File

@ -8,9 +8,9 @@ strings:
ib: ib:
inputs: inputs:
#- Base.lproj/About.storyboard
- Base.lproj/Main.storyboard - Base.lproj/Main.storyboard
- Base.lproj/Preferences.storyboard - Base.lproj/Preferences.storyboard
- Base.lproj/Purchase.storyboard
- Base.lproj/Service.storyboard - Base.lproj/Service.storyboard
#- Base.lproj/Shortcuts.storyboard #- Base.lproj/Shortcuts.storyboard
outputs: outputs:

View File

@ -240,6 +240,12 @@
"shortcuts.edit.sections.all.header" = "Existierende Kurzbefehle"; "shortcuts.edit.sections.all.header" = "Existierende Kurzbefehle";
"purchase.title" = "Kaufen";
"purchase.sections.products.footer" = "Jedes Produkt ist ein einmaliger Kauf. Der Kauf eines Providers beinhaltet kein VPN-Abonnement.";
"purchase.cells.full_version.extra_description" = "Alle Anbieter (inklusive Zukünftige)\n%@";
"purchase.cells.restore.title" = "Einkäufe wiederherstellen";
"purchase.cells.restore.description" = "Wenn Sie diese App oder Funktion in der Vergangenheit gekauft haben, können Sie Ihre Einkäufe wiederherstellen und dieser Bildschirm wird nicht mehr angezeigt.";
"donation.title" = "Spenden"; "donation.title" = "Spenden";
"donation.sections.one_time.header" = "Einmalig"; "donation.sections.one_time.header" = "Einmalig";
"donation.sections.one_time.footer" = "Wenn du dich erkenntlich zeigen möchtest für meine Arbeit, gibt es hier ein paar Beträge die du direkt spenden kannst.\n\nDu bezahlst pro Spende nur einmal und kannst mehrmals spenden wenn du möchtest."; "donation.sections.one_time.footer" = "Wenn du dich erkenntlich zeigen möchtest für meine Arbeit, gibt es hier ein paar Beträge die du direkt spenden kannst.\n\nDu bezahlst pro Spende nur einmal und kannst mehrmals spenden wenn du möchtest.";

View File

@ -240,6 +240,12 @@
"shortcuts.edit.sections.all.header" = "Υπάρχουσες συντομεύσεις"; "shortcuts.edit.sections.all.header" = "Υπάρχουσες συντομεύσεις";
"purchase.title" = "Αγορά";
"purchase.sections.products.footer" = "Κάθε προϊόν είναι μια αγορά. Οι αγορές παρόχων δεν περιλαμβάνουν τη συνδρομή VPN.";
"purchase.cells.full_version.extra_description" = "Όλοι οι πάροχοι (περιλαμβάνονται και οι μελλοντικοί)\n%@";
"purchase.cells.restore.title" = "Επαναφορά Αγορών";
"purchase.cells.restore.description" = "Εαν αγοράσατε την εφαρμογή στο παρελθόν, μπορείτε να κάνετε επαναφορά αγορών και αυτή η οθόνη δε θα εμφανιστεί ξανά.";
"donation.title" = "Δωρεά"; "donation.title" = "Δωρεά";
"donation.sections.one_time.header" = "Μια Φορά"; "donation.sections.one_time.header" = "Μια Φορά";
"donation.sections.one_time.footer" = "Αν είστε χαρούμενη με τη δουλειά μου, εδώ είναι λίγα ποσά που μπορείτε να δώσετε αμέσως.\n\nΘα χρεωθείτε μόνο μία φορά και μπορείτε να δώσετε πολλές φορές."; "donation.sections.one_time.footer" = "Αν είστε χαρούμενη με τη δουλειά μου, εδώ είναι λίγα ποσά που μπορείτε να δώσετε αμέσως.\n\nΘα χρεωθείτε μόνο μία φορά και μπορείτε να δώσετε πολλές φορές.";

View File

@ -240,6 +240,12 @@
"shortcuts.edit.sections.all.header" = "Existing shortcuts"; "shortcuts.edit.sections.all.header" = "Existing shortcuts";
"purchase.title" = "Purchase";
"purchase.sections.products.footer" = "Every product is a one-time purchase. Provider purchases do not include a VPN subscription.";
"purchase.cells.full_version.extra_description" = "All providers (including future ones)\n%@";
"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.";
"donation.title" = "Donate"; "donation.title" = "Donate";
"donation.sections.one_time.header" = "One time"; "donation.sections.one_time.header" = "One time";
"donation.sections.one_time.footer" = "If you want to display gratitude for my free work, here are a couple amounts you can donate instantly.\n\nYou will only be charged once per donation, and you can donate multiple times."; "donation.sections.one_time.footer" = "If you want to display gratitude for my free work, here are a couple amounts you can donate instantly.\n\nYou will only be charged once per donation, and you can donate multiple times.";

View File

@ -240,6 +240,12 @@
"shortcuts.edit.sections.all.header" = "Atajos existentes"; "shortcuts.edit.sections.all.header" = "Atajos existentes";
"purchase.title" = "Compra";
"purchase.sections.products.footer" = "Cada producto es una compra única y no recurrente. La compra de un proveedor no incluye una suscripción al servicio.";
"purchase.cells.full_version.extra_description" = "Todos los proveedores (incluye los futuros)\n%@";
"purchase.cells.restore.title" = "Restaurar compras";
"purchase.cells.restore.description" = "Si compraste esta aplicación o funcionalidad anteriormente, puedes restaurar tus compras y esta pantalla no volverá a aparecer.";
"donation.title" = "Donar"; "donation.title" = "Donar";
"donation.sections.one_time.header" = "Única"; "donation.sections.one_time.header" = "Única";
"donation.sections.one_time.footer" = "Si te gusta mi trabajo, aquí puedes colaborar con una donación.\n\nSólo se te cobrará una vez por donación, y puedes donar las veces que quieras."; "donation.sections.one_time.footer" = "Si te gusta mi trabajo, aquí puedes colaborar con una donación.\n\nSólo se te cobrará una vez por donación, y puedes donar las veces que quieras.";

View File

@ -240,6 +240,12 @@
"shortcuts.edit.sections.all.header" = "Raccourcis existants"; "shortcuts.edit.sections.all.header" = "Raccourcis existants";
"purchase.title" = "Acheter";
"purchase.sections.products.footer" = "Chaque produit est un achat unique. Les achats n'incluent pas une souscription à un service de VPN.";
"purchase.cells.full_version.extra_description" = "Tous les fournisseurs (incluant les prochains)\n%@";
"purchase.cells.restore.title" = "Restaurer les achats";
"purchase.cells.restore.description" = "Si vous avez acheté l'application ou une fonctionnalité dans le passé, vous pouvez restaurer les achats et ce message ne s'affichera plus.";
"donation.title" = "Faire un don"; "donation.title" = "Faire un don";
"donation.sections.one_time.header" = "Une seule fois"; "donation.sections.one_time.header" = "Une seule fois";
"donation.sections.one_time.footer" = "Si vous voulez manifester votre gratitude envers mon travail bénévole, voici certains montants pour faire un don instantanément.\n\n Vous n'allez être chargé qu'une seule fois par don et vous pouvez faire un don plus d'une fois."; "donation.sections.one_time.footer" = "Si vous voulez manifester votre gratitude envers mon travail bénévole, voici certains montants pour faire un don instantanément.\n\n Vous n'allez être chargé qu'une seule fois par don et vous pouvez faire un don plus d'une fois.";

View File

@ -240,6 +240,12 @@
"shortcuts.edit.sections.all.header" = "Comandi esistenti"; "shortcuts.edit.sections.all.header" = "Comandi esistenti";
"purchase.title" = "Acquista";
"purchase.sections.products.footer" = "Ogni prodotto è un acquisto unico e non ricorrente. L'acquisto di un provider non include una sottoscrizione.";
"purchase.cells.full_version.extra_description" = "Tutti i provider (inclusi quelli futuri)\n%@";
"purchase.cells.restore.title" = "Ripristina acquisti";
"purchase.cells.restore.description" = "Se hai comprato quest'applicazione o funzionalità in precedenza, puoi ripristinare i tuoi acquisti in modo che questa schermata non compaia più.";
"donation.title" = "Donazione"; "donation.title" = "Donazione";
"donation.sections.one_time.header" = "Unica"; "donation.sections.one_time.header" = "Unica";
"donation.sections.one_time.footer" = "Se vuoi mostrare gratitudine per il mio lavoro a titolo gratuito, qui trovi varie somme da donare all'istante.\n\nLa donazione ti sarà addebitata solo una volta, e puoi effettuare più donazioni."; "donation.sections.one_time.footer" = "Se vuoi mostrare gratitudine per il mio lavoro a titolo gratuito, qui trovi varie somme da donare all'istante.\n\nLa donazione ti sarà addebitata solo una volta, e puoi effettuare più donazioni.";

View File

@ -240,6 +240,12 @@
"shortcuts.edit.sections.all.header" = "Bestaande snelkoppelingen"; "shortcuts.edit.sections.all.header" = "Bestaande snelkoppelingen";
"purchase.title" = "Aanschaffen";
"purchase.sections.products.footer" = "Elk product is een eenmalige aankoop. Aankopen van providers bevatten geen VPN-abonnement.";
"purchase.cells.full_version.extra_description" = "Alle providers (inclusief toekomstige)\n%@";
"purchase.cells.restore.title" = "Herstel Aankopen";
"purchase.cells.restore.description" = "Als u deze app of functie in het verleden heeft gekocht, kunt u uw aankopen herstellen en wordt dit scherm niet meer getoond.";
"donation.title" = "Donatie"; "donation.title" = "Donatie";
"donation.sections.one_time.header" = "Eenmalig"; "donation.sections.one_time.header" = "Eenmalig";
"donation.sections.one_time.footer" = "Als je dankbaarheid wilt tonen voor mijn gratis werk, zijn hier een paar bedragen die je direct kunt doneren.\n\nHet bedrag wordt slechts één keer per donatie in rekening gebracht en u kunt meerdere keren doneren."; "donation.sections.one_time.footer" = "Als je dankbaarheid wilt tonen voor mijn gratis werk, zijn hier een paar bedragen die je direct kunt doneren.\n\nHet bedrag wordt slechts één keer per donatie in rekening gebracht en u kunt meerdere keren doneren.";

View File

@ -240,6 +240,12 @@
"shortcuts.edit.sections.all.header" = "Istniejące skróty"; "shortcuts.edit.sections.all.header" = "Istniejące skróty";
"purchase.title" = "Kup";
"purchase.sections.products.footer" = "Każdy produkt to zakup jednorazowy. Kuipno usługodawcy nie zawiera subskrypcji VPN.";
"purchase.cells.full_version.extra_description" = "Wszyscy usługodawcy (włączając przyszłych)\n%@";
"purchase.cells.restore.title" = "Przywróć zakup";
"purchase.cells.restore.description" = "Jeśli kupiłeś tą aplikację lub funkcję wcześniej, możesz przywrócić swoje zakupy i ten ekran nie będzie wyświetlony ponownie.";
"donation.title" = "Dotacja"; "donation.title" = "Dotacja";
"donation.sections.one_time.header" = "Jeden raz"; "donation.sections.one_time.header" = "Jeden raz";
"donation.sections.one_time.footer" = "Jeśli chcesz docenić moją pracę, poniżej znajdziesz kilka kwot do wyboru dotacji.\n\nTwoje konto zostanie obciążone tylko raz na jedną dotację, możesz wysłać dotację kilka razy."; "donation.sections.one_time.footer" = "Jeśli chcesz docenić moją pracę, poniżej znajdziesz kilka kwot do wyboru dotacji.\n\nTwoje konto zostanie obciążone tylko raz na jedną dotację, możesz wysłać dotację kilka razy.";

View File

@ -240,6 +240,12 @@
"shortcuts.edit.sections.all.header" = "Atalhos existentes"; "shortcuts.edit.sections.all.header" = "Atalhos existentes";
"purchase.title" = "Comprar";
"purchase.sections.products.footer" = "Todo produto é uma compra única. As compras do fornecedor não incluem uma assinatura VPN.";
"purchase.cells.full_version.extra_description" = "Todos os provedores (incluindo os futuros)\n%@";
"purchase.cells.restore.title" = "Restaurar compras";
"purchase.cells.restore.description" = "Se você comprou este aplicativo ou recurso no passado, pode restaurar suas compras e essa tela não será exibida novamente.";
"donation.title" = "Doar"; "donation.title" = "Doar";
"donation.sections.one_time.header" = "Uma vez"; "donation.sections.one_time.header" = "Uma vez";
"donation.sections.one_time.footer" = "Se você deseja mostrar gratidão pelo meu trabalho, aqui estão alguns valores do qual você pode contribuir.\n\nVocé só será cobrado uma única vez, ou doar mais vezes caso desejar."; "donation.sections.one_time.footer" = "Se você deseja mostrar gratidão pelo meu trabalho, aqui estão alguns valores do qual você pode contribuir.\n\nVocé só será cobrado uma única vez, ou doar mais vezes caso desejar.";

View File

@ -240,6 +240,12 @@
"shortcuts.edit.sections.all.header" = "Существующие команды"; "shortcuts.edit.sections.all.header" = "Существующие команды";
"purchase.title" = "Покупка";
"purchase.sections.products.footer" = "Каждый продукт является разовой покупкой. Покупка провайдера не включает подписку на VPN.";
"purchase.cells.full_version.extra_description" = "Все провайдеры (включая добавленных в будущем)\n%@";
"purchase.cells.restore.title" = "Восстановить покупки";
"purchase.cells.restore.description" = "Если Вы купили это приложение или совершили встроенные покупки в прошлом, вы можете восстановить ваши покупки, и этот баннер больше не появится.";
"donation.title" = "Пожертвовать"; "donation.title" = "Пожертвовать";
"donation.sections.one_time.header" = "Один раз"; "donation.sections.one_time.header" = "Один раз";
"donation.sections.one_time.footer" = "Если Вы хотите поблагодарить мою бесплатную работу, здесь есть несколько сумм, которые Вы можете пожертвовать прямо сейчас.\n\nСумма будет списана только один раз, а Вы можете пожертвовать несколько раз."; "donation.sections.one_time.footer" = "Если Вы хотите поблагодарить мою бесплатную работу, здесь есть несколько сумм, которые Вы можете пожертвовать прямо сейчас.\n\nСумма будет списана только один раз, а Вы можете пожертвовать несколько раз.";

View File

@ -240,6 +240,12 @@
"shortcuts.edit.sections.all.header" = "Befintliga genvägar"; "shortcuts.edit.sections.all.header" = "Befintliga genvägar";
"purchase.title" = "Köp";
"purchase.sections.products.footer" = "Varje produkt är ett engångsköp. Leverantörsköp inkluderar inte ett VPN-abonnemang.";
"purchase.cells.full_version.extra_description" = "Alla leverantörer (inklusive framtida)\n%@";
"purchase.cells.restore.title" = "Återställ köp";
"purchase.cells.restore.description" = "Om du köpte den här appen eller funktionen tidigare kan du återställa dina inköp och den här skärmen visas inte igen.";
"donation.title" = "Donera"; "donation.title" = "Donera";
"donation.sections.one_time.header" = "En gång"; "donation.sections.one_time.header" = "En gång";
"donation.sections.one_time.footer" = "Om du vill visa tacksamhet för mitt fria arbete, här är några belopp du kan donera direkt. \n\nDu betalas endast en gång per donation, och du kan donera flera gånger. "; "donation.sections.one_time.footer" = "Om du vill visa tacksamhet för mitt fria arbete, här är några belopp du kan donera direkt. \n\nDu betalas endast en gång per donation, och du kan donera flera gånger. ";

View File

@ -240,6 +240,12 @@
"shortcuts.edit.sections.all.header" = "已经存在的捷径"; "shortcuts.edit.sections.all.header" = "已经存在的捷径";
"purchase.title" = "购买";
"purchase.sections.products.footer" = "每件产品都是一次性的购买。 购买的提供商并不包含VPN订阅。";
"purchase.cells.full_version.extra_description" = "所有的提供商(包括未来添加的)\n%@";
"purchase.cells.restore.title" = "恢复购买";
"purchase.cells.restore.description" = "如果你购买过此应用或其特征, 你可以恢复购买,此页面将不在显示。";
"donation.title" = "捐助"; "donation.title" = "捐助";
"donation.sections.one_time.header" = "一次性"; "donation.sections.one_time.header" = "一次性";
"donation.sections.one_time.footer" = "如果你想对我免费的工作表示感谢,这里是一些你可以捐助的数额。\n\n每次捐助只需要付款一次你可以捐助多次。"; "donation.sections.one_time.footer" = "如果你想对我免费的工作表示感谢,这里是一些你可以捐助的数额。\n\n每次捐助只需要付款一次你可以捐助多次。";

View File

@ -359,12 +359,7 @@ public class AppConstants {
static let lastFullVersionBuild = 2016 static let lastFullVersionBuild = 2016
#else #else
static var isBetaFullVersion: Bool { static let isBetaFullVersion = false
guard !ProcessInfo.processInfo.arguments.contains("FULL_VERSION") else {
return true
}
return false
}
static let lastFullVersionBuild = 0 static let lastFullVersionBuild = 0
#endif #endif

View File

@ -553,25 +553,29 @@ public class ConnectionService: Codable {
log.verbose(protocolConfiguration) log.verbose(protocolConfiguration)
var rules: [NEOnDemandRule] = [] var rules: [NEOnDemandRule] = []
#if os(iOS) do {
if profile.trustedNetworks.includesMobile { try ProductManager.shared.verifyEligibleForTrustedNetworks()
let rule = policyRule(for: profile) #if os(iOS)
rule.interfaceTypeMatch = .cellular if profile.trustedNetworks.includesMobile {
rules.append(rule) let rule = policyRule(for: profile)
} rule.interfaceTypeMatch = .cellular
#else rules.append(rule)
if profile.trustedNetworks.includesEthernet { }
let rule = policyRule(for: profile) #else
rule.interfaceTypeMatch = .ethernet if profile.trustedNetworks.includesEthernet {
rules.append(rule) let rule = policyRule(for: profile)
} rule.interfaceTypeMatch = .ethernet
#endif rules.append(rule)
let reallyTrustedWifis = Array(profile.trustedNetworks.includedWiFis.filter { $1 }.keys) }
if !reallyTrustedWifis.isEmpty { #endif
let rule = policyRule(for: profile) let reallyTrustedWifis = Array(profile.trustedNetworks.includedWiFis.filter { $1 }.keys)
rule.interfaceTypeMatch = .wiFi if !reallyTrustedWifis.isEmpty {
rule.ssidMatch = reallyTrustedWifis let rule = policyRule(for: profile)
rules.append(rule) rule.interfaceTypeMatch = .wiFi
rule.ssidMatch = reallyTrustedWifis
rules.append(rule)
}
} catch {
} }
let connection = NEOnDemandRuleConnect() let connection = NEOnDemandRuleConnect()
connection.interfaceTypeMatch = .any connection.interfaceTypeMatch = .any