Skip to main content

Myket In-App Purchase Guide for Java and Kotlin

Android Native
Sample project

For a complete, runnable Myket Android in-app billing implementation, see the official sample project on GitHub.

The Myket Billing Client SDK is the official Android SDK for integrating In-App Purchases (IAP) in applications distributed through the Myket Android App Store. The SDK supports both Kotlin and Java.

Features​

  • In-App Products: Supports consumable products (such as coins, fuel, or credits) and non-consumable products (such as premium upgrades or ad removal).
  • Built-in Signature Verification: RSA signature verification using the store public key to validate transactions.
  • 3-Tier Fallback Mechanism:
    1. IPC Service Binding: Direct AIDL binding to the Myket market service.
    2. Broadcast Receiver Fallback: Secondary communication channel if direct service binding is restricted.
    3. Web Payment (Chrome Custom Tabs): Automatic web checkout flow when the Myket app is not installed on the user's device.
  • Multi-Store Support: Build for multiple stores using Gradle Product Flavors without rewriting billing logic.
  • Simplified Activity Result Handling: Internal transparent proxy activities eliminate manual onActivityResult delegation in version 2.2+.
  • Android 11+ Ready: Pre-configured package visibility declarations (<queries>) are merged automatically.

Requirements​

  • Minimum SDK: Android 5.0 (API Level 21)
  • Compile SDK: API Level 34+
  • Java / Kotlin: Java 11+ or Java 17 toolchain

Installation & Configuration​

1. Configure Maven Repository​

Add the official Myket Maven repository to dependencyResolutionManagement in your settings.gradle.kts (or settings.gradle):

dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
maven("https://maven.myket.ir")
}
}

2. Add Dependency​

You can declare the dependency using Version Catalog (recommended) or directly in your app-level build.gradle.

In gradle/libs.versions.toml:

[versions]
myketBilling = "2.2"

[libraries]
myket-billing = { module = "ir.myket:billing", version.ref = "myketBilling" }

In your app-level build.gradle.kts:

dependencies {
implementation(libs.myket.billing)
}

Option B: Direct Gradle Dependency​

dependencies {
implementation("ir.myket:billing:2.2")
}

3. Store Configuration (Manifest Placeholders)​

The SDK utilizes Android manifest placeholders to dynamically bind to the market service and request appropriate billing permissions.

Single Store (Myket Default)​

Add the following placeholders to your app's defaultConfig in build.gradle.kts (or build.gradle):

android {
defaultConfig {
// ...
val marketApplicationId = "ir.mservices.market"
val marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
manifestPlaceholders["marketApplicationId"] = marketApplicationId
manifestPlaceholders["marketBindAddress"] = marketBindAddress
manifestPlaceholders["marketPermission"] = "$marketApplicationId.BILLING"

// Your Base64 RSA Public Key obtained from the Myket Developer Portal
buildConfigField(
"String",
"IAB_PUBLIC_KEY",
"\"YOUR_BASE64_ENCODED_PUBLIC_KEY\""
)
}
}

Replace YOUR_BASE64_ENCODED_PUBLIC_KEY with your application's public key obtained from the Myket Developer Portal.

4. Multi-Store Configuration (Product Flavors)​

If you distribute your application across multiple Android markets, configure Gradle productFlavors:

android {
flavorDimensions += "store"

productFlavors {
create("myket") {
dimension = "store"
val marketApplicationId = "ir.mservices.market"
val marketBindAddress = "ir.mservices.market.InAppBillingService.BIND"
manifestPlaceholders["marketApplicationId"] = marketApplicationId
manifestPlaceholders["marketBindAddress"] = marketBindAddress
manifestPlaceholders["marketPermission"] = "$marketApplicationId.BILLING"
buildConfigField("String", "IAB_PUBLIC_KEY", "\"YOUR_MYKET_PUBLIC_KEY\"")
}

create("bazaar") {
dimension = "store"
val marketApplicationId = "com.farsitel.bazaar"
val marketBindAddress = "ir.cafebazaar.pardakht.InAppBillingService.BIND"
manifestPlaceholders["marketApplicationId"] = marketApplicationId
manifestPlaceholders["marketBindAddress"] = marketBindAddress
manifestPlaceholders["marketPermission"] = "$marketApplicationId.permission.PAY_THROUGH_BAZAAR"
buildConfigField("String", "IAB_PUBLIC_KEY", "\"YOUR_BAZAAR_PUBLIC_KEY\"")
}
}
}

Step-by-Step Integration Guide​

