IAP
This commit is contained in:
@@ -16,7 +16,8 @@ struct FeelsApp: App {
|
|||||||
|
|
||||||
let persistenceController = PersistenceController.shared
|
let persistenceController = PersistenceController.shared
|
||||||
@StateObject var iapManager = IAPManager()
|
@StateObject var iapManager = IAPManager()
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date()
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
BGTaskScheduler.shared.cancelAllTaskRequests()
|
BGTaskScheduler.shared.cancelAllTaskRequests()
|
||||||
BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTask.updateDBMissingID, using: nil) { (task) in
|
BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTask.updateDBMissingID, using: nil) { (task) in
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ 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 subscriptions = [Product: (status: [Product.SubscriptionInfo.Status], renewalInfo: RenewalInfo)?]()
|
@Published private(set) var subscriptions = [Product: (status: [Product.SubscriptionInfo.Status], renewalInfo: RenewalInfo)?]()
|
||||||
|
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date()
|
||||||
|
|
||||||
public var sortedSubscriptionKeysByPriceOptions: [Product] {
|
public var sortedSubscriptionKeysByPriceOptions: [Product] {
|
||||||
subscriptions.keys.sorted(by: {
|
subscriptions.keys.sorted(by: {
|
||||||
$0.price < $1.price
|
$0.price < $1.price
|
||||||
@@ -74,23 +75,52 @@ 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? {
|
||||||
|
Calendar.current.date(byAdding: .day, value: 30, to: firstLaunchDate) ?? nil
|
||||||
|
}
|
||||||
|
|
||||||
private let iapIdentifiers = Set([
|
private let iapIdentifiers = Set([
|
||||||
"com.88oakapps.ifeel.IAP.subscription.weekly",
|
"com.88oakapps.ifeel.IAP.subscription.weekly",
|
||||||
"com.88oakapps.ifeel.IAP.subscription.monthly",
|
"com.88oakapps.ifeel.IAP.subscription.monthly",
|
||||||
"com.88oakapps.ifeel.IAP.subscription.yearly"
|
"com.88oakapps.ifeel.IAP.subscription.yearly"
|
||||||
])
|
])
|
||||||
|
|
||||||
|
var expireOnTimer: Timer?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
//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()
|
refresh()
|
||||||
|
|
||||||
|
setUpdateTimer()
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
deinit {
|
||||||
updateListenerTask?.cancel()
|
updateListenerTask?.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setUpdateTimer() {
|
||||||
|
if let expireDate = expireDate {
|
||||||
|
expireOnTimer = Timer.init(fire: expireDate, interval: 0, repeats: false, block: { _ in
|
||||||
|
self.decideShowIAP()
|
||||||
|
})
|
||||||
|
RunLoop.main.add(expireOnTimer!, forMode: .common)
|
||||||
|
} else {
|
||||||
|
if let expireOnTimer = expireOnTimer {
|
||||||
|
expireOnTimer.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func refresh() {
|
func refresh() {
|
||||||
Task {
|
Task {
|
||||||
//During store initialization, request products from the App Store.
|
//During store initialization, request products from the App Store.
|
||||||
@@ -104,16 +134,25 @@ class IAPManager: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func decideShowIAP() {
|
func decideShowIAP() {
|
||||||
guard !subscriptions.isEmpty else {
|
// if we have a sub in the subscriptions dict
|
||||||
return
|
// then we dont need to show
|
||||||
}
|
|
||||||
var tmpShowIAP = true
|
|
||||||
for (_, value) in self.subscriptions {
|
for (_, value) in self.subscriptions {
|
||||||
if value != nil {
|
if value != nil {
|
||||||
tmpShowIAP = false
|
DispatchQueue.main.async {
|
||||||
|
self.showIAP = false
|
||||||
|
}
|
||||||
return
|
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 = tmpShowIAP
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class UserDefaultsStore {
|
|||||||
case showNSFW
|
case showNSFW
|
||||||
case shape
|
case shape
|
||||||
case daysFilter
|
case daysFilter
|
||||||
|
case firstLaunchDate
|
||||||
|
|
||||||
case contentViewCurrentSelectedHeaderViewBackDays
|
case contentViewCurrentSelectedHeaderViewBackDays
|
||||||
case contentViewHeaderTag
|
case contentViewHeaderTag
|
||||||
@@ -53,6 +54,7 @@ class UserDefaultsStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func moodMoodImagable() -> MoodImagable.Type {
|
static func moodMoodImagable() -> MoodImagable.Type {
|
||||||
|
return MoodImages.Emoji.moodImages
|
||||||
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.moodImages.rawValue) as? Int,
|
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.moodImages.rawValue) as? Int,
|
||||||
let model = MoodImages.init(rawValue: data) {
|
let model = MoodImages.init(rawValue: data) {
|
||||||
return model.moodImages
|
return model.moodImages
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ struct AddMoodHeaderView: View {
|
|||||||
}, label: {
|
}, label: {
|
||||||
mood.icon
|
mood.icon
|
||||||
.resizable()
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fit)
|
||||||
.frame(width: CGFloat(50), height: CGFloat(50), alignment: .center)
|
.frame(width: CGFloat(50), height: CGFloat(50), alignment: .center)
|
||||||
.foregroundColor(moodTint.color(forMood: mood))
|
.foregroundColor(moodTint.color(forMood: mood))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -88,8 +88,12 @@ struct DayView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onAppear{
|
||||||
|
iapManager.decideShowIAP()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// MARK: Views
|
// MARK: Views
|
||||||
public var mainView: some View {
|
public var mainView: some View {
|
||||||
VStack {
|
VStack {
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ 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(
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ struct PurchaseButtonView: View {
|
|||||||
}
|
}
|
||||||
@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()
|
||||||
|
|
||||||
var iapManager: IAPManager
|
var iapManager: IAPManager
|
||||||
|
|
||||||
private let height: Float
|
private let height: Float
|
||||||
|
private let showCountdownTimer: Bool
|
||||||
private let showManageSubClosure: (() -> Void)?
|
private let showManageSubClosure: (() -> Void)?
|
||||||
|
|
||||||
@State var isPurchasing: Bool = false
|
@State var isPurchasing: Bool = false
|
||||||
@@ -30,10 +33,11 @@ struct PurchaseButtonView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(height: Float, iapManager: IAPManager, showManageSubClosure: (() -> Void)? = nil) {
|
public init(height: Float, iapManager: IAPManager, showManageSubClosure: (() -> Void)? = nil, showCountdownTimer: Bool = false) {
|
||||||
self.height = height
|
self.height = height
|
||||||
self.showManageSubClosure = showManageSubClosure
|
self.showManageSubClosure = showManageSubClosure
|
||||||
self.iapManager = iapManager
|
self.iapManager = iapManager
|
||||||
|
self.showCountdownTimer = showCountdownTimer
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -95,6 +99,7 @@ struct PurchaseButtonView: View {
|
|||||||
private var buyOptionsView: some View {
|
private var buyOptionsView: some View {
|
||||||
VStack {
|
VStack {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
theme.currentTheme.secondaryBGColor
|
||||||
VStack(spacing: 20) {
|
VStack(spacing: 20) {
|
||||||
Text(String(localized: "purchase_view_title"))
|
Text(String(localized: "purchase_view_title"))
|
||||||
.font(.body)
|
.font(.body)
|
||||||
@@ -102,6 +107,34 @@ struct PurchaseButtonView: View {
|
|||||||
.foregroundColor(textColor)
|
.foregroundColor(textColor)
|
||||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
if showCountdownTimer {
|
||||||
|
if let date = Calendar.current.date(byAdding: .day, value: 30, to: firstLaunchDate) {
|
||||||
|
HStack {
|
||||||
|
if iapManager.daysLeftBeforeIAP > 0 {
|
||||||
|
Text(String(localized: "purchase_view_current_subscription_expires_in"))
|
||||||
|
.font(.body)
|
||||||
|
.bold()
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
|
Text(date, style: .relative)
|
||||||
|
.font(.body)
|
||||||
|
.bold()
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
} else {
|
||||||
|
Text(String(localized: "purchase_view_current_subscription_expired_on"))
|
||||||
|
.font(.body)
|
||||||
|
.bold()
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
|
||||||
|
Text(date, style: .date)
|
||||||
|
.font(.body)
|
||||||
|
.bold()
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||||
|
}
|
||||||
|
}
|
||||||
HStack {
|
HStack {
|
||||||
ForEach(iapManager.sortedSubscriptionKeysByPriceOptions) { product in
|
ForEach(iapManager.sortedSubscriptionKeysByPriceOptions) { product in
|
||||||
Button(action: {
|
Button(action: {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ struct SettingsView: View {
|
|||||||
@AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true
|
@AppStorage(UserDefaultsStore.Keys.deleteEnable.rawValue, store: GroupUserDefaults.groupDefaults) private var deleteEnabled = true
|
||||||
@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()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
@@ -56,6 +57,8 @@ struct SettingsView: View {
|
|||||||
// fixWeekday
|
// fixWeekday
|
||||||
exportData
|
exportData
|
||||||
importData
|
importData
|
||||||
|
editFirstLaunchDatePast
|
||||||
|
resetLaunchDate
|
||||||
Divider()
|
Divider()
|
||||||
}
|
}
|
||||||
Spacer()
|
Spacer()
|
||||||
@@ -73,6 +76,7 @@ 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
|
||||||
@@ -142,12 +146,12 @@ struct SettingsView: View {
|
|||||||
private var subscriptionInfoView: some View {
|
private var subscriptionInfoView: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
theme.currentTheme.secondaryBGColor
|
theme.currentTheme.secondaryBGColor
|
||||||
PurchaseButtonView(height: iapManager.currentSubscription != nil ? 300 : 175, iapManager: iapManager, showManageSubClosure: {
|
PurchaseButtonView(height: iapManager.currentSubscription != nil ? 300 : 200, iapManager: iapManager, showManageSubClosure: {
|
||||||
Task {
|
Task {
|
||||||
await
|
await
|
||||||
self.showManageSubscription()
|
self.showManageSubscription()
|
||||||
}
|
}
|
||||||
})
|
}, showCountdownTimer: true)
|
||||||
}
|
}
|
||||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||||
}
|
}
|
||||||
@@ -157,6 +161,7 @@ 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"))
|
||||||
@@ -214,6 +219,41 @@ struct SettingsView: View {
|
|||||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var editFirstLaunchDatePast: some View {
|
||||||
|
ZStack {
|
||||||
|
theme.currentTheme.secondaryBGColor
|
||||||
|
Button(action: {
|
||||||
|
var tmpDate = Date()
|
||||||
|
tmpDate = Calendar.current.date(byAdding: .day, value: -29, to: tmpDate)!
|
||||||
|
tmpDate = Calendar.current.date(byAdding: .hour, value: -23, to: tmpDate)!
|
||||||
|
tmpDate = Calendar.current.date(byAdding: .minute, value: -59, to: tmpDate)!
|
||||||
|
tmpDate = Calendar.current.date(byAdding: .second, value: -45, to: tmpDate)!
|
||||||
|
firstLaunchDate = tmpDate
|
||||||
|
}, label: {
|
||||||
|
Text("Set first launch date back 29 days, 23 hrs, 45 seconds")
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
})
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||||
|
}
|
||||||
|
|
||||||
|
private var resetLaunchDate: some View {
|
||||||
|
ZStack {
|
||||||
|
theme.currentTheme.secondaryBGColor
|
||||||
|
Button(action: {
|
||||||
|
firstLaunchDate = Date()
|
||||||
|
}, label: {
|
||||||
|
Text("Reset luanch date to current date")
|
||||||
|
.foregroundColor(textColor)
|
||||||
|
})
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||||
|
}
|
||||||
|
|
||||||
private var clearDB: some View {
|
private var clearDB: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
theme.currentTheme.secondaryBGColor
|
theme.currentTheme.secondaryBGColor
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ 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
|
||||||
|
|||||||
@@ -112,6 +112,8 @@
|
|||||||
"purchase_view_other_options" = "Other Options";
|
"purchase_view_other_options" = "Other Options";
|
||||||
"purchase_view_cancel" = "Cancel";
|
"purchase_view_cancel" = "Cancel";
|
||||||
"purchase_view_current_subscription" = "Current Subscription";
|
"purchase_view_current_subscription" = "Current Subscription";
|
||||||
|
"purchase_view_current_subscription_expires_in" = "Trial expires in:";
|
||||||
|
"purchase_view_current_subscription_expired_on" = "Trial expired on:";
|
||||||
|
|
||||||
/* not used */
|
/* not used */
|
||||||
"onboarding_title_title" = "What would you like the reminder to say?";
|
"onboarding_title_title" = "What would you like the reminder to say?";
|
||||||
|
|||||||
Reference in New Issue
Block a user