changeset 28:4f862c618b44

Implemented RevenueCat
author Dennis Concepción Martín <dennisconcepcionmartin@gmail.com>
date Thu, 22 Jul 2021 19:06:01 +0100
parents d95582268b44
children c52966834f83
files Simoleon.xcodeproj/project.pbxproj Simoleon.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved Simoleon.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate Simoleon/Assets.xcassets/Subscription.imageset/Contents.json Simoleon/Assets.xcassets/Subscription.imageset/SimoleonSubscription.png Simoleon/ContentView.swift Simoleon/Conversion.swift Simoleon/Helpers/CurrencySelector.swift Simoleon/Helpers/LockedCurrencyPicker.swift Simoleon/Helpers/RestoreButton.swift Simoleon/Helpers/SubscribeButton.swift Simoleon/Helpers/SubscriberInfo.swift Simoleon/Helpers/SubscriptionController.swift Simoleon/Helpers/SubscriptionFeature.swift Simoleon/Info.plist Simoleon/Settings.swift Simoleon/SimoleonApp.swift Simoleon/Subscription.swift Simoleon/Tests/RevenueCatTest.swift
diffstat 19 files changed, 663 insertions(+), 15 deletions(-) [+]
line wrap: on
line diff
--- a/Simoleon.xcodeproj/project.pbxproj	Wed Jul 21 12:36:10 2021 +0100
+++ b/Simoleon.xcodeproj/project.pbxproj	Thu Jul 22 19:06:01 2021 +0100
@@ -3,7 +3,7 @@
 	archiveVersion = 1;
 	classes = {
 	};
-	objectVersion = 50;
+	objectVersion = 52;
 	objects = {
 
 /* Begin PBXBuildFile section */
@@ -11,6 +11,9 @@
 		950A377826A820F800CAB175 /* DefaultCurrency+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 950A377526A820F400CAB175 /* DefaultCurrency+CoreDataClass.swift */; };
 		9555933A269B0AB8000FD726 /* ParseJson.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95559339269B0AB8000FD726 /* ParseJson.swift */; };
 		9555933D269B0E0A000FD726 /* CurrencyMetadata.json in Resources */ = {isa = PBXBuildFile; fileRef = 9555933C269B0E0A000FD726 /* CurrencyMetadata.json */; };
+		95562D4D26A8962A0047E778 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95562D4C26A8962A0047E778 /* StoreKit.framework */; };
+		95562D5226A8AEF60047E778 /* Purchases in Frameworks */ = {isa = PBXBuildFile; productRef = 95562D5126A8AEF60047E778 /* Purchases */; };
+		95562D5526A8B0B70047E778 /* RevenueCatTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95562D5426A8B0B70047E778 /* RevenueCatTest.swift */; };
 		957065E226A5FE0400523E68 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 957065E126A5FE0400523E68 /* Settings.swift */; };
 		9585BB1226A6B71B00E3193E /* ReadConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9585BB1126A6B71B00E3193E /* ReadConfig.swift */; };
 		9585BB1426A6B7F400E3193E /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9585BB1326A6B7F400E3193E /* Request.swift */; };
@@ -37,6 +40,13 @@
 		95C5B2342697752700941585 /* Simoleon.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 95C5B2322697752700941585 /* Simoleon.xcdatamodeld */; };
 		95C5B23F2697752700941585 /* SimoleonTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5B23E2697752700941585 /* SimoleonTests.swift */; };
 		95C5B24A2697752700941585 /* SimoleonUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95C5B2492697752700941585 /* SimoleonUITests.swift */; };
+		95D8C8C726A95D2900BCC188 /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D8C8C626A95D2900BCC188 /* Subscription.swift */; };
+		95D8C8CB26A970F400BCC188 /* SubscriptionFeature.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D8C8CA26A970F400BCC188 /* SubscriptionFeature.swift */; };
+		95D8C8CD26A9784500BCC188 /* SubscribeButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D8C8CC26A9784500BCC188 /* SubscribeButton.swift */; };
+		95D8C8CF26A98A7900BCC188 /* RestoreButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D8C8CE26A98A7900BCC188 /* RestoreButton.swift */; };
+		95D8C8D126A9BC6200BCC188 /* LockedCurrencyPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D8C8D026A9BC6200BCC188 /* LockedCurrencyPicker.swift */; };
+		95D8C8D326A9C17300BCC188 /* SubscriptionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D8C8D226A9C17300BCC188 /* SubscriptionController.swift */; };
+		95D8C8D526A9E20F00BCC188 /* SubscriberInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95D8C8D426A9E20F00BCC188 /* SubscriberInfo.swift */; };
 		95DD4ABB269B33810027CA1F /* CurrencyPairs.json in Resources */ = {isa = PBXBuildFile; fileRef = 95DD4ABA269B33810027CA1F /* CurrencyPairs.json */; };
 		95E76436269DFC1A008E9F31 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 95E76435269DFC1A008E9F31 /* LaunchScreen.storyboard */; };
 		95E7643A269E0037008E9F31 /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95E76439269E0037008E9F31 /* CloudKit.framework */; };
