Hi, I updated the in-app-purchase extension, and it seems to be working. I can retrieve the product prices and make purchases using the buy button (the Google dialog window finally opens).
It’s updated to the latest version of the cordova-plugin-purchase. You just need to save this code in a text file, save it as JSON, and import it as a new extension.
I don’t know anything about Java, JSON, Cordova, or JavaScript. I used ChatGPT, a lot of patience, and dozens of exports. So, it would be helpful to run some tests to ensure there are no bugs.
I haven’t implemented refund functions yet, but I’ll work on it in the coming days. In the meantime, Merry Christmas!
-
At the beginning of the scene
-
During the scene
{
"author": "",
"category": "Ads",
"extensionNamespace": "",
"fullName": "Mobile In-App Purchase (v13 updated)",
"helpPath": "/extensions/in-app-purchase/setup",
"iconUrl": "data:image/svg+xml;base64,...",
"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 In-App Purchases for games on Android or iOS, using cordova-plugin-purchase >=13.",
"version": "0.1.0",
"description": [
"Updated for Cordova-Plugin-Purchase v13+.",
"Register your products, call LoadStore(), then retrieve product info or place orders, watch events, finalize purchases."
],
"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 in the plugin's store object",
"fullName": "Register a Product",
"functionType": "Action",
"name": "RegisterItem",
"sentence": "Register product _PARAM1_ as _PARAM2_ (platform: _PARAM3_).",
"events": [
{
"type": "BuiltinCommonInstructions::JsCode",
"inlineCode": [
"// Cordova Purchase: store.register(...)",
"if (!window.CdvPurchase || !window.CdvPurchase.store) return;",
"const store = window.CdvPurchase.store;",
"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": "Load/Initialize the Store (store.initialize). This triggers 'storeisready' after success.",
"fullName": "LoadStore (initialize)",
"functionType": "Action",
"name": "LoadStore",
"sentence": "Load (initialize) the store.",
"events": [
{
"type": "BuiltinCommonInstructions::JsCode",
"inlineCode": [
"if(!window.CdvPurchase || !window.CdvPurchase.store) return;",
"const store = window.CdvPurchase.store;",
"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() => promise with array of errors or empty",
"store.initialize().then(errors => {",
" if (errors && errors.length) {",
" console.warn('Store initialization returned errors:', errors);",
" // optional, handle them as needed",
" }",
" // store is considered ready once initialization is done",
" console.info('CordovaPurchase store is ready!');",
" runtimeScene.getVariables().get('IAP_STORE_READY').setBoolean(true);",
" // Once ready, let's do a store.update() to load price info and purchases",
" store.update();",
"}).catch(err => {",
" console.error('store.initialize() failed:', err);",
"});"
]
}
],
"parameters": []
},
{
"description": "Order a product by its productId (single offer).",
"fullName": "OrderItem",
"functionType": "Action",
"name": "OrderItem",
"sentence": "Order product _PARAM1_",
"events": [
{
"type": "BuiltinCommonInstructions::JsCode",
"inlineCode": [
"if(!window.CdvPurchase || !window.CdvPurchase.store) return;",
"const store = window.CdvPurchase.store;",
"const productId = eventsFunctionContext.getArgument('id');",
"",
"const product = store.get(productId);",
"if (!product) {",
" console.warn('OrderItem: product not found:', productId);",
" return;",
"}",
"// In simple cases, if there's only one Offer, we do product.getOffer()",
"const offer = product.getOffer();",
"if (!offer) {",
" console.warn('OrderItem: No default offer for', productId);",
" return;",
"}",
"offer.order().then(error => {",
" if (error) {",
" console.warn('OrderItem failed:', error);",
" } else {",
" console.log('Order placed successfully');",
" }",
"});"
]
}
],
"parameters": [
{ "name": "id", "type": "string" }
]
},
{
"description": "Get product data (including price, etc.) into a GDevelop variable",
"fullName": "GetProduct",
"functionType": "Action",
"name": "GetProduct",
"sentence": "Store data of product _PARAM1_ in scene variable _PARAM2_",
"events": [
{
"type": "BuiltinCommonInstructions::JsCode",
"inlineCode": [
"if(!window.CdvPurchase || !window.CdvPurchase.store) return;",
"const store = window.CdvPurchase.store;",
"const productId = eventsFunctionContext.getArgument('id');",
"const product = store.get(productId);",
"if (!product) {",
" console.warn('GetProduct: product not found', productId);",
" return;",
"}",
"const varName = eventsFunctionContext.getArgument('variableName');",
"const dest = runtimeScene.getVariables().get(varName);",
"const plain = JSON.parse(JSON.stringify(product));",
"// Save entire product object into variable structure",
"dest.fromJSObject(plain);",
"console.info('GetProduct: Data stored in variable', varName);"
]
}
],
"parameters": [
{ "name": "id", "type": "string" },
{ "name": "variableName", "type": "string" }
]
},
{
"description": "Watch item event (approved or finished). For advanced usage. Not mandatory.",
"fullName": "WatchItemEvent",
"functionType": "Action",
"name": "WatchItemEvent",
"sentence": "Watch event _PARAM3_ for product _PARAM1_ => 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) => {",
" // Check if transaction includes this productId",
" const isProductInTransaction = transaction.products.some(tp => tp.id === productId);",
" if (!isProductInTransaction) return;",
" // Check if we haven't processed the transaction yet",
" if (knownTransactions.includes(transaction.transactionId)) return;",
"",
" // Match the event with the transaction state",
" if (eventName === 'approved' && transaction.state === 'approved') {",
" runtimeScene.getVariables().get(variableName).setBoolean(true);",
" knownTransactions.push(transaction.transactionId);",
" }",
" else if (eventName === 'finished' && transaction.state === 'finished') {",
" runtimeScene.getVariables().get(variableName).setBoolean(true);",
" knownTransactions.push(transaction.transactionId);",
" }",
" runtimeScene.getVariables().get('IAP_KNOWN_TRANSACTIONS').setString(JSON.stringify(knownTransactions));",
" };",
" store.when().approved(transactionCallback);",
" store.when().finished(transactionCallback);",
" runtimeScene.getVariables().get('IAP_WATCHERS_REGISTERED').setBoolean(true);",
"}",
"",
"// If store is ready, register watchers immediately; otherwise once ready.",
"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) {",
" store.when().receiptsReady(() => {",
" const isStillReady = runtimeScene.getVariables().get('IAP_STORE_READY').getAsBoolean();",
" if (isStillReady && !runtimeScene.getVariables().get('IAP_WATCHERS_REGISTERED').getAsBoolean()) {",
" registerWatchers();",
" }",
" });",
"}"
]
}
],
"parameters": [
{ "name": "id", "type": "string" },
{ "name": "variableName", "type": "string" },
{
"name": "event",
"type": "stringWithSelector",
"supplementaryInformation": "[\"approved\",\"finished\"]"
}
]
},
{
"description": "Finalize a purchase (mark as delivered)",
"fullName": "FinalizePurchase",
"functionType": "Action",
"name": "FinalizePurchase",
"sentence": "Finish purchase of _PARAM1_",
"events": [
{
"type": "BuiltinCommonInstructions::JsCode",
"inlineCode": [
"if(!window.CdvPurchase || !window.CdvPurchase.store) return;",
"const store = window.CdvPurchase.store;",
"const productId = eventsFunctionContext.getArgument('id');",
"",
"// Find the local transaction with that productId that's in 'approved' state, finish it:",
"const allTransactions = store.localTransactions;",
"const matching = allTransactions.find(t => t.state === 'approved' && t.products.some(p => p.id === productId));",
"if (!matching) {",
" console.log('No approved transaction found for', productId);",
" return;",
"}",
"matching.finish().then(() => {",
" console.log('FinalizePurchase: success');",
"}).catch(err => {",
" console.warn('FinalizePurchase: failed:', err);",
"});"
]
}
],
"parameters": [
{ "name": "id", "type": "string" }
]
},
{
"description": "Check if store is ready (IAP_STORE_READY is true).",
"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": []
}