changeset 13:bdfff35dd43c

implement RevenueCat
author Dennis C. M. <dennis@denniscm.com>
date Wed, 12 Oct 2022 11:47:29 +0200
parents ce7ea84f67f5
children 136928bae534
files GeoQuiz.xcodeproj/project.pbxproj GeoQuiz/BuyPremiumModalView.swift GeoQuiz/Components/ActivityAlertHelper.swift GeoQuiz/ContentView.swift GeoQuiz/Logic/GameModeEnum.swift GeoQuiz/Logic/StoreKitRCClass.swift
diffstat 6 files changed, 313 insertions(+), 116 deletions(-) [+]
line wrap: on
line diff
--- 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 = "<group>"; };
 		95C4315528C64A8C00212131 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
 		95C4315828C6500000212131 /* GameButtonHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameButtonHelper.swift; sourceTree = "<group>"; };
+		95CA293F28F5769700CE0B7A /* GameModeEnum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GameModeEnum.swift; sourceTree = "<group>"; };
+		95CA294F28F6BB4500CE0B7A /* ActivityAlertHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityAlertHelper.swift; sourceTree = "<group>"; };
 		95E6188428DDDB5C003359ED /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
 		95FA409928D9876B00129B60 /* GuessTheFlagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuessTheFlagView.swift; sourceTree = "<group>"; };
 		95FA409B28D9881100129B60 /* CountryGameClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryGameClass.swift; sourceTree = "<group>"; };
@@ -113,6 +117,7 @@
 				95919DB528F076BF00F21F8F /* UserClass.swift */,
 				95197EFC28F339AE00FE67E9 /* StoreKitRCClass.swift */,
 				95AE8D5628C8750E0067F219 /* LoadFunc.swift */,
+				95CA293F28F5769700CE0B7A /* GameModeEnum.swift */,
 			);
 			path = Logic;
 			sourceTree = "<group>";
@@ -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)";
--- 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())
     }
 }
--- /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()
+    }
+}
--- 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) {
--- /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
+}
--- 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
         }
     }
 }