@@ -64,6 +74,8 @@
 		950A377626A820F400CAB175 /* DefaultCurrency+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultCurrency+CoreDataProperties.swift"; sourceTree = "<group>"; };
 		95559339269B0AB8000FD726 /* ParseJson.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseJson.swift; sourceTree = "<group>"; };
 		9555933C269B0E0A000FD726 /* CurrencyMetadata.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = CurrencyMetadata.json; sourceTree = "<group>"; };
+		95562D4C26A8962A0047E778 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; };
+		95562D5426A8B0B70047E778 /* RevenueCatTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevenueCatTest.swift; sourceTree = "<group>"; };
 		957065E126A5FE0400523E68 /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
 		9585BB0F26A6B58500E3193E /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
 		9585BB1026A6B5ED00E3193E /* ConfigTemplate.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ConfigTemplate.xcconfig; sourceTree = "<group>"; };
@@ -98,6 +110,13 @@
 		95C5B2452697752700941585 /* SimoleonUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SimoleonUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		95C5B2492697752700941585 /* SimoleonUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimoleonUITests.swift; sourceTree = "<group>"; };
 		95C5B24B2697752700941585 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		95D8C8C626A95D2900BCC188 /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = "<group>"; };
+		95D8C8CA26A970F400BCC188 /* SubscriptionFeature.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFeature.swift; sourceTree = "<group>"; };
+		95D8C8CC26A9784500BCC188 /* SubscribeButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeButton.swift; sourceTree = "<group>"; };
+		95D8C8CE26A98A7900BCC188 /* RestoreButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RestoreButton.swift; sourceTree = "<group>"; };
+		95D8C8D026A9BC6200BCC188 /* LockedCurrencyPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockedCurrencyPicker.swift; sourceTree = "<group>"; };
+		95D8C8D226A9C17300BCC188 /* SubscriptionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionController.swift; sourceTree = "<group>"; };
+		95D8C8D426A9E20F00BCC188 /* SubscriberInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriberInfo.swift; sourceTree = "<group>"; };
 		95DD4ABA269B33810027CA1F /* CurrencyPairs.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = CurrencyPairs.json; sourceTree = "<group>"; };
 		95E76435269DFC1A008E9F31 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
 		95E76437269E0033008E9F31 /* Simoleon.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Simoleon.entitlements; sourceTree = "<group>"; };
@@ -110,6 +129,8 @@
 			buildActionMask = 2147483647;
 			files = (
 				95E7643A269E0037008E9F31 /* CloudKit.framework in Frameworks */,
+				95562D4D26A8962A0047E778 /* StoreKit.framework in Frameworks */,
+				95562D5226A8AEF60047E778 /* Purchases in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -163,6 +184,14 @@
 			path = Resources;
 			sourceTree = "<group>";
 		};
+		95562D5326A8B0A70047E778 /* Tests */ = {
+			isa = PBXGroup;
+			children = (
+				95562D5426A8B0B70047E778 /* RevenueCatTest.swift */,
+			);
+			path = Tests;
+			sourceTree = "<group>";
+		};
 		95C5B21B2697752600941585 = {
 			isa = PBXGroup;
 			children = (
@@ -196,6 +225,7 @@
 				95B54F4326A4842C001DC0D8 /* Conversion.swift */,
 				95C5179E26A5F34200BC2B24 /* Favourites.swift */,
 				957065E126A5FE0400523E68 /* Settings.swift */,
+				95D8C8C626A95D2900BCC188 /* Subscription.swift */,
 				95C5B22B2697752700941585 /* Assets.xcassets */,
 				95C5B2302697752700941585 /* Persistence.swift */,
 				95C5B2352697752700941585 /* Info.plist */,
@@ -206,6 +236,7 @@
 				95559338269B0AAA000FD726 /* Functions */,
 				9555933B269B0DF9000FD726 /* Resources */,
 				95C5B22D2697752700941585 /* Preview Content */,
+				95562D5326A8B0A70047E778 /* Tests */,
 			);
 			path = Simoleon;
 			sourceTree = "<group>";
@@ -239,6 +270,7 @@
 		95E76438269E0037008E9F31 /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
+				95562D4C26A8962A0047E778 /* StoreKit.framework */,
 				95E76439269E0037008E9F31 /* CloudKit.framework */,
 			);
 			name = Frameworks;
@@ -254,6 +286,12 @@
 				95C5179026A5DC8E00BC2B24 /* ConditionalWrapper.swift */,
 				95C5179826A5EC9F00BC2B24 /* FavouriteButton.swift */,
 				95C517A026A5F6C000BC2B24 /* ResignKeyboard.swift */,
