In-App Purchases Made Simple

The process of adding in-app purchases to a Cascades app has proven daunting for many of you out there, but today I will attempt to make life easier for all of you.  About 6 months ago, when I first attempted to add in-app purchases to a Cascades app, there was a lot of head scratching and staring at the screen, but eventually it clicked well enough for me to implement.  After that, I spent a lot of time really wrapping my head around the code, and I am finally at a point where I can simplify the presentation of this code. Interested? Keep reading...

First off, the bulk of the functional code in my sample comes courtesy of the Freemium Sample app on the BlackBerry GitHub. I used the aformentioned sample to figure out the whole process, but in this sample I have attempted to make it much easier to understand by simplifying the QML and explaining what's going on in the code.

Note: This code has been tested only on BlackBerry 10 OS 10.2

Before trying to implement this in an existing project, I suggest starting a new project and examining this sample to ensure you understand the code. Also, before beginning, please compile and run the attached sample to see how it functions. With that in mind, create a new blank Cascades project, let's drop in some files and code pulled directly from the Freemium App Sample project.

First things first, grab the PurchaseStore.cpp and PurchaseStore.hpp files from the BlackBerry GitHub repo and drop those, as-is, into your projects src directory. Next open up your project's applicationui.cpp file and add the code in blue below.

applicationui.cpp