1. Initialize IabHelper & Connect (startSetup)​

Instantiate IabHelper with your application context and Base64-encoded store public key, then call startSetup to connect to the billing service:

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import ir.myket.billingclient.IabHelper
import ir.myket.billingclient.util.IabResult
import ir.myket.billingclient.util.Inventory
import ir.myket.billingclient.util.Purchase

class MainActivity : AppCompatActivity() {

private var iabHelper: IabHelper? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

// Initialize helper
iabHelper = IabHelper(this, BuildConfig.IAB_PUBLIC_KEY).apply {
enableDebugLogging(BuildConfig.DEBUG, "MyketBilling")
}

// Connect to billing service
iabHelper?.startSetup { result ->
if (!result.isSuccess) {
// Setup failed (e.g. billing service unavailable)
Log.e("Billing", "Setup failed: ${result.message}")
return@startSetup
}

// Billing is ready. Query inventory & owned purchases.
queryInventory()
}
}
}
warning

Do not invoke any purchase or inventory operations until startSetup succeeds (result.isSuccess is true).

2. Query Inventory and Owned Items (queryInventoryAsync)​

Immediately after connecting and on app startup, query the user's active purchases and SKU details. This is essential for:

  1. Unconsumed Consumables Recovery: If the app terminated or crashed before a purchase was consumed and credited, the purchase remains in the user's active inventory. You must detect, consume, and fulfill it.
  2. Non-Consumable Entitlements: Verify ownership for non-consumable items (such as premium unlocks) and grant access.
  3. Product Metadata: Retrieve up-to-date titles, descriptions, and localized prices configured in the Myket Developer Portal.
private fun queryInventory() {
val skuList = listOf("premium_upgrade", "coin_pack_100")

iabHelper?.queryInventoryAsync(true, skuList) { result, inventory ->
if (result.isFailure || inventory == null) {
Log.e("Billing", "Failed to query inventory: ${result.message}")
return@queryInventoryAsync
}

// 1. Check non-consumable product
val premiumPurchase = inventory.getPurchase("premium_upgrade")
val isPremium = premiumPurchase != null && verifyDeveloperPayload(premiumPurchase)

// 2. Check and consume unconsumed consumable items (e.g. after crash before consume)
val coinPurchase = inventory.getPurchase("coin_pack_100")
if (coinPurchase != null && verifyDeveloperPayload(coinPurchase)) {
consumeProduct(coinPurchase)
}

// Update UI accordingly
updateUi(isPremium)
}
}

Purchase Class Fields​

Method / PropertyTypeDescription
getItemType()StringItem type (inapp or subs)
getOrderId()StringUnique Myket transaction/order identifier
getPackageName()StringPackage name of the purchasing application
getSku()StringProduct SKU identifier
getPurchaseTime()longTimestamp of transaction completion (epoch milliseconds)
getPurchaseState()intPurchase state (0 = purchased/success)
getDeveloperPayload()StringCustom metadata string attached at the start of purchase
getToken()StringUnique purchase token used for verification and consumption
getOriginalJson()StringRaw JSON string payload received from billing service
getSignature()StringDigital RSA signature for verifying payload authenticity

3. Launching a Purchase Flow (launchPurchaseFlow)​

Initiate the checkout UI for an in-app product.

No onActivityResult Delegation Required in SDK 2.2

In Myket Billing Client 2.2, you do not need to override or delegate onActivityResult in your Activity. The SDK launches transparent internal proxy activities and routes purchase results directly to your callback.

private fun buyItem(sku: String) {
val developerPayload = generateSecurePayload()

iabHelper?.launchPurchaseFlow(
this,
sku,
{ result, purchase ->
if (result.isFailure || purchase == null) {
Log.e("Billing", "Purchase failed: ${result.message}")
return@launchPurchaseFlow
}

if (!verifyDeveloperPayload(purchase)) {
Log.e("Billing", "Authenticity verification failed.")
return@launchPurchaseFlow
}

// Handle successful purchase
when (purchase.sku) {
"coin_pack_100" -> consumeProduct(purchase)
"premium_upgrade" -> unlockPremiumFeature(purchase)
}
},
developerPayload
)
}

4. Consuming Purchased Items (consumeAsync)​

Consumable items (e.g., coins, credits, fuel) must be consumed before they can be purchased again by the same user:

private fun consumeProduct(purchase: Purchase) {
iabHelper?.consumeAsync(purchase) { consumedPurchase, result ->
if (result.isSuccess) {
// Provision the goods to the user
creditUserCoins(100)
Log.d("Billing", "Product successfully consumed: ${consumedPurchase.sku}")
} else {
Log.e("Billing", "Consumption failed: ${result.message}")
}
}
}
danger

