iap work
This commit is contained in:
@@ -46,7 +46,6 @@ struct FeelsApp: App {
|
||||
|
||||
if phase == .active {
|
||||
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||
iapManager.refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -81,9 +81,6 @@ struct DayView: View {
|
||||
})
|
||||
}
|
||||
}
|
||||
.onAppear{
|
||||
iapManager.decideShowIAP()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -99,7 +99,6 @@ struct MonthView: View {
|
||||
}
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_month_view")
|
||||
iapManager.decideShowIAP()
|
||||
})
|
||||
.padding([.top])
|
||||
.background(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -74,7 +74,6 @@ struct YearView: View {
|
||||
}
|
||||
.onAppear(perform: {
|
||||
self.viewModel.filterEntries(startDate: Date(timeIntervalSince1970: 0), endDate: Date())
|
||||
iapManager.decideShowIAP()
|
||||
})
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
|
||||
Reference in New Issue
Block a user