UNPKG

cc.fovea.cordova.purchase

Version:

Cordova Purchase plugin for iOS, Android, Windows (AppStore, Play, UWP)

1,309 lines (1,169 loc) 136 kB
// // 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