Implementing a new backend for Qt Purchasing

A bit more than a year ago, we released the Qt Purchasing module as part of our commercial offering. This module provides cross-platform APIs to enable in-app purchases in your application, and the current released version works together with the app store on iOS and the Google Play store on Android.

Recently, however, we decided to release the module into open source under LGPLv3 in Qt 5.7, hoping that it will prove to be useful for the Qt community as a whole, and that we can work together to evolve it further. There are many different app stores out there, and the ambitious goal should of course be to support all of them! So far, so good: Just a few days after the Qt Purchasing module had been uploaded to the Qt Project, Jake Petroules contributed a patch which enables module to work with the Mac Store on OS X as well.

To assist those of your out there who might have interest in a particular online store and want to participate in increasing the value of Qt Purchasing, I decided to write a guide on how you would go about actually implementing a new backend, as the interfaces are currently largely undocumented outside of Andy Nichols' and my own brain.

QInAppPurchaseBackend

The first thing you want to do, is to create a subclass of QInAppPurchaseBackend. Throughout this blog, I will refer to a hypothetical store instead of an actual one, as this will allow us to focus on how the APIs were intended to be used instead of gritty platform details.


class QAcmeInAppPurchaseBackend: public QInAppPurchaseBackend
{
public:
    void initialize();
    bool isReady() const;
    void queryProducts(const QList &products);
    void queryProduct(QInAppProduct::ProductType productType, const QString &identifier);
    void restorePurchases();
    void setPlatformProperty(const QString &propertyName, const QString &value);
};

I'll go through the functions one by one, but first, to make sure your backend is created when the application initializes Qt Purchasing, edit the QInAppPurchaseBackendFactory to create an instance of your class.


QInAppPurchaseBackend *QInAppPurchaseBackendFactory::create()
{
#if defined(Q_OS_ANDROID)
    return new QAndroidInAppPurchaseBackend;
#elif defined (Q_OS_MAC)
    return new QMacInAppPurchaseBackend;

// Making a backend for ACME OS #elif defined (Q_OS_ACME) return new QAcmeInAppPurchaseBackend;

#else return new QInAppPurchaseBackend; #endif }

Note that an application is assumed to connect to single store which is identifiable at run-time.

The initialize() and isReady() functions

If your backend needs to do any initialization work, implement the initialize() function. This is called when the user of the APIs registers the first product, either by calling QInAppStore::registerProduct(), or declaratively in QML.

The initialize() function should either execute all necessary steps to initialize() synchronously and then emit the ready() signal, or, if the process is asynchronous, as it is with Google Play, it should launch the initialization process and emit ready() when it's done.


void QAcmeInAppPurchaseBackend::initialize()
{
    NativeAcmeAPI::connectToAppStore(this);
}

void QAcmeInAppPurchaseBackend::nativeAcmeAppStoreReadyCallback() { m_isReady = true; emit ready(); }

bool QAcmeInAppPurchaseBackend::isReady() const { return m_isReady; }

The isReady() function should return true if, and only if, the backend is ready.

The queryProducts() and queryProduct() functions

Once the backend has been initialized, it should be ready for calls to either queryProducts() or queryProduct(). Since the underlying APIs might not support getting a list of all products associated with the application, the user will register them manually with the IDs they have been assigned in the external store.

The queryProduct() function should be implemented to collect information about the product from the external store. If the product does not exist, it should emit productQueryFailed(), or it should emit productQueryDone() when the information has been collected. This can also be done asynchronously if necessary.

Implementing the queryProducts() function is optional. It can be used to bundle more than one product in a single request. By default, the function will just call queryProduct() for each of the items in the list.


void QAcmeInAppPurchaseBackend::queryProduct(QInAppProduct::ProductType productType,
                                             const QString &identifier)
{
    NativeAcmeAPIProduct *nativeProduct = NativeAcmeAPI::getProduct(identifier);
    if (nativeProduct == 0) {
        emit productQueryFailed(productType, identifier);
        return;
    }

QAcmeInAppProduct *product = new QAcmeInAppProduct(identifier, productType, nativeProduct->price(), nativeProduct->title(), nativeProduct->description()); emit productQueryDone(product); }

The product type in the query helps the user differentiate between products that can only be purchased once (unlockables) and that can be purchased any number of times (consumables). In the case where the external store supports the same categories, the backend should report failure if the product was queried with the wrong type. On some systems, however, such as Google Play, the type will only affect how the product is used: If it is requested as consumable, the product will be consumed and can be purchased again once the application has finalized the transaction. If it is unlockable, the transaction will be stored in Google Play's database forever and will never happen again.

Note the use of a custom QInAppProduct subclass in the hypothetical code above. More about this later.

The restorePurchases() function

Restoring previous purchases originates from the concept on iOS and OS X. In case the user has uninstalled and reinstalled the application (or installed the same application on a new device), the application is expected to provide a way for the user to re-register all the unlockable products they have previously purchased. This requires the user to input their password on the device, so the event should be triggered by them, e.g. by clicking on a button in the UI.

When this function is called, the backend is expected to emit a transactionReady() signal for each of the unlockable products currently owned by the user, and they will each need to be finalized again.


void QAcmeInAppPurchaseBackend::restorePurchases()
{
    for (int i = 0; i < NativeAcmeAPI::transactionCount(); ++i) {
            NativeAcmeTransaction *nativeTransaction = NativeAcmeAPI::transaction(i);
            nativeTransaction->setFinalized(false);

QAcmeInAppTransaction *transaction = new QAcmeInAppTransaction(QinAppTransaction::PurchaseApproved, product(nativeTransaction->identifier())); transaction->setOrderId(nativeTransaction->orderId()); transaction->setTimestamp(nativeTransaction->purchaseTimestamp());

emit transactionReady(transaction); } } }

More about finalization of transactions later.

The setPlatformProperty() function

The setPlatformProperty() function is optional, and for the sake of giving a good cross-platform experience, it should be used as little as possible. It is provided, though, as a last resort for those very platform-specific APIs which do not make sense on other platforms.

For instance, Google Play has the concept of a public key which can be used to verify purchases. This is inherently platform-specific, because even if another store had the exact same concept, the public key would not be the same, so the developer would still have to specify it on a per-platform level.

If we were to attempt to provide a cross-platform API for this, we would quickly end up with ugly application code such as the following.


void MyApplication::initialize()
{
// Yuck
#if defined(Q_OS_ANDROID)
    m_store->setPublicKey("ABCABCABC");
#elif defined(Q_OS_ACME)
    m_store->setPublicKey("DEFDEFDEF");
#endif
}

A better solution is to bake the platform name into the property name and provide something which does not require #ifdefs when using the APIs.


void MyApplication::initialize()
{
    m_store->setPlatformProperty("AndroidPublicKey", "ABCABCABC");
    m_store->setPlatformProperty("AcmePublicKey", "DEFDEFDEF");
}

So you can implement the function if there are similar concepts on the platform you are targeting, but always consider whether expanding the cross-platform APIs make the end result simpler.

QInAppProduct

The next class we want to subclass is QInAppProduct. This is an abstraction of a single product which can be purchased in the store. It should be populated with information on construction, and the purchase() function must be implemented.


QAcmeInAppProduct::QAcmeInAppProduct(const QString &price,
                                     const QString &title,
                                     const QString &description,
                                     QInAppProduct::ProductType productType,
                                     const QString &identifier)
: QInAppProduct(price, title, description, productType, identifier) {}

void QAcmeInAppProduct::purchase() { NativeAcmeAPI::attemptPurchase(identifier()); }

void QAcmeInAppPurchaseBackend::nativeAcmePurchaseAcceptedCallback(const QString &orderId, const QString &timestamp) { QAcmeInAppTransaction *transaction = new QAcmeInAppTransaction(QInAppTransaction::PurchaseApproved, product(identifier())); transaction->setOrderId(orderId); transaction->setTimestamp(timestamp);

emit transactionReady(transaction); }

void QAcmeInAppPurchaseBackend::nativeAcmePurchaseRejectedCallback() { QAcmeInAppTransaction *transaction = new QAcmeInAppTransaction(QInAppTransaction::PurchaseFailed, product(identifier())); transaction->setOrderId(orderId); transaction->setTimestamp(timestamp); transaction->setFailureReason(QInAppTransaction::CanceledByUser);

emit transactionReady(transaction); }

void QAcmeInAppPurchaseBackend::nativeAcmePurchaseErrorCallback(const QString &errorString) { QAcmeInAppTransaction *transaction = new QAcmeInAppTransaction(QInAppTransaction::PurchaseFailed, product(identifier())); transaction->setOrderId(orderId); transaction->setTimestamp(timestamp); transaction->setFailureReason(QInAppTransaction::ErrorOccurred); transaction->setErrorString(errorString);

emit transactionReady(transaction); }

You should always follow up a call to purchase() with an emission of the transactionReady() signal. The signal is used to communicate both successful purchases and failed ones. If possible, differentiate between failures caused by errors and those caused by the end user rejecting the transaction.

QInAppTransaction

The final class to subclass is QInAppTransaction. At the minimum, you will have to implement the finalize() function, but there are also other virtual functions that can be used to provide useful information from the system.

The orderId(), failureReason(), errorString(), timestamp() and platformProperty() functions

First off are some simple accessor functions. They are implemented as virtual functions to provide flexibility with regards to how they are implemented. A simple implementation, for instance, would be to have symmetrical setters for them in the class.


QString QAcmeInAppTransaction::orderId() const
{
    return m_orderId;
}

void QAcmeInAppTransaction::setOrderId(const QString &orderId) { m_orderId = orderId; }

...and so forth.

The order ID is a unique identifier for the transaction, if it were successful, or just empty if not. This and the timestamp can be used by the application developer for logging purposes, e.g. to help the customer later on if they wish to cancel the purchase after it has already been approved.

The failure reason gives the cause of a failed transaction (or NoFailure if it were successful), and the error string can give extra information from the system if an error occurred.

The platformProperty() function is the inverse of the setPlatformProperty() in QInAppPurchaseBackend. If the transaction contains platform specific data that does not make sense as common APIs, it can be exposed through this function, otherwise it can be ignored.

The finalize() function

The last function we'll discuss is the finalize() function. In order to make transactions safe against sudden crashes or shutdowns due to battery running out, each transaction is handled in two steps. First the user approves the purchase on their side, which emits a transactionReady() signal, and then the application verifies that it has registered the necessary information from the transaction by calling finalize() on it. If a product has been purchased by the user and the transaction has not yet been finalized, the application should keep emitting transactionReady() signals for the transaction until it has been finalized to make sure the information is not lost. This can be done e.g. on startup, when the backend is initialized.

The application has to be written to support multiple transactionReady() signals for the same transactions, so it's better to emit the signal too many times than too few.


void QAcmeInAppPurchaseBackend::initialize()
{
    /* ... */

for (int i = 0; i < NativeAcmeAPI::unfinalizedTransactionCount(); ++i) { NativeAcmeTransaction *nativeTransaction = NativeAcmeAPI::unfinalizedTransaction(i); QAcmeInAppTransaction *transaction = new QAcmeInAppTransaction(QinAppTransaction::PurchaseApproved, product(nativeTransaction->identifier())); transaction->setOrderId(nativeTransaction->orderId()); transaction->setTimestamp(nativeTransaction->purchaseTimestamp());

emit transactionReady(transaction); } }

This addition to the initialize() function will go through all transactions that have yet to be finalized on startup and emit new transactionReady() signals for them, ensuring that the application is notified of the transactions.

Thank you!

I hope this was a useful introduction to the Qt Purchasing backend interfaces. Excuse any mistakes or typos in the code snippets. They were all done inline for illustration purposes, so even if there was such a thing as a NativeAcmeAPI, it's quite possible they do not compile.

Do let me know if you have any questions, either here, or look me up on IRC. Good luck!


Blog Topics:

Comments