Vernissage/Vernissage/EnvironmentObjects/TipsStore.swift

147 lines
4.5 KiB
Swift
Raw Normal View History

2023-02-13 21:10:07 +01:00
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
2023-03-28 10:35:38 +02:00
// Licensed under the Apache License 2.0.
2023-02-13 21:10:07 +01:00
//
2023-02-13 21:10:07 +01:00
import Foundation
import StoreKit
2023-04-07 14:38:50 +02:00
import ServicesKit
2023-10-01 18:45:49 +02:00
import OSLog
import EnvironmentKit
2023-02-13 21:10:07 +01:00
2023-10-19 13:24:02 +02:00
@Observable final class TipsStore {
2023-02-13 21:10:07 +01:00
2023-02-14 07:32:00 +01:00
/// Products are registered in AppStore connect (and for development in InAppPurchaseStoreKitConfiguration.storekit file).
2023-10-19 13:24:02 +02:00
private(set) var items = [Product]()
2023-02-14 07:32:00 +01:00
/// Status of the purchase.
2023-10-19 13:24:02 +02:00
private(set) var status: ActionStatus? {
didSet {
2023-02-13 21:10:07 +01:00
switch status {
case .failed:
self.hasError = true
default:
self.hasError = false
}
}
}
2023-02-14 07:32:00 +01:00
/// True when error during purchase occures.
2023-10-19 13:24:02 +02:00
var hasError = false
2023-02-14 07:32:00 +01:00
/// Error during purchase.
2023-02-13 21:10:07 +01:00
var error: PurchaseError? {
switch status {
case .failed(let error):
return error
default:
return nil
}
}
2023-02-14 07:32:00 +01:00
/// Listener responsible for waiting for new events from AppStore (when transaction didn't finish during the purchase).
2023-02-13 21:10:07 +01:00
private var transactionListener: Task<Void, Error>?
2023-02-13 21:10:07 +01:00
init() {
transactionListener = configureTransactionListener()
Task { [weak self] in
await self?.retrieve()
}
}
2023-02-13 21:10:07 +01:00
deinit {
transactionListener?.cancel()
}
2023-02-13 21:10:07 +01:00
/// Purchase new product.
public func purchase(_ product: Product) async {
do {
let result = try await product.purchase()
try await self.handlePurchase(from: result)
} catch {
self.status = .failed(.system(error))
2023-10-18 11:14:56 +02:00
ErrorService.shared.handle(error, message: "global.error.purchaseFailed", showToastr: false)
2023-02-13 21:10:07 +01:00
}
}
2023-02-13 21:10:07 +01:00
/// Reset status of the purchase/action.
public func reset() {
self.status = nil
}
2023-02-13 21:10:07 +01:00
/// Handle purchase result.
private func handlePurchase(from result: Product.PurchaseResult) async throws {
switch result {
case .success(let verificationResult):
let transaction = try self.checkVerified(verificationResult)
self.status = .successful
await transaction.finish()
case .userCancelled:
2023-10-01 18:45:49 +02:00
Logger.main.warning("User click cancel before their transaction started.")
2023-02-13 21:10:07 +01:00
case .pending:
2023-10-01 18:45:49 +02:00
Logger.main.warning("User needs to complete some action on their account before their complete the purchase.")
2023-02-13 21:10:07 +01:00
default:
break
}
}
2023-02-13 21:10:07 +01:00
/// We have to verify if transaction ends successfuly.
private func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
switch result {
case .unverified:
throw PurchaseError.failedVerification
case .verified(let signedType):
return signedType
}
}
2023-02-13 21:10:07 +01:00
/// Configure listener of interrupted transactions.
private func configureTransactionListener() -> Task<Void, Error> {
Task.detached(priority: .background) { @MainActor [weak self] in
do {
for await result in Transaction.updates {
let transaction = try self?.checkVerified(result)
self?.status = .successful
await transaction?.finish()
}
} catch {
self?.status = .failed(.system(error))
2023-10-18 11:14:56 +02:00
ErrorService.shared.handle(error, message: "global.error.cannotConfigureTransactionListener", showToastr: false)
2023-02-13 21:10:07 +01:00
}
}
}
2023-02-13 21:10:07 +01:00
/// Retrieve products from Apple store.
private func retrieve() async {
do {
let products = try await Product.products(for: ProductIdentifiers.allCases.map({ $0.rawValue }))
.sorted(by: { $0.price < $1.price })
2023-02-13 21:10:07 +01:00
self.items = products
} catch {
self.status = .failed(.system(error))
2023-10-18 11:14:56 +02:00
ErrorService.shared.handle(error, message: "global.error.cannotDownloadInAppProducts", showToastr: false)
2023-02-13 21:10:07 +01:00
}
}
}
2023-02-14 07:32:00 +01:00
extension TipsStore {
2023-02-13 21:10:07 +01:00
public enum ActionStatus: Equatable {
case successful
case failed(PurchaseError)
2023-02-14 07:32:00 +01:00
public static func == (lhs: TipsStore.ActionStatus, rhs: TipsStore.ActionStatus) -> Bool {
2023-02-13 21:10:07 +01:00
switch (lhs, rhs) {
case (.successful, .successful):
return true
case (let .failed(lhsError), let .failed(rhsError)):
return lhsError.localizedDescription == rhsError.localizedDescription
default:
return false
}
}
}
}