cc.fovea.cordova.purchase
Version:
Cordova Purchase plugin for iOS, Android, Windows (AppStore, Play, UWP)
1,309 lines (1,169 loc) • 136 kB
JavaScript
//
// Cordova Purchase Plugin
//
// Author: Jean-Christophe Hoelt
// Copyright (c)2014
//
// License: MIT
//
//
// !!! WARNING !!!
// This file is auto-generated from files located in `src/js`
//
// DO NOT EDIT DIRECTLY OR YOUR CHANGES WILL BE LOST
//
/// ### Philosophy
///
/// The `store` API is mostly events based. As a user of this plugin,
/// you will have to register listeners to changes happening to the products
/// you register.
///
/// The core of the listening mechanism is the [`when()`](#when) method. It allows you to
/// be notified of changes to one or a set of products using a [`query`](#queries) mechanism:
/// ```js
/// store.when("product").updated(refreshScreen);
/// store.when("full version").owned(unlockApp);
/// store.when("subscription").approved(serverCheck);
/// store.when("downloadable content").downloaded(showContent);
/// etc.
/// ```
///
/// The `updated` event is fired whenever one of the fields of a product is
/// changed (its `owned` status for instance).
///
/// This event provides a generic way to track the statuses of your purchases,
/// to unlock features when needed and to refresh your views accordingly.
///
/// ### Registering products
///
/// The store needs to know the type and identifiers of your products before you
/// can use them in your code.
///
/// Use [`store.register()`](#register) before your first call to
/// [`store.refresh()`](#refresh).
///
/// Once registered, you can use [`store.get()`](#get) to retrieve
/// the [`product object`](#product) from the store.
///
/// ```js
/// store.register({
/// id: "cc.fovea.purchase.consumable1",
/// alias: "100 coins",
/// type: store.CONSUMABLE
/// });
/// ...
/// var p = store.get("100 coins");
/// // or
/// var p = store.get("cc.fovea.purchase.consumable1");
/// ```
///
/// The product `id` and `type` have to match products defined in your
/// Apple and Google developer consoles.
///
/// Learn how to do that in [HOWTO: Create New Products](https://github.com/j3k0/cordova-plugin-purchase/wiki/HOWTO#create-new-products).
///
/// ### Displaying products
///
/// Right after you registered your products, nothing much is known about them
/// except their `id`, `type` and an optional `alias`.
///
/// When you perform the initial [`refresh()`](#refresh) call, the store's server will
/// be contacted to load informations about the registered products: human
/// readable `title` and `description`, `price`, etc.
///
/// This isn't an optional step as some despotic store owners (like Apple) require you
/// to display information about a product as retrieved from their server: no
/// hard-coding of price and title allowed! This is also convenient for you
/// as you can change the price of your items knowing that it'll be reflected instantly
/// on your clients' devices.
///
/// However, the information may not be available when the first view that needs
/// them appears on screen. For you, the best option is to have your view monitor
/// changes made to the product.
///
/// #### monitor changes
///
/// Let's demonstrate this with an example:
///
/// ```js
/// // method called when the screen showing your purchase is made visible
/// function show() {
/// render();
/// store.when("cc.fovea.test1").updated(render);
/// }
///
/// function render() {
///
/// // Get the product from the pool.
/// var product = store.get("cc.fovea.test1");
///
/// if (!product) {
/// $el.html("");
/// }
/// else if (product.state === store.REGISTERED) {
/// $el.html("<div class=\"loading\" />");
/// }
/// else if (product.state === store.INVALID) {
/// $el.html("");
/// }
/// else {
/// // Good! Product loaded and valid.
/// $el.html(
/// "<div class=\"title\">" + product.title + "</div>"
/// + "<div class=\"description\">" + product.description + "</div>"
/// + "<div class=\"price\">" + product.price + "</div>"
/// );
///
/// // Is this product owned? Give him a special class.
/// if (product.owned)
/// $el.addClass("owned");
/// else
/// $el.removeClass("owned");
///
/// // Is an order for this product in progress? Can't be ordered right now?
/// if (product.canPurchase)
/// $el.addClass("can-purchase");
/// else
/// $el.removeClass("can-purchase");
/// }
/// }
///
/// // method called when the view is hidden
/// function hide() {
/// // stop monitoring the product
/// store.off(render);
/// }
/// ```
///
/// In this example, `render` redraw the purchase element whatever
/// happens to the product. When the view is hidden, we stop listening to changes
/// (`store.off(render)`).
///
/// ### <a name="purchasing"></a> Purchasing
///
/// #### initiate a purchase
///
/// Purchases are initiated using the [`store.order()`](#order) method.
///
/// The store will manage the internal purchase flow that'll end:
///
/// - with an `approved` [event](#events). The product enters the `APPROVED` state.
/// - with a `cancelled` [event](#events). The product gets back to the `VALID` state.
/// - with an `error` [event](#events). The product gets back to the `VALID` state.
///
/// See [product life-cycle](#life-cycle) for details about product states.
///
/// #### finish a purchase
///
/// Once the transaction is approved, the product still isn't owned: the store needs
/// confirmation that the purchase was delivered before closing the transaction.
///
/// To confirm delivery, you'll use the [`product.finish()`](#finish) method.
///
/// #### example usage
///
/// During initialization:
/// ```js
/// store.when("extra chapter").approved(function(product) {
/// // download the feature
/// app.downloadExtraChapter().then(function() {
/// product.finish();
/// });
/// });
/// ```
///
/// When the purchase button is clicked:
/// ```js
/// store.order("full version");
/// ```
///
/// #### un-finished purchases
///
/// If your app wasn't able to deliver the content, `product.finish()` won't be called.
///
/// Don't worry: the `approved` event will be re-triggered the next time you
/// call [`store.refresh()`](#refresh), which can very well be the next time
/// the application starts. Pending transactions are persistant.
///
/// #### simple case
///
/// In the most simple case, where:
///
/// - delivery of purchases is only local ;
/// - you don't want to implement receipt validation ;
///
/// you may just want to finish all purchases automatically. You can do it this way:
/// ```js
/// store.when("product").approved(function(p) {
/// p.finish();
/// });
/// ```
///
/// NOTE: the "product" query will match any purchases (see [here](#queries) to learn more details about queries).
///
/// ### Receipt validation
///
/// Some unthoughtful users will try to use fake "purchases" to access features
/// they should normally pay for. If that's a concern, you should implement
/// receipt validation, ideally server side validation.
///
/// When a purchase has been approved by the store, it's enriched with
/// [transaction](#transactions) information (`product.transaction` attribute).
///
/// To verfify a purchase you'll have to do three things:
///
/// - configure the [validator](#validator).
/// - call [`product.verify()`](#verify) from the `approved` event,
/// before finishing the transaction.
/// - finish the transaction when transaction is `verified`.
///
/// #### example using a validation URL
///
/// ```js
/// store.validator = "http://192.168.0.7:1980/check-purchase";
///
/// store.when("my stuff").approved(function(product) {
/// product.verify();
/// });
///
/// store.when("my stuff").verified(function(product) {
/// product.finish();
/// });
/// ```
///
/// For an example using a validation callback instead, see the documentation of [the validator method](#validator).
///
/// ### Subscriptions
///
/// For subscription, you MUST implement remote [receipt validation](#receipt-validation).
///
/// If the validator returns a `store.PURCHASE_EXPIRED` error code, the subscription will
/// automatically loose its `owned` status.
///
/// Typically, you'll enable and disable access to your content this way.
/// ```js
/// store.when("cc.fovea.subcription").updated(function(product) {
/// if (product.owned)
/// app.subscriberMode();
/// else
/// app.guestMode();
/// });
/// ```
// ### Security
//
// You will initiate a purchase with `store.order("product.id")`.
//
// 99% of the times, the purchase will be approved immediately by billing system.
//
// However, connection can be lost between you sending a purchase request
// and the server answering to you. In that case, the purchase shouldn't
// be lost (because the user paid for it), that's why the store will notify
// you of an approved purchase during the next application startup.
//
// The same can also happen if the user bought a product from another device, using his
// same account.
//
// For that reason, you should register all your features-unlocking listeners at
// startup, before the first call to `store.refresh()`
//
///
/// # <a name="store"></a>*store* object ##
///
/// `store` is the global object exported by the purchase plugin.
///
/// As with any other plugin, this object shouldn't be used before
/// the "deviceready" event is fired. Check cordova's documentation
/// for more details if needed.
///
/// Find below all public attributes and methods you can use.
///
var store = {};
/// ## <a name="verbosity"></a>*store.verbosity*
///
/// The `verbosity` property defines how much you want `store.js` to write on the console. Set to:
///
/// - `store.QUIET` or `0` to disable all logging (default)
/// - `store.ERROR` or `1` to show only error messages
/// - `store.WARNING` or `2` to show warnings and errors
/// - `store.INFO` or `3` to also show information messages
/// - `store.DEBUG` or `4` to enable internal debugging messages.
///
/// See the [logging levels](#logging-levels) constants.
store.verbosity = 0;
/// ## <a name="sandbox"></a>*store.sandbox*
///
/// The `sandbox` property defines if you want to invoke the platform purchase sandbox
///
/// - Windows will use the IAP simulator if true (see Windows docs)
/// - Android: NOT IN USE
/// - iOS: NOT IN USE
store.sandbox = false;
(function(){
///
/// ## Constants
///
///
/// ### product types
///
/*///*/ store.FREE_SUBSCRIPTION = "free subscription";
/*///*/ store.PAID_SUBSCRIPTION = "paid subscription";
/*///*/ store.NON_RENEWING_SUBSCRIPTION = "non renewing subscription";
/*///*/ store.CONSUMABLE = "consumable";
/*///*/ store.NON_CONSUMABLE = "non consumable";
///
/// ### error codes
///
// KEEP SYNCHRONIZED with git_modules/android_iap/v3/src/android/com/smartmobilesoftware/util/IabHelper.java
// KEEP SYNCHRONIZED with src/ios/InAppPurchase.m
var ERROR_CODES_BASE = 6777000;
/*///*/ store.ERR_SETUP = ERROR_CODES_BASE + 1; //
/*///*/ store.ERR_LOAD = ERROR_CODES_BASE + 2; //
/*///*/ store.ERR_PURCHASE = ERROR_CODES_BASE + 3; //
/*///*/ store.ERR_LOAD_RECEIPTS = ERROR_CODES_BASE + 4;
/*///*/ store.ERR_CLIENT_INVALID = ERROR_CODES_BASE + 5;
/*///*/ store.ERR_PAYMENT_CANCELLED = ERROR_CODES_BASE + 6; // Purchase has been cancelled by user.
/*///*/ store.ERR_PAYMENT_INVALID = ERROR_CODES_BASE + 7; // Something suspicious about a purchase.
/*///*/ store.ERR_PAYMENT_NOT_ALLOWED = ERROR_CODES_BASE + 8;
/*///*/ store.ERR_UNKNOWN = ERROR_CODES_BASE + 10; //
/*///*/ store.ERR_REFRESH_RECEIPTS = ERROR_CODES_BASE + 11;
/*///*/ store.ERR_INVALID_PRODUCT_ID = ERROR_CODES_BASE + 12; //
/*///*/ store.ERR_FINISH = ERROR_CODES_BASE + 13;
/*///*/ store.ERR_COMMUNICATION = ERROR_CODES_BASE + 14; // Error while communicating with the server.
/*///*/ store.ERR_SUBSCRIPTIONS_NOT_AVAILABLE = ERROR_CODES_BASE + 15; // Subscriptions are not available.
/*///*/ store.ERR_MISSING_TOKEN = ERROR_CODES_BASE + 16; // Purchase information is missing token.
/*///*/ store.ERR_VERIFICATION_FAILED = ERROR_CODES_BASE + 17; // Verification of store data failed.
/*///*/ store.ERR_BAD_RESPONSE = ERROR_CODES_BASE + 18; // Verification of store data failed.
/*///*/ store.ERR_REFRESH = ERROR_CODES_BASE + 19; // Failed to refresh the store.
/*///*/ store.ERR_PAYMENT_EXPIRED = ERROR_CODES_BASE + 20;
/*///*/ store.ERR_DOWNLOAD = ERROR_CODES_BASE + 21;
/*///*/ store.ERR_SUBSCRIPTION_UPDATE_NOT_AVAILABLE = ERROR_CODES_BASE + 22;
/*///*/ store.ERR_PRODUCT_NOT_AVAILABLE = ERROR_CODES_BASE + 23; // Error code indicating that the requested product is not available in the store.
/*///*/ store.ERR_CLOUD_SERVICE_PERMISSION_DENIED = ERROR_CODES_BASE + 24; // Error code indicating that the user has not allowed access to Cloud service information.
/*///*/ store.ERR_CLOUD_SERVICE_NETWORK_CONNECTION_FAILED = ERROR_CODES_BASE + 25; // Error code indicating that the device could not connect to the network.
/*///*/ store.ERR_CLOUD_SERVICE_REVOKED = ERROR_CODES_BASE + 26; // Error code indicating that the user has revoked permission to use this cloud service.
/*///*/ store.ERR_PRIVACY_ACKNOWLEDGEMENT_REQUIRED = ERROR_CODES_BASE + 27; // Error code indicating that the user has not yet acknowledged Apple’s privacy policy for Apple Music.
/*///*/ store.ERR_UNAUTHORIZED_REQUEST_DATA = ERROR_CODES_BASE + 28; // Error code indicating that the app is attempting to use a property for which it does not have the required entitlement.
/*///*/ store.ERR_INVALID_OFFER_IDENTIFIER = ERROR_CODES_BASE + 29; // Error code indicating that the offer identifier is invalid.
/*///*/ store.ERR_INVALID_OFFER_PRICE = ERROR_CODES_BASE + 30; // Error code indicating that the price you specified in App Store Connect is no longer valid.
/*///*/ store.ERR_INVALID_SIGNATURE = ERROR_CODES_BASE + 31; // Error code indicating that the signature in a payment discount is not valid.
/*///*/ store.ERR_MISSING_OFFER_PARAMS = ERROR_CODES_BASE + 32; // Error code indicating that parameters are missing in a payment discount.
///
/// ### product states
///
/*///*/ store.REGISTERED = 'registered';
/*///*/ store.INVALID = 'invalid';
/*///*/ store.VALID = 'valid';
/*///*/ store.REQUESTED = 'requested';
/*///*/ store.INITIATED = 'initiated';
/*///*/ store.APPROVED = 'approved';
/*///*/ store.FINISHED = 'finished';
/*///*/ store.OWNED = 'owned';
/*///*/ store.DOWNLOADING = 'downloading';
/*///*/ store.DOWNLOADED = 'downloaded';
///
/// ### logging levels
///
/*///*/ store.QUIET = 0;
/*///*/ store.ERROR = 1;
/*///*/ store.WARNING = 2;
/*///*/ store.INFO = 3;
/*///*/ store.DEBUG = 4;
///
/// ### validation error codes
///
/*///*/ store.INVALID_PAYLOAD = 6778001;
/*///*/ store.CONNECTION_FAILED = 6778002;
/*///*/ store.PURCHASE_EXPIRED = 6778003;
/*///*/ store.PURCHASE_CONSUMED = 6778004;
/*///*/ store.INTERNAL_ERROR = 6778005;
/*///*/ store.NEED_MORE_DATA = 6778006;
///
/// ### special purpose
///
/*///*/ store.APPLICATION = "application";
})();
(function() {
function defer(thisArg, cb, delay) {
setTimeout(function() {
cb.call(thisArg);
}, delay || 1);
}
var delay = defer;
/// ## <a name="product"></a>*store.Product* object ##
///
/// Most events methods give you access to a `product` object.
store.Product = function(options) {
if (!options)
options = {};
///
/// Products object have the following fields and methods.
///
/// ### *store.Product* public attributes
///
/// - `product.id` - Identifier of the product on the store
this.id = options.id || null;
/// - `product.alias` - Alias that can be used for more explicit [queries](#queries)
this.alias = options.alias || options.id || null;
/// - `product.type` - Family of product, should be one of the defined [product types](#product-types).
var type = this.type = options.type || null;
if (type !== store.CONSUMABLE && type !== store.NON_CONSUMABLE && type !== store.PAID_SUBSCRIPTION && type !== store.FREE_SUBSCRIPTION && type !== store.NON_RENEWING_SUBSCRIPTION && type !== store.APPLICATION)
throw new TypeError("Invalid product type");
/// - `product.group` - Name of the group your subscription product is a member of (default to `"default"`). If you don't set anything, all subscription will be members of the same group.
var defaultGroup = this.type === store.PAID_SUBSCRIPTION ? "default" : "";
this.group = options.group || defaultGroup;
/// - `product.state` - Current state the product is in (see [life-cycle](#life-cycle) below). Should be one of the defined [product states](#product-states)
this.state = options.state || "";
/// - `product.title` - Localized name or short description
this.title = options.title || options.localizedTitle || null;
/// - `product.description` - Localized longer description
this.description = options.description || options.localizedDescription || null;
/// - `product.priceMicros` - Price in micro-units (divide by 1000000 to get numeric price)
this.priceMicros = options.priceMicros || null;
/// - `product.price` - Localized price, with currency symbol
this.price = options.price || null;
/// - `product.currency` - Currency code (optionaly)
this.currency = options.currency || null;
/// - `product.countryCode` - Country code. Available only on iOS
this.countryCode = options.countryCode || null;
// - `product.localizedTitle` - Localized name or short description ready for display
// this.localizedTitle = options.localizedTitle || options.title || null;
// - `product.localizedDescription` - Localized longer description ready for display
// this.localizedDescription = options.localizedDescription || options.description || null;
// - `product.localizedPrice` - Localized price (with currency) ready for display
// this.localizedPrice = options.localizedPrice || null;
/// - `product.loaded` - Product has been loaded from server, however it can still be either `valid` or not
this.loaded = options.loaded;
/// - `product.valid` - Product has been loaded and is a valid product
/// - when product definitions can't be loaded from the store, you should display instead a warning like: "You cannot make purchases at this stage. Try again in a moment. Make sure you didn't enable In-App-Purchases restrictions on your phone."
this.valid = options.valid;
/// - `product.canPurchase` - Product is in a state where it can be purchased
this.canPurchase = options.canPurchase;
/// - `product.owned` - Product is owned
this.owned = options.owned;
/// - `product.deferred` - Purchase has been initiated but is waiting for external action (for example, Ask to Buy on iOS)
this.deferred = options.deferred;
/// - `product.introPrice` - Localized introductory price, with currency symbol
this.introPrice = options.introPrice || null;
/// - `product.introPriceMicros` - Introductory price in micro-units (divide by 1000000 to get numeric price)
this.introPriceMicros = options.introPriceMicros || null;
/// - `product.introPricePeriod` - Duration the introductory price is available (in period-unit)
this.introPricePeriod = options.introPricePeriod || null;
this.introPriceNumberOfPeriods = options.introPriceNumberOfPeriods || null; // legacy
/// - `product.introPricePeriodUnit` - Period for the introductory price ("Day", "Week", "Month" or "Year")
this.introPricePeriodUnit = options.introPricePeriodUnit || null;
this.introPriceSubscriptionPeriod = options.introPriceSubscriptionPeriod || null; // legacy
/// - `product.introPricePaymentMode` - Payment mode for the introductory price ("PayAsYouGo", "UpFront", or "FreeTrial")
this.introPricePaymentMode = options.introPricePaymentMode || null;
/// - `product.ineligibleForIntroPrice` - True when a trial or introductory price has been applied to a subscription. Only available after [receipt validation](#validator). Available only on iOS
this.ineligibleForIntroPrice = options.ineligibleForIntroPrice || null;
/// - `product.discounts` - Array of discounts available for the product. Each discount exposes the following fields:
/// - `id` - The discount identifier
/// - `price` - Localized price, with currency symbol
/// - `priceMicros` - Price in micro-units (divide by 1000000 to get numeric price)
/// - `period` - Number of subscription periods
/// - `periodUnit` - Unit of the subcription period ("Day", "Week", "Month" or "Year")
/// - `paymentMode` - "PayAsYouGo", "UpFront", or "FreeTrial"
/// - `eligible` - True if the user is deemed eligible for this discount by the platform
this.discounts = [];
/// - `product.downloading` - Product is downloading non-consumable content
this.downloading = options.downloading;
/// - `product.downloaded` - Non-consumable content has been successfully downloaded for this product
this.downloaded = options.downloaded;
/// - `product.additionalData` - additional data possibly required for product purchase
this.additionalData = options.additionalData || null;
/// - `product.transaction` - Latest transaction data for this product (see [transactions](#transactions)).
this.transaction = null;
/// - `product.expiryDate` - Latest known expiry date for a subscription (a javascript Date)
/// - `product.lastRenewalDate` - Latest date a subscription was renewed (a javascript Date)
/// - `product.billingPeriod` - Duration of the billing period for a subscription, in the units specified by the `billingPeriodUnit` property. (_not available on iOS < 11.2_)
/// - `product.billingPeriodUnit` - Units of the billing period for a subscription. Possible values: Minute, Hour, Day, Week, Month, Year. (_not available on iOS < 11.2_)
/// - `product.trialPeriod` - Duration of the trial period for the subscription, in the units specified by the `trialPeriodUnit` property (windows only)
/// - `product.trialPeriodUnit` - Units of the trial period for a subscription (windows only)
// Some more fields set by [Fovea.Billing](https://billing.fovea.cc) receipt validator.
// - `product.isBillingRetryPeriod` -
// - `product.isTrialPeriod` -
// - `product.isIntroPeriod` -
// - `product.discountId` -
// - `product.priceConsentStatus` -
// - `product.renewalIntent` -
// - `product.renewalIntentChangeDate` -
// - `product.purchaseDate` -
// - `product.cancelationReason` -
this.stateChanged();
};
///
/// ### *store.Product* public methods
///
/// #### <a name="finish"></a>`product.finish()` ##
///
/// Call `product.finish()` to confirm to the store that an approved order has been delivered.
/// This will change the product state from `APPROVED` to `FINISHED` (see [life-cycle](#life-cycle)).
///
/// As long as you keep the product in state `APPROVED`:
///
/// - the money may not be in your account (i.e. user isn't charged)
/// - you will receive the `approved` event each time the application starts,
/// where you should try again to finish the pending transaction.
///
/// ##### example use
/// ```js
/// store.when("product.id").approved(function(product){
/// // synchronous
/// app.unlockFeature();
/// product.finish();
/// });
/// ```
///
/// ```js
/// store.when("product.id").approved(function(product){
/// // asynchronous
/// app.downloadFeature(function() {
/// product.finish();
/// });
/// });
/// ```
store.Product.prototype.finish = function() {
store.log.debug("product -> defer finishing " + this.id);
defer(this, function() {
store.log.debug("product -> finishing " + this.id);
if (this.state !== store.FINISHED) {
this.set('state', store.FINISHED);
// The platform store should now handle the FINISHED event
// and change the product status to VALID or OWNED.
}
});
};
/// #### <a name="verify"></a>`product.verify()` ##
///
/// Initiate purchase validation as defined by the [`store.validator`](#validator).
///
store.Product.prototype.verify = function() {
var that = this;
var nRetry = 0;
// Callbacks set by the Promise
var noop = function() {};
var doneCb = noop;
var successCb = noop;
var expiredCb = noop;
var errorCb = noop;
var tryValidation = function() {
function getData(data, key) {
if (!data)
return null;
return data.data && data.data[key] || data[key];
}
// No need to verify a which status isn't approved
// It means it already has been
if (that.state !== store.APPROVED)
return;
store._validator(that, function(success, data) {
if (!data) data = {};
store.log.debug("verify -> " + JSON.stringify({
success: success,
data: data
}));
var dataTransaction = getData(data, 'transaction');
if (dataTransaction) {
that.transaction = Object.assign({}, that.transaction || {}, dataTransaction);
store._extractTransactionFields(that);
that.trigger("updated");
}
if (success) {
store.log.debug("verify -> success: " + JSON.stringify(data));
// Process the list of products that are ineligible
// for introductory prices.
if (data && data.ineligible_for_intro_price &&
data.ineligible_for_intro_price.forEach) {
var ineligibleGroups = {};
data.ineligible_for_intro_price.forEach(function(pid) {
var p = store.get(pid);
if (p && p.group)
ineligibleGroups[p.group] = true;
});
store.products.forEach(function(p) {
if (data.ineligible_for_intro_price.indexOf(p.id) >= 0) {
store.log.debug('verify -> ' + p.id + ' ineligibleForIntroPrice:true');
p.set('ineligibleForIntroPrice', true);
}
else {
if (p.group && ineligibleGroups[p.group]) {
store.log.debug('verify -> ' + p.id + ' ineligibleForIntroPrice:true');
p.set('ineligibleForIntroPrice', true);
}
else {
store.log.debug('verify -> ' + p.id + ' ineligibleForIntroPrice:false');
p.set('ineligibleForIntroPrice', false);
}
}
});
}
if (data && data.collection && data.collection.forEach) {
// new behavior: the validator sets products state in the collection
// (including expiry status)
data.collection.forEach(function(purchase) {
var p = store.get(purchase.id);
if (p) {
p.set(purchase);
}
});
}
else if (that.expired) {
// old behavior: a valid receipt means the subscription isn't expired.
that.set("expired", false);
}
store.utils.callExternal('verify.success', successCb, that, data);
store.utils.callExternal('verify.done', doneCb, that);
that.trigger("verified");
}
else {
store.log.debug("verify -> error: " + JSON.stringify(data));
var msg = data && data.error && data.error.message ? data.error.message : '';
var err = new store.Error({
code: store.ERR_VERIFICATION_FAILED,
message: "Transaction verification failed: " + msg
});
if (data.code === store.PURCHASE_EXPIRED) {
err = new store.Error({
code: store.ERR_PAYMENT_EXPIRED,
message: "Transaction expired: " + msg
});
that.set("expired", true);
store.error(err);
store.utils.callExternal('verify.error', errorCb, err);
store.utils.callExternal('verify.done', doneCb, that);
that.trigger("expired");
that.set("state", store.VALID);
store.utils.callExternal('verify.expired', expiredCb, that);
}
else if (nRetry < 4) {
// It failed... let's try one more time. Maybe the appStoreReceipt wasn't updated yet.
nRetry += 1;
delay(this, tryValidation, 1500 * nRetry * nRetry);
}
else {
store.log.debug("validation failed, no retrying, trigger an error");
store.error(err);
store.utils.callExternal('verify.error', errorCb, err);
store.utils.callExternal('verify.done', doneCb, that);
that.trigger("unverified");
}
}
});
};
defer(this, function() {
if (that.state !== store.APPROVED) {
if (that.type !== store.APPLICATION) {
var err = new store.Error({
code: store.ERR_VERIFICATION_FAILED,
message: "Product isn't in the APPROVED state"
});
store.error(err);
store.utils.callExternal('verify.error', errorCb, err);
}
store.utils.callExternal('verify.done', doneCb, that);
return;
}
});
// For some reason, the appStoreReceipt isn't always immediately available.
delay(this, tryValidation, 1000);
/// ##### return value
/// A Promise with the following methods:
///
var ret = {
/// - `done(function(product){})`
/// - called whether verification failed or succeeded.
done: function(cb) { doneCb = cb; return this; },
/// - `expired(function(product){})`
/// - called if the purchase expired.
expired: function(cb) { expiredCb = cb; return this; },
/// - `success(function(product, purchaseData){})`
/// - called if the purchase is valid and verified.
/// - `purchaseData` is the device dependent transaction details
/// returned by the validator, which you can most probably ignore.
success: function(cb) { successCb = cb; return this; },
/// - `error(function(err){})`
/// - validation failed, either because of expiry or communication
/// failure.
/// - `err` is a [store.Error object](#errors), with a code expected to be
/// `store.ERR_PAYMENT_EXPIRED` or `store.ERR_VERIFICATION_FAILED`.
error: function(cb) { errorCb = cb; return this; }
};
///
return ret;
};
store._extractTransactionFields = function(that, t) {
t = t || that.transaction;
store.log.debug('transaction fields for ' + that.id);
// using legacy transactions (platform specific)
if (t.type === 'ios-appstore' && t.expires_date_ms) {
that.lastRenewalDate = new Date(parseInt(t.purchase_date_ms));
that.expiryDate = new Date(parseInt(t.expires_date_ms));
store.log.debug('expiryDate: ' + that.expiryDate.toISOString());
}
else if (t.type === 'android-playstore' && t.expiryTimeMillis > 0) {
that.lastRenewalDate = new Date(parseInt(t.startTimeMillis));
that.expiryDate = new Date(parseInt(t.expiryTimeMillis));
store.log.debug('expiryDate: ' + that.expiryDate.toISOString());
}
// using unified transaction fields
if (t.expiryDate)
that.expiryDate = new Date(t.expiryDate);
if (t.lastRenewalDate)
that.lastRenewalDate = new Date(t.lastRenewalDate);
if (t.renewalIntent)
that.renewalIntent = t.renewalIntent;
// owned?
if (that.type === store.PAID_SUBSCRIPTION && +that.expiryDate) {
var now = +new Date();
if (now > that.expiryDate.getTime() + 60000) {
window.setTimeout(function() {
if (that.state === store.OWNED) {
that.set('state', store.APPROVED);
that.verify();
}
}, 30000);
}
}
return t;
};
///
/// ### life-cycle
///
/// A product will change state during the application execution.
///
/// Find below a diagram of the different states a product can pass by.
///
/// REGISTERED +--> INVALID
/// |
/// +--> VALID +--> REQUESTED +--> INITIATED +-+
/// |
/// ^ +------------------------------+
/// | |
/// | | +--> DOWNLOADING +--> DOWNLOADED +
/// | | | |
/// | +--> APPROVED +--------------------------------+--> FINISHED +--> OWNED
/// | |
/// +-------------------------------------------------------------+
///
/// #### states definitions
///
/// - `REGISTERED`: right after being declared to the store using [`store.register()`](#register)
/// - `INVALID`: the server didn't recognize this product, it cannot be used.
/// - `VALID`: the server sent extra information about the product (`title`, `price` and such).
/// - `REQUESTED`: order (purchase) requested by the user
/// - `INITIATED`: order transmitted to the server
/// - `APPROVED`: purchase approved by server
/// - `FINISHED`: purchase delivered by the app (see [Finish a Purchase](#finish-a-purchase))
/// - `OWNED`: purchase is owned (only for non-consumable and subscriptions)
/// - `DOWNLOADING` purchased content is downloading (only for non-consumable)
/// - `DOWNLOADED` purchased content is downloaded (only for non-consumable)
///
/// #### Notes
///
/// - 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
///
/// Each time the product changes state, appropriate events is triggered.
///
/// Learn more about events [here](#events) and about listening to events [here](#when).
///
})();
(function(){
///
/// ## <a name="errors"></a>*store.Error* object
///
/// All error callbacks takes an `error` object as parameter.
store.Error = function(options) {
if (!options)
options = {};
///
/// Errors have the following fields:
///
/// - `error.code` - An integer [error code](#error-codes). See the [error codes](#error-codes) section for more details.
this.code = options.code || store.ERR_UNKNOWN;
/// - `error.message` - Human readable message string, useful for debugging.
this.message = options.message || "unknown error";
///
};
/// ## <a name="error"></a>*store.error(callback)*
///
/// Register an error handler.
///
/// `callback` is a function taking an [error](#errors) as argument.
///
/// ### example use:
///
/// store.error(function(e){
/// console.log("ERROR " + e.code + ": " + e.message);
/// });
///
store.error = function(cb, altCb) {
var ret = cb;
if (cb instanceof store.Error) {
store.error.callbacks.trigger(cb);
}
else if (typeof cb === "function") {
store.error.callbacks.push(cb);
}
/// ### alternative usage
///
/// - `store.error(code, callback)`
/// - only call the callback for errors with the given error code.
/// - **example**: `store.error(store.ERR_SETUP, function() { ... });`
else if (typeof altCb === "function") {
ret = function(err) {
if (err.code === cb)
altCb();
};
store.error(ret);
}
else if (cb.code && cb.message) {
store.error.callbacks.trigger(new store.Error(cb));
}
else if (cb.code) {
// error message is null(unknown error)
store.error.callbacks.trigger(new store.Error(cb));
}
///
return ret;
};
/// ### unregister the error callback
/// To unregister the callback, you will use [`store.off()`](#off):
/// ```js
/// var handler = store.error(function() { ... } );
/// ...
/// store.off(handler);
/// ```
///
// Unregister a callback registered with `store.error`
// this method is called by `store.off`.
store.error.unregister = function(cb) {
store.error.callbacks.unregister(cb);
};
})();
(function() {
/// ## <a name="register"></a>*store.register(product)*
/// Add (or register) a product into the store.
///
/// A product can't be used unless registered first!
///
/// Product is an object with fields :
///
/// - `id`
/// - `type`
/// - `alias` (optional)
///
/// See documentation for the [product](#product) object for more information.
///
store.register = function(product) {
if (!product)
return;
if (typeof product.length === 'number')
registerProducts(product);
else
store.register([product]);
};
/// ##### example usage
///
/// ```js
/// store.register({
/// id: "cc.fovea.inapp1",
/// alias: "full version",
/// type: store.NON_CONSUMABLE
/// });
/// ```
///
// ## <a name="registerProducts"></a>*registerProducts(products)*
// Adds (or register) products into the store. Products can't be used
// unless registered first!
//
// Products is an array of object with fields :
//
// - `id`
// - `type`
// - `alias` (optional)
//
// See documentation for the [product](#product) object for more information.
function registerProducts(products) {
for (var i = 0; i < products.length; ++i) {
products[i].state = store.REGISTERED;
var p = new store.Product(products[i]);
if (!p.alias)
p.alias = p.id;
// Check if id or alias contain filtered-out keywords
if (p.id !== store._queries.uniqueQuery(p.id))
continue;
if (p.alias !== store._queries.uniqueQuery(p.alias))
continue;
if (hasKeyword(p.id) || hasKeyword(p.alias))
continue;
store.products.push(p);
}
}
///
/// ### Reserved keywords
/// Some reserved keywords can't be used in the product `id` and `alias`:
var keywords = [ ///
'product', /// - `product`
'order', /// - `order`
store.REGISTERED, /// - `registered`
store.VALID, /// - `valid`
store.INVALID, /// - `invalid`
store.REQUESTED, /// - `requested`
store.INITIATED, /// - `initiated`
store.APPROVED, /// - `approved`
store.OWNED, /// - `owned`
store.FINISHED, /// - `finished`
store.DOWNLOADING,/// - `downloading`
store.DOWNLOADED, /// - `downloaded`
'refreshed' /// - `refreshed`
]; ///
function hasKeyword(string) {
if (!string)
return false;
var tokens = string.split(' ');
for (var i = 0; i < tokens.length; ++i) {
var token = tokens[i];
for (var j = 0; j < keywords.length; ++j) {
if (token === keywords[j])
return true;
}
}
return false;
}
})();
(function() {
/// ## <a name="get"></a>*store.get(id)*
/// Retrieve a [product](#product) from its `id` or `alias`.
///
/// ##### example usage
//
/// ```js
/// var product = store.get("cc.fovea.product1");
/// ```
///
store.get = function(id) {
var product = store.products.byId[id] || store.products.byAlias[id];
return product;
};
})();
(function(){
/// ## <a name="when"></a>*store.when(query)*
///
/// Register a callback for a product-related event.
///
store.when = function(query, once, callback) {
// No arguments, will match all products.
if (typeof query === 'undefined')
query = '';
// In case the first arguemnt is a product, convert to its id
if (typeof query === 'object' && query instanceof store.Product)
query = query.id;
if (typeof once === 'function') {
return store.when("", query, once);
}
else if (typeof once !== 'string') {
var ret = {};
var addPromise = function(name) {
ret[name] = function(cb) {
store._queries.callbacks.add(query, name, cb, once);
return this;
};
};
///
/// ### return value
///
/// Return a Promise with methods to register callbacks for
/// product events defined below.
///
/// #### events
///
/// - `loaded(product)`
/// - Called when [product](#product) data is loaded from the store.
addPromise('loaded');
/// - `updated(product)`
/// - Called when any change occured to a product.
addPromise('updated');
/// - `error(err)`
/// - Called when an [order](#order) failed.
/// - The `err` parameter is an [error object](#errors)
addPromise('error');
/// - `approved(product)`
/// - Called when a product [order](#order) is approved.
addPromise('approved');
/// - `owned(product)`
/// - Called when a non-consumable product or subscription is owned.
addPromise('owned');
/// - `cancelled(product)`
/// - Called when a product [order](#order) is cancelled by the user.
addPromise('cancelled');
/// - `refunded(product)`
/// - Called when an order is refunded by the user.
addPromise('refunded');
/// - Actually, all other product states have their promise
/// - `registered`, `valid`, `invalid`, `requested`,
/// `initiated` and `finished`
addPromise('registered');
addPromise('valid');
addPromise('invalid');
addPromise('requested');
addPromise('initiated');
addPromise('finished');
/// - `verified(product)`
/// - Called when receipt validation successful
addPromise('verified');
/// - `unverified(product)`
/// - Called when receipt verification failed
addPromise('unverified');
/// - `expired(product)`
/// - Called when validation find a subscription to be expired
addPromise('expired');
/// - `downloading(product, progress, time_remaining)`
/// - Called when content download is started
addPromise("downloading");
/// - `downloaded(product)`
/// - Called when content download has successfully completed
addPromise("downloaded");
return ret;
}
else {
///
/// ### alternative usage
///
/// - `store.when(query, action, callback)`
/// - Register a callback using its action name. Beware that this is more
/// error prone, as there are not gonna be any error in case of typos.
///
/// ```js
/// store.when("cc.fovea.inapp1", "approved", function(product) { ... });
/// ```
///
var action = once;
store._queries.callbacks.add(query, action, callback);
}
};
/// ### unregister a callback
///
/// To unregister a callback, use [`store.off()`](#off).
///
// Remove any callbacks registered with `when`
store.when.unregister = function(cb) {
store._queries.callbacks.unregister(cb);
};
///
/// ## queries
///
/// The [`when`](#when) and [`once`](#once) methods take a `query` parameter.
/// Those queries allow to select part of the products (or orders) registered
/// into the store and get notified of events related to those products.
///
/// No filters:
///
/// - `"product"` or `"order"` - for all products.
///
/// Filter by product types:
///
/// - `"consumable"` - all consumable products.
/// - `"non consumable"` - all non consumable products.
/// - `"subscription"` - all subscriptions.
/// - `"free subscription"` - all free subscriptions.
/// - `"paid subscription"` - all paid subscriptions.
///
/// Filter by product state:
///
/// - `"valid"` - all products in the VALID state.
/// - `"invalid"` - all products in the INVALID state.
/// - `"owned"` - all products in the OWNED state.
/// - etc. (see [here](#product-states) for all product states).
///
/// Filter individual products:
///
/// - `"PRODUCT_ID"` - product with the given product id (replace by your own product id)
/// - `"ALIAS"` - product with the given alias
///
/// Notice that you can add the "product" and "order" keywords anywhere in your query,
/// it won't change anything but may seem nicer to read.
///
/// #### example
///
/// - `"consumable order"` - all consumable products
/// - `"full version"` - the `alias` of a registered [`product`](#product)
/// - `"order cc.fovea.inapp1"` - the `id` of a registered [`product`](#product)
/// - equivalent to just `"cc.fovea.inapp1"`
/// - `"invalid product"` - an invalid product
/// - equivalent to just `"invalid"`
///
})();
(function(){
/// ## <a name="once"></a>*store.once(query)*
///
/// Identical to [`store.when`](#when), but the callback will be called only once.
/// After being called, the callback will be unregistered.
store.once = function(query, action, callback) {
if (typeof action === 'function') {
return store.when(query, action, true);
}
else if (typeof action === 'undefined') {
return store.when(query, true);
}
else {
///
/// ### alternative usage
///
/// - `store.once(query, action, callback)`
/// - Same remarks as `store.when(query, action, callback)`
///
store._queries.callbacks.add(query, action, callback, true);
}
};
store.once.unregister = store.when.unregister;
})();
(function() {
// Store all pending callbacks, prevents promises to be called multiple times.
var callbacks = {};
// Next call to `order` will store its callbacks using this ID, then increment the ID.
var callbackId = 0;
///
/// ## <a name="order"></a>*store.order(product, additionalData)*
///
/// Initiate the purchase of a product.
///
/// The `product` argument can be either:
///
/// - the `store.Product` object
/// - the product `id`
/// - the product `alias`
///
/// The `additionalData` argument can be either:
/// - null
/// - object with attributes:
/// - `oldSku`, a string with the old subscription to upgrade/downgrade on Android.
/// **Note**: if another subscription product is already owned that is member of
/// the same group, `oldSku` will be set automatically for you (see `product.group`).
/// - `prorationMode`, a string that describe the proration mode to apply when upgrading/downgrading a subscription (with `oldSku`) on Android. See https://developer.android.com/google/play/billing/subs#change
/// **Possible values:**
/// - `DEFERRED` - Replacement takes effect when the old plan expires, and the new price will be charged at the same time.
/// - `IMMEDIATE_AND_CHARGE_PRORATED_PRICE` - Replacement takes effect immediately, and the billing cycle remains the same.
/// - `IMMEDIATE_WITHOUT_PRORATION` - Replacement takes effect immediately, and the new price will be charged on next recurrence time.
/// - `IMMEDIATE_WITH_TIME_PRORATION` - Replacement takes effect immediately, and the remaining time will be prorated and credited to the user.
/// - `discount`, a object that describes the discount to apply with the purchase (iOS only):
/// - `id`, discount identifier
/// - `key`, key identifier
/// - `nonce`, uuid value for the nonce
/// - `timestamp`, time at which the signature was generated (in milliseconds since epoch)
/// - `signature`, cryptographic signature that unlock the discount
///
/// See the ["Purchasing section"](#purchasing) to learn more about
/// the purchase process.
///
/// See ["Subscriptions Offer Best Practices"](https://developer.apple.com/videos/play/wwdc2019/305/)
/// for more details on subscription offers.
///
store.order = function(pid, additionalData) {
var p = pid;
if (typeof pid === "string") {
p = store.products.byId[pid] || store.pr