Skip to content

Commit

Permalink
Merge pull request j3k0#418 from seibelj/non_renewable_subscription_docs
Browse files Browse the repository at this point in the history
Non renewable subscription docs
  • Loading branch information
j3k0 committed Mar 7, 2016
2 parents c5273f2 + e06d680 commit 99ff557
Show file tree
Hide file tree
Showing 5 changed files with 101 additions and 4 deletions.
1 change: 1 addition & 0 deletions doc/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ Find below a diagram of the different states a product can pass by.
- When finished, a consumable product will get back to the `VALID` state, while other will enter the `OWNED` state.
- Any error in the purchase process will bring a product back to the `VALID` state.
- During application startup, products may go instantly from `REGISTERED` to `APPROVED` or `OWNED`, for example if they are purchased non-consumables or non-expired subscriptions.
- Non-Renewing Subscriptions are iOS products only. Please see the [iOS Non Renewing Subscriptions documentation](https://github.com/j3k0/cordova-plugin-purchase/blob/master/doc/ios.md#non-renewing) for a detailed explanation.

#### state changes

Expand Down
87 changes: 87 additions & 0 deletions doc/ios.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,90 @@ Demo application project

- Edit [config.xml](https://github.com/dpa99c/cordova-plugin-purchase-demo/blob/master/config.xml) and set the `id` attribute in the `<widget>` element to that of your app Identifier
- Edit [www/index.js](https://github.com/dpa99c/cordova-plugin-purchase-demo/blob/master/www/js/index.js) and set the `id` fields under `store.register` are for your IAP Identifiers.


### <a name="non-renewing"></a>Non-Renewing iOS Subscriptions

iOS has a special product type called Non-Renewing Subscriptions. You use this when you want something subscription based, but don't want Apple to auto-renew and manage your subscriptions. This means that the burden is on you, the developer, to manually implement necessary functionality like syncing between devices, prompting for renewals, etc. Apple will verify this functionality during the AppStore review process and reject your app if it does not implement this.

Anecdotally, non-renewing subscriptions are easier to implement and test than auto-renewing subscriptions, because you don't need to deal with receipt validation or wait hours for test subscriptions to expire. You also have more flexibility on subscription time periods than the limited options of auto-renewing subscriptions.

Although non-renewing subscriptions are officially subscriptions, you can think of them like consumable products, that you can purchase repeatedly during development and testing.

Key things to remember are:

- You must prompt the user to renew when a subscription is about to expire. This isn't required by Apple, this is simply good business sense. Otherwise, users will have a gap between their subscription expiring and when they renew.
- If a user purchases a new subscription before the existing has expired, you must add additional time to their subscription. For instance, if they purchase a year's subscription, then after 10 months they purchase another one, the subscription must now have 14 months remaining. This is required by Apple.
- You must sync between all devices using the same Apple ID. Alternatively, if your app has a custom authentication mechanism that is not tied to an Apple ID, you must sync between all devices that login using your custom authentication. You must provide testing credentials to Apple during the AppStore review process so they can verify this. This is required by Apple.

Please read the [Apple Documentation](https://developer.apple.com/library/ios/documentation/LanguagesUtilities/Conceptual/iTunesConnectInAppPurchase_Guide/Chapters/CreatingInAppPurchaseProducts.html) for official information.

What follows is an example of how to implement non-renewing subscriptions in your JavaScript code. Remember, this is iOS only. When registering your product, use `store.NON_RENEWING_SUBSCRIPTION`.

This is made more difficult because non-renewing subscriptions always receive a series of lifecycle events everytime the app is started, which requires you to implement more code to handle various edge cases. Necessary helper functions you need to write are explained in the example below, called on a hypothetical `my_app_utils` class which contains a persistent state that lasts even if the app is killed, such as with HTML5 Local Storage.

This full body of code, which registers all necessary handlers and refreshes the `store`, must be executed every time the app starts.

```javascript
// Register the non-renewing subscription product with the store. You must
// create this in iTunes Connect.
store.register({
id: "my_product_id",
alias: "My Product",
type: store.NON_RENEWING_SUBSCRIPTION
});

// Called when store.order("my_product_id") is executed. The user can
// still cancel after this has been called.
store.when("my_product_id").initiated(function(p) {
// Write a function that identifies this product ID as having been
// initiated to purchase.
my_app_utils.setIsProductPurchaseInitiated("my_product_id", true);
});

// Called when the user has cancelled purchasing the product, after it has
// been initiated.
store.when("my_product_id").cancelled(function(p) {
// Write a function that marks this product ID as not being purchased
my_app_utils.setIsProductPurchaseInitiated("my_product_id", false);
});

// Purchase has been executed successfully. Must call finish to charge the user
// and put the product into the owned state.
store.when("my_product_id").approved(function(p) {
p.finish();
});

// Called when the product purchase is finished. This gets called every time
// the app starts after the product has been purchased, so we use a helper
// function to determine if we actually need to purchase the non-renewing
// subscription on our own server.
store.when("my_product_id").owned(function(p) {

if (my_app_utils.getIsProductPurchaseInitiated("my_product_id")) {
// Prevent another upgrade from happening
my_app_utils.setIsProductPurchaseInitiated("my_product_id", false);
// All necessary logic to purchase the product, such as talking
// to your server, changing the UI, etc.
my_app_utils.purchaseNonRenewingSubscription('my_product_id');
}
else {
console.log("my_product_id purchase NOT initiated, NOT upgrading!");
}
});

// Errors communicating with the iTunes server happen quite often,
// so it's highly recommended you implement some feedback to the user.
store.error(function(e){
console.log("storekit ERROR " + e.code + ": " + e.message);
my_app_utils.alertUserAboutITunesError({
title: 'Subscription Purchase Error',
template: 'We could not reach the Apple iTunes ordering server. ' +
'Please ensure you are connected to the Internet and try ' +
'again.'
});
});

// Refresh the store to start everything
store.refresh();
```
8 changes: 6 additions & 2 deletions src/js/platforms/ios-adapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@ store.when("finished", function(product) {
});

function storekitFinish(product) {
if (product.type === store.CONSUMABLE) {
if (product.transaction && product.transaction.id)
if (product.type === store.CONSUMABLE || product.type === store.NON_RENEWING_SUBSCRIPTION) {
if (product.transaction && product.transaction.id) {
storekit.finish(product.transaction.id);
}
else {
store.log.debug("ios -> error: unable to find transaction for " + product.id);
}
}
else if (product.transactions) {
store.log.debug("ios -> finishing all " + product.transactions.length + " transactions for " + product.id);
Expand Down
1 change: 1 addition & 0 deletions src/js/product.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,7 @@ store.Product.prototype.verify = function() {
/// - When finished, a consumable product will get back to the `VALID` state, while other will enter the `OWNED` state.
/// - Any error in the purchase process will bring a product back to the `VALID` state.
/// - During application startup, products may go instantly from `REGISTERED` to `APPROVED` or `OWNED`, for example if they are purchased non-consumables or non-expired subscriptions.
/// - Non-Renewing Subscriptions are iOS products only. Please see the [iOS Non Renewing Subscriptions documentation](https://github.com/j3k0/cordova-plugin-purchase/blob/master/doc/ios.md#non-renewing) for a detailed explanation.
///
/// #### state changes
///
Expand Down
8 changes: 6 additions & 2 deletions www/store-ios.js
Original file line number Diff line number Diff line change
Expand Up @@ -1167,8 +1167,12 @@ store.sandbox = false;
if (product.type === store.CONSUMABLE) product.set("state", store.VALID); else product.set("state", store.OWNED);
});
function storekitFinish(product) {
if (product.type === store.CONSUMABLE) {
if (product.transaction && product.transaction.id) storekit.finish(product.transaction.id);
if (product.type === store.CONSUMABLE || product.type === store.NON_RENEWING_SUBSCRIPTION) {
if (product.transaction && product.transaction.id) {
storekit.finish(product.transaction.id);
} else {
store.log.debug("ios -> error: unable to find transaction for " + product.id);
}
} else if (product.transactions) {
store.log.debug("ios -> finishing all " + product.transactions.length + " transactions for " + product.id);
for (var i = 0; i < product.transactions.length; ++i) {
Expand Down

0 comments on commit 99ff557

Please sign in to comment.