In-App Purchase How To – Part 6

Today you will enter code in the Nifty-IAP project’s ProductViewController.swift file to implement tasks shown on this card.

tasks-card4

Before proceeding I assume you’ve read part 1, part 2, part 3part 4, and part 5 of the tutorial. I also assume you created a branch called part6 from the branch called part5.

Task 1

Here is a diagram and description of task 1.

Diagram 1

Diagram 1

  1. User request to restore purchases in the Nifty app store
  2. Nifty app store send restore purchases request to StoreKit
  3. StoreKit in turn send restore purchases request to the App Store
  4. App Store restore purchases
  5. App Store return a restored purchased transaction to StoreKit
  6. StoreKit pass the transaction to the Nifty app store
  7. Nifty App Store deliver products and updated its user interfaces

To implement task 1 in the Nifty application’s ProductViewController.swift file, you’ll have to enter this code in the restoreButtonTapped() function.

@IBAction func restoreButtonTapped(sender: UIButton) {
  SKPaymentQueue.defaultQueue().restoreCompletedTransactions()
}

The statement you entered in the restoreButtonTapped() function ask the SKPayment queue to restore previously completed purchase transactions. The payment queue respond by delivering a new transaction for each previously completed transaction that can be restored. Each transaction includes a copy of the original purchase transaction. After the completed transactions are delivered, the payment queue calls the observer’s function, paymentQueueRestoreCompletedTransactionsFinished(). If an error occurred while restoring transactions, the observer will be notified through its paymentQueue function; which is called restoreCompletedTransactionsFailedWithError().

With that said, here is the code to implement the paymentQueueRestoredCompletedTransactionsFinished() function in the ProductViewController class. Put the function at the end of the class.

func paymentQueueRestoreCompletedTransactionsFinished(queue: SKPaymentQueue) {
  print("*** Purchased Transactions Restored ***")
  for transaction:SKPaymentTransaction in queue.transactions {
      if transaction.payment.productIdentifier == "net.theapplady.Nifty_video1" {
        self.deliverProduct(transaction) // unlock Hello music video
            
      } else if transaction.payment.productIdentifier == "net.theapplady.Nifty_Video2" {
          self.deliverProduct(transaction) // unlock Crazy music video
            
      } else if transaction.payment.productIdentifier == "net.theapplady.Nifty_Video3"{
          self.deliverProduct(transaction) // unlock Shake It Off music video
            
      } else if transaction.payment.productIdentifier == "net.theapplady.Nifty_Video4" {
          self.deliverProduct(transaction) // unlock Skyfall music video
      }
  }
}

Go ahead and run the app on you iPhone device and tap the Restore button. As you can see in the image below of the Nifty app store, it shows which purchased products were restored.

niftyiap-figure6-9

As before, you can play a video by tapping the tableView cell that contain a checkmark. When you are done testing the Nifty-IAP app, terminate it in Xcode and use Xcode’s Source Control/Commit command to commit changes made to the ProductViewController class, in the Git repository.

Task 2

So far you entered code in the ProductViewController class to restore previously purchased products, only when the user tap the Restore button. This technique works for these product types:

  • Non-Consumable product
  • Auto-Renewable Subscription
  • Free Subscription

If you provide Consumable product types in your iOS application. The user cannot restored them from Apples’s App Store. This makes logical sense because consumable products are consumed or used up within your iOS app.

If you provide Non-Renewable Subscription product types in your iOS application, you the app developer is responsible for providing a way for the user to restore them. Apple provide the following technologies you can use to enable the user to restore Non-Renewable Subscriptions:

  • iCloud
  • your server
  • NSUserDefault
  • iOS keychain

In the remainder of today’s tutorial you’ll use the iOS keychain and a third party Swift keychain class to keep track of products the user purchased in the Nifty app and automatically restore them. In the real world, you should use the technique you’ll learn today only if you provide Non-Renewable subscription products in your iOS application. Specifically, you will implement these iOS keychain tasks in the Nifty-IAP app. The first three tasks shown in the table below will be done in individual user defined functions.

