From 2c8772f79a39101be6471ed21ddc220189bf917b Mon Sep 17 00:00:00 2001 From: Trey t Date: Fri, 23 Dec 2022 11:20:33 -0600 Subject: [PATCH] iap work --- Shared/FeelsApp.swift | 1 - Shared/IAPManager.swift | 174 ++++++++++++------- Shared/views/DayView/DayView.swift | 3 - Shared/views/MonthView/MonthView.swift | 1 - Shared/views/PurchaseButtonView.swift | 63 +------ Shared/views/SettingsView/SettingsView.swift | 4 +- Shared/views/YearView/YearView.swift | 1 - 7 files changed, 114 insertions(+), 133 deletions(-) diff --git a/Shared/FeelsApp.swift b/Shared/FeelsApp.swift index ad37f87..1e840b2 100644 --- a/Shared/FeelsApp.swift +++ b/Shared/FeelsApp.swift @@ -46,7 +46,6 @@ struct FeelsApp: App { if phase == .active { UIApplication.shared.applicationIconBadgeNumber = 0 - iapManager.refresh() } } } diff --git a/Shared/IAPManager.swift b/Shared/IAPManager.swift index 719eb92..ef31986 100644 --- a/Shared/IAPManager.swift +++ b/Shared/IAPManager.swift @@ -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() + @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? = 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: diff --git a/Shared/views/DayView/DayView.swift b/Shared/views/DayView/DayView.swift index 8fcb922..d327251 100644 --- a/Shared/views/DayView/DayView.swift +++ b/Shared/views/DayView/DayView.swift @@ -81,9 +81,6 @@ struct DayView: View { }) } } - .onAppear{ - iapManager.decideShowIAP() - } } diff --git a/Shared/views/MonthView/MonthView.swift b/Shared/views/MonthView/MonthView.swift index fd2c1c1..ef51738 100644 --- a/Shared/views/MonthView/MonthView.swift +++ b/Shared/views/MonthView/MonthView.swift @@ -99,7 +99,6 @@ struct MonthView: View { } .onAppear(perform: { EventLogger.log(event: "show_month_view") - iapManager.decideShowIAP() }) .padding([.top]) .background( diff --git a/Shared/views/PurchaseButtonView.swift b/Shared/views/PurchaseButtonView.swift index 331345e..b0d0da2 100644 --- a/Shared/views/PurchaseButtonView.swift +++ b/Shared/views/PurchaseButtonView.swift @@ -9,12 +9,6 @@ import SwiftUI import StoreKit struct PurchaseButtonView: View { - enum ViewStatus: String { - case needToBuy - case error - case success - case subscribed - } @AppStorage(UserDefaultsStore.Keys.theme.rawValue, store: GroupUserDefaults.groupDefaults) private var theme: Theme = .system @AppStorage(UserDefaultsStore.Keys.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor @AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date() @@ -24,15 +18,7 @@ struct PurchaseButtonView: View { private let height: Float private let showCountdownTimer: Bool private let showManageSubClosure: (() -> Void)? - - @State var isPurchasing: Bool = false - - @State private var viewStatus: ViewStatus = .needToBuy { - didSet { - isPurchasing = false - } - } - + public init(height: Float, iapManager: IAPManager, showManageSubClosure: (() -> Void)? = nil, showCountdownTimer: Bool = false) { self.height = height self.showManageSubClosure = showManageSubClosure @@ -42,24 +28,15 @@ struct PurchaseButtonView: View { var body: some View { ZStack { - switch viewStatus { - case .needToBuy, .error: + switch iapManager.showIAP { + case true: buyOptionsView .frame(height: CGFloat(height)) .background(theme.currentTheme.secondaryBGColor) - case .success: + case false: subscribedView .frame(height: CGFloat(height)) .background(theme.currentTheme.secondaryBGColor) - case .subscribed: - subscribedView - .frame(height: CGFloat(height)) - .background(theme.currentTheme.secondaryBGColor) - } - } - .onAppear{ - if let _ = iapManager.currentSubscription { - viewStatus = .subscribed } } } @@ -173,21 +150,7 @@ struct PurchaseButtonView: View { .frame(minWidth: 0, maxWidth: .infinity) .background(.clear) } - - private var successView: some View { - HStack { - Text("it worked") - } - .background(.green) - } - - private var errorView: some View { - HStack { - Text("something broke") - } - .background(.red) - } - + private var subscribedView: some View { VStack(alignment: .leading) { Text(String(localized: "purchase_view_current_subscription")) @@ -253,23 +216,9 @@ struct PurchaseButtonView: View { .padding([.leading, .trailing]) } - private var purchasingView: some View { - HStack { - Text("purcasing") - } - .background(.yellow) - } - private func purchase(product: Product) { - isPurchasing = true Task { - do { - if let _ = try await iapManager.purchase(product) { - viewStatus = .success - } - } catch { - viewStatus = .error - } + try await iapManager.purchase(product) } } } diff --git a/Shared/views/SettingsView/SettingsView.swift b/Shared/views/SettingsView/SettingsView.swift index 7d9031e..440b64a 100644 --- a/Shared/views/SettingsView/SettingsView.swift +++ b/Shared/views/SettingsView/SettingsView.swift @@ -77,7 +77,6 @@ struct SettingsView: View { } .onAppear(perform: { EventLogger.log(event: "show_settings_view") - iapManager.setUpdateTimer() }) .background( theme.currentTheme.bg @@ -162,7 +161,6 @@ struct SettingsView: View { Spacer() Button(action: { EventLogger.log(event: "tap_settings_close") - iapManager.decideShowIAP() dismiss() }, label: { Text(String(localized: "settings_view_exit")) @@ -545,7 +543,7 @@ struct SettingsView: View { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene{ do { try await StoreKit.AppStore.showManageSubscriptions(in: windowScene) - iapManager.refresh() + iapManager.updateEverything() } catch { print("Sheet can not be opened") } diff --git a/Shared/views/YearView/YearView.swift b/Shared/views/YearView/YearView.swift index 3363722..c734500 100644 --- a/Shared/views/YearView/YearView.swift +++ b/Shared/views/YearView/YearView.swift @@ -74,7 +74,6 @@ struct YearView: View { } .onAppear(perform: { self.viewModel.filterEntries(startDate: Date(timeIntervalSince1970: 0), endDate: Date()) - iapManager.decideShowIAP() }) .background( theme.currentTheme.bg