Mercurial > public > simoleon
changeset 28:4f862c618b44
Implemented RevenueCat
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 + } +}
--- 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() + } +}