Design purchase screen
- Required product - Full version - Restore purchases
This commit is contained in:
parent
6e46757d99
commit
1e6c5ba91b
|
@ -4,7 +4,6 @@
|
|||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
|
@ -17,7 +16,7 @@
|
|||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="mav-8y-ejl" kind="relationship" relationship="rootViewController" id="wpW-sp-YUJ"/>
|
||||
<segue destination="bQc-2A-qWz" kind="relationship" relationship="rootViewController" id="0da-C5-Txv"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cwJ-TA-B3C" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
|
@ -25,20 +24,71 @@
|
|||
<point key="canvasLocation" x="-781" y="113"/>
|
||||
</scene>
|
||||
<!--Purchase View Controller-->
|
||||
<scene sceneID="xj5-bV-pGe">
|
||||
<scene sceneID="93l-dg-vRI">
|
||||
<objects>
|
||||
<viewController id="mav-8y-ejl" customClass="PurchaseViewController" customModule="Passepartout" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="ehY-J4-Cyo">
|
||||
<tableViewController id="bQc-2A-qWz" customClass="PurchaseViewController" customModule="Passepartout" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" id="5WE-4Q-uDQ">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="8QA-Ho-NSB"/>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" id="X7r-gy-0QO"/>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="xqo-Gt-MGj" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
<prototypes>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="PurchaseTableViewCell" id="0XE-hK-4Ro" customClass="PurchaseTableViewCell" customModule="Passepartout" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="28" width="414" height="105"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="0XE-hK-4Ro" id="Fe6-eH-sBT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="105"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Dit-R5-2h7">
|
||||
<rect key="frame" x="20" y="20" width="374" height="65"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" text="Title" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Hf0-aN-JRs">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="24.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="medium" pointSize="20"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Description" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gB1-nL-LEc">
|
||||
<rect key="frame" x="0.0" y="44.5" width="374" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="gB1-nL-LEc" secondAttribute="trailing" id="062-HW-sKW"/>
|
||||
<constraint firstItem="gB1-nL-LEc" firstAttribute="leading" secondItem="Dit-R5-2h7" secondAttribute="leading" id="3je-C8-e0v"/>
|
||||
<constraint firstItem="Hf0-aN-JRs" firstAttribute="leading" secondItem="Dit-R5-2h7" secondAttribute="leading" id="7cX-Z8-Z9p"/>
|
||||
<constraint firstItem="Hf0-aN-JRs" firstAttribute="top" secondItem="Dit-R5-2h7" secondAttribute="top" id="FOS-1V-gEv"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Hf0-aN-JRs" secondAttribute="trailing" id="MQl-hH-lvW"/>
|
||||
<constraint firstItem="gB1-nL-LEc" firstAttribute="top" secondItem="Hf0-aN-JRs" secondAttribute="bottom" constant="20" id="dKT-d6-56g"/>
|
||||
<constraint firstAttribute="bottom" secondItem="gB1-nL-LEc" secondAttribute="bottom" id="h1s-nk-2kw"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="Dit-R5-2h7" secondAttribute="trailing" constant="20" id="Kzi-2M-P4i"/>
|
||||
<constraint firstItem="Dit-R5-2h7" firstAttribute="leading" secondItem="Fe6-eH-sBT" secondAttribute="leading" constant="20" id="M5n-u9-S3P"/>
|
||||
<constraint firstItem="Dit-R5-2h7" firstAttribute="top" secondItem="Fe6-eH-sBT" secondAttribute="top" constant="20" id="xZt-JQ-Zyk"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Dit-R5-2h7" secondAttribute="bottom" constant="20" id="zfx-7a-jXF"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<connections>
|
||||
<outlet property="labelDescription" destination="gB1-nL-LEc" id="zIn-g5-Rl0"/>
|
||||
<outlet property="labelTitle" destination="Hf0-aN-JRs" id="E65-5C-zZR"/>
|
||||
</connections>
|
||||
</tableViewCell>
|
||||
</prototypes>
|
||||
<sections/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="bQc-2A-qWz" id="WNX-QW-EjN"/>
|
||||
<outlet property="delegate" destination="bQc-2A-qWz" id="oqL-qc-xkY"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" id="azV-cT-GZs"/>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="VLq-IA-G4Y" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="23" y="113"/>
|
||||
<point key="canvasLocation" x="87" y="113"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -78,6 +78,18 @@ internal enum L10n {
|
|||
}
|
||||
}
|
||||
}
|
||||
internal enum Purchase {
|
||||
/// Purchase
|
||||
internal static let title = L10n.tr("App", "purchase.title")
|
||||
internal enum Cells {
|
||||
internal enum Restore {
|
||||
/// If you bought this app or feature in the past, you can restore your purchases and this screen won't show again.
|
||||
internal static let description = L10n.tr("App", "purchase.cells.restore.description")
|
||||
/// Restore purchases
|
||||
internal static let title = L10n.tr("App", "purchase.cells.restore.title")
|
||||
}
|
||||
}
|
||||
}
|
||||
internal enum Service {
|
||||
internal enum Alerts {
|
||||
internal enum Location {
|
||||
|
|
|
@ -130,6 +130,10 @@ extension UILabel {
|
|||
func applyLight(_ theme: Theme) {
|
||||
textColor = theme.palette.primaryLightText
|
||||
}
|
||||
|
||||
func applyAccent(_ theme: Theme) {
|
||||
textColor = theme.palette.accent1
|
||||
}
|
||||
}
|
||||
|
||||
extension UIButton {
|
||||
|
|
|
@ -59,3 +59,7 @@
|
|||
|
||||
"shortcuts.edit.title" = "Manage shortcuts";
|
||||
"shortcuts.edit.cells.add_shortcut.caption" = "Add shortcut";
|
||||
|
||||
"purchase.title" = "Purchase";
|
||||
"purchase.cells.restore.title" = "Restore purchases";
|
||||
"purchase.cells.restore.description" = "If you bought this app or feature in the past, you can restore your purchases and this screen won't show again.";
|
||||
|
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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<SectionType, RowType> = StrongTableModel()
|
||||
|
||||
func reloadModel() {
|
||||
model.clear()
|
||||
model.add(.products)
|
||||
|
||||
var rows: [RowType] = []
|
||||
let pm = ProductManager.shared
|
||||
if let skFeature = pm.product(withIdentifier: feature) {
|
||||
self.skFeature = skFeature
|
||||
rows.append(.feature)
|
||||
}
|
||||
if let skFullVersion = pm.product(withIdentifier: .fullVersion) {
|
||||
self.skFullVersion = skFullVersion
|
||||
rows.append(.fullVersion)
|
||||
}
|
||||
rows.append(.restore)
|
||||
model.set(rows, forSection: .products)
|
||||
}
|
||||
|
||||
// MARK: UIViewController
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
guard let _ = feature else {
|
||||
fatalError("No feature set for purchase")
|
||||
}
|
||||
|
||||
title = L10n.App.Purchase.title
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
0E5E5DE1215119DD00E318A3 /* VPNConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNConfiguration.swift; sourceTree = "<group>"; };
|
||||
0E5E5DE421511C5F00E318A3 /* GracefulVPN.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GracefulVPN.swift; sourceTree = "<group>"; };
|
||||
0E6268932369AD0600355F75 /* PurchaseTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PurchaseTableViewCell.swift; sourceTree = "<group>"; };
|
||||
0E66A26F225FE25800F9C779 /* PoolCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = PoolCategory.swift; path = ../Model/Profiles/PoolCategory.swift; sourceTree = "<group>"; };
|
||||
0E6ACB7722B1A57C001B3C99 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Intents.strings; sourceTree = "<group>"; };
|
||||
0E6ACB7822B1A5BB001B3C99 /* pt */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = pt; path = pt.lproj/Core.strings; sourceTree = "<group>"; };
|
||||
|
@ -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 */,
|
||||
|
|
Loading…
Reference in New Issue