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