Never consume non-consumable products. Consuming an item removes it from the user's active inventory on Myket servers, causing them to permanently lose ownership.

5. Releasing Resources (onDestroy)​

Always release the service connection when your Activity or ViewModel is destroyed to prevent service leaks:

override fun onDestroy() {
super.onDestroy()
iabHelper?.dispose()
iabHelper = null
}

Complete Java Code Sample (MainActivity.java)​

Here is a complete, self-contained MainActivity.java example demonstrating setup, querying, consuming, and cleanup:

package com.example.myketbilling;

import android.os.Bundle;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import ir.myket.billingclient.IabHelper;
import ir.myket.billingclient.util.Purchase;
import java.util.Arrays;

public class MainActivity extends AppCompatActivity {
private static final String TAG = "MyketBilling";
private IabHelper mHelper;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

mHelper = new IabHelper(this, BuildConfig.IAB_PUBLIC_KEY);
mHelper.enableDebugLogging(BuildConfig.DEBUG, TAG);

mHelper.startSetup(result -> {
if (!result.isSuccess()) {
Log.e(TAG, "Setup error: " + result.getMessage());
return;
}

mHelper.queryInventoryAsync(true, Arrays.asList("gas", "premium"), (invResult, inv) -> {
if (invResult.isSuccess() && inv != null) {
Purchase gas = inv.getPurchase("gas");
if (gas != null) {
mHelper.consumeAsync(gas, (purchase, res) -> {
if (res.isSuccess()) {
Log.d(TAG, "Gas consumed and credited.");
}
});
}

Purchase premium = inv.getPurchase("premium");
if (premium != null) {
Log.d(TAG, "User owns premium access.");
}
}
});
});
}

@Override
protected void onDestroy() {
super.onDestroy();
if (mHelper != null) {
mHelper.dispose();
mHelper = null;
}
}
}

Security Best Practices​

  1. Protect Your Public Key:
    • Do not commit your plain Base64 public key into public version-control repositories.
    • Assemble the key at runtime or store it securely in native libraries (C++/NDK) or obfuscated configuration fields.
  2. Authoritative Server-Side Validation:
    • For high-security applications and anti-fraud protection, send Purchase.getOriginalJson() and Purchase.getSignature() to your backend server.
    • Verify the signature using your Myket public key or via the Myket Developer API.
  3. Prevent Duplicate Delivery with Purchase Tokens:
    • Use the purchase token (purchase.getToken()) as a unique transaction ID. Record every processed token in your database to prevent duplicate fulfillment upon re-sent requests.
  4. Delivery Sequence for Consumables:
    • Deliver consumable goods only after consumeAsync completes successfully, or retain a robust local state machine to consume on subsequent app launches without duplicate crediting.
  5. Developer Payload:
    • Generate a cryptographically unique developer payload (e.g., account hash + server nonce) when initiating purchases.
    • Verify that the payload returned in Purchase.getDeveloperPayload() matches the originating account and intent.
tip

The developerPayload string must not exceed 800 characters.

ProGuard / R8 Configuration​

If code shrinking and obfuscation (R8/ProGuard) are enabled, add the following rules to your proguard-rules.pro:

# Myket Billing Client
-keep class ir.myket.billingclient.** { *; }
-keep interface ir.myket.billingclient.** { *; }
-keep class com.android.vending.billing.** { *; }

# Models & Reflection
-keepclassmembers class ir.myket.billingclient.util.** {
public <fields>;
public <methods>;
}

Sample Project & Testing​

A fully functional sample application demonstrating consumable products (gas) and non-consumable upgrades (premium) is available in the :sample module (TrivialDrive).

Subscription support

The subscription flow in the sample does not mean Myket currently supports subscriptions. Myket does not currently support subscription products; do not use the subscription example for a real product.

Testing Checklist Before Release​

You do not need to publish your app publicly or have it approved to begin testing:

  1. Register your app package name in the Myket Developer Portal.
  2. Add your public key to your app's build configuration.
  3. Define one or more low-priced test products in the portal.
  4. Install the latest Myket application on your physical device or emulator.
  5. Verify key scenarios:
    • Successful purchase of consumable and non-consumable items
    • User cancellation of purchase dialog
    • App termination/crash right after payment and recovery via queryInventoryAsync on restart
    • Consumable item re-purchase limitation before consumption
    • Web payment fallback flow when Myket app is absent