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

@@ -46,7 +46,6 @@ struct FeelsApp: App {
if phase == .active { if phase == .active {
UIApplication.shared.applicationIconBadgeNumber = 0 UIApplication.shared.applicationIconBadgeNumber = 0
iapManager.refresh()
} }
} }
} }

View File

@@ -20,7 +20,12 @@ public enum StoreError: Error {
class IAPManager: ObservableObject { class IAPManager: ObservableObject {
@Published private(set) var showIAP = false @Published private(set) var showIAP = false
@Published private(set) var showIAPWarning = 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)?]() @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() @AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date()
@Published private(set) var isLoadingSubscriptions = false @Published private(set) var isLoadingSubscriptions = false
@@ -30,6 +35,39 @@ class IAPManager: ObservableObject {
$0.price < $1.price $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? { public var currentSubscription: Product? {
let sortedProducts = subscriptions.keys.sorted(by: { let sortedProducts = subscriptions.keys.sorted(by: {
@@ -78,14 +116,6 @@ class IAPManager: ObservableObject {
var updateListenerTask: Task<Void, Error>? = nil 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? { public var expireDate: Date? {
Calendar.current.date(byAdding: .day, value: 30, to: firstLaunchDate) ?? nil 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. //Start a transaction listener as close to app launch as possible so you don't miss any transactions.
updateListenerTask = listenForTransactions() updateListenerTask = listenForTransactions()
refresh() updateEverything()
setUpdateTimer()
} }
deinit { deinit {
updateListenerTask?.cancel() updateListenerTask?.cancel()
} }
func setUpdateTimer() { public func updateEverything() {
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() {
Task { 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() await requestProducts()
//Deliver products that the customer purchases. // Deliver products that the customer purchases.
await updateCustomerProductStatus() await updateCustomerProductStatus()
decideShowIAP() self.updateShowVariables()
decideShowIAPWarning()
self.setUpdateTimer()
DispatchQueue.main.async { DispatchQueue.main.async {
self.isLoadingSubscriptions = false self.isLoadingSubscriptions = false
} }
} }
} }
func decideShowIAP() { private func updateShowVariables() {
// 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
}
DispatchQueue.main.async { DispatchQueue.main.async {
self.showIAP = tmpShowIAP self.showIAP = self.shouldShowIAP
self.showIAPWarning = self.shouldShowIAPWarning
} }
} }
func decideShowIAPWarning() { private func setUpdateTimer() {
DispatchQueue.main.async { if !self.showIAPWarning {
if self.currentSubscription != nil { if let expireOnTimer = expireOnTimer {
self.showIAPWarning = false expireOnTimer.invalidate()
} else { }
self.showIAPWarning = true 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. //Deliver products to the user.
await self.updateCustomerProductStatus() await self.updateCustomerProductStatus()
self.updateShowVariables()
//Always finish a transaction. //Always finish a transaction.
await transaction.finish() 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 @MainActor
func updateCustomerProductStatus() async { func updateCustomerProductStatus() async {
var purchasedSubscriptions: [Product] = [] var purchasedSubscriptions: [Product] = []
@@ -247,7 +286,6 @@ class IAPManager: ObservableObject {
case .nonRenewable: case .nonRenewable:
break break
case .autoRenewable: case .autoRenewable:
print("subscriptions2 \(subscriptions)")
if let subscription = subscriptions.first(where: { if let subscription = subscriptions.first(where: {
$0.key.id == transaction.productID $0.key.id == transaction.productID
}) { }) {
@@ -260,11 +298,6 @@ class IAPManager: ObservableObject {
print() print()
} }
} }
print("purchasedSubscriptions \(purchasedSubscriptions)")
if !purchasedSubscriptions.isEmpty {
self.showIAP = false
self.showIAPWarning = false
}
for sub in purchasedSubscriptions { for sub in purchasedSubscriptions {
guard let statuses = try? await sub.subscription?.status else { guard let statuses = try? await sub.subscription?.status else {
@@ -285,6 +318,10 @@ class IAPManager: ObservableObject {
} }
func purchase(_ product: Product) async throws -> Transaction? { func purchase(_ product: Product) async throws -> Transaction? {
DispatchQueue.main.async {
self.isPurchasing = true
}
//Begin purchasing the `Product` the user selects. //Begin purchasing the `Product` the user selects.
let result = try await product.purchase() let result = try await product.purchase()
@@ -297,11 +334,14 @@ class IAPManager: ObservableObject {
//The transaction is verified. Deliver content to the user. //The transaction is verified. Deliver content to the user.
await updateCustomerProductStatus() await updateCustomerProductStatus()
self.updateShowVariables()
//Always finish a transaction. //Always finish a transaction.
await transaction.finish() await transaction.finish()
decideShowIAP() DispatchQueue.main.async {
decideShowIAPWarning() self.isPurchasing = false
}
return transaction return transaction
case .userCancelled, .pending: case .userCancelled, .pending:

View File

@@ -81,9 +81,6 @@ struct DayView: View {
}) })
} }
} }
.onAppear{
iapManager.decideShowIAP()
}
} }

View File

@@ -99,7 +99,6 @@ struct MonthView: View {
} }
.onAppear(perform: { .onAppear(perform: {
EventLogger.log(event: "show_month_view") EventLogger.log(event: "show_month_view")
iapManager.decideShowIAP()
}) })
.padding([.top]) .padding([.top])
.background( .background(

View File

@@ -9,12 +9,6 @@ import SwiftUI
import StoreKit import StoreKit
struct PurchaseButtonView: View { 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.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.textColor.rawValue, store: GroupUserDefaults.groupDefaults) private var textColor: Color = DefaultTextColor.textColor
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date() @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 height: Float
private let showCountdownTimer: Bool private let showCountdownTimer: Bool
private let showManageSubClosure: (() -> Void)? 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) { public init(height: Float, iapManager: IAPManager, showManageSubClosure: (() -> Void)? = nil, showCountdownTimer: Bool = false) {
self.height = height self.height = height
self.showManageSubClosure = showManageSubClosure self.showManageSubClosure = showManageSubClosure
@@ -42,24 +28,15 @@ struct PurchaseButtonView: View {
var body: some View { var body: some View {
ZStack { ZStack {
switch viewStatus { switch iapManager.showIAP {
case .needToBuy, .error: case true:
buyOptionsView buyOptionsView
.frame(height: CGFloat(height)) .frame(height: CGFloat(height))
.background(theme.currentTheme.secondaryBGColor) .background(theme.currentTheme.secondaryBGColor)
case .success: case false:
subscribedView subscribedView
.frame(height: CGFloat(height)) .frame(height: CGFloat(height))
.background(theme.currentTheme.secondaryBGColor) .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) .frame(minWidth: 0, maxWidth: .infinity)
.background(.clear) .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 { private var subscribedView: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(String(localized: "purchase_view_current_subscription")) Text(String(localized: "purchase_view_current_subscription"))
@@ -253,23 +216,9 @@ struct PurchaseButtonView: View {
.padding([.leading, .trailing]) .padding([.leading, .trailing])
} }
private var purchasingView: some View {
HStack {
Text("purcasing")
}
.background(.yellow)
}
private func purchase(product: Product) { private func purchase(product: Product) {
isPurchasing = true
Task { Task {
do { try await iapManager.purchase(product)
if let _ = try await iapManager.purchase(product) {
viewStatus = .success
}
} catch {
viewStatus = .error
}
} }
} }
} }

View File

@@ -77,7 +77,6 @@ struct SettingsView: View {
} }
.onAppear(perform: { .onAppear(perform: {
EventLogger.log(event: "show_settings_view") EventLogger.log(event: "show_settings_view")
iapManager.setUpdateTimer()
}) })
.background( .background(
theme.currentTheme.bg theme.currentTheme.bg
@@ -162,7 +161,6 @@ struct SettingsView: View {
Spacer() Spacer()
Button(action: { Button(action: {
EventLogger.log(event: "tap_settings_close") EventLogger.log(event: "tap_settings_close")
iapManager.decideShowIAP()
dismiss() dismiss()
}, label: { }, label: {
Text(String(localized: "settings_view_exit")) Text(String(localized: "settings_view_exit"))
@@ -545,7 +543,7 @@ struct SettingsView: View {
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene{ if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene{
do { do {
try await StoreKit.AppStore.showManageSubscriptions(in: windowScene) try await StoreKit.AppStore.showManageSubscriptions(in: windowScene)
iapManager.refresh() iapManager.updateEverything()
} catch { } catch {
print("Sheet can not be opened") print("Sheet can not be opened")
} }

View File

@@ -74,7 +74,6 @@ struct YearView: View {
} }
.onAppear(perform: { .onAppear(perform: {
self.viewModel.filterEntries(startDate: Date(timeIntervalSince1970: 0), endDate: Date()) self.viewModel.filterEntries(startDate: Date(timeIntervalSince1970: 0), endDate: Date())
iapManager.decideShowIAP()
}) })
.background( .background(
theme.currentTheme.bg theme.currentTheme.bg