In-app-purchase updated!

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!

  1. At the beginning of the scene

  2. 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": []
}

2 Likes