diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index 19c384d..7d133c7 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -144,7 +144,6 @@ C4BA2AE22995ABE700CB4FBA /* SquareArp_Loop_River.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4BA2AE12995ABE700CB4FBA /* SquareArp_Loop_River.wav */; }; C4BA2AE42995AC0D00CB4FBA /* Arpeggio_Loop_River.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4BA2AE32995AC0D00CB4FBA /* Arpeggio_Loop_River.wav */; }; C4BA2AE62995AC3F00CB4FBA /* Loop_ToneSD_Boavista.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4BA2AE52995AC3E00CB4FBA /* Loop_ToneSD_Boavista.wav */; }; - C4BA2AE82995ACC200CB4FBA /* Clave_Loop_LLL.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4BA2AE72995ACC200CB4FBA /* Clave_Loop_LLL.wav */; }; C4BA2AEA2995AD1C00CB4FBA /* SEM_Synths_Loop4_Nothing_Like_You.wav in Resources */ = {isa = PBXBuildFile; fileRef = C4BA2AE92995AD1C00CB4FBA /* SEM_Synths_Loop4_Nothing_Like_You.wav */; }; C4BA2AF12996A11900CB4FBA /* CustomSound+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */; }; C4BA2AF22996A11900CB4FBA /* CustomSound+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4BA2AEE2996A11900CB4FBA /* CustomSound+CoreDataProperties.swift */; }; @@ -406,7 +405,6 @@ C4BA2AE12995ABE700CB4FBA /* SquareArp_Loop_River.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = SquareArp_Loop_River.wav; sourceTree = ""; }; C4BA2AE32995AC0D00CB4FBA /* Arpeggio_Loop_River.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = Arpeggio_Loop_River.wav; sourceTree = ""; }; C4BA2AE52995AC3E00CB4FBA /* Loop_ToneSD_Boavista.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = Loop_ToneSD_Boavista.wav; sourceTree = ""; }; - C4BA2AE72995ACC200CB4FBA /* Clave_Loop_LLL.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = Clave_Loop_LLL.wav; sourceTree = ""; }; C4BA2AE92995AD1C00CB4FBA /* SEM_Synths_Loop4_Nothing_Like_You.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = SEM_Synths_Loop4_Nothing_Like_You.wav; sourceTree = ""; }; C4BA2AEB2996A09600CB4FBA /* LeCountdown.0.5.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.5.1.xcdatamodel; sourceTree = ""; }; C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomSound+CoreDataClass.swift"; sourceTree = ""; }; @@ -780,7 +778,6 @@ children = ( C4E5D67B29B8D4A5008E7465 /* Low_Tom_Disto_Earth.wav */, C4BA2AE92995AD1C00CB4FBA /* SEM_Synths_Loop4_Nothing_Like_You.wav */, - C4BA2AE72995ACC200CB4FBA /* Clave_Loop_LLL.wav */, C4BA2AE52995AC3E00CB4FBA /* Loop_ToneSD_Boavista.wav */, C4BA2AE32995AC0D00CB4FBA /* Arpeggio_Loop_River.wav */, C4BA2AE12995ABE700CB4FBA /* SquareArp_Loop_River.wav */, @@ -1094,7 +1091,6 @@ C4A16DAD29D0AB1F00143D5E /* ESM_MVG_fx_ui_one_shot_cancel_clicky_reverb_digital_long.wav in Resources */, C4BA2ADE2995ABA800CB4FBA /* MatriarchFxs_Loop2_Collider.wav in Resources */, C415D3EF29C376DD0037B215 /* QP01 0017 Stream sparkling.wav in Resources */, - C4BA2AE82995ACC200CB4FBA /* Clave_Loop_LLL.wav in Resources */, C4E5D67C29B8D4A5008E7465 /* Low_Tom_Disto_Earth.wav in Resources */, C4BA2AE02995ABD200CB4FBA /* HighChords_Loop_River.wav in Resources */, C4E5D66D29B753D7008E7465 /* AppShortcuts.strings in Resources */, diff --git a/LeCountdown/Model/NSManagedContext+Extensions.swift b/LeCountdown/Model/NSManagedContext+Extensions.swift index ad1c790..bbbaadf 100644 --- a/LeCountdown/Model/NSManagedContext+Extensions.swift +++ b/LeCountdown/Model/NSManagedContext+Extensions.swift @@ -38,9 +38,13 @@ extension NSManagedObjectContext { return try self.fetch(request) } - func count(entityName: String) throws -> Int { + func count(entityName: String) -> Int { let fetchRequest = NSFetchRequest(entityName: entityName) - return try self.count(for: fetchRequest) + do { + return try self.count(for: fetchRequest) + } catch { + return 0 + } } } diff --git a/LeCountdown/Sound/Sound.swift b/LeCountdown/Sound/Sound.swift index 9783256..d16abcd 100644 --- a/LeCountdown/Sound/Sound.swift +++ b/LeCountdown/Sound/Sound.swift @@ -79,7 +79,6 @@ enum Sound: Int, CaseIterable, Identifiable, Localized { // StephanBodzin case sbSEM_Synths_Loop4_Nothing_Like_You - case sbClave_Loop_LLL case sbLoop_ToneSD_Boavista case sbArpeggio_Loop_River case sbSquareArp_Loop_River @@ -112,7 +111,6 @@ enum Sound: Int, CaseIterable, Identifiable, Localized { var localizedString: String { switch self { case .sbSEM_Synths_Loop4_Nothing_Like_You: return "Nothing Like You" - case .sbClave_Loop_LLL: return "LLL" case .sbLoop_ToneSD_Boavista: return "Boavista" case .sbArpeggio_Loop_River: return "River 1" case .sbSquareArp_Loop_River: return "River 2" @@ -142,7 +140,6 @@ enum Sound: Int, CaseIterable, Identifiable, Localized { var fileName: String { switch self { case .sbSEM_Synths_Loop4_Nothing_Like_You: return "SEM_Synths_Loop4_Nothing_Like_You.wav" - case .sbClave_Loop_LLL: return "Clave_Loop_LLL.wav" case .sbLoop_ToneSD_Boavista: return "Loop_ToneSD_Boavista.wav" case .sbArpeggio_Loop_River: return "Arpeggio_Loop_River.wav" case .sbSquareArp_Loop_River: return "SquareArp_Loop_River.wav" @@ -171,7 +168,7 @@ enum Sound: Int, CaseIterable, Identifiable, Localized { var playlist: Playlist { switch self { - case .sbSEM_Synths_Loop4_Nothing_Like_You, .sbClave_Loop_LLL, .sbLoop_ToneSD_Boavista, .sbArpeggio_Loop_River, .sbSquareArp_Loop_River, .sbHighChords_Loop_River, .sbMatriarchFxs_Loop2_Collider: + case .sbSEM_Synths_Loop4_Nothing_Like_You, .sbLoop_ToneSD_Boavista, .sbArpeggio_Loop_River, .sbSquareArp_Loop_River, .sbHighChords_Loop_River, .sbMatriarchFxs_Loop2_Collider: return .stephanBodzin case .FF_SH_bowl_drone_tapping_C, .FF_SH_bowl_drone_tap_hold_E, .EX_ATSM_Koshi_Chimes_Aria_Tuning_Texture_Longer_Dm, .EX_ATSM_Bell_Binaural_Flam_Eb, .EX_ATSM_160_Metal_Tonal_Percussion_Sansula_Loop_Call_Am, .EX_ATSM_125_Metal_Percussion_Wing_Loop_Chimey_Dm, .EX_ATSM_140_Koshi_Chimes_Aria_Tuning_Loop_Wondering_Am, .EX_ATSM_20_Inch_Highwall_Bowl_Hit_Ring_Ab: return .relax @@ -184,7 +181,7 @@ enum Sound: Int, CaseIterable, Identifiable, Localized { var isRestricted: Bool { switch self { - case .sbSEM_Synths_Loop4_Nothing_Like_You: + case .sbSEM_Synths_Loop4_Nothing_Like_You, .sbLoop_ToneSD_Boavista, .FF_SH_bowl_drone_tapping_C, .EX_ATSM_Bell_Binaural_Flam_Eb, .tropicalForestMorning, .rain_soft: return false default: return true diff --git a/LeCountdown/Sound_Assets/Stephan_Bodzin/Clave_Loop_LLL.wav b/LeCountdown/Sound_Assets/Stephan_Bodzin/Clave_Loop_LLL.wav deleted file mode 100644 index b5b8f7a..0000000 Binary files a/LeCountdown/Sound_Assets/Stephan_Bodzin/Clave_Loop_LLL.wav and /dev/null differ diff --git a/LeCountdown/Subscription/AppGuard.swift b/LeCountdown/Subscription/AppGuard.swift index 8ef3999..17cf484 100644 --- a/LeCountdown/Subscription/AppGuard.swift +++ b/LeCountdown/Subscription/AppGuard.swift @@ -14,7 +14,16 @@ public enum StoreError: Error { enum StorePlan: String, CaseIterable { case none - case unlimited = "com.staxriver.enchant.unlimited" + case monthly = "com.staxriver.enchant.monthly" + case yearly = "com.staxriver.enchant.yearly" + + var formattedPeriod: String { + switch self { + case .none: return "" + case .monthly: return NSLocalizedString("month", comment: "") + case .yearly: return NSLocalizedString("year", comment: "") + } + } } extension Notification.Name { @@ -115,15 +124,16 @@ extension Notification.Name { } var isAuthorized: Bool { - return self.currentPlan == .unlimited + return false //self.currentPlan != .none } var currentPlan: StorePlan { #if DEBUG - return .unlimited + return .yearly #else - if let currentBestPlan = self.currentBestPlan, let plan = StorePlan(rawValue: currentBestPlan.productID) { + if let currentBestPlan = self.currentBestPlan, + let plan = StorePlan(rawValue: currentBestPlan.productID) { return plan } return .none @@ -131,13 +141,13 @@ extension Notification.Name { } fileprivate func _updateBestPlan() { - - if let unlimited = self.purchasedTransactions.first(where: { $0.productID == StorePlan.unlimited.rawValue }) { - self.currentBestPlan = unlimited + if let monthly = self.purchasedTransactions.first(where: { $0.productID == StorePlan.monthly.rawValue }) { + self.currentBestPlan = monthly + } else if let yearly = self.purchasedTransactions.first(where: { $0.productID == StorePlan.yearly.rawValue }) { + self.currentBestPlan = yearly } else { self.currentBestPlan = nil } - } } diff --git a/LeCountdown/Subscription/Store.swift b/LeCountdown/Subscription/Store.swift index c7136cb..7f454ef 100644 --- a/LeCountdown/Subscription/Store.swift +++ b/LeCountdown/Subscription/Store.swift @@ -44,7 +44,7 @@ class Store: ObservableObject { @MainActor func requestProducts() async { do { - let identifiers: [String] = [StorePlan.unlimited.rawValue] + let identifiers: [String] = [StorePlan.monthly.rawValue, StorePlan.yearly.rawValue] products = try await Product.products(for: identifiers) Logger.log("products = \(self.products.count)") self.delegate?.productsReceived() diff --git a/LeCountdown/Subscription/StoreView.swift b/LeCountdown/Subscription/StoreView.swift index d5b1d3f..b1d0a12 100644 --- a/LeCountdown/Subscription/StoreView.swift +++ b/LeCountdown/Subscription/StoreView.swift @@ -47,13 +47,9 @@ struct StoreView: View, StoreDelegate { var body: some View { - if let product = self.store.products.first { - - PlanView(productName: product.displayName, - price: product.displayPrice) { - self._purchase() - } - + if !self.store.products.isEmpty { + PlanView() + .environmentObject(self.store) } else { ProgressView() .progressViewStyle(.circular) @@ -61,16 +57,6 @@ struct StoreView: View, StoreDelegate { } - fileprivate func _purchase() { - - if let product = self.store.products.first { - Task { - try await store.purchase(product) - } - } - - } - // MARK: - StoreDelegate func productsReceived() { @@ -85,16 +71,16 @@ struct StoreView: View, StoreDelegate { struct PlanView: View { - var productName: String - var price: String + @EnvironmentObject var store: Store - var actionHandler: () -> () +// var productName: String +// var price: String var body: some View { VStack { - Text(productName) + Text("Permanent enchantment") .font(.title) .fontWeight(.bold) .padding() @@ -116,24 +102,40 @@ struct PlanView: View { PlayerWrapperView() - Button { - self.actionHandler() - } label: { - HStack { - Spacer() - VStack { - Text("Purchase") - Text(self.price).font(.title3) + Text("Purchase").font(.title) + + ForEach(self.store.products) { product in + + Button { + self._purchase(product: product) + } label: { + HStack { + Spacer() + VStack { + + if let plan = StorePlan(rawValue: product.id) { + Text("\(product.displayPrice) / \(plan.formattedPeriod)").font(.title3) + } else { + Text("Plan not found") + } + + } + Spacer() } - Spacer() } + .buttonStyle(.borderedProminent) + .fontWeight(.medium) } - .font(.title) - .buttonStyle(.borderedProminent).fontWeight(.medium) }.padding() } + fileprivate func _purchase(product: Product) { + Task { + try await store.purchase(product) + } + } + } struct PlayerWrapperView: View { @@ -194,8 +196,6 @@ struct PlayerWrapperView: View { struct StoreView_Previews: PreviewProvider { static var previews: some View { - PlanView(productName: "Pro version", - price: "$0.99 / month", - actionHandler: {}) + PlanView().environmentObject(Store()) } } diff --git a/LeCountdown/Views/PresetsView.swift b/LeCountdown/Views/PresetsView.swift index cdbf3e5..6a6b80f 100644 --- a/LeCountdown/Views/PresetsView.swift +++ b/LeCountdown/Views/PresetsView.swift @@ -18,6 +18,8 @@ struct PresetsView: View { @Environment(\.managedObjectContext) private var viewContext @StateObject var model: PresetModel = PresetModel() + + @State var isShowingSubscription: Bool = false @State var isPresented: Bool = false @@ -50,7 +52,7 @@ struct PresetsView: View { VStack(alignment: .leading, spacing: 10.0) { Button { - self.isShowingNewCountdown = true + self._showNewCountdown() } label: { Text(NSLocalizedString("Create countdown", comment: "").uppercased()) .frame(maxWidth: .infinity) @@ -110,8 +112,20 @@ struct PresetsView: View { CountdownEditView(isPresented: $isPresented, preset: self.model.selectedPreset, tabSelection: self.tabSelection) .environment(\.managedObjectContext, viewContext) }) + .sheet(isPresented: $isShowingSubscription, content: { + StoreView() + }) .navigationTitle("Create") } + + fileprivate func _showNewCountdown() { + if AppGuard.main.isAuthorized || viewContext.count(entityName: "AbstractTimer") < 4 { + self.isShowingNewCountdown = true + } else { + self.isShowingSubscription = true + } + } + } struct TimerItemView: View { diff --git a/LeCountdown/Views/Reusable/SoundSelectionView.swift b/LeCountdown/Views/Reusable/SoundSelectionView.swift index 0a81338..b37cb4e 100644 --- a/LeCountdown/Views/Reusable/SoundSelectionView.swift +++ b/LeCountdown/Views/Reusable/SoundSelectionView.swift @@ -46,7 +46,7 @@ struct PlaylistSectionView: View { self._playSound(sound) } Spacer() - if AppGuard.main.isAuthorized { + if !sound.isRestricted || AppGuard.main.isAuthorized { RightAlignToggleRow(item: sound, selected: self.model.binding(sound: sound), keyPath: \.formattedDuration) { selected in self.model.selectSound(sound, selected: selected) }.frame(width: 120.0) diff --git a/LeCountdown/fr.lproj/Localizable.strings b/LeCountdown/fr.lproj/Localizable.strings index 4d777e4..9bbe9e1 100644 --- a/LeCountdown/fr.lproj/Localizable.strings +++ b/LeCountdown/fr.lproj/Localizable.strings @@ -255,3 +255,5 @@ "Play cancellation sound" = "Jouer son d'annulation"; "Contact us" = "Contactez-nous"; "Confirmation" = "Confirmation"; +"month" = "mois"; +"year" = "an";