In-app purchases in RubyMotion

Building iPhone apps is fun, but you need to at some point reap the rewards of your hard work. That's where in-app purchases (IAPs) come in.

There are a few options out there, such as Helu by my friend Ivan Acosta-Rubio. But Helu hasn't been updated in a while and I ran into a difficult to track down bug that I had to work around. Plus, the API, while simple enough, isn't quite "ProMotion-like". I considered rewriting Helu to work better, but eventually decided it would be better to draw inspiration from it and go a different direction.

That's why Kevin VanGelder and I built a new gem called ProMotion-iap.

More on this later. First, a tiny bit of background.

StoreKit

IAPs are handled by a Cocoa library called StoreKit. StoreKit contains a fairly tight collection of classes and delegate protocols to retrieve, purchase, and restore purchases -- non-consumable, consumable, subscription, and others.

iTunes Connect

Before you get too far down this path, you'll need to set up an in-app purchase in iTunes Connect and connect it to your app. Apple does a pretty good job of walking you through this process, so I won't detail it here, but you'll want to make sure you have a simple "Product ID" to reference from your app.

ProMotion-iap

ProMotion-iap is provides a module (PM::IAP) that exposes several methods to you when you include it. It's well tested and running in a production app.

You can also use a convenience class for single in-app purchase products, PM::IAP::Product. I'll provide my examples using that class, but check out the README for the included module examples too which allow for bulk operations.

Note that, despite its name, ProMotion-iap doesn't require ProMotion to run. Feel free to add it to your Gemfile and include it in any class you want.

Also note that this article is targeting version 0.2. Some things might change in minor ways before 1.0.

Retrieving in-app purchase products

class PurchaseScreen < PM::Screen  
  def on_load

    PM::IAP::Product.new("productid1").retrieve do |product, error|
      # product looks something like the following
      {
        product_id:               "productid1",
        title:                    "title",
        description:              "description",
        price:                    <BigDecimal 0.99>,
        formatted_price:          "$0.99",
        price_locale:             <NSLocale>,
        downloadable:             false,
        download_content_lengths: <?>, # TODO: ?
        download_content_version: <?>, # TODO: ?
        product:                  <SKProduct>
      }
    end

  end
end  

retrieve does an asynchronous call to iTunes to retrieve the product, then map it into a readable hash that you can see in the example above. The SKProduct instance that we get from StoreKit is still available in the [:product] key, so if you need to do something else with it you still can. The formatted_price is done for you using Apple's recommended method.

Purchasing an in-app product

class PurchaseScreen < PM::Screen  
  def on_load

    PM::IAP::Product.new("productid").purchase do |status, transaction|
      case status
      when :in_progress then show_spinner
      when :deferred    then hide_spinner
      when :purchased   then complete_transaction
      when :canceled    then go_away
      when :error
        # Failed to purchase
        transaction.error.localizedDescription # => error message
      end
    end

  end
end  

This one is a little different. The block will be called multiple times for the product with various status updates. So the best thing to do is provide a case statement (it's still legal to use those in Ruby, right?) and then do whatever you need to do for that update.

The methods called after then are your own. So, you might have something like this:

def complete_transaction  
  UIAlertView.alert "Thanks!", "You just bought a smudgeron!"
  User.current.smudgerons += 1
  User.current.save
end  

Restoring IAPs

Apple recently rejected an iPhone app we developed at ClearSight because there wasn't a way to "restore in-app purchases." ProMotion-iap makes this easy to fix:

class PurchaseScreen < PM::Screen  
  def on_load

    PM::IAP::Product.new("productid").restore do |status, product|
      if status == :restored
        # Update your UI, notify the user
      end
    end

  end
end  

For now, status will always be :restored, but we will likely change that in the future to show error conditions, so be aware of it.

You can restore all of the in-app purchases at once by using the included module method, restore_iaps. Check out the README for more.

Next steps

Kevin and I will be fleshing out the rest of the StoreKit API soon, including subscription products. In the meantime, go check it out and file an issue if you run into anything.

UPDATED 2/16/15: Clarified my decision on why I didn't use Helu, since it caused some confusion.