iap work
This commit is contained in:
@@ -46,7 +46,6 @@ struct FeelsApp: App {
|
|||||||
|
|
||||||
if phase == .active {
|
if phase == .active {
|
||||||
UIApplication.shared.applicationIconBadgeNumber = 0
|
UIApplication.shared.applicationIconBadgeNumber = 0
|
||||||
iapManager.refresh()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -81,9 +81,6 @@ struct DayView: View {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear{
|
|
||||||
iapManager.decideShowIAP()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user