#include "applicationui.hpp"
#include <bb/cascades/Application>
#include <bb/cascades/QmlDocument>
#include <bb/cascades/AbstractPane>
#include <bb/cascades/LocaleHandler>
using namespace bb::cascades;
ApplicationUI::ApplicationUI(bb::cascades::Application *app) :
        QObject(app)
{
m_purchaseStore = new PurchaseStore(this);
// prepare the localization m_pTranslator = new QTranslator(this); m_pLocaleHandler = new LocaleHandler(this); bool res = QObject::connect(m_pLocaleHandler, SIGNAL(systemLanguageChanged()), this, SLOT(onSystemLanguageChanged())); // This is only available in Debug builds Q_ASSERT(res); // Since the variable is not used in the app, this is added to avoid a // compiler warning Q_UNUSED(res); // initial load onSystemLanguageChanged(); // Create scene document from main.qml asset, the parent is set // to ensure the document gets destroyed properly at shut down. QmlDocument *qml = QmlDocument::create("asset:///main.qml").parent(this);
qml->setContextProperty("PurchaseStore", m_purchaseStore);
// Create root object for the UI AbstractPane *root = qml->createRootObject(); // Set created root object as the application scene app->setScene(root); } void ApplicationUI::onSystemLanguageChanged() { QCoreApplication::instance()->removeTranslator(m_pTranslator); // Initiate, load and install the application translation files. QString locale_string = QLocale().name(); QString file_name = QString("SimpleInAppPurchases_%1").arg(locale_string); if (m_pTranslator->load(file_name, "app/native/qm")) { QCoreApplication::instance()->installTranslator(m_pTranslator); } }

An now add the following code in blue to you project's applicationui.hpp file

applicationui.hpp

#ifndef ApplicationUI_HPP_
#define ApplicationUI_HPP_
#include 
#include "PurchaseStore.hpp"
namespace bb { namespace cascades { class Application; class LocaleHandler; } } class QTranslator; /*! * @brief Application object * * */ class ApplicationUI : public QObject { Q_OBJECT public: ApplicationUI(bb::cascades::Application *app); virtual ~ApplicationUI() { } private slots: void onSystemLanguageChanged(); private: QTranslator* m_pTranslator; bb::cascades::LocaleHandler* m_pLocaleHandler;
PurchaseStore *m_purchaseStore;
}; #endif /* ApplicationUI_HPP_ */

The QML

Moving to the QML now, open up the main.qml file and reference the code below.  Please see the comments in the code for context and explanations.

import bb.cascades 1.2
//import the bb.platform library so we can use the Payment Manager
import bb.platform 1.2
Page {
    Container {
        //Todo: fill me with QML
        layout: DockLayout {
        }
        horizontalAlignment: HorizontalAlignment.Fill
        verticalAlignment: VerticalAlignment.Fill
        Container {
            horizontalAlignment: HorizontalAlignment.Center
            verticalAlignment: VerticalAlignment.Center
            /*
             * Below we call digital goods created from the DigitalGood.qml document
             * Obviously, digital goods don't require a UI, but for demonstrative purposes, these have UI
             * Assigned to each digital good must be a sku in the form of a string
             * The sku is a unique identifier used by BlackBerry World to associate a purchase with a product
             * In order to request a purchase from BlackBerry World, a sku is absolutely required!
             */
            DigitalGood {
                id: digitalgood1
                sku: "prodsku0001"
                goodPayMentManager: storePaymentManager
                horizontalAlignment: HorizontalAlignment.Center
            }
            Divider {
            }
            DigitalGood {
                id: digitalgood2
                sku: "prodsku0002"
                goodPayMentManager: storePaymentManager
                horizontalAlignment: HorizontalAlignment.Center
            }
            //The button below clears the local purchase store and is meant to beused during testing only
            Button {
                text: "Delete all Purchases"
                horizontalAlignment: HorizontalAlignment.Center
                onClicked: {
                    onClicked: PurchaseStore.deletePurchaseRecords()
                }
            }
        }
    }
    attachedObjects: [
        /*
         * Below is the Payment Manager which allows BlackBerry device users to initiate the purchase of digital goods from within an application
         * In other words, the PaymentManager is what communicates with BlackBerry World
         * 
         */
        PaymentManager {
            id: storePaymentManager
            property bool busy: false
            onExistingPurchasesFinished: {
                storePaymentManager.busy = false;
                //If there's no error message in the response...
                if (reply.errorCode == 0) {
                    //...save the purchase(s) to local cache
                    for (var i = 0; i < reply.purchases.length; ++ i) {
                        //If an existing purchase is found, the purchase(s) is/are stored locally...
                        PurchaseStore.storePurchase(reply.purchases[i]["digitalGoodSku"]);
                        console.log(reply.purchases[i].receipt["digitalGoodSku"]);
                    }
                } else {
                    //if there is an error, we throw the error to the console
                    console.log("Error: " + reply.errorText);
                }
            }
            onPurchaseFinished: {
                //this signal fires when the porcess of making a purchase is complete
                //If there's no error message in the response...
                if (reply.errorCode == 0) {
                    //...save the purchase to local cache
                    PurchaseStore.storePurchase(reply.digitalGoodSku);
                } else {
                    console.log("Error: " + reply.errorText);
                }
            }
        }
    ]
    onCreationCompleted: {
        //The connection mode is set, in this sample, to 0 which is local/sandboxmode.
        //This must be changed to Production Mode (1) before publishing to BlackBerry World
        storePaymentManager.setConnectionMode(0); // 0=sandbox mode | 1=production mode
        // As soon as the page loads, lets retrieve any past purchases from local store
        PurchaseStore.retrieveLocalPurchases();
        //Below, we also retrieve the BlackBerry World cache to check for existing purchases
        storePaymentManager.requestExistingPurchases(false);
        //See Digitalgood.qml...
    }
}

To simplify things, I use a custom DigitalGood.qml file in the assets directory. So create this file and reference the code below.

DigitalGood.qml

import bb.cascades 1.2
//import the bb.platform library so we can use the Payment Manager
import bb.platform 1.2
Container {
    /*
     * Don't get confused here...
     * All we are doing with the goodPayMentManager property is taking the 'storePaymentManager' from main.qml
     * and exposing it as a property here in the Digitalgood.qml.
     * If you reference the main.qml, you will see we assign storePaymentManager to this poroperty
     * This is neccessary in order to communicate with the PaymentManager (in main.qml) from this qml document 
     * 
     */
    property PaymentManager goodPayMentManager
    // The sku string property is used to store the sku of the digital good
    property string sku
    // The owned property indicates whether or not the user currently owns the project
    property bool owned: false
    onOwnedChanged: {
        //Simple color change to correspond with the status of 'owned'
        if (owned == true) {
            background = Color.create("#8000ffff")
        } else {
            background = Color.DarkRed
        }
    }
    background: Color.DarkRed
    topPadding: 12
    bottomPadding: 12
    leftPadding: 12
    rightPadding: 12
    Label {
        text: "Hi! I'm a digital good!"
    }
    Button {
        text: "Buy me"
        enabled: ! owned
        onClicked: {
            // When this button is clicked, we initiate the transaction, and pass the sku (assigned in main.qml)
            goodPayMentManager.requestPurchase("", sku, "", "", "")
        }
    }
    Label {
        // visual indication of the good's owned status
        text: "Owned: " + owned
    }
    /*
     * VERY IMPORTANT -- this function is connected to a signal emitted by the PurchaseStore.cpp
     * The signal (purchaseRetrieved) is emitted anytime the sku property for the digital good matches a sku
     * in the local purchase store. So, if the sku matches a digital good, that digital good's 'owned'
     * property changes to true indicating the digital good is owned by the user
     */
    function purchaseMade(digitalGoodSku) {
        owned |= (digitalGoodSku == sku);
    }
    //This is used generally only when testing and is tied to a signal indicating that the local purchase store has been cleared
    function purchaseRecordsDeleted() {
        owned = false;
    }
    onCreationCompleted: {
        //This is where the purchaseRetrieved signal is connected to the purchaseMade() JS funtion above
        PurchaseStore.purchaseRetrieved.connect(purchaseMade);
        PurchaseStore.purchaseRecordsDeleted.connect(purchaseRecordsDeleted);
    }
}

The .pro file

Another consideration is your project's .pro file. Add the following library (in blue) to your profile as follows...

APP_NAME = SimpleInAppPurchases


LIBS += -lbbplatform
CONFIG += qt warn_on cascades10 include(config.pri)

Wrapping Up... 

I hope the comments in the QML above explain what is going on, the sample is actually very simple, so if you feel overwhelmed just step back and let all this sink in. Please keep in mind that this sample project is pretty bare bones, so be careful because omitting code in the sample could break it. Just make sure you understand the code before chopping it up.

That's about it! Take your time with this sample, and as always Happy Coding!

Leave your comments

Post comment as a guest

0
  • No comments found
Subscribe to the official OSBB BBM Channel!

osbbchannelQR

C00013E89

Back to top