iOS Keychain Task When it is done
1 Add a purchased product id in the iOS keychain When the user purchased a product
2 Get a product id from the iOS keychain and add it in the purchasedProductIdentifiers set Before the user sees the ProductView
3 Discard a product id from the iOS keychain and the purchasedProductIdentifier set At your discretion
4 Discard all product ids from the iOS keychain and the purchasedProductIdentifiers set At your discretion

What is iOS Keychain

iOS keychain is an encrypted container for storing small pieces of data that needs to be secured. On an iOS device, each application has its own keychain to which only the application has access to it. Data stored in the iOS keychain are safe and unaccessible by other applications. Furthermore, keychain data are stored separately from the application’s bundle. You can think of the iOS keychain as a physical keychain; further more, you can think of the piece of data stored in the iOS keychain as a key.

niftyiap-figure9-3

In the ProductViewController class enter the highlighted statement shown below. It declare a keychain object.

class ProductViewController: UIViewController, SKProductsRequestDelegate, SKPaymentTransactionObserver {
  @IBOutlet weak var headerLabel: UILabel!
  @IBOutlet weak var tableView: UITableView!
    
  var buyNowButton = UIButton()
  var moviePlayer: AVPlayerViewController!
  var product: SKProduct!
  var productsArray = [SKProduct]()
  var purchasedProductIdentifiers: Set<String> = Set()
  var productIdentifiers = Set<String>()
  let keychain = KeychainSwift()
  ...
}

Enter code shown below at the end of the ProductViewController class.

func addKeyToKeychain(productIdentifier:String) {
    if productIdentifier == "net.theapplady.Nifty_video1" {
        keychain.set(productIdentifier, forKey: "videoKey1")
        
    } else if productIdentifier == "net.theapplady.Nifty_Video2" {
        keychain.set(productIdentifier, forKey: "videoKey2")
        
    } else if productIdentifier == "net.theapplady.Nifty_Video3" {
        keychain.set(productIdentifier, forKey: "videoKey3")
        
    } else if productIdentifier == "net.theapplady.Nifty_Video4" {
        keychain.set(productIdentifier, forKey: "videoKey4")
    }
}

func getKeyFromKeychain(key: String, productIdentifier: String) {
    if let pid = keychain.get(key) {
        purchasedProductIdentifiers.insert(pid)
        print("An entry was added in the purchasedProductIdentifiers set")
    }
}

func discardKeyFromKeychain(key: String, productName:String) {
    let alert = UIAlertController(title: "Are you sure you want to do this?",
    message: "Delete an item from the keychain for the video, \(productName)",
    preferredStyle: .Alert)
    
    let yesButton = UIAlertAction(title: "Yes, I'm sure", style: .Default) {
        UIAlertAction in
        switch productName {
        case "Crazy":
            // Delete an item in the app's iOS keychain and the set
            self.keychain.delete("videoKey2")
            self.purchasedProductIdentifiers.remove("net.theapplady.Nifty_Video2")
        case "Shake It Off":
            // Delete an item in the app's iOS keychain and the set
            self.keychain.delete("videoKey3")
            self.purchasedProductIdentifiers.remove("net.theapplady.Nifty_Video3")
        case "Skyfall":
            // Delete an item in the app's iOS keychain and the set
            self.keychain.delete("videoKey4")
            self.purchasedProductIdentifiers.remove("net.theapplady.Nifty_Video4")
        case "Hello":
            // Delete an item in the app's iOS keychain and the set
            self.keychain.delete("videoKey1")
            self.purchasedProductIdentifiers.remove("net.theapplady.Nifty_video1")
        default:
            break
        }
        
        self.tableView.reloadData()
    }
    
    let cancelButton = UIAlertAction(title: "Cancel", style: .Default) {
        UIAlertAction in
        alert.dismissViewControllerAnimated(true, completion: nil)
    }
    
    alert.addAction(cancelButton)
    alert.addAction(yesButton)
    
    // Delay presentation of the alert view
    dispatch_async(dispatch_get_main_queue(), {
        self.presentViewController(alert, animated: true, completion: nil)
    })
}

