Skip to content

User Guide

Baggers edited this page Jul 11, 2017 · 4 revisions

Fuse.RMStore is a Fuse wrapper around the excellent RMStore library. Apple's in-app purchase API isn't the prettiest thing to read or work with so RMStore cleans it up a little.

There are still plenty of things in the process of selling through Apple's in-app purchase (From now on referred to as IAP) system that we can't abstract or fix, so if in doubt, the official docs are the place to go.

Including Fuse.RMStore

Fuse.RMStore is a very normal Fuse package and using it in your project is simple. You will need to reference the project in your unoproj:

"Projects": [
    "../src/Fuse.RMStore.unoproj",
],

And in your JavaScript you need to require it:

var RMStore = require("RMStore");

Fuse.RMStore pulls down RMStore using CocoaPods. To get CocoaPods you can simply run:

sudo gem install cocoapods

This works using the default ruby that is install on every Mac, you don't need anything else. See here for more details.

Building

When you build your project make sure you build with the -DCOCOAPODS flag.

Your first build may take a little longer than usual as the dependencies are pulled down, but it will be cached for future builds.

Note We aren't kidding about that first build, it took nearly 15 minutes on my machine.. Crazy, and unfortunately, not something we can fix.

When XCode opens go to the 'App Capabilities' section turn on 'In App Purchases' (this will be done automatically in future)

Designing Your App’s Products

This section from the docs remains unchanged when using IAP from Fuse. Fuse.RMStore only makes IAP available from JS and does not seek to dictate what you sell or how you present it. Read the chapter to get an overview of what is allowed and how Apple want you to present things.

Retrieving Product Information

Official docs

The first section of this chapter of the official docs is useful. Read up to the Embedding Product IDs in the App Bundle section.

The TLDR is that you will have given your product's unique names when you set them up on iTuneConnect. For various reasons not all may be available to this user (region specific purchases for example).

The first thing you will want to do is check if the user is allowed to buy anything (parental restrictions on the phone may disallow this).

To find out simply check the value of RMStore.canMakePayments.

So next you need to query what the user can buy. To do this use RMStore.requestProducts

RMStore.requestProducts(["testsubscription0"]).then(function(result) {
    console.log("Product request succeeded:");
    console.log(JSON.stringify(result));
}).catch(function(e) {
    console.log("Product request failed");
});

requestProducts takes a single array of strings. Those strings must be products IDs as defined on iTunesConnect. The result as a promise that resolved to a RequestProductResult which is defined as:

An object with the following fields:

- valid: an array of SKProductInformation. These are the successfully validated products
- invalid: an array of strings. These are the ids of the invalid products

SKProductInformation is defined as:

An object with the following fields

- price: string
- priceLocale: string
- localizedDescription: string
- productIdentifier: string
- localizedTitle: string

So it's nice an easy to get hold of the product details, and to see what is and isn't available.

A quick note on 'price'

Fuse.RMStore return the price as a localized string. The reason for this is that JS only has doubles and we don't want floating point inaccuracy in prices. We have chose an exact string instead of a inexact numeric. We use the recommended method described in the official docs for formatting the product's price.

Requesting a Payment

This is nice and simple, you just call addPayment with the productIdentifier you validated using requestProducts

RMStore.addPayment("testsubscription0").then(function(result) {
    console.log("Product payment succeeded:");
    console.log(JSON.stringify(result));
}).catch(function(e) {
    console.log("Product request failed");
});

As before, this returns a promise. The promise resolves to a SKPaymentTransactionInfo object.

SKPaymentTransactionInfo is defined as

An object with the following fields

- payment: A SKPaymentInfo object
- transactionState: a TransactionState string
- transactionIdentifier: string
- transactionDateUTC: string UTC time in ISO-8601 format
- error: null or a string

SKPaymentInfo is defined as

And object with the following fields

- productIdentifier: string
- quantity: int
- applicationUsername: string

The TransactionState string is one of the following

- "Purchasing"
- "Purchased"
- "Failed"
- "Restored"
- "Deferred"

Detecting Irregular Activity

Apple's official documentation on this is good but the TLDR is this: By providing some hashed version of your internal user-id Apple can more often detect some kinds of fraudulent behaviors.

To provide this from JS, Fuse.RMStore's requestProducts & restoreTransactions functions both take an optional string argument. That string is used as the applicationUsername as described in Apple's docs.

Delivering Products

RMStore really pays off here as it does a lot of the mandatory work behind the scenes, so we just need to focus on the important stuff.

Waiting for the App Store to Process Transactions

Your don't have to think about the 'transaction queue' as it is handled internally. When transactions are complete we fire the then of your promises.

Persisting the Purchase

This is another important section in the official docs.

For the most part you are going to be using the 'receipt' or your own server.

