When using in-app billing, you should verify the purchase data, by checking if the INAPP_PURCHASE_DATA was signed with the INAPP_DATA_SIGNATURE by using the base64 encoded public key from Google Play store.
See the explanation of INAPP_PURCHASE_DATA and INAPP_DATA_SIGNATURE here.
There is a Security class you can use to verify the purchase:
public class Security {
private static final String TAG = "IABUtil/Security";
private static final String KEY_FACTORY_ALGORITHM = "RSA";
private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
/**
* Verifies that the data was signed with the given signature, and returns
* the verified purchase. The data is in JSON format and signed
* with a private key. The data also contains the {#link PurchaseState}
* and product ID of the purchase.
* #param base64PublicKey the base64-encoded public key to use for verifying.
* #param signedData the signed JSON string (signed, not encrypted)
* #param signature the signature for the data, signed with the private key
*/
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
TextUtils.isEmpty(signature)) {
Log.e(TAG, "Purchase verification failed: missing data.");
return false;
}
PublicKey key = Security.generatePublicKey(base64PublicKey);
return Security.verify(key, signedData, signature);
}
/**
* Generates a PublicKey instance from a string containing the
* Base64-encoded public key.
*
* #param encodedPublicKey Base64-encoded public key
* #throws IllegalArgumentException if encodedPublicKey is invalid
*/
public static PublicKey generatePublicKey(String encodedPublicKey) {
try {
byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT);
KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (InvalidKeySpecException e) {
Log.e(TAG, "Invalid key specification.");
throw new IllegalArgumentException(e);
}
}
/**
* Verifies that the signature from the server matches the computed
* signature on the data. Returns true if the data is correctly signed.
*
* #param publicKey public key associated with the developer account
* #param signedData signed data from server
* #param signature server signature
* #return true if the data and signature match
*/
public static boolean verify(PublicKey publicKey, String signedData, String signature) {
byte[] signatureBytes;
try {
signatureBytes = Base64.decode(signature, Base64.DEFAULT);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Base64 decoding failed.");
return false;
}
try {
Signature sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(signatureBytes)) {
Log.e(TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "NoSuchAlgorithmException.");
} catch (InvalidKeyException e) {
Log.e(TAG, "Invalid key specification.");
} catch (SignatureException e) {
Log.e(TAG, "Signature exception.");
}
return false;
}
}
You have to invoke the verifyPurchase and pass the purchase data, the given signature and the base64PublicKey public key from Google Play. There is a wrapper implementation I can use for the purchase flow in my app.
If you look into the IabHelper implementation, they pass the public key for the verification in constructor. The documentation of the ctor says:
* #param base64PublicKey Your application's public key, encoded in base64.
* This is used for verification of purchase signatures. You can find your app's base64-encoded
* public key in your application's page on Google Play Developer Console. Note that this
* is NOT your "developer public key".
*/
I guess they mean the Base64-encoded RSA public key in Licensing & in-app billing section in Google Play:
Maybe I don't know enough about cryptography but how is this possible that I use a public key from Google Play to check an encryption which is supposedly made with my "developers private key" (see explanation in first link). Do they mean my "private key I used to sign the app"? I don't think so, because they cannot know my (local) private key (I use to sign my app) and what does it have to do with this public key from Google Play, so what do they mean with "developer's private key".
So my questions are:
Did I understand it right that the public key is the key from
Licensing & in-app billing?
Do I also need to add licensing to my app to get this
verification working or should this work "out of the box", so can I
omit this step?
What is the "developer's private key" Google is using to sign the
purchase data and where do I see it? (I need to run some Unit tests
on my server to check my implementation and I want to encrypt the
INAPP_PURCHASE_DATA to get the INAPP_DATA_SIGNATURE as well to be
able to get a valid security check if I verify it with the given
public key.
[UPDATE]. Obviously, the private key is hidden:
The Google Play Console exposes the public key for licensing to any developer signed in to the Play Console, but it keeps the private key hidden from all users in a secure location.
See: https://developer.android.com/google/play/licensing/adding-licensing.html
There are two types of signature asymmetric & symmetric :
Asymm uses a pair of keys, the private and the public, the keys have a mathematical relationship between them, one chunk of data signed with the private key can be verified with the public one. The private key is never published, but the public is.
Then Google created a pair of keys for your in-app billing ... but you only need to know the public to verify.
No body will generate a valid signature without the private key.
Instead Symm uses the same key in both sides, that poses the problem to share the key with the risk to be sniffed, but it has the advantage to be faster than asymm.
UPDATE
Do I also need to add licensing to my app to get this verification
working or should this work "out of the box", so can I omit this step?
Depends, if you want to know if the app has been installed from the official Google Play Store then you need verify licensing, that applies better if your app is a paid app, instead if your app is free but it has in-app products the important thing is to know if they purchased legally the item.
For me it is more important to verify purchases in a external server, there you have a nice example https://stackoverflow.com/a/48645216/7690376
I am trying to get expiry time or status of subscription to ensure if user is paying regularly for my item or not . When i query using
Purchase monthlySubscription = inv.getPurchase("itemName");
or
ArrayList<String> ownedSkus = ownedItems.getStringArrayList("INAPP_PURCHASE_ITEM_LIST");
It returns following data
{
"packageName":"com.abcPackage",
"productId":"auto1week",
"purchaseTime":1453369299644,
"purchaseState":0,
"developerPayload":"PAY_LOAD",
"purchaseToken":"TOKEN",
"autoRenewing":true
}
The problem is , purchaseTime remains same after several weeks which is supposed to be change after every purchase.
I tried google Play developers API
https://developers.google.com/android-publisher/#subscriptions
but i am having a hard time implementing it on my android device .
I will be grateful if someone can guide me step by step process to get this data on android device.
https://developers.google.com/android-publisher/api-ref/purchases/subscriptions
Any help in this regard will be highly appreciated.
Not sure if this will help, but here is my server side code (in java) that connects to the developer API and returns the expiration of the subscription.
I created a Service Account in the Google Developer Console, and followed the somewhat obtuse instructions to create a key file in src/resources/keys.json. APPLICATION_NAME is the package name of my app, and PRODUCT_ID is the subscription ID from the Google PLAY developer console.
Sorry it's not really 'step by step' as you asked for, but I also am doing verification on the server side instead of on the client. I suppose on the client you could do some sort of soft-verification by checking purchaseState == 0 (1=cancelled, 2=refunded), and autoRenewing==true. You may get stuck there if they cancel though, since you are still supposed to provide service through the duration of the subscription.
public static Long doSomeWork(String token){
log.debug("Google Validation: Doing some work:" + token);
try{
// Creating new Trusted Transport
HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
// Getting Auth Creds
Credential cred = getAuthCredential();
// Building Android Publisher API call
AndroidPublisher ap = new AndroidPublisher.Builder(httpTransport, JSON_FACTORY, cred)
.setApplicationName(APPLICATION_NAME).build();
// Get Subscription
AndroidPublisher.Purchases.Subscriptions.Get get = ap.purchases().subscriptions().get(
APPLICATION_NAME,
PRODUCT_ID,
token);
SubscriptionPurchase subscription = get.execute();
log.debug(subscription.toPrettyString());
log.debug("DONE (not null)");
return subscription.getExpiryTimeMillis();
} catch(IOException ex) {
ex.printStackTrace();
} catch (GeneralSecurityException ex2) {
ex2.printStackTrace();
}
log.debug("DONE (failure) (0)");
return 0L;
}
private static Credential getAuthCredential(){
log.debug("getAuthCredential");
try{
//Read the credentials from the keys file. This file is obtained from the
// Google Developer Console (not the Play Developer Console
InputStream is = GoogleReceiptValidation.class.getClassLoader().getResourceAsStream("keys.json");
String str = IOUtils.toString(is);
is.close();
JSONObject obj = new JSONObject(str);
InputStream stream = new ByteArrayInputStream(obj.toString().getBytes());
//This is apparently "beta functionality".
GoogleCredential creds = GoogleCredential.fromStream(stream);
creds = creds.createScoped(Collections.singleton(AndroidPublisherScopes.ANDROIDPUBLISHER));
return creds;
} catch (IOException ex){
ex.printStackTrace();
} catch (JSONException ex2){
ex2.printStackTrace();
}
log.debug("No Creds found - returning null");
return null;
}
I am having trouble with license testing my in-app products. I have an app published to the Google Play beta channel, and I have my relevant Google account listed as a license tester in the dev console. (Meaning I can make "purchases" without actually paying for the item(s).)
When I visit the in-app store for the first time, everything works fine. However, upon "purchasing" an item, I receive a signature verification error in the purchase flow response. From that point forward, I also receive the same signature error when querying for the store inventory.
I need help with this part. I have seen posts stating that the method verifyPurchase within Security.java is to blame. Those other posts state the issue is with android.test.purchased returning an empty String for the signature. What I'm seeing is different. My call passes the isEmpty(signature) test, but then is rejected later in the code. Why is Google In-App Billing returning a signature that is invalid? I have included the relevant code below.
public class Security {
// ...
// This method is part of the trace from
// IabHelper.queryInventory and IabHelper.onActivityResult
public static boolean verifyPurchase(String base64PublicKey, String signedData,
String signature) {
if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
TextUtils.isEmpty(signature)) { // Most say their problem is here
Log.e(TAG, "Purchase verification failed: missing data.");
return false;
}
PublicKey key = Security.generatePublicKey(base64PublicKey);
return Security.verify(key, signedData, signature); // My code gets here, but...
}
public static boolean verify(PublicKey publicKey, String signedData,
String signature) {
Signature sig;
try {
sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig = initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(Base64.decode(signature))) { // ...verify fails; return false
Log.e(TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "NoSuchAlgorithmException.");
} catch (InvalidKeyException e) {
Log.e(TAG, "Invalid key specification.");
} catch (SignatureException e) {
Log.e(TAG, "Signature exception.");
} catch (Base64DecoderException e) {
Log.e(TAG, "Base64 decoding failed.");
}
return false;
}
}
Update: As I mentioned in my comment, I was running a signed APK, with the debuggable flag set to true. I have now tried making an actual purchase, with a real credit card; I saw the exact same results. Even though the Google Play purchase flow completed as expected, I did not actually receive my product. Upon returning to the store, Google Play did not return the in-app inventory.
My problem actually had nothing to do with the Google Play In-App Billing Java code. I was reading the wrong license key for my app. My project has several different flavors. When this particular flavor was added, the file that contains the license key was copied from a different flavor and never changed. Now that I have corrected the key, everything is working as intended.
TL;DR: Verify that your Google Play license key is correct.
I am using this as my request url:
`String isbnUrl = "https://www.googleapis.com/books/v1/volumes?q=isbn:" + isbn + "&key=" + myAPIKEY;`
Can anyone tell me why I keep getting this response:
{
"error":{
"errors":[
{
"domain":"usageLimits",
"reason":"ipRefererBlocked",
"message":"There is a per-IP or per-Referer restriction configured on your API key and the request does not match these restrictions. Please use the Google Developers Console to update your API key configuration if request from this IP or referer should be allowed.",
"extendedHelp":"https://console.developers.google.com"
}
],
"code":403,
"message":"There is a per-IP or per-Referer restriction configured on your API key and the request does not match these restrictions. Please use the Google Developers Console to update your API key configuration if request from this IP or referer should be allowed."
}
}
I have gone through the process of getting an API for my Android app using the debug keystore and release keystore and can't seem to get it to work I have tried adding my key as a header as suggested as an answer here: Google Books API 403 Access Not Configured.
I thought this was the answer but then realized by accident that it was the same as not supplying a key at all. I came to this realization after entering the wrong String as the key and it still worked.
In the developer console I am seeing that it receives the request from my API under usage response code section: Client errors (4xx).
I would really appreciate any help if anyone has figured out how to get this API to work the way Google wants by including the key.
Problem is when setting up your API key restriction for android app, you specified the package name and SHA-1 certificate fingerprint. Therefore your API key will only accept request from your app with package name and SHA-1 certificate fingerprint specified.
So when you send an request to Google, you MUST add these information in the header of each request with following keys:
Key: "X-Android-Package", value: your app package name
Key: "X-Android-Cert", value: SHA-1 certificate of your apk
FIRST, get your app SHA signature (you will need Guava library):
/**
* Gets the SHA1 signature, hex encoded for inclusion with Google Cloud Platform API requests
*
* #param packageName Identifies the APK whose signature should be extracted.
* #return a lowercase, hex-encoded
*/
public static String getSignature(#NonNull PackageManager pm, #NonNull String packageName) {
try {
PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
if (packageInfo == null
|| packageInfo.signatures == null
|| packageInfo.signatures.length == 0
|| packageInfo.signatures[0] == null) {
return null;
}
return signatureDigest(packageInfo.signatures[0]);
} catch (PackageManager.NameNotFoundException e) {
return null;
}
}
private static String signatureDigest(Signature sig) {
byte[] signature = sig.toByteArray();
try {
MessageDigest md = MessageDigest.getInstance("SHA1");
byte[] digest = md.digest(signature);
return BaseEncoding.base16().lowerCase().encode(digest);
} catch (NoSuchAlgorithmException e) {
return null;
}
}
Then, add package name and SHA certificate signature to request header:
java.net.URL url = new URL(REQUEST_URL);
HttpURLConnection connection = (HttpURLConnection)url.openConnection();
try {
connection.setDoInput(true);
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
connection.setRequestProperty("Accept", "application/json");
// add package name to request header
String packageName = mActivity.getPackageName();
connection.setRequestProperty("X-Android-Package", packageName);
// add SHA certificate to request header
String sig = getSignature(mActivity.getPackageManager(), packageName);
connection.setRequestProperty("X-Android-Cert", sig);
connection.setRequestMethod("POST");
// ADD YOUR REQUEST BODY HERE
// ....................
} catch (Exception e) {
e.printStackTrace();
} finally {
connection.disconnect();
}
You can see full answer here.
Enjoy coding :D
I have tried for several days to solve this problem, using the Dungeons demo code that comes with the SDK. I've tried to Google for an answer but can't find one.
In the Dungeons demo, I passed my public key from the dev console.
Signed the apk and uploaded to console without publish.
Testing for both android.test.purchased & product list created on console with published for subscription (The main feature I want for my app).
But still I get an error of Signature verification failed and then the signature does not match data. How can I solve this?
public static ArrayList<VerifiedPurchase> verifyPurchase(String signedData, String signature)
{
if (signedData == null) {
Log.e(TAG, "data is null");
return null;
}
if (Consts.DEBUG) {
Log.i(TAG, "signedData: " + signedData);
}
boolean verified = false;
if (!TextUtils.isEmpty(signature)) {
String base64EncodedPublicKey = "MIIBIjA....AQAB";
PublicKey key = Security.generatePublicKey(base64EncodedPublicKey);
verified = Security.verify(key, signedData, signature);
if (!verified) {
Log.w(TAG, "signature does not match data.");
return null;
}
}
}
public static boolean verify(PublicKey publicKey, String signedData, String signature)
{
if (Consts.DEBUG) {
Log.i(TAG, "signature: " + signature);
}
Signature sig;
try {
sig = Signature.getInstance(SIGNATURE_ALGORITHM);
sig.initVerify(publicKey);
sig.update(signedData.getBytes());
if (!sig.verify(Base64.decode(signature))) {
Log.e(TAG, "Signature verification failed.");
return false;
}
return true;
} catch (NoSuchAlgorithmException e) {
Log.e(TAG, "NoSuchAlgorithmException.");
} catch (InvalidKeyException e) {
Log.e(TAG, "Invalid key specification.");
} catch (SignatureException e) {
Log.e(TAG, "Signature exception.");
} catch (Base64DecoderException e) {
Log.e(TAG, "Base64 decoding failed.");
}
return false;
}
This problem is still going on in the current Google billing version. Basically the android.test.purchased is broken; After you buy android.test.purchased the verifyPurchase function in Security.java will always fail and the QueryInventoryFinishedListener will stop at the line if (result.isFailure()); this is because the android.test.purchased item always fails the TextUtils.isEmpty(signature) check in Security.java as it is not a real item and has no signature returned by the server.
My advice (from lack of any other solution) is to NEVER use "android.test.purchased". There are various code tweaks on the net but none of them work 100%.
If you have used the android.test.purchased then one way to get rid of the error is to do the following:-
Edit Security.java and change the "return false" line in the verifyPurchase to "return true" - this is temporary, we'll be putting it back in a minute.
In your QueryInventoryFinishedListener, after the "if (result.isFailure()) {...}" lines add the following to consume and get rid of your never ending android.test.purchased item:
if (inventory.hasPurchase(SKU_ANDROID_TEST_PURCHASE_GOOD)) {
mHelper.consumeAsync(inventory.getPurchase(SKU_ANDROID_TEST_PURCHASE_GOOD),null);
}
Run your app so the consunmeAsync happens, this gets rid of the "android.test.purchased" item on the server.
Remove the consumeAsync code (or comment it out).
Back in the Security.java, change the "return true" back to "return false".
Your QueryInventoryFinishedListener will no longer error on the verify, everything is back to "normal" (if you can call it that). Remember - don't bother using android.test.purchased again as it will just cause this error again... it's broke! The only real way to test your purchasing it to upload an APK, wait for it to appear, and then test it (the same APK) on your device with logging enabled.
Yes, the problem still occurs.
After I bought android.test.purchased I start getting the error on quering the inventory.
It is possible to fix your phone by just clearing data of Google Play Store application and running Google Play one time.
When you clear data of Google Play it forgets that you bought android.test.purchased
Please check that base64EncodedPublicKey and the one from the Play Developer Console are equal.
Once you re-upload the APK in the Developer Console, the public key may change, if so update your base64EncodedPublicKey.
You can skip the verifying process for those "android.test.*" product ids. If you are using the sample code from the TrivialDrive example, open IabHelper.java, find the following line code, change it from
if (Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) { ... }
into
boolean verifySignature = !sku.startsWith("android.test."); // or inplace the condition in the following line
if (verifySignature && !Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) { ... }
It's harmless, even if you forgot to rollback the code. So, you can continue to test the further workflow step.
Based on GMTDev's answer, this is what I do in order to fix the testing issues when consuming products in the simplest possible way. In Security.java, replace the verifyPurchase() method with this:
public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
TextUtils.isEmpty(signature)) {
Log.e(TAG, "Purchase verification failed: missing data.");
return BuildConfig.DEBUG; // Line modified by Cristian. Original line was: return false;
}
PublicKey key = Security.generatePublicKey(base64PublicKey);
return Security.verify(key, signedData, signature);
}
I only modified one line (see comment), and this way you can keep the code like that for debugging and still publish your release versions safely.
The error is caused because of the wrong license key. Maybe the license key is probably from your another app.
The solution is to use the proper license key from :
Play console > App > Development Tools > Licensing & in-app billing
What worked for me, while using In-app Billing v3 and the included utility classes, was consuming the test purchase within the returned onActivityResult call.
No changes to IabHelper, Security, or any of the In-app Billing util classes are needed to avoid this for future test purchases.
If you have already tried purchasing the test product and are now stuck on the purchase signature verification failed error, which you likely are since you are looking up answers for this error, then you should:
make the changes that GMTDev recommended
run the app to ensure that it consumes the product
remove/undo GMTDev's changes
implement the code below within onActivityResult.
Not only does this allow for the purchase testing process to be fluid but this should also avoid any conflicting issues with iab returning the " Item Already Owned " error when attempting to repurchase the test product.
If this is being called from within a fragment and your fragment's onActivityResult isn't being called then be sure to call YourFragmentName.onActivityResult(requestCode, resultCode, data) from your parent ActivityFragment if necessary. This is explained in more detail in Calling startIntentSenderForResult from Fragment (Android Billing v3).
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_PURCHASE) {
//this ensures that the mHelper.flagEndAsync() gets called
//prior to starting a new async request.
mHelper.handleActivityResult(requestCode, resultCode, data);
//get needed data from Intent extra to recreate product object
int responseCode = data.getIntExtra("RESPONSE_CODE", 0);
String purchaseData = data.getStringExtra("INAPP_PURCHASE_DATA");
String dataSignature = data.getStringExtra("INAPP_DATA_SIGNATURE");
// Strip out getActivity() if not being used within a fragment
if (resultCode == getActivity().RESULT_OK) {
try {
JSONObject jo = new JSONObject(purchaseData);
String sku = jo.getString("productId");
//only auto consume the android.test.purchased product
if (sku.equals("android.test.purchased")) {
//build the purchase object from the response data
Purchase purchase = new Purchase("inapp", purchaseData, dataSignature);
//consume android.test.purchased
mHelper.consumeAsync(purchase,null);
}
} catch (JSONException je) {
//failed to parse the purchase data
je.printStackTrace();
} catch (IllegalStateException ise) {
//most likely either disposed, not setup, or
//another billing async process is already running
ise.printStackTrace();
} catch (Exception e) {
//unexpected error
e.printStackTrace();
}
}
}
}
It will only remove the purchase if it's sku is "android.test.purchased" so it should be safe to use.
This Solution worked for me. I changed the new verifyPurchase method in purchase class with old one.
Signature verification fails only for the default test product.
A quick fix :
Goto IabHelper class.
Invert the if conditions of Security.verifyPurchase.
Thats it!
Remember to revert the changes when test product is replaced by actual product
Ran into the same issue (signature verification, and getting rid of the test purchase) today (Oct 30, 2018).
The signature issue is probably being caused by the fact that these test sku's are not really part of your app, and are thus do not have your app's signature. I did open a ticket with Google, but not sure if they can fix this. The workaround, as others pointed out, is to replace the code
if (verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature())) {
with
if (verifyValidSignature(purchase.getOriginalJson(), purchase.getSignature()) ||
(purchase.getSku().startsWith("android.test.")) ) {
Regarding "how to get rid of the purchase of android.test.purchased SKU", I found that a simple reboot of the device, followed by waiting for a minute or so and/or re-starting your app a couple of times fixed it for me (i.e. I didn't have to 'consume' the purchase by code). I am guessing that the wait is needed so that the Play store completes syncing with Google's servers. (Not sure if this will continue to work this way in the future, but if it works for you now, this might help you move forward.)
Check this answer:
Is the primary account on your test device the same as your Google
Play developer account?
If not you won't get signatures on the android.test.* static responses
unless the app has been published on Play before.
See the table at
http://developer.android.com/guide/market/billing/billing_testing.html#static-responses-table
for the full set of conditions.
And it's comment:
I don't think the static ids return signature anymore. See
https://groups.google.com/d/topic/android-developers/PCbCJdOl480/discussion
Also, previously the sample code (used by many big apps) from Google Play Billing Library allowed an empty signature. That's why it's static purchases worked there.
But it was a security hole, so when it was published, Google submitted an update.
I have the same problem and follow #Deadolus said based on https://www.gaffga.de/implementing-in-app-billing-for-android/
The key point is we need to make the SKU is consumable even the inventory query result is failed. Below is the sample how i did that.
IabHelper.QueryInventoryFinishedListener mGotInventoryListener = new IabHelper.QueryInventoryFinishedListener() {
public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
Log.d(TAG, "Query inventory finished.");
// Have we been disposed of in the meantime? If so, quit.
if (mHelper == null) return;
// Is it a failure?
if (result.isFailure()) {
try {
Purchase purchase = new Purchase("inapp", "{\"packageName\":\"PACKAGE_NAME\","+
"\"orderId\":\"transactionId.android.test.purchased\","+
"\"productId\":\"android.test.purchased\",\"developerPayload\":\"\",\"purchaseTime\":0,"+
"\"purchaseState\":0,\"purchaseToken\":\"inapp:PACKAGE_NAME :android.test.purchased\"}",
"");
} catch (JSONException e) {
e.printStackTrace();
}
mHelper.consumeAsync(purchase, null);
complain("Failed to query inventory: " + result);
return;
}
Log.d(TAG, "Query inventory was successful.");
/*
* Check for items we own. Notice that for each purchase, we check
* the developer payload to see if it's correct! See
* verifyDeveloperPayload().
*/
}
};
Replace PACKAGE_NAME in the code above with the package name of your app.
This is what worked for me:
Call BillingClient.querySkuDetailsAsync to query if item if available
Wait for SkuDetailsResponseListener.onSkuDetailsResponse
Wait another 500ms
Start purchase using BillingClient.launchBillingFlow...
The step 3 shouldn't be necessary because when I received onSkuDetailsResponse it should be OK but it isn't, had to wait a little bit. After that purchase works, no more "Item not available error". This is how I tested it:
clear my app data
clear Google Play data
run app
purchase android.test.purchased
try to purchase my items (it fails with item not available)
use my solution above, it works
For Cordova and Hybrid apps you need to use this.iap.subscribe(this.productId) method to subscription InAppPurchase.
Following are the code working fine for me:
getProdutIAP() {
this.navCtrl.push('subscribeDialogPage');
this.iap
.getProducts(['productID1']).then((products: any) => {
this.buy(products);
})
.catch((err) => {
console.log(JSON.stringify(err));
alert('Finished Purchase' + JSON.stringify(err));
console.log(err);
});
}
buy(products: any) {
// this.getProdutIAP();
// alert(products[0].productId);
this.iap.subscribe(products[0].productId).then((buydata: any) => {
alert('buy Purchase' + JSON.stringify(buydata));
// this.sub();
}).catch((err) => {
// this.navCtrl.push('subscribeDialogPage');
alert('buyError' + JSON.stringify(err));
});
}
sub() {
this.platform.ready().then(() => {
this.iap
.subscribe(this.productId)
.then((data) => {
console.log('subscribe Purchase' + JSON.stringify(data));
alert('subscribe Purchase' + JSON.stringify(data));
this.getReceipt();
}).catch((err) => {
this.getReceipt();
alert('subscribeError' + JSON.stringify(err));
console.log(err);
});
})
}