diff --git a/CHANGELOG.md b/CHANGELOG.md index 90679740..a80b72b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ All notable changes to this project will be documented in this file. +## [0.13.0](https://github.com/bizz84/SwiftyStoreKit/releases/tag/0.13.0) Add support for downloading content hosted with Apple + +* Add support for downloading content hosted with Apple ([#343](https://github.com/bizz84/SwiftyStoreKit/pull/343), related issue: [#128](https://github.com/bizz84/SwiftyStoreKit/issues/128)) + ## [0.12.1](https://github.com/bizz84/SwiftyStoreKit/releases/tag/0.12.1) Assert that `completeTransactions` was called when the app launches. * Assert that `completeTransactions()` was called when the app launches ([#337](https://github.com/bizz84/SwiftyStoreKit/pull/337), related issue: [#287](https://github.com/bizz84/SwiftyStoreKit/issues/287)) diff --git a/README.md b/README.md index c2e4e02a..cb1a3bf6 100644 --- a/README.md +++ b/README.md @@ -255,6 +255,48 @@ SwiftyStoreKit provides three operations that can be performed **atomically** or * Restoring purchases * Completing transactions on app launch +### Downloading content hosted with Apple + +Quoting Apple Docs: + +> When you create a product in iTunes Connect, you can associate one or more pieces of downloadable content with it. At runtime, when a product is purchased by a user, your app uses SKDownload objects to download the content from the App Store. + +> Your app never directly creates a SKDownload object. Instead, after a payment is processed, your app reads the transaction object’s downloads property to retrieve an array of SKDownload objects associated with the transaction. + +> To download the content, you queue a download object on the payment queue and wait for the content to be downloaded. After a download completes, read the download object’s contentURL property to get a URL to the downloaded content. Your app must process the downloaded file before completing the transaction. For example, it might copy the file into a directory whose contents are persistent. When all downloads are complete, you finish the transaction. After the transaction is finished, the download objects cannot be queued to the payment queue and any URLs to the downloaded content are invalid. + +To start the downloads: + +```swift +SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1", quantity: 1, atomically: false) { result in + switch result { + case .success(let product): + let downloads = purchase.transaction.downloads + if !downloads.isEmpty { + SwiftyStoreKit.start(downloads) + } + case .error(let error): + print("\(error)") + } +} +``` + +To check the updated downloads, setup a `updatedDownloadsHandler` block in your AppDelegate: + +```swift +SwiftyStoreKit.updatedDownloadsHandler = { downloads in + + // contentURL is not nil if downloadState == .finished + let contentURLs = downloads.flatMap { $0.contentURL } + if contentURLs.count == downloads.count { + // process all downloaded files, then finish the transaction + SwiftyStoreKit.finishTransaction(downloads[0].transaction) + } +} +``` + +To control the state of the downloads, SwiftyStoreKit offers `start()`, `pause()`, `resume()`, `cancel()` methods. + ## Receipt verification According to [Apple - Delivering Products](https://developer.apple.com/library/content/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/DeliverProduct.html#//apple_ref/doc/uid/TP40008267-CH5-SW4): diff --git a/SwiftyStoreKit-iOS-Demo/AppDelegate.swift b/SwiftyStoreKit-iOS-Demo/AppDelegate.swift index d83ed972..6f3b6eed 100644 --- a/SwiftyStoreKit-iOS-Demo/AppDelegate.swift +++ b/SwiftyStoreKit-iOS-Demo/AppDelegate.swift @@ -32,19 +32,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey : Any]? = nil) -> Bool { - completeIAPTransactions() + setupIAP() return true } - func completeIAPTransactions() { + func setupIAP() { SwiftyStoreKit.completeTransactions(atomically: true) { purchases in for purchase in purchases { switch purchase.transaction.transactionState { case .purchased, .restored: - if purchase.needsFinishTransaction { + let downloads = purchase.transaction.downloads + if !downloads.isEmpty { + SwiftyStoreKit.start(downloads) + } else if purchase.needsFinishTransaction { // Deliver content from server, then: SwiftyStoreKit.finishTransaction(purchase.transaction) } @@ -54,5 +57,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } } + + SwiftyStoreKit.updatedDownloadsHandler = { downloads in + + // contentURL is not nil if downloadState == .finished + let contentURLs = downloads.flatMap { $0.contentURL } + if contentURLs.count == downloads.count { + print("Saving: \(contentURLs)") + SwiftyStoreKit.finishTransaction(downloads[0].transaction) + } + } } } diff --git a/SwiftyStoreKit-iOS-Demo/ViewController.swift b/SwiftyStoreKit-iOS-Demo/ViewController.swift index 4c1f8c99..7a56f904 100644 --- a/SwiftyStoreKit-iOS-Demo/ViewController.swift +++ b/SwiftyStoreKit-iOS-Demo/ViewController.swift @@ -133,6 +133,10 @@ class ViewController: UIViewController { NetworkActivityIndicatorManager.networkOperationFinished() if case .success(let purchase) = result { + let downloads = purchase.transaction.downloads + if !downloads.isEmpty { + SwiftyStoreKit.start(downloads) + } // Deliver content from server, then: if purchase.needsFinishTransaction { SwiftyStoreKit.finishTransaction(purchase.transaction) @@ -150,9 +154,14 @@ class ViewController: UIViewController { SwiftyStoreKit.restorePurchases(atomically: true) { results in NetworkActivityIndicatorManager.networkOperationFinished() - for purchase in results.restoredPurchases where purchase.needsFinishTransaction { - // Deliver content from server, then: - SwiftyStoreKit.finishTransaction(purchase.transaction) + for purchase in results.restoredPurchases { + let downloads = purchase.transaction.downloads + if !downloads.isEmpty { + SwiftyStoreKit.start(downloads) + } else if purchase.needsFinishTransaction { + // Deliver content from server, then: + SwiftyStoreKit.finishTransaction(purchase.transaction) + } } self.showAlert(self.alertForRestorePurchases(results)) } diff --git a/SwiftyStoreKit/PaymentQueueController.swift b/SwiftyStoreKit/PaymentQueueController.swift index f50d5701..0ac196e2 100644 --- a/SwiftyStoreKit/PaymentQueueController.swift +++ b/SwiftyStoreKit/PaymentQueueController.swift @@ -47,7 +47,12 @@ public protocol PaymentQueue: class { func remove(_ observer: SKPaymentTransactionObserver) func add(_ payment: SKPayment) - + + func start(_ downloads: [SKDownload]) + func pause(_ downloads: [SKDownload]) + func resume(_ downloads: [SKDownload]) + func cancel(_ downloads: [SKDownload]) + func restoreCompletedTransactions(withApplicationUsername username: String?) func finishTransaction(_ transaction: SKPaymentTransaction) @@ -151,7 +156,21 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver { paymentQueue.finishTransaction(skTransaction) } + func start(_ downloads: [SKDownload]) { + paymentQueue.start(downloads) + } + func pause(_ downloads: [SKDownload]) { + paymentQueue.pause(downloads) + } + func resume(_ downloads: [SKDownload]) { + paymentQueue.resume(downloads) + } + func cancel(_ downloads: [SKDownload]) { + paymentQueue.cancel(downloads) + } + var shouldAddStorePaymentHandler: ShouldAddStorePaymentHandler? + var updatedDownloadsHandler: UpdatedDownloadsHandler? // MARK: SKPaymentTransactionObserver func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) { @@ -210,6 +229,7 @@ class PaymentQueueController: NSObject, SKPaymentTransactionObserver { func paymentQueue(_ queue: SKPaymentQueue, updatedDownloads downloads: [SKDownload]) { + updatedDownloadsHandler?(downloads) } func paymentQueue(_ queue: SKPaymentQueue, shouldAddStorePayment payment: SKPayment, for product: SKProduct) -> Bool { diff --git a/SwiftyStoreKit/SwiftyStoreKit+Types.swift b/SwiftyStoreKit/SwiftyStoreKit+Types.swift index bb3a686d..df349151 100644 --- a/SwiftyStoreKit/SwiftyStoreKit+Types.swift +++ b/SwiftyStoreKit/SwiftyStoreKit+Types.swift @@ -55,6 +55,7 @@ public protocol PaymentTransaction { var transactionDate: Date? { get } var transactionState: SKPaymentTransactionState { get } var transactionIdentifier: String? { get } + var downloads: [SKDownload] { get } } // Add PaymentTransaction conformance to SKPaymentTransaction @@ -80,6 +81,7 @@ public struct RestoreResults { } public typealias ShouldAddStorePaymentHandler = (_ payment: SKPayment, _ product: SKProduct) -> Bool +public typealias UpdatedDownloadsHandler = (_ downloads: [SKDownload]) -> Void // MARK: Receipt verification diff --git a/SwiftyStoreKit/SwiftyStoreKit.swift b/SwiftyStoreKit/SwiftyStoreKit.swift index d812893b..70d3442d 100644 --- a/SwiftyStoreKit/SwiftyStoreKit.swift +++ b/SwiftyStoreKit/SwiftyStoreKit.swift @@ -216,6 +216,28 @@ extension SwiftyStoreKit { sharedInstance.paymentQueueController.shouldAddStorePaymentHandler = shouldAddStorePaymentHandler } } + + /** + * Register a handler for paymentQueue(_:updatedDownloads:) + */ + public static var updatedDownloadsHandler: UpdatedDownloadsHandler? { + didSet { + sharedInstance.paymentQueueController.updatedDownloadsHandler = updatedDownloadsHandler + } + } + + public class func start(_ downloads: [SKDownload]) { + sharedInstance.paymentQueueController.start(downloads) + } + public class func pause(_ downloads: [SKDownload]) { + sharedInstance.paymentQueueController.pause(downloads) + } + public class func resume(_ downloads: [SKDownload]) { + sharedInstance.paymentQueueController.resume(downloads) + } + public class func cancel(_ downloads: [SKDownload]) { + sharedInstance.paymentQueueController.cancel(downloads) + } } extension SwiftyStoreKit { diff --git a/SwiftyStoreKitTests/PaymentQueueSpy.swift b/SwiftyStoreKitTests/PaymentQueueSpy.swift index d66fc4cc..f763989c 100644 --- a/SwiftyStoreKitTests/PaymentQueueSpy.swift +++ b/SwiftyStoreKitTests/PaymentQueueSpy.swift @@ -44,4 +44,20 @@ class PaymentQueueSpy: PaymentQueue { finishTransactionCalledCount += 1 } + + func start(_ downloads: [SKDownload]) { + + } + + func pause(_ downloads: [SKDownload]) { + + } + + func resume(_ downloads: [SKDownload]) { + + } + + func cancel(_ downloads: [SKDownload]) { + + } }