@Andi For testing multiple purchases of non-consumable products, you can go ahead and refund/revoke the purchase in Google Play and delete the user in RevenueCat (which will clear their sandbox transactions).
If you want us to dig into your purchase logs (to learn more about the error you experienced), please open a support ticket. We’re happy to help. Thanks!
Thanks, I’ve done that already. It worked the first time, but now I seem to be stuck on my second refund attempt. I have the purchase refunded on Google Play (it’s not yet pending the processing of the refund, it’s already refunded), and deleted the user on revenuecat, but I keep on getting the error message:
Error
You already own this item.
The exact message that I get from my logger is:
ERROR : This product is already active for the user.
I already tried restarting my phone, but doesn’t seem to be resolved. Is this a cache issue, or are you familiar with this problem? Thanks!
The rough code that I have, first block up top (before purchasePackage) can be ignored, it’s app-specific to find the right package on my product page to make the purchase.
try {
// Find the package across all offerings
let rcPackage = null;
// Search through all available offerings
if (allOfferings) {
for (const offeringKey in allOfferings) {
const offering = allOfferingsgofferingKey];
if (offering.availablePackages) {
// Find matching package in this offering
const foundPackage = offering.availablePackages.find(
(pkg) => pkg.product.identifier === packageItem.purchaseProductId
);
if (foundPackage) {
rcPackage = foundPackage;
break;
}
}
}
}
// Make the purchase
const { customerInfo } = await Purchases.purchasePackage(rcPackage);
logger.info(customerInfo);
if (customerInfo.entitlements.activeipackageItem.purchaseProductId]) {
await registerForRaceFree({
variables: {
input: {
raceId: id,
packageId: packageItem.id,
distanceId: selectedDistance,
email: user?.primaryEmailAddress?.emailAddress || "",
},
},
});
toast.success(t("toastMessageSuccessfullyRegistered"), {
duration: 3000,
});
}
} catch (error) {
logger.error(error);
if (!error.userCancelled) {
// toast.error(`Purchase failed: ${error.message || "Unknown error"}`, {
// duration: 3000,
// });
toast.error(
t("toastMessagePurchaseFailed", { message: error.message }),
{
duration: 3000,
}
);
}
By the way, I don’t use entitlements. So I modified my code to check for transactions, rather than an active entitlement, but keep it future-proof to use entitlements. Though for now, I think they only complicate my app/purchasing flow:
try {
// Find the package across all offerings
let rcPackage = null;
// Search through all available offerings
if (allOfferings) {
for (const offeringKey in allOfferings) {
const offering = allOfferingsiofferingKey];
if (offering.availablePackages) {
// Find matching package in this offering
const foundPackage = offering.availablePackages.find(
(pkg) => pkg.product.identifier === packageItem.purchaseProductId
);
if (foundPackage) {
rcPackage = foundPackage;
break;
}
}
}
}
// Make the purchase
const { customerInfo } = await Purchases.purchasePackage(rcPackage);
logger.info("customerInfo");
logger.info(customerInfo);
// Check for entitlement (future compatibility)
const hasEntitlement =
customerInfo.entitlements.activetpackageItem.purchaseProductId];
// Check for direct product purchase (current logic)
const hasPurchasedProduct =
customerInfo.nonSubscriptionTransactions?.some(
(tx) =>
tx.productId === packageItem.purchaseProductId ||
tx.productIdentifier === packageItem.purchaseProductId
) ||
customerInfo.allPurchasedProductIdentifiers?.includes(
packageItem.purchaseProductId
);
if (hasEntitlement || hasPurchasedProduct) {
await registerForRaceFree({
variables: {
input: {
raceId: id,
packageId: packageItem.id,
distanceId: selectedDistance,
email: user?.primaryEmailAddress?.emailAddress || "",
},
},
});
toast.success(t("toastMessageSuccessfullyRegistered"), {
duration: 3000,
});
}
} catch (error) {
logger.error(error);
if (!error.userCancelled) {
// toast.error(`Purchase failed: ${error.message || "Unknown error"}`, {
// duration: 3000,
// });
toast.error(
t("toastMessagePurchaseFailed", { message: error.message }),
{
duration: 3000,
}
);
}
}
I tried to check purchases before making the purchase, and got this error:
logger.info("restoredPurchases");
const restoredPurchases = await Purchases.restorePurchases();
logger.info(restoredPurchases);
>{"nativeStackAndroid":d],"userInfo":{"underlyingErrorMessage":"Backend Code: 7651 - The payment for this non-subscription product is not complete.","readableErrorCode":"UnknownBackendError","readable_error_code":"UnknownBackendError","message":"There was an unknown backend error.","code":16},"code":"16"}]
This sounds as if the product isn’t properly purchased, or still stuck in a purchasing flow? I’m not sure. The product was purchased correctly before, imo. All purchases that I can see in google play have been successfully charged AND successfully refunded.
Btw, thanks, I have opened a support request to dig further into my logs.
I believe I found the “solution”/problem.
https://stackoverflow.com/questions/36821357/you-already-own-this-item-google-play-inapp-error
https://stackoverflow.com/questions/68839798/how-to-remove-entitlement-after-refund-a-test-order-in-app-purchase-in-play-co
If you forget to remove the entitlement during refund, then you are screwed. There’s no way to remove the entitlement for the user, so they’ve been refunded, but they are still entitled to the product.
The second stackoverflow shows how to “consume” the non-consumable and make it available again for purchase, but I don’t think this is possible with RevenueCat, so the only viable option seems to be to create a new user (very sub-optimal for testing), or create a new product… and then be really careful when issuing refunds. It’s not perfect.
If you know another way to get rid of the entitlement for RevenueCat workflow, I’d be really grateful!
Hi @Andi! Thanks so much for your code samples and sharing the results of your investigation. I just replied to your support ticket, but for the benefit of others, I wanted to share that refunding Google payments via the RevenueCat dashboard or API (and not via Google Play) also revokes the entitlement. I suspect you refunded via Google Play Console and didn’t remove the entitlement, but please let me know if I’m mistaken. Thanks!