This commit is contained in:
Trey t
2022-12-23 11:20:33 -06:00
parent 488832b777
commit 2c8772f79a
7 changed files with 114 additions and 133 deletions

View File

@@ -20,7 +20,12 @@ public enum StoreError: Error {
class IAPManager: ObservableObject {
@Published private(set) var showIAP = false
@Published private(set) var showIAPWarning = false
@Published private(set) var isPurchasing = false
@Published private(set) var subscriptions = [Product: (status: [Product.SubscriptionInfo.Status], renewalInfo: RenewalInfo)?]()
private(set) var purchasedProductIDs = Set<String>()
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date()
@Published private(set) var isLoadingSubscriptions = false
@@ -30,6 +35,39 @@ class IAPManager: ObservableObject {
$0.price < $1.price
})
}
public var daysLeftBeforeIAP: Int {
let daysSinceInstall = Calendar.current.dateComponents([.day, .hour, .minute, .second], from: firstLaunchDate, to: Date())
if let days = daysSinceInstall.day {
return 30 - days
}
return 0
}
private var shouldShowIAP: Bool {
if shouldShowIAPWarning && daysLeftBeforeIAP <= 0{
return true
}
return false
}
private var shouldShowIAPWarning: Bool {
// if we have't fetch all subscriptions yet use faster
// purchasedProductIDs
if subscriptions.isEmpty {
if purchasedProductIDs.isEmpty {
return true
} else {
return false
}
} else {
if currentSubscription == nil {
return true
}
return false
}
}
public var currentSubscription: Product? {
let sortedProducts = subscriptions.keys.sorted(by: {
@@ -78,14 +116,6 @@ class IAPManager: ObservableObject {
var updateListenerTask: Task<Void, Error>? = nil
public var daysLeftBeforeIAP: Int {
let daysSinceInstall = Calendar.current.dateComponents([.day, .hour, .minute, .second], from: firstLaunchDate, to: Date())
if let days = daysSinceInstall.day {
return 30 - days
}
return 0
}
public var expireDate: Date? {
Calendar.current.date(byAdding: .day, value: 30, to: firstLaunchDate) ?? nil
}
@@ -104,76 +134,66 @@ class IAPManager: ObservableObject {
//Start a transaction listener as close to app launch as possible so you don't miss any transactions.
updateListenerTask = listenForTransactions()
refresh()
setUpdateTimer()
updateEverything()
}
deinit {
updateListenerTask?.cancel()
}
func setUpdateTimer() {
if let expireDate = expireDate {
expireOnTimer = Timer.init(fire: expireDate, interval: 0, repeats: false, block: { _ in
self.decideShowIAP()
self.decideShowIAPWarning()
})
RunLoop.main.add(expireOnTimer!, forMode: .common)
} else {
if let expireOnTimer = expireOnTimer {
expireOnTimer.invalidate()
}
}
}
func refresh() {
public func updateEverything() {
Task {
//During store initialization, request products from the App Store.
// get current sub from local cache
await updatePurchasedProducts()
// update local variables to show iap warning / purchase views
self.updateShowVariables()
// if they have a subscription we dont care about showing the loading indicator
if !self.showIAP {
DispatchQueue.main.async {
self.isLoadingSubscriptions = false
}
}
// During store initialization, request products from the App Store.
await requestProducts()
//Deliver products that the customer purchases.
// Deliver products that the customer purchases.
await updateCustomerProductStatus()
decideShowIAP()
decideShowIAPWarning()
self.updateShowVariables()
self.setUpdateTimer()
DispatchQueue.main.async {
self.isLoadingSubscriptions = false
}
}
}
func decideShowIAP() {
// if we have a sub in the subscriptions dict
// then we dont need to show
for (_, value) in self.subscriptions {
if value != nil {
DispatchQueue.main.async {
self.showIAP = false
}
return
}
}
var tmpShowIAP = true
// if its passed 30 days with no sub
if daysLeftBeforeIAP <= 0 {
tmpShowIAP = true
} else {
tmpShowIAP = false
}
private func updateShowVariables() {
DispatchQueue.main.async {
self.showIAP = tmpShowIAP
self.showIAP = self.shouldShowIAP
self.showIAPWarning = self.shouldShowIAPWarning
}
}
func decideShowIAPWarning() {
DispatchQueue.main.async {
if self.currentSubscription != nil {
self.showIAPWarning = false
} else {
self.showIAPWarning = true
private func setUpdateTimer() {
if !self.showIAPWarning {
if let expireOnTimer = expireOnTimer {
expireOnTimer.invalidate()
}
return
}
if let expireDate = expireDate {
expireOnTimer = Timer.init(fire: expireDate, interval: 0, repeats: false, block: { _ in
self.updateShowVariables()
})
RunLoop.main.add(expireOnTimer!, forMode: .common)
} else {
if let expireOnTimer = expireOnTimer {
expireOnTimer.invalidate()
}
}
}
@@ -187,6 +207,8 @@ class IAPManager: ObservableObject {
//Deliver products to the user.
await self.updateCustomerProductStatus()
self.updateShowVariables()
//Always finish a transaction.
await transaction.finish()
@@ -230,6 +252,23 @@ class IAPManager: ObservableObject {
}
}
// quickly check current entitlments if we have a sub
private func updatePurchasedProducts() async {
for await result in Transaction.currentEntitlements {
guard case .verified(let transaction) = result else {
continue
}
if transaction.revocationDate == nil {
self.purchasedProductIDs.insert(transaction.productID)
} else {
self.purchasedProductIDs.remove(transaction.productID)
}
}
}
// fetch all subscriptions and fill out subscriptions with current
// status of each
@MainActor
func updateCustomerProductStatus() async {
var purchasedSubscriptions: [Product] = []
@@ -247,7 +286,6 @@ class IAPManager: ObservableObject {
case .nonRenewable:
break
case .autoRenewable:
print("subscriptions2 \(subscriptions)")
if let subscription = subscriptions.first(where: {
$0.key.id == transaction.productID
}) {
@@ -260,11 +298,6 @@ class IAPManager: ObservableObject {
print()
}
}
print("purchasedSubscriptions \(purchasedSubscriptions)")
if !purchasedSubscriptions.isEmpty {
self.showIAP = false
self.showIAPWarning = false
}
for sub in purchasedSubscriptions {
guard let statuses = try? await sub.subscription?.status else {
@@ -285,6 +318,10 @@ class IAPManager: ObservableObject {
}
func purchase(_ product: Product) async throws -> Transaction? {
DispatchQueue.main.async {
self.isPurchasing = true
}
//Begin purchasing the `Product` the user selects.
let result = try await product.purchase()
@@ -297,11 +334,14 @@ class IAPManager: ObservableObject {
//The transaction is verified. Deliver content to the user.
await updateCustomerProductStatus()
self.updateShowVariables()
//Always finish a transaction.
await transaction.finish()
decideShowIAP()
decideShowIAPWarning()
DispatchQueue.main.async {
self.isPurchasing = false
}
return transaction
case .userCancelled, .pending: