In-App purchasing demo | Qt 6.2

- Qt 6.2
- In-App purchasing demo
- Qt 6.2.4 Reference Documentation

# In-App purchasing demo

     /****************************************************************************
     **
     ** Copyright (C) 2021 The Qt Company Ltd.
     ** Contact: https://www.qt.io/licensing/
     **
     ** This file is part of the examples of the Qt Toolkit.
     **
     ** $QT_BEGIN_LICENSE:BSD$
     ** Commercial License Usage
     ** Licensees holding valid commercial Qt licenses may use this file in
     ** accordance with the commercial license agreement provided with the
     ** Software or, alternatively, in accordance with the terms contained in
     ** a written agreement between you and The Qt Company. For licensing terms
     ** and conditions see https://www.qt.io/terms-conditions. For further
     ** information use the contact form at https://www.qt.io/contact-us.
     **
     ** BSD License Usage
     ** Alternatively, you may use this file under the terms of the BSD license
     ** as follows:
     **
     ** "Redistribution and use in source and binary forms, with or without
     ** modification, are permitted provided that the following conditions are
     ** met:
     **   * Redistributions of source code must retain the above copyright
     **     notice, this list of conditions and the following disclaimer.
     **   * Redistributions in binary form must reproduce the above copyright
     **     notice, this list of conditions and the following disclaimer in
     **     the documentation and/or other materials provided with the
     **     distribution.
     **   * Neither the name of The Qt Company Ltd nor the names of its
     **     contributors may be used to endorse or promote products derived
     **     from this software without specific prior written permission.
     **
     **
     ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
     ** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
     ** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
     ** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
     ** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
     ** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
     ** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
     ** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
     ** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     ** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE."
     **
     ** $QT_END_LICENSE$
     **
     ****************************************************************************/

     #include <QDebug>

     #include <QJniEnvironment>
     #include <QtCore>

     #include "androidinapppurchasebackend.h"
     #include "androidinappproduct.h"
     #include "androidinapptransaction.h"
     #include "../inapp/inappstore.h"

     AndroidInAppPurchaseBackend::AndroidInAppPurchaseBackend(QObject *parent)
         : InAppPurchaseBackend(parent)
         , m_isReady(false)
     {
         m_javaObject = QJniObject("org/qtproject/qt/android/purchasing/InAppPurchase",
                                   "(Landroid/content/Context;J)V",
                                   QNativeInterface::QAndroidApplication::context(),
                                   this);

         if (!m_javaObject.isValid()) {
             qWarning("Cannot initialize IAP backend for Android due to missing dependency: InAppPurchase class");
             return;
         }
     }

     void AndroidInAppPurchaseBackend::initialize()
     {
         m_javaObject.callMethod<void>("initializeConnection");
     }

     bool AndroidInAppPurchaseBackend::isReady() const
     {
         QMutexLocker locker(&m_mutex);
         return m_isReady;
     }

     void AndroidInAppPurchaseBackend::restorePurchases()
     {
         for (const QString &purchasedUnlockeble : purchasedUnlockebles) {
             InAppProduct *product = store()->registeredProduct(purchasedUnlockeble);
             Q_ASSERT(product != 0);

             checkFinalizationStatus(product, InAppTransaction::PurchaseRestored);
         }

     }

     void AndroidInAppPurchaseBackend::queryProducts(const QList<Product> &products)
     {
         QMutexLocker locker(&m_mutex);
         QJniEnvironment environment;

         QStringList newProducts;
         for (int i = 0; i < products.size(); ++i) {
             const Product &product = products.at(i);
             if (m_productTypeForPendingId.contains(product.identifier)) {
                 qWarning("Product query already pending for %s", qPrintable(product.identifier));
                 continue;
             }
             else{
                 m_productTypeForPendingId[product.identifier] = product.productType;
                 newProducts.append(product.identifier);
             }
         }

         if (newProducts.isEmpty())
             return;

         jclass cls = environment->FindClass("java/lang/String");
         jobjectArray productIds = environment->NewObjectArray(newProducts.size(), cls, 0);
         environment->DeleteLocalRef(cls);

         for (int i = 0; i < newProducts.size(); ++i) {
             QJniObject identifier = QJniObject::fromString(newProducts.at(i));
             environment->SetObjectArrayElement(productIds, i, identifier.object());
         }

         m_javaObject.callMethod<void>("queryDetails",
                                       "([Ljava/lang/String;)V",
                                       productIds);
         environment->DeleteLocalRef(productIds);
     }

     void AndroidInAppPurchaseBackend::queryProduct(InAppProduct::ProductType productType,
                                                     const QString &identifier)
     {
         queryProducts(QList<Product>() << Product(productType, identifier));
     }

     void AndroidInAppPurchaseBackend::setPlatformProperty(const QString &propertyName, const QString &value)
     {
         QMutexLocker locker(&m_mutex);
         if (propertyName.compare(QStringLiteral("AndroidPublicKey"), Qt::CaseInsensitive) == 0) {
             m_javaObject.callMethod<void>("setPublicKey",
                                           "(Ljava/lang/String;)V",
                                           QJniObject::fromString(value).object<jstring>());
         }
     }

     void AndroidInAppPurchaseBackend::consumeTransaction(const QString &purchaseToken)
     {
         QMutexLocker locker(&m_mutex);
         m_javaObject.callMethod<void>("consumePurchase",
                                       "(Ljava/lang/String;)V",
                                       QJniObject::fromString(purchaseToken).object<jstring>());
     }

     void AndroidInAppPurchaseBackend::registerFinalizedUnlockable(const QString &purchaseToken)
     {
         QMutexLocker locker(&m_mutex);
         m_javaObject.callMethod<void>("acknowledgeUnlockablePurchase",
                                       "(Ljava/lang/String;)V",
                                       QJniObject::fromString(purchaseToken).object<jstring>());
     }

     bool AndroidInAppPurchaseBackend::transactionFinalizedForProduct(InAppProduct *product)
     {
         Q_ASSERT(m_infoForPurchase.contains(product->identifier()));

         if (product->productType() != InAppProduct::Consumable && purchasedUnlockebles.contains(product->identifier()))
         {
             return true;
         }
         return false;
     }

     void AndroidInAppPurchaseBackend::checkFinalizationStatus(InAppProduct *product,
                                                                InAppTransaction::TransactionStatus status)
     {
         // Verifies the finalization status of an item based on the following logic:
         // 1. If the item is not purchased yet, do nothing (it's either never been purchased, or it's a
         //    consumed consumable.
         // 2. If the item is purchased, and it's a consumable, it's unfinalized. Emit a new transaction.
         //    Consumable items are consumed when they are finalized.
         // 3. If the item is purchased, and it's an unlockable, check the local cache for finalized
         //    unlockable purchases. If it's not there, then the transaction is unfinalized. This means
         //    that if the cache gets deleted or corrupted, the worst-case scenario is that the transactions
         //    are republished.
         QHash<QString, PurchaseInfo>::iterator it = m_infoForPurchase.find(product->identifier());

         if (it == m_infoForPurchase.end()) {
             return;
         }

         const PurchaseInfo &info = it.value();
         if (transactionFinalizedForProduct(product)) {
             AndroidInAppTransaction *transaction = new AndroidInAppTransaction(info.signature,
                                                                                  info.data,
                                                                                  info.purchaseToken,
                                                                                  info.orderId,
                                                                                  status,
                                                                                  product,
                                                                                  info.timestamp,
                                                                                  InAppTransaction::NoFailure,
                                                                                  QString(),
                                                                                  this);
             emit transactionReady(transaction);
         }
     }

     void AndroidInAppPurchaseBackend::registerProduct(const QString &productId,
                                                        const QString &price,
                                                        const QString &title,
                                                        const QString &description)
     {
         QMutexLocker locker(&m_mutex);
         QHash<QString, InAppProduct::ProductType>::iterator it = m_productTypeForPendingId.find(productId);
         Q_ASSERT(it != m_productTypeForPendingId.end());

         AndroidInAppProduct *product = new AndroidInAppProduct(this, price, title, description, it.value(), it.key(), this);

         emit productQueryDone(product);
     }

     void AndroidInAppPurchaseBackend::registerPurchased(const QString &identifier,
                                                          const QString &signature,
                                                          const QString &data,
                                                          const QString &purchaseToken,
                                                          const QString &orderId,
                                                          const QDateTime &timestamp)
     {
         QMutexLocker locker(&m_mutex);
         m_infoForPurchase.insert(identifier, PurchaseInfo(signature, data, purchaseToken, orderId, timestamp));

         QHash<QString, InAppProduct::ProductType>::iterator it = m_productTypeForPendingId.find(identifier);
         if (it.value() == InAppProduct::Unlockable && !purchasedUnlockebles.contains(identifier))
             purchasedUnlockebles.append(identifier);

     }

     void AndroidInAppPurchaseBackend::registerReady()
     {
         QMutexLocker locker(&m_mutex);
         m_isReady = true;
         emit ready();
     }

     void AndroidInAppPurchaseBackend::purchaseProduct(AndroidInAppProduct *product)
     {
         QMutexLocker locker(&m_mutex);
         if (!m_javaObject.isValid()) {
             purchaseFailed(product, InAppTransaction::ErrorOccurred, QStringLiteral("Java backend is not initialized"));
             return;
         }

         int requestCode = 0;
         while (m_activePurchaseRequests.contains(requestCode)) {
             requestCode++;
             if (requestCode == 0) {
                 qWarning("No available request code for purchase request.");
                 return;
             }
         }

         m_activePurchaseRequests[requestCode] = product;

         m_javaObject.callMethod<void>("launchBillingFlow",
                                       "(Ljava/lang/String;I)V",
                                       QJniObject::fromString(product->identifier()).object<jstring>(),
                                       requestCode);
     }

     void AndroidInAppPurchaseBackend::purchaseFailed(int requestCode, int failureReason, const QString &errorString)
     {
         QMutexLocker locker(&m_mutex);
         InAppProduct *product = m_activePurchaseRequests.take(requestCode);
         if (product == 0) {
             qWarning("No product registered for requestCode %d", requestCode);
             return;
         }

         purchaseFailed(product, failureReason, errorString);
     }

     void AndroidInAppPurchaseBackend::purchaseFailed(InAppProduct *product, int failureReason, const QString &errorString)
     {
         InAppTransaction *transaction = new AndroidInAppTransaction(QString(),
                                                                       QString(),
                                                                       QString(),
                                                                       QString(),
                                                                       InAppTransaction::PurchaseFailed,
                                                                       product,
                                                                       QDateTime(),
                                                                       InAppTransaction::FailureReason(failureReason),
                                                                       errorString,
                                                                       this);
         emit transactionReady(transaction);
     }

     void AndroidInAppPurchaseBackend::purchaseSucceeded(int requestCode,
                                                          const QString &signature,
                                                          const QString &data,
                                                          const QString &purchaseToken,
                                                          const QString &orderId,
                                                          const QDateTime &timestamp)

     {
         QMutexLocker locker(&m_mutex);
         InAppProduct *product = m_activePurchaseRequests.take(requestCode);
         if (product == 0) {
             qWarning("No product registered for requestCode %d", requestCode);
             return;
         }

         m_infoForPurchase.insert(product->identifier(), PurchaseInfo(signature, data, purchaseToken, orderId, timestamp));
         InAppTransaction *transaction = new AndroidInAppTransaction(signature,
                                                                       data,
                                                                       purchaseToken,
                                                                       orderId,
                                                                       InAppTransaction::PurchaseApproved,
                                                                       product,
                                                                       timestamp,
                                                                       InAppTransaction::NoFailure,
                                                                       QString(),
                                                                       this);
         emit transactionReady(transaction);
     }