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>"; };
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>"; };
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>"; };
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; };
@ -804,6 +805,7 @@
children = (
0EE315DB2733104700F5D461 /* Packages */,
0E23B4A12298559800304C30 /* Config.xcconfig */,
0EDCEF692B337BEB0023A7FF /* PassepartoutLibrary.xctestplan */,
0E9AA982259F7674003FAFF1 /* Passepartout */,
0E57F63920C83FC5008323CF /* Products */,
374B9F085E1148C37CF9117A /* Frameworks */,
@ -1131,7 +1133,7 @@
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1510;
LastUpgradeCheck = 1430;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "Davide De Rosa";
TargetAttributes = {
0E41BD96286711C3006346B4 = {

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -67,13 +67,17 @@ struct DonateView: View {
)
// reloading
.onAppear {
productManager.refreshProducts()
}.onChange(of: scenePhase) { newValue in
.task {
await productManager.refreshProducts()
}
.onChange(of: scenePhase) { newValue in
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 {
func purchaseProduct(_ product: InAppProduct) {
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>) {
switch result {
case .success(let value):
if case .done = value {
alertType = .thankYou
isAlertPresented = true
} else {
// cancelled
}
case .failure(let error):
ErrorHandler.shared.handle(
title: L10n.Donate.title,
message: L10n.Donate.Alerts.Purchase.Failure.message(AppError(error).localizedDescription)
)
func handlePurchaseResult(_ result: InAppPurchaseResult) {
if case .done = result {
alertType = .thankYou
isAlertPresented = true
} else {
// cancelled
}
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)
// reloading
.onAppear {
productManager.refreshProducts()
}.onChange(of: scenePhase) { newValue in
.task {
await productManager.refreshProducts()
}
.onChange(of: scenePhase) { newValue in
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) {
purchaseState = .purchasing(product)
productManager.purchase(product) {
switch $0 {
case .success(let result):
Task {
do {
let result = try await productManager.purchase(product)
switch result {
case .done:
isPresented = false
@ -258,8 +262,7 @@ private extension PaywallView.PurchaseView {
break
}
purchaseState = nil
case .failure(let error):
} catch {
pp_log.error("Unable to purchase: \(error)")
ErrorHandler.shared.handle(
title: product.localizedTitle,
@ -274,8 +277,12 @@ private extension PaywallView.PurchaseView {
func restorePurchases() {
purchaseState = .restoring
productManager.restorePurchases {
if let error = $0 {
Task {
do {
try await productManager.restorePurchases()
isPresented = false
purchaseState = nil
} catch {
pp_log.error("Unable to restore purchases: \(error)")
ErrorHandler.shared.handle(
title: L10n.Paywall.Items.Restore.title,
@ -283,10 +290,7 @@ private extension PaywallView.PurchaseView {
) {
purchaseState = nil
}
return
}
isPresented = false
purchaseState = nil
}
}
}

View File

@ -23,8 +23,8 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import SwiftUI
import PassepartoutLibrary
import SwiftUI
struct PaywallView: View {
@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"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -26,8 +26,13 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
shouldUseLaunchSchemeArgsEnv = "YES">
<TestPlans>
<TestPlanReference
reference = "container:../PassepartoutLibrary.xctestplan"
default = "YES">
</TestPlanReference>
</TestPlans>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"

View File

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1430"
LastUpgradeVersion = "1510"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"

View File

@ -46,8 +46,9 @@ let package = Package(
.target(
name: "PassepartoutLibrary",
dependencies: [
"PassepartoutFrontend",
"PassepartoutVPNImpl",
"PassepartoutProvidersImpl"
"PassepartoutProvidersImpl",
]),
.target(
name: "PassepartoutVPNImpl",

View File

@ -95,9 +95,10 @@ public final class ProductManager: NSObject, ObservableObject {
self?.reloadReceipt()
}
reloadReceipt()
refreshProducts()
Task {
await refreshProducts()
let isBeta = await SandboxChecker().isBeta
appType = overriddenAppType ?? (isBeta ? .beta : .freemium)
pp_log.info("App type: \(appType)")
@ -109,7 +110,7 @@ public final class ProductManager: NSObject, ObservableObject {
inApp.canMakePurchases()
}
public func refreshProducts() {
public func refreshProducts() async {
let ids = LocalProduct.all
guard !ids.isEmpty else {
return
@ -119,17 +120,15 @@ public final class ProductManager: NSObject, ObservableObject {
return
}
isRefreshingProducts = true
Task {
do {
let productsMap = try await inApp.requestProducts(withIdentifiers: ids)
pp_log.debug("In-app products: \(productsMap.keys.map(\.rawValue))")
do {
let productsMap = try await inApp.requestProducts(withIdentifiers: ids)
pp_log.debug("In-app products: \(productsMap.keys.map(\.rawValue))")
products = Array(productsMap.values)
isRefreshingProducts = false
} catch {
pp_log.warning("Unable to list products: \(error)")
isRefreshingProducts = false
}
products = Array(productsMap.values)
isRefreshingProducts = false
} catch {
pp_log.warning("Unable to list products: \(error)")
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 {
assertionFailure("Unrecognized product: \(product)")
pp_log.warning("Unrecognized product: \(product)")
return
}
Task {
do {
let result = try await inApp.purchase(productWithIdentifier: pid)
reloadReceipt()
completionHandler(.success(result))
} catch {
completionHandler(.failure(error))
}
return .cancelled
}
let result = try await inApp.purchase(productWithIdentifier: pid)
reloadReceipt()
return result
}
public func restorePurchases(completionHandler: @escaping (Error?) -> Void) {
Task {
do {
try await inApp.restorePurchases()
completionHandler(nil)
} catch {
completionHandler(error)
}
}
public func restorePurchases() async throws {
try await inApp.restorePurchases()
}
}