cc.fovea.cordova.purchase
Advanced tools
Comparing version 7.3.0-beta.0 to 7.4.0
@@ -347,12 +347,6 @@ # API Documentation | ||
- `product.description` - Localized longer description | ||
- `product.priceMicros` - Price in micro-units (divide by 1000000 to get numeric price) | ||
- `product.priceMicros` - Localized price, in micro-units (divide by 1000000 to get numeric price) | ||
- `product.price` - Localized price, with currency symbol | ||
- `product.currency` - Currency code (optionaly) | ||
- `product.countryCode` - Country code. Available only on iOS | ||
- `product.introPrice` - Localized introductory price, with currency symbol. Available only on iOS | ||
- `product.introPriceMicros` - Introductory price in micro-units (divide by 1000000 to get numeric price). Available only on iOS | ||
- `product.introPriceNumberOfPeriods` - number of periods the introductory price is available. Available only on iOS | ||
- `product.introPriceSubscriptionPeriod` - Period for the introductory price ("Day", "Week", "Month" or "Year"). Available only on iOS | ||
- `product.introPricePaymentMode` - Payment mode for the introductory price ("PayAsYouGo", "UpFront", or "FreeTrial"). Available only on iOS | ||
- `product.ineligibleForIntroPrice` - True when a trial or introductory price has been applied to a subscription. Only available after receipt validation. Available only on iOS | ||
- `product.loaded` - Product has been loaded from server, however it can still be either `valid` or not | ||
@@ -359,0 +353,0 @@ - `product.valid` - Product has been loaded and is a valid product |
@@ -11,5 +11,1 @@ # Troubleshooting | ||
See: https://github.com/j3k0/cordova-plugin-purchase/issues/76#issuecomment-407454342 | ||
#### Android - "error:0c0000b1:ASN.1 encoding routines:OPENSSL_internal:TOO_LONG" | ||
If you see this error in your adb logcat output, it means the billing key is not set correctly (for some reasons this makes openssl crash). |
{ | ||
"name": "cc.fovea.cordova.purchase", | ||
"version": "7.3.0-beta.0", | ||
"version": "7.4.0", | ||
"description": "Cordova Purchase plugin for iOS and Android (AppStore and PlayStore)", | ||
@@ -42,11 +42,12 @@ "cordova": { | ||
"devDependencies": { | ||
"cordova": "^6.5.0", | ||
"coveralls": "^2.13.3", | ||
"eslint": "^3.19.0", | ||
"cordova": "^6.3.1", | ||
"eslint": "^3.8.1", | ||
"istanbul": "^0.4.5", | ||
"jshint": "^2.9.6", | ||
"mocha": "^3.5.3", | ||
"jshint": "^2.9.4", | ||
"mocha": "^3.1.2", | ||
"preprocessor": "^1.4.0", | ||
"uglify-js": "^2.7.4" | ||
} | ||
"uglify-js": "^2.7.4", | ||
"coveralls": "^2.11.14" | ||
}, | ||
"types": "./src/js/index.d.ts" | ||
} |
@@ -1,5 +0,10 @@ | ||
declare var store: store.IStore; | ||
declare var store: IapStore.IStore; | ||
declare namespace store { | ||
export type StoreProductType = 'consumable' | 'non consumable' | 'free subscription' | 'paid subscription'; | ||
declare namespace IapStore { | ||
export type StoreProductType = | ||
'consumable' | | ||
'non consumable' | | ||
'free subscription' | | ||
'paid subscription' | | ||
'non renewing subscription'; | ||
@@ -24,2 +29,9 @@ export interface IError { | ||
verified(callback: (product: IStoreProduct) => void): IWhen; | ||
expired(callback: (product: IStoreProduct) => void): IWhen; | ||
finished(callback: (product: IStoreProduct) => void): IWhen; | ||
initiated(callback: (product: IStoreProduct) => void): IWhen; | ||
invalid(callback: (product: IStoreProduct) => void): IWhen; | ||
registered(callback: (product: IStoreProduct) => void): IWhen; | ||
requested(callback: (product: IStoreProduct) => void): IWhen; | ||
valid(callback: (product: IStoreProduct) => void): IWhen; | ||
} | ||
@@ -64,3 +76,23 @@ | ||
ERR_CLIENT_INVALID: number; | ||
ERR_PAYMENT_CANCELLED: number; | ||
ERR_PAYMENT_INVALID: number; | ||
ERR_PAYMENT_NOT_ALLOWED: number; | ||
ERR_UNKNOWN: number; | ||
ERR_REFRESH_RECEIPTS: number; | ||
ERR_INVALID_PRODUCT_ID: number; | ||
ERR_FINISH: number; | ||
ERR_COMMUNICATION: number; | ||
ERR_SUBSCRIPTIONS_NOT_AVAILABLE: number; | ||
ERR_MISSING_TOKEN: number; | ||
ERR_VERIFICATION_FAILED: number; | ||
ERR_BAD_RESPONSE: number; | ||
ERR_REFRESH: number; | ||
ERR_PAYMENT_EXPIRED: number; | ||
ERR_DOWNLOAD: number; | ||
ERR_SUBSCRIPTION_UPDATE_NOT_AVAILABLE: number; | ||
INVALID_PAYLOAD: number; | ||
CONNECTION_FAILED: number; | ||
PURCHASE_EXPIRED: number; | ||
verbosity: number | boolean; | ||
@@ -79,6 +111,6 @@ validator: string | IValidator; | ||
off(callback: Function): void; | ||
order(id: string): void; | ||
order(id: string, additionalData?: null | { oldPurchasedSkus: string[] } | { developerPayload: string }): void; | ||
} | ||
export type TransactionType = 'ios-appstore' | 'android-playstore'; | ||
export type TransactionType = 'ios-appstore' | 'android-playstore' | 'windows-store-transaction'; | ||
export type StoreProductState = | ||
@@ -131,3 +163,21 @@ 'approved' | | ||
verify: () => void; | ||
countryCode: string; | ||
additionalData: any; | ||
priceMicros: number; | ||
} | ||
} | ||
// For backward compatibility prior v7.1.4. | ||
declare namespace store { | ||
export type StoreProductType = IapStore.StoreProductType; | ||
export type IError = IapStore.IError; | ||
export type IWhen = IapStore.IWhen; | ||
export type IValidatorCallback = IapStore.IValidatorCallback; | ||
export type IValidator = IapStore.IValidator; | ||
export type IStore = IapStore.IStore; | ||
export type TransactionType = IapStore.TransactionType; | ||
export type StoreProductState = IapStore.StoreProductState; | ||
export type ITransaction = IapStore.ITransaction; | ||
export type IRegisterRequest = IapStore.IRegisterRequest; | ||
export type IStoreProduct = IapStore.IStoreProduct; | ||
} |
@@ -41,8 +41,2 @@ /*global storekit */ | ||
} | ||
// User will enter its password, so let's refresh the receipt. | ||
store._latest_receipt = null; | ||
storekit.setAppStoreReceipt(null); | ||
// And initiate the purchase | ||
storekit.purchase(product.id, 1); | ||
@@ -215,15 +209,4 @@ }); | ||
products.push(store.products[i].id); | ||
// refresh receipts | ||
if (!storekit.appStoreReceipt) { | ||
storekit.refreshReceipts(function (data) { | ||
storekitSetApplicationData(data); | ||
store.log.debug("ios -> loading products"); | ||
storekit.load(products, storekitLoaded, storekitLoadFailed); | ||
}); | ||
} | ||
else { | ||
store.log.debug("ios -> loading products"); | ||
storekit.load(products, storekitLoaded, storekitLoadFailed); | ||
} | ||
store.log.debug("ios -> loading products"); | ||
storekit.load(products, storekitLoaded, storekitLoadFailed); | ||
} | ||
@@ -247,15 +230,9 @@ | ||
store.log.debug("ios -> owned? " + p.owned); | ||
var v = validProducts[i]; | ||
p.set({ | ||
title: v.title, | ||
description: v.description, | ||
price: v.price, | ||
priceMicros: v.priceMicros, | ||
currency: v.currency, | ||
countryCode: v.countryCode, | ||
introPrice: v.introPrice, | ||
introPriceMicros: v.introPriceMicros, | ||
introPriceNumberOfPeriods: v.introPriceNumberOfPeriods, | ||
introPriceSubscriptionPeriod: v.introPriceSubscriptionPeriod, | ||
introPricePaymentMode: v.introPricePaymentMode, | ||
title: validProducts[i].title, | ||
price: validProducts[i].price, | ||
priceMicros: validProducts[i].priceMicros, | ||
description: validProducts[i].description, | ||
currency: validProducts[i].currency, | ||
countryCode: validProducts[i].countryCode, | ||
state: store.VALID | ||
@@ -309,13 +286,5 @@ }); | ||
if (store._latest_receipt) { | ||
refreshing = false; | ||
storekit.setAppStoreReceipt(store._latest_receipt); | ||
callCallbacks(); | ||
return; | ||
} | ||
storekit.refreshReceipts(function(data) { | ||
storekit.refreshReceipts(function() { | ||
// success | ||
refreshing = false; | ||
storekitSetApplicationData(data); | ||
callCallbacks(); | ||
@@ -330,2 +299,5 @@ }, | ||
// The better default is now for validation services to use the | ||
// `latest_receipt_info` field. If if doesn't we can ask the user to implement | ||
// the below: | ||
// store.when("expired", function() { | ||
@@ -463,27 +435,2 @@ // storekitRefreshReceipts(); | ||
function storekitSetApplicationData(data) { | ||
// Why create a product whose ID equals the application bundle ID? | ||
// Is allows to trigger a validation of the appStoreReceipt. | ||
if (data) { | ||
var p = data.bundleIdentifier ? store.get(data.bundleIdentifier) : null; | ||
if (!p) { | ||
p = new store.Product({ | ||
id: data.bundleIdentifier || "application data", | ||
alias: "application data", | ||
type: store.NON_CONSUMABLE | ||
}); | ||
store.register(p); | ||
} | ||
p.version = data.bundleShortVersion; | ||
p.transaction = { | ||
type: 'ios-appstore', | ||
appStoreReceipt: data.appStoreReceipt, | ||
signature: data.signature | ||
}; | ||
p.trigger("loaded"); | ||
p.set('state', store.APPROVED); | ||
} | ||
} | ||
// Restore purchases. | ||
@@ -494,7 +441,26 @@ // store.restore = function() { | ||
storekit.restore(); | ||
// User has entered his password, so let's refresh the receipt. | ||
store._latest_receipt = null; | ||
storekit.setAppStoreReceipt(null); | ||
storekit.refreshReceipts(storekitSetApplicationData); | ||
storekit.refreshReceipts(function(data) { | ||
// What the point of this? | ||
// Why create a product whose ID equals the application bundle ID (?) | ||
// Is it just to trigger force a validation of the appStoreReceipt? | ||
if (data) { | ||
var p = data.bundleIdentifier ? store.get(data.bundleIdentifier) : null; | ||
if (!p) { | ||
p = new store.Product({ | ||
id: data.bundleIdentifier || "application data", | ||
alias: "application data", | ||
type: store.NON_CONSUMABLE | ||
}); | ||
store.register(p); | ||
} | ||
p.version = data.bundleShortVersion; | ||
p.transaction = { | ||
type: 'ios-appstore', | ||
appStoreReceipt: data.appStoreReceipt, | ||
signature: data.signature | ||
}; | ||
p.trigger("loaded"); | ||
p.set('state', store.APPROVED); | ||
} | ||
}); | ||
}); | ||
@@ -555,11 +521,29 @@ | ||
store._prepareForValidation = function(product, callback) { | ||
storekit.loadReceipts(function(r) { | ||
if (!product.transaction) { | ||
product.transaction = { | ||
type: 'ios-appstore' | ||
}; | ||
} | ||
product.transaction.appStoreReceipt = r.appStoreReceipt; | ||
callback(); | ||
}); | ||
var nRetry = 0; | ||
function loadReceipts() { | ||
storekit.setAppStoreReceipt(null); | ||
storekit.loadReceipts(function(r) { | ||
if (!product.transaction) { | ||
product.transaction = { | ||
type: 'ios-appstore' | ||
}; | ||
} | ||
product.transaction.appStoreReceipt = r.appStoreReceipt; | ||
if (product.transaction.id) | ||
product.transaction.transactionReceipt = r.forTransaction(product.transaction.id); | ||
if (!product.transaction.appStoreReceipt && !product.transaction.transactionReceipt) { | ||
nRetry ++; | ||
if (nRetry < 2) { | ||
setTimeout(loadReceipts, 500); | ||
return; | ||
} | ||
else if (nRetry === 2) { | ||
storekit.refreshReceipts(loadReceipts); | ||
return; | ||
} | ||
} | ||
callback(); | ||
}); | ||
} | ||
loadReceipts(); | ||
}; | ||
@@ -566,0 +550,0 @@ |
@@ -428,7 +428,4 @@ /** | ||
var that = this; | ||
var handled = false; | ||
var loaded = function (args) { | ||
if (handled) return; | ||
handled = true; | ||
var base64 = args[0]; | ||
@@ -453,4 +450,2 @@ var bundleIdentifier = args[1]; | ||
var error = function(errMessage) { | ||
if (handled) return; | ||
handled = true; | ||
log('refresh receipt failed: ' + errMessage); | ||
@@ -461,20 +456,4 @@ protectCall(that.options.error, 'options.error', InAppPurchase.prototype.ERR_REFRESH_RECEIPTS, 'Failed to refresh receipt: ' + errMessage); | ||
var timeout = function() { | ||
if (handled) return; | ||
// Why a double timeout? | ||
// Because the first one can be "stuck" to until the native UI is done | ||
// doing its job. If that happens, it'll get call right when the javascript | ||
// engine recovers control, and as such gets called before the | ||
// `loaded`/`error` handlers. | ||
setTimeout(function() { | ||
if (handled) return; | ||
log('timeout... re-refreshing appStoreReceipt'); | ||
exec('appStoreRefreshReceipt', [], loaded, error); | ||
setTimeout(timeout, 60000); | ||
}, 1000); | ||
}; | ||
log('refreshing appStoreReceipt'); | ||
exec('appStoreRefreshReceipt', [], loaded, error); | ||
setTimeout(timeout, 60000); | ||
}; | ||
@@ -523,7 +502,4 @@ | ||
this.appStoreReceipt = base64; | ||
log('storing appStoreReceipt'); | ||
if (window.localStorage) { | ||
if (window.localStorage && base64) { | ||
window.localStorage.sk_appStoreReceipt = base64; | ||
window.localStorage.sk_appStoreReceiptDate = '' + (+new Date()); | ||
log('storing appStoreReceipt: ' + window.localStorage.sk_appStoreReceiptDate); | ||
} | ||
@@ -534,9 +510,2 @@ }; | ||
this.appStoreReceipt = window.localStorage.sk_appStoreReceipt; | ||
var t = parseInt(window.localStorage.sk_appStoreReceiptDate); | ||
var hoursAgo = Math.round((new Date() - t) / 360000) / 10; | ||
log('appStoreReceipt stored: ' + window.localStorage.sk_appStoreReceiptDate); | ||
log('appStoreReceipt has been refreshed ' + hoursAgo + ' hours ago'); | ||
// reset "appStoreReceipt" every week | ||
if ((new Date() - t) > 7 * 24 * 3600000) | ||
this.appStoreReceipt = null; | ||
} | ||
@@ -543,0 +512,0 @@ if (this.appStoreReceipt === 'null') |
@@ -46,3 +46,3 @@ (function() { | ||
/// - `product.priceMicros` - Price in micro-units (divide by 1000000 to get numeric price) | ||
/// - `product.priceMicros` - Localized price, in micro-units (divide by 1000000 to get numeric price) | ||
this.priceMicros = options.priceMicros || null; | ||
@@ -59,22 +59,2 @@ | ||
/// - `product.introPrice` - Localized introductory price, with currency symbol. Available only on iOS | ||
this.introPrice = options.introPrice || null; | ||
/// - `product.introPriceMicros` - Introductory price in micro-units (divide by 1000000 to get numeric price). Available only on iOS | ||
this.introPriceMicros = options.introPriceMicros || null; | ||
/// - `product.introPriceNumberOfPeriods` - number of periods the introductory price is available. Available only on iOS | ||
this.introPriceNumberOfPeriods = options.introPriceNumberOfPeriods || null; | ||
/// - `product.introPriceSubscriptionPeriod` - Period for the introductory price ("Day", "Week", "Month" or "Year"). Available only on iOS | ||
this.introPriceSubscriptionPeriod = options.introPriceSubscriptionPeriod || null; | ||
/// - `product.introPricePaymentMode` - Payment mode for the introductory price ("PayAsYouGo", "UpFront", or "FreeTrial"). Available only on iOS | ||
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. Available only on iOS | ||
this.ineligibleForIntroPrice = options.ineligibleForIntroPrice || null; | ||
// - `product.localizedTitle` - Localized name or short description ready for display | ||
@@ -178,2 +158,8 @@ // this.localizedTitle = options.localizedTitle || options.title || null; | ||
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 | ||
@@ -185,14 +171,16 @@ // It means it already has been | ||
store._validator(that, function(success, data) { | ||
store.log.debug("verify -> " + JSON.stringify(success)); | ||
// Update the appStoreReceipt | ||
if (data && data.latest_receipt) | ||
store._latest_receipt = data.latest_receipt; | ||
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); | ||
that.trigger("updated"); | ||
} | ||
if (success) { | ||
if (that.expired) | ||
that.set("expired", false); | ||
if (data.transaction) | ||
that.transaction = Object.assign(that.transaction || {}, | ||
data.transaction); | ||
store.log.debug("verify -> success: " + JSON.stringify(data)); | ||
@@ -202,17 +190,5 @@ store.utils.callExternal('verify.success', successCb, that, data); | ||
that.trigger("verified"); | ||
// Process the list of products that are ineligible | ||
// for introductory prices. | ||
if (data && data.ineligible_for_intro_price && | ||
data.ineligible_for_intro_price.forEach) { | ||
data.ineligible_for_intro_price.forEach(function(pid) { | ||
var p = store.get(pid); | ||
if (p) | ||
p.set('ineligibleForIntroPrice', true); | ||
}); | ||
} | ||
} | ||
else { | ||
store.log.debug("verify -> error: " + JSON.stringify(data)); | ||
if (!data) data = {}; | ||
var msg = (data && data.error && data.error.message ? data.error.message : ''); | ||
@@ -223,2 +199,8 @@ var err = new store.Error({ | ||
}); | ||
if (getData(data, "latest_receipt")) { | ||
// when the server is making use of the latest_receipt, | ||
// there is no need to retry | ||
store.log.debug("verify -> server did use the latest_receipt, no retries"); | ||
nRetry = 999999; | ||
} | ||
if (data.code === store.PURCHASE_EXPIRED) { | ||
@@ -253,3 +235,3 @@ err = new store.Error({ | ||
else { | ||
store.log.debug("validation failed 5 times, stop retrying, trigger an error"); | ||
store.log.debug("validation failed, no retrying, trigger an error"); | ||
store.error(err); | ||
@@ -256,0 +238,0 @@ store.utils.callExternal('verify.error', errorCb, err); |
@@ -41,8 +41,2 @@ (function() { | ||
// In order not to send big batch of identical requests, we'll | ||
// add verification requests to this list, then process them a little | ||
// moment later. | ||
store._validatorPendingCallbacks = {}; | ||
store._validatorTimer = null; | ||
// | ||
@@ -57,69 +51,34 @@ // ## store._validator | ||
store._validator = function(product, callback, isPrepared) { | ||
if (!store.validator) { | ||
callback(true, product); | ||
return; | ||
} | ||
// Add the callback to the list of pending ones for this product | ||
if (!store._validatorPendingCallbacks[product.id]) | ||
store._validatorPendingCallbacks[product.id] = []; | ||
store._validatorPendingCallbacks[product.id].push(callback); | ||
// (Re)set a timeout to call the actual validation in 500ms | ||
// for all products at once | ||
if (store._validatorTimer) clearTimeout(store._validatorTimer); | ||
store._validatorTimer = setTimeout(go.bind(store, false, null), 500); | ||
// Start the validation process | ||
function go(isPrepared, pendingCallbacks) { | ||
// Clear the timer and retrieve the list ofpending callbacks | ||
if (!isPrepared) { | ||
store._validatorTimer = null; | ||
pendingCallbacks = store._validatorPendingCallbacks; | ||
store._validatorPendingCallbacks = {}; | ||
} | ||
// Process all products in series (synchronously) | ||
var productId = Object.keys(pendingCallbacks)[0]; | ||
if (productId) { | ||
if (store._prepareForValidation) | ||
store._prepareForValidation(store.get(productId), callValidate); | ||
else | ||
callValidate(); | ||
} | ||
function callValidate() { | ||
var product = store.get(productId); | ||
validate(product, function(success, data) { | ||
// done validating, process the next product | ||
pendingCallbacks[productId].forEach(function(callback) { | ||
callback(success, data); | ||
}); | ||
delete pendingCallbacks[productId]; | ||
go(true, pendingCallbacks); | ||
}); | ||
} | ||
if (store._prepareForValidation && isPrepared !== true) { | ||
store._prepareForValidation(product, function() { | ||
store._validator(product, callback, true); | ||
}); | ||
return; | ||
} | ||
function validate(product, callback) { | ||
if (!store.validator) | ||
callback(true, product); | ||
if (typeof store.validator === 'string') { | ||
store.utils.ajax({ | ||
url: store.validator, | ||
method: 'POST', | ||
data: product, | ||
success: function(data) { | ||
store.log.debug("validator success, response: " + JSON.stringify(data)); | ||
callback(data && data.ok, data.data); | ||
}, | ||
error: function(status, message, data) { | ||
var fullMessage = "Error " + status + ": " + message; | ||
store.log.debug("validator failed, response: " + JSON.stringify(fullMessage)); | ||
store.log.debug("body => " + JSON.stringify(data)); | ||
callback(false, fullMessage); | ||
} | ||
}); | ||
} | ||
else { | ||
store.validator(product, callback); | ||
} | ||
if (typeof store.validator === 'string') { | ||
store.utils.ajax({ | ||
url: store.validator, | ||
method: 'POST', | ||
data: product, | ||
success: function(data) { | ||
store.log.debug("validator success, response: " + JSON.stringify(data)); | ||
callback(data && data.ok, data.data); | ||
}, | ||
error: function(status, message, data) { | ||
var fullMessage = "Error " + status + ": " + message; | ||
store.log.debug("validator failed, response: " + JSON.stringify(fullMessage)); | ||
store.log.debug("body => " + JSON.stringify(data)); | ||
callback(false, fullMessage); | ||
} | ||
}); | ||
} | ||
else { | ||
store.validator(product, callback); | ||
} | ||
}; | ||
@@ -126,0 +85,0 @@ |
@@ -56,5 +56,3 @@ /*eslint-env mocha */ | ||
} | ||
}, | ||
loadAppStoreReceipt: function() { | ||
}, | ||
} | ||
}; | ||
@@ -91,3 +89,3 @@ })(); | ||
assert.equal(1, storekit.initCalled | 0); | ||
assert.equal(1, storekit.initCalled); | ||
assert.equal(false, storekit.initialized); | ||
@@ -103,3 +101,3 @@ | ||
assert.equal(true, storekit.initialized); | ||
assert.equal(1, storekit.loadCalled | 0); | ||
assert.equal(1, storekit.loadCalled); | ||
assert.equal(false, storekit.loaded); | ||
@@ -106,0 +104,0 @@ assert.equal(false, store.ready(), "store shouldn't be ready after failed load"); |
@@ -78,3 +78,3 @@ /*eslint-env mocha */ | ||
done(); | ||
}, 5000); | ||
}, 1500); | ||
}; | ||
@@ -81,0 +81,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
0
814654
93
14644