Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/non atomic purchases #107

Merged
merged 10 commits into from
Nov 16, 2016
99 changes: 88 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ SwiftyStoreKit is a lightweight In App Purchases framework for iOS 8.0+, tvOS 9.
<img src="https://github.com/bizz84/SwiftyStoreKit/raw/master/Screenshots/Preview.png" width="320">
<img src="https://github.com/bizz84/SwiftyStoreKit/raw/master/Screenshots/Preview2.png" width="320">

### Setup + Complete Transactions
## App startup

### Complete Transactions

Apple recommends to register a transaction observer [as soon as the app starts](https://developer.apple.com/library/ios/technotes/tn2387/_index.html):
> Adding your app's observer at launch ensures that it will persist during all launches of your app, thus allowing your app to receive all the payment queue notifications.
Expand All @@ -33,13 +35,17 @@ SwiftyStoreKit supports this by calling `completeTransactions()` when the app st
```swift
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {

SwiftyStoreKit.completeTransactions() { completedTransactions in
SwiftyStoreKit.completeTransactions(atomically: true) { products in

for completedTransaction in completedTransactions {
for product in products {

if completedTransaction.transactionState == .purchased || completedTransaction.transactionState == .restored {
if product.transaction.transactionState == .purchased || product.transaction.transactionState == .restored {

print("purchased: \(completedTransaction.productId)")
if product.needsFinishTransaction {
// Deliver content from server, then:
SwiftyStoreKit.finishTransaction(product.transaction)
}
print("purchased: \(product)")
}
}
}
Expand All @@ -49,6 +55,8 @@ func application(application: UIApplication, didFinishLaunchingWithOptions launc

If there are any pending transactions at this point, these will be reported by the completion block so that the app state and UI can be updated.

## Purchases

### Retrieve products info
```swift
SwiftyStoreKit.retrieveProductsInfo(["com.musevisions.SwiftyStoreKit.Purchase1"]) { result in
Expand All @@ -64,13 +72,33 @@ SwiftyStoreKit.retrieveProductsInfo(["com.musevisions.SwiftyStoreKit.Purchase1"]
}
}
```

### Purchase a product

* **Atomic**: to be used when the content is delivered immediately.

```swift
SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1") { result in
SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1", atomically: true) { result in
switch result {
case .success(let productId):
print("Purchase Success: \(productId)")
case .success(let product):
print("Purchase Success: \(product.productId)")
case .error(let error):
print("Purchase Failed: \(error)")
}
}
```

* **Non-Atomic**: to be used when the content is delivered by the server.

```swift
SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1", atomically: false) { result in
switch result {
case .success(let product):
// fetch content from your server, then:
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
print("Purchase Success: \(product.productId)")
case .error(let error):
print("Purchase Failed: \(error)")
}
Expand All @@ -79,20 +107,69 @@ SwiftyStoreKit.purchaseProduct("com.musevisions.SwiftyStoreKit.Purchase1") { res

### Restore previous purchases

* **Atomic**: to be used when the content is delivered immediately.

```swift
SwiftyStoreKit.restorePurchases(atomically: true) { results in
if results.restoreFailedProducts.count > 0 {
print("Restore Failed: \(results.restoreFailedProducts)")
}
else if results.restoredProducts.count > 0 {
print("Restore Success: \(results.restoredProducts)")
}
else {
print("Nothing to Restore")
}
}
```

* **Non-Atomic**: to be used when the content is delivered by the server.

```swift
SwiftyStoreKit.restorePurchases() { results in
SwiftyStoreKit.restorePurchases(atomically: false) { results in
if results.restoreFailedProducts.count > 0 {
print("Restore Failed: \(results.restoreFailedProducts)")
}
else if results.restoredProductIds.count > 0 {
print("Restore Success: \(results.restoredProductIds)")
else if results.restoredProducts.count > 0 {
for product in results.restoredProducts {
// fetch content from your server, then:
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
}
print("Restore Success: \(results.restoredProducts)")
}
else {
print("Nothing to Restore")
}
}
```

#### What does atomic / non-atomic mean?

When you purchase a product the following things happen:

* A payment is added to the payment queue for your IAP.
* When the payment has been processed with Apple, the payment queue is updated so that the appropriate transaction can be handled.
* If the transaction state is **purchased** or **restored**, the app can unlock the functionality purchased by the user.
* The app should call `finishTransaction()` to complete the purchase.

This is what is [recommended by Apple](https://developer.apple.com/reference/storekit/skpaymentqueue/1506003-finishtransaction):

> Your application should call finishTransaction(_:) only after it has successfully processed the transaction and unlocked the functionality purchased by the user.

* A purchase is **atomic** when the app unlocks the functionality purchased by the user immediately and call `finishTransaction()` at the same time. This is desirable if you're unlocking functionality that is already inside the app.

* In cases when you need to make a request to your own server in order to unlock the functionality, you can use a **non-atomic** purchase instead.

SwiftyStoreKit provides three operations that can be performed **atomically** or **non-atomically**:

* Making a purchase
* Restoring purchases
* Completing transactions on app launch

## Receipt verification

### Retrieve local receipt

```swift
Expand Down
12 changes: 8 additions & 4 deletions SwiftyStoreKit-iOS-Demo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {

func completeIAPTransactions() {

SwiftyStoreKit.completeTransactions() { completedTransactions in
SwiftyStoreKit.completeTransactions(atomically: true) { products in

for completedTransaction in completedTransactions {
for product in products {

if completedTransaction.transactionState == .purchased || completedTransaction.transactionState == .restored {
if product.transaction.transactionState == .purchased || product.transaction.transactionState == .restored {

print("purchased: \(completedTransaction.productId)")
if product.needsFinishTransaction {
// Deliver content from server, then:
SwiftyStoreKit.finishTransaction(product.transaction)
}
print("purchased: \(product.productId)")
}
}
}
Expand Down
39 changes: 26 additions & 13 deletions SwiftyStoreKit-iOS-Demo/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,31 @@ class ViewController: UIViewController {
func purchase(_ purchase: RegisteredPurchase) {

NetworkActivityIndicatorManager.networkOperationStarted()
SwiftyStoreKit.purchaseProduct(AppBundleId + "." + purchase.rawValue) { result in
SwiftyStoreKit.purchaseProduct(AppBundleId + "." + purchase.rawValue, atomically: true) { result in
NetworkActivityIndicatorManager.networkOperationFinished()

if case .success(let product) = result {
// Deliver content from server, then:
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
}
self.showAlert(self.alertForPurchaseResult(result))
}
}

@IBAction func restorePurchases() {

NetworkActivityIndicatorManager.networkOperationStarted()
SwiftyStoreKit.restorePurchases() { results in
SwiftyStoreKit.restorePurchases(atomically: true) { results in
NetworkActivityIndicatorManager.networkOperationFinished()

for product in results.restoredProducts {
// Deliver content from server, then:
if product.needsFinishTransaction {
SwiftyStoreKit.finishTransaction(product.transaction)
}
}
self.showAlert(self.alertForRestorePurchases(results))
}
}
Expand Down Expand Up @@ -176,7 +189,7 @@ extension ViewController {
}
}

func alertForProductRetrievalInfo(_ result: SwiftyStoreKit.RetrieveResults) -> UIAlertController {
func alertForProductRetrievalInfo(_ result: RetrieveResults) -> UIAlertController {

if let product = result.retrievedProducts.first {
let priceString = product.localizedPrice!
Expand All @@ -191,10 +204,10 @@ extension ViewController {
}
}

func alertForPurchaseResult(_ result: SwiftyStoreKit.PurchaseResult) -> UIAlertController {
func alertForPurchaseResult(_ result: PurchaseResult) -> UIAlertController {
switch result {
case .success(let productId):
print("Purchase Success: \(productId)")
case .success(let product):
print("Purchase Success: \(product.productId)")
return alertWithTitle("Thank You", message: "Purchase completed")
case .error(let error):
print("Purchase Failed: \(error)")
Expand All @@ -214,14 +227,14 @@ extension ViewController {
}
}

func alertForRestorePurchases(_ results: SwiftyStoreKit.RestoreResults) -> UIAlertController {
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.restoredProductIds.count > 0 {
print("Restore Success: \(results.restoredProductIds)")
else if results.restoredProducts.count > 0 {
print("Restore Success: \(results.restoredProducts)")
return alertWithTitle("Purchases Restored", message: "All purchases have been restored")
}
else {
Expand All @@ -231,7 +244,7 @@ extension ViewController {
}


func alertForVerifyReceipt(_ result: SwiftyStoreKit.VerifyReceiptResult) -> UIAlertController {
func alertForVerifyReceipt(_ result: VerifyReceiptResult) -> UIAlertController {

switch result {
case .success(let receipt):
Expand All @@ -248,7 +261,7 @@ extension ViewController {
}
}

func alertForVerifySubscription(_ result: SwiftyStoreKit.VerifySubscriptionResult) -> UIAlertController {
func alertForVerifySubscription(_ result: VerifySubscriptionResult) -> UIAlertController {

switch result {
case .purchased(let expiresDate):
Expand All @@ -263,7 +276,7 @@ extension ViewController {
}
}

func alertForVerifyPurchase(_ result: SwiftyStoreKit.VerifyPurchaseResult) -> UIAlertController {
func alertForVerifyPurchase(_ result: VerifyPurchaseResult) -> UIAlertController {

switch result {
case .purchased:
Expand All @@ -275,7 +288,7 @@ extension ViewController {
}
}

func alertForRefreshReceipt(_ result: SwiftyStoreKit.RefreshReceiptResult) -> UIAlertController {
func alertForRefreshReceipt(_ result: RefreshReceiptResult) -> UIAlertController {
switch result {
case .success(let receiptData):
print("Receipt refresh Success: \(receiptData.base64EncodedString)")
Expand Down
12 changes: 8 additions & 4 deletions SwiftyStoreKit-macOS-Demo/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,17 @@ class AppDelegate: NSObject, NSApplicationDelegate {

func completeIAPTransactions() {

SwiftyStoreKit.completeTransactions() { completedTransactions in
SwiftyStoreKit.completeTransactions(atomically: true) { products in

for completedTransaction in completedTransactions {
for product in products {

if completedTransaction.transactionState == .purchased || completedTransaction.transactionState == .restored {
if product.transaction.transactionState == .purchased || product.transaction.transactionState == .restored {

print("purchased: \(completedTransaction.productId)")
if product.needsFinishTransaction {
// Deliver content from server, then:
SwiftyStoreKit.finishTransaction(product.transaction)
}
print("purchased: \(product.productId)")
}
}
}
Expand Down
Loading