iap - wip

This commit is contained in:
Trey t
2022-07-17 10:26:00 -05:00
parent 6c239c5e26
commit bd238e5743
15 changed files with 897 additions and 22 deletions

296
Shared/IAPManager.swift Normal file
View File

@@ -0,0 +1,296 @@
/*
See LICENSE folder for this samples licensing information.
Abstract:
The store class is responsible for requesting products from the App Store and starting purchases.
*/
import Foundation
import StoreKit
import SwiftUI
typealias Transaction = StoreKit.Transaction
typealias RenewalInfo = StoreKit.Product.SubscriptionInfo.RenewalInfo
typealias RenewalState = StoreKit.Product.SubscriptionInfo.RenewalState
public enum StoreError: Error {
case failedVerification
}
class IAPManager: ObservableObject {
@Published private(set) var showIAP = false
@Published private(set) var subscriptions = [Product: (status: [Product.SubscriptionInfo.Status], renewalInfo: RenewalInfo)?]()
public var sortedSubscriptionKeysByPriceOptions: [Product] {
subscriptions.keys.sorted(by: {
$0.price < $1.price
})
}
public var currentSubscription: Product? {
let sortedProducts = subscriptions.keys.sorted(by: {
$0.price > $1.price
})
// first see if we have a product + sub that is set to autorenew
for product in sortedProducts {
if let _value = subscriptions[product]??.renewalInfo {
if _value.willAutoRenew {
return product
}
}
}
// if no auto renew then return
// highest priced that has a sub status
for product in sortedProducts {
if let _ = subscriptions[product]??.status {
return product
}
}
return nil
}
// for all products return the one that is set
// to auto renew
public var nextRenewllOption: Product? {
if let currentSubscription = currentSubscription,
let info = subscriptions[currentSubscription],
let status = info?.status {
for aStatus in status {
if let renewalInfo = try? checkVerified(aStatus.renewalInfo),
renewalInfo.willAutoRenew {
if let renewToProduct = subscriptions.first(where: {
$0.key.id == renewalInfo.autoRenewPreference
})?.key {
return renewToProduct
}
}
}
}
return nil
}
var updateListenerTask: Task<Void, Error>? = nil
private let iapIdentifiers = Set([
"com.88oakapps.ifeel.IAP.subscription.weekly",
"com.88oakapps.ifeel.IAP.subscription.monthly",
"com.88oakapps.ifeel.IAP.subscription.yearly"
])
init() {
//Start a transaction listener as close to app launch as possible so you don't miss any transactions.
updateListenerTask = listenForTransactions()
refresh()
}
deinit {
updateListenerTask?.cancel()
}
func refresh() {
Task {
//During store initialization, request products from the App Store.
await requestProducts()
//Deliver products that the customer purchases.
await updateCustomerProductStatus()
decideShowIAP()
}
}
func decideShowIAP() {
guard !subscriptions.isEmpty else {
return
}
var tmpShowIAP = true
for (_, value) in self.subscriptions {
if value != nil {
tmpShowIAP = false
return
}
}
DispatchQueue.main.async {
self.showIAP = tmpShowIAP
}
}
func listenForTransactions() -> Task<Void, Error> {
return Task.detached {
//Iterate through any transactions that don't come from a direct call to `purchase()`.
for await result in Transaction.updates {
do {
let transaction = try self.checkVerified(result)
//Deliver products to the user.
await self.updateCustomerProductStatus()
//Always finish a transaction.
await transaction.finish()
} catch {
//StoreKit has a transaction that fails verification. Don't deliver content to the user.
print("Transaction failed verification")
}
}
}
}
// fetch all available iap from remote and store locally
// in subscriptions
@MainActor
func requestProducts() async {
do {
subscriptions.removeAll()
//Request products from the App Store using the identifiers that the Products.plist file defines.
let storeProducts = try await Product.products(for: iapIdentifiers)
//Filter the products into categories based on their type.
for product in storeProducts {
switch product.type {
case .consumable:
break
case .nonConsumable:
break
case .autoRenewable:
subscriptions.updateValue(nil, forKey: product)
case .nonRenewable:
break
default:
//Ignore this product.
print("Unknown product")
}
}
} catch {
print("Failed product request from the App Store server: \(error)")
}
}
@MainActor
func updateCustomerProductStatus() async {
var purchasedSubscriptions: [Product] = []
// Iterate through all of the user's purchased products.
for await result in Transaction.currentEntitlements {
do {
//Check whether the transaction is verified. If it isnt, catch `failedVerification` error.
let transaction = try checkVerified(result)
//Check the `productType` of the transaction and get the corresponding product from the store.
switch transaction.productType {
case .nonConsumable:
break
case .nonRenewable:
break
case .autoRenewable:
print("subscriptions2 \(subscriptions)")
if let subscription = subscriptions.first(where: {
$0.key.id == transaction.productID
}) {
purchasedSubscriptions.append(subscription.key)
}
default:
break
}
} catch {
print()
}
}
print("purchasedSubscriptions \(purchasedSubscriptions)")
if !purchasedSubscriptions.isEmpty {
self.showIAP = false
}
for sub in purchasedSubscriptions {
guard let statuses = try? await sub.subscription?.status else {
return
}
for status in statuses {
switch status.state {
case .expired, .revoked:
continue
default:
if let renewalInfo = try? checkVerified(status.renewalInfo) {
subscriptions.updateValue((statuses, renewalInfo), forKey: sub)
}
}
}
}
}
func purchase(_ product: Product) async throws -> Transaction? {
//Begin purchasing the `Product` the user selects.
let result = try await product.purchase()
switch result {
case .success(let verification):
//Check whether the transaction is verified. If it isn't,
//this function rethrows the verification error.
let transaction = try checkVerified(verification)
//The transaction is verified. Deliver content to the user.
await updateCustomerProductStatus()
//Always finish a transaction.
await transaction.finish()
decideShowIAP()
return transaction
case .userCancelled, .pending:
return nil
default:
return nil
}
}
func isPurchased(_ product: Product) async throws -> Bool {
//Determine whether the user purchases a given product.
switch product.type {
case .nonRenewable:
return false
case .nonConsumable:
return false
case .autoRenewable:
return subscriptions.keys.contains(product)
default:
return false
}
}
func checkVerified<T>(_ result: VerificationResult<T>) throws -> T {
//Check whether the JWS passes StoreKit verification.
switch result {
case .unverified:
//StoreKit parses the JWS, but it fails verification.
throw StoreError.failedVerification
case .verified(let safe):
//The result is verified. Return the unwrapped value.
return safe
}
}
func sortByPrice(_ products: [Product]) -> [Product] {
products.sorted(by: { return $0.price < $1.price })
}
func colorForIAPButton(iapIdentifier: String) -> Color {
if iapIdentifier == "com.88oakapps.ifeel.IAP.subscription.weekly" {
return DefaultMoodTint.color(forMood: .horrible)
}
else if iapIdentifier == "com.88oakapps.ifeel.IAP.subscription.monthly" {
return DefaultMoodTint.color(forMood: .average)
}
else if iapIdentifier == "com.88oakapps.ifeel.IAP.subscription.yearly" {
return DefaultMoodTint.color(forMood: .great)
}
return .blue
}
}