passepartout-apple/Passepartout/App/Views/PaywallView+Purchase.swift

294 lines
7.9 KiB
Swift
Raw Normal View History

2022-04-12 13:09:14 +00:00
//
// PaywallView+Purchase.swift
// Passepartout
//
// Created by Davide De Rosa on 3/12/22.
2023-03-17 15:56:19 +00:00
// Copyright (c) 2023 Davide De Rosa. All rights reserved.
2022-04-12 13:09:14 +00:00
//
// 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/>.
//
2022-06-23 21:31:01 +00:00
import PassepartoutLibrary
import StoreKit
import SwiftUI
2022-04-12 13:09:14 +00:00
extension PaywallView {
struct PurchaseView: View {
fileprivate enum PurchaseState {
case purchasing(SKProduct)
case restoring
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
@Environment(\.scenePhase) private var scenePhase
@ObservedObject private var productManager: ProductManager
@Binding private var isPresented: Bool
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
private let feature: LocalProduct?
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
@State private var purchaseState: PurchaseState?
init(isPresented: Binding<Bool>, feature: LocalProduct? = nil) {
2022-04-12 13:09:14 +00:00
productManager = .shared
_isPresented = isPresented
2022-04-12 13:09:14 +00:00
self.feature = feature
}
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
var body: some View {
List {
productsSection
.disabled(purchaseState != nil)
}.navigationTitle(Unlocalized.appName)
// reloading
.onAppear {
productManager.refreshProducts()
}.onChange(of: scenePhase) { newValue in
if newValue == .active {
productManager.refreshProducts()
}
2022-04-23 10:08:24 +00:00
}.themeAnimation(on: productManager.isRefreshingProducts)
2022-04-12 13:09:14 +00:00
}
}
}
2023-03-17 20:55:47 +00:00
private struct PurchaseRow: View {
var product: SKProduct?
2022-04-12 13:09:14 +00:00
let title: String
2023-03-17 20:55:47 +00:00
let extra: String?
2022-04-12 13:09:14 +00:00
let action: () -> Void
2022-04-12 13:09:14 +00:00
let purchaseState: PaywallView.PurchaseView.PurchaseState?
2023-03-17 20:55:47 +00:00
var body: some View {
VStack(alignment: .leading) {
actionButton
.padding(.bottom, 5)
2022-04-12 13:09:14 +00:00
extra.map {
Text($0)
.frame(maxHeight: .infinity)
2022-04-12 13:09:14 +00:00
}
}.padding([.top, .bottom])
2022-04-12 13:09:14 +00:00
}
}
2023-03-17 20:55:47 +00:00
private typealias RowModel = (product: SKProduct, extra: String?)
2022-04-12 13:09:14 +00:00
// MARK: -
private extension PaywallView.PurchaseView {
var productsSection: some View {
Section {
if !productManager.isRefreshingProducts {
ForEach(productRowModels, id: \.product.productIdentifier, content: productRow)
} else {
ProgressView()
2022-04-12 13:09:14 +00:00
}
restoreRow
} header: {
Text(L10n.Paywall.title)
} footer: {
Text(L10n.Paywall.Sections.Products.footer)
2022-04-12 13:09:14 +00:00
}
}
func productRow(_ model: RowModel) -> some View {
PurchaseRow(
product: model.product,
title: model.product.localizedTitle,
extra: model.extra,
action: {
purchaseProduct(model.product)
},
purchaseState: purchaseState
)
}
var restoreRow: some View {
PurchaseRow(
title: L10n.Paywall.Items.Restore.title,
extra: L10n.Paywall.Items.Restore.description,
action: restorePurchases,
purchaseState: purchaseState
)
}
2022-04-12 13:09:14 +00:00
}
private extension PaywallView.PurchaseView {
var skFeature: SKProduct? {
2022-04-12 13:09:14 +00:00
guard let feature = feature else {
return nil
}
return productManager.product(withIdentifier: feature)
}
2023-03-17 20:55:47 +00:00
var skPlatformVersion: SKProduct? {
#if targetEnvironment(macCatalyst)
2022-09-04 18:09:31 +00:00
productManager.product(withIdentifier: .fullVersion_macOS)
#else
2022-09-04 18:09:31 +00:00
productManager.product(withIdentifier: .fullVersion_iOS)
2022-04-12 13:09:14 +00:00
#endif
}
// hide full version if already bought the other platform version
var skFullVersion: SKProduct? {
#if targetEnvironment(macCatalyst)
guard !productManager.hasPurchased(.fullVersion_iOS) else {
2022-04-12 13:09:14 +00:00
return nil
}
#else
guard !productManager.hasPurchased(.fullVersion_macOS) else {
2022-04-12 13:09:14 +00:00
return nil
}
#endif
return productManager.product(withIdentifier: .fullVersion)
}
var platformVersionExtra: [String] {
2022-09-04 18:09:31 +00:00
productManager.featureProducts(excluding: [
2022-04-12 13:09:14 +00:00
.fullVersion,
.fullVersion_iOS,
.fullVersion_macOS
]).map {
$0.localizedTitle
}.sorted {
$0.lowercased() < $1.lowercased()
}
}
var fullVersionExtra: [String] {
2022-09-04 18:09:31 +00:00
productManager.featureProducts(including: [
2022-04-12 13:09:14 +00:00
.fullVersion_iOS,
.fullVersion_macOS
]).map {
$0.localizedTitle
}.sorted {
$0.lowercased() < $1.lowercased()
}
}
var productRowModels: [RowModel] {
2022-04-12 13:09:14 +00:00
var models: [RowModel] = []
skPlatformVersion.map {
let extra = platformVersionExtra.joined(separator: "\n")
models.append(($0, extra))
}
skFullVersion.map {
let extra = fullVersionExtra.joined(separator: "\n")
models.append(($0, extra))
}
skFeature.map {
models.append(($0, nil))
}
return models
}
}
private extension PurchaseRow {
2023-03-17 20:55:47 +00:00
2022-04-12 13:09:14 +00:00
@ViewBuilder
var actionButton: some View {
2022-04-12 13:09:14 +00:00
if let product = product {
purchaseButton(product)
} else {
restoreButton
}
}
2023-03-17 20:55:47 +00:00
func purchaseButton(_ product: SKProduct) -> some View {
2022-04-12 13:09:14 +00:00
HStack {
Button(title, action: action)
Spacer()
if case .purchasing(let pending) = purchaseState, pending.productIdentifier == product.productIdentifier {
ProgressView()
} else {
product.localizedPrice.map {
Text($0)
.themeSecondaryTextStyle()
2022-04-12 13:09:14 +00:00
}
}
}
}
2023-03-17 20:55:47 +00:00
var restoreButton: some View {
2022-04-12 13:09:14 +00:00
HStack {
Button(title, action: action)
Spacer()
if case .restoring = purchaseState {
ProgressView()
}
}
}
}
// MARK: -
private extension PaywallView.PurchaseView {
func purchaseProduct(_ product: SKProduct) {
purchaseState = .purchasing(product)
productManager.purchase(product) {
switch $0 {
case .success(let result):
switch result {
case .done:
isPresented = false
case .cancelled:
break
}
purchaseState = nil
case .failure(let error):
pp_log.error("Unable to purchase: \(error)")
ErrorHandler.shared.handle(
title: product.localizedTitle,
message: AppError(error).localizedDescription
) {
purchaseState = nil
}
}
}
}
func restorePurchases() {
purchaseState = .restoring
productManager.restorePurchases {
if let error = $0 {
pp_log.error("Unable to restore purchases: \(error)")
ErrorHandler.shared.handle(
title: L10n.Paywall.Items.Restore.title,
message: AppError(error).localizedDescription
) {
purchaseState = nil
}
return
}
isPresented = false
purchaseState = nil
}
}
}