Table of Contents
This project provides a library wrapper, that provides an abstraction of the native app store libraries, and makes the necessary functionalities available in Qt6 and QML. Compatible with Apple App Store, Google Play Store, and Microsoft Store.
Here's why:
- In-App-Purchasing might be an important way of monetizing your Qt/QML mobile app.
- QtPurchasing didn't make it to Qt6.
To add In-App-Purchasing capabilities to your Qt6/QML project follow the steps below.
- Qt/QML 6 (6.8 or higher)
- Apple StoreKit (iOS)
- Android Billing Client (Android)
- Windows SDK with WinRT support (Windows)
- Clone this repo into a folder in your project.
git clone https://github.com/moritzstoetter/qt6purchasing.git- Copy 'android/GooglePlayBilling.java' to
QT_ANDROID_PACKAGE_SOURCE_DIR/src/com/COMPANY_NAME/APP_NAME/GooglePlayBilling.javaFor more information on how to include custom Java-Code in your Android App see Deploying an Application on Android. - Add the qt6purchasing library's QML plugin to your project. In your project's
CMakeLists.txtadd the following:- Ensure your project applies Qt 6.8 policies.
qt_standard_project_setup(REQUIRES 6.8)
- Make this library's QML components available in the same build folder as all your own.
set(QT_QML_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) - Link your app target to this library's QML module.
target_link_libraries(APP_TARGET PRIVATE ... qt6purchasinglibplugin )
- Ensure your project applies Qt 6.8 policies.
For Windows applications using Microsoft Store integration, you must initialize COM apartment mode in your application's main.cpp:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#ifdef Q_OS_WIN
#include <windows.h>
#include <winrt/base.h>
#endif
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
#ifdef Q_OS_WIN
// CRITICAL: Fix COM apartment initialization for WinRT
try {
winrt::uninit_apartment();
winrt::init_apartment(); // Defaults to multi-threaded
} catch (...) {
// Continue anyway - some functionality might still work
}
#endif
QQmlApplicationEngine engine;
engine.loadFromModule("YourModule", "Main");
return app.exec();
}Why this is required:
- Qt initializes COM in single-threaded apartment mode
- WinRT Store APIs require multi-threaded apartment mode
- This must be done at the process level before any WinRT usage
- Without this, Store API calls will hang indefinitely
Windows products require both a generic identifier and a Microsoft-specific microsoftStoreId:
Qt6Purchasing.Product {
identifier: "premium_upgrade" // Generic cross-platform identifier
microsoftStoreId: "9NBLGGH4TNMP" // Store ID from Microsoft Partner Center
type: Qt6Purchasing.Product.Unlockable
}The microsoftStoreId should be the exact Store ID from your Microsoft Partner Center add-on configuration.
- Durable → Maps to
Product.Unlockable(non-consumable, purchased once) - UnmanagedConsumable → Maps to
Product.Consumable(can be repurchased after consumption)
The library automatically handles consumable fulfillment for Microsoft Store. When you call store.finalize(transaction) on a consumable purchase, the library reports fulfillment to Microsoft Store with a unique tracking ID, allowing the user to repurchase the same consumable.
On iOS, you must call the early initialization in your main() function before creating the QML engine:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#ifdef Q_OS_IOS
#include "apple/appleappstorebackend.h"
#endif
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
#ifdef Q_OS_IOS
// Critical: Initialize iOS IAP observer before QML engine creation
AppleAppStoreBackend::initializeEarly();
#endif
QQmlApplicationEngine engine;
engine.loadFromModule("YourModule", "Main");
return app.exec();
}Why this is required:
- Apple requires transaction observers to be added at app launch in
application(_:didFinishLaunchingWithOptions:) - Qt/QML creates the Store component later during QML instantiation
- Without early initialization, transactions that occur during app startup can be lost
- The early observer queues transactions until the full backend is ready
- In your QML file include the purchasing module:
import Qt6Purchasing- Use it like this, for a product that is called "test_1" in the app store(s):
Qt6Purchasing.Store {
id: iapStore
Qt6Purchasing.Product {
id: testingProduct
identifier: "test_1"
type: Qt6Purchasing.Product.Consumable
}
}
StoreItem {
id: testingStoreItem
product: testingProduct
onIapCompleted: root.accepted()
}StoreItem.qml:
import Qt6Purchasing
Item {
id: root
required property Qt6Purchasing.Product product
signal iapCompleted
enum PurchasingStatus {
NoPurchase,
PurchaseProcessing,
PurchaseSuccess,
PurchaseFail
}
property int purchasingStatus: StoreItem.PurchasingStatus.NoPurchase
function purchase() {
purchasingStatus = StoreItem.PurchasingStatus.PurchaseProcessing
product.purchase()
}
function finalize(transaction) {
purchasingStatus = StoreItem.PurchasingStatus.PurchaseSuccess
iapStore.finalize(transaction)
}
Connections {
target: product
function onPurchaseSucceeded(transaction) {
finalize(transaction)
}
function onPurchaseRestored(transaction) {
finalize(transaction)
}
function onPurchaseFailed(error, platformCode, message) {
purchasingStatus = StoreItem.PurchasingStatus.PurchaseFail
}
function onConsumePurchaseSucceeded(transaction) {
root.iapCompleted()
}
}
}- Important: Call
store.finalize(transaction)in bothonPurchaseSucceededandonPurchaseRestoredhandlers. This ensures:- Consumables are properly consumed and can be repurchased
- Durables/Unlockables complete their transaction acknowledgment
- Platform backends handle the finalization appropriately for each product type
Understanding these scenarios is critical for robust production deployment:
What happens: Purchase requires external approval (parental consent, payment verification).
Platform notes:
- iOS:
SKPaymentTransactionStateDeferredtriggers pending state for Ask to Buy scenarios. - Android: Family-managed accounts with purchase approval requirements trigger pending state.
- Windows: Microsoft family accounts with purchase restrictions cause deferred purchases.
Library behaviour: Automatically detects deferred transactions and emits purchasePending signal. The platform keeps the transaction in queue awaiting approval.
Developer action: Handle onPurchasePending to update UI showing "awaiting approval" state. Do not grant content. Wait for final onPurchaseSucceeded or onPurchaseFailed callback.
What happens: Purchase starts but network fails mid-transaction.
Platform notes:
- iOS: Transaction may remain in
SKPaymentTransactionStatePurchasingindefinitely until network recovers. - Android: Returns
BillingResponseCode.SERVICE_UNAVAILABLEor similar network errors. - Windows: Store API calls timeout and purchase dialog may remain open.
Library behaviour: Maps platform-specific network errors to PurchaseError enum values. Transactions may remain in purchasing state until network recovers.
Developer action: Handle onPurchaseFailed with network-related errors gracefully. Allow retry attempts. Never grant content without confirmed onPurchaseSucceeded.
What happens: Transaction completes on platform side but app crashes before calling store.finalize(transaction).
Platform notes:
- iOS: Unfinished transactions remain in
SKPaymentQueueand are redelivered on app launch. - Android: Unacknowledged purchases remain available via
queryPurchases()on startup. - Windows: Unfulfilled consumables remain in purchase history until consumed.
Library behaviour: Automatically detects unfinished transactions on next app launch. Delivers them via purchaseRestored signal during startup. Maintains transaction queue integrity across app sessions.
Developer action: Always handle onPurchaseRestored the same way as onPurchaseSucceeded. Call store.finalize(transaction) in both handlers. Implement idempotent content delivery - check if user already has the purchased item before granting it again.
What happens: Purchase succeeds but store.finalize(transaction) is never called (due to app crashes, network issues, or code bugs).
Platform notes:
- iOS: Blocks future purchases of same consumable with "already owned" error until consumed.
- Android: Similar blocking behavior - consumable marked as owned until acknowledged.
- Windows: Consumable fulfillment not reported to Store, may prevent repurchase.
Library behaviour: Preserves transaction data and platform purchase state. On next app launch, unfinalized consumables are delivered via the purchase restore process (see scenario 3 above). The library treats restored consumables the same as any other restored purchase.
Developer action: ALWAYS call store.finalize(transaction) in both onPurchaseSucceeded AND onPurchaseRestored handlers for consumables. This ensures consumables are finalized even if the app crashed after purchase. Implement consumption tracking to ensure no consumables are left unfinalized.
What happens: User redeems offer code directly from platform store, bypassing your app's purchase flow.
Platform notes:
- iOS: App Store promotional codes trigger transaction observer without app-initiated purchase.
- Android: Play Store promotional codes and Play Pass redemptions trigger purchase callbacks.
- Windows: Microsoft Store promotional codes trigger purchase notifications.
Library behaviour: Delivers promotional purchases through normal purchaseSucceeded signal flow. No special handling required from library perspective.
Developer action: Handle unexpected onPurchaseSucceeded calls that weren't initiated by your app's UI. Don't assume all purchases originate from user tapping your purchase buttons.
What happens: Platform store services are temporarily unavailable.
Platform notes:
- iOS: App Store downtime typically causes
NetworkErrororUnknownErrorrather than specific service unavailable errors. - Android: Google Play Billing service interruptions mapped to
ServiceUnavailablefor billing disconnections. - Windows: Microsoft Store maintenance periods mapped to
ServiceUnavailablefor server errors and store disconnections.
Library behaviour: Maps platform service errors to appropriate PurchaseError values. Maintains app stability during store outages.
Developer action: Handle onPurchaseFailed with service-related errors gracefully. Show user-friendly "store temporarily unavailable" messages. Implement retry mechanisms with reasonable delays.
What happens: Restore purchases doesn't return all expected results due to account or platform limitations.
Platform notes:
- iOS: Cross-device restore requires same Apple ID and account mismatches prevent some restores.
- Android: Restore works via Google account and purchase history is tied to specific Google account.
- Windows: Microsoft account-based restore with purchases tied to specific Microsoft account and device family.
Library behaviour: Calls platform restore APIs and delivers all available restored purchases via purchaseRestored signals. Logs warnings for purchases that cannot be mapped to registered products.
Developer action: Handle partial restore scenarios gracefully. Inform users about account requirements for restore (same Apple ID, Google account, Microsoft account). Don't assume restore will return all historical purchases.
What happens: Different behavior between testing and production deployments.
Platform notes:
- iOS: Sandbox environment doesn't support Ask to Buy testing and receipt validation differs from production.
- Android: Testing tracks have different validation requirements and purchase flows than production.
- Windows: Debug/development apps have different Store integration behavior than published apps.
Library behaviour: Works in both sandbox/testing and production environments. Provides same API surface across environments.
Developer action: Test thoroughly in production environment before release. Document sandbox limitations for your QA team. Be aware that some features (like iOS Ask to Buy) cannot be tested in sandbox environments.
Important: Store backends must be created and destroyed on the main thread. The library uses static instances internally for routing platform callbacks, which requires main-thread access for thread safety.
In QML, this happens automatically since QML components are created on the main thread.
Any contributions you make are greatly appreciated.
If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!
- Fork the Project
- Create your Feature Branch (
git checkout -b feature/AmazingFeature) - Commit your Changes (
git commit -m 'Add some AmazingFeature') - Push to the Branch (
git push origin feature/AmazingFeature) - Open a Pull Request
Distributed under the MIT License. See LICENSE.txt for more information.
Moritz Stötter - moritzstoetter.dev - [email protected]