Overview
KlugBilling sells one-time products and subscriptions from your AIR app. On iPhone and iPad it talks to the App Store, on Android to Google Play, and your ActionScript code stays the same for both.
It loads your products with localized titles and prices, opens the store's purchase sheet, reports every step of a transaction and closes it once you have delivered the product. Restoring earlier purchases, the iOS app receipt and the signed purchase data of Google Play are included, so your server can verify every sale.
On Android KlugBilling uses the current Google Play Billing Library 8. It is added automatically when the app is packaged and meets Google Play's requirements for new apps and updates.
KlugBilling sells the subscriptions of our apps on iOS and Android, our proof of concept.
Features
- Self-contained: one ANE, no core library or dependency ANEs to add
- One API for the App Store and Google Play
- One-time products and auto-renewing subscriptions
- Localized titles, descriptions and prices straight from the store
- Subscription periods, offers and pricing phases
- Every transaction state as an event: purchasing, purchased, failed, cancelled, restored, deferred
- Restore purchases (iOS) and list owned purchases (Android)
- Data for server-side checks: the iOS app receipt, Google's signed purchase data and signature
- Android: purchases are checked against your Play public key
Supported platforms
On other platforms InAppBilling.isSupported returns false, so the same code can stay in a cross-platform app.
Getting started
Add the extension to your application descriptor:
<extensions>
<extensionID>com.klug.billing</extensionID>
</extensions>
No manifest entries and no activities are needed. Create your products in App Store Connect and in the Google Play Console with the same product IDs, and copy the base64 public key of your app from the Play Console (Monetization setup).
Code examples
Four complete flows, taken from the API that runs in our apps. Every call returns at once; results arrive as events on InAppBilling.service. Full documentation →
import com.klug.billing.*; import com.klug.billing.events.*; var store:InAppBillingService = InAppBilling.service; if ( InAppBilling.isSupported ) // iOS, Android { store.addEventListener( InAppBillingEvent.SETUP_SUCCESS, onSetup ); store.addEventListener( InAppBillingEvent.SETUP_FAILURE, onSetupFailed ); store.addEventListener( PurchaseEvent.PURCHASES_UPDATED, onPurchasesUpdated ); // see 3 var service:BillingService = new BillingService(); if ( store.platform == "android" ) service.setServiceType( InAppBillingServiceTypes.GOOGLE_PLAY_INAPP_BILLING ) .setGooglePlayPublicKey( "MIIBIjANBgkqh..." ); // from the Play Console, Android only else service.setServiceType( InAppBillingServiceTypes.APPLE_INAPP_PURCHASE ); store.setup( service ); } function onSetup( e:InAppBillingEvent ):void { store.checkAvailability( function( result:String ):void { if ( result == InAppBillingAvailability.AVAILABLE ) loadProducts(); // see 2 else hideShop(); // e.g. purchases disabled by Screen Time }); }
function loadProducts():void { store.addEventListener( ProductEvent.PRODUCTS_LOADED, onProducts ); store.addEventListener( ProductEvent.PRODUCTS_FAILED, function( e:ProductEvent ):void { trace( "store not reachable: " + e.message ); }); // one-time products first, subscriptions second store.getProducts( [ "com.example.removeads" ], [ "com.example.pro.monthly", "com.example.pro.yearly" ] ); } function onProducts( e:ProductEvent ):void { for each ( var p:Product in e.products ) { // e.g. "Pro Yearly · 19,99 €" in the user's currency and language addShopButton( p.id, p.title + " · " + p.priceString ); // your app function } }
function buy( productId:String ):void { var request:PurchaseRequest = new PurchaseRequest().setProductId( productId ); if ( store.checkPurchaseRequestValid( request ) ) store.makePurchase( request ); } // every step of every transaction, also open ones from earlier sessions function onPurchasesUpdated( e:PurchaseEvent ):void { for each ( var p:Purchase in e.data ) { switch ( p.transactionState ) { case Purchase.STATE_PURCHASED: case Purchase.STATE_RESTORED: // iOS: p.transactionReceipt (app receipt) // Android: p.originalMessage + p.signature (Google's signed purchase data) verifyOnServer( p, function():void { // your server call unlock( p.productId ); // your app function store.finishPurchase( p ); // close it, or the store sends it again }); break; case Purchase.STATE_FAILED: case Purchase.STATE_CANCELLED: store.finishPurchase( p ); break; case Purchase.STATE_DEFERRED: showHint( "Waiting for approval" ); // e.g. Ask to Buy break; } } }
function restore():void { if ( store.isGetPurchasesSupported ) // Android: list what the Google account owns { store.addEventListener( PurchaseEvent.GET_PURCHASES_COMPLETE, function( e:PurchaseEvent ):void { for each ( var p:Purchase in e.data ) unlock( p.productId ); }); store.getPurchases(); } else // iOS: restored transactions arrive in onPurchasesUpdated with STATE_RESTORED { store.addEventListener( PurchaseEvent.RESTORE_PURCHASES_SUCCESS, function( e:PurchaseEvent ):void { showHint( "Purchases restored" ); }); store.restorePurchases(); } }
API overview
| InAppBilling.isSupported | True on iOS and Android, false elsewhere. |
| InAppBilling.service | The single store service; all calls and events below. |
| setup(service) | Connects to the store described by a BillingService. Result: SETUP_SUCCESS or SETUP_FAILURE. |
| checkAvailability(callback) | AVAILABLE, NOT_AVAILABLE or STORE_UPGRADE_REQUIRED. |
| canMakePayments | False when purchases are disabled on the device. |
| getProducts(productIds, subscriptionIds) | Loads store data. Result: PRODUCTS_LOADED, INVALID_PRODUCT or PRODUCTS_FAILED. |
| getProduct(id) | A loaded Product: title, description, price, priceString, currencyCode, subscriptionPeriod, subscriptionOffers. |
| makePurchase(request) | Opens the store's purchase sheet for a PurchaseRequest; optional subscription offer. |
| finishPurchase(purchase) | Closes a transaction after delivery. Result: FINISH_SUCCESS or FINISH_FAILED. |
| getPendingPurchases() | Transactions the store still holds open. |
| restorePurchases() | iOS: restores earlier purchases of the Apple ID. |
| getPurchases() | Android: lists the purchases of the Google account. |
| applicationReceipt | iOS: the app receipt as base64 for your server, with refresh(). |
| dispose() | Disconnects from the store. |
Events
| InAppBillingEvent | SETUP_SUCCESS, SETUP_FAILURE, RESTORE_PURCHASES_SUCCESS, RESTORE_PURCHASES_FAILED |
| ProductEvent | PRODUCTS_LOADED (products), INVALID_PRODUCT, PRODUCTS_FAILED |
| PurchaseEvent | PURCHASES_UPDATED (data = Purchase objects), PURCHASE_FAILED, FINISH_SUCCESS, FINISH_FAILED, RESTORE_PURCHASES_SUCCESS, RESTORE_PURCHASES_FAILED, GET_PURCHASES_COMPLETE, GET_PURCHASES_FAILED |
| AvailabilityEvent | COMPLETE with the availability result |
| ApplicationReceiptEvent | REFRESH_SUCCESS, REFRESH_FAILED |
Buy KlugBilling
A yearly licence for one AIR application, with all updates for 12 months. After payment you receive the extension, the ActionScript library and the documentation by e-mail, usually within one business day. Licence and terms
- Licence for one AIR application
- All updates for 12 months
- Answer by e-mail within 7 days
- Licence for one AIR application
- All updates for 12 months
- Answer by e-mail within 48 hours
Requirements
| AIR SDK | 51.x (HARMAN) |
| iOS | iOS 13 or later, products in App Store Connect |
| Android | Android 6.0 (API 23) or later, products in the Google Play Console |
| Google Play | Play Billing Library 8.3, added at packaging |
| Server (recommended) | Verifies the iOS app receipt or Google's signed purchase data |