In App Purchases Price shows 0

Hey,
so I’ve added In App Purchases in my app, but after uploading it to google play closed testing track and even after getting it reviewed by google, when i download the app and install the app on my phone the price somehow always shows 0.
In the google play console I’ve added those items using the same ID(name) as in Gdevelop, configured all the prices, activated the products but somehow it still shows 0.
When i click on those buttons, it will order the correct item, so i know they are linked properly, it’s just the price doesn’t update

I don’t think there is an issue in the events, since i basically copied what was in the IAP example, I’ve stored all the IAP Data in a structure just as the IAP example

I’ve tried just using “.price” instead of “.offers[0].pricingPhases[0].price”, but still the same.

Update1.
So after test buying one of the product, i somehow didn’t receive the product but my card got charged, and after if i click on the button again, it tells me “error you already own this item” even though it is set as a “consumable” item. It tells me the same even after reopening the app or reinstalling.

Can someone tell me what I’m doing wrong or if someone has the same issue?
Is it possible that there is some issue in the IAP extension itself?

1 Like

I have the same problem, I don’t know how to get the price, I would also like to verify when the user pays to remove the ads, because when I close the game and come back I don’t know how to verify.

I used the template: “https://thegemdev.itch.io/gdevelop-in-app-purchase-template” but it doesn’t work.

i have the same problem, even do the same like a tutorial

Hello, I’m facing the same issue, and I can’t seem to find a solution. Additionally, since I think I might be doing something wrong, I’m forced to export the project every time (which takes up to 20 minutes), upload it to Google (another 20 minutes waiting for approval), and then update the new version.

Often, I end up using all ten daily exports without resolving anything.

For this reason, I would like to ask two things:

  1. Is there a way to solve this, or at least some information to understand if In-App Purchases are not working?

  2. I love GDevelop and always recommend it. Unfortunately, the commercial aspect of the program and community support in this area is a problem. The fact that there has been no response or support to this post since April creates significant challenges for those who dedicate effort, time, and financial resources to developing a game. Even a simple message saying, “Okay, we’ll look into it,” or “You’re on your own,” would be helpful. We’re talking about a critical feature: in my case, for example, I’m about to publish my game after months of work, and I find out (after recruiting the 20 testers required by Google for publication, purchasing servers, hosting, etc.) that In-App Purchases don’t work.

I’m truly concerned and hope this can be resolved, both for me and for other users.

1 Like

Meh, most people don’t like having to pay for in game purchases with real money anyway, it’s done you a favour.

I’ve never read such a useless comment.

For others and for the extension authors, here are some details: I ran several tests on variables and structures, but the price remains 0.

Sometimes (one out of thirty), pressing the BUY button brings up the Google Play Store window.

Other times, clicking on BUY does nothing. However, if you minimize the app and reopen it, the transaction goes through (but the Google confirmation window doesn’t appear).

If you have any additional details, it might be helpful to share them here. Thank you.

I asked GPT-01 (latest version) to perform a check based on the issues reported by other users and the results of my tests. Here are the possible causes it identified:

Unaddressed Updates to Google Play Billing API

Google periodically requires updates to the latest version of the Google Play Billing Library. If the GDevelop extension (or the internal Cordova plugin) hasn’t been updated to align with the latest API versions, issues may arise with loading prices and properly initializing the purchase window.

Compatibility Issues Between Cordova and GDevelop

The GDevelop In-App Purchase extension relies on Cordova plugins. If the Cordova plugin being used is outdated or no longer maintained, random bugs may occur, such as the purchase confirmation window not opening or prices not being read correctly.

Delays or Timeouts in Product Fetching

Sometimes loading data (prices, product availability) from the Play Store can take time. If GDevelop’s logic runs too quickly, without properly waiting for the product fetch operation to complete, values may appear as zero, or the purchase interface may not activate. The fact that “minimizing and reopening” the app causes the transaction to succeed suggests an issue with synchronization or event listening.

Restrictions or Changes in Security Policies

Google frequently updates policies related to purchase methods. If the app hasn’t integrated the latest requirements (e.g., Billing Library v.5 or v.6, or additional permissions), the confirmation window might not appear reliably.

Issues in Callback Flow (Unmanaged Events)

Even if your code hasn’t changed, partially updated libraries or plugins might have altered how purchase events are reported to the app. If the “success” or “error” callbacks aren’t handled correctly, it can lead to intermittent behavior.

Test Accounts or Unreliable Testing Environments

Sometimes these issues occur only in test environments (e.g., with test accounts). If the issue occurs in production, it’s less likely, but if you’ve encountered it with a tester account, it could be due to test configuration issues on the Play Store (expired test licenses, account changes, or Play Services issues).


