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 */; };
0E6268942369AD0600355F75 /* PurchaseTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E6268932369AD0600355F75 /* PurchaseTableViewCell.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 */; };
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, ); }; };
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 */; };
0E9AA978259F756A003FAFF1 /* 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>"; };
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>"; };
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>"; };
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>"; };
@ -500,6 +504,8 @@
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>"; };
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>"; };
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>"; };
@ -771,6 +777,7 @@
0E569F7D259F41690022DFB8 /* Providers.xcassets */,
0E569F85259F41690022DFB8 /* Main.storyboard */,
0E569F81259F41690022DFB8 /* Preferences.storyboard */,
0E6BA54D25C9EE3A000CDFAC /* Purchase.storyboard */,
0E569F83259F41690022DFB8 /* Service.storyboard */,
);
path = macOS;
@ -801,6 +808,7 @@
isa = PBXGroup;
children = (
0E569F65259F41690022DFB8 /* Preferences */,
0E6BA54125C9ED91000CDFAC /* Purchase */,
0E569F6C259F41690022DFB8 /* Service */,
0E569F69259F41690022DFB8 /* OrganizerProfileTableView.swift */,
0E569F6A259F41690022DFB8 /* OrganizerViewController.swift */,
@ -932,6 +940,15 @@
path = iOS;
sourceTree = "<group>";
};
0E6BA54125C9ED91000CDFAC /* Purchase */ = {
isa = PBXGroup;
children = (
0E79D31D25CC0CF600D12964 /* PurchaseProductView.swift */,
0E79D2C725C9F1B300D12964 /* PurchaseViewController.swift */,
);
path = Purchase;
sourceTree = "<group>";
};
0E89DFCC213EEDE700741BA1 /* Organizer */ = {
isa = PBXGroup;
children = (
@ -1486,6 +1503,7 @@
0E52031D259F58BF00CBAB56 /* Providers.xcassets in Resources */,
0E52047D259F642600CBAB56 /* Preferences.storyboard in Resources */,
0E52038F259F593F00CBAB56 /* App.strings in Resources */,
0E6BA54B25C9EE3A000CDFAC /* Purchase.storyboard in Resources */,
0E520385259F593B00CBAB56 /* Credits.html in Resources */,
0E52047C259F642600CBAB56 /* Service.storyboard in Resources */,
0E52032B259F58DD00CBAB56 /* TextTableView.xib in Resources */,
@ -1882,6 +1900,7 @@
0E294AA225AE2B0B00CB4908 /* Descriptible.swift in Sources */,
0E520356259F590600CBAB56 /* PreferencesGeneralViewController.swift in Sources */,
0E520348259F58FE00CBAB56 /* MTUViewController.swift in Sources */,
0E79D31E25CC0CF600D12964 /* PurchaseProductView.swift in Sources */,
0E52037E259F593B00CBAB56 /* SwiftGen+Segues.swift in Sources */,
0E52037C259F593B00CBAB56 /* Theme.swift in Sources */,
0E52035E259F591300CBAB56 /* StatusMenu.swift in Sources */,
@ -1895,6 +1914,7 @@
0E520354259F590600CBAB56 /* PreferencesViewController.swift in Sources */,
0E520343259F58FE00CBAB56 /* ProfileCustomizationViewController.swift in Sources */,
0E520346259F58FE00CBAB56 /* TrustedNetworksViewController.swift in Sources */,
0E79D2C825C9F1B300D12964 /* PurchaseViewController.swift in Sources */,
0E520338259F58F500CBAB56 /* ProviderServiceView.swift in Sources */,
0E520347259F58FE00CBAB56 /* ProxyViewController.swift in Sources */,
0E52037B259F593B00CBAB56 /* IssueReporter.swift in Sources */,
@ -2247,6 +2267,14 @@
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
0E6BA54D25C9EE3A000CDFAC /* Purchase.storyboard */ = {
isa = PBXVariantGroup;
children = (
0E6BA54C25C9EE3A000CDFAC /* Base */,
);
name = Purchase.storyboard;
sourceTree = "<group>";
};
0ED38ADC213F44D00004D387 /* Organizer.storyboard */ = {
isa = PBXVariantGroup;
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 Alerts {
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 {
/// 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")

View File

@ -43,9 +43,13 @@ class PurchaseViewController: UITableViewController, StrongTableHost {
weak var delegate: PurchaseViewControllerDelegate?
private var skFeature: SKProduct?
private var skPlatformVersion: SKProduct?
private var skFullVersion: SKProduct?
private var platformVersionExtra: String?
private var fullVersionExtra: String?
// MARK: StrongTableHost
@ -55,10 +59,14 @@ class PurchaseViewController: UITableViewController, StrongTableHost {
func reloadModel() {
model.clear()
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] = []
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) {
self.skFullVersion = skFullVersion
rows.append(.fullVersion)
@ -70,11 +78,17 @@ class PurchaseViewController: UITableViewController, StrongTableHost {
rows.append(.restore)
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)"
}.sortedCaseInsensitive()
let featureBullets = featureBulletsList.joined(separator: "\n")
fullVersionExtra = L10n.App.Purchase.Cells.FullVersion.extraDescription(featureBullets)
let platformBullets = platformBulletsList.joined(separator: "\n")
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
@ -86,7 +100,7 @@ class PurchaseViewController: UITableViewController, StrongTableHost {
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))
isLoading = true
@ -113,6 +127,13 @@ class PurchaseViewController: UITableViewController, StrongTableHost {
}
purchase(sk)
}
private func purchasePlatformVersion() {
guard let sk = skPlatformVersion else {
return
}
purchase(sk)
}
private func purchaseFullVersion() {
guard let sk = skFullVersion else {
@ -174,8 +195,10 @@ extension PurchaseViewController {
enum RowType {
case feature
case fullVersion
case platformVersion
case fullVersion
case restore
}
@ -203,6 +226,12 @@ extension PurchaseViewController {
}
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:
guard let product = skFullVersion else {
fatalError("Loaded full version cell, yet no corresponding product?")
@ -211,8 +240,8 @@ extension PurchaseViewController {
case .restore:
cell.fill(
title: L10n.App.Purchase.Cells.Restore.title,
description: L10n.App.Purchase.Cells.Restore.description
title: L10n.Core.Purchase.Cells.Restore.title,
description: L10n.Core.Purchase.Cells.Restore.description
)
}
return cell
@ -225,6 +254,9 @@ extension PurchaseViewController {
case .feature:
purchaseFeature()
case .platformVersion:
purchasePlatformVersion()
case .fullVersion:
purchaseFullVersion()

View File

@ -63,9 +63,3 @@
"shortcuts.edit.title" = "Kurzbefehle bearbeiten";
"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.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.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.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.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.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.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.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.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.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.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.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()
StatusMenu.shared.install()
ProductManager.shared.reviewPurchases()
// if let appCenterSecret = appCenterSecret, !appCenterSecret.isEmpty {
// 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"/>
</buttonCell>
<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>
</button>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="C9Y-vu-Z9f">

View File

@ -24,6 +24,7 @@
//
import Cocoa
import PassepartoutCore
class Macros {
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 {
static func get<T: NSView>() -> T {
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 enum Purchase: StoryboardType {
internal static let storyboardName = "Purchase"
internal static let initialScene = InitialSceneType<PurchaseViewController>(storyboard: Purchase.self)
}
internal enum Service: StoryboardType {
internal static let storyboardName = "Service"

View File

@ -19,6 +19,7 @@ internal enum StoryboardSegue {
internal enum Service: String, SegueType {
case accountSegueIdentifier = "AccountSegueIdentifier"
case customizeSegueIdentifier = "CustomizeSegueIdentifier"
case trustedNetworkAddSegueIdentifier = "TrustedNetworkAddSegueIdentifier"
}
}
// 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 {
/// 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")

View File

@ -131,28 +131,32 @@ class StatusMenu: NSObject {
let menuSupport = NSMenu()
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 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 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
itemReview.target = self
// itemDonate.target = self
// itemGitHubSponsors.target = self
// itemTranslate.target = self
itemFAQ.target = self
itemReport.target = self
// menuSupport.addItem(itemDonate)
menuSupport.addItem(itemCommunity)
// menuSupport.addItem(.separator())
// menuSupport.addItem(itemGitHubSponsors)
// 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(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: "")
menu.setSubmenu(menuSupport, for: 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 {
return
}
do {
try ProductManager.shared.verifyEligible(forProvider: metadata)
} catch {
presentPurchaseScreen(forProduct: metadata.product)
return
}
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?) {
do {
try ProductManager.shared.verifyEligibleForTrustedNetworks()
} catch {
checkTrustEthernet.state = .off
presentPurchaseScreen(forProduct: .fullVersion_macOS)
return
}
trustedNetworks.includesEthernet = (checkTrustEthernet.state == .on)
delegate?.profileCustomization(self, didUpdateTrustedNetworks: trustedNetworks)
@ -122,6 +129,18 @@ class TrustedNetworksViewController: NSViewController, ProfileCustomization {
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?) {
if let addVC = segue.destinationController as? TrustedNetworksAddViewController {
addVC.delegate = self

View File

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

View File

@ -240,6 +240,12 @@
"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.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.";

View File

@ -240,6 +240,12 @@
"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.sections.one_time.header" = "Μια Φορά";
"donation.sections.one_time.footer" = "Αν είστε χαρούμενη με τη δουλειά μου, εδώ είναι λίγα ποσά που μπορείτε να δώσετε αμέσως.\n\nΘα χρεωθείτε μόνο μία φορά και μπορείτε να δώσετε πολλές φορές.";

View File

@ -240,6 +240,12 @@
"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.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.";

View File

@ -240,6 +240,12 @@
"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.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.";

View File

@ -240,6 +240,12 @@
"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.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.";

View File

@ -240,6 +240,12 @@
"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.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.";

View File

@ -240,6 +240,12 @@
"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.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.";

View File

@ -240,6 +240,12 @@
"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.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.";

View File

@ -240,6 +240,12 @@
"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.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.";

View File

@ -240,6 +240,12 @@
"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.sections.one_time.header" = "Один раз";
"donation.sections.one_time.footer" = "Если Вы хотите поблагодарить мою бесплатную работу, здесь есть несколько сумм, которые Вы можете пожертвовать прямо сейчас.\n\nСумма будет списана только один раз, а Вы можете пожертвовать несколько раз.";

View File

@ -240,6 +240,12 @@
"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.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. ";

View File

@ -240,6 +240,12 @@
"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.sections.one_time.header" = "一次性";
"donation.sections.one_time.footer" = "如果你想对我免费的工作表示感谢,这里是一些你可以捐助的数额。\n\n每次捐助只需要付款一次你可以捐助多次。";

View File

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

View File

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