-
Notifications
You must be signed in to change notification settings - Fork 4
User Guide
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.
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.
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)
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.
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.
Fuse.RMStore
return the price as a localized string. The reason for this is that JS only has double
s 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.
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"
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.
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.
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.
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.
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
The transactions are automatically finished after the promise completes so there is no need to worry about this
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
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 SKPaymentTransaction
s. These were described in the Requesting a Payment
section above.
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.
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
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.
No
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.
This does not stop adding and testing purchases. We did all our testing internally with that message showing in our account.
Nope
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?
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
This is a bad experience. What should I do?
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.