Above code implemented three user defined functions in the ProductViewController class. A description of each user-defined function follows.

The addKeyToKeychain() Function

This function add the product id passed to it, in the app’s iOS keychain. If the product identifier already exists in the iOS keychain, the keychain object’s set() function overwrite the value of the appropriate dictionary’s key. As you can see, an if statement determines which dictionary’s value will be saved or overwritten in the iOS keychain.

The getKeyFromKeychain() Function

This function get a product id from the iOS keychain and insert it in the purchasedProductIdentifier set. As you can see the function takes a single parameter; which is a purchased product identifier. We use it in the keychain object’s get() function to get an item from the iOS keychain. Since the item the get() function returns is an optional String, we unwrap it and store it in a constant called pid. Within the if statement block we insert the pid constant in the purchasedProductIdentifiers set. Now, if an invalid product identifier is passed to the getKeyFromKeychain() function, nothing is inserted in the purchasedProductIdentifiers set.

The discardKeyFromKeychain() Function

This function permanently delete the product id passed to it, from the iOS keychain. Before the function do that; it display an alert view; which ask the user if she really want to delete the provided product identifier from the iOS keychain. If the user tap the alert view’s Yes, I’m Sure button; the discardKeyFromKeychain() function go ahead and permanently discard the provided key from the app’s iOS keychain via the keychain object’s delete() function. The Cancel button simply dismiss the alert button.

Test The User Defined Functions

It is time to test the user defined functions you implemented in the ProductViewController class. Start by entering highlighted statements in the viewDidLoad function.

override func viewDidLoad() {
    super.viewDidLoad()
    
    let path = NSBundle.mainBundle().pathForResource("product_ids", ofType: "plist")
    let result = NSArray(contentsOfFile: path!)
    
    if let object = result {
        for row in object {
            productIdentifiers.insert(row as! String)
        }
    }
    
    requestProductData()
    SKPaymentQueue.defaultQueue().addTransactionObserver(self)
    
    addKeyToKeychain("net.theapplady.Nifty_Video3")
    if keychain.lastResultCode == noErr {
        print("\nEntry saved in the application's iOS keychain")
    }
    
    addKeyToKeychain("net.theapplady.Nifty_Video4")
    if keychain.lastResultCode == noErr {
        print("Entry saved in the application's iOS keychain")
    }
}

Now, run the app on your iOS iPhone device and you will see the Apple Sign In to iTunes Store alert view on your device’s screen.

niftyiap-figure7-3

Figure 6-1

In addition to seeing above alert view on your device’s screen; in Xcode’s console, you will see these messages. The second and third one is produced by the print statements you entered in the viewDidLoad function.

Product request was sent to the App Store.

Entry saved in the application's iOS keychain
Entry saved in the application's iOS keychain
There are 4 elements in the productsArray.

Use your sandbox password to sign in to iTunes Store or tap the Cancel button.

To test code you entered in the getKeyFromKeychain() function, terminate the app in Xcode and enter these statements below existing code that’s in the viewDidLoad function.

getKeyFromKeychain("videoKey3", productIdentifier:"net.theapplady.Nifty_Video3")
getKeyFromKeychain("videoKey4", productIdentifier:"net.theapplady.Nifty_Video4")

When you run the app again on your iPhone device, you will see Apple’s Sign In to iTunes Store alert view again. When you dismiss it, the ProductView will look like this:

Figure 9-4

Figure 6-2

In addition to output shown on your iPhone device’s screen, you will see messages shown below, in Xcode’s console. The fourth and fifth message was produced by the print statement you entered in the getKeyFromKeychain() function.

Product request was sent to the App Store.

Entry saved in the application's iOS keychain
Entry saved in the application's iOS keychain
An entry was added in the purchasedProductIdentifiers set
An entry was added in the purchasedProductIdentifiers set
There are 4 elements in the productsArray.

To test code in the discardKeyFromKeychain() function, terminate the app in Xcode, and modify the viewDidLoad function by entering highlighted statements shown below. As you can see, the last statement call the discardKeyFromKeychain() function which discard an item from the app’s iOS keychain and the purchasedProductIdentifier set.

override func viewDidLoad() {
    super.viewDidLoad()
    
    let path = NSBundle.mainBundle().pathForResource("product_ids", ofType: "plist")
    let result = NSArray(contentsOfFile: path!)
    
    if let object = result {
        for row in object {
            productIdentifiers.insert(row as! String)
        }
    }
    
    requestProductData()
    SKPaymentQueue.defaultQueue().addTransactionObserver(self)
    
    getKeyFromKeychain("videoKey3", productIdentifier:"net.theapplady.Nifty_Video3")
    getKeyFromKeychain("videoKey4", productIdentifier:"net.theapplady.Nifty_Video4")
    discardKeyFromKeychain("videoKey3", productName: "Shake It Off")
}

Run the app on your iPhone device and dismiss Sign In to iTunes Store alert view. You will see this alert view.

niftyiap-figure9-1

Figure 6-3

Above alert view was produced by code you entered in the discardKeyFromKeychain() function. Notice behind the alert view that a checkmark is shown in the second and third cell’s accessoryView. That’s because you didn’t comment out the getKeyFromKeyChain() function that was called twice in the viewDidLoad function.

Now, tap above alert view’s Yes, I’m sure button. The alert view is dismissed, the tableView cells are refreshed, and the Nifty app store look like Figure 6-4 now. As you can see, the Buy Now button appear again in the second cell’s accessoryView. That’s because code you entered in the discardKeyFromKeychain() function removed an item from the iOS keychain and the purchasedProductIdentifiers set, for that cell’s product.

niftyiap-figure7-5a

Figure 6-4

Alrighty then; terminate the app in Xcode and comment out the last statement in the viewDidLoad() function.

//discardKeyFromKeychain("videoKey3", productName: "Shake It Off")

Now, run the app on your iPhone device one last time. The alert view shown in Figure 6-3 above, won’t appear on your iPhone device; however, the Nifty app store still look like the one shown in Figure 6-4 above.

The keychain object’s clear() function enables you to remove all items from the Nifty-IAP application iOS keychain. Please use it with caution. When you use the clear function; don’t forget to call the removeAll function on the purchasedProductIdentifiers set. With that said; modify the viewDidLoad() function so it look like this:

override func viewDidLoad() {
    super.viewDidLoad()
    
    let path = NSBundle.mainBundle().pathForResource("product_ids", ofType: "plist")
    let result = NSArray(contentsOfFile: path!)
    
    if let object = result {
        for row in object {
            productIdentifiers.insert(row as! String)
        }
    }
    
    requestProductData()
    SKPaymentQueue.defaultQueue().addTransactionObserver(self)
   
    keychain.clear()
    purchasedProductIdentifiers.removeAll()
    
    getKeyFromKeychain("videoKey3", productIdentifier:"net.theapplady.Nifty_Video3")
    getKeyFromKeychain("videoKey4", productIdentifier:"net.theapplady.Nifty_Video4")
    
    if purchasedProductIdentifiers.contains("net.theapplady.Nifty_Video3") {
        print("\nShake It Off is in the application's iOS keychain")
    }
    
    if purchasedProductIdentifiers.contains("net.theapplady.Nifty_Video4") {
        print("Skyfall is in the application's iOS keychain\n")
    }
    print("Total entries in the purchasedProductIdentifiers set is \(purchasedProductIdentifiers.count)")
}

Now, the statements on line 16 and 17 above remove all items from the app’s iOS keychain and the purchasedProductIdentifiers. Code in the last two if statements only run if the specified product identifiers exists in the purchasedProductIdentifiers set. Since they don’t; the Nifty app store will look like this:

niftyiap-figure6-7

Figure 6-5

You will see this output in Xcode’s console:

Product request was sent to the App Store.
Total entries in the purchasedProductIdentifiers set is, 0
There are 4 elements in the productsArray

That’s it you are done testing the iOS keychain code in the ProductViewController class’ viewDidLoad() function.

Let us assume products that are in the Nifty-IAP app bundle’s plist file are Non-Renewable Subscriptions. Let us also assume you already purchased the last three products and restored them by tapping the Restore button causing the Nifty app store to look like this:

niftyiap-figure6-9

Before proceeding I want you to terminate the app.

Now, to automatically restore the last three products you’ve purchase in the Nifty app store, you’ll have to modify the viewDidLoad function so it look like this:

override func viewDidLoad() {
    super.viewDidLoad()
    
    let path = NSBundle.mainBundle().pathForResource("product_ids", ofType: "plist")
    let result = NSArray(contentsOfFile: path!)
        
    if let object = result {
      for row in object {
        productIdentifiers.insert(row as! String)
      }
    }
    
    keychain.clear()
    purchasedProductIdentifiers.removeAll()
    requestProductData()
    SKPaymentQueue.defaultQueue().addTransactionObserver(self)
    
    addKeyToKeychain("net.theapplady.Nifty_video1")
    addKeyToKeychain("net.theapplady.Nifty_Video3")
    addKeyToKeychain("net.theapplady.Nifty_Video4")

    getKeyFromKeychain("videoKey1", productIdentifier:"net.theapplady.Nifty_video1")
    getKeyFromKeychain("videoKey3", productIdentifier:"net.theapplady.Nifty_Video3")
    getKeyFromKeychain("videoKey4", productIdentifier:"net.theapplady.Nifty_Video4")
    
    /*** DEBUG CODE ***/
    print("Total entries in the iOS keychain is \(purchasedProductIdentifiers.count)")
}

Run the application on your iPhone and the Nifty app store will look like this:

niftyiap-figure6-9

Terminate the app in Xcode and locate the updatedTransactions() function. Next, enter the highlighted statement in it.

func paymentQueue(queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
  ...
  case SKPaymentTransactionState.Purchased:
     print("Transaction Approved")
     print("The product identifier of the approved product is \(transaction.payment.productIdentifier)")
    
     addKeyToKeychain(transaction.payment.productIdentifier)
     deliverProduct(transaction)
     SKPaymentQueue.defaultQueue().finishTransaction(transaction)
  ...
}

Next, modify the viewDidLoad function to look like this:

override func viewDidLoad() {
    super.viewDidLoad()
    
    let path = NSBundle.mainBundle().pathForResource("product_ids", ofType: "plist")
    let result = NSArray(contentsOfFile: path!)
    
    if let object = result {
        for row in object {
            productIdentifiers.insert(row as! String)
        }
    }
    
    //keychain.clear()
    //purchasedProductIdentifiers.removeAll()
    
    requestProductData()
    SKPaymentQueue.defaultQueue().addTransactionObserver(self)
    
    getKeyFromKeychain("videoKey2", productIdentifier:"net.theapplady.Nifty_Video2")
}

Now, run the application on your iPhone device and purchase the first product. As expected, the app update the first tableView’s accessoryView by replacing the Buy Now button with a checkmark. Go ahead and watch the Crazy video. When you are done, return to the Nifty app store and terminate the app in Xcode. The next time you run the app on your iPhone device, you will see a checkmark only  in the first cell’s accessoryView. Use Xcode’s Source Control/Commit to save changes you made to the project in the Git repository.

sign-post-thendCongratulations! You reached the end of the In-App Purchase How To tutorial series. In it you learned how to perform tasks shown on these cards:

tasks-card1

Card 1

tasks-card2

Card 2

tasks-card3

Card 3

Card 4

Card 4