+				95D8C8CA26A970F400BCC188 /* SubscriptionFeature.swift */,
+				95D8C8CC26A9784500BCC188 /* SubscribeButton.swift */,
+				95D8C8CE26A98A7900BCC188 /* RestoreButton.swift */,
+				95D8C8D026A9BC6200BCC188 /* LockedCurrencyPicker.swift */,
+				95D8C8D226A9C17300BCC188 /* SubscriptionController.swift */,
+				95D8C8D426A9E20F00BCC188 /* SubscriberInfo.swift */,
 			);
 			path = Helpers;
 			sourceTree = "<group>";
@@ -275,6 +313,7 @@
 			);
 			name = Simoleon;
 			packageProductDependencies = (
+				95562D5126A8AEF60047E778 /* Purchases */,
 			);
 			productName = Simoleon;
 			productReference = 95C5B2242697752600941585 /* Simoleon.app */;
@@ -348,6 +387,7 @@
 			);
 			mainGroup = 95C5B21B2697752600941585;
 			packageReferences = (
+				95562D5026A8AEF60047E778 /* XCRemoteSwiftPackageReference "purchases-ios" */,
 			);
 			productRefGroup = 95C5B2252697752600941585 /* Products */;
 			projectDirPath = "";
@@ -394,20 +434,28 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				95D8C8D326A9C17300BCC188 /* SubscriptionController.swift in Sources */,
 				95C5179926A5EC9F00BC2B24 /* FavouriteButton.swift in Sources */,
 				95C5179C26A5EFBE00BC2B24 /* Favourite+CoreDataClass.swift in Sources */,
 				950A377826A820F800CAB175 /* DefaultCurrency+CoreDataClass.swift in Sources */,
 				95C5B2312697752700941585 /* Persistence.swift in Sources */,
 				9585BB1226A6B71B00E3193E /* ReadConfig.swift in Sources */,
+				95D8C8CB26A970F400BCC188 /* SubscriptionFeature.swift in Sources */,
 				95AEBC9526A03ECB00613729 /* ContentView.swift in Sources */,
 				95AEBC9B26A04A4200613729 /* CurrencyMetadataModel.swift in Sources */,
+				95D8C8D526A9E20F00BCC188 /* SubscriberInfo.swift in Sources */,
+				95D8C8CD26A9784500BCC188 /* SubscribeButton.swift in Sources */,
 				950A377726A820F800CAB175 /* DefaultCurrency+CoreDataProperties.swift in Sources */,
 				9585BB1A26A6E8FD00E3193E /* SimpleSuccess.swift in Sources */,
 				9555933A269B0AB8000FD726 /* ParseJson.swift in Sources */,
+				95D8C8CF26A98A7900BCC188 /* RestoreButton.swift in Sources */,
 				95C5179D26A5EFBE00BC2B24 /* Favourite+CoreDataProperties.swift in Sources */,
 				95C5179F26A5F34200BC2B24 /* Favourites.swift in Sources */,
 				95C5B2282697752600941585 /* SimoleonApp.swift in Sources */,
+				95562D5526A8B0B70047E778 /* RevenueCatTest.swift in Sources */,
 				95B54F4A26A4A450001DC0D8 /* ConversionBox.swift in Sources */,
+				95D8C8C726A95D2900BCC188 /* Subscription.swift in Sources */,
+				95D8C8D126A9BC6200BCC188 /* LockedCurrencyPicker.swift in Sources */,
 				95C517A126A5F6C000BC2B24 /* ResignKeyboard.swift in Sources */,
 				95AEBC9D26A04D4600613729 /* CurrencyRow.swift in Sources */,
 				95AEBCA326A0900E00613729 /* CurrencyQuoteModel.swift in Sources */,
@@ -747,6 +795,25 @@
 		};
 /* End XCConfigurationList section */
 
