diff --git a/SwiftyStoreKit-tvOS-Demo/AppDelegate.swift b/SwiftyStoreKit-tvOS-Demo/AppDelegate.swift index 0688171..c584cde 100644 --- a/SwiftyStoreKit-tvOS-Demo/AppDelegate.swift +++ b/SwiftyStoreKit-tvOS-Demo/AppDelegate.swift @@ -13,7 +13,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Override point for customization after application launch. return true @@ -41,6 +40,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. } - } - diff --git a/SwiftyStoreKit-tvOS-Demo/Base.lproj/Main.storyboard b/SwiftyStoreKit-tvOS-Demo/Base.lproj/Main.storyboard index 5fc9bb5..a14a3f2 100644 --- a/SwiftyStoreKit-tvOS-Demo/Base.lproj/Main.storyboard +++ b/SwiftyStoreKit-tvOS-Demo/Base.lproj/Main.storyboard @@ -1,14 +1,18 @@ - - + + + + + - + + - + @@ -16,6 +20,80 @@ + + + + + + + + + + diff --git a/SwiftyStoreKit-tvOS-Demo/ViewController.swift b/SwiftyStoreKit-tvOS-Demo/ViewController.swift index 630ba39..3c4b9bb 100644 --- a/SwiftyStoreKit-tvOS-Demo/ViewController.swift +++ b/SwiftyStoreKit-tvOS-Demo/ViewController.swift @@ -7,19 +7,277 @@ // import UIKit +import StoreKit +import SwiftyStoreKit + +enum RegisteredPurchase: String { + + case purchase1 + case purchase2 + case nonConsumablePurchase + case consumablePurchase + case autoRenewablePurchase + case nonRenewingPurchase +} class ViewController: UIViewController { - override func viewDidLoad() { - super.viewDidLoad() - // Do any additional setup after loading the view, typically from a nib. + let appBundleId = "com.musevisions.tvOS.SwiftyStoreKit" + + let purchase1Suffix = RegisteredPurchase.purchase1 + let purchase2Suffix = RegisteredPurchase.autoRenewablePurchase + + // MARK: actions + @IBAction func getInfo1() { + getInfo(purchase1Suffix) + } + @IBAction func purchase1() { + purchase(purchase1Suffix) + } + @IBAction func verifyPurchase1() { + verifyPurchase(purchase1Suffix) + } + @IBAction func getInfo2() { + getInfo(purchase2Suffix) + } + @IBAction func purchase2() { + purchase(purchase2Suffix) + } + @IBAction func verifyPurchase2() { + verifyPurchase(purchase2Suffix) } - override func didReceiveMemoryWarning() { - super.didReceiveMemoryWarning() - // Dispose of any resources that can be recreated. + func getInfo(_ purchase: RegisteredPurchase) { + + SwiftyStoreKit.retrieveProductsInfo([appBundleId + "." + purchase.rawValue]) { result in + + self.showAlert(self.alertForProductRetrievalInfo(result)) + } } + func purchase(_ purchase: RegisteredPurchase) { + + SwiftyStoreKit.purchaseProduct(appBundleId + "." + purchase.rawValue, atomically: true) { result in + + if case .success(let product) = result { + // Deliver content from server, then: + if product.needsFinishTransaction { + SwiftyStoreKit.finishTransaction(product.transaction) + } + } + if let alert = self.alertForPurchaseResult(result) { + self.showAlert(alert) + } + } + } + + @IBAction func restorePurchases() { + + SwiftyStoreKit.restorePurchases(atomically: true) { results in + + for product in results.restoredProducts { + // Deliver content from server, then: + if product.needsFinishTransaction { + SwiftyStoreKit.finishTransaction(product.transaction) + } + } + self.showAlert(self.alertForRestorePurchases(results)) + } + } + + @IBAction func verifyReceipt() { + + let appleValidator = AppleReceiptValidator(service: .production) + SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secret") { result in + + self.showAlert(self.alertForVerifyReceipt(result)) + + if case .error(let error) = result { + if case .noReceiptData = error { + self.refreshReceipt() + } + } + } + } + + func verifyPurchase(_ purchase: RegisteredPurchase) { + + let appleValidator = AppleReceiptValidator(service: .production) + SwiftyStoreKit.verifyReceipt(using: appleValidator, password: "your-shared-secret") { result in + + switch result { + case .success(let receipt): + + let productId = self.appBundleId + "." + purchase.rawValue + + switch purchase { + case .autoRenewablePurchase: + let purchaseResult = SwiftyStoreKit.verifySubscription( + type: .autoRenewable, + productId: productId, + inReceipt: receipt, + validUntil: Date() + ) + self.showAlert(self.alertForVerifySubscription(purchaseResult)) + case .nonRenewingPurchase: + let purchaseResult = SwiftyStoreKit.verifySubscription( + type: .nonRenewing(validDuration: 60), + productId: productId, + inReceipt: receipt, + validUntil: Date() + ) + self.showAlert(self.alertForVerifySubscription(purchaseResult)) + default: + let purchaseResult = SwiftyStoreKit.verifyPurchase( + productId: productId, + inReceipt: receipt + ) + self.showAlert(self.alertForVerifyPurchase(purchaseResult)) + } + + case .error(let error): + self.showAlert(self.alertForVerifyReceipt(result)) + if case .noReceiptData = error { + self.refreshReceipt() + } + } + } + } + + func refreshReceipt() { + + SwiftyStoreKit.refreshReceipt { result in + + self.showAlert(self.alertForRefreshReceipt(result)) + } + } } +// MARK: User facing alerts +extension ViewController { + + func alertWithTitle(_ title: String, message: String) -> UIAlertController { + + let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil)) + return alert + } + + func showAlert(_ alert: UIAlertController) { + guard let _ = self.presentedViewController else { + self.present(alert, animated: true, completion: nil) + return + } + } + + func alertForProductRetrievalInfo(_ result: RetrieveResults) -> UIAlertController { + + if let product = result.retrievedProducts.first { + let priceString = product.localizedPrice! + return alertWithTitle(product.localizedTitle, message: "\(product.localizedDescription) - \(priceString)") + } else if let invalidProductId = result.invalidProductIDs.first { + return alertWithTitle("Could not retrieve product info", message: "Invalid product identifier: \(invalidProductId)") + } else { + let errorString = result.error?.localizedDescription ?? "Unknown error. Please contact support" + return alertWithTitle("Could not retrieve product info", message: errorString) + } + } + + func alertForPurchaseResult(_ result: PurchaseResult) -> UIAlertController? { + switch result { + case .success(let product): + print("Purchase Success: \(product.productId)") + return alertWithTitle("Thank You", message: "Purchase completed") + case .error(let error): + print("Purchase Failed: \(error)") + switch error.code { + case .unknown: return alertWithTitle("Purchase failed", message: "Unknown error. Please contact support") + case .clientInvalid: // client is not allowed to issue the request, etc. + return alertWithTitle("Purchase failed", message: "Not allowed to make the payment") + case .paymentCancelled: // user cancelled the request, etc. + return nil + case .paymentInvalid: // purchase identifier was invalid, etc. + return alertWithTitle("Purchase failed", message: "The purchase identifier was invalid") + case .paymentNotAllowed: // this device is not allowed to make the payment + return alertWithTitle("Purchase failed", message: "The device is not allowed to make the payment") + case .storeProductNotAvailable: // Product is not available in the current storefront + return alertWithTitle("Purchase failed", message: "The product is not available in the current storefront") + case .cloudServicePermissionDenied: // user has not allowed access to cloud service information + return alertWithTitle("Purchase failed", message: "Access to cloud service information is not allowed") + case .cloudServiceNetworkConnectionFailed: // the device could not connect to the nework + return alertWithTitle("Purchase failed", message: "Could not connect to the network") + } + } + } + + func alertForRestorePurchases(_ results: RestoreResults) -> UIAlertController { + + if results.restoreFailedProducts.count > 0 { + print("Restore Failed: \(results.restoreFailedProducts)") + return alertWithTitle("Restore failed", message: "Unknown error. Please contact support") + } else if results.restoredProducts.count > 0 { + print("Restore Success: \(results.restoredProducts)") + return alertWithTitle("Purchases Restored", message: "All purchases have been restored") + } else { + print("Nothing to Restore") + return alertWithTitle("Nothing to restore", message: "No previous purchases were found") + } + } + + func alertForVerifyReceipt(_ result: VerifyReceiptResult) -> UIAlertController { + + switch result { + case .success(let receipt): + print("Verify receipt Success: \(receipt)") + return alertWithTitle("Receipt verified", message: "Receipt verified remotly") + case .error(let error): + print("Verify receipt Failed: \(error)") + switch error { + case .noReceiptData : + return alertWithTitle("Receipt verification", message: "No receipt data, application will try to get a new one. Try again.") + default: + return alertWithTitle("Receipt verification", message: "Receipt verification failed") + } + } + } + + func alertForVerifySubscription(_ result: VerifySubscriptionResult) -> UIAlertController { + + switch result { + case .purchased(let expiresDate): + print("Product is valid until \(expiresDate)") + return alertWithTitle("Product is purchased", message: "Product is valid until \(expiresDate)") + case .expired(let expiresDate): + print("Product is expired since \(expiresDate)") + return alertWithTitle("Product expired", message: "Product is expired since \(expiresDate)") + case .notPurchased: + print("This product has never been purchased") + return alertWithTitle("Not purchased", message: "This product has never been purchased") + } + } + + func alertForVerifyPurchase(_ result: VerifyPurchaseResult) -> UIAlertController { + + switch result { + case .purchased: + print("Product is purchased") + return alertWithTitle("Product is purchased", message: "Product will not expire") + case .notPurchased: + print("This product has never been purchased") + return alertWithTitle("Not purchased", message: "This product has never been purchased") + } + } + + func alertForRefreshReceipt(_ result: RefreshReceiptResult) -> UIAlertController { + switch result { + case .success(let receiptData): + print("Receipt refresh Success: \(receiptData.base64EncodedString)") + return alertWithTitle("Receipt refreshed", message: "Receipt refreshed successfully") + case .error(let error): + print("Receipt refresh Failed: \(error)") + return alertWithTitle("Receipt refresh failed", message: "Receipt refresh failed") + } + } + +} diff --git a/SwiftyStoreKit.xcodeproj/project.pbxproj b/SwiftyStoreKit.xcodeproj/project.pbxproj index e3da70c..76a256d 100644 --- a/SwiftyStoreKit.xcodeproj/project.pbxproj +++ b/SwiftyStoreKit.xcodeproj/project.pbxproj @@ -34,6 +34,8 @@ 654287F31E79F5A000F61800 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 654287F21E79F5A000F61800 /* ViewController.swift */; }; 654287F61E79F5A000F61800 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 654287F41E79F5A000F61800 /* Main.storyboard */; }; 654287F81E79F5A000F61800 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 654287F71E79F5A000F61800 /* Assets.xcassets */; }; + 654287FD1E79F75000F61800 /* SwiftyStoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54C0D52C1CF7404500F90BCE /* SwiftyStoreKit.framework */; }; + 654287FE1E79F75000F61800 /* SwiftyStoreKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 54C0D52C1CF7404500F90BCE /* SwiftyStoreKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 658A08371E2EC24E0074A98F /* PaymentQueueController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 658A08361E2EC24E0074A98F /* PaymentQueueController.swift */; }; 658A08381E2EC24E0074A98F /* PaymentQueueController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 658A08361E2EC24E0074A98F /* PaymentQueueController.swift */; }; 658A08391E2EC24E0074A98F /* PaymentQueueController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 658A08361E2EC24E0074A98F /* PaymentQueueController.swift */; }; @@ -79,6 +81,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 654287FF1E79F75000F61800 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 6502F5F61B985833004E342D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 54C0D52B1CF7404500F90BCE; + remoteInfo = SwiftyStoreKit_tvOS; + }; 658A08441E2EC5120074A98F /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 6502F5F61B985833004E342D /* Project object */; @@ -110,6 +119,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 654288011E79F75100F61800 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 654287FE1E79F75000F61800 /* SwiftyStoreKit.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; 65F7DF921DCD524300835D30 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -146,7 +166,7 @@ 650307F71E317BCF001332A4 /* CompleteTransactionsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompleteTransactionsController.swift; sourceTree = ""; }; 650307FB1E33154F001332A4 /* ProductsInfoController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProductsInfoController.swift; sourceTree = ""; }; 653722801DB8282600C8F944 /* SKProduct+LocalizedPrice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SKProduct+LocalizedPrice.swift"; sourceTree = ""; }; - 654287EE1E79F5A000F61800 /* SwiftyStoreKit-tvOS-Demo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SwiftyStoreKit-tvOS-Demo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 654287EE1E79F5A000F61800 /* SwiftyStoreKit-tvOSDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "SwiftyStoreKit-tvOSDemo.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 654287F01E79F5A000F61800 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 654287F21E79F5A000F61800 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 654287F51E79F5A000F61800 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -216,6 +236,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 654287FD1E79F75000F61800 /* SwiftyStoreKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -266,7 +287,7 @@ C4FD3A011C2954C10035CFF3 /* SwiftyStoreKit_macOSDemo.app */, 54C0D52C1CF7404500F90BCE /* SwiftyStoreKit.framework */, 658A083E1E2EC5120074A98F /* SwiftyStoreKitTests.xctest */, - 654287EE1E79F5A000F61800 /* SwiftyStoreKit-tvOS-Demo.app */, + 654287EE1E79F5A000F61800 /* SwiftyStoreKit-tvOSDemo.app */, ); name = Products; sourceTree = ""; @@ -445,21 +466,23 @@ productReference = 6502F62D1B985C40004E342D /* SwiftyStoreKit.framework */; productType = "com.apple.product-type.framework"; }; - 654287ED1E79F5A000F61800 /* SwiftyStoreKit-tvOS-Demo */ = { + 654287ED1E79F5A000F61800 /* SwiftyStoreKit-tvOSDemo */ = { isa = PBXNativeTarget; - buildConfigurationList = 654287FC1E79F5A000F61800 /* Build configuration list for PBXNativeTarget "SwiftyStoreKit-tvOS-Demo" */; + buildConfigurationList = 654287FC1E79F5A000F61800 /* Build configuration list for PBXNativeTarget "SwiftyStoreKit-tvOSDemo" */; buildPhases = ( 654287EA1E79F5A000F61800 /* Sources */, 654287EB1E79F5A000F61800 /* Frameworks */, 654287EC1E79F5A000F61800 /* Resources */, + 654288011E79F75100F61800 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( + 654288001E79F75000F61800 /* PBXTargetDependency */, ); - name = "SwiftyStoreKit-tvOS-Demo"; + name = "SwiftyStoreKit-tvOSDemo"; productName = "SwiftyStoreKit-tvOS-Demo"; - productReference = 654287EE1E79F5A000F61800 /* SwiftyStoreKit-tvOS-Demo.app */; + productReference = 654287EE1E79F5A000F61800 /* SwiftyStoreKit-tvOSDemo.app */; productType = "com.apple.product-type.application"; }; 658A083D1E2EC5120074A98F /* SwiftyStoreKitTests */ = { @@ -531,6 +554,7 @@ TargetAttributes = { 54C0D52B1CF7404500F90BCE = { CreatedOnToolsVersion = 7.3.1; + DevelopmentTeam = M54ZVB688G; }; 6502F5FD1B985833004E342D = { CreatedOnToolsVersion = 7.0; @@ -579,7 +603,7 @@ 54C0D52B1CF7404500F90BCE /* SwiftyStoreKit_tvOS */, 6502F5FD1B985833004E342D /* SwiftyStoreKit_iOSDemo */, C4FD3A001C2954C10035CFF3 /* SwiftyStoreKit_macOSDemo */, - 654287ED1E79F5A000F61800 /* SwiftyStoreKit-tvOS-Demo */, + 654287ED1E79F5A000F61800 /* SwiftyStoreKit-tvOSDemo */, 658A083D1E2EC5120074A98F /* SwiftyStoreKitTests */, ); }; @@ -782,6 +806,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 654288001E79F75000F61800 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 54C0D52B1CF7404500F90BCE /* SwiftyStoreKit_tvOS */; + targetProxy = 654287FF1E79F75000F61800 /* PBXContainerItemProxy */; + }; 658A08451E2EC5120074A98F /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 6502F62C1B985C40004E342D /* SwiftyStoreKit_iOS */; @@ -847,6 +876,7 @@ "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = M54ZVB688G; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -871,6 +901,7 @@ "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; + DEVELOPMENT_TEAM = M54ZVB688G; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; @@ -1063,6 +1094,7 @@ 654287FA1E79F5A000F61800 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CLANG_ANALYZER_NONNULL = YES; @@ -1083,6 +1115,7 @@ 654287FB1E79F5A000F61800 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; ASSETCATALOG_COMPILER_LAUNCHIMAGE_NAME = LaunchImage; CLANG_ANALYZER_NONNULL = YES; @@ -1253,7 +1286,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 654287FC1E79F5A000F61800 /* Build configuration list for PBXNativeTarget "SwiftyStoreKit-tvOS-Demo" */ = { + 654287FC1E79F5A000F61800 /* Build configuration list for PBXNativeTarget "SwiftyStoreKit-tvOSDemo" */ = { isa = XCConfigurationList; buildConfigurations = ( 654287FA1E79F5A000F61800 /* Debug */, diff --git a/SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-tvOS-Demo.xcscheme b/SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-tvOS-Demo.xcscheme index c245cf1..5b668f3 100644 --- a/SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-tvOS-Demo.xcscheme +++ b/SwiftyStoreKit.xcodeproj/xcshareddata/xcschemes/SwiftyStoreKit-tvOS-Demo.xcscheme @@ -15,8 +15,8 @@ @@ -33,8 +33,8 @@ @@ -56,8 +56,8 @@ @@ -75,8 +75,8 @@