# HG changeset patch # User Dennis C. M. # Date 1665568049 -7200 # Node ID bdfff35dd43cb946bc2e3c12120a71c1df19556d # Parent ce7ea84f67f562a1c3ccd65dbd2dc75b17fb70c8 implement RevenueCat diff -r ce7ea84f67f5 -r bdfff35dd43c GeoQuiz.xcodeproj/project.pbxproj --- a/GeoQuiz.xcodeproj/project.pbxproj Sun Oct 09 19:46:44 2022 +0200 +++ b/GeoQuiz.xcodeproj/project.pbxproj Wed Oct 12 11:47:29 2022 +0200 @@ -42,6 +42,8 @@ 95C430F928D0A8E500480D23 /* GradientExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C430F828D0A8E500480D23 /* GradientExtension.swift */; }; 95C4315628C64A8C00212131 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C4315528C64A8C00212131 /* ContentView.swift */; }; 95C4315928C6500000212131 /* GameButtonHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C4315828C6500000212131 /* GameButtonHelper.swift */; }; + 95CA294028F5769700CE0B7A /* GameModeEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CA293F28F5769700CE0B7A /* GameModeEnum.swift */; }; + 95CA295028F6BB4500CE0B7A /* ActivityAlertHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95CA294F28F6BB4500CE0B7A /* ActivityAlertHelper.swift */; }; 95FA409A28D9876B00129B60 /* GuessTheFlagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FA409928D9876B00129B60 /* GuessTheFlagView.swift */; }; 95FA409C28D9881100129B60 /* CountryGameClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95FA409B28D9881100129B60 /* CountryGameClass.swift */; }; /* End PBXBuildFile section */ @@ -82,6 +84,8 @@ 95C430F828D0A8E500480D23 /* GradientExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GradientExtension.swift; sourceTree = ""; }; 95C4315528C64A8C00212131 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 95C4315828C6500000212131 /* GameButtonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameButtonHelper.swift; sourceTree = ""; }; + 95CA293F28F5769700CE0B7A /* GameModeEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameModeEnum.swift; sourceTree = ""; }; + 95CA294F28F6BB4500CE0B7A /* ActivityAlertHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityAlertHelper.swift; sourceTree = ""; }; 95E6188428DDDB5C003359ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 95FA409928D9876B00129B60 /* GuessTheFlagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuessTheFlagView.swift; sourceTree = ""; }; 95FA409B28D9881100129B60 /* CountryGameClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryGameClass.swift; sourceTree = ""; }; @@ -113,6 +117,7 @@ 95919DB528F076BF00F21F8F /* UserClass.swift */, 95197EFC28F339AE00FE67E9 /* StoreKitRCClass.swift */, 95AE8D5628C8750E0067F219 /* LoadFunc.swift */, + 95CA293F28F5769700CE0B7A /* GameModeEnum.swift */, ); path = Logic; sourceTree = ""; @@ -192,6 +197,7 @@ 955A658028D703EB00CEEC6D /* GameToolbarHelper.swift */, 95BC392C28EC42570049AB49 /* CityMapHelper.swift */, 95919DBB28F08D0600F21F8F /* LinkHelper.swift */, + 95CA294F28F6BB4500CE0B7A /* ActivityAlertHelper.swift */, 952E41E828DC521200198643 /* GameAlertsModifier.swift */, 95C430F828D0A8E500480D23 /* GradientExtension.swift */, 951D197228D485E000671FAD /* ColorExtension.swift */, @@ -300,7 +306,9 @@ 951AFAEF28E565FE00A4A4BD /* CountryModel.swift in Sources */, 95030CEA28D1BA4D001AA3A1 /* AnswerButtonHelper.swift in Sources */, 95FA409C28D9881100129B60 /* CountryGameClass.swift in Sources */, + 95CA295028F6BB4500CE0B7A /* ActivityAlertHelper.swift in Sources */, 955A658128D703EB00CEEC6D /* GameToolbarHelper.swift in Sources */, + 95CA294028F5769700CE0B7A /* GameModeEnum.swift in Sources */, 95AE8D5728C8750E0067F219 /* LoadFunc.swift in Sources */, 9590359528E098FF00B24560 /* ProfileModalView.swift in Sources */, 955950BB28F15FF2001BDEE8 /* FormatterExtension.swift in Sources */, @@ -452,6 +460,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.dennistech.GeoQuiz; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -484,6 +493,7 @@ "$(inherited)", "@executable_path/Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = io.dennistech.GeoQuiz; PRODUCT_NAME = "$(TARGET_NAME)"; diff -r ce7ea84f67f5 -r bdfff35dd43c GeoQuiz/BuyPremiumModalView.swift --- a/GeoQuiz/BuyPremiumModalView.swift Sun Oct 09 19:46:44 2022 +0200 +++ b/GeoQuiz/BuyPremiumModalView.swift Wed Oct 12 11:47:29 2022 +0200 @@ -9,79 +9,89 @@ struct BuyPremiumModalView: View { @Environment(\.dismiss) var dismiss - @StateObject var storeKitRC = StoreKitRC() + @ObservedObject var storeKitRC: StoreKitRC var body: some View { NavigationView { - ScrollView(showsIndicators: false) { - VStack(alignment: .center, spacing: 20) { - VStack(spacing: 20) { - Text("Unlock Premium 🤩") - .font(.largeTitle.bold()) + ZStack { + ScrollView(showsIndicators: false) { + VStack(alignment: .center, spacing: 20) { + VStack(spacing: 20) { + Text("Unlock all games 🤩") + .font(.largeTitle.bold()) + + Text("Unlock three more game modes to become a geography master and support the future development of GeoQuiz.") + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + .frame(maxWidth: 400) + } + .padding() + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 20) { + Group { + Image("GuessTheCapital") + .resizable() + + Image("GuessTheCountry") + .resizable() + + Image("GuessThePopulation") + .resizable() + } + .scaledToFit() + .cornerRadius(25) + .frame(height: 500) + } + .padding() + } - Text("Unlock three more game modes to become a geography master and support the future development of GeoQuiz.") - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - .frame(maxWidth: 400) - } - .padding() - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: 20) { - Group { - Image("GuessTheCapital") - .resizable() - - Image("GuessTheCountry") - .resizable() - - Image("GuessThePopulation") - .resizable() + VStack(spacing: 10) { + Text("A one-time payment.") + .font(.title) + .fontWeight(.semibold) + + Text("No subscriptions.") + .font(.title2) + .fontWeight(.semibold) + .foregroundColor(.secondary) + + VStack { + if let package = storeKitRC.offerings?.current?.lifetime { + Button { + storeKitRC.buy(package) + } label: { + Text("Buy for \(package.storeProduct.localizedPriceString)") + .font(.headline) + .padding() + } + .buttonStyle(.borderedProminent) + .padding(.top) + } else { + ProgressView() + } } - .scaledToFit() - .cornerRadius(25) - .frame(height: 500) + + Button("Restore purchases", action: storeKitRC.restorePurchase) } .padding() + + VStack { + Text("GeoQuiz is an indie game") + Text("I appreciate your support ❤️") + } + .font(.callout) + .foregroundColor(.secondary) + .padding() } - - VStack(spacing: 10) { - Text("A one-time payment.") - .font(.title) - .fontWeight(.semibold) - - Text("No subscriptions.") - .font(.title2) - .fontWeight(.semibold) - .foregroundColor(.secondary) - - if let productPrice = storeKitRC.productPrice { - Button { - // Buy - } label: { - Text("Buy for \(productPrice)") - .font(.headline) - .padding() - } - .buttonStyle(.borderedProminent) - .padding(.top) - } else { - ProgressView() - .padding(.top) - } - } - .padding() - - VStack { - Text("GeoQuiz is an indie game") - Text("I appreciate your support ❤️") - } - .font(.callout) - .foregroundColor(.secondary) - .padding() + } + + if storeKitRC.showingActivityAlert { + ActivityAlert() } } .navigationBarTitleDisplayMode(.inline) + .onAppear(perform: storeKitRC.fetchOfferings) .toolbar { ToolbarItem(placement: .cancellationAction) { Button { @@ -91,17 +101,26 @@ } } } - .alert("Something went wrong 🤕", isPresented: $storeKitRC.showingErrorAlert) { - Button("OK", role: .cancel) { dismiss() } - } message: { - Text(storeKitRC.errorMessage) - } + } + .disabled(storeKitRC.showingActivityAlert) + .interactiveDismissDisabled(storeKitRC.showingActivityAlert) + + .alert(storeKitRC.errorAlertTitle, isPresented: $storeKitRC.showingErrorAlert) { + Button("OK", role: .cancel) { } + } message: { + Text(storeKitRC.errorAlertMessage) + } + + .alert("GeoQuiz Premium is active!", isPresented: $storeKitRC.showingSuccessAlert) { + Button("OK", role: .cancel) { dismiss() } + } message: { + Text("Thanks for supporting indie apps ❤️") } } } struct BuyPremiumModalView_Previews: PreviewProvider { static var previews: some View { - BuyPremiumModalView() + BuyPremiumModalView(storeKitRC: StoreKitRC()) } } diff -r ce7ea84f67f5 -r bdfff35dd43c GeoQuiz/Components/ActivityAlertHelper.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/GeoQuiz/Components/ActivityAlertHelper.swift Wed Oct 12 11:47:29 2022 +0200 @@ -0,0 +1,26 @@ +// +// ActivityAlertHelper.swift +// GeoQuiz +// +// Created by Dennis Concepción Martín on 12/10/22. +// + +import SwiftUI + +struct ActivityAlert: View { + var body: some View { + VStack(spacing: 10) { + ProgressView() + Text("Loading") + } + .padding() + .background(.regularMaterial) + .cornerRadius(10) + } +} + +struct ActivityAlert_Previews: PreviewProvider { + static var previews: some View { + ActivityAlert() + } +} diff -r ce7ea84f67f5 -r bdfff35dd43c GeoQuiz/ContentView.swift --- a/GeoQuiz/ContentView.swift Sun Oct 09 19:46:44 2022 +0200 +++ b/GeoQuiz/ContentView.swift Wed Oct 12 11:47:29 2022 +0200 @@ -8,47 +8,113 @@ import SwiftUI struct ContentView: View { + @State private var gameModeSelection: GameMode? = nil + @State private var showingBuyPremiumModalView = false @State private var showingSettingsModalView = false @State private var showingProfileModalView = false + @StateObject var storeKitRC = StoreKitRC() + var body: some View { NavigationView { - ScrollView(showsIndicators: false) { - VStack(alignment: .leading, spacing: 30) { - Text("Select a game 🎮") - .font(.largeTitle.bold()) - .padding(.bottom) - - NavigationLink(destination: GuessTheFlagView()) { - GameButton( - gradient: .main, - level: "Level 1", symbol: "flag.fill", name: "Guess the flag" - ) + VStack { + NavigationLink( + destination: GuessTheFlagView(), + tag: GameMode.guessTheFlag, + selection: $gameModeSelection) + { + EmptyView() + } + + NavigationLink( + destination: GuessTheCapitalView(), + tag: GameMode.guessTheCapital, + selection: $gameModeSelection) + { + EmptyView() + } + + NavigationLink( + destination: GuessTheCountryView(), + tag: GameMode.guessTheCountry, + selection: $gameModeSelection) + { + EmptyView() + } + + NavigationLink( + destination: GuessThePopulationView(), + tag: GameMode.guessThePopulation, + selection: $gameModeSelection) + { + EmptyView() + } + + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 30) { + Text("Select a game 🎮") + .font(.largeTitle.bold()) + .padding(.bottom) + + Button { + gameModeSelection = .guessTheFlag + } label: { + GameButton( + gradient: .main, + level: "Level 1", + symbol: "flag.fill", + name: "Guess the flag" + ) + } + + Button { + if storeKitRC.isActive { + gameModeSelection = .guessTheCapital + } else { + showingBuyPremiumModalView = true + } + } label: { + GameButton( + gradient: .secondary, + level: "Level 2", + symbol: storeKitRC.isActive ? "building.2.fill": "lock.fill", + name: "Guess the capital" + ) + } + + Button { + if storeKitRC.isActive { + gameModeSelection = .guessTheCountry + } else { + showingBuyPremiumModalView = true + } + } label: { + GameButton( + gradient: .tertiary, + level: "Level 3", + symbol: storeKitRC.isActive ? "globe.americas.fill": "lock.fill", + name: "Guess the country" + ) + } + + Button { + if storeKitRC.isActive { + gameModeSelection = .guessThePopulation + } else { + showingBuyPremiumModalView = true + } + } label: { + GameButton( + gradient: .quaternary, + level: "Level 4", + symbol: storeKitRC.isActive ? "person.fill": "lock.fill", + name: "Guess the population" + ) + } } - - NavigationLink(destination: GuessTheCapitalView()) { - GameButton( - gradient: .secondary, - level: "Level 2", symbol: "building.2.fill", name: "Guess the capital" - ) - } - - NavigationLink(destination: GuessTheCountryView()) { - GameButton( - gradient: .tertiary, - level: "Level 3", symbol: "globe.americas.fill", name: "Guess the country" - ) - } - - NavigationLink(destination: GuessThePopulationView()) { - GameButton( - gradient: .quaternary, - level: "Level 4", symbol: "person.fill", name: "Guess the population" - ) - } + .padding() } - .padding() } .navigationTitle("GeoQuiz") .navigationBarTitleDisplayMode(.inline) @@ -62,10 +128,12 @@ } ToolbarItemGroup { - Button { - showingBuyPremiumModalView = true - } label: { - Label("Buy premium", systemImage: "star") + if !storeKitRC.isActive { + Button { + showingBuyPremiumModalView = true + } label: { + Label("Buy premium", systemImage: "star") + } } Button { @@ -76,7 +144,7 @@ } } .sheet(isPresented: $showingBuyPremiumModalView) { - BuyPremiumModalView() + BuyPremiumModalView(storeKitRC: storeKitRC) } .sheet(isPresented: $showingSettingsModalView) { diff -r ce7ea84f67f5 -r bdfff35dd43c GeoQuiz/Logic/GameModeEnum.swift --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/GeoQuiz/Logic/GameModeEnum.swift Wed Oct 12 11:47:29 2022 +0200 @@ -0,0 +1,12 @@ +// +// GameModeEnum.swift +// GeoQuiz +// +// Created by Dennis Concepción Martín on 11/10/22. +// + +import Foundation + +enum GameMode { + case guessTheFlag, guessTheCapital, guessTheCountry, guessThePopulation +} diff -r ce7ea84f67f5 -r bdfff35dd43c GeoQuiz/Logic/StoreKitRCClass.swift --- a/GeoQuiz/Logic/StoreKitRCClass.swift Sun Oct 09 19:46:44 2022 +0200 +++ b/GeoQuiz/Logic/StoreKitRCClass.swift Wed Oct 12 11:47:29 2022 +0200 @@ -7,27 +7,89 @@ import Foundation import RevenueCat +import SwiftUI class StoreKitRC: ObservableObject { - @Published var productPrice: String? + @Published var errorAlertTitle = "" + @Published var errorAlertMessage = "" + @Published var showingErrorAlert = false - @Published var errorMessage = "" - + @Published var showingSuccessAlert = false + @Published var showingActivityAlert = false + + @Published var offerings: Offerings? = nil + @Published var customerInfo: CustomerInfo? { + didSet { + isActive = customerInfo?.entitlements["Premium"]?.isActive == true + } + } + + @Published var isActive = false + init() { + Purchases.shared.getCustomerInfo { (customerInfo, error) in + self.customerInfo = customerInfo + } + } + + func buy(_ package: Package) { + showingActivityAlert = true - // Get product metadata - Purchases.shared.getOfferings { (offerings, error) in - if let package = offerings?.current?.lifetime?.storeProduct { - self.productPrice = package.localizedPriceString + Purchases.shared.purchase(package: package) { (transaction, customerInfo, error, userCancelled) in + if customerInfo?.entitlements["Premium"]?.isActive == true { + self.showingSuccessAlert = true + } + + if let error = error as? RevenueCat.ErrorCode { + switch error { + case .purchaseCancelledError: + self.errorAlertTitle = "Purchase cancelled" + self.errorAlertMessage = "" + self.showingErrorAlert = true + default: + self.errorAlertTitle = "The purchase failed" + self.errorAlertMessage = "If the problem persists, contact me at dmartin@dennistech.io" + self.showingErrorAlert = true + } + } + + self.customerInfo = customerInfo + self.showingActivityAlert = false + } + } + + func restorePurchase() { + showingActivityAlert = true + + Purchases.shared.restorePurchases { customerInfo, error in + if customerInfo?.entitlements["Premium"]?.isActive == true { + self.showingSuccessAlert = true } else { - self.errorMessage = "There was an error fetching the product. Please, contact the developer at dmartin@dennistech.io." + self.errorAlertTitle = "Opps!" + self.errorAlertMessage = "You don't have GeoQuiz Premium unlocked." self.showingErrorAlert = true } - if let error = error { - self.errorMessage = error.localizedDescription + if let _ = error { + self.errorAlertTitle = "The purchase couldn't be restored" + self.errorAlertMessage = "If the problem persists, contact me at dmartin@dennistech.io" self.showingErrorAlert = true } + + self.customerInfo = customerInfo + self.showingActivityAlert = false + } + } + + func fetchOfferings() { + Purchases.shared.getOfferings { (offerings, error) in + if let _ = error { + self.errorAlertTitle = "The product couldn't be fetched" + self.errorAlertMessage = "If the problem persists, contact me at dmartin@dennistech.io" + self.showingErrorAlert = true + } + + self.offerings = offerings } } }