+/* Begin XCRemoteSwiftPackageReference section */
+		95562D5026A8AEF60047E778 /* XCRemoteSwiftPackageReference "purchases-ios" */ = {
+			isa = XCRemoteSwiftPackageReference;
+			repositoryURL = "https://github.com/RevenueCat/purchases-ios.git";
+			requirement = {
+				kind = upToNextMajorVersion;
+				minimumVersion = 3.12.2;
+			};
+		};
+/* End XCRemoteSwiftPackageReference section */
+
+/* Begin XCSwiftPackageProductDependency section */
+		95562D5126A8AEF60047E778 /* Purchases */ = {
+			isa = XCSwiftPackageProductDependency;
+			package = 95562D5026A8AEF60047E778 /* XCRemoteSwiftPackageReference "purchases-ios" */;
+			productName = Purchases;
+		};
+/* End XCSwiftPackageProductDependency section */
+
 /* Begin XCVersionGroup section */
 		95C5B2322697752700941585 /* Simoleon.xcdatamodeld */ = {
 			isa = XCVersionGroup;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved	Thu Jul 22 19:06:01 2021 +0100
@@ -0,0 +1,16 @@
+{
+  "object": {
+    "pins": [
+      {
+        "package": "Purchases",
+        "repositoryURL": "https://github.com/RevenueCat/purchases-ios.git",
+        "state": {
+          "branch": null,
+          "revision": "fd6c25818690e9399bccc713a2b627385b547a8d",
+          "version": "3.12.2"
+        }
+      }
+    ]
+  },
+  "version": 1
+}
Binary file Simoleon.xcodeproj/project.xcworkspace/xcuserdata/dennis.xcuserdatad/UserInterfaceState.xcuserstate has changed
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Assets.xcassets/Subscription.imageset/Contents.json	Thu Jul 22 19:06:01 2021 +0100
@@ -0,0 +1,21 @@
+{
+  "images" : [
+    {
+      "filename" : "SimoleonSubscription.png",
+      "idiom" : "universal",
+      "scale" : "1x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "2x"
+    },
+    {
+      "idiom" : "universal",
+      "scale" : "3x"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}
Binary file Simoleon/Assets.xcassets/Subscription.imageset/SimoleonSubscription.png has changed
--- a/Simoleon/ContentView.swift	Wed Jul 21 12:36:10 2021 +0100
+++ b/Simoleon/ContentView.swift	Thu Jul 22 19:06:01 2021 +0100
@@ -6,12 +6,16 @@
 //
 
 import SwiftUI
+import Purchases
 
 struct ContentView: View {
     @State private var tab: Tab = .convert
+    @StateObject var subscriptionController = SubscriptionController()
+    
     var body: some View {
         TabView(selection: $tab) {
             Conversion(fetchUserSettings: true, currencyPair: "USD/GBP")
+                .environmentObject(subscriptionController)
                 .tabItem {
                     Label("Convert", systemImage: "arrow.counterclockwise.circle")
                 }
@@ -24,11 +28,24 @@
                 .tag(Tab.favourites)
             
             Settings()
+                .environmentObject(subscriptionController)
                 .tabItem {
                     Label("Settings", systemImage: "gear")
                 }
                 .tag(Tab.settings)
         }
+        .onAppear(perform: checkEntitlements)
+    }
+    
+    private func checkEntitlements() {
+        Purchases.shared.purchaserInfo { (purchaserInfo, error) in
+            if purchaserInfo?.entitlements["all"]?.isActive == true {
+                print("User's subscription is active")
+                self.subscriptionController.isActive = true
+            } else {
+                print("User's subscription expired or doesn't exist")
+            }
+        }
     }
     
     private enum Tab {
--- a/Simoleon/Conversion.swift	Wed Jul 21 12:36:10 2021 +0100
+++ b/Simoleon/Conversion.swift	Thu Jul 22 19:06:01 2021 +0100
@@ -19,6 +19,7 @@
     
     @Environment(\.managedObjectContext) private var viewContext
     @FetchRequest(sortDescriptors: []) private var defaultCurrency: FetchedResults<DefaultCurrency>
+    
     let currencyMetadata: [String: CurrencyMetadataModel] = parseJson("CurrencyMetadata.json")
     
     var body: some View {
--- a/Simoleon/Helpers/CurrencySelector.swift	Wed Jul 21 12:36:10 2021 +0100
+++ b/Simoleon/Helpers/CurrencySelector.swift	Thu Jul 22 19:06:01 2021 +0100
@@ -6,12 +6,16 @@
 //
 
 import SwiftUI
+import Purchases
 
 struct CurrencySelector: View {
     @Binding var currencyPair: String
     @Binding var showingCurrencySelector: Bool
+    @EnvironmentObject var subscriptionController: SubscriptionController
+    
     @State private var searchCurrency = ""
     @State private var searching = false
+    @State private var showingSubscriptionPaywall = false
     
     var body: some View {
         NavigationView {
@@ -30,10 +34,7 @@
                 
                 Section(header: Text("All currencies")) {
                     ForEach(currencyPairs(), id: \.self) { currencyPair in
-                        Button(action: {
-                            self.currencyPair = currencyPair
-                            showingCurrencySelector = false
-                        }) {
+                        Button(action: { select(currencyPair) }) {
                             CurrencyRow(currencyPair: currencyPair)
                         }
                     }
@@ -41,19 +42,20 @@
             }
             .gesture(DragGesture()
                  .onChanged({ _ in
-                     UIApplication.shared.dismissKeyboard()
+                    UIApplication.shared.dismissKeyboard()
+                    searching = false
                  })
              )
             .navigationTitle("Currencies")
             .navigationBarTitleDisplayMode(.inline)
             .toolbar {
-                ToolbarItem(placement: .confirmationAction) {
-                    Button("OK", action: { showingCurrencySelector = false })
+                ToolbarItem(placement: .cancellationAction) {
+                    Button("Cancel", action: { showingCurrencySelector = false })
                 }
                 
-                ToolbarItem(placement: .cancellationAction) {
+                ToolbarItem(placement: .confirmationAction) {
                     if searching {
-                         Button("Cancel") {
+                         Button("OK") {
                             searchCurrency = ""
                              withAnimation {
                                 searching = false
@@ -64,6 +66,9 @@
                 }
             }
         }
+        .sheet(isPresented: $showingSubscriptionPaywall) {
+            Subscription(showingSubscriptionPaywall: $showingSubscriptionPaywall)
+        }
     }
     
     private func currencyPairs() -> [String] {
@@ -75,11 +80,22 @@
             return currencyPairs.filter { $0.contains(searchCurrency.uppercased()) }
         }
     }
+    
+    
+    private func select(_ currencyPair: String) {
+        if subscriptionController.isActive {
+            self.currencyPair = currencyPair
+            showingCurrencySelector = false
+        } else {
+            showingSubscriptionPaywall = true
+        }
+    }
 }
 
 
 struct CurrencySelector_Previews: PreviewProvider {
     static var previews: some View {
         CurrencySelector(currencyPair: .constant("USD/GBP"), showingCurrencySelector: .constant(false))
+            .environmentObject(SubscriptionController())
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Helpers/LockedCurrencyPicker.swift	Thu Jul 22 19:06:01 2021 +0100
@@ -0,0 +1,28 @@
+//
+//  LockedCurrencyPicker.swift
+//  Simoleon
+//
+//  Created by Dennis Concepción Martín on 22/07/2021.
+//
+
+import SwiftUI
+
+struct LockedCurrencyPicker: View {
+    var body: some View {
+        HStack {
+            Text("Default currency")
+            Spacer()
+            Text("USD/GBP")
+                .foregroundColor(Color(.systemGray))
+            
+            Image(systemName: "lock")
+                .foregroundColor(Color(.systemGray))
+        }
+    }
+}
+
+struct LockedCurrencyPicker_Previews: PreviewProvider {
+    static var previews: some View {
+        LockedCurrencyPicker()
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Helpers/RestoreButton.swift	Thu Jul 22 19:06:01 2021 +0100
@@ -0,0 +1,60 @@
+//
+//  RestoreButton.swift
+//  Simoleon
+//
+//  Created by Dennis Concepción Martín on 22/07/2021.
+//
+
+import SwiftUI
+import Purchases
+
+struct RestoreButton: View {
+    @Binding var showingSubscriptionPaywall: Bool
+    @EnvironmentObject var subscriptionController: SubscriptionController
+    
+    @State private var restoringPurchases = false
+    @State private var alertTitle = ""
+    @State private var alertMessage = ""
+    @State private var showingAlert = false
+    
+    var body: some View {
+        Button(action: restorePurchases) {
+            if restoringPurchases {
+                ProgressView()
+            } else {
+                Text("Restore purchases")
+            }
+        }
+        .alert(isPresented: $showingAlert) {
+            Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("Ok")))
+        }
+    }
+    
+    private func restorePurchases() {
+        restoringPurchases = true
+        
+        Purchases.shared.restoreTransactions { purchaserInfo, error in
+            if purchaserInfo?.entitlements["all"]?.isActive == true {
+                subscriptionController.isActive = true
+                showingSubscriptionPaywall = false
+            } else {
+                alertTitle = "No subscriptions found"
+                alertMessage = "You are not subscripted to Simoleon yet."
+                restoringPurchases = false
+                showingAlert = true
+            }
+            
+            if let error = error as NSError? {
+                alertTitle = error.localizedDescription
+                alertMessage = error.localizedFailureReason ?? "If the problem persists send an email to dmartin@dennistech.io"
+                showingAlert = true
+            }
+        }
+    }
+}
+
+struct RestoreButton_Previews: PreviewProvider {
+    static var previews: some View {
+        RestoreButton(showingSubscriptionPaywall: .constant(true))
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Helpers/SubscribeButton.swift	Thu Jul 22 19:06:01 2021 +0100
@@ -0,0 +1,109 @@
+//
+//  SubscribeButton.swift
+//  Simoleon
+//
+//  Created by Dennis Concepción Martín on 22/07/2021.
+//
+
+import SwiftUI
+import Purchases
+
+struct SubscribeButton: View {
+    @Binding var showingSubscriptionPaywall: Bool
+    @EnvironmentObject var subscriptionController: SubscriptionController
+    
+    @State private var subscribeButtonText = ""
+    @State private var showingPrice = false
+    @State private var alertTitle = ""
+    @State private var alertMessage = ""
+    @State private var showingAlert = false
+    
+    var body: some View {
+        Button(action: purchaseMonthlySubscription) {
+            RoundedRectangle(cornerRadius: 15)
+                .frame(height: 60)
+                .overlay(
+                    VStack {
+                        if showingPrice {
+                            Text(subscribeButtonText)
+                                .foregroundColor(.white)
+                                .fontWeight(.semibold)
+                        } else {
+                            ProgressView()
+                        }
+                    }
+                )
+        }
+        .onAppear(perform: fetchMonthlySubscription)
+        .alert(isPresented: $showingAlert) {
+            Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("Ok")))
+        }
+    }
+    
+    private func fetchMonthlySubscription() {
+        Purchases.shared.offerings { (offerings, error) in
+            if let product = offerings?.current?.monthly?.product {
+                let price = formatCurrency(product.priceLocale, product.price)
+                subscribeButtonText = "Subscribe for \(price) / month"
+                showingPrice = true
+            }
+            
+            if let error = error as NSError? {
+                alertTitle = error.localizedDescription
+                alertMessage = error.localizedFailureReason ?? "If the problem persists send an email to dmartin@dennistech.io"
+                subscribeButtonText = "-"
+                showingPrice = true
+                showingAlert = true
+            }
+        }
+    }
+    
+    private func purchaseMonthlySubscription() {
+        showingPrice = false
+        
+        Purchases.shared.offerings { (offerings, error) in
+            if let package = offerings?.current?.monthly {
+                
+                Purchases.shared.purchasePackage(package) { (transaction, purchaserInfo, error, userCancelled) in
+                    if purchaserInfo?.entitlements["all"]?.isActive == true {
+                        showingPrice = true
+                        subscriptionController.isActive = true
+                        showingSubscriptionPaywall = false
+                    }
+                    
+                    if let error = error as NSError? {
+                        alertTitle = error.localizedDescription
+                        alertMessage = error.localizedFailureReason ?? "If the problem persists send an email to dmartin@dennistech.io"
+                        showingPrice = true
+                        showingAlert = true
+                    }
+                }
+                
+                if let error = error as NSError? {
+                    alertTitle = error.localizedDescription
+                    alertMessage = error.localizedFailureReason ?? "If the problem persists send an email to dmartin@dennistech.io"
+                    showingPrice = true
+                    showingAlert = true
+                }
+            }
+        }
+    }
+    
+    private func formatCurrency(_ locale: Locale, _ amount: NSDecimalNumber) -> String {
+        let formatter = NumberFormatter()
+        formatter.locale = locale
+        formatter.numberStyle = .currency
+        
+        if let formattedAmount = formatter.string(from: amount as NSNumber) {
+            return formattedAmount
+        } else {
+            return "\(amount)\(locale.currencySymbol!)"
+        }
+    }
+}
+
+struct SubscribeButton_Previews: PreviewProvider {
+    static var previews: some View {
+        SubscribeButton(showingSubscriptionPaywall: .constant(true))
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Helpers/SubscriberInfo.swift	Thu Jul 22 19:06:01 2021 +0100
@@ -0,0 +1,77 @@
+//
+//  SubscriberInfo.swift
+//  Simoleon
+//
+//  Created by Dennis Concepción Martín on 22/07/2021.
+//
+
+import SwiftUI
+import Purchases
+
+struct SubscriberInfo: View {
+    @State private var memberSince: Date? = nil
+    @State private var expiration: Date? = nil
+    @State private var latestPurchase: Date? = nil
+    @State private var showingAlert = false
+    @State private var alertTitle = ""
+    @State private var alertMessage = ""
+    
+    var body: some View {
+        VStack {
+            List {
+                if let memberSince = self.memberSince {
+                    Text("Member since \(formatDate(memberSince))")
+                } else {
+                    Text("-")
+                }
+                
+                if let expiration = self.expiration {
+                    Text("Expires at \(formatDate(expiration))")
+                } else {
+                    Text("-")
+                }
+                
+                if let latestPurchase = self.latestPurchase {
+                    Text("Latest purchase \(formatDate(latestPurchase))")
+                } else {
+                    Text("-")
+                }
+            }
+            .listStyle(InsetGroupedListStyle())
+        }
+        .navigationTitle("Information")
+        .onAppear(perform: getInfo)
+        .alert(isPresented: $showingAlert) {
+            Alert(title: Text(alertTitle), message: Text(alertMessage), dismissButton: .default(Text("Ok")))
+        }
+    }
+    
+    private func getInfo() {
+        Purchases.shared.purchaserInfo { (purchaserInfo, error) in
+            self.memberSince = purchaserInfo?.entitlements["all"]?.originalPurchaseDate
+            self.expiration = purchaserInfo?.entitlements["all"]?.expirationDate
+            self.latestPurchase = purchaserInfo?.entitlements["all"]?.latestPurchaseDate
+
+            if let error = error as NSError? {
+                alertTitle = error.localizedDescription
+                alertMessage = error.localizedFailureReason ?? "If the problem persists send an email to dmartin@dennistech.io"
+                showingAlert = true
+            }
+        }
+    }
+    
+    private func formatDate(_ date: Date) -> String {
+        let formatter = DateFormatter()
+        formatter.dateStyle = .long
+        let dateString = formatter.string(from: date)
+        
+        return dateString
+    }
+}
+
+struct SubscriberInfo_Previews: PreviewProvider {
+    static var previews: some View {
+        SubscriberInfo()
+            .environmentObject(SubscriptionController())
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Helpers/SubscriptionController.swift	Thu Jul 22 19:06:01 2021 +0100
@@ -0,0 +1,12 @@
+//
+//  SubscriptionController.swift
+//  Simoleon
+//
+//  Created by Dennis Concepción Martín on 22/07/2021.
+//
+
+import SwiftUI
+
+class SubscriptionController: ObservableObject {
+    @Published var isActive = false
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Helpers/SubscriptionFeature.swift	Thu Jul 22 19:06:01 2021 +0100
@@ -0,0 +1,41 @@
+//
+//  SubscriptionFeature.swift
+//  Simoleon
+//
+//  Created by Dennis Concepción Martín on 22/07/2021.
+//
+
+import SwiftUI
+
+struct SubscriptionFeature: View {
+    var symbol: String
+    var title: String
+    var text: String
+    var colour: Color
+    
+    var body: some View {
+        HStack(alignment:.top) {
+            Image(systemName: symbol)
+                .foregroundColor(colour)
+                .font(.title)
+            
+            VStack(alignment: .leading) {
+                Text(title)
+                    .font(.headline)
+                
+                Text(text)
+            }
+        }
+    }
+}
+
+struct SubscriptionFeature_Previews: PreviewProvider {
+    static var previews: some View {
+        SubscriptionFeature(
+            symbol: "star.circle.fill",
+            title: "Favourite currencies",
+            text: "Save your favourite currencies to access them quickly.",
+            colour: Color(.systemYellow)
+        )
+    }
+}
--- a/Simoleon/Info.plist	Wed Jul 21 12:36:10 2021 +0100
+++ b/Simoleon/Info.plist	Thu Jul 22 19:06:01 2021 +0100
@@ -2,6 +2,8 @@
 <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
 <plist version="1.0">
 <dict>
+	<key>PURCHASES_KEY</key>
+	<string>$(PURCHASES_KEY)</string>
 	<key>API_KEY</key>
 	<string>$(API_KEY)</string>
 	<key>API_URL</key>
--- a/Simoleon/Settings.swift	Wed Jul 21 12:36:10 2021 +0100
+++ b/Simoleon/Settings.swift	Thu Jul 22 19:06:01 2021 +0100
@@ -6,20 +6,39 @@
 //
 
 import SwiftUI
+import Purchases
 
 struct Settings: View {
+    @EnvironmentObject var subscriptionController: SubscriptionController
     @Environment(\.managedObjectContext) private var viewContext
     @FetchRequest(sortDescriptors: []) private var defaultCurrency: FetchedResults<DefaultCurrency>
+    
     @State private var selectedDefaultCurrency = ""
+    @State private var showingSubscriptionPaywall = false
+    
     let currencyPairs: [String] = parseJson("CurrencyPairs.json")
     
     var body: some View {
         List {
+            Section(header: Text("Subscription")) {
+                NavigationLink("Information", destination: SubscriberInfo())
+                if !subscriptionController.isActive {
+                    Text("Subscribe")
+                        .onTapGesture { showingSubscriptionPaywall = true }
+                }
+            }
+            
             Section(header: Text("Preferences")) {
-                Picker("Default currency", selection: $selectedDefaultCurrency) {
-                    ForEach(currencyPairs.sorted(), id: \.self) { currencyPair in
-                        Text(currencyPair)
+                if subscriptionController.isActive {
+                    Picker("Default currency", selection: $selectedDefaultCurrency) {
+                        ForEach(currencyPairs.sorted(), id: \.self) { currencyPair in
+                            Text(currencyPair)
+                        }
                     }
+                } else {
+                    LockedCurrencyPicker()
+                        .contentShape(Rectangle())
+                        .onTapGesture { showingSubscriptionPaywall = true }
                 }
             }
             
@@ -60,15 +79,20 @@
                 Link("Privacy Policy", destination: URL(string: "https://dennistech.io")!)
             }
         }
-        .onAppear(perform: setCurrency)
+        .onAppear(perform: onAppear)
         .listStyle(InsetGroupedListStyle())
         .navigationTitle("Settings")
+        .sheet(isPresented: $showingSubscriptionPaywall) {
+            Subscription(showingSubscriptionPaywall: $showingSubscriptionPaywall)
+                .environmentObject(subscriptionController)
+        }
         .if(UIDevice.current.userInterfaceIdiom == .phone) { content in
             NavigationView { content }
         }
     }
     
-    private func setCurrency() {
+    private func onAppear() {
+        // Set initial value of the picker
         if selectedDefaultCurrency == "" {
             self.selectedDefaultCurrency = defaultCurrency.first?.pair ?? "USD/GBP"
         } else {
@@ -96,5 +120,6 @@
 struct Settings_Previews: PreviewProvider {
     static var previews: some View {
         Settings()
+            .environmentObject(SubscriptionController())
     }
 }
--- a/Simoleon/SimoleonApp.swift	Wed Jul 21 12:36:10 2021 +0100
+++ b/Simoleon/SimoleonApp.swift	Thu Jul 22 19:06:01 2021 +0100
@@ -6,10 +6,14 @@
 //
 
 import SwiftUI
+import Purchases
 
 @main
 struct SimoleonApp: App {
     let persistenceController = PersistenceController.shared
+    init() {
+            Purchases.configure(withAPIKey: "\(readConfig("PURCHASES_KEY")!)")
+        }
 
     var body: some Scene {
         WindowGroup {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Subscription.swift	Thu Jul 22 19:06:01 2021 +0100
@@ -0,0 +1,89 @@
+//
+//  Subscription.swift
+//  Simoleon
+//
+//  Created by Dennis Concepción Martín on 22/07/2021.
+//
+
+import SwiftUI
+
+struct Subscription: View {
+    @Binding var showingSubscriptionPaywall: Bool
+    
+    var body: some View {
+        NavigationView {
+            ScrollView {
+                VStack(alignment: .leading, spacing: 20) {
+                    HStack {
+                        Spacer()
+                        VStack {
+                            Image("Subscription")
+                                .resizable()
+                                .aspectRatio(contentMode: .fit)
+                                .frame(width: 100, height: 100)
+                                .cornerRadius(25)
+                            
+                            Text("Unlock all access")
+                                .font(.title)
+                                .fontWeight(.semibold)
+                                .padding(.top)
+                        }
+                        
+                        Spacer()
+                    }
+                    
+                    Divider()
+                    
+                    SubscriptionFeature(
+                        symbol: "star.circle.fill",
+                        title: "Favourite currencies",
+                        text: "Save your favourite currencies to access them quickly.",
+                        colour: Color(.systemYellow)
+                    )
+                    
+                    SubscriptionFeature(
+                        symbol: "flag.circle.fill",
+                        title: "Over 170 currencies",
+                        text: "Have access to almost every currency of the world.",
+                        colour: Color(.systemRed)
+                    )
+                    
+                    SubscriptionFeature(
+                        symbol: "icloud.circle.fill",
+                        title: "Simoleon on all your devices",
+                        text: "Your settings and favourite currencies in all your devices.",
+                        colour: Color(.systemBlue)
+                    )
+                    
+                    SubscriptionFeature(
+                        symbol: "bitcoinsign.circle.fill",
+                        title: "Cryptos and commodities",
+                        text: "Convert your currency between cryptos, gold, and silver.",
+                        colour: Color(.systemOrange)
+                    )
+                    Spacer()
+                    SubscribeButton(showingSubscriptionPaywall: $showingSubscriptionPaywall)
+                    HStack {
+                        Spacer()
+                        RestoreButton(showingSubscriptionPaywall: $showingSubscriptionPaywall)
+                        Spacer()
+                    }
+                    
+                }
+                .padding(.bottom)
+                .padding(.horizontal, 40)
+            }
+            .toolbar {
+                ToolbarItem(placement: .cancellationAction) {
+                    Button("Cancel", action: { showingSubscriptionPaywall = false })
+                }
+            }
+        }
+    }
+}
+
+struct Subscription_Previews: PreviewProvider {
+    static var previews: some View {
+        Subscription(showingSubscriptionPaywall: .constant(false))
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Simoleon/Tests/RevenueCatTest.swift	Thu Jul 22 19:06:01 2021 +0100
@@ -0,0 +1,63 @@
+//
+//  RevenueCatTest.swift
+//  Simoleon
+//
+//  Created by Dennis Concepción Martín on 21/07/2021.
+//
+
+import SwiftUI
+import Purchases
+
+struct RevenueCatTest: View {
+    @State private var productName = ""
+    @State private var price = ""
+    
+    var body: some View {
+        VStack (alignment: .leading) {
+            Text(productName)
+            Text(price)
+            Button("Buy", action: purchaseProMonthlySubscription)
+        }
+        .onAppear(perform: fetchProMonthlySubscription)
+    }
+    
+    private func fetchProMonthlySubscription() {
+        Purchases.shared.offerings { (offerings, error) in
+            if let product = offerings?.current?.monthly?.product {
+                self.productName = product.localizedTitle
+                self.price = formatCurrency(product.priceLocale, product.price)
+            }
+        }
+    }
+    
+    private func purchaseProMonthlySubscription() {
+        Purchases.shared.offerings { (offerings, error) in
+            if let package = offerings?.current?.monthly {
+                Purchases.shared.purchasePackage(package) { (transaction, purchaserInfo, error, userCancelled) in
+                    if purchaserInfo?.entitlements["all"]?.isActive == true {
+                        print("Ok")
+                    }
+                }
+            }
+        }
+    }
+    
+    private func formatCurrency(_ locale: Locale, _ amount: NSDecimalNumber) -> String {
+        let formatter = NumberFormatter()
+        formatter.locale = locale
+        formatter.numberStyle = .currency
+        
+        if let formattedAmount = formatter.string(from: amount as NSNumber) {
+            return formattedAmount
+        } else {
+            // Handle error
+            return ""
+        }
+    }
+}
+
+struct RevenueCatTest_Previews: PreviewProvider {
+    static var previews: some View {
+        RevenueCatTest()
+    }
+}