The 'receipt' is automatically managed by RMStore and validated using OpenSSL. For details on querying the contents of the receipt please see the Restoring Purchased Products section below.

If you want to validate this yourself you should get the receipt as base64 using the getEncryptedReceiptAsBase64 function. You should then pass this (securely) to your server so that your server can validate the receipt with the AppStore. More details can be found here.

If you want to talk to iCloud or use iOS' 'User Defaults' system you will need to use foreign code via uno.

Delivering Associated Content

The content related to a purchase must be either:

  • Included in the app already (e.g. unlocking a difficulty level of a game)
  • Downloaded from your server

How this is done is left up to you. Please see the Fuse docs to learn how to do HTTP requests

Finishing the Transaction

The transactions are automatically finished after the promise completes so there is no need to worry about this

Working with Subscriptions

Apple has a approach to this that requires quite a bit of work from you. Please read the official docs to see what you need to do

Restoring Purchased Products

As with the previous section, the official docs give the most accurate overview on what they expect you to provide.

The two things they explain in that chapter are:

  • Refreshing the App Receipt
  • Restoring Completed Transactions

To refresh the app receipt call the following:

RMStore.refreshReceipt().then(function() {
    console.log("refreshing receipt succeeded");
}).catch(function(e) {
    console.log("refreshing receipt failed");
});

To restore completed transactions call:

RMStore.restoreTransactions().then(function(result) {
    console.log("restoring transactions succeeded:");
    console.log(JSON.stringify(result));
}).catch(function(e) {
    console.log("restoring transactions failed");
});

The promise resolves to a list of SKPaymentTransactions. These were described in the Requesting a Payment section above.

Purchases in your Receipt

One very important additions is that, in Fuse.RMStore you can call getReceiptTransactions at any point to a list of ReceiptTransactionInfo.

ReceiptTransactionInfo is defined as the following

An object with the following fields

- quantity: int
- productIdentifier: string
- transactionIdentifier: string
- originalTransactionIdentifier: `string
- purchaseDate: string UTC time in ISO-8601 format
- originalPurchaseDate: string UTC time in ISO-8601 format
- subscriptionExpirationDate: string UTC time in ISO-8601 format
- cancellationDate: string UTC time in ISO-8601 format
- webOrderLineItemID: int

Each ReceiptTransactionInfo describes a purchase in your receipt.

Preparing for App Review

We have nothing to add here other than reiterate a couple of things.

  • Make sure you turn on 'In App Purchases' in the 'App Capabilities' section of XCode
  • Make sure you sign your app when you are submitting the final version of your app

Q/A

Q: I cannot add Products in my iTunesConnect account. What is happening?

A:

If you have issues adding products in iTunesConnect then double check everything in 'Agreements Tax and banking'.

Apple are extremely strict on this stuff and yet they give very few decent warnings that something is incorrect.

The only issues we have seen with adding products has been related to legal documents.

Q: Do I need to have my app reviewed before I can add Purchases for testing?

A:

No

Q: What about this line at the top of my iTuneConnect account?

Once your binary has been updloaded and your first In-App Purchase has been submitted for review, additiional In-App Purchases can be submitted using the table below.

A:

This does not stop adding and testing purchases. We did all our testing internally with that message showing in our account.

Q: Does my app need to be signed for release for testing purchases?

A:

Nope

Q: When I start my app I see a bunch of purchases like this:

2016-09-15 16:05:15.969 Opportunity[942:382537] RMStore: transaction purchased with product someproduct
2016-09-15 16:05:16.030 Opportunity[942:382537] RMStore: transaction purchased with product someproduct
2016-09-15 16:05:16.044 Opportunity[942:382537] RMStore: transaction purchased with product someproduct
2016-09-15 16:05:16.057 Opportunity[942:382537] RMStore: transaction purchased with product someproduct
2016-09-15 16:05:16.072 Opportunity[942:382537] RMStore: transaction purchased with product someproduct

What happened?

A:

This most likely means that you have a subscription that has renewed. The reason it happened many times was because time goes faster for test accounts on iTunesConnect. See here for details

Q: Every time I refresh the receipt or restore purchases the user has to enter their credentials.

This is a bad experience. What should I do?

A:

Agreed, this is a poor experience. Apple explains when those methods should be used in their docs but they don't explain what to do in the other cases.

There a two ways to find out what has been purchased without bothering the user:

  • Query your server
  • Look at the receipt

Which one to use really depends on your choices regarding your backend.

You may want to just examine the receipt to see that a certain product has been bought.

You may want to use getEncryptedReceiptAsBase64 and send the receipt to your backend. This lets you validate the receipt with the AppStore and have server-side logic decide what your user has access to.

You are pretty much free to implement it how you like, so long as what you provide the correct experience as dictated in the Apple docs.