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:
Davide De Rosa 2023-12-21 08:09:52 +01:00 committed by GitHub
parent a0da930d98
commit 1551b59f21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 133 additions and 78 deletions

View File

@ -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 = {

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -67,13 +67,17 @@ struct DonateView: View {
) )
// reloading // reloading
.onAppear { .task {
productManager.refreshProducts() await productManager.refreshProducts()
}.onChange(of: scenePhase) { newValue in }
.onChange(of: scenePhase) { newValue in
if newValue == .active { if newValue == .active {
productManager.refreshProducts() Task {
await productManager.refreshProducts()
}
} }
}.themeAnimation(on: productManager.isRefreshingProducts) }
.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): alertType = .thankYou
if case .done = value { isAlertPresented = true
alertType = .thankYou } else {
isAlertPresented = true // cancelled
} else {
// cancelled
}
case .failure(let error):
ErrorHandler.shared.handle(
title: L10n.Donate.title,
message: L10n.Donate.Alerts.Purchase.Failure.message(AppError(error).localizedDescription)
)
} }
pendingDonationIdentifier = nil pendingDonationIdentifier = nil
} }
func handlePurchaseError(_ error: Error) {
ErrorHandler.shared.handle(
title: L10n.Donate.title,
message: L10n.Donate.Alerts.Purchase.Failure.message(AppError(error).localizedDescription)
)
pendingDonationIdentifier = nil
}
} }

View File

@ -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 }
.onChange(of: scenePhase) { newValue in
if newValue == .active { if newValue == .active {
productManager.refreshProducts() Task {
await productManager.refreshProducts()
}
} }
}.themeAnimation(on: productManager.isRefreshingProducts) }
.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
} }
} }
} }

View File

@ -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

View File

@ -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
}

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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",

View File

@ -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,17 +120,15 @@ 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))")
products = Array(productsMap.values) products = Array(productsMap.values)
isRefreshingProducts = false isRefreshingProducts = false
} catch { } catch {
pp_log.warning("Unable to list products: \(error)") pp_log.warning("Unable to list products: \(error)")
isRefreshingProducts = false isRefreshingProducts = false
}
} }
} }
@ -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)
reloadReceipt()
completionHandler(.success(result))
} catch {
completionHandler(.failure(error))
}
} }
let result = try await inApp.purchase(productWithIdentifier: pid)
reloadReceipt()
return result
} }
public func restorePurchases(completionHandler: @escaping (Error?) -> Void) { public func restorePurchases() async throws {
Task { try await inApp.restorePurchases()
do {
try await inApp.restorePurchases()
completionHandler(nil)
} catch {
completionHandler(error)
}
}
} }
} }