Use async in ProductManager (#438)
Drop legacy completion handlers. Push `Task` to the views. Also: - Group library tests in a test plan - Fix a broken library dependency
This commit is contained in:
parent
a0da930d98
commit
1551b59f21
|
@ -492,6 +492,7 @@
|
||||||
0ED89C1627DE0E05008B36D6 /* IntentEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentEditView.swift; sourceTree = "<group>"; };
|
0ED89C1627DE0E05008B36D6 /* IntentEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentEditView.swift; sourceTree = "<group>"; };
|
||||||
0ED89C1B27DE3ABC008B36D6 /* ShortcutsView+Add.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShortcutsView+Add.swift"; sourceTree = "<group>"; };
|
0ED89C1B27DE3ABC008B36D6 /* ShortcutsView+Add.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ShortcutsView+Add.swift"; sourceTree = "<group>"; };
|
||||||
0ED89C1D27DE3F8D008B36D6 /* IntentAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentAddView.swift; sourceTree = "<group>"; };
|
0ED89C1D27DE3F8D008B36D6 /* IntentAddView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntentAddView.swift; sourceTree = "<group>"; };
|
||||||
|
0EDCEF692B337BEB0023A7FF /* PassepartoutLibrary.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = PassepartoutLibrary.xctestplan; sourceTree = "<group>"; };
|
||||||
0EDDEC7C28D0DC130017802E /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
0EDDEC7C28D0DC130017802E /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
0EDE02C127F61C79000FBE3C /* EditableTextList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableTextList.swift; sourceTree = "<group>"; };
|
0EDE02C127F61C79000FBE3C /* EditableTextList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditableTextList.swift; sourceTree = "<group>"; };
|
||||||
0EDE8DBF20C86910004C739C /* PassepartoutOpenVPNTunnel.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PassepartoutOpenVPNTunnel.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
0EDE8DBF20C86910004C739C /* PassepartoutOpenVPNTunnel.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = PassepartoutOpenVPNTunnel.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
@ -804,6 +805,7 @@
|
||||||
children = (
|
children = (
|
||||||
0EE315DB2733104700F5D461 /* Packages */,
|
0EE315DB2733104700F5D461 /* Packages */,
|
||||||
0E23B4A12298559800304C30 /* Config.xcconfig */,
|
0E23B4A12298559800304C30 /* Config.xcconfig */,
|
||||||
|
0EDCEF692B337BEB0023A7FF /* PassepartoutLibrary.xctestplan */,
|
||||||
0E9AA982259F7674003FAFF1 /* Passepartout */,
|
0E9AA982259F7674003FAFF1 /* Passepartout */,
|
||||||
0E57F63920C83FC5008323CF /* Products */,
|
0E57F63920C83FC5008323CF /* Products */,
|
||||||
374B9F085E1148C37CF9117A /* Frameworks */,
|
374B9F085E1148C37CF9117A /* Frameworks */,
|
||||||
|
@ -1131,7 +1133,7 @@
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
LastSwiftUpdateCheck = 1510;
|
LastSwiftUpdateCheck = 1510;
|
||||||
LastUpgradeCheck = 1430;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "Davide De Rosa";
|
ORGANIZATIONNAME = "Davide De Rosa";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
0E41BD96286711C3006346B4 = {
|
0E41BD96286711C3006346B4 = {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1510"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1510"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1510"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|
|
@ -67,13 +67,17 @@ struct DonateView: View {
|
||||||
)
|
)
|
||||||
|
|
||||||
// reloading
|
// reloading
|
||||||
.onAppear {
|
.task {
|
||||||
productManager.refreshProducts()
|
await productManager.refreshProducts()
|
||||||
}.onChange(of: scenePhase) { newValue in
|
|
||||||
if newValue == .active {
|
|
||||||
productManager.refreshProducts()
|
|
||||||
}
|
}
|
||||||
}.themeAnimation(on: productManager.isRefreshingProducts)
|
.onChange(of: scenePhase) { newValue in
|
||||||
|
if newValue == .active {
|
||||||
|
Task {
|
||||||
|
await productManager.refreshProducts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.themeAnimation(on: productManager.isRefreshingProducts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,25 +151,32 @@ private extension ProductManager {
|
||||||
private extension DonateView {
|
private extension DonateView {
|
||||||
func purchaseProduct(_ product: InAppProduct) {
|
func purchaseProduct(_ product: InAppProduct) {
|
||||||
pendingDonationIdentifier = product.productIdentifier
|
pendingDonationIdentifier = product.productIdentifier
|
||||||
productManager.purchase(product, completionHandler: handlePurchaseResult)
|
Task {
|
||||||
|
do {
|
||||||
|
let result = try await productManager.purchase(product)
|
||||||
|
handlePurchaseResult(result)
|
||||||
|
} catch {
|
||||||
|
handlePurchaseError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handlePurchaseResult(_ result: Result<InAppPurchaseResult, Error>) {
|
func handlePurchaseResult(_ result: InAppPurchaseResult) {
|
||||||
switch result {
|
if case .done = result {
|
||||||
case .success(let value):
|
|
||||||
if case .done = value {
|
|
||||||
alertType = .thankYou
|
alertType = .thankYou
|
||||||
isAlertPresented = true
|
isAlertPresented = true
|
||||||
} else {
|
} else {
|
||||||
// cancelled
|
// cancelled
|
||||||
}
|
}
|
||||||
|
pendingDonationIdentifier = nil
|
||||||
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
func handlePurchaseError(_ error: Error) {
|
||||||
ErrorHandler.shared.handle(
|
ErrorHandler.shared.handle(
|
||||||
title: L10n.Donate.title,
|
title: L10n.Donate.title,
|
||||||
message: L10n.Donate.Alerts.Purchase.Failure.message(AppError(error).localizedDescription)
|
message: L10n.Donate.Alerts.Purchase.Failure.message(AppError(error).localizedDescription)
|
||||||
)
|
)
|
||||||
}
|
|
||||||
pendingDonationIdentifier = nil
|
pendingDonationIdentifier = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -57,13 +57,17 @@ extension PaywallView {
|
||||||
}.navigationTitle(Unlocalized.appName)
|
}.navigationTitle(Unlocalized.appName)
|
||||||
|
|
||||||
// reloading
|
// reloading
|
||||||
.onAppear {
|
.task {
|
||||||
productManager.refreshProducts()
|
await productManager.refreshProducts()
|
||||||
}.onChange(of: scenePhase) { newValue in
|
|
||||||
if newValue == .active {
|
|
||||||
productManager.refreshProducts()
|
|
||||||
}
|
}
|
||||||
}.themeAnimation(on: productManager.isRefreshingProducts)
|
.onChange(of: scenePhase) { newValue in
|
||||||
|
if newValue == .active {
|
||||||
|
Task {
|
||||||
|
await productManager.refreshProducts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.themeAnimation(on: productManager.isRefreshingProducts)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -247,9 +251,9 @@ private extension PaywallView.PurchaseView {
|
||||||
func purchaseProduct(_ product: InAppProduct) {
|
func purchaseProduct(_ product: InAppProduct) {
|
||||||
purchaseState = .purchasing(product)
|
purchaseState = .purchasing(product)
|
||||||
|
|
||||||
productManager.purchase(product) {
|
Task {
|
||||||
switch $0 {
|
do {
|
||||||
case .success(let result):
|
let result = try await productManager.purchase(product)
|
||||||
switch result {
|
switch result {
|
||||||
case .done:
|
case .done:
|
||||||
isPresented = false
|
isPresented = false
|
||||||
|
@ -258,8 +262,7 @@ private extension PaywallView.PurchaseView {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
purchaseState = nil
|
purchaseState = nil
|
||||||
|
} catch {
|
||||||
case .failure(let error):
|
|
||||||
pp_log.error("Unable to purchase: \(error)")
|
pp_log.error("Unable to purchase: \(error)")
|
||||||
ErrorHandler.shared.handle(
|
ErrorHandler.shared.handle(
|
||||||
title: product.localizedTitle,
|
title: product.localizedTitle,
|
||||||
|
@ -274,8 +277,12 @@ private extension PaywallView.PurchaseView {
|
||||||
func restorePurchases() {
|
func restorePurchases() {
|
||||||
purchaseState = .restoring
|
purchaseState = .restoring
|
||||||
|
|
||||||
productManager.restorePurchases {
|
Task {
|
||||||
if let error = $0 {
|
do {
|
||||||
|
try await productManager.restorePurchases()
|
||||||
|
isPresented = false
|
||||||
|
purchaseState = nil
|
||||||
|
} catch {
|
||||||
pp_log.error("Unable to restore purchases: \(error)")
|
pp_log.error("Unable to restore purchases: \(error)")
|
||||||
ErrorHandler.shared.handle(
|
ErrorHandler.shared.handle(
|
||||||
title: L10n.Paywall.Items.Restore.title,
|
title: L10n.Paywall.Items.Restore.title,
|
||||||
|
@ -283,10 +290,7 @@ private extension PaywallView.PurchaseView {
|
||||||
) {
|
) {
|
||||||
purchaseState = nil
|
purchaseState = nil
|
||||||
}
|
}
|
||||||
return
|
}
|
||||||
}
|
|
||||||
isPresented = false
|
|
||||||
purchaseState = nil
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,8 @@
|
||||||
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
|
||||||
//
|
//
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import PassepartoutLibrary
|
import PassepartoutLibrary
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
struct PaywallView: View {
|
struct PaywallView: View {
|
||||||
@ObservedObject private var productManager: ProductManager
|
@ObservedObject private var productManager: ProductManager
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
{
|
||||||
|
"configurations" : [
|
||||||
|
{
|
||||||
|
"id" : "848BBF4E-F054-4BFC-A034-AD5C49863245",
|
||||||
|
"name" : "Test Scheme Action",
|
||||||
|
"options" : {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"defaultOptions" : {
|
||||||
|
"codeCoverage" : false
|
||||||
|
},
|
||||||
|
"testTargets" : [
|
||||||
|
{
|
||||||
|
"target" : {
|
||||||
|
"containerPath" : "container:",
|
||||||
|
"identifier" : "PassepartoutCoreTests",
|
||||||
|
"name" : "PassepartoutCoreTests"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target" : {
|
||||||
|
"containerPath" : "container:",
|
||||||
|
"identifier" : "PassepartoutFrontendTests",
|
||||||
|
"name" : "PassepartoutFrontendTests"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target" : {
|
||||||
|
"containerPath" : "container:",
|
||||||
|
"identifier" : "PassepartoutProvidersTests",
|
||||||
|
"name" : "PassepartoutProvidersTests"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target" : {
|
||||||
|
"containerPath" : "container:",
|
||||||
|
"identifier" : "PassepartoutServicesTests",
|
||||||
|
"name" : "PassepartoutServicesTests"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 1
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1510"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|
|
@ -26,8 +26,13 @@
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
shouldAutocreateTestPlan = "YES">
|
<TestPlans>
|
||||||
|
<TestPlanReference
|
||||||
|
reference = "container:../PassepartoutLibrary.xctestplan"
|
||||||
|
default = "YES">
|
||||||
|
</TestPlanReference>
|
||||||
|
</TestPlans>
|
||||||
</TestAction>
|
</TestAction>
|
||||||
<LaunchAction
|
<LaunchAction
|
||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1430"
|
LastUpgradeVersion = "1510"
|
||||||
version = "1.7">
|
version = "1.7">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|
|
@ -46,8 +46,9 @@ let package = Package(
|
||||||
.target(
|
.target(
|
||||||
name: "PassepartoutLibrary",
|
name: "PassepartoutLibrary",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
|
"PassepartoutFrontend",
|
||||||
"PassepartoutVPNImpl",
|
"PassepartoutVPNImpl",
|
||||||
"PassepartoutProvidersImpl"
|
"PassepartoutProvidersImpl",
|
||||||
]),
|
]),
|
||||||
.target(
|
.target(
|
||||||
name: "PassepartoutVPNImpl",
|
name: "PassepartoutVPNImpl",
|
||||||
|
|
|
@ -95,9 +95,10 @@ public final class ProductManager: NSObject, ObservableObject {
|
||||||
self?.reloadReceipt()
|
self?.reloadReceipt()
|
||||||
}
|
}
|
||||||
reloadReceipt()
|
reloadReceipt()
|
||||||
refreshProducts()
|
|
||||||
|
|
||||||
Task {
|
Task {
|
||||||
|
await refreshProducts()
|
||||||
|
|
||||||
let isBeta = await SandboxChecker().isBeta
|
let isBeta = await SandboxChecker().isBeta
|
||||||
appType = overriddenAppType ?? (isBeta ? .beta : .freemium)
|
appType = overriddenAppType ?? (isBeta ? .beta : .freemium)
|
||||||
pp_log.info("App type: \(appType)")
|
pp_log.info("App type: \(appType)")
|
||||||
|
@ -109,7 +110,7 @@ public final class ProductManager: NSObject, ObservableObject {
|
||||||
inApp.canMakePurchases()
|
inApp.canMakePurchases()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func refreshProducts() {
|
public func refreshProducts() async {
|
||||||
let ids = LocalProduct.all
|
let ids = LocalProduct.all
|
||||||
guard !ids.isEmpty else {
|
guard !ids.isEmpty else {
|
||||||
return
|
return
|
||||||
|
@ -119,7 +120,6 @@ public final class ProductManager: NSObject, ObservableObject {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
isRefreshingProducts = true
|
isRefreshingProducts = true
|
||||||
Task {
|
|
||||||
do {
|
do {
|
||||||
let productsMap = try await inApp.requestProducts(withIdentifiers: ids)
|
let productsMap = try await inApp.requestProducts(withIdentifiers: ids)
|
||||||
pp_log.debug("In-app products: \(productsMap.keys.map(\.rawValue))")
|
pp_log.debug("In-app products: \(productsMap.keys.map(\.rawValue))")
|
||||||
|
@ -131,7 +131,6 @@ public final class ProductManager: NSObject, ObservableObject {
|
||||||
isRefreshingProducts = false
|
isRefreshingProducts = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public func product(withIdentifier identifier: LocalProduct) -> InAppProduct? {
|
public func product(withIdentifier identifier: LocalProduct) -> InAppProduct? {
|
||||||
inApp.product(withIdentifier: identifier)
|
inApp.product(withIdentifier: identifier)
|
||||||
|
@ -167,31 +166,19 @@ public final class ProductManager: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func purchase(_ product: InAppProduct, completionHandler: @escaping (Result<InAppPurchaseResult, Error>) -> Void) {
|
public func purchase(_ product: InAppProduct) async throws -> InAppPurchaseResult {
|
||||||
guard let pid = LocalProduct(rawValue: product.productIdentifier) else {
|
guard let pid = LocalProduct(rawValue: product.productIdentifier) else {
|
||||||
|
assertionFailure("Unrecognized product: \(product)")
|
||||||
pp_log.warning("Unrecognized product: \(product)")
|
pp_log.warning("Unrecognized product: \(product)")
|
||||||
return
|
return .cancelled
|
||||||
}
|
}
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
let result = try await inApp.purchase(productWithIdentifier: pid)
|
let result = try await inApp.purchase(productWithIdentifier: pid)
|
||||||
reloadReceipt()
|
reloadReceipt()
|
||||||
completionHandler(.success(result))
|
return result
|
||||||
} catch {
|
|
||||||
completionHandler(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public func restorePurchases(completionHandler: @escaping (Error?) -> Void) {
|
public func restorePurchases() async throws {
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
try await inApp.restorePurchases()
|
try await inApp.restorePurchases()
|
||||||
completionHandler(nil)
|
|
||||||
} catch {
|
|
||||||
completionHandler(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue