Interface with StoreKit Using the Store Manager

In Add In-App Purchase Support to the Project, you added a group of files. One of those files, StoreManager.swift, is responsible for interfacing with StoreKit.

StoreKit, or rather the StoreKit framework, is what handles all of the in-app purchases and interactions your game has with the App Store. This includes loading the product details, prompting the player for payment details, and validating receipts (something you won’t do in this book).

When working with StoreKit and in-app purchases, you’re mostly dealing with products, payments, requests, and transactions using classes like:

  • SKProduct
  • SKRequest
  • SKPayment
  • SKPaymentTransaction

These four classes (some of which you’ve already been using but may not have even realized) are just some of what’s included with StoreKit—and what you can use to support in-app purchases within your games.

In fact, StoreKit offers more than just in-app purchase support. You can interact with Apple Music and also provide recommendations for third-party content and reviews for your games. But StoreKit is a big topic, and there’s not much room left in this book to cover everything, so you’ll need to keep your focus on using the helper files included with this book (while I’ll explain just the important bits about StoreKit).

Using the StoreKit Delegate Methods

Open the StoreManager.swift file and have a look around. The first thing you’ll see are some custom notifications.

With in-app purchases, and specifically these helper files, you’ll track the following six actions:

  • Successful purchases
  • Purchasing failures
  • Successful restorations
  • Restoration failures
  • Product requests

The ShopScene extension (ShopScene.swift) is observing these notifications. If you look at the ShopScene.setupObservers() method, you’ll see that each notification calls a corresponding selector method:

  • purchaseSuccess(_:)
  • purchaseFailure(_:)
  • restoredSuccess(_:)
  • restoredComplete(_:)
  • restoredFailure(_:)
  • requestComplete(_:)

To give you an idea of what these methods do, here’s a look at the purchaseSuccess(_:) method:

 @objc​ ​func​ ​purchaseSuccess​(_ notification: ​Notification​) {
 updateMessageText​(with: ​ShopMessages​.success)
 updateUI​()
 }

And here’s a look at the requestComplete(_:) method:

 @objc​ ​func​ ​requestComplete​(_ notification: ​Notification​) {
 setupShop​()
 }

For the most part, these six methods do the same thing: they receive the notification object and act accordingly—whether it’s setting up the shop or showing a message and updating the UI.

So, how do these notifications fit in? You’re about to find out.

Loading Products and Reviewing Class Properties

Before getting too deep into the StoreKit delegate methods and custom notifications, have a look at the properties set up in the StoreManager class:

 var​ availableProducts = [​SKProduct​]()
 var​ invalidProductIdentifiers = [​String​]()
 
 var​ purchasedTransactions = [​SKPaymentTransaction​]()
 var​ restoredTransactions = [​SKPaymentTransaction​]()
 
 private​ ​var​ productsRequest: ​SKProductsRequest​?

You’ll use these properties to interact with the store. Notice the productsRequest property. This property holds the main SKProductsRequest object.

The SKProductsRequest[71] object is what you’ll use to grab a list of available products and their details from the App Store. These are the details stored in the SKProduct[72] object and what you’re using in the ShopScene.setupProduct(_:) method to set up the shop and display the product details.

However, at the moment, your project has very little awareness of the ShopManager class or its objects, requests, and callbacks.

Open the AppDelegate.swift file, and inside the application(_:didFinishLaunchingWithOptions:) method, below the line that reads GameData.shared.loadDataWithFileName("gamedata.json"), add the following code:

 // Attach an observer to the payment queue
 SKPaymentQueue​.​default​().​add​(​StoreManager​.shared)
 
 // Fetch products
 StoreManager​.shared.​fetchProducts​()

This code sets up a payment observer[73] and starts the request responsible for retrieving the product information from the App Store. Without a payment queue, your game would have no way to interact with the App Store for payment processing. And, without products, your players would have no way to purchase things.

Switch back to the StoreManager.swift file and look at the fetchProducts() method:

 func​ ​fetchProducts​() {
  productsRequest?.​cancel​()
  productsRequest = ​SKProductsRequest​(productIdentifiers: productIdentifiers)
  productsRequest?.delegate = ​self
  productsRequest?.​start​()
 }

This method creates a new SKProductsRequest object, passing in the product identifiers, and then starts the request.

Scroll through the rest of the StoreManager.swift file and look at the different delegate methods. (Yes, there are a lot, but each one includes an explanation above it.)

Stop for a moment and look at the productsRequest(_:didReceive:) method:

 func​ ​productsRequest​(_ request: ​SKProductsRequest​,
  didReceive response: ​SKProductsResponse​) {
 print​(​"productsRequest(_:didReceive)"​)
 
 // Populate the `availableProducts` array
 if​ !response.products.isEmpty {
  availableProducts = response.products
  }
 
 // Populate the `invalidProductIdentifiers` array
 if​ !response.invalidProductIdentifiers.isEmpty {
  invalidProductIdentifiers = response.invalidProductIdentifiers
  }
 
 // For testing and verifying
 for​ p ​in​ availableProducts {
 print​(​" - Product (available): ​​(​p.productIdentifier​)​​ "
  + ​"​​(​p.localizedTitle​)​​ ​​(​p.price.floatValue​)​​"​)
  }
 
 for​ p ​in​ invalidProductIdentifiers {
 print​(​" - Product (invalid): ​​(​p​)​​"​)
  }
 
 // Send notification that the products request operation is complete
 NotificationCenter​.​default​.​post​(name: .productsRequestComplete, object: ​nil​)
 }

This method is just one of the delegate methods the shop uses. Notice how this codes reads the SKProductsResponse object and parses out the product information. It then posts a notification, which is observed by the ShopScene class, as you can see here:

 NotificationCenter​.​default​.​addObserver​(​self​,
  selector: ​#selector(​​self.requestComplete​​)​,
  name: .productsRequestComplete,
  object: ​nil​)

This particular observer calls the following method:

 @objc​ ​func​ ​requestComplete​(_ notification: ​Notification​) {
 setupShop​()
 }

You saw this method earlier; it’s the one that calls the method that sets up the shop scene.

You’re ready to test. Build and run the project. Tap the Shop button and you’ll see the Remove Ads and Continue (1x) products show up in the shop:

images/MonetizingYourGamesWithInAppPurchases/iap-build-02.png

Now that you have products showing up in the shop, you’re ready to hook up the code that handles purchases and restorations.

Buying Products and Restoring Purchases

When it comes to buying and restoring in-app purchases, it’s important to know that players can restore only non-consumable products. Restoring consumable products is not (and should not be) supported. For example, players can’t purchase a consumable product on device A, and then restore that consumable product to devices B and C. This is by design.

Two methods in the ShopScene class need updating to handle purchasing and restoration: purchaseSuccess(_:) and restoredSuccess(_:).

Open the ShopScene.swift file and add the following code in both the purchaseSuccess(_:) and restoredSuccess(_:) methods (be sure to place this code above the lines that read updateUI()):

 if​ ​let​ productIdentifier = notification.object ​as?​ ​String​ {
 let​ product = ​StoreProducts​.​Product​(productID: productIdentifier)
 
 if​ ​let​ gp = ​GameData​.shared.products.​first​(where:
  { $0.id == productIdentifier }) {
  gp.quantity += 1
  } ​else​ {
  product.quantity = 1
 GameData​.shared.products.​append​(product)
  }
 }

This code verifies the product ID and then adds the product to the GameData.shared.products array, which indicates ownership of the product. Remember, when the products array changes (such as with an add or update), the game data is saved.

You also need to modify the shop’s UI so that players know when a non-consumable product has been purchased. Locate the updateUI() method and update it to match this:

 func​ ​updateUI​() {
  gameScene?.​updateContinueButton​()
 
 for​ product ​in​ ​GameData​.shared.products {
 if​ ​let​ id =
 StoreManager​.shared.​resourceNameForProductIdentifier​(product.id) {
 let​ productNodeName = ​"shop.product.​​(​id​)​​"
 if​ product.isConsumable == ​false​ {
 let​ ownedNodeName = ​"//​​(​productNodeName​)​​/owned"
 let​ ownedNode = ​childNode​(withName: ownedNodeName)
  ownedNode?.isHidden = ​false
 // print(" ownedNodeName: (ownedNodeName)")
 
 let​ unownedNodeName = ​"//​​(​productNodeName​)​​/unowned"
 let​ unownedNode = ​childNode​(withName: unownedNodeName)
  unownedNode?.isHidden = ​true
 // print(" unownedNodeName: (unownedNodeName)")
  }
  }
  }
 }

This code checks the products’ purchase status and shows or hides certain shop scene elements depending on the product’s current state. However, because players can purchase non-consumable products only once and consumable products as many times as they want, the code behaves differently depending on what type of product it is. For example, once players purchase or restore the “Remove Ads” product, they’ll see a badge showing them that they already own the product, like this:

images/MonetizingYourGamesWithInAppPurchases/iap-product-status.png

Now that you have a way to purchase and restore products, it’s time to test things out on a physical device, which is a necessary step for testing in-app purchases.

Why Can’t I Access the Store?

images/aside-icons/warning.png

If you were eager and tried to buy a product while testing in the simulator, you likely saw the error message: Cannot connect to iTunes Store. The reason you see this error is because you must use a physical device to test in-app purchases.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset