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>"; };
|
||||
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 = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1430"
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
|
|
|
@ -46,8 +46,9 @@ let package = Package(
|
|||
.target(
|
||||
name: "PassepartoutLibrary",
|
||||
dependencies: [
|
||||
"PassepartoutFrontend",
|
||||
"PassepartoutVPNImpl",
|
||||
"PassepartoutProvidersImpl"
|
||||
"PassepartoutProvidersImpl",
|
||||
]),
|
||||
.target(
|
||||
name: "PassepartoutVPNImpl",
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue