Adds yearly plan

main
Laurent 3 years ago
parent ea459d7a2a
commit f36323fea8
  1. 4
      LeCountdown.xcodeproj/project.pbxproj
  2. 8
      LeCountdown/Model/NSManagedContext+Extensions.swift
  3. 7
      LeCountdown/Sound/Sound.swift
  4. BIN
      LeCountdown/Sound_Assets/Stephan_Bodzin/Clave_Loop_LLL.wav
  5. 26
      LeCountdown/Subscription/AppGuard.swift
  6. 2
      LeCountdown/Subscription/Store.swift
  7. 70
      LeCountdown/Subscription/StoreView.swift
  8. 16
      LeCountdown/Views/PresetsView.swift
  9. 2
      LeCountdown/Views/Reusable/SoundSelectionView.swift
  10. 2
      LeCountdown/fr.lproj/Localizable.strings

@ -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 = "<group>"; };
C4BA2AE32995AC0D00CB4FBA /* Arpeggio_Loop_River.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = Arpeggio_Loop_River.wav; sourceTree = "<group>"; };
C4BA2AE52995AC3E00CB4FBA /* Loop_ToneSD_Boavista.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = Loop_ToneSD_Boavista.wav; sourceTree = "<group>"; };
C4BA2AE72995ACC200CB4FBA /* Clave_Loop_LLL.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = Clave_Loop_LLL.wav; sourceTree = "<group>"; };
C4BA2AE92995AD1C00CB4FBA /* SEM_Synths_Loop4_Nothing_Like_You.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = SEM_Synths_Loop4_Nothing_Like_You.wav; sourceTree = "<group>"; };
C4BA2AEB2996A09600CB4FBA /* LeCountdown.0.5.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.5.1.xcdatamodel; sourceTree = "<group>"; };
C4BA2AED2996A11900CB4FBA /* CustomSound+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CustomSound+CoreDataClass.swift"; sourceTree = "<group>"; };
@ -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 */,

@ -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<NSFetchRequestResult>(entityName: entityName)
return try self.count(for: fetchRequest)
do {
return try self.count(for: fetchRequest)
} catch {
return 0
}
}
}

@ -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

@ -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
}
}
}

@ -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()

@ -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())
}
}

@ -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 {

@ -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)

@ -255,3 +255,5 @@
"Play cancellation sound" = "Jouer son d'annulation";
"Contact us" = "Contactez-nous";
"Confirmation" = "Confirmation";
"month" = "mois";
"year" = "an";

Loading…
Cancel
Save