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 {
UIApplication.shared.applicationIconBadgeNumber = 0
iapManager.refresh()
}
}
}

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:

View File

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

View File

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

View File

@@ -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)
}
}
}

View File

@@ -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")
}

View File

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