Migrate to v13
Version 13 of the plugin introduces significant breaking changes compared to v11 and earlier. This guide outlines the key differences and how to update your existing code.
Ionic / Capacitor Users: Note that @ionic-native/in-app-purchase-2 is not compatible with cordova-plugin-purchase v13+. You should remove the Ionic Native wrapper and use the plugin directly via the CdvPurchase global namespace (e.g., CdvPurchase.store). Ensure you wait for the platform to be ready (this.platform.ready() in Ionic) before accessing CdvPurchase.store.
1. Global Namespace Change
- Before: The plugin exposed its API through the global
window.storeobject. - After (v13+): The API is now under the
window.CdvPurchasenamespace. The main entry point isCdvPurchase.store.
Migration:
- Option A (Recommended): Replace all instances of
store.withCdvPurchase.store..// Before
store.register(...);
store.when(...);
x // After CdvPurchase.store.register(...); CdvPurchase.store.when(...); ```
- Option B (Quick Fix, Less Recommended): Re-assign the global
storevariable afterdeviceready. This maintains compatibility with old code but pollutes the global scope and might cause type issues if not declared properly.// After deviceready
window.store = window.CdvPurchase.store;
// If using TypeScript, add declaration:
// declare interface Window { store: CdvPurchase.Store; }
2. Initialization and Refreshing
- Before:
store.refresh()was used ambiguously for initialization, refreshing prices/status, and restoring purchases. - After (v13+): These actions are split into separate, clearer methods:
store.initialize([...platforms]): Mandatory first call. Initializes specified platforms (e.g.,[Platform.APPLE_APPSTORE, Platform.GOOGLE_PLAY]), loads product details, and fetches initial receipt/purchase status. Call this once afterdeviceready.store.update(): Refreshes product pricing and potentially updates local purchase status by communicating with the stores. Use this if you need to refresh prices long after initialization (e.g., when the user enters the store screen). It's rate-limited by default (store.minTimeBetweenUpdates) to avoid excessive calls.store.restorePurchases(): Specifically triggers the platform's restore mechanism (primarily for non-consumables/subscriptions on iOS/AppStore) to retrieve purchases made with the same store account on other devices.
Migration:
- Replace your initial
store.refresh()call withstore.initialize([...platforms]). Ensure you pass the platforms you intend to use. - If you used
store.refresh()later to update prices, replace it withstore.update(). - If you used
store.refresh()specifically for restoring purchases (often via a "Restore Purchases" button), replace it withstore.restorePurchases().
3. Product Registration
- Before:
store.register({ id, type, alias }) - After (v13+):
store.register({ id, type, platform }). Theplatformfield (e.g.,CdvPurchase.Platform.APPLE_APPSTORE) is now mandatory to specify which store the product belongs to. Thealiasfield has been removed.
Migration:
- Add the correct
platform: CdvPurchase.Platform.XXXfield to each product registration object. - Remove the
aliasfield if present.
// Before
store.register({
id: 'my_product_ios',
type: store.NON_CONSUMABLE,
alias: 'Unlock Feature'
});
store.register({
id: 'my_product_android',
type: store.NON_CONSUMABLE,
alias: 'Unlock Feature'
});
// After
const { store, ProductType, Platform } = CdvPurchase;
store.register([{
id: 'my_product_ios',
type: ProductType.NON_CONSUMABLE,
platform: Platform.APPLE_APPSTORE
}, {
id: 'my_product_android',
type: ProductType.NON_CONSUMABLE,
platform: Platform.GOOGLE_PLAY
}]);
4. Product Information Refactoring
- Before: The
Productobject mixed metadata (title, price), local purchase state (approved, finished), and validated data (expiryDate, owned). - After (v13+): Information is separated into distinct objects for clarity and accuracy:
Product: Contains store metadata (title, description, offers/pricing). Accessed viastore.productsorstore.get().Offer: Represents a specific pricing option for a product (e.g., monthly vs. annual, introductory vs. regular). Accessed viaproduct.offersorproduct.getOffer(). ContainspricingPhases.PricingPhase: Details a specific period within an offer (e.g., free trial, intro price, regular price). Containsprice,priceMicros,currency,billingPeriod, etc.Receipt: Represents the purchase data reported locally by the device's SDK. Accessed viastore.localReceipts. Containstransactions. Do not rely solely on this for entitlement.Transaction: Represents a single purchase attempt within a receipt. ContainstransactionId,state,purchaseDate, etc.VerifiedReceipt: The result from yourstore.validator. Accessed via theverifiedevent orstore.verifiedReceipts. Contains acollectionofVerifiedPurchaseobjects and is the trusted source for entitlement.VerifiedPurchase: Authoritative purchase data from the validator (includesid,purchaseDate,expiryDate,isExpired,renewalIntent, etc.).
Migration:
- Access metadata like
title,descriptiondirectly from theProductobject (store.get('id', platform).title). - Access pricing via
product.pricing(for simple cases:product.offers[0].pricingPhases[0]) or iterate throughproduct.offers[].pricingPhases[]for complex offers/subscriptions. - Check local transaction state via
store.findInLocalReceipts(product)?.state. - Check validated ownership and expiry via
store.owned(product)or by inspectingstore.verifiedPurchases. Do not rely on local receipts for subscription status or permanent non-consumable unlocks.
See the table in the main Migration Guide for a detailed mapping of old fields to new locations.
5. Event Handling (store.when())
- Before:
store.when("filter").event(callback)used filters (product id, type, alias) directly in thewhencall. Events often related directly to aProductobject that mixed states. - After (v13+):
store.when().event(callback). Filters are removed fromwhen(). Events are more specific to the data they relate to (productUpdated,receiptUpdated,approved(for Transaction),verified(for VerifiedReceipt),finished(for Transaction)).
Migration:
- Remove filters from
when()calls (e.g., changestore.when('my_product').approved(...)tostore.when().approved(...)). - Implement filtering logic inside your callback functions if needed (check
transaction.products[0].id,receipt.collection[0].id,product.id). - Adjust callbacks based on the new event signatures (e.g.,
approvedreceives aTransaction,verifiedreceives aVerifiedReceipt).
// Before
store.when("my_product", "approved", (product) => {
product.verify(); // Called verify on the mixed Product object
});
store.when("my_product", "verified", (product) => {
product.finish(); // Called finish on the mixed Product object
});
store.when("my_product", "updated", (product) => {
// update UI based on mixed product state
});
// After (v13+)
const { store } = CdvPurchase;
store.when()
.approved((transaction) => {
// Check if this transaction is for the product we care about
if (transaction.products.some(p => p.id === 'my_product')) {
console.log('Transaction approved for my_product:', transaction.transactionId);
transaction.verify(); // Call verify on the Transaction object
}
})
.verified((receipt) => {
// Check if the verified receipt contains the product we care about
// Note: A receipt might contain multiple purchases/transactions
const relevantPurchase = receipt.collection.find(p => p.id === 'my_product');
if (relevantPurchase) {
console.log('Receipt verified containing my_product');
// Grant entitlement based on 'relevantPurchase' data
// Finish the transaction(s) associated with this receipt
receipt.finish();
}
})
.productUpdated((product) => {
// This event is now only for metadata updates (title, price, etc.)
if (product.id === 'my_product') {
console.log('Product metadata updated for my_product');
// update UI display for my_product (price, title...)
}
})
.receiptUpdated((receipt) => {
// This event reflects changes in local transaction state
// Check if any transaction for 'my_product' changed state
receipt.transactions.forEach(transaction => {
if (transaction.products.some(p => p.id === 'my_product')) {
console.log(`Local transaction state update for my_product: ${transaction.state}`);
// Update UI based on local state if needed (e.g., show "Pending")
// But rely on 'verified' for actual ownership confirmation.
}
});
});
updatedEvent: Split intoproductUpdated(for metadata changes) andreceiptUpdated(for local purchase data changes). Update your listeners accordingly.ownedEvent: Removed. Check ownership status withinverifiedorreceiptUpdatedhandlers usingstore.owned(product)or by inspectingstore.verifiedPurchases. See the main Migration Guide for strategies.errorEvent (Product): Removed. Handle purchase errors via the promise returned byoffer.order()orstore.requestPayment(). General plugin errors are still caught bystore.error().
6. Ordering Products
- Before:
store.order("productId")orstore.order(product) - After (v13+):
offer.order(additionalData?). You now order a specificOfferobject obtained from aProduct.
Migration:
- Get the
Productobject:const product = CdvPurchase.store.get('productId', platform); - Get the desired
Offer:const offer = product?.getOffer();(orproduct?.getOffer('offerId')if multiple offers exist). - Call order on the offer:
if (offer) offer.order(additionalData);
7. Finishing and Verifying
- Before:
product.verify()andproduct.finish()were called on the mixedProductobject. - After (v13+): Methods are now on
Transaction,Receipt, orVerifiedReceiptobjects.transaction.verify(): Verifies the specific transaction's receipt data. Typically called in theapprovedhandler.transaction.finish(): Finishes/Acknowledges/Consumes the specific transaction.receipt.verify(): Verifies all transactions within the local receipt (less common).receipt.finish(): Finishes all transactions within the local receipt.verifiedReceipt.finish(): Finishes the transaction(s) associated with the verified receipt. Typically called in theverifiedhandler.
Migration:
- Typically, you'll call
transaction.verify()in theapprovedhandler andreceipt.finish()(orverifiedReceipt.finish()) in theverifiedhandler.
8. Enumerations
- Before: Constants were attached directly to the
storeobject (e.g.,store.CONSUMABLE,store.ERR_SETUP,store.DEBUG). - After (v13+): Constants are organized into proper enums within the
CdvPurchasenamespace:CdvPurchase.ProductType.CONSUMABLE,CdvPurchase.ProductType.PAID_SUBSCRIPTION, etc.CdvPurchase.Platform.APPLE_APPSTORE,CdvPurchase.Platform.GOOGLE_PLAY, etc.CdvPurchase.ErrorCode.SETUP,CdvPurchase.ErrorCode.PURCHASE, etc.CdvPurchase.LogLevel.DEBUG,CdvPurchase.LogLevel.INFO, etc.CdvPurchase.TransactionState.APPROVED,CdvPurchase.TransactionState.FINISHED, etc.
Migration:
- Update references to use the new enum syntax (e.g., change
store.CONSUMABLEtoCdvPurchase.ProductType.CONSUMABLE). - Backward Compatibility: For easier migration, the old constants are currently still attached to the
storeobject as properties (e.g.,CdvPurchase.store.CONSUMABLE), but using the enums (CdvPurchase.ProductType.CONSUMABLE) is strongly recommended for clarity, type safety, and future-proofing.
9. Braintree Integration
Version 13 introduces official Braintree support via the cordova-plugin-purchase-braintree extension. If you were using previous unofficial Braintree integrations, you'll need to migrate to the new adapter structure, using store.initialize() with Platform.BRAINTREE and store.requestPayment(). Refer to the Braintree Use Case.
Review these changes carefully and update your application code accordingly. Test thoroughly after migration, especially the purchase and validation flows. Consult the API documentation for detailed information on the new methods and classes.
What Changed in v13.12 -- v13.15
If you are already on v13.x and upgrading to v13.12 or later, this section covers the significant additions, new features, and behavioral changes introduced across these releases.
Capacitor Native Plugin (v13.15+)
The plugin now ships a dedicated native Capacitor adapter as a separate package. Capacitor apps no longer need the Cordova compatibility layer.
Installation:
npm install capacitor-plugin-cdv-purchase
npx cap sync
The JavaScript API (CdvPurchase.store) remains identical -- only the native bridge changes. Capacitor 6, 7, and 8 are supported.
Migration from Cordova-Capacitor compatibility mode: If you previously used cordova-plugin-purchase inside a Capacitor app via @awesome-cordova-plugins or similar wrappers, switch to the native package for better performance and simpler builds. Remove the Cordova plugin and install capacitor-plugin-cdv-purchase instead.
StoreKit 2 Extension (v13.14+)
StoreKit 2 is supported as an optional extension. When cordova-plugin-purchase-storekit2 is installed, the Apple AppStore adapter automatically upgrades from StoreKit 1 to StoreKit 2 on iOS 15+ devices -- no code changes are needed.
Installation:
cordova plugin add cordova-plugin-purchase-storekit2
# or for Capacitor:
npm install cordova-plugin-purchase-storekit2
npx cap sync
What changes with StoreKit 2:
- Per-transaction JWS (JSON Web Signature) tokens replace the monolithic
appStoreReceipt - Receipt validation uses transaction type
apple-sk2with ajwsRepresentationfield - Native async/await APIs for product loading, purchases, and transaction observation
- Built-in support for manage-subscriptions and offer-code redemption sheets
Fallback behavior: If the extension is not installed, or the device runs iOS < 15, the adapter continues using StoreKit 1 transparently. No conditional logic is required in your app code.
Important: When both plugins are installed on iOS 15+, the StoreKit 1 observer is automatically disabled to prevent duplicate transaction delivery.
Multi-Quantity Consumable Purchases (v13.15+)
Multi-quantity purchases are now supported on iOS (in addition to the existing Android support). Pass a quantity value (1--10, Apple's limit) when ordering:
const error = await store.order(offer, { quantity: 3 });
Platforms advertise support via a capability check:
if (store.checkSupport(CdvPurchase.Platform.APPLE_APPSTORE, 'orderQuantity')) {
// show the quantity picker
}
The VerifiedPurchase object now includes an optional quantity field reflecting the number of units purchased.
Storefront / Country Code API (v13.15+)
A new store.getStorefront() method returns the user's billing country:
const storefront = store.getStorefront();
// { countryCode: 'US' }
store.when().storefrontUpdated(sf => {
console.log('Storefront changed to:', sf.countryCode);
});
Use this for regional pricing display, product filtering, or legal prompts. The value refreshes automatically after order, restore, and update flows.
Available on Apple AppStore (SK1 + SK2), Google Play, and through the native Capacitor plugin.
IapticJS Adapter
A new Platform.IAPTIC_JS adapter allows web-based apps (using Stripe via the iaptic-js SDK) to participate in the same purchase flow as native platforms. Initialize it alongside other platforms:
store.initialize([
Platform.APPLE_APPSTORE,
Platform.GOOGLE_PLAY,
{ platform: Platform.IAPTIC_JS, options: { /* iaptic-js config */ } }
]);
This is primarily useful for apps that offer web-based Stripe payments in addition to native store purchases, managed through a single Iaptic backend.
Privacy Policy for Validator Requests
Control what device information is sent with receipt validation requests via store.validator_privacy_policy:
CdvPurchase.store.validator_privacy_policy = ['fraud', 'support', 'analytics'];
Allowed values:
fraud-- hashed device fingerprint for fraud detectionsupport-- OS, OS version, device manufacturer, Cordova versionanalytics-- same data assupport, intended for analytics usetracking-- device identifiers and/or UUID (if available)
Default: ['fraud', 'support', 'analytics']. Requires cordova-plugin-device to be installed.
Google Play Billing Library 8.3 (v13.13+)
The plugin upgraded from Billing Library 7.x to 8.3.0. Key behavioral changes:
minSdkVersion23 required. If your app targets API < 23, you must update.- Suspended subscriptions now returned in purchases list. Previously, Google Play omitted paused or payment-on-hold subscriptions. Now they appear with an
expirationDatein the past, sostore.owned()correctly returnsfalse. No code changes needed -- the existing expiry check handles this. - One-time product offers. In-app products can now have multiple offers (v12.0 format), similar to subscriptions. The plugin resolves the first available offer with pricing data.
- Pending purchases enabled for one-time products and prepaid plans.
- Auto service reconnection for more reliable BillingClient connections when Google Play Services disconnects.
Other Notable Changes
- Validation payload optimization (v13.15.2): The full product catalog is sent with validation requests at most once per 24 hours instead of on every call, reducing payload size significantly.
store.restorePurchases()deduplication (v13.15.3): Repeated restore calls on Android no longer accumulate stale duplicates in the internal purchase list.- iOS sandbox dialog loop fix (v13.15.3): A synthetic receipt fallback prevents the sandbox sign-in dialog from looping when the app receipt fails to load.
- Stale ownership at startup fix (v13.15.3):
loadReceipts()now properly awaits pending transactions instead of relying on a fixed timeout.
Breaking Changes Summary
| Change | Version | Action Required |
|---|---|---|
minSdkVersion 23 (Android) | 13.13 | Update build.gradle if targeting API < 23 |
| Suspended subscriptions in purchases list (Android) | 13.13 | None -- handled automatically by expiry check |
| SK1 observer disabled when SK2 extension installed (iOS) | 13.15.2 | None -- automatic; be aware if you relied on SK1-specific behavior |