Here is the in-depth analysis between Google’s guidelines and the in-app purchase JS code

From the analysis of the code you provided and Google’s requirements, it appears that the current approach used by the extension (based on window.CdvPurchase and the Cordova plugin) does not seem to follow the initialization flow required by Google for the latest BillingClient Library.

Let’s look at the possible reasons why the current implementation might not work as expected:

No Explicit Initialization of BillingClient

Google now requires creating and starting an instance of BillingClient at the app’s launch or when it comes to the foreground. The code shows that you are using window.CdvPurchase and store to register and initialize products, but there is no trace of the BillingClient creation logic (e.g., BillingClient.newBuilder(context).setListener(...)). It is possible that the Cordova extension in use is not updated to use the latest Play billing libraries.

Cordova IAP Library Not Updated

The Cordova plugin on which GDevelop is based might be lagging behind the latest versions of the Billing Library. This causes anomalous behaviors such as prices not loading or confirmation windows not appearing consistently. Google continues to modify the requirements, and if the library has not been updated, the internal initialization phase (billing client connection) is not executed correctly.

Failure to Manage the App’s Lifecycle

Google suggests opening the BillingClient connection at the app’s launch or in the onActivityResumed method. In your code, there is no logic for reconnecting or maintaining the connection when the app returns to the foreground. The extension should, in theory, handle these aspects, but if it does not (or if there is a bug), you might experience intermittent issues with purchase windows.

Asynchrony in Loading Products

The code shows that you call store.initialize() and store.update() and then wait for store.ready(). There might be a delay in fetching the products or in synchronizing with the internal BillingClient. If the connection is not yet ready when you try to display the offer, the price might appear as zero, and the purchase window might not appear.

Outdated Plugin or Bridge

Even if you followed the standard procedure with window.CdvPurchase, if the plugin itself has not correctly implemented the BillingClient logic, you might not see any evident errors in your JavaScript code but simply experience unreliable behavior. Check if the Cordova plugin for in-app purchases you are using has been updated to the latest version, compatible with BillingLibrary v5 or v6.

Update: I successfully updated In-app-purchase to the new version of cordova-plugin-purchase. I had GPT-01 (the new version) compare both the documentation for cordova-plugin-purchase and Google’s Billing API.

Some bugs have been fixed: previously, minimizing and reopening the app would execute transactions even though the user hadn’t pressed the buy button.

However, the core issue remains: the price is still 0, and purchases cannot be made. According to some users on GitHub, this might be related to the use of Chinese phones (mine is an OPPO), which Google restricts due to the embargo from a few years ago.

Question: has anyone experiencing these issues also been using Chinese phones?

{
  "author": "",
  "category": "Ads",
  "extensionNamespace": "",
  "fullName": "Mobile In-App Purchase (experimental)",
  "helpPath": "/extensions/in-app-purchase/setup",
  "iconUrl": "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOi...==",
  "name": "InAppPurchase",
  "previewIconUrl": "https://resources.gdevelop-app.com/assets/Icons/Glyphster Pack/Master/SVG/Shopping and Ecommerce/Shopping and Ecommerce_wallet_money_cash.svg",
  "shortDescription": "Add products to buy directly in your game (\"In-App Purchase\"), for games published on Android or iOS.",
  "version": "0.0.9",
  "description": [
    "Manage IAP for Android/iOS.",
    "Register products, finalize store, wait for Store is ready, get products, order item, watch approved/finished events, finalize purchase.",
    "Works with cordova-plugin-purchase >=13.12.0"
  ],
  "origin": {
    "identifier": "InAppPurchase",
    "name": "gdevelop-extension-store"
  },
  "tags": [
    "purchase",
    "iap",
    "android",
    "ios",
    "monetization"
  ],
  "authorIds": [],
  "dependencies": [
    {
      "exportName": "cordova-plugin-purchase",
      "name": "Purchase plugin",
      "type": "cordova",
      "version": "13.12.0"
    }
  ],
  "eventsFunctions": [
    {
      "description": "Register a Product",
      "fullName": "Register a Product",
      "functionType": "Action",
      "name": "RegisterItem",
      "sentence": "Register product _PARAM1_ as a _PARAM2_ (platform: _PARAM3_)",
      "events": [
        {
          "type": "BuiltinCommonInstructions::JsCode",
          "inlineCode": [
            "if(!window.CdvPurchase || !window.CdvPurchase.store) return;",
            "window.CdvPurchase.store.register({",
            "    id: eventsFunctionContext.getArgument(\"id\"),",
            "    type: eventsFunctionContext.getArgument(\"type\"),",
            "    platform: eventsFunctionContext.getArgument(\"platform\")",
            "});"
          ]
        }
      ],
      "parameters": [
        { "name": "id", "type": "string" },
        { "name": "type", "type": "stringWithSelector", "supplementaryInformation": "[\"consumable\",\"non consumable\",\"paid subscription\",\"non renewing subscription\"]" },
        { "name": "platform", "type": "stringWithSelector", "supplementaryInformation": "[\"android-playstore\",\"ios-appstore\",\"braintree\",\"windows-store-transaction\",\"test\"]" }
      ]
    },
    {
      "description": "Finalize store registration",
      "fullName": "Finalize registration",
      "functionType": "Action",
      "name": "FinalizeRegistration",
      "sentence": "Finalize store registration",
      "events": [
        {
          "type": "BuiltinCommonInstructions::JsCode",
          "inlineCode": [
            "if(!window.CdvPurchase || !window.CdvPurchase.store) return;",
            "const store = window.CdvPurchase.store;",
            "",
            "// VERSIONE X Reset state variables",
            "runtimeScene.getVariables().get('IAP_STORE_READY').setBoolean(false);",
            "runtimeScene.getVariables().get('IAP_WATCHERS_REGISTERED').setBoolean(false);",
            "runtimeScene.getVariables().get('IAP_KNOWN_TRANSACTIONS').setString(JSON.stringify([]));",
            "",
            "store.initialize();",
            "store.update();",
            "store.ready(() => {",
            "  console.info('IAP store is ready!');",
            "  runtimeScene.getVariables().get('IAP_STORE_READY').setBoolean(true);",
            "  // Track known transactions at startup",
            "  const knownTransactions = store.localTransactions.map(t=>t.transactionId);",
            "  runtimeScene.getVariables().get('IAP_KNOWN_TRANSACTIONS').setString(JSON.stringify(knownTransactions));",
            "});"
          ]
        }
      ],
      "parameters": []
    },
    {
      "description": "Order a product",
      "fullName": "Order a product",
      "functionType": "Action",
      "name": "OrderItem",
      "sentence": "Order product _PARAM1_",
      "events": [
        {
          "type": "BuiltinCommonInstructions::JsCode",
          "inlineCode": [
            "if(!window.CdvPurchase || !window.CdvPurchase.store) return;",
            "const productId = eventsFunctionContext.getArgument(\"id\");",
            "const product = window.CdvPurchase.store.get(productId);",
            "if (!product) { console.warn('OrderItem: product not found:', productId); return; }",
            "const offer = product.getOffer();",
            "if (!offer) { console.warn('OrderItem: no offer for', productId); return; }",
            "offer.order().catch(err => console.error('OrderItem failed:', err));"
          ]
        }
      ],
      "parameters": [
        { "name": "id", "type": "string" }
      ]
    },
    {
      "description": "Get product data",
      "fullName": "GetProduct",
      "functionType": "Action",
      "name": "GetProduct",
      "sentence": "Store data of _PARAM1_ in scene variable named _PARAM2_",
      "events": [
        {
          "type": "BuiltinCommonInstructions::JsCode",
          "inlineCode": [
            "if(!window.CdvPurchase || !window.CdvPurchase.store) return;",
            "const id = eventsFunctionContext.getArgument(\"id\");",
            "const product = window.CdvPurchase.store.get(id);",
            "if (!product) { console.warn('GetProduct: product not found', id); return; }",
            "const varName = eventsFunctionContext.getArgument(\"variableName\");",
            "const dest = runtimeScene.getVariables().get(varName);",
            "dest.fromJSObject(product);"
          ]
        }
      ],
      "parameters": [
        { "name": "id", "type": "string" },
        { "name": "variableName", "type": "string" }
      ]
    },
    {
      "description": "Watch item event (approved or finished).",
      "fullName": "WatchItemEvent",
      "functionType": "Action",
      "name": "WatchItemEvent",
      "sentence": "Watch the event _PARAM3_ for product _PARAM1_ and set _PARAM2_ to true when it happens",
      "events": [
        {
          "type": "BuiltinCommonInstructions::JsCode",
          "inlineCode": [
            "if(!window.CdvPurchase || !window.CdvPurchase.store) return;",
            "const store = window.CdvPurchase.store;",
            "const productId = eventsFunctionContext.getArgument(\"id\");",
            "const variableName = eventsFunctionContext.getArgument(\"variableName\");",
            "const eventName = eventsFunctionContext.getArgument(\"event\");",
            "",
            "function registerWatchers() {",
            "  const knownStr = runtimeScene.getVariables().get('IAP_KNOWN_TRANSACTIONS').getAsString();",
            "  let knownTransactions = [];",
            "  try { knownTransactions = JSON.parse(knownStr); } catch(e) {}",
            "",
            "  const transactionCallback = (transaction) => {",
            "    if (knownTransactions.includes(transaction.transactionId)) return; // already handled",
            "    const isProductInTransaction = transaction.products.some(tp => tp.id === productId);",
            "    const isStateWatched = (transaction.state === eventName);",
            "    if (isProductInTransaction && isStateWatched) {",
            "      runtimeScene.getVariables().get(variableName).setBoolean(true);",
            "      knownTransactions.push(transaction.transactionId);",
            "      runtimeScene.getVariables().get('IAP_KNOWN_TRANSACTIONS').setString(JSON.stringify(knownTransactions));",
            "    }",
            "  };",
            "",
            "  if (eventName === 'approved') {",
            "    store.when().approved(transactionCallback);",
            "  } else if (eventName === 'finished') {",
            "    store.when().finished(transactionCallback);",
            "  }",
            "  runtimeScene.getVariables().get('IAP_WATCHERS_REGISTERED').setBoolean(true);",
            "}",
            "",
            "const isReady = runtimeScene.getVariables().get('IAP_STORE_READY').getAsBoolean();",
            "const watchersRegistered = runtimeScene.getVariables().get('IAP_WATCHERS_REGISTERED').getAsBoolean();",
            "if (isReady && !watchersRegistered) {",
            "  registerWatchers();",
            "} else if (!isReady) {",
            "  // If not ready yet, once ready, register watchers.",
            "  store.ready(() => {",
            "    const isStillReady = runtimeScene.getVariables().get('IAP_STORE_READY').getAsBoolean();",
            "    const watchersRegistered2 = runtimeScene.getVariables().get('IAP_WATCHERS_REGISTERED').getAsBoolean();",
            "    if (isStillReady && !watchersRegistered2) {",
            "      registerWatchers();",
            "    }",
            "  });",
            "}"
          ]
        }
      ],
      "parameters": [
        { "name": "id", "type": "string" },
        { "name": "variableName", "type": "string" },
        { "name": "event", "type": "stringWithSelector", "supplementaryInformation": "[\"finished\",\"approved\"]" }
      ]
    },
    {
      "description": "Finalize a purchase",
      "fullName": "FinalizePurchase",
      "functionType": "Action",
      "name": "FinalizePurchase",
      "sentence": "Mark purchase of _PARAM1_ as delivered",
      "events": [
        {
          "type": "BuiltinCommonInstructions::JsCode",
          "inlineCode": [
            "if(!window.CdvPurchase || !window.CdvPurchase.store) return;",
            "const productId = eventsFunctionContext.getArgument(\"id\");",
            "const transactions = window.CdvPurchase.store.localTransactions;",
            "const nonFinished = transactions.filter(t => t.state === 'approved');",
            "for (const tr of nonFinished) {",
            "  const pIds = tr.products.map(p => p.id);",
            "  if (pIds.includes(productId)) {",
            "    tr.finish().catch(err=>console.error('FinalizePurchase failed:', err));",
            "    return;",
            "  }",
            "}"
          ]
        }
      ],
      "parameters": [
        { "name": "id", "type": "string" }
      ]
    },
    {
      "description": "Check if store is ready",
      "fullName": "StoreReady",
      "functionType": "Condition",
      "name": "StoreReady",
      "sentence": "Store is ready",
      "events": [
        {
          "type": "BuiltinCommonInstructions::JsCode",
          "inlineCode": [
            "eventsFunctionContext.returnValue = runtimeScene.getVariables().get('IAP_STORE_READY').getAsBoolean();"
          ]
        }
      ],
      "parameters": []
    }
  ],
  "eventsBasedBehaviors": [],
  "eventsBasedObjects": []
}

1 Like

I can’t answer this as I currently own a Samsung.

For me, I set the text info to VariableString(BUYDRAGONSPACKNUMBER01INFO.offers[0].pricingPhases[0].price) and it finally shows the price. For some reason yesterday it wasn’t working properly, so I was looking online for solutions, who knows maybe I mistyped. I asked GPT today, and after typing the above, it worked.

Also @GoodGame, maybe you first declared your object as Non-Consumable? Not sure how it works for Consumable products, but I noticed that for Non-Consumables even if you refund them if you don’t tick remove entitlement, it won’t be properly removed from the memory.

To clear it, I set my product as consumable in Gdevelop, then cleared the data of Play store from my phone (This is the important part. For some reason purchases are saved inside the memory/cache?? of Playstore), then cleared the data from the game to delete the saved variables, and loaded again the game to check if the purchase was still there.