IAP
This commit is contained in:
@@ -16,7 +16,8 @@ struct FeelsApp: App {
|
||||
|
||||
let persistenceController = PersistenceController.shared
|
||||
@StateObject var iapManager = IAPManager()
|
||||
|
||||
@AppStorage(UserDefaultsStore.Keys.firstLaunchDate.rawValue, store: GroupUserDefaults.groupDefaults) private var firstLaunchDate = Date()
|
||||
|
||||
init() {
|
||||
BGTaskScheduler.shared.cancelAllTaskRequests()
|
||||
BGTaskScheduler.shared.register(forTaskWithIdentifier: BGTask.updateDBMissingID, using: nil) { (task) in
|
||||
|
||||
@@ -20,7 +20,8 @@ public enum StoreError: Error {
|
||||
class IAPManager: ObservableObject {
|
||||
@Published private(set) var showIAP = false
|
||||
@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] {
|
||||
subscriptions.keys.sorted(by: {
|
||||
$0.price < $1.price
|
||||
@@ -74,23 +75,52 @@ 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
|
||||
}
|
||||
|
||||
private let iapIdentifiers = Set([
|
||||
"com.88oakapps.ifeel.IAP.subscription.weekly",
|
||||
"com.88oakapps.ifeel.IAP.subscription.monthly",
|
||||
"com.88oakapps.ifeel.IAP.subscription.yearly"
|
||||
])
|
||||
|
||||
var expireOnTimer: Timer?
|
||||
|
||||
init() {
|
||||
//Start a transaction listener as close to app launch as possible so you don't miss any transactions.
|
||||
updateListenerTask = listenForTransactions()
|
||||
|
||||
refresh()
|
||||
|
||||
setUpdateTimer()
|
||||
}
|
||||
|
||||
deinit {
|
||||
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() {
|
||||
Task {
|
||||
//During store initialization, request products from the App Store.
|
||||
@@ -104,16 +134,25 @@ class IAPManager: ObservableObject {
|
||||
}
|
||||
|
||||
func decideShowIAP() {
|
||||
guard !subscriptions.isEmpty else {
|
||||
return
|
||||
}
|
||||
var tmpShowIAP = true
|
||||
// if we have a sub in the subscriptions dict
|
||||
// then we dont need to show
|
||||
for (_, value) in self.subscriptions {
|
||||
if value != nil {
|
||||
tmpShowIAP = false
|
||||
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 {
|
||||
self.showIAP = tmpShowIAP
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ class UserDefaultsStore {
|
||||
case showNSFW
|
||||
case shape
|
||||
case daysFilter
|
||||
case firstLaunchDate
|
||||
|
||||
case contentViewCurrentSelectedHeaderViewBackDays
|
||||
case contentViewHeaderTag
|
||||
@@ -53,6 +54,7 @@ class UserDefaultsStore {
|
||||
}
|
||||
|
||||
static func moodMoodImagable() -> MoodImagable.Type {
|
||||
return MoodImages.Emoji.moodImages
|
||||
if let data = GroupUserDefaults.groupDefaults.object(forKey: UserDefaultsStore.Keys.moodImages.rawValue) as? Int,
|
||||
let model = MoodImages.init(rawValue: data) {
|
||||
return model.moodImages
|
||||
|
||||
@@ -40,6 +40,7 @@ struct AddMoodHeaderView: View {
|
||||
}, label: {
|
||||
mood.icon
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: CGFloat(50), height: CGFloat(50), alignment: .center)
|
||||
.foregroundColor(moodTint.color(forMood: mood))
|
||||
})
|
||||
|
||||
@@ -88,8 +88,12 @@ struct DayView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear{
|
||||
iapManager.decideShowIAP()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Views
|
||||
public var mainView: some View {
|
||||
VStack {
|
||||
|
||||
@@ -85,6 +85,7 @@ struct MonthView: View {
|
||||
}
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_month_view")
|
||||
iapManager.decideShowIAP()
|
||||
})
|
||||
.padding([.top])
|
||||
.background(
|
||||
|
||||
@@ -17,9 +17,12 @@ struct PurchaseButtonView: View {
|
||||
}
|
||||
@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()
|
||||
|
||||
var iapManager: IAPManager
|
||||
|
||||
private let height: Float
|
||||
private let showCountdownTimer: Bool
|
||||
private let showManageSubClosure: (() -> Void)?
|
||||
|
||||
@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.showManageSubClosure = showManageSubClosure
|
||||
self.iapManager = iapManager
|
||||
self.showCountdownTimer = showCountdownTimer
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -95,6 +99,7 @@ struct PurchaseButtonView: View {
|
||||
private var buyOptionsView: some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
VStack(spacing: 20) {
|
||||
Text(String(localized: "purchase_view_title"))
|
||||
.font(.body)
|
||||
@@ -102,6 +107,34 @@ struct PurchaseButtonView: View {
|
||||
.foregroundColor(textColor)
|
||||
.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 {
|
||||
ForEach(iapManager.sortedSubscriptionKeysByPriceOptions) { product in
|
||||
Button(action: {
|
||||
|
||||
@@ -28,6 +28,7 @@ struct SettingsView: View {
|
||||
@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.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 {
|
||||
ScrollView {
|
||||
@@ -56,6 +57,8 @@ struct SettingsView: View {
|
||||
// fixWeekday
|
||||
exportData
|
||||
importData
|
||||
editFirstLaunchDatePast
|
||||
resetLaunchDate
|
||||
Divider()
|
||||
}
|
||||
Spacer()
|
||||
@@ -73,6 +76,7 @@ struct SettingsView: View {
|
||||
}
|
||||
.onAppear(perform: {
|
||||
EventLogger.log(event: "show_settings_view")
|
||||
iapManager.setUpdateTimer()
|
||||
})
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
@@ -142,12 +146,12 @@ struct SettingsView: View {
|
||||
private var subscriptionInfoView: some View {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
PurchaseButtonView(height: iapManager.currentSubscription != nil ? 300 : 175, iapManager: iapManager, showManageSubClosure: {
|
||||
PurchaseButtonView(height: iapManager.currentSubscription != nil ? 300 : 200, iapManager: iapManager, showManageSubClosure: {
|
||||
Task {
|
||||
await
|
||||
self.showManageSubscription()
|
||||
}
|
||||
})
|
||||
}, showCountdownTimer: true)
|
||||
}
|
||||
.cornerRadius(Constants.viewsCornerRaidus, corners: [.topLeft, .topRight, .bottomLeft, .bottomRight])
|
||||
}
|
||||
@@ -157,6 +161,7 @@ struct SettingsView: View {
|
||||
Spacer()
|
||||
Button(action: {
|
||||
EventLogger.log(event: "tap_settings_close")
|
||||
iapManager.decideShowIAP()
|
||||
dismiss()
|
||||
}, label: {
|
||||
Text(String(localized: "settings_view_exit"))
|
||||
@@ -214,6 +219,41 @@ struct SettingsView: View {
|
||||
.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 {
|
||||
ZStack {
|
||||
theme.currentTheme.secondaryBGColor
|
||||
|
||||
@@ -66,6 +66,7 @@ struct YearView: View {
|
||||
}
|
||||
.onAppear(perform: {
|
||||
self.viewModel.filterEntries(startDate: Date(timeIntervalSince1970: 0), endDate: Date())
|
||||
iapManager.decideShowIAP()
|
||||
})
|
||||
.background(
|
||||
theme.currentTheme.bg
|
||||
|
||||
@@ -112,6 +112,8 @@
|
||||
"purchase_view_other_options" = "Other Options";
|
||||
"purchase_view_cancel" = "Cancel";
|
||||
"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 */
|
||||
"onboarding_title_title" = "What would you like the reminder to say?";
|
||||
|
||||
Reference in